import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OnEvent } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { PunchType, TripStatus } from '../common/constants/enums';
import { APP_EVENTS } from '../common/constants/events';
import { trackingCoverageGapThresholdSeconds } from '../config/runtime-config';
import type { JwtUserPayload } from '../auth/jwt.types';
import { GpsPoint, GpsPointDocument } from '../gps/schemas/gps-point.schema';
import { Punch, PunchDocument } from '../punches/schemas/punch.schema';
import { TripsService } from '../trips/trips.service';
import { TripDocument } from '../trips/schemas/trip.schema';
import { docId } from '../common/utils/mongo.util';
import { enrichTripLocationsFromPunches } from '../trips/trip-api.mapper';
import { TrackingEventsService } from './tracking-events.service';
import {
  TrackingCoverageSnapshot,
  TrackingCoverageSnapshotDocument,
} from './schemas/tracking-coverage-snapshot.schema';
import {
  aggregateCoverageSummary,
  computeLegCoverage,
  type CoveragePoint,
  type CoverageTrackingEvent,
  type LegCoverageResult,
} from './tracking-coverage.util';

interface LegWindow {
  legId: string;
  legNumber: number;
  fromLocation?: string;
  toLocation?: string;
  departureAt: Date;
  arrivalAt: Date | null;
}

@Injectable()
export class TrackingCoverageService {
  private readonly logger = new Logger(TrackingCoverageService.name);

  constructor(
    @InjectModel(TrackingCoverageSnapshot.name)
    private readonly snapshotModel: Model<TrackingCoverageSnapshotDocument>,
    @InjectModel(GpsPoint.name)
    private readonly gpsModel: Model<GpsPointDocument>,
    @InjectModel(Punch.name)
    private readonly punchModel: Model<PunchDocument>,
    private readonly trips: TripsService,
    private readonly trackingEvents: TrackingEventsService,
    private readonly config: ConfigService,
  ) {}

  @OnEvent(APP_EVENTS.GPS_BATCH_PERSISTED)
  onGpsBatch(payload: { tripId: string }) {
    if (payload?.tripId) {
      void this.recomputeAndStore(payload.tripId).catch((err) =>
        this.logger.warn(`Coverage recompute failed: ${err}`),
      );
    }
  }

  @OnEvent(APP_EVENTS.PUNCH_RECORDED)
  onPunch(payload: { tripId: string; type: PunchType }) {
    if (
      payload?.tripId &&
      (payload.type === PunchType.ARRIVAL ||
        payload.type === PunchType.DEPARTURE)
    ) {
      void this.recomputeAndStore(payload.tripId).catch((err) =>
        this.logger.warn(`Coverage recompute failed: ${err}`),
      );
    }
  }

  @OnEvent(APP_EVENTS.TRACKING_COVERAGE_RECOMPUTE)
  onRecompute(payload: { tripId: string }) {
    if (payload?.tripId) {
      void this.recomputeAndStore(payload.tripId).catch((err) =>
        this.logger.warn(`Coverage recompute failed: ${err}`),
      );
    }
  }

  async getCoverageReport(
    requestId: string,
    actor: JwtUserPayload,
    legId?: string,
    includeEvents = false,
  ) {
    await this.trips.assertCanRead(actor, requestId);
    const snapshot = await this.recomputeAndStore(requestId);
    let legs = snapshot.legs.map((l) => this.formatLeg(l));
    if (legId) {
      legs = legs.filter((l) => l.legId === legId);
    }

    const response: Record<string, unknown> = {
      requestId: snapshot.requestId,
      tripId: snapshot.requestId,
      legs,
      summary: snapshot.summary,
    };

    if (includeEvents && legs.length) {
      const trip = await this.trips.findDocumentByTripId(requestId);
      const events = await this.trackingEvents.listForTrip(docId(trip));
      response.legs = legs.map((leg) => ({
        ...leg,
        events: events
          .filter(
            (e) =>
              !e.legId ||
              e.legId === leg.legId ||
              String(e.legId) === String(leg.legId),
          )
          .map((e) => ({
            type: e.type,
            timestamp: e.timestamp,
            legId: e.legId ?? null,
            sessionId: e.sessionId ?? null,
            metadata: e.metadata ?? null,
          })),
      }));
    }

    return response;
  }

  async getSummaryForTrip(requestId: string) {
    const cached = await this.snapshotModel
      .findOne({ requestId })
      .lean()
      .exec();
    if (cached?.summary) return cached.summary;
    const snapshot = await this.recomputeAndStore(requestId);
    return snapshot.summary;
  }

  async recomputeAndStore(requestId: string) {
    const trip = await this.trips.findDocumentByTripId(requestId);
    const tripDbId = docId(trip);
    const punches = await this.punchModel
      .find({ tripId: tripDbId })
      .sort({ timestamp: 1 })
      .lean()
      .exec();

    const enriched = enrichTripLocationsFromPunches(
      trip.toObject() as unknown as Record<string, unknown>,
      punches as unknown as Record<string, unknown>[],
    );

    const legWindows = this.buildLegWindows(
      trip,
      enriched,
      punches as Array<{ type: string; timestamp: Date }>,
    );

    const allPoints = await this.gpsModel
      .find({ tripId: tripDbId })
      .sort({ timestamp: 1 })
      .select('latitude longitude timestamp legId')
      .lean()
      .exec();

    const points: CoveragePoint[] = allPoints.map((p) => ({
      timestamp: p.timestamp,
      latitude: p.latitude,
      longitude: p.longitude,
      legId: p.legId,
    }));

    const gapThreshold = trackingCoverageGapThresholdSeconds(this.config);
    const legResults: LegCoverageResult[] = [];

    for (const leg of legWindows) {
      const windowEnd = leg.arrivalAt ?? new Date();
      const events = await this.trackingEvents.listForTrip(
        tripDbId,
        leg.departureAt,
        windowEnd,
      );
      const coverageEvents: CoverageTrackingEvent[] = events.map((e) => ({
        type: e.type,
        timestamp: e.timestamp,
        legId: e.legId,
        metadata: e.metadata as Record<string, unknown>,
      }));

      legResults.push(
        computeLegCoverage(
          {
            ...leg,
            points,
            events: coverageEvents,
          },
          gapThreshold,
        ),
      );
    }

    const summary = aggregateCoverageSummary(legResults);
    const computedAt = new Date();

    const doc = await this.snapshotModel
      .findOneAndUpdate(
        { requestId },
        {
          $set: {
            requestId,
            tripDbId,
            legs: legResults.map((l) => ({
              legId: l.legId,
              legNumber: l.legNumber,
              fromLocation: l.fromLocation,
              toLocation: l.toLocation,
              departureAt: new Date(l.departureAt),
              arrivalAt: l.arrivalAt ? new Date(l.arrivalAt) : undefined,
              expectedDurationMinutes: l.expectedDurationMinutes,
              trackedDurationMinutes: l.trackedDurationMinutes,
              gapDurationMinutes: l.gapDurationMinutes,
              coveragePercent: l.coveragePercent,
              pointCount: l.pointCount,
              gaps: l.gaps.map((g) => ({
                from: new Date(g.from),
                to: new Date(g.to),
                durationMinutes: g.durationMinutes,
                reason: g.reason,
                suspectedCause: g.suspectedCause,
              })),
            })),
            summary,
            computedAt,
          },
        },
        { upsert: true, new: true },
      )
      .exec();

    return doc!;
  }

  private buildLegWindows(
    trip: TripDocument,
    enriched: Record<string, unknown>,
    punches: Array<{ type: string; timestamp: Date }>,
  ): LegWindow[] {
    const legs: LegWindow[] = [];
    const departure = punches.find(
      (p) => String(p.type).toUpperCase() === PunchType.DEPARTURE,
    );
    const arrival = punches.find(
      (p) => String(p.type).toUpperCase() === PunchType.ARRIVAL,
    );

    if (departure) {
      legs.push({
        legId: 'leg-1',
        legNumber: 1,
        fromLocation: enriched.fromLocation as string | undefined,
        toLocation: enriched.toLocation as string | undefined,
        departureAt: departure.timestamp,
        arrivalAt: arrival?.timestamp ?? null,
      });
    }

    if (
      arrival &&
      (trip.status === TripStatus.RETURN_TRIP ||
        trip.status === TripStatus.COMPLETED)
    ) {
      legs.push({
        legId: 'leg-2',
        legNumber: 2,
        fromLocation: enriched.toLocation as string | undefined,
        toLocation: enriched.fromLocation as string | undefined,
        departureAt: arrival.timestamp,
        arrivalAt: trip.completedAt ?? null,
      });
    }

    return legs;
  }

  private formatLeg(leg: {
    legId: string;
    legNumber: number;
    fromLocation?: string;
    toLocation?: string;
    departureAt?: Date;
    arrivalAt?: Date;
    expectedDurationMinutes: number;
    trackedDurationMinutes: number;
    gapDurationMinutes: number;
    coveragePercent: number;
    pointCount: number;
    gaps: Array<{
      from: Date;
      to: Date;
      durationMinutes: number;
      reason: string;
      suspectedCause?: string;
    }>;
  }) {
    return {
      legId: leg.legId,
      legNumber: leg.legNumber,
      fromLocation: leg.fromLocation ?? null,
      toLocation: leg.toLocation ?? null,
      departureAt: leg.departureAt?.toISOString() ?? null,
      arrivalAt: leg.arrivalAt?.toISOString() ?? null,
      expectedDurationMinutes: leg.expectedDurationMinutes,
      trackedDurationMinutes: leg.trackedDurationMinutes,
      gapDurationMinutes: leg.gapDurationMinutes,
      coveragePercent: leg.coveragePercent,
      pointCount: leg.pointCount,
      gaps: leg.gaps.map((g) => ({
        from: g.from.toISOString(),
        to: g.to.toISOString(),
        durationMinutes: g.durationMinutes,
        reason: g.reason,
        suspectedCause: g.suspectedCause,
      })),
    };
  }
}
