import {
  BOOKING_WINDOW_WS_EVENTS,
  BOOKING_WINDOW_WS_NAMESPACE,
  type BatchBoardChangedEvent,
  type BookingWindowPhaseEvent,
} from '@edr/types';
import { Logger } from '@nestjs/common';
import {
  OnGatewayConnection,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

import { WsAuthService } from '../notification-inbox/ws-auth.service';
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';

/**
 * Server → client push for booking-window state changes. Same handshake model
 * as the notifications gateway: clients only listen, the token is verified on
 * connect. Events are broadcast namespace-wide — window state is route-scoped
 * public information for signed-in users, and clients filter/invalidate their
 * own queries.
 */
@WebSocketGateway({
  namespace: BOOKING_WINDOW_WS_NAMESPACE,
  cors: { origin: true, credentials: true },
})
export class BookingWindowGateway implements OnGatewayConnection {
  private readonly logger = new Logger(BookingWindowGateway.name);

  @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));
    if (!userId) {
      this.logger.debug(`Rejected booking-window handshake ${socket.id}`);
      socket.disconnect(true);
      return;
    }
    socket.data.userId = userId;
    // Log at info so "is anyone actually connected?" is answerable from the
    // API log when diagnosing missing live updates.
    this.logger.log(`Booking-window client connected (user ${userId})`);
  }

  /** Push a schedule's current window state to every connected client. */
  emitPhase(schedule: TrainSchedule): void {
    const payload: BookingWindowPhaseEvent = {
      scheduleId: schedule.id,
      originYardId: schedule.originStationId,
      destinationYardId: schedule.destinationStationId,
      direction: schedule.direction ?? null,
      phase: (schedule.windowPhase ?? 'PRE_WINDOW') as BookingWindowPhaseEvent['phase'],
      bookingWindowStatus: schedule.bookingWindowStatus ?? null,
      bookingCycleNo: schedule.bookingCycleNo,
      windowOpensAt: schedule.windowOpensAt?.toISOString() ?? null,
      windowClosesAt: schedule.windowClosesAt?.toISOString() ?? null,
      docReviewEndsAt: schedule.docReviewEndsAt?.toISOString() ?? null,
      paymentPhaseEndsAt: schedule.paymentPhaseEndsAt?.toISOString() ?? null,
      scheduledDepartureDate: schedule.scheduledDepartureDate?.toISOString() ?? null,
    };
    this.server.emit(BOOKING_WINDOW_WS_EVENTS.PHASE, payload);
  }

  /**
   * Announce that a schedule's batch-board data changed (payment, allocation,
   * fill, expiry, …). Broadcast namespace-wide like PHASE — the payload carries
   * no board data, only the scheduleId; clients holding that board refetch.
   */
  emitBatchChanged(scheduleId: string, reason: string): void {
    const payload: BatchBoardChangedEvent = {
      scheduleId,
      reason,
      at: new Date().toISOString(),
    };
    this.server.emit(BOOKING_WINDOW_WS_EVENTS.BATCH_CHANGED, payload);
  }

  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;
  }
}
