import { Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Repository } from 'typeorm';

import { GroupMember } from '@/entities/group-member.entity';

@WebSocketGateway({ cors: true })
export class MapGateway implements OnGatewayConnection, OnGatewayDisconnect {
  private readonly logger = new Logger(MapGateway.name);

  @WebSocketServer()
  server: Server;

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

  async handleConnection(client: Socket) {
    try {
      const token = client.handshake.auth?.token;
      const payload = this.jwtService.verify(token, {
        secret: process.env.JWT_SECRET,
      });
      const userId = payload.sub;

      if (!userId) {
        client.disconnect();
        return;
      }

      client.data.userId = userId;
      await client.join(`user_${userId}`);

      const groups = await this.groupMemberRepo.find({
        where: { user: { id: userId } },
        relations: ['group'],
      });
      await Promise.all(
        groups
          .filter((membership) => membership.group?.id)
          .map((membership) => client.join(`group_${membership.group.id}`)),
      );

      this.logger.log(`Client connected: ${client.id}, userId: ${userId}`);
    } catch (error) {
      this.logger.error('JWT error', error);
      client.disconnect();
    }
  }

  async handleDisconnect(client: Socket) {
    const { userId, tripId } = client.data;
    this.logger.log(`Client disconnected: ${client.id}, userId: ${userId}`);
    if (userId && tripId) {
      await this.emitOfflineIfLastSocket(tripId, userId);
    }
  }

  @SubscribeMessage('join-trip')
  async handleJoinTrip(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { tripId: string; userName?: string },
  ) {
    const { tripId, userName } = data;
    const userId = client.data.userId;
    if (!tripId || !userId) return;

    const room = `trip_${tripId}`;
    const existingSockets = await this.server.in(room).fetchSockets();
    const existingUsers = new Map<string, (typeof existingSockets)[number]>();
    existingSockets.forEach((socket) => {
      if (socket.data.userId && !existingUsers.has(socket.data.userId)) {
        existingUsers.set(socket.data.userId, socket);
      }
    });

    existingUsers.forEach((socket) => {
      client.emit('user-online', {
        userId: socket.data.userId,
        userName: socket.data.userName || socket.data.userId,
        lat: socket.data.lat ?? 0,
        lng: socket.data.lng ?? 0,
        hasLocation:
          typeof socket.data.lat === 'number' &&
          typeof socket.data.lng === 'number',
      });
    });

    await client.join(room);
    client.data.tripId = tripId;
    client.data.userName = userName || userId;

    client.broadcast.to(room).emit('user-online', {
      userId,
      userName: client.data.userName,
      lat: client.data.lat ?? 0,
      lng: client.data.lng ?? 0,
      hasLocation:
        typeof client.data.lat === 'number' &&
        typeof client.data.lng === 'number',
    });
  }

  @SubscribeMessage('update-location')
  handleUpdateLocation(
    @ConnectedSocket() client: Socket,
    @MessageBody()
    data: { tripId: string; lat: number; lng: number },
  ) {
    const { tripId, lat, lng } = data;
    const userId = client.data.userId;
    if (!userId || tripId !== client.data.tripId) return;

    client.data.lat = lat;
    client.data.lng = lng;
    client.broadcast.to(`trip_${tripId}`).emit('location-update', {
      userId,
      userName: client.data.userName,
      lat,
      lng,
    });
  }

  @SubscribeMessage('leave-trip')
  async handleLeaveTrip(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { tripId: string },
  ) {
    const { tripId } = data;
    const userId = client.data.userId;
    if (!tripId || !userId) return;

    await client.leave(`trip_${tripId}`);
    client.data.tripId = undefined;
    await this.emitOfflineIfLastSocket(tripId, userId);
  }

  private async emitOfflineIfLastSocket(tripId: string, userId: string) {
    const room = `trip_${tripId}`;
    const sockets = await this.server.in(room).fetchSockets();
    if (!sockets.some((socket) => socket.data.userId === userId)) {
      this.server.to(room).emit('user-offline', userId);
    }
  }
}
