// src/chat/chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';
import { ChatService } from './chat.service';
import {
  SendMessageDto,
  TypingDto,
  MarkReadDto,
  JoinRoomDto,
} from './dto/chat.dto';
import { PushService } from './push.service';
// Map of userId → socketId for online tracking
const onlineUsers = new Map<number, string>();

@WebSocketGateway({
  cors: {
    origin: process.env.APP_URL || 'http://localhost:3011',
    credentials: true,
  },
  namespace: '/chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  // ✅ FIX: use definite assignment assertion (!) to satisfy TypeScript
  @WebSocketServer()
  server!: Server;

  private readonly logger = new Logger(ChatGateway.name);

  // In the constructor — add PushService
  constructor(
    private readonly chatService: ChatService,
    private readonly pushService: PushService, // ← ADD
  ) {}

  // ── Extract userId from JWT token in handshake ────────────────────────────
  private getUserId(client: Socket): number | null {
    try {
      const token =
        (client.handshake.auth as Record<string, string>)?.token ||
        client.handshake.headers?.authorization?.replace('Bearer ', '');
      if (!token) return null;
      const payload = JSON.parse(
        Buffer.from(token.split('.')[1], 'base64').toString(),
      ) as Record<string, unknown>;
      const id = payload?.sub ?? payload?.id ?? payload?.userId ?? null;
      return id ? Number(id) : null;
    } catch {
      return null;
    }
  }

  // ── User connects ─────────────────────────────────────────────────────────
  async handleConnection(client: Socket) {
    const userId = this.getUserId(client);
    if (!userId) {
      this.logger.warn(`Chat: unauthenticated connection rejected`);
      client.disconnect();
      return;
    }

    client.data.userId = userId;
    onlineUsers.set(userId, client.id);
    this.logger.log(`Chat: User ${userId} connected (${client.id})`);

    // Auto-join all rooms the user belongs to
    const rooms = (await this.chatService.getRoomsForUser(userId)) as {
      id: number;
    }[];
    for (const room of rooms) {
      void client.join(`room_${room.id}`);
    }

    // Broadcast online status
    this.server.emit('user_online', { userId });
  }

  // ── User disconnects ──────────────────────────────────────────────────────
  handleDisconnect(client: Socket) {
    const userId = client.data.userId as number | undefined;
    if (userId) {
      onlineUsers.delete(userId);
      this.logger.log(`Chat: User ${userId} disconnected`);
      this.server.emit('user_offline', {
        userId,
        last_seen: new Date().toISOString(),
      });
    }
  }

  // ── Join a specific room ──────────────────────────────────────────────────
  @SubscribeMessage('join_room')
  async handleJoinRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() dto: JoinRoomDto,
  ) {
    await client.join(`room_${dto.roomId}`);
    this.logger.log(
      `User ${client.data.userId as number} joined room ${dto.roomId}`,
    );
    return { event: 'joined', roomId: dto.roomId };
  }

  // ── Send message ──────────────────────────────────────────────────────────
  // Replace handleMessage with this version
  @SubscribeMessage('send_message')
  async handleMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() dto: SendMessageDto,
  ) {
    const senderId = client.data.userId as number | undefined;
    if (!senderId) return { success: false, error: 'Not authenticated' };

    try {
      const saved = await this.chatService.saveMessage(dto, senderId);

      const senderName = saved.sender
        ? `${saved.sender.firstName} ${saved.sender.lastName}`.trim()
        : '';

      const payload = {
        id: saved.id,
        room_id: saved.room_id,
        sender_id: saved.sender_id,
        sender_name: senderName,
        message: saved.message,
        type: saved.type,
        file_url: saved.file_url,
        file_name: saved.file_name,
        reply_to_id: saved.reply_to_id,
        created_at: saved.created_at,
      };

      // Broadcast to everyone in room (including sender — confirms delivery)
      this.server.to(`room_${dto.roomId}`).emit('new_message', payload);

      this.logger.log(`Message from User ${senderId} in room ${dto.roomId}`);

      // ╔═══════════════════════════════════════════════════════════╗
      // ║  PUSH NOTIFICATIONS — fire-and-forget, don't block reply ║
      // ╚═══════════════════════════════════════════════════════════╝
      void this.sendPushToOfflineMembers(
        dto.roomId,
        senderId,
        senderName,
        saved.message,
      );

      return { success: true, messageId: saved.id };
    } catch (err: unknown) {
      const message = err instanceof Error ? err.message : 'Unknown error';
      this.logger.error(`send_message error: ${message}`);
      return { success: false, error: message };
    }
  }

  // ── Helper: send push to OTHER members of the room ──────────────────────────
  private async sendPushToOfflineMembers(
    roomId: number,
    senderId: number,
    senderName: string,
    message: string,
  ): Promise<void> {
    try {
      const members = (await this.chatService.getRoomMembers(roomId)) as {
        id: number;
      }[];

      for (const member of members) {
        if (member.id === senderId) continue; // skip sender
        // Send to ALL other members — push works whether they're online or not.
        await this.pushService.sendToUser(member.id, {
          senderName: senderName || 'Someone',
          message,
          roomId,
        });
      }
    } catch (err) {
      this.logger.error(`Push fan-out failed: ${(err as Error).message}`);
    }
  }

  // ── Typing started ────────────────────────────────────────────────────────
  @SubscribeMessage('typing_start')
  handleTypingStart(
    @ConnectedSocket() client: Socket,
    @MessageBody() dto: TypingDto,
  ) {
    const userId = client.data.userId as number;
    client
      .to(`room_${dto.roomId}`)
      .emit('user_typing', { userId, roomId: dto.roomId });
  }

  // ── Typing stopped ────────────────────────────────────────────────────────
  @SubscribeMessage('typing_stop')
  handleTypingStop(
    @ConnectedSocket() client: Socket,
    @MessageBody() dto: TypingDto,
  ) {
    const userId = client.data.userId as number;
    client
      .to(`room_${dto.roomId}`)
      .emit('user_stop_typing', { userId, roomId: dto.roomId });
  }

  // ── Mark room as read ─────────────────────────────────────────────────────
  @SubscribeMessage('mark_read')
  async handleMarkRead(
    @ConnectedSocket() client: Socket,
    @MessageBody() dto: MarkReadDto,
  ) {
    const userId = client.data.userId as number | undefined;
    if (!userId) return;

    await this.chatService.markAsRead(dto.roomId, userId);

    // Notify others — for blue tick display
    client.to(`room_${dto.roomId}`).emit('messages_read', {
      roomId: dto.roomId,
      userId,
      read_at: new Date().toISOString(),
    });
  }

  // ── Get online users ──────────────────────────────────────────────────────
  @SubscribeMessage('get_online_users')
  handleGetOnlineUsers() {
    return { onlineUserIds: Array.from(onlineUsers.keys()) };
  }

  // ── Helper: emit event to a specific user ─────────────────────────────────
  emitToUser(userId: number, event: string, data: unknown) {
    const socketId = onlineUsers.get(userId);
    if (socketId) {
      this.server.to(socketId).emit(event, data);
    }
  }

  // ── Helper: check if user is online ──────────────────────────────────────
  isOnline(userId: number): boolean {
    return onlineUsers.has(userId);
  }
}
