import {
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
  ConnectedSocket,
} from "@nestjs/websockets";
import { JwtService } from "@nestjs/jwt";
import { Server, Socket } from "socket.io";
import { PrismaService } from "../../common/prisma/prisma.service";
import { validateCorsOrigin } from "../../common/security/origins";

@WebSocketGateway({ cors: { origin: validateCorsOrigin, credentials: false }, namespace: "/", maxHttpBufferSize: 32 * 1024 })
export class ConversationsGateway
  implements OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  server!: Server;

  constructor(private readonly jwt: JwtService, private readonly prisma: PrismaService) {}

  private consume(client: Socket, key: string, limit: number, windowMs: number) {
    const now = Date.now();
    const rates = (client.data.rates ??= {}) as Record<string, { count: number; resetAt: number }>;
    const current = rates[key];
    if (!current || current.resetAt <= now) {
      rates[key] = { count: 1, resetAt: now + windowMs };
      return true;
    }
    current.count += 1;
    return current.count <= limit;
  }

  async handleConnection(client: Socket) {
    const header = client.handshake.headers.authorization;
    const token = String(client.handshake.auth?.token || (header?.startsWith("Bearer ") ? header.slice(7) : ""));
    try {
      const payload = await this.jwt.verifyAsync<{ sub: string; tokenVersion?: number; tokenType?: string; exp: number }>(token, {
        secret: process.env.JWT_ACCESS_SECRET,
        issuer: "mawjod-api",
        audience: "mawjod-client",
        algorithms: ["HS256"],
      });
      if (payload.tokenType !== "access") throw new Error("invalid_token_type");
      const user = await this.prisma.user.findUnique({ where: { id: payload.sub } });
      if (!user?.isActive || ["SUSPENDED", "BANNED", "DELETED"].includes(user.status) || user.tokenVersion !== (payload.tokenVersion ?? 0)) throw new Error("session_revoked");
      client.data.userId = payload.sub;
      client.data.tokenVersion = payload.tokenVersion ?? 0;
      client.data.expires = payload.exp;
    } catch {
      client.emit("auth_error", { message: "invalid_access_token" });
      client.disconnect(true);
      return;
    }
    client.emit("connected", { id: client.id });
  }

  handleDisconnect(client: Socket) {
    client.disconnect(true);
  }

  @SubscribeMessage("join")
  async handleJoin(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: { conversationId: string },
  ) {
    if (!client.data.userId) return { error: "authorization_required" };
    if (!this.consume(client, "join", 20, 60_000)) return { error: "rate_limit_exceeded" };
    if (typeof payload?.conversationId !== "string" || payload.conversationId.length > 128) return { error: "invalid_conversation" };
    const conversation = await this.prisma.conversation.findFirst({ where: { id: payload.conversationId, OR: [{ customerId: client.data.userId }, { providerId: client.data.userId }] }, select: { id: true } });
    if (!conversation) return { error: "conversation_member_only" };
    await client.join(payload.conversationId);
    return { joined: true };
  }

  @SubscribeMessage("leave")
  handleLeave(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: { conversationId: string },
  ) {
    if (typeof payload?.conversationId === "string" && payload.conversationId.length <= 128) client.leave(payload.conversationId);
  }

  @SubscribeMessage("typing")
  async handleTyping(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: { conversationId: string },
  ) {
    if (typeof payload?.conversationId !== "string" || !client.rooms.has(payload.conversationId)) return { error: "conversation_not_joined" };
    if (!this.consume(client, "typing", 30, 10_000)) return { error: "rate_limit_exceeded" };
    this.server.to(payload.conversationId).emit("typing", { conversationId: payload.conversationId, userId: client.data.userId });
  }

  /** Called by ConversationsService after saving a message to DB */
  async broadcastMessage(conversationId: string, message: object) {
    if (!this.server) return;
    const clients = await this.server.in(conversationId).fetchSockets();
    for (const client of clients) {
      const user = client.data.userId ? await this.prisma.user.findUnique({ where: { id: client.data.userId }, select: { isActive: true, tokenVersion: true } }) : null;
      if (!user?.isActive || user.tokenVersion !== client.data.tokenVersion || client.data.expires * 1000 < Date.now()) { client.disconnect(true); continue; }
      client.emit("message", message);
    }
  }
}
