import {
  BadRequestException,
  ForbiddenException,
  Injectable,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { createHash } from 'crypto';
import { Model } from 'mongoose';
import { TrackingEventType, TripStatus } from '../common/constants/enums';
import { APP_EVENTS } from '../common/constants/events';
import type { JwtUserPayload } from '../auth/jwt.types';
import { TripsService } from '../trips/trips.service';
import { UsersService } from '../users/users.service';
import { docId } from '../common/utils/mongo.util';
import { TrackingEventsBatchDto } from './dto/tracking-event.dto';
import {
  TrackingEvent,
  TrackingEventDocument,
} from './schemas/tracking-event.schema';

const EMIT_EVENT_STATUSES: TripStatus[] = [
  TripStatus.STARTED,
  TripStatus.TRAVELLING,
  TripStatus.RETURN_TRIP,
  TripStatus.ARRIVED,
  TripStatus.MEETING_STARTED,
  TripStatus.MEETING_COMPLETED,
];

@Injectable()
export class TrackingEventsService {
  constructor(
    @InjectModel(TrackingEvent.name)
    private readonly eventModel: Model<TrackingEventDocument>,
    private readonly trips: TripsService,
    private readonly users: UsersService,
    private readonly events: EventEmitter2,
  ) {}

  private parseEventType(raw: string): TrackingEventType {
    const match = Object.values(TrackingEventType).find((v) => v === raw);
    if (!match) {
      throw new BadRequestException(`Invalid tracking event type: ${raw}`);
    }
    return match;
  }

  private async assertCanEmit(actor: JwtUserPayload, requestId: string) {
    const trip = await this.trips.findDocumentByTripId(requestId);
    const user = await this.users.findByUid(actor.uid);
    if (!user || docId(user) !== trip.userId) {
      throw new ForbiddenException(
        'Only the trip owner can upload tracking events',
      );
    }
    if (!EMIT_EVENT_STATUSES.includes(trip.status)) {
      throw new BadRequestException('Trip is not in an active tracking state');
    }
    return { trip, tripDbId: docId(trip) };
  }

  private buildDedupeKey(
    requestId: string,
    type: string,
    timestamp: string,
    legId?: string,
    sessionId?: string,
  ) {
    const raw = `${requestId}|${type}|${timestamp}|${legId ?? ''}|${sessionId ?? ''}`;
    return createHash('sha1').update(raw).digest('hex');
  }

  async ingestBatch(
    requestId: string,
    actor: JwtUserPayload,
    dto: TrackingEventsBatchDto,
  ) {
    const { tripDbId } = await this.assertCanEmit(actor, requestId);

    const ops = dto.events.map((e) => {
      const type = this.parseEventType(String(e.type));
      const ts = new Date(e.timestamp);
      const dedupeKey = this.buildDedupeKey(
        requestId,
        type,
        ts.toISOString(),
        e.legId,
        e.sessionId,
      );
      return {
        updateOne: {
          filter: { requestId, dedupeKey },
          update: {
            $set: {
              requestId,
              tripDbId,
              legId: e.legId,
              sessionId: e.sessionId,
              type,
              timestamp: ts,
              metadata: e.metadata,
              dedupeKey,
            },
          },
          upsert: true,
        },
      };
    });

    if (ops.length) {
      await this.eventModel.bulkWrite(ops, { ordered: false });
      this.events.emit(APP_EVENTS.TRACKING_COVERAGE_RECOMPUTE, { tripId: requestId });
    }

    return { inserted: ops.length, requestId };
  }

  async listForTrip(tripDbId: string, from?: Date, to?: Date) {
    const query: Record<string, unknown> = { tripDbId };
    if (from || to) {
      query.timestamp = {
        ...(from ? { $gte: from } : {}),
        ...(to ? { $lte: to } : {}),
      };
    }
    return this.eventModel.find(query).sort({ timestamp: 1 }).lean().exec();
  }
}
