import {
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { OnEvent } from '@nestjs/event-emitter';
import { Namespace, Socket } from 'socket.io';
import {
  APP_EVENTS,
  SOCKET_EVENTS,
  SOCKET_NAMESPACES,
  TRACKING_SOCKET_EVENTS,
} from '../common/constants/events';
import { TripStatus } from '../common/constants/enums';
import { resolveJwtAccessSecret } from '../config/runtime-config';
import type { JwtUserPayload } from '../auth/jwt.types';
import { resolveStoredRole } from '../users/user-api.mapper';
import { LiveTrackingService } from './live-tracking.service';
import {
  mapTripStatusToTracking,
  trackingStatusFromPunch,
  type TrackingLifecycleStatus,
} from './tracking-status.util';
import { extractSocketToken } from './ws-auth.util';
import type { TrackingJoinDto, TrackingLocationUpdateDto } from './dto/tracking-location.dto';

function tripRoom(tripId: string) {
  return `trip:${tripId}`;
}

@Injectable()
@WebSocketGateway({
  namespace: SOCKET_NAMESPACES.TRACKING,
  cors: { origin: true, credentials: true },
  pingInterval: 25000,
  pingTimeout: 20000,
})
export class TripTrackingGateway
  implements OnGatewayConnection, OnGatewayDisconnect
{
  private readonly logger = new Logger(TripTrackingGateway.name);

  @WebSocketServer()
  server: Namespace;

  constructor(
    private readonly jwt: JwtService,
    private readonly config: ConfigService,
    private readonly liveTracking: LiveTrackingService,
  ) {}

  async handleConnection(client: Socket) {
    try {
      const token = extractSocketToken(client);
      if (!token) throw new UnauthorizedException();
      const payload = this.jwt.verify<JwtUserPayload>(token, {
        secret: resolveJwtAccessSecret(this.config),
      });
      client.data.user = {
        ...payload,
        role: resolveStoredRole(payload.role),
      };
      client.data.joinedTrips = new Set<string>();
      this.logger.debug(`Tracking client connected: ${payload.uid}`);
    } catch {
      client.disconnect(true);
    }
  }

  handleDisconnect(client: Socket) {
    const joined = client.data.joinedTrips as Set<string> | undefined;
    if (joined?.size) {
      this.logger.debug(
        `Tracking client disconnected (${client.data.user?.uid}), had ${joined.size} trip room(s)`,
      );
    }
  }

  @SubscribeMessage(TRACKING_SOCKET_EVENTS.JOIN)
  async onJoin(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: TrackingJoinDto,
  ) {
    const actor = client.data.user as JwtUserPayload | undefined;
    if (!actor) return { ok: false, error: 'unauthorized' };

    try {
      await this.liveTracking.authorizeJoin(actor, data.tripId);
      await client.join(tripRoom(data.tripId));
      (client.data.joinedTrips as Set<string>).add(data.tripId);
      return { ok: true, tripId: data.tripId };
    } catch (err) {
      const message =
        err instanceof Error ? err.message : 'join_failed';
      return { ok: false, error: message };
    }
  }

  @SubscribeMessage(TRACKING_SOCKET_EVENTS.LEAVE)
  async onLeave(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: TrackingJoinDto,
  ) {
    await client.leave(tripRoom(data.tripId));
    (client.data.joinedTrips as Set<string>)?.delete(data.tripId);
    return { ok: true, tripId: data.tripId };
  }

  @SubscribeMessage(TRACKING_SOCKET_EVENTS.LOCATION_UPDATE)
  async onLocationUpdate(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: TrackingLocationUpdateDto,
  ) {
    const actor = client.data.user as JwtUserPayload | undefined;
    if (!actor) return { ok: false, error: 'unauthorized' };

    const result = await this.liveTracking.handleLocationUpdate(actor, data);
    if (result.broadcast) {
      this.server
        .to(tripRoom(data.tripId))
        .emit(TRACKING_SOCKET_EVENTS.LOCATION_UPDATED, result.broadcast);
    }
    return {
      ok: result.ok,
      persisted: result.persisted,
      reason: result.reason,
    };
  }

  private emitTrackingStatus(tripId: string, status: TrackingLifecycleStatus) {
    this.server
      .to(tripRoom(tripId))
      .emit(TRACKING_SOCKET_EVENTS.TRACKING_STATUS, { tripId, status });
  }

  @OnEvent(APP_EVENTS.TRIP_STATUS_CHANGED)
  onTripStatusChanged(trip: { tripId: string; status: TripStatus; userId?: string }) {
    if (!trip?.tripId) return;
    const status = mapTripStatusToTracking(trip.status);
    this.emitTrackingStatus(trip.tripId, status);
    if (trip.status === TripStatus.COMPLETED || trip.status === TripStatus.CANCELLED) {
      if (trip.userId) {
        this.liveTracking.clearTripCache(trip.tripId, trip.userId);
      } else {
        this.liveTracking.clearTripCacheForTripId(trip.tripId);
      }
    }
  }

  @OnEvent(APP_EVENTS.PUNCH_RECORDED)
  onPunchRecorded(payload: { tripId: string; type: string }) {
    const status = trackingStatusFromPunch(payload.type as never);
    if (status && payload.tripId) {
      this.emitTrackingStatus(payload.tripId, status);
    }
  }

  @OnEvent(APP_EVENTS.TRIP_CREATED)
  onTripCreatedBroadcast(trip: any) {
    if (trip) {
      this.server.emit(SOCKET_EVENTS.TRIP_UPDATE, trip);
    }
  }

  @OnEvent(APP_EVENTS.TRIP_UPDATED)
  onTripUpdatedBroadcast(trip: any) {
    if (trip) {
      this.server.emit(SOCKET_EVENTS.TRIP_UPDATE, trip);
    }
  }

  @OnEvent(APP_EVENTS.TRIP_STATUS_CHANGED)
  onTripStatusChangedBroadcast(trip: any) {
    if (trip) {
      this.server.emit(SOCKET_EVENTS.TRIP_UPDATE, trip);
    }
  }

  @OnEvent(APP_EVENTS.TRIP_DELETED)
  onTripDeletedBroadcast(payload: { tripId: string; tripDbId?: string }) {
    if (payload?.tripId) {
      this.liveTracking.clearTripCacheForTripId(payload.tripId);
      this.server.emit('trip:delete', payload.tripId);
    }
  }
}
