/**
 * Notifications WebSocket Gateway
 *
 * Handles real-time notification delivery via WebSocket
 */

import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  ConnectedSocket,
  MessageBody,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger, UseGuards } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { NotificationService } from '../services/notification.service';
import { Notification } from '@domain/entities/notification.entity';

/**
 * WebSocket authentication guard
 */
@UseGuards()
@WebSocketGateway({
  cors: {
    origin: process.env.FRONTEND_URL || ['http://localhost:3000', 'http://localhost:3001'],
    credentials: true,
  },
  namespace: '/notifications',
})
export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  private readonly logger = new Logger(NotificationsGateway.name);
  private userSockets: Map<string, Set<string>> = new Map(); // userId -> Set of socket IDs

  constructor(
    private readonly jwtService: JwtService,
    private readonly notificationService: NotificationService
  ) {}

  /**
   * Handle client connection
   */
  async handleConnection(client: Socket) {
    try {
      // Extract JWT token from handshake
      const token = this.extractToken(client);
      if (!token) {
        this.logger.warn(`Client ${client.id} connection rejected: No token provided`);
        client.disconnect();
        return;
      }

      // Verify JWT token
      const payload = await this.jwtService.verifyAsync(token);
      const userId = payload.sub;

      // Store socket connection for user
      if (!this.userSockets.has(userId)) {
        this.userSockets.set(userId, new Set());
      }
      this.userSockets.get(userId)!.add(client.id);

      // Store user ID in socket data for later use
      client.data.userId = userId;
      client.data.organizationId = payload.organizationId;

      // Join user-specific room
      client.join(`user:${userId}`);

      this.logger.log(`Client ${client.id} connected for user ${userId}`);

      // Send unread count on connection
      const unreadCount = await this.notificationService.getUnreadCount(userId);
      client.emit('unread_count', { count: unreadCount });

      // Send recent notifications on connection
      const recentNotifications = await this.notificationService.getRecentNotifications(userId, 10);
      client.emit('recent_notifications', {
        notifications: recentNotifications.map(n => this.mapNotificationToDto(n)),
      });
    } catch (error: any) {
      this.logger.error(
        `Error during client connection: ${error?.message || 'Unknown error'}`,
        error?.stack
      );
      client.disconnect();
    }
  }

  /**
   * Handle client disconnection
   */
  handleDisconnect(client: Socket) {
    const userId = client.data.userId;
    if (userId && this.userSockets.has(userId)) {
      this.userSockets.get(userId)!.delete(client.id);
      if (this.userSockets.get(userId)!.size === 0) {
        this.userSockets.delete(userId);
      }
    }
    this.logger.log(`Client ${client.id} disconnected`);
  }

  /**
   * Handle mark notification as read
   */
  @SubscribeMessage('mark_as_read')
  async handleMarkAsRead(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { notificationId: string }
  ) {
    try {
      const userId = client.data.userId;
      await this.notificationService.markAsRead(data.notificationId);

      // Send updated unread count
      const unreadCount = await this.notificationService.getUnreadCount(userId);
      this.emitToUser(userId, 'unread_count', { count: unreadCount });

      return { success: true };
    } catch (error: any) {
      this.logger.error(`Error marking notification as read: ${error?.message}`);
      return { success: false, error: error?.message };
    }
  }

  /**
   * Handle mark all notifications as read
   */
  @SubscribeMessage('mark_all_as_read')
  async handleMarkAllAsRead(@ConnectedSocket() client: Socket) {
    try {
      const userId = client.data.userId;
      await this.notificationService.markAllAsRead(userId);

      // Send updated unread count (should be 0)
      this.emitToUser(userId, 'unread_count', { count: 0 });

      return { success: true };
    } catch (error: any) {
      this.logger.error(`Error marking all notifications as read: ${error?.message}`);
      return { success: false, error: error?.message };
    }
  }

  /**
   * Handle get unread count
   */
  @SubscribeMessage('get_unread_count')
  async handleGetUnreadCount(@ConnectedSocket() client: Socket) {
    try {
      const userId = client.data.userId;
      const unreadCount = await this.notificationService.getUnreadCount(userId);
      return { count: unreadCount };
    } catch (error: any) {
      this.logger.error(`Error getting unread count: ${error?.message}`);
      return { count: 0 };
    }
  }

  /**
   * Send notification to a specific user
   */
  async sendNotificationToUser(userId: string, notification: Notification) {
    const notificationDto = this.mapNotificationToDto(notification);

    // Emit to all connected sockets for this user
    this.emitToUser(userId, 'new_notification', { notification: notificationDto });

    // Update unread count
    const unreadCount = await this.notificationService.getUnreadCount(userId);
    this.emitToUser(userId, 'unread_count', { count: unreadCount });

    this.logger.log(`Notification sent to user ${userId}: ${notification.title}`);
  }

  /**
   * Broadcast notification to organization
   */
  async broadcastToOrganization(organizationId: string, notification: Notification) {
    const notificationDto = this.mapNotificationToDto(notification);
    this.server.to(`org:${organizationId}`).emit('new_notification', {
      notification: notificationDto,
    });

    this.logger.log(`Notification broadcasted to organization ${organizationId}`);
  }

  /**
   * Helper: Emit event to all sockets of a user
   */
  private emitToUser(userId: string, event: string, data: any) {
    this.server.to(`user:${userId}`).emit(event, data);
  }

  /**
   * Helper: Extract JWT token from socket handshake
   */
  private extractToken(client: Socket): string | null {
    // Check Authorization header
    const authHeader = client.handshake.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
      return authHeader.substring(7);
    }

    // Check query parameter
    const token = client.handshake.query.token;
    if (typeof token === 'string') {
      return token;
    }

    // Check auth object (socket.io-client way)
    const auth = client.handshake.auth;
    if (auth && typeof auth.token === 'string') {
      return auth.token;
    }

    return null;
  }

  /**
   * Helper: Map notification entity to DTO
   */
  private mapNotificationToDto(notification: Notification) {
    return {
      id: notification.id,
      type: notification.type,
      priority: notification.priority,
      title: notification.title,
      message: notification.message,
      metadata: notification.metadata,
      read: notification.read,
      readAt: notification.readAt?.toISOString(),
      actionUrl: notification.actionUrl,
      createdAt: notification.createdAt.toISOString(),
    };
  }
}
