import { Logger } from '@nestjs/common';
import {
  OnGatewayConnection,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Passenger as PassengerTypes } from '@edr/types';

import { WsAuthService } from './ws-auth.service';

/**
 * Server → client push for passenger support chat. Clients only *listen* (no
 * `@SubscribeMessage`); the handshake is authenticated in `handleConnection`.
 * Each socket joins a room based on its side:
 *  - backoffice staff → the shared `backoffice` room (see every conversation).
 *  - passengers        → their `user:<iamUserId>` room (their own tickets only).
 *
 * Side is decided by the presence of a `Passenger` row for the IAM user id
 * (staff have none). A message is emitted to BOTH the owner's room and the
 * backoffice room so the customer thread, the sender's echo, and every agent's
 * inbox update live.
 */
@WebSocketGateway({
  namespace: PassengerTypes.PASSENGER_SUPPORT_WS_NAMESPACE,
  cors: { origin: true, credentials: true },
})
export class SupportGateway implements OnGatewayConnection {
  private readonly logger = new Logger(SupportGateway.name);

  private static readonly BACKOFFICE_ROOM = 'backoffice';

  @WebSocketServer()
  private readonly server!: Server;

  constructor(private readonly wsAuth: WsAuthService) {}

  async handleConnection(socket: Socket): Promise<void> {
    const userId = await this.wsAuth.resolveUserId(this.extractToken(socket));

    // A valid token means a backoffice agent: the portal connects only with a
    // device/guest id (never a token), so every token-authed socket is staff.
    // Join the shared backoffice room — no passenger-row heuristic needed.
    if (userId) {
      socket.data.userId = userId;
      socket.data.side = 'AGENT';
      await socket.join(SupportGateway.BACKOFFICE_ROOM);
      socket.emit('support:hello', {
        side: 'AGENT',
        room: SupportGateway.BACKOFFICE_ROOM,
        userId,
      });
      this.logger.debug(`support socket ${socket.id} → AGENT (backoffice)`);
      return;
    }

    // Guest: no valid token, but a client-generated guestId scopes the room.
    // Anyone holding the guestId can see that thread (no account = weaker
    // ownership) — an accepted MVP trade-off for guest support.
    const guestId = this.extractGuestId(socket);
    if (guestId) {
      socket.data.guestId = guestId;
      socket.data.side = 'USER';
      await socket.join(`guest:${guestId}`);
      return;
    }

    this.logger.debug(`Rejected passenger-support handshake ${socket.id}`);
    socket.disconnect(true);
  }

  /** Push a new message + updated conversation to the owner + backoffice rooms. */
  emitMessage(
    ownerRoom: string | null,
    conversation: PassengerTypes.PassengerSupportConversationDto,
    message: PassengerTypes.PassengerSupportMessageDto,
  ): void {
    const payload = { conversation, message };
    for (const room of this.targetRooms(ownerRoom)) {
      const to = this.server.to(room);
      to.emit(PassengerTypes.PASSENGER_SUPPORT_WS_EVENTS.MESSAGE_NEW, payload);
      to.emit(
        PassengerTypes.PASSENGER_SUPPORT_WS_EVENTS.CONVERSATION_UPDATED,
        conversation,
      );
    }
  }

  /** Push a conversation metadata change (e.g. status) to both rooms. */
  emitConversationUpdated(
    ownerRoom: string | null,
    conversation: PassengerTypes.PassengerSupportConversationDto,
  ): void {
    for (const room of this.targetRooms(ownerRoom)) {
      this.server
        .to(room)
        .emit(
          PassengerTypes.PASSENGER_SUPPORT_WS_EVENTS.CONVERSATION_UPDATED,
          conversation,
        );
    }
  }

  private targetRooms(ownerRoom: string | null): string[] {
    const rooms = [SupportGateway.BACKOFFICE_ROOM];
    if (ownerRoom) rooms.push(ownerRoom);
    return rooms;
  }

  private extractGuestId(socket: Socket): string | undefined {
    const authGuest = socket.handshake.auth?.guestId as string | undefined;
    if (authGuest) return authGuest;
    const queryGuest = socket.handshake.query?.guestId;
    if (typeof queryGuest === 'string') return queryGuest;
    return undefined;
  }

  private extractToken(socket: Socket): string | undefined {
    const authToken = socket.handshake.auth?.token as string | undefined;
    if (authToken) return authToken;

    const queryToken = socket.handshake.query?.token;
    if (typeof queryToken === 'string') return queryToken;

    const header = socket.handshake.headers?.authorization;
    if (header?.startsWith('Bearer ')) return header.slice(7);

    return undefined;
  }
}
