import { Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { PresenceService } from '../presence/presence.service';
import { PrivateChatService } from '../private-chat/private-chat.service';
import { LobbyService } from '../lobby/lobby.service';
import { RealtimeUser } from './interfaces/realtime-user.interface';

type AuthenticatedSocket = Socket & {
  user?: RealtimeUser;
};

@WebSocketGateway({
  namespace: '/realtime',
  cors: {
    origin: '*',
    credentials: true,
  },
})
export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  private readonly logger = new Logger(RealtimeGateway.name);
  private readonly onlineUsers = new Map<string, RealtimeUser & { socketId: string }>();
  private readonly devLobbyMessages: any[] = [];
  private readonly devPrivateMessages: any[] = [];

  constructor(
    private readonly jwtService: JwtService,
    private readonly presenceService: PresenceService,
    private readonly privateChatService: PrivateChatService,
    private readonly lobbyService: LobbyService,
  ) {}

  async handleConnection(client: AuthenticatedSocket) {
    try {
      const user = this.getUserFromSocket(client);
      client.user = user;
      client.join(`user:${user.id}`);
      client.join('lobby');

      this.onlineUsers.set(user.id, { ...user, socketId: client.id });
      await this.safeMarkOnline(user.id);

      this.server.emit('presence:online', this.publicUser(user));
      this.server.emit('presence:list', this.getOnlineUsers());

      client.emit('connection:success', {
        message: 'Connected to Hi2Chat realtime server',
        user,
        onlineUsers: this.getOnlineUsers(),
      });

      this.logger.log(`User connected: ${user.username} (${user.id})`);
    } catch (error) {
      client.emit('connection:error', {
        message: 'Unauthorized websocket connection',
      });
      client.disconnect(true);
    }
  }

  async handleDisconnect(client: AuthenticatedSocket) {
    const user = client.user;

    if (!user) {
      return;
    }

    this.onlineUsers.delete(user.id);
    await this.safeMarkOffline(user.id);

    this.server.emit('presence:offline', this.publicUser(user));
    this.server.emit('presence:list', this.getOnlineUsers());

    this.logger.log(`User disconnected: ${user.username} (${user.id})`);
  }

  @SubscribeMessage('presence:ping')
  handlePing(@ConnectedSocket() client: AuthenticatedSocket) {
    return {
      event: 'presence:pong',
      data: {
        userId: client.user?.id,
        timestamp: new Date().toISOString(),
      },
    };
  }

  @SubscribeMessage('presence:list')
  handlePresenceList() {
    return {
      event: 'presence:list',
      data: this.getOnlineUsers(),
    };
  }

  @SubscribeMessage('lobby:join')
  handleJoinLobby(@ConnectedSocket() client: AuthenticatedSocket) {
    client.join('lobby');
    this.server.to('lobby').emit('lobby:user-joined', this.publicUser(client.user));

    return {
      event: 'lobby:joined',
      data: { room: 'lobby' },
    };
  }

  @SubscribeMessage('lobby:leave')
  handleLeaveLobby(@ConnectedSocket() client: AuthenticatedSocket) {
    client.leave('lobby');
    this.server.to('lobby').emit('lobby:user-left', this.publicUser(client.user));

    return {
      event: 'lobby:left',
      data: { room: 'lobby' },
    };
  }

  @SubscribeMessage('typing:start')
  handleTypingStart(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { room?: string; receiverId?: string },
  ) {
    const room = body?.room || 'lobby';
    const payload = {
      userId: client.user?.id,
      username: client.user?.username,
      room,
      receiverId: body?.receiverId,
    };

    if (body?.receiverId) {
      client.to(`user:${body.receiverId}`).emit('typing:start', payload);
      return;
    }

    client.to(room).emit('typing:start', payload);
  }

  @SubscribeMessage('typing:stop')
  handleTypingStop(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { room?: string; receiverId?: string },
  ) {
    const room = body?.room || 'lobby';
    const payload = {
      userId: client.user?.id,
      username: client.user?.username,
      room,
      receiverId: body?.receiverId,
    };

    if (body?.receiverId) {
      client.to(`user:${body.receiverId}`).emit('typing:stop', payload);
      return;
    }

    client.to(room).emit('typing:stop', payload);
  }

  @SubscribeMessage('lobby:message:send')
  async handleLobbyMessageSend(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody()
    body: { content: string; room?: string; messageType?: string; replyToMessageId?: string },
  ) {
    const sender = client.user;

    if (!sender) {
      client.emit('lobby:message:error', { message: 'Unauthorized' });
      return;
    }

    const room = body.room || 'lobby';
    const isDevUser = sender.id.startsWith('dev-');
    const message = isDevUser
      ? this.createDevLobbyMessage(sender, body, room)
      : await this.lobbyService.sendMessage(sender.id, {
          content: body.content,
          room,
          messageType: body.messageType || 'text',
          replyToMessageId: body.replyToMessageId,
        });

    const payload = {
      ...message,
      sender: this.publicUser(sender),
    };

    this.server.to(room).emit('lobby:message:new', payload);

    return {
      event: 'lobby:message:sent',
      data: payload,
    };
  }

  @SubscribeMessage('lobby:message')
  async handleLobbyMessageLegacy(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { content: string; room?: string; messageType?: string },
  ) {
    return this.handleLobbyMessageSend(client, body);
  }

  @SubscribeMessage('lobby:history')
  async handleLobbyHistory(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { room?: string; limit?: number },
  ) {
    const room = body?.room || 'lobby';
    const limit = body?.limit || 50;
    const messages = client.user?.id?.startsWith('dev-')
      ? this.devLobbyMessages.filter((m) => m.room === room).slice(-limit).reverse()
      : await this.lobbyService.history(room, limit);

    return {
      event: 'lobby:history',
      data: messages,
    };
  }

  @SubscribeMessage('message:send')
  async handleLegacyMessageSend(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { content: string; room?: string; messageType?: string },
  ) {
    return this.handleLobbyMessageSend(client, body);
  }

  @SubscribeMessage('private-message:send')
  async handlePrivateMessageSend(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody()
    body: { receiverId: string; content: string; messageType?: string; replyToMessageId?: string },
  ) {
    const sender = client.user;

    if (!sender) {
      client.emit('private-message:error', { message: 'Unauthorized' });
      return;
    }

    if (!body?.receiverId || !body?.content?.trim()) {
      client.emit('private-message:error', { message: 'receiverId and content are required' });
      return;
    }

    const isDevConversation = sender.id.startsWith('dev-') || body.receiverId.startsWith('dev-');
    const message = isDevConversation
      ? this.createDevPrivateMessage(sender, body)
      : await this.privateChatService.send({
          senderId: sender.id,
          receiverId: body.receiverId,
          content: body.content,
          messageType: body.messageType || 'text',
          replyToMessageId: body.replyToMessageId,
        });

    const payload = {
      ...message,
      sender: this.publicUser(sender),
      receiver: this.publicUser(this.onlineUsers.get(body.receiverId)),
    };

    this.server.to(`user:${body.receiverId}`).emit('private-message:new', payload);
    client.emit('private-message:sent', payload);

    return {
      event: 'private-message:sent',
      data: payload,
    };
  }

  @SubscribeMessage('private-message:history')
  async handlePrivateMessageHistory(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { otherUserId: string; limit?: number },
  ) {
    const user = client.user;
    const otherUserId = body?.otherUserId;
    const limit = body?.limit || 50;

    if (!user || !otherUserId) {
      return { event: 'private-message:history', data: [] };
    }

    const isDevConversation = user.id.startsWith('dev-') || otherUserId.startsWith('dev-');
    const messages = isDevConversation
      ? this.devPrivateMessages
          .filter(
            (m) =>
              (m.senderId === user.id && m.receiverId === otherUserId) ||
              (m.senderId === otherUserId && m.receiverId === user.id),
          )
          .slice(-limit)
          .reverse()
      : await this.privateChatService.conversation(user.id, otherUserId, limit);

    return {
      event: 'private-message:history',
      data: messages,
    };
  }

  @SubscribeMessage('private-message:delivered')
  async handlePrivateMessageDelivered(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { messageId: string; senderId?: string },
  ) {
    if (client.user?.id?.startsWith('dev-')) {
      const message = this.updateDevPrivateMessageStatus(body.messageId, 'delivered');
      if (message?.senderId) this.server.to(`user:${message.senderId}`).emit('private-message:delivered', message);
      return { event: 'private-message:delivered', data: message };
    }

    const message = await this.privateChatService.markDelivered(body.messageId);
    this.server.to(`user:${message.senderId}`).emit('private-message:delivered', message);
    return { event: 'private-message:delivered', data: message };
  }

  @SubscribeMessage('private-message:read')
  async handlePrivateMessageRead(
    @ConnectedSocket() client: AuthenticatedSocket,
    @MessageBody() body: { messageId: string },
  ) {
    if (client.user?.id?.startsWith('dev-')) {
      const message = this.updateDevPrivateMessageStatus(body.messageId, 'read');
      if (message?.senderId) this.server.to(`user:${message.senderId}`).emit('private-message:read', message);
      return { event: 'private-message:read', data: message };
    }

    const message = await this.privateChatService.markRead(body.messageId);
    this.server.to(`user:${message.senderId}`).emit('private-message:read', message);
    return { event: 'private-message:read', data: message };
  }

  private async safeMarkOnline(userId: string) {
    if (userId.startsWith('dev-')) {
      return;
    }

    try {
      await this.presenceService.markOnline(userId);
    } catch (error) {
      this.logger.warn(`Presence markOnline skipped for ${userId}: ${error?.message || error}`);
    }
  }

  private async safeMarkOffline(userId: string) {
    if (userId.startsWith('dev-')) {
      return;
    }

    try {
      await this.presenceService.markOffline(userId);
    } catch (error) {
      this.logger.warn(`Presence markOffline skipped for ${userId}: ${error?.message || error}`);
    }
  }

  private getUserFromSocket(client: Socket): RealtimeUser {
    const authToken = client.handshake.auth?.token;
    const headerToken = client.handshake.headers.authorization?.replace(
      'Bearer ',
      '',
    );
    const token = authToken || headerToken;

    if (!token) {
      return this.getDevUserFromSocket(client);
    }

    const payload = this.jwtService.verify(token);

    return {
      id: payload.sub,
      username: payload.username,
      email: payload.email,
      role: payload.role,
    };
  }

  private getDevUserFromSocket(client: Socket): RealtimeUser {
    const devMode =
      process.env.ENABLE_DEV_SOCKET === 'true' ||
      (process.env.ENABLE_DEV_SOCKET !== 'false' && process.env.NODE_ENV !== 'production');

    if (!devMode) {
      throw new Error('Missing token');
    }

    const requestedName = String(client.handshake.auth?.devUsername || '').trim();
    const username = requestedName || `DevUser-${client.id.slice(0, 5)}`;

    return {
      id: `dev-${client.id}`,
      username,
      email: `${username.toLowerCase().replace(/[^a-z0-9]+/g, '.')}@dev.local`,
      role: 'DEV_TESTER',
    };
  }

  private createDevLobbyMessage(
    sender: RealtimeUser,
    body: { content: string; messageType?: string; replyToMessageId?: string },
    room: string,
  ) {
    const now = new Date();
    const message = {
      id: `dev-lobby-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      senderId: sender.id,
      room,
      content: body.content,
      messageType: body.messageType || 'text',
      replyToMessageId: body.replyToMessageId || null,
      status: 'sent',
      isEdited: false,
      isDeleted: false,
      createdAt: now,
      updatedAt: now,
      sender: this.publicUser(sender),
    };

    this.devLobbyMessages.push(message);
    if (this.devLobbyMessages.length > 200) {
      this.devLobbyMessages.shift();
    }

    return message;
  }

  private createDevPrivateMessage(
    sender: RealtimeUser,
    body: { receiverId: string; content: string; messageType?: string; replyToMessageId?: string },
  ) {
    const now = new Date();
    const receiver = this.onlineUsers.get(body.receiverId);
    const message = {
      id: `dev-private-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      senderId: sender.id,
      receiverId: body.receiverId,
      content: body.content,
      messageType: body.messageType || 'text',
      replyToMessageId: body.replyToMessageId || null,
      status: 'sent',
      isDelivered: !!receiver,
      isRead: false,
      isEdited: false,
      isDeleted: false,
      deliveredAt: receiver ? now : null,
      readAt: null,
      createdAt: now,
      updatedAt: now,
      sender: this.publicUser(sender),
      receiver: this.publicUser(receiver),
    };

    this.devPrivateMessages.push(message);
    if (this.devPrivateMessages.length > 500) {
      this.devPrivateMessages.shift();
    }

    return message;
  }

  private updateDevPrivateMessageStatus(messageId: string, status: 'delivered' | 'read') {
    const message = this.devPrivateMessages.find((m) => m.id === messageId);
    if (!message) return null;

    message.status = status;
    message.isDelivered = true;
    message.deliveredAt = message.deliveredAt || new Date();

    if (status === 'read') {
      message.isRead = true;
      message.readAt = new Date();
    }

    message.updatedAt = new Date();
    return message;
  }

  private getOnlineUsers() {
    return Array.from(this.onlineUsers.values()).map((user) => this.publicUser(user));
  }

  private publicUser(user?: Partial<RealtimeUser> | null) {
    if (!user) return null;
    return {
      id: user.id,
      userId: user.id,
      username: user.username,
      email: user.email,
      role: user.role,
    };
  }
}
