import { JwtService } from '@nestjs/jwt';
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { InjectRepository } from '@nestjs/typeorm';
import { ConversationMember } from '@/entities/conversation-member.entity';
import { Repository } from 'typeorm';

type CallMedia = 'audio' | 'video';

type CallSignal = {
  groupId: string;
  targetSocketId: string;
  signal: unknown;
};

@WebSocketGateway({ cors: true })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  constructor(
    private readonly jwtService: JwtService,
    @InjectRepository(ConversationMember)
    private readonly memberRepo: Repository<ConversationMember>,
  ) {}

  async handleConnection(client: Socket) {
    try {
      const token = client.handshake.auth?.token;
      const payload = this.jwtService.verify(token, {
        secret: process.env.JWT_SECRET,
      });
      client.data.userId = payload.sub;
      client.data.userName = payload.name || payload.email || 'Thành viên';
      await client.join(`user_${payload.sub}`);
      const memberships = await this.memberRepo.find({
        where: { user: { id: payload.sub } },
        relations: ['conversation'],
      });
      await Promise.all(
        memberships.map((membership) =>
          client.join(`conversation_${membership.conversation.id}`),
        ),
      );
    } catch {
      client.disconnect();
    }
  }

  async handleDisconnect(client: Socket) {
    await this.leaveCall(client);
  }

  private async membership(client: Socket, groupId: string) {
    if (!client.data.userId || !groupId) return null;
    return this.memberRepo.findOne({
      where: {
        conversation: { group: { id: groupId } },
        user: { id: client.data.userId },
      },
      relations: ['conversation', 'user'],
    });
  }

  @SubscribeMessage('call:join')
  async joinCall(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { groupId: string; media?: CallMedia },
  ) {
    const membership = await this.membership(client, data?.groupId);
    if (!membership) {
      client.emit('call:error', {
        message: 'Bạn không thuộc cuộc trò chuyện này',
      });
      return { ok: false, message: 'Bạn không thuộc cuộc trò chuyện này' };
    }
    client.data.userName =
      membership.user?.name || client.data.userName || 'Thành viên';

    if (client.data.callGroupId && client.data.callGroupId !== data.groupId) {
      await this.leaveCall(client);
    }

    const room = `call_${data.groupId}`;
    const sockets = await this.server.in(room).fetchSockets();
    const participants = sockets.map((socket) => ({
      socketId: socket.id,
      userId: socket.data.userId,
      name: socket.data.userName,
      media: socket.data.callMedia as CallMedia,
    }));

    client.data.callGroupId = data.groupId;
    client.data.callConversationId = membership.conversation.id;
    client.data.callMedia = data.media === 'audio' ? 'audio' : 'video';
    await client.join(room);
    client.emit('call:participants', { groupId: data.groupId, participants });
    client.to(room).emit('call:user-joined', {
      groupId: data.groupId,
      socketId: client.id,
      userId: client.data.userId,
      name: client.data.userName,
      media: client.data.callMedia,
    });

    if (participants.length === 0) {
      this.server
        .to(`conversation_${membership.conversation.id}`)
        .emit('call:started', {
          groupId: data.groupId,
          userId: client.data.userId,
          name: client.data.userName,
          media: client.data.callMedia,
        });
    }
    return { ok: true, participantCount: participants.length + 1 };
  }

  @SubscribeMessage('call:signal')
  async relayCallSignal(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: CallSignal,
  ) {
    if (
      !data?.targetSocketId ||
      client.data.callGroupId !== data.groupId ||
      !(await this.membership(client, data.groupId))
    ) {
      return;
    }
    const target = (
      await this.server.in(`call_${data.groupId}`).fetchSockets()
    ).find((socket) => socket.id === data.targetSocketId);
    if (!target) return;
    this.server.to(data.targetSocketId).emit('call:signal', {
      groupId: data.groupId,
      fromSocketId: client.id,
      userId: client.data.userId,
      name: client.data.userName,
      signal: data.signal,
    });
  }

  @SubscribeMessage('call:leave')
  async onLeaveCall(@ConnectedSocket() client: Socket) {
    await this.leaveCall(client);
  }

  private async leaveCall(client: Socket) {
    const groupId = client.data.callGroupId as string | undefined;
    if (!groupId) return;
    const conversationId = client.data.callConversationId as string | undefined;
    const room = `call_${groupId}`;
    client.to(room).emit('call:user-left', {
      groupId,
      socketId: client.id,
      userId: client.data.userId,
    });
    await client.leave(room);
    const remaining = await this.server.in(room).fetchSockets();
    if (remaining.length === 0 && conversationId) {
      this.server.to(`conversation_${conversationId}`).emit('call:ended', {
        groupId,
      });
    }
    delete client.data.callGroupId;
    delete client.data.callConversationId;
    delete client.data.callMedia;
  }

  sendToConversation(conversationId: string, event: string, data: unknown) {
    this.server.to(`conversation_${conversationId}`).emit(event, data);
  }

  sendToUser(userId: string, event: string, data: unknown) {
    this.server.to(`user_${userId}`).emit(event, data);
  }

  joinUser(userId: string, conversationId: string) {
    this.server
      .in(`user_${userId}`)
      .socketsJoin(`conversation_${conversationId}`);
  }

  removeUser(userId: string, conversationId: string) {
    this.server
      .in(`user_${userId}`)
      .socketsLeave(`conversation_${conversationId}`);
  }
}
