import {
  ForbiddenException,
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { randomUUID } from 'crypto';
import { TripStatus } from '../common/constants/enums';
import { isManagementRole, isOperationsRole } from '../common/constants/roles';
import { resolveStoredRole } from '../users/user-api.mapper';
import { APP_EVENTS } from '../common/constants/events';
import type { JwtUserPayload } from '../auth/jwt.types';
import { UsersService } from '../users/users.service';
import { Trip, TripDocument } from './schemas/trip.schema';
import { Punch, PunchDocument } from '../punches/schemas/punch.schema';
import { GpsPoint, GpsPointDocument } from '../gps/schemas/gps-point.schema';
import { GpsStop, GpsStopDocument } from '../gps/schemas/gps-stop.schema';
import {
  TrackingEvent,
  TrackingEventDocument,
} from '../tracking/schemas/tracking-event.schema';
import {
  TrackingCoverageSnapshot,
  TrackingCoverageSnapshotDocument,
} from '../tracking/schemas/tracking-coverage-snapshot.schema';
import { docId } from '../common/utils/mongo.util';
import { CreateTripDto, UpdateTripDto } from './dto/trip.dto';
import { AddNextClientDto } from './dto/add-next-client.dto';
import { enrichTripLocationsFromPunches, parseFuelType, parseVehicleType } from './trip-api.mapper';
import { toTripCreatorUser } from '../users/user-api.mapper';
import { assertFuelTypeForVehicle } from '../settings/fuel-rates.util';
import {
  addNextClientLeg,
  addReturnLeg,
  ensureTripLegs,
  hasPunch,
  syncTripTopLevelFromLegs,
  type TripLegRecord,
} from './trip-leg.util';
import { FuelRatesService } from '../settings/fuel-rates.service';
import {
  computeTravelAllowance,
  resolveTripDistanceKm,
  type FuelRatesRecord,
} from '../settings/fuel-rates.util';

const ACTIVE_STATUSES: TripStatus[] = [
  TripStatus.STARTED,
  TripStatus.TRAVELLING,
  TripStatus.ARRIVED,
  TripStatus.MEETING_STARTED,
  TripStatus.MEETING_COMPLETED,
  TripStatus.RETURN_TRIP,
];

@Injectable()
export class TripsService {
  constructor(
    @InjectModel(Trip.name) private readonly tripModel: Model<TripDocument>,
    @InjectModel(Punch.name) private readonly punchModel: Model<PunchDocument>,
    @InjectModel(GpsPoint.name)
    private readonly gpsPointModel: Model<GpsPointDocument>,
    @InjectModel(GpsStop.name)
    private readonly gpsStopModel: Model<GpsStopDocument>,
    @InjectModel(TrackingEvent.name)
    private readonly trackingEventModel: Model<TrackingEventDocument>,
    @InjectModel(TrackingCoverageSnapshot.name)
    private readonly trackingCoverageModel: Model<TrackingCoverageSnapshotDocument>,
    private readonly users: UsersService,
    private readonly events: EventEmitter2,
    private readonly fuelRates: FuelRatesService,
  ) {}

  private tripJson(trip: TripDocument, extras?: Record<string, unknown>) {
    const obj = trip.toObject();
    return { ...obj, id: docId(trip), ...extras };
  }

  private normalizeUserId(userId: unknown): string {
    if (userId == null) return '';
    if (typeof userId === 'string') return userId;
    if (
      typeof userId === 'object' &&
      userId !== null &&
      'toString' in userId &&
      typeof (userId as { toString(): string }).toString === 'function'
    ) {
      return (userId as { toString(): string }).toString();
    }
    return String(userId);
  }

  private async buildTripUserMap(userIds: string[]) {
    const uniqueIds = [...new Set(userIds.filter(Boolean))];
    const userMap = new Map<string, ReturnType<typeof toTripCreatorUser>>();

    await Promise.all(
      uniqueIds.map(async (id) => {
        const user = await this.users.findById(id);
        if (!user) return;
        const clientUser = await this.users.toClientUserWithManager(user);
        userMap.set(this.normalizeUserId(id), toTripCreatorUser(clientUser));
        userMap.set(docId(user), toTripCreatorUser(clientUser));
      }),
    );

    return userMap;
  }

  async create(actor: JwtUserPayload, dto: CreateTripDto) {
    const user = await this.users.findByUid(actor.uid);
    if (!user) throw new NotFoundException('User not found');

    if (!dto.fromLocation?.trim() || !dto.toLocation?.trim()) {
      throw new BadRequestException(
        'fromLocation and toLocation are required when creating a travel request',
      );
    }

    const trip = await this.tripModel.create({
      tripId: randomUUID(),
      userId: docId(user),
      requestDate: new Date(),
      city: dto.city,
      fromLocation: dto.fromLocation,
      toLocation: dto.toLocation,
      clientName: dto.clientName,
      originLat: dto.originLat,
      originLng: dto.originLng,
      destinationLat: dto.destinationLat,
      destinationLng: dto.destinationLng,
      googlePlaceId: dto.googlePlaceId,
      plannedDistance: dto.plannedDistance,
      vehicleType: dto.vehicleType,
      fuelType: dto.fuelType,
      purpose: dto.purpose,
      status: TripStatus.CREATED,
    });

    const u = await this.users.findById(docId(user));
    const result = this.tripJson(trip, {
      user: u
        ? { uid: u.uid, fullName: u.fullName, employeeCode: u.employeeCode }
        : null,
    });
    this.events.emit(APP_EVENTS.TRIP_CREATED, result);
    return result;
  }

  private async resolveActorUserId(actor: JwtUserPayload): Promise<string | null> {
    const user = await this.users.findByUid(actor.uid);
    return user ? docId(user) : null;
  }

  private async buildUserScopedQuery(
    actor: JwtUserPayload,
    mine?: boolean,
    userId?: string,
  ): Promise<Record<string, unknown>> {
    const query: Record<string, unknown> = {};
    const actorRole = resolveStoredRole(actor.role);

    if (mine || !isOperationsRole(actorRole)) {
      const actorUserId = await this.resolveActorUserId(actor);
      if (actorUserId) query.userId = actorUserId;
      return query;
    }

    if (userId) {
      const target =
        (await this.users.findByUid(userId)) ?? (await this.users.findById(userId));
      if (target) query.userId = docId(target);
    }

    return query;
  }

  async getOne(tripId: string, actor: JwtUserPayload) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanViewTrip(trip.userId, actor);
    return this.findByTripId(tripId);
  }

  async findByTripId(tripId: string) {
    const trip = await this.tripModel.findOne({ tripId }).exec();
    if (!trip) throw new NotFoundException('Trip not found');
    const punches = await this.punchModel
      .find({ tripId: docId(trip) })
      .sort({ timestamp: 1 })
      .lean()
      .exec();
    const user = await this.users.findById(trip.userId);
    const tripUser = user
      ? toTripCreatorUser(await this.users.toClientUserWithManager(user))
      : null;
    const raw = {
      ...this.tripJson(trip),
      punches: punches.map((p) => ({ ...p, id: p._id.toString() })),
      user: tripUser,
    };
    const located = enrichTripLocationsFromPunches(
      raw,
      raw.punches as Record<string, unknown>[],
    );
    const rates = await this.fuelRates.getRates();
    return this.enrichTripFinancials(located, rates);
  }

  private enrichTripFinancials(
    trip: Record<string, unknown>,
    rates: FuelRatesRecord,
  ): Record<string, unknown> {
    const legs = Array.isArray(trip.tripLegs)
      ? (trip.tripLegs as Record<string, unknown>[])
      : [];
    const distanceKm = resolveTripDistanceKm(trip, legs);
    const fuelType =
      (trip.fuelType as string | undefined) ??
      (trip.fuel_type as string | undefined);
    const vehicleType =
      (trip.vehicleType as string | undefined) ??
      (trip.vehicle_type as string | undefined);
    const allowance = computeTravelAllowance(
      distanceKm,
      vehicleType,
      fuelType,
      rates,
    );
    return {
      ...trip,
      totalDistanceKm: distanceKm,
      travelAllowance: allowance.travelAllowance,
      fuelRatePerKm: allowance.fuelRatePerKm,
    };
  }

  async findDocumentByTripId(tripId: string): Promise<TripDocument> {
    const trip = await this.tripModel.findOne({ tripId }).exec();
    if (!trip) throw new NotFoundException('Trip not found');
    return trip;
  }

  async list(
    actor: JwtUserPayload,
    filters: {
      userId?: string;
      status?: TripStatus;
      from?: string;
      to?: string;
      cursor?: string;
      limit?: number;
    },
  ) {
    const limit = filters.limit ?? 20;
    const query: Record<string, unknown> = {};

    if (!isOperationsRole(resolveStoredRole(actor.role))) {
      const user = await this.users.findByUid(actor.uid);
      if (user) query.userId = docId(user);
    } else if (filters.userId) {
      const target =
        (await this.users.findByUid(filters.userId)) ??
        (await this.users.findById(filters.userId));
      if (target) query.userId = docId(target);
    }

    if (filters.status) query.status = filters.status;
    if (filters.from || filters.to) {
      query.createdAt = {
        ...(filters.from ? { $gte: new Date(filters.from) } : {}),
        ...(filters.to ? { $lte: new Date(filters.to) } : {}),
      };
    }
    if (filters.cursor && Types.ObjectId.isValid(filters.cursor)) {
      query._id = { $lt: new Types.ObjectId(filters.cursor) };
    }

    const trips = await this.tripModel
      .find(query)
      .sort({ createdAt: -1 })
      .limit(limit + 1)
      .exec();

    const hasMore = trips.length > limit;
    const items = hasMore ? trips.slice(0, limit) : trips;
    const userMap = await this.buildTripUserMap(items.map((t) => t.userId));
    const rates = await this.fuelRates.getRates();

    return {
      items: items.map((t) =>
        this.enrichTripFinancials(
          {
            ...this.tripJson(t),
            user: userMap.get(this.normalizeUserId(t.userId)) ?? null,
          },
          rates,
        ),
      ),
      nextCursor: hasMore ? docId(items[items.length - 1]) : null,
    };
  }

  async paginateTravelRequests(
    actor: JwtUserPayload,
    params: { mine?: boolean; page?: number; limit?: number; userId?: string },
  ) {
    const page = Math.max(1, params.page ?? 1);
    const limit = Math.min(100, Math.max(1, params.limit ?? 20));
    const skip = (page - 1) * limit;
    const query = await this.buildUserScopedQuery(actor, params.mine, params.userId);

    const [items, total, pending, completed] = await Promise.all([
      this.tripModel
        .find(query)
        .sort({ requestDate: -1, createdAt: -1 })
        .skip(skip)
        .limit(limit)
        .lean()
        .exec(),
      this.tripModel.countDocuments(query).exec(),
      this.tripModel
        .countDocuments({
          ...query,
          status: { $nin: [TripStatus.COMPLETED, TripStatus.CANCELLED] },
        })
        .exec(),
      this.tripModel.countDocuments({ ...query, status: TripStatus.COMPLETED }).exec(),
    ]);

    const tripIds = items.map((t) => t._id.toString());
    const punches = tripIds.length
      ? await this.punchModel
          .find({ tripId: { $in: tripIds } })
          .sort({ timestamp: 1 })
          .lean()
          .exec()
      : [];
    const punchMap = new Map<string, Array<Record<string, unknown>>>();
    for (const p of punches) {
      const k = p.tripId;
      const arr = punchMap.get(k) ?? [];
      arr.push({ ...p, id: p._id.toString() });
      punchMap.set(k, arr);
    }

    const totalPages = Math.max(1, Math.ceil(total / limit));
    const userMap = await this.buildTripUserMap(items.map((t) => t.userId));
    const rates = await this.fuelRates.getRates();

    return {
      items: items.map((t) => {
        const located = enrichTripLocationsFromPunches(
          {
            ...t,
            id: t._id.toString(),
            requestDate: (t as any).requestDate ?? (t as any).createdAt,
            punches: punchMap.get(t._id.toString()) ?? [],
            user: userMap.get(this.normalizeUserId(t.userId)) ?? null,
          },
          punchMap.get(t._id.toString()) ?? [],
        );
        return this.enrichTripFinancials(located, rates);
      }),
      meta: {
        page,
        limit,
        total,
        totalPages,
        pending,
        completed,
      },
    };
  }

  async getActiveTrip(actor: JwtUserPayload) {
    const query = await this.buildUserScopedQuery(actor, true, undefined);
    const trip = await this.tripModel
      .findOne({
        ...query,
        status: {
          $in: [
            TripStatus.TRAVELLING,
            TripStatus.RETURN_TRIP,
            TripStatus.MEETING_STARTED,
            TripStatus.ARRIVED,
            TripStatus.MEETING_COMPLETED,
          ],
        },
      })
      .sort({ updatedAt: -1 })
      .exec();
    if (!trip) throw new NotFoundException('No active trip found');
    return this.findByTripId(trip.tripId);
  }

  async summary(actor: JwtUserPayload, mine = true) {
    const query = await this.buildUserScopedQuery(actor, mine, undefined);
    const [total, pending, completed] = await Promise.all([
      this.tripModel.countDocuments(query).exec(),
      this.tripModel
        .countDocuments({
          ...query,
          status: { $nin: [TripStatus.COMPLETED, TripStatus.CANCELLED] },
        })
        .exec(),
      this.tripModel.countDocuments({ ...query, status: TripStatus.COMPLETED }).exec(),
    ]);
    return { total, pending, completed };
  }

  async updateStatus(tripId: string, status: TripStatus, actor: JwtUserPayload) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanModifyTrip(trip.userId, actor);

    const update: Record<string, unknown> = { status };
    if (status === TripStatus.STARTED) update.startedAt = new Date();
    if (status === TripStatus.ARRIVED) update.arrivedAt = new Date();
    if (status === TripStatus.COMPLETED) update.completedAt = new Date();

    trip.set(update);
    await trip.save();

    const user = await this.users.findById(trip.userId);
    const result = this.tripJson(trip, {
      user: user ? { uid: user.uid, fullName: user.fullName } : null,
    });
    this.events.emit(APP_EVENTS.TRIP_STATUS_CHANGED, result);
    return result;
  }

  async update(tripId: string, actor: JwtUserPayload, dto: UpdateTripDto) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanModifyTrip(trip.userId, actor);
    const patch: Partial<UpdateTripDto> = {
      city: dto.city,
      fromLocation: dto.fromLocation,
      toLocation: dto.toLocation,
      clientName: dto.clientName,
      purpose: dto.purpose,
    };
    trip.set(patch);
    await trip.save();
    const result = this.tripJson(trip);
    this.events.emit(APP_EVENTS.TRIP_UPDATED, result);
    return result;
  }

  async updateFromLegacyPatch(
    tripId: string,
    actor: JwtUserPayload,
    body: Record<string, unknown>,
  ) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanModifyTrip(trip.userId, actor);

    if (body.fromLocation != null) {
      trip.fromLocation = String(body.fromLocation);
    }
    if (body.toLocation != null) {
      trip.toLocation = String(body.toLocation);
    }
    if (body.clientName != null) {
      trip.clientName = String(body.clientName);
    }
    if (body.purpose != null) {
      trip.purpose = String(body.purpose);
    }
    if (body.originLat != null) {
      trip.originLat = Number(body.originLat);
    }
    if (body.originLng != null) {
      trip.originLng = Number(body.originLng);
    }
    if (body.destinationLat != null) {
      trip.destinationLat = Number(body.destinationLat);
    }
    if (body.destinationLng != null) {
      trip.destinationLng = Number(body.destinationLng);
    }
    if (body.vehicleType != null) {
      trip.vehicleType = parseVehicleType(String(body.vehicleType));
    }
    if (body.fuelType !== undefined) {
      if (body.fuelType == null || body.fuelType === '') {
        trip.fuelType = undefined;
      } else {
        trip.fuelType = parseFuelType(String(body.fuelType));
      }
    }
    assertFuelTypeForVehicle(trip.vehicleType, trip.fuelType);

    const tripLegs = body.tripLegs;
    if (Array.isArray(tripLegs)) {
      trip.set('tripLegs', tripLegs);
      trip.markModified('tripLegs');
      syncTripTopLevelFromLegs(trip, tripLegs as Record<string, unknown>[]);
    }

    if (body.currentLegIndex != null) {
      trip.currentLegIndex = Number(body.currentLegIndex);
    }

    await trip.save();
    this.events.emit(APP_EVENTS.TRIP_UPDATED, this.tripJson(trip));
    return this.findByTripId(tripId);
  }

  async addNextClient(
    tripId: string,
    actor: JwtUserPayload,
    dto: AddNextClientDto,
  ) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanModifyTrip(trip.userId, actor);

    const punches = await this.punchModel
      .find({ tripId: docId(trip) })
      .sort({ timestamp: 1 })
      .lean()
      .exec();

    const updatedLegs = addNextClientLeg(
      trip,
      dto,
      punches.map((p) => ({ ...p, id: p._id.toString() })),
    );
    syncTripTopLevelFromLegs(trip, updatedLegs);
    trip.set('tripLegs', updatedLegs);
    trip.markModified('tripLegs');
    await trip.save();

    const result = await this.findByTripId(tripId);
    this.events.emit(APP_EVENTS.TRIP_UPDATED, result);
    return result;
  }

  async startReturnTrip(
    tripId: string,
    actor: JwtUserPayload,
    _location?: {
      timestamp?: string;
      latitude?: number;
      longitude?: number;
    },
  ) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanModifyTrip(trip.userId, actor);

    const punches = await this.punchModel
      .find({ tripId: docId(trip) })
      .sort({ timestamp: 1 })
      .lean()
      .exec();

    const updatedLegs = addReturnLeg(
      trip,
      punches.map((p) => ({ ...p, id: p._id.toString() })),
    );
    syncTripTopLevelFromLegs(trip, updatedLegs);
    if (trip.originLat != null && trip.originLng != null) {
      trip.destinationLat = trip.originLat;
      trip.destinationLng = trip.originLng;
    }
    trip.set('tripLegs', updatedLegs);
    trip.markModified('tripLegs');
    await trip.save();

    const result = await this.findByTripId(tripId);
    this.events.emit(APP_EVENTS.TRIP_UPDATED, result);
    return result;
  }

  async getActiveTrips(userIds?: string[]) {
    const query: Record<string, unknown> = { status: { $in: ACTIVE_STATUSES } };
    if (userIds?.length) query.userId = { $in: userIds };
    const trips = await this.tripModel.find(query).sort({ updatedAt: -1 }).exec();
    return Promise.all(
      trips.map(async (t) => {
        const user = await this.users.findById(t.userId);
        return {
          ...this.tripJson(t),
          user: user
            ? {
                uid: user.uid,
                fullName: user.fullName,
                employeeCode: user.employeeCode,
                presence: user.presence,
              }
            : null,
        };
      }),
    );
  }

  async updateMetrics(
    tripId: string,
    metrics: {
      totalDistance?: number;
      travelDuration?: number;
      idleTime?: number;
      meetingDuration?: number;
      productivityScore?: number;
      tripEfficiency?: number;
    },
  ) {
    const trip = await this.tripModel
      .findOneAndUpdate({ tripId }, metrics, { new: true })
      .exec();
    return trip ? this.tripJson(trip) : null;
  }

  async remove(tripId: string, actor: JwtUserPayload) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanModifyTrip(trip.userId, actor);

    if (!(await this.canDeleteTrip(trip))) {
      throw new BadRequestException(
        'Only travel requests that have not started can be deleted',
      );
    }

    const tripDbId = docId(trip);

    await Promise.all([
      this.punchModel.deleteMany({ tripId: tripDbId }).exec(),
      this.gpsPointModel.deleteMany({ tripId: tripDbId }).exec(),
      this.gpsStopModel.deleteMany({ tripId: tripDbId }).exec(),
      this.trackingEventModel
        .deleteMany({ $or: [{ requestId: tripId }, { tripDbId }] })
        .exec(),
      this.trackingCoverageModel
        .deleteMany({ $or: [{ requestId: tripId }, { tripDbId }] })
        .exec(),
      this.tripModel.deleteOne({ tripId }).exec(),
    ]);

    this.events.emit(APP_EVENTS.TRIP_DELETED, { tripId, tripDbId });

    return { ok: true, tripId };
  }

  private async canDeleteTrip(trip: TripDocument): Promise<boolean> {
    if (trip.status !== TripStatus.CREATED) {
      return false;
    }
    if (trip.startedAt) {
      return false;
    }

    const legs = Array.isArray(trip.tripLegs)
      ? (trip.tripLegs as TripLegRecord[])
      : [];
    if (
      legs.some(
        (leg) =>
          hasPunch(leg, 'departurePunch') ||
          hasPunch(leg, 'arrivalPunch') ||
          hasPunch(leg, 'meetingStartPunch') ||
          hasPunch(leg, 'meetingEndPunch'),
      )
    ) {
      return false;
    }

    const punchCount = await this.punchModel
      .countDocuments({ tripId: docId(trip) })
      .exec();
    return punchCount === 0;
  }

  async assertCanRead(actor: JwtUserPayload, tripId: string) {
    const trip = await this.findDocumentByTripId(tripId);
    await this.assertCanViewTrip(trip.userId, actor);
    return trip;
  }

  private async assertCanViewTrip(tripUserId: string, actor: JwtUserPayload) {
    if (isOperationsRole(resolveStoredRole(actor.role))) return;
    const user = await this.users.findByUid(actor.uid);
    if (!user || docId(user) !== tripUserId) {
      throw new ForbiddenException('Not allowed for this trip');
    }
  }

  private async assertCanModifyTrip(tripUserId: string, actor: JwtUserPayload) {
    if (isManagementRole(resolveStoredRole(actor.role))) return;
    const user = await this.users.findByUid(actor.uid);
    if (!user || docId(user) !== tripUserId) {
      throw new ForbiddenException('Not allowed for this trip');
    }
  }
}
