import {
  WebSocketGateway,
  WebSocketServer,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  ConnectedSocket,
  MessageBody,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '@chatapp/database';
import { RedisService } from '@chatapp/redis';

interface JwtPayload {
  userId: string;
  email: string;
  iat?: number;
  exp?: number;
}

interface SendMessagePayload {
  channelId: string;
  text: string;
  threadId?: string;
}

interface TypingPayload {
  channelId: string;
}

interface PresencePayload {
  status: string;
}

interface EditMessagePayload {
  messageId: string;
  channelId: string;
  text: string;
}

interface DeleteMessagePayload {
  messageId: string;
  channelId: string;
}

interface MessageReadPayload {
  channelId: string;
}

interface ReactionPayload {
  messageId: string;
  channelId: string;
  emoji: string;
}

@WebSocketGateway({
  cors: {
    origin: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3001'],
    credentials: true,
  },
  namespace: '/',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  private readonly logger = new Logger(ChatGateway.name);

  @WebSocketServer()
  server!: Server;

  constructor(
    private readonly jwtService: JwtService,
    private readonly prisma: PrismaService,
    private readonly redisService: RedisService,
  ) {}

  // ---------------------------------------------------------------------------
  // Connection / Disconnection
  // ---------------------------------------------------------------------------

  async handleConnection(client: Socket): Promise<void> {
    try {
      const token = this.extractToken(client);
      if (!token) {
        this.logger.warn(`Connection rejected: no token`);
        client.emit('error', { message: 'No token provided' });
        client.disconnect();
        return;
      }

      const payload = await this.jwtService.verifyAsync<JwtPayload>(token, {
        secret: process.env.JWT_SECRET,
      });

      // Store user info on the socket
      (client as any).userId = payload.userId;
      (client as any).email = payload.email;

      // Join rooms for all channels the user is a member of
      const memberships = await this.prisma.channelMember.findMany({
        where: { userId: payload.userId, leftAt: null },
        select: { channelId: true, workspaceId: true },
      });

      const roomNames = memberships.map((m) => `channel:${m.channelId}`);
      // Also join a personal room for direct messages / notifications
      roomNames.push(`user:${payload.userId}`);

      // Join workspace rooms for presence broadcasting
      const workspaceIds = [...new Set(memberships.map((m) => m.workspaceId))];
      for (const wsId of workspaceIds) {
        roomNames.push(`workspace:${wsId}`);
      }

      await client.join(roomNames);

      // Set presence to online
      await this.redisService.setPresence(payload.userId, 'online');

      // Update user's presence in DB
      await this.prisma.user.update({
        where: { id: payload.userId },
        data: { presence: 'online', lastActiveAt: new Date() },
      }).catch(() => void 0);

      // Broadcast presence update to workspace
      for (const wsId of workspaceIds) {
        this.server.to(`workspace:${wsId}`).emit('presence:update', {
          userId: payload.userId,
          status: 'online',
        });
      }

      this.logger.log(`User ${payload.email} connected, joined ${roomNames.length} rooms`);

      client.emit('connected', {
        userId: payload.userId,
        rooms: roomNames,
      });
    } catch (err: any) {
      this.logger.warn(`Connection rejected: ${err?.message || err}`);
      client.emit('error', { message: 'Invalid or expired token' });
      client.disconnect();
    }
  }

  async handleDisconnect(client: Socket): Promise<void> {
    const userId = (client as any).userId;
    if (userId) {
      await this.redisService.setPresence(userId, 'offline');

      // Update user's presence in DB
      await this.prisma.user.update({
        where: { id: userId },
        data: { presence: 'offline', lastActiveAt: new Date() },
      }).catch(() => void 0);

      // Broadcast presence update to workspaces
      const memberships = await this.prisma.channelMember.findMany({
        where: { userId, leftAt: null },
        select: { workspaceId: true },
      }).catch(() => []);
      const workspaceIds = [...new Set(memberships.map((m: any) => m.workspaceId))];
      for (const wsId of workspaceIds) {
        this.server.to(`workspace:${wsId}`).emit('presence:update', {
          userId,
          status: 'offline',
        });
      }

      this.logger.log(`User ${(client as any).email || userId} disconnected`);
    }
  }

  // ---------------------------------------------------------------------------
  // Message events
  // ---------------------------------------------------------------------------

  @SubscribeMessage('message:send')
  async handleMessageSend(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: SendMessagePayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) {
      client.emit('error', { message: 'Not authenticated' });
      return;
    }

    // Verify the user is a member of the channel
    const membership = await this.prisma.channelMember.findFirst({
      where: { channelId: data.channelId, userId, leftAt: null },
    });
    if (!membership) {
      client.emit('error', { message: 'Not a member of this channel' });
      return;
    }

    // Create the message in the database
    const message = await this.prisma.message.create({
      data: {
        workspaceId: membership.workspaceId,
        channelId: data.channelId,
        userId,
        text: data.text,
        threadId: data.threadId || null,
        threadRootId: data.threadId || null,
      },
    });

    // Update channel's lastMessageAt
    await this.prisma.channel.update({
      where: { id: data.channelId },
      data: { lastMessageAt: new Date() },
    });

    // Broadcast to all members in the channel room
    this.server.to(`channel:${data.channelId}`).emit('message:new', message);
  }

  @SubscribeMessage('message:edit')
  async handleMessageEdit(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: EditMessagePayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) {
      client.emit('error', { message: 'Not authenticated' });
      return;
    }

    const message = await this.prisma.message.findUnique({ where: { id: data.messageId } });
    if (!message || message.userId !== userId) {
      client.emit('error', { message: 'Cannot edit this message' });
      return;
    }

    const updated = await this.prisma.message.update({
      where: { id: data.messageId },
      data: {
        text: data.text,
        isEdited: true,
        editedAt: new Date(),
        editedBy: userId,
      },
    });

    this.server.to(`channel:${data.channelId}`).emit('message:edit', {
      id: updated.id,
      channelId: updated.channelId,
      text: updated.text,
      isEdited: updated.isEdited,
      editedAt: updated.editedAt,
      editedBy: updated.editedBy,
    });
  }

  @SubscribeMessage('message:delete')
  async handleMessageDelete(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: DeleteMessagePayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) {
      client.emit('error', { message: 'Not authenticated' });
      return;
    }

    const message = await this.prisma.message.findUnique({ where: { id: data.messageId } });
    if (!message) {
      client.emit('error', { message: 'Message not found' });
      return;
    }

    // Only the message author or channel admin/owner can delete
    if (message.userId !== userId) {
      const membership = await this.prisma.channelMember.findFirst({
        where: { channelId: data.channelId, userId, leftAt: null },
      });
      if (!membership || !['admin', 'owner'].includes(membership.role)) {
        client.emit('error', { message: 'Cannot delete this message' });
        return;
      }
    }

    await this.prisma.message.update({
      where: { id: data.messageId },
      data: {
        isDeleted: true,
        deletedAt: new Date(),
        deletedBy: userId,
      },
    });

    this.server.to(`channel:${data.channelId}`).emit('message:delete', {
      id: data.messageId,
      channelId: data.channelId,
      deletedBy: userId,
    });
  }

  // ---------------------------------------------------------------------------
  // Typing indicators
  // ---------------------------------------------------------------------------

  @SubscribeMessage('typing:start')
  async handleTypingStart(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: TypingPayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) return;

    await this.redisService.setTyping(data.channelId, userId);

    // Emit to all other members in the channel (excluding sender)
    client.to(`channel:${data.channelId}`).emit('typing:start', {
      channelId: data.channelId,
      userId,
    });
  }

  @SubscribeMessage('typing:stop')
  async handleTypingStop(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: TypingPayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) return;

    await this.redisService.del(`typing:${data.channelId}:${userId}`);

    // Emit to all other members in the channel (excluding sender)
    client.to(`channel:${data.channelId}`).emit('typing:stop', {
      channelId: data.channelId,
      userId,
    });
  }

  // ---------------------------------------------------------------------------
  // Presence
  // ---------------------------------------------------------------------------

  @SubscribeMessage('presence:update')
  async handlePresenceUpdate(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: PresencePayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) return;

    const status = data.status || 'online';

    // Update presence in Redis
    await this.redisService.setPresence(userId, status);

    // Update user's presence in DB
    await this.prisma.user.update({
      where: { id: userId },
      data: { presence: status, lastActiveAt: new Date() },
    }).catch(() => void 0);

    // Broadcast to all workspaces the user belongs to
    const memberships = await this.prisma.channelMember.findMany({
      where: { userId, leftAt: null },
      select: { workspaceId: true },
    }).catch(() => []);
    const workspaceIds = [...new Set(memberships.map((m: any) => m.workspaceId))];

    for (const wsId of workspaceIds) {
      this.server.to(`workspace:${wsId}`).emit('presence:update', {
        userId,
        status,
      });
    }
  }

  // ---------------------------------------------------------------------------
  // Message read receipts
  // ---------------------------------------------------------------------------

  @SubscribeMessage('message:read')
  async handleMessageRead(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: MessageReadPayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) {
      client.emit('error', { message: 'Not authenticated' });
      return;
    }

    // Verify membership
    const membership = await this.prisma.channelMember.findFirst({
      where: { channelId: data.channelId, userId, leftAt: null },
    });
    if (!membership) {
      client.emit('error', { message: 'Not a member of this channel' });
      return;
    }

    // Update last read timestamp and reset unread counts
    await this.prisma.channelMember.update({
      where: { id: membership.id },
      data: {
        lastReadTs: new Date(),
        unreadCount: 0,
        unreadMentionCount: 0,
      },
    });

    // Emit read acknowledgment back to the user
    client.emit('read_ack', {
      channelId: data.channelId,
      userId,
      lastReadTs: new Date().toISOString(),
    });

    // Also broadcast to the channel so other members can see read receipts
    this.server.to(`channel:${data.channelId}`).emit('message:read', {
      channelId: data.channelId,
      userId,
      lastReadTs: new Date().toISOString(),
    });
  }

  // ---------------------------------------------------------------------------
  // Reactions
  // ---------------------------------------------------------------------------

  @SubscribeMessage('reaction:add')
  async handleReactionAdd(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: ReactionPayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) {
      client.emit('error', { message: 'Not authenticated' });
      return;
    }

    // Verify the message exists and user is a member of the channel
    const message = await this.prisma.message.findUnique({
      where: { id: data.messageId },
    });
    if (!message) {
      client.emit('error', { message: 'Message not found' });
      return;
    }

    const membership = await this.prisma.channelMember.findFirst({
      where: { channelId: data.channelId, userId, leftAt: null },
    });
    if (!membership) {
      client.emit('error', { message: 'Not a member of this channel' });
      return;
    }

    // Create reaction (ignore if already exists due to unique constraint)
    try {
      await this.prisma.messageReaction.create({
        data: { messageId: data.messageId, userId, emoji: data.emoji },
      });
    } catch {
      // Reaction already exists, ignore
    }

    // Broadcast to all members in the channel
    this.server.to(`channel:${data.channelId}`).emit('reaction:add', {
      messageId: data.messageId,
      channelId: data.channelId,
      userId,
      emoji: data.emoji,
    });
  }

  @SubscribeMessage('reaction:remove')
  async handleReactionRemove(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: ReactionPayload,
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) {
      client.emit('error', { message: 'Not authenticated' });
      return;
    }

    // Verify membership
    const membership = await this.prisma.channelMember.findFirst({
      where: { channelId: data.channelId, userId, leftAt: null },
    });
    if (!membership) {
      client.emit('error', { message: 'Not a member of this channel' });
      return;
    }

    // Remove reaction
    await this.prisma.messageReaction.deleteMany({
      where: { messageId: data.messageId, userId, emoji: data.emoji },
    });

    // Broadcast to all members in the channel
    this.server.to(`channel:${data.channelId}`).emit('reaction:remove', {
      messageId: data.messageId,
      channelId: data.channelId,
      userId,
      emoji: data.emoji,
    });
  }


  // ---------------------------------------------------------------------------
  // Thread replies
  // ---------------------------------------------------------------------------

  @SubscribeMessage('thread:reply')
  async handleThreadReply(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { threadRootId: string; channelId: string; text: string },
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) {
      client.emit('error', { message: 'Not authenticated' });
      return;
    }

    // Verify membership
    const membership = await this.prisma.channelMember.findFirst({
      where: { channelId: data.channelId, userId, leftAt: null },
    });
    if (!membership) {
      client.emit('error', { message: 'Not a member of this channel' });
      return;
    }

    // Verify the thread root message exists
    const rootMessage = await this.prisma.message.findUnique({
      where: { id: data.threadRootId },
    });
    if (!rootMessage) {
      client.emit('error', { message: 'Thread root message not found' });
      return;
    }

    // Create the reply in the database
    const reply = await this.prisma.message.create({
      data: {
        workspaceId: membership.workspaceId,
        channelId: data.channelId,
        userId,
        text: data.text,
        threadRootId: data.threadRootId,
        threadId: data.threadRootId,
      },
      include: {
        user: { select: { id: true, displayName: true, avatarUrl: true } },
      },
    });

    // Update parent reply count and latest reply timestamp
    await this.prisma.message.update({
      where: { id: data.threadRootId },
      data: {
        replyCount: { increment: 1 },
        latestReplyTs: new Date(),
      },
    });

    // Broadcast to all members in the channel room
    this.server.to(`channel:${data.channelId}`).emit('thread:new_reply', {
      threadRootId: data.threadRootId,
      reply,
      replyCount: (rootMessage.replyCount || 0) + 1,
    });

    // Notify the thread root author if it's a different user
    if (rootMessage.userId !== userId) {
      this.server.to(`user:${rootMessage.userId}`).emit('notification', {
        type: 'thread_reply',
        title: 'Nueva respuesta en tu hilo',
        body: `${reply.user?.displayName || 'Alguien'} respondio a tu mensaje`,
        messageId: reply.id,
        threadRootId: data.threadRootId,
        channelId: data.channelId,
      });
    }
  }

  // ---------------------------------------------------------------------------
  // Channel join/leave events
  // ---------------------------------------------------------------------------

  @SubscribeMessage('channel:join')
  async handleChannelJoin(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { channelId: string },
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) return;

    await client.join(`channel:${data.channelId}`);
    client.to(`channel:${data.channelId}`).emit('channel:join', {
      channelId: data.channelId,
      userId,
    });
  }

  @SubscribeMessage('channel:leave')
  async handleChannelLeave(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { channelId: string },
  ): Promise<void> {
    const userId = (client as any).userId;
    if (!userId) return;

    await client.leave(`channel:${data.channelId}`);
    client.to(`channel:${data.channelId}`).emit('channel:leave', {
      channelId: data.channelId,
      userId,
    });
  }

  // ---------------------------------------------------------------------------
  // Helpers
  // ---------------------------------------------------------------------------

  private extractToken(client: Socket): string | undefined {
    // Token can come from handshake auth or auth header
    const auth = (client.handshake as any).auth;
    if (auth && auth.token) {
      return auth.token;
    }

    const headers = client.handshake.headers;
    const authorization = headers['authorization'] as string | undefined;
    if (authorization) {
      const [type, token] = authorization.split(' ');
      if (type === 'Bearer' && token) {
        return token;
      }
    }

    return undefined;
  }
}
