import { Inject } from '@nestjs/common';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
  type OnGatewayConnection,
  type OnGatewayDisconnect,
  type OnGatewayInit,
} from '@nestjs/websockets';
import type { Server, Socket } from 'socket.io';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports
import { TokenService, type JwtPayload } from '@modules/auth';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports
import { LoggerService } from '@modules/shared';
import { MarkConversationReadCommand } from '../../application/commands/mark-read/mark-read.command';
import { SendMessageCommand } from '../../application/commands/send-message/send-message.command';
import type { MessageSentEvent } from '../../domain/events/message-sent.event';
import type { ConversationReadEvent } from '../../domain/events/conversation-read.event';
import {
  CONVERSATION_REPOSITORY,
  type IConversationRepository,
} from '../../domain/repositories/conversation.repository';

@WebSocketGateway({
  namespace: '/messaging',
  cors: {
    origin: (process.env['CORS_ORIGINS'] ?? 'http://localhost:3000')
      .split(',')
      .map((o) => o.trim()),
    credentials: true,
  },
})
export class MessagingGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  server!: Server;

  private readonly userSockets = new Map<string, Set<string>>();

  constructor(
    private readonly tokenService: TokenService,
    private readonly logger: LoggerService,
    private readonly commandBus: CommandBus,
    @Inject(CONVERSATION_REPOSITORY)
    private readonly conversationRepo: IConversationRepository,
  ) {}

  afterInit(): void {
    this.logger.log('MessagingGateway initialized', 'MessagingGateway');
  }

  async handleConnection(client: Socket): Promise<void> {
    try {
      const payload = this.extractAndVerifyToken(client);
      if (!payload) {
        client.disconnect(true);
        return;
      }

      client.data['userId'] = payload.sub;

      // Join user's personal room for direct messaging events
      await client.join(`user:${payload.sub}`);

      // Join rooms for all active conversations
      const conversations = await this.conversationRepo.findByUserId(payload.sub, 100);
      for (const conv of conversations) {
        await client.join(`conversation:${conv.id}`);
      }

      if (!this.userSockets.has(payload.sub)) {
        this.userSockets.set(payload.sub, new Set());
      }
      this.userSockets.get(payload.sub)!.add(client.id);

      this.logger.debug(
        `WS messaging connected: user=${payload.sub} socket=${client.id} conversations=${conversations.length}`,
        'MessagingGateway',
      );
    } catch (error) {
      this.logger.error(
        `WS messaging connection error: ${error instanceof Error ? error.message : error}`,
        error instanceof Error ? error.stack : undefined,
        'MessagingGateway',
      );
      client.disconnect(true);
    }
  }

  handleDisconnect(client: Socket): void {
    const userId = client.data['userId'] as string | undefined;
    if (userId) {
      const sockets = this.userSockets.get(userId);
      if (sockets) {
        sockets.delete(client.id);
        if (sockets.size === 0) {
          this.userSockets.delete(userId);
        }
      }
    }
    this.logger.debug(
      `WS messaging disconnected: user=${userId ?? 'unknown'} socket=${client.id}`,
      'MessagingGateway',
    );
  }

  /* ────────────────────────────────────────────
   * Client → Server message handlers
   * ──────────────────────────────────────────── */

  @SubscribeMessage('message:send')
  async handleSendMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { conversationId: string; content: string; type?: string },
  ): Promise<void> {
    const userId = client.data['userId'] as string;
    if (!userId) return;

    try {
      const message = await this.commandBus.execute(
        new SendMessageCommand(
          data.conversationId,
          userId,
          data.content,
          (data.type as 'TEXT' | 'IMAGE' | 'FILE') ?? 'TEXT',
        ),
      );
      client.emit('message:sent', { messageId: message.id, conversationId: data.conversationId });
    } catch (error) {
      client.emit('message:error', {
        conversationId: data.conversationId,
        error: error instanceof Error ? error.message : 'Không thể gửi tin nhắn',
      });
    }
  }

  @SubscribeMessage('message:typing')
  async handleTyping(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { conversationId: string },
  ): Promise<void> {
    const userId = client.data['userId'] as string;
    if (!userId) return;

    client.to(`conversation:${data.conversationId}`).emit('message:typing', {
      conversationId: data.conversationId,
      userId,
    });
  }

  @SubscribeMessage('message:stop-typing')
  async handleStopTyping(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { conversationId: string },
  ): Promise<void> {
    const userId = client.data['userId'] as string;
    if (!userId) return;

    client.to(`conversation:${data.conversationId}`).emit('message:stop-typing', {
      conversationId: data.conversationId,
      userId,
    });
  }

  @SubscribeMessage('conversation:read')
  async handleMarkRead(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { conversationId: string },
  ): Promise<void> {
    const userId = client.data['userId'] as string;
    if (!userId) return;

    try {
      await this.commandBus.execute(
        new MarkConversationReadCommand(data.conversationId, userId),
      );
      client.emit('conversation:read-ack', { conversationId: data.conversationId });
    } catch {
      // Non-critical — silently fail
    }
  }

  @SubscribeMessage('conversation:join')
  async handleJoinConversation(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { conversationId: string },
  ): Promise<void> {
    const userId = client.data['userId'] as string;
    if (!userId) return;

    // Verify the user is a participant before joining the room
    const conversation = await this.conversationRepo.findById(data.conversationId);
    if (conversation && conversation.participants.some((p) => p.userId === userId)) {
      await client.join(`conversation:${data.conversationId}`);
    }
  }

  /* ────────────────────────────────────────────
   * Domain event handlers
   * ──────────────────────────────────────────── */

  @OnEvent('message.sent', { async: true })
  async handleMessageSent(event: MessageSentEvent): Promise<void> {
    try {
      this.server.to(`conversation:${event.conversationId}`).emit('message:new', {
        id: event.aggregateId,
        conversationId: event.conversationId,
        senderId: event.senderId,
        type: event.type,
        content: event.content,
        createdAt: event.occurredAt.toISOString(),
      });
    } catch (error) {
      this.logger.error(
        `Failed to emit WS message for conversation ${event.conversationId}: ${
          error instanceof Error ? error.message : error
        }`,
        error instanceof Error ? error.stack : undefined,
        'MessagingGateway',
      );
    }
  }

  @OnEvent('conversation.read', { async: true })
  async handleConversationRead(event: ConversationReadEvent): Promise<void> {
    try {
      this.server.to(`conversation:${event.conversationId}`).emit('message:read', {
        conversationId: event.conversationId,
        userId: event.userId,
        readAt: event.occurredAt.toISOString(),
      });
    } catch (error) {
      this.logger.error(
        `Failed to emit WS read receipt for conversation ${event.conversationId}: ${
          error instanceof Error ? error.message : error
        }`,
        error instanceof Error ? error.stack : undefined,
        'MessagingGateway',
      );
    }
  }

  /* ────────────────────────────────────────────
   * Private helpers
   * ──────────────────────────────────────────── */

  private extractAndVerifyToken(client: Socket): JwtPayload | null {
    const raw: unknown =
      client.handshake.auth?.['token'] ??
      client.handshake.headers?.['authorization'] ??
      client.handshake.query?.['token'];

    if (!raw || typeof raw !== 'string') {
      this.logger.warn(
        `WS messaging auth failed: no token provided (socket=${client.id})`,
        'MessagingGateway',
      );
      return null;
    }

    const token = raw.startsWith('Bearer ') ? raw.slice(7) : raw;
    const payload = this.tokenService.verifyAccessToken(token);
    if (!payload) {
      this.logger.warn(
        `WS messaging auth failed: invalid token (socket=${client.id})`,
        'MessagingGateway',
      );
    }
    return payload;
  }
}
