import {
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { OnEvent } from '@nestjs/event-emitter';
import { Namespace, Server, Socket } from 'socket.io';
import { UserRole } from '../common/constants/enums';
import { GpsService } from '../gps/gps.service';
import { APP_EVENTS, SOCKET_EVENTS } from '../common/constants/events';
import { resolveJwtAccessSecret } from '../config/runtime-config';
import type { JwtUserPayload } from '../auth/jwt.types';
import { GpsBatchDto } from '../gps/dto/gps.dto';

function extractToken(client: Socket): string | null {
  const auth = client.handshake.auth?.token ?? client.handshake.headers?.authorization;
  if (typeof auth === 'string') {
    return auth.startsWith('Bearer ') ? auth.slice(7) : auth;
  }
  const x = client.handshake.headers['x-access-token'];
  if (typeof x === 'string') return x;
  return null;
}

abstract class BaseTrackingGateway implements OnGatewayConnection {
  protected readonly logger = new Logger(this.constructor.name);

  constructor(
    protected readonly jwt: JwtService,
    protected readonly config: ConfigService,
    protected readonly gps: GpsService,
  ) {}

  abstract server: Namespace;
  protected abstract allowedRoles: UserRole[];

  async handleConnection(client: Socket) {
    try {
      const token = extractToken(client);
      if (!token) throw new UnauthorizedException();
      const payload = this.jwt.verify<JwtUserPayload>(token, {
        secret: resolveJwtAccessSecret(this.config),
      });
      if (!this.allowedRoles.includes(payload.role)) {
        client.disconnect();
        return;
      }
      client.data.user = payload;
      this.logger.debug(`Client connected: ${payload.uid}`);
    } catch {
      client.disconnect();
    }
  }

  @SubscribeMessage(SOCKET_EVENTS.JOIN_TRIP)
  joinTrip(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { tripId: string },
  ) {
    void client.join(`trip:${data.tripId}`);
    return { ok: true };
  }

  @SubscribeMessage(SOCKET_EVENTS.LEAVE_TRIP)
  leaveTrip(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { tripId: string },
  ) {
    void client.leave(`trip:${data.tripId}`);
    return { ok: true };
  }

  @SubscribeMessage(SOCKET_EVENTS.SUBSCRIBE_LIVE)
  subscribeLive(@ConnectedSocket() client: Socket) {
    void client.join('live');
    return { ok: true };
  }

  @SubscribeMessage(SOCKET_EVENTS.UNSUBSCRIBE_LIVE)
  unsubscribeLive(@ConnectedSocket() client: Socket) {
    void client.leave('live');
    return { ok: true };
  }

  @OnEvent(APP_EVENTS.GPS_BATCH_PERSISTED)
  onGpsBatch(payload: {
    tripId: string;
    lastPoint?: { latitude: number; longitude: number; timestamp: Date };
  }) {
    this.server.to(`trip:${payload.tripId}`).emit(SOCKET_EVENTS.GPS_UPDATE, payload);
    this.server.to('live').emit(SOCKET_EVENTS.GPS_UPDATE, payload);
  }

  @OnEvent(APP_EVENTS.TRIP_STATUS_CHANGED)
  onTripUpdate(trip: unknown) {
    this.server.emit(SOCKET_EVENTS.TRIP_UPDATE, trip);
  }
}

@Injectable()
@WebSocketGateway({ namespace: '/admin', cors: { origin: true } })
export class AdminGateway extends BaseTrackingGateway {
  @WebSocketServer()
  server!: Namespace;

  protected allowedRoles = [
    UserRole.SUPER_ADMIN,
    UserRole.ADMIN,
  ];

  constructor(jwt: JwtService, config: ConfigService, gps: GpsService) {
    super(jwt, config, gps);
  }

  @SubscribeMessage(SOCKET_EVENTS.GPS_BATCH)
  async gpsBatch(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { tripId: string; batch: GpsBatchDto },
  ) {
    const user = client.data.user as JwtUserPayload;
    return this.gps.ingestBatch(data.tripId, user, data.batch);
  }
}

@Injectable()
@WebSocketGateway({ namespace: '/hod', cors: { origin: true } })
export class HodGateway extends BaseTrackingGateway {
  @WebSocketServer()
  server!: Namespace;

  protected allowedRoles = [
    UserRole.MANAGER,
    UserRole.HOD,
    UserRole.SUPER_ADMIN,
    UserRole.ADMIN,
  ];

  constructor(jwt: JwtService, config: ConfigService, gps: GpsService) {
    super(jwt, config, gps);
  }
}

@Injectable()
@WebSocketGateway({ namespace: '/employee', cors: { origin: true } })
export class EmployeeGateway extends BaseTrackingGateway {
  @WebSocketServer()
  server!: Namespace;

  protected allowedRoles = [
    UserRole.EMPLOYEE,
    UserRole.REPORTING_MANAGER,
    UserRole.HOD,
    UserRole.ADMIN,
    UserRole.SUPER_ADMIN,
  ];

  constructor(jwt: JwtService, config: ConfigService, gps: GpsService) {
    super(jwt, config, gps);
  }

  @SubscribeMessage(SOCKET_EVENTS.GPS_BATCH)
  async gpsBatch(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { tripId: string; batch: GpsBatchDto },
  ) {
    const user = client.data.user as JwtUserPayload;
    return this.gps.ingestBatch(data.tripId, user, data.batch);
  }
}
