import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { FindOptionsWhere, In, IsNull, Not } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { attachMileFinancials } from '../../common/mile-financials.util';
import { VehicleAvailability } from '../vehicles/entities/vehicle.entity';
import { BookingsRepository } from "../bookings/bookings.repository";
import { DriversService } from "../drivers/drivers.service";
import { SmsClientService } from "../notifications/sms-client.service";
import { VehiclesService } from "../vehicles/vehicles.service";
import { CreateFirstMileDto } from "./dto/create-first-mile.dto";
import { UpdateFirstMileDto } from "./dto/update-first-mile.dto";
import { FirstMile, FirstMileStatus } from "./entities/first-mile.entity";
import { FirstMileContainerAllocation } from "./entities/first-mile-container-allocation.entity";
import { FirstMileVehicleAssignment } from "./entities/first-mile-vehicle-assignment.entity";
import { FirstMileRepository } from "./first-mile.repository";
import { OnEvent } from "@nestjs/event-emitter";
import { BillingService, InvoiceEventPayload } from "../billing/billing.service";
import { FleetHistoryService } from "../fleet-history/fleet-history.service";
import { FleetEventType } from "../fleet-history/entities/fleet-event.entity";

type FirstMileListFilter = {
  status?: FirstMileStatus;
  bookingId?: string;
  vehicleId?: string;
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortOrder?: string;
};

const SORTABLE_FIELDS: (keyof FirstMile)[] = [
  "status",
  "advancedPayment",
  "remainingPayment",
  "createdAt",
];

@Injectable()
export class FirstMileService {
  private readonly logger = new Logger(FirstMileService.name);

  constructor(
    @InjectDataSource() private readonly dataSource: DataSource,
    private readonly firstMileRepository: FirstMileRepository,
    private readonly bookingsRepository: BookingsRepository,
    private readonly vehiclesService: VehiclesService,
    private readonly driversService: DriversService,
    private readonly smsClient: SmsClientService,
    private readonly history: FleetHistoryService,
    private readonly billing: BillingService,
  ) { }

  /** Attach real invoice info so the UI shows an invoice link only when one
   *  exists — not merely because distance was entered. Batched (no N+1). */
  private async attachInvoices(records: FirstMile[]): Promise<void> {
    const invoices = await this.billing.findBySourceIds(
      'first_mile',
      records.map((r) => r.id),
    );
    const byId = new Map<string, { id: string; number: string; status: string }>();
    for (const inv of invoices) {
      if (!byId.has(inv.sourceId)) {
        byId.set(inv.sourceId, { id: inv.id, number: inv.invoiceNumber, status: String(inv.status) });
      }
    }
    for (const r of records) {
      (r as FirstMile & { invoice?: unknown }).invoice = byId.get(r.id) ?? null;
    }
    await attachMileFinancials(this.dataSource, records, 'FIRST_MILE');
  }

  /** Resolve a vehicle's driver + human labels, for stamping mile events onto
   *  the driver's timeline and naming the vehicle. Best-effort — never throws. */
  private async vehicleInfo(
    vehicleId?: string | null,
  ): Promise<{ driverId: string | null; plate: string | null; driverName: string | null }> {
    if (!vehicleId) return { driverId: null, plate: null, driverName: null };
    try {
      const v = await this.vehiclesService.findById(vehicleId);
      return {
        driverId: v.assignedDriverId ?? null,
        plate: v.plateNumber ?? v.code ?? null,
        driverName: v.assignedDriverName ?? null,
      };
    } catch {
      return { driverId: null, plate: null, driverName: null };
    }
  }

  /** A leg counts as having a vehicle if it has a direct assignment or at least
   *  one container allocation carrying a vehicle. Gates the IN_TRANSIT move. */
  private async hasAssignedVehicle(
    recordId: string,
    directVehicleId?: string | null,
  ): Promise<boolean> {
    if (directVehicleId) return true;
    const [junction, allocations] = await Promise.all([
      this.dataSource.manager.count(FirstMileVehicleAssignment, {
        where: { firstMileId: recordId },
      }),
      this.dataSource.manager.count(FirstMileContainerAllocation, {
        where: { firstMileId: recordId, vehicleId: Not(IsNull()) },
      }),
    ]);
    return junction > 0 || allocations > 0;
  }

  /** Human booking reference for a first-mile record, for the history timeline. */
  private async resolveBookingRef(record: FirstMile): Promise<string | null> {
    const loaded = (record as FirstMile & { booking?: { reference?: string } })
      .booking?.reference;
    if (loaded) return loaded;
    if (!record.bookingId) return null;
    try {
      const b = await this.bookingsRepository.findById(record.bookingId);
      return (b as { reference?: string } | null)?.reference ?? null;
    } catch {
      return null;
    }
  }

  /**
   * Look up a booking by its human-readable reference and confirm it has been
   * paid before any first-mile work proceeds. Throws if the reference is
   * unknown or the booking has not reached PAID status.
   */
  async acceptBooking(bookingId: string): Promise<FirstMile | null> {
    const booking = await this.bookingsRepository.findById(bookingId, {
      relations: { serviceType: true },
    });

    if (!booking) {
      return null;
    }

    return this.acceptEligibleBooking(booking);
  }

  async acceptBookingByReference(
    bookingReference: string,
  ): Promise<FirstMile | null> {
    const [booking] = await this.bookingsRepository.findAll({
      where: { reference: bookingReference },
      relations: { serviceType: true },
      take: 1,
    });

    if (!booking) {
      throw new NotFoundException(`Booking ${bookingReference} not found`);
    }

    return this.acceptEligibleBooking(booking);
  }

  /**
   * Shared accept path: validates payment + first-mile eligibility, rejects an
   * already-assigned booking, then creates the first-mile record. Throws a
   * meaningful HTTP error instead of returning null so the client can surface
   * why an accept was refused.
   */
  private async acceptEligibleBooking(booking: {
    id: string;
    reference?: string;
    paymentStatus?: string | null;
    tradeDirection?: string | null;
    firstMilePickupAddress?: string | null;
    serviceType?: { includesFirstMile?: boolean | null } | null;
  }): Promise<FirstMile | null> {
    if (booking.paymentStatus !== "PAID") {
      return null;
    }
    if (!this.bookingRequestsFirstMile(booking)) {
      return null;
    }

    const existing = await this.findByBookingId(booking.id);
    if (existing) {
      return null;
    }

    return this.create({
      bookingId: booking.id,
      advancedPayment: 0,
    });
  }
  async findAll(filter: FirstMileListFilter = {}): Promise<{
    data: FirstMile[];
    meta: { total: number; page: number; pageSize: number; totalPages: number };
  }> {
    const page = filter.page ?? 1;
    const pageSize = filter.pageSize ?? 50;
    const sortBy = SORTABLE_FIELDS.includes(filter.sortBy as keyof FirstMile)
      ? (filter.sortBy as keyof FirstMile)
      : "createdAt";
    const sortOrder =
      filter.sortOrder?.toUpperCase() === "ASC" ? "ASC" : "DESC";

    const where: FindOptionsWhere<FirstMile> = {};
    if (filter.status) where.status = filter.status;
    if (filter.bookingId) where.bookingId = filter.bookingId;
    if (filter.vehicleId) where.vehicleId = filter.vehicleId;

    const [data, total] = await this.firstMileRepository.findAndCount({
      where,
      relations: {
        booking: {
          company: true,
          serviceType: true,
          originYard: true,
          destinationYard: true,
          cargoType: true,
          bookingContainers: { containerType: true, units: true },
        },
        vehicle: true,
        vehicleAssignments: { vehicle: true },
      },
      order: { [sortBy]: sortOrder },
      skip: (page - 1) * pageSize,
      take: pageSize,
    });

    await this.attachInvoices(data);

    return {
      data,
      meta: {
        total,
        page,
        pageSize,
        totalPages: Math.max(1, Math.ceil(total / pageSize)),
      },
    };
  }

  @OnEvent("firstmile.invoice.paid")
  async onBookingInvoicePaid(payload: InvoiceEventPayload): Promise<void> {
    try {
      await this.firstMileRepository.update(payload.sourceId, {
        paid: true,
      } as any);
      this.logger.log(
        `Marked first-mile record ${payload.sourceId} as paid (invoice ${payload.invoiceId})`,
      );
    } catch (err) {
      this.logger.error(
        `Failed to update first-mile payment status for record ${payload.sourceId}: ${String(err)}`,
      );
    }
  }

  async findById(id: string): Promise<FirstMile> {
    const record = await this.firstMileRepository.findById(id, {
      relations: {
        booking: {
          company: true,
          serviceType: true,
          originYard: true,
          destinationYard: true,
          cargoType: true,
          bookingContainers: { containerType: true, units: true },
        },
        vehicle: true,
        vehicleAssignments: { vehicle: true },
      },
    });

    if (!record) {
      throw new NotFoundException(`First-mile record ${id} not found`);
    }

    await this.attachInvoices([record]);

    return record;
  }

  async create(dto: CreateFirstMileDto): Promise<FirstMile> {
    const existing = await this.findByBookingId(dto.bookingId);
    if (existing) {
      return existing;
    }

    const record = await this.firstMileRepository.create({
      bookingId: dto.bookingId,
      status: dto.status ?? "READY_TO_TRANSIT",
      advancedPayment: dto.advancedPayment ?? 0,
      remainingPayment: dto.remainingPayment ?? 0,
      estimatedKm: dto.estimatedKm ?? null,
      exactKm: dto.exactKm ?? null,
      vehicleId: dto.vehicleId ?? null,
      paid: (dto as any).paid ?? false,
    });

    if (dto.vehicleId) {
      await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY);
      const info = await this.vehicleInfo(dto.vehicleId);
      await this.history.record({
        eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
        vehicleId: dto.vehicleId,
        firstMileId: record.id,
        driverId: info.driverId,
        label: record.status,
        metadata: {
          mile: 'FIRST',
          bookingRef: await this.resolveBookingRef(record),
          vehiclePlate: info.plate,
          driverName: info.driverName,
        },
      });
    }

    return record;
  }

  private async findByBookingId(bookingId: string): Promise<FirstMile | null> {
    const [records] = await this.firstMileRepository.findAndCount({
      where: { bookingId },
      relations: {
        booking: {
          company: true,
          serviceType: true,
          originYard: true,
          destinationYard: true,
          cargoType: true,
        },
        vehicle: true,
      },
      take: 1,
    });
    return records[0] ?? null;
  }

  private bookingRequestsFirstMile(booking: {
    tradeDirection?: string | null;
    firstMilePickupAddress?: string | null;
    serviceType?: { includesFirstMile?: boolean | null } | null;
  }): boolean {
    // The pickup address is the only record of what the contract chose.
    // `serviceType.includesFirstMile` used to satisfy this too, but every
    // service type ships with it set to true, so the OR made the address check
    // dead and admitted every paid export booking into the queue.
    return Boolean(
      booking.tradeDirection === 'EXPORT' && booking.firstMilePickupAddress?.trim(),
    );
  }

  async update(id: string, dto: UpdateFirstMileDto): Promise<FirstMile> {
    const existing = await this.findById(id);

    // A leg can only go IN_TRANSIT once a vehicle is assigned (allowing a vehicle
    // assigned in this same request).
    if (dto.status === 'IN_TRANSIT' && existing.status !== 'IN_TRANSIT') {
      const vehicleId =
        dto.vehicleId !== undefined ? dto.vehicleId : existing.vehicleId;
      if (!(await this.hasAssignedVehicle(id, vehicleId))) {
        throw new BadRequestException(
          'Assign a vehicle before marking this first-mile leg in transit',
        );
      }
    }

    const dtoAny = dto as any;
    const updated = await this.firstMileRepository.update(id, {
      ...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
      ...(dto.status !== undefined ? { status: dto.status } : {}),
      ...(dto.advancedPayment !== undefined
        ? { advancedPayment: dto.advancedPayment }
        : {}),
      ...(dto.remainingPayment !== undefined
        ? { remainingPayment: dto.remainingPayment }
        : {}),
      ...(dto.estimatedKm !== undefined
        ? { estimatedKm: dto.estimatedKm }
        : {}),
      ...(dto.exactKm !== undefined ? { exactKm: dto.exactKm } : {}),
      ...(dto.vehicleId !== undefined ? { vehicleId: dto.vehicleId } : {}),
      ...(dtoAny.paid !== undefined ? { paid: dtoAny.paid } : {}),
    } as any);

    if (!updated) {
      throw new NotFoundException(`First-mile record ${id} not found`);
    }

    // Keep vehicle statuses in sync: new vehicle goes BUSY, replaced one goes back to FREE
    if (dto.vehicleId !== undefined && dto.vehicleId !== existing.vehicleId) {
      if (dto.vehicleId) {
        await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY);
      }
      if (existing.vehicleId) {
        await this.vehiclesService.releaseIfUnused([existing.vehicleId]);
      }
      // Audit the mile↔vehicle (re)assignment on both vehicle and driver lines.
      const bookingRef = await this.resolveBookingRef(existing);
      if (existing.vehicleId) {
        const info = await this.vehicleInfo(existing.vehicleId);
        await this.history.record({
          eventType: FleetEventType.MILE_VEHICLE_RELEASED,
          vehicleId: existing.vehicleId,
          firstMileId: id,
          driverId: info.driverId,
          metadata: {
            mile: 'FIRST',
            bookingRef,
            vehiclePlate: info.plate,
            driverName: info.driverName,
          },
        });
      }
      if (dto.vehicleId) {
        const info = await this.vehicleInfo(dto.vehicleId);
        await this.history.record({
          eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
          vehicleId: dto.vehicleId,
          firstMileId: id,
          driverId: info.driverId,
          label: updated.status,
          metadata: {
            mile: 'FIRST',
            bookingRef,
            vehiclePlate: info.plate,
            driverName: info.driverName,
          },
        });
      }
    }

    // Notify assigned driver on every explicit vehicle assignment or reassignment
    if (dto.vehicleId) {
      void this.notifyDriverAssignment(dto.vehicleId, existing);
    }

    if (dto.status !== undefined && dto.status !== existing.status) {
      const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null;
      const info = await this.vehicleInfo(vehicleId);
      await this.history.record({
        eventType: FleetEventType.MILE_STATUS_CHANGED,
        firstMileId: id,
        vehicleId,
        driverId: info.driverId,
        fromValue: existing.status,
        toValue: dto.status,
        metadata: {
          mile: 'FIRST',
          bookingRef: await this.resolveBookingRef(existing),
          vehiclePlate: info.plate,
          driverName: info.driverName,
        },
      });
    }

    // Trip finished — release the vehicles it was holding
    if (dto.status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') {
      await this.releaseVehicles(updated);
    }

    return updated;
  }

  async updateStatus(id: string, status: FirstMileStatus): Promise<FirstMile> {
    const existing = await this.findById(id);

    if (status === 'IN_TRANSIT' && existing.status !== 'IN_TRANSIT') {
      if (!(await this.hasAssignedVehicle(id, existing.vehicleId))) {
        throw new BadRequestException(
          'Assign a vehicle before marking this first-mile leg in transit',
        );
      }
    }

    const updated = await this.firstMileRepository.update(id, { status });

    if (!updated) {
      throw new NotFoundException(`First-mile record ${id} not found`);
    }

    if (status !== existing.status) {
      const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null;
      const info = await this.vehicleInfo(vehicleId);
      await this.history.record({
        eventType: FleetEventType.MILE_STATUS_CHANGED,
        firstMileId: id,
        vehicleId,
        driverId: info.driverId,
        fromValue: existing.status,
        toValue: status,
        metadata: {
          mile: 'FIRST',
          bookingRef: await this.resolveBookingRef(existing),
          vehiclePlate: info.plate,
          driverName: info.driverName,
        },
      });
    }

    if (status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') {
      await this.releaseVehicles(updated);
    }

    return updated;
  }

  /**
   * Free every vehicle held by this record (direct assignment + container
   * allocations), unless still in use by another active trip.
   */
  private async releaseVehicles(record: FirstMile): Promise<void> {
    const [assignments, recordAllocations] = await Promise.all([
      this.dataSource.manager.find(FirstMileVehicleAssignment, {
        where: { firstMileId: record.id },
      }),
      this.dataSource.manager.find(FirstMileContainerAllocation, {
        where: { firstMileId: record.id },
      }),
    ]);
    const vehicleIds = [
      ...new Set(
        [
          ...assignments.map((a) => a.vehicleId),
          ...recordAllocations.map((a) => a.vehicleId),
          record.vehicleId ?? null,
        ].filter((id): id is string => Boolean(id)),
      ),
    ];
    await this.vehiclesService.releaseIfUnused(vehicleIds);
  }

  /**
   * Replace the full set of vehicles serving a first-mile pickup (multi-truck).
   * Diffs against the current junction rows, syncing availability + audit history
   * for each added/removed vehicle. The first vehicle is mirrored onto the legacy
   * `vehicleId` column for back-compat with single-vehicle readers.
   */
  async setVehicles(
    id: string,
    inputs: Array<{
      vehicleId: string;
      containerNumber?: string | null;
      tons?: number | null;
      quantity?: number | null;
    }>,
  ): Promise<FirstMile> {
    const existing = await this.findById(id);
    // Dedupe by vehicleId, keeping the load details; preserve order.
    const desiredMap = new Map<
      string,
      { containerNumber: string | null; tons: number | null; quantity: number | null }
    >();
    for (const inp of inputs) {
      if (inp.vehicleId) {
        desiredMap.set(inp.vehicleId, {
          containerNumber: inp.containerNumber ?? null,
          tons: inp.tons ?? null,
          quantity: inp.quantity ?? null,
        });
      }
    }
    const desired = [...desiredMap.keys()];
    const desiredSet = new Set(desired);

    // Bulk drawdown: assigned tonnage may not exceed what the booking declares.
    const totalTons = [...desiredMap.values()].reduce((s, v) => s + (Number(v.tons) || 0), 0);
    if (totalTons > 0 && existing.bookingId) {
      const [b]: Array<{ vgm: string | null }> = await this.dataSource.query(
        `SELECT cargo_total_weight_vgm AS vgm FROM freight.bookings
          WHERE id = $1 AND deleted_at IS NULL`,
        [existing.bookingId],
      );
      const declared = Number(b?.vgm ?? 0);
      if (declared > 0 && totalTons > declared + 0.001) {
        throw new BadRequestException(
          `Assigned tonnage (${totalTons} t) exceeds the booking's declared ${declared} t`,
        );
      }
    }

    const manager = this.dataSource.manager;
    const current = await manager.find(FirstMileVehicleAssignment, {
      where: { firstMileId: id },
    });
    const junctionSet = new Set(current.map((a) => a.vehicleId));
    // Fold the legacy vehicleId into the release set — a vehicle assigned via the
    // old single-vehicle path has no junction row but must still be freed.
    const releaseIds = [...new Set(
      current.map((a) => a.vehicleId).concat(existing.vehicleId ? [existing.vehicleId] : []),
    )];
    const added = desired.filter((v) => !junctionSet.has(v));
    const removed = releaseIds.filter((v) => !desiredSet.has(v));
    // Vehicles that stay but whose load details changed.
    const changed = current.filter((a) => {
      const want = desiredMap.get(a.vehicleId);
      if (!want) return false;
      return (
        (a.containerNumber ?? null) !== want.containerNumber ||
        (a.tons == null ? null : Number(a.tons)) !== want.tons ||
        (a.quantity ?? null) !== want.quantity
      );
    });

    await this.dataSource.transaction(async (tx) => {
      if (removed.length) {
        await tx.delete(FirstMileVehicleAssignment, {
          firstMileId: id,
          vehicleId: In(removed),
        });
      }
      for (const vehicleId of added) {
        const want = desiredMap.get(vehicleId);
        await tx.insert(FirstMileVehicleAssignment, {
          firstMileId: id,
          vehicleId,
          containerNumber: want?.containerNumber ?? null,
          tons: want?.tons ?? null,
          quantity: want?.quantity ?? null,
        });
      }
      for (const row of changed) {
        const want = desiredMap.get(row.vehicleId);
        await tx.update(
          FirstMileVehicleAssignment,
          { firstMileId: id, vehicleId: row.vehicleId },
          {
            containerNumber: want?.containerNumber ?? null,
            tons: want?.tons ?? null,
            quantity: want?.quantity ?? null,
          },
        );
      }
    });

    // Legacy primary vehicle = first of the set (null when cleared).
    await this.firstMileRepository.update(id, { vehicleId: desired[0] ?? null } as any);

    const bookingRef = await this.resolveBookingRef(existing);
    for (const vehicleId of added) {
      await this.vehiclesService.setAvailability(vehicleId, VehicleAvailability.BUSY);
      void this.notifyDriverAssignment(vehicleId, existing);
      const info = await this.vehicleInfo(vehicleId);
      await this.history.record({
        eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
        vehicleId,
        firstMileId: id,
        driverId: info.driverId,
        label: existing.status,
        metadata: { mile: 'FIRST', bookingRef, vehiclePlate: info.plate, driverName: info.driverName },
      });
    }
    for (const vehicleId of removed) {
      await this.vehiclesService.releaseIfUnused([vehicleId]);
      const info = await this.vehicleInfo(vehicleId);
      await this.history.record({
        eventType: FleetEventType.MILE_VEHICLE_RELEASED,
        vehicleId,
        firstMileId: id,
        driverId: info.driverId,
        metadata: { mile: 'FIRST', bookingRef, vehiclePlate: info.plate, driverName: info.driverName },
      });
    }

    return this.findById(id);
  }

  /**
   * Record each truck's actual distance. The pickup total (exact_km) is their
   * sum and drives billing; `remainingPayment` (total km × rate) is recomputed
   * client-side. Does NOT generate an invoice — that's a separate explicit step.
   */
  async setDistances(
    id: string,
    distances: Array<{ vehicleId: string; distanceKm: number }>,
    remainingPayment?: number,
  ): Promise<FirstMile> {
    await this.findById(id);

    // Distances are locked once the invoice exists.
    const invoices = await this.billing.findBySourceIds('first_mile', [id]);
    if (invoices.length) {
      throw new BadRequestException(
        'Distances cannot be changed after the invoice is generated',
      );
    }

    for (const d of distances) {
      await this.dataSource.manager.update(
        FirstMileVehicleAssignment,
        { firstMileId: id, vehicleId: d.vehicleId },
        { distanceKm: d.distanceKm },
      );
    }

    // Billing is per truck: amount = Σ (truck distance × truck price/km). The
    // per-vehicle rate + currency live on the vehicle, so we ignore the legacy
    // FIRST_MILE flat rate and any client-sent amount. `remainingPayment` param
    // kept only for signature back-compat.
    void remainingPayment;
    const assignments = await this.dataSource.manager.find(FirstMileVehicleAssignment, {
      where: { firstMileId: id },
      relations: { vehicle: true },
    });
    const total = assignments.reduce((s, a) => s + (Number(a.distanceKm) || 0), 0);
    const amount = assignments.reduce(
      (s, a) => s + (Number(a.distanceKm) || 0) * (Number(a.vehicle?.pricePerKm) || 0),
      0,
    );
    await this.firstMileRepository.update(id, {
      exactKm: total,
      remainingPayment: amount,
    } as any);
    return this.findById(id);
  }

  private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise<void> {
    try {
      const vehicle = await this.vehiclesService.findById(vehicleId);
      if (!vehicle.assignedDriverId) {
        this.logger.warn(
          `Vehicle ${vehicleId} has no assigned driver — skipping SMS`,
        );
        return;
      }

      const driver = await this.driversService.findById(
        vehicle.assignedDriverId,
      );
      if (!driver.phoneNumber) {
        this.logger.warn(
          `Driver ${vehicle.assignedDriverId} has no phone number — skipping SMS`,
        );
        return;
      }

      const booking = (
        record as FirstMile & {
          booking?: {
            reference?: string;
            firstMilePickupAddress?: string | null;
            originYard?: { label?: string } | null;
          };
        }
      ).booking;

      const driverName =
        `${driver.firstName ?? ""} ${driver.lastName ?? ""}`.trim();
      const message =
        `Dear ${driverName}, you have been assigned to a first-mile pickup. ` +
        `Booking: ${booking?.reference ?? record.bookingId}. Vehicle: ${vehicle.plateNumber ?? vehicleId}. ` +
        (booking?.firstMilePickupAddress
          ? `Pickup: ${booking.firstMilePickupAddress}. `
          : "") +
        (booking?.originYard?.label
          ? `Destination: ${booking.originYard.label}.`
          : "");

      void this.smsClient.sendSms({
        to: driver.phoneNumber,
        message,
      });

      this.logger.log(
        `SMS queued to driver ${driver.phoneNumber} for vehicle ${vehicleId} assignment`,
      );
    } catch (err) {
      this.logger.error(
        `Failed to notify driver for vehicle ${vehicleId}: ${String(err)}`,
      );
    }
  }

  async remove(id: string): Promise<void> {
    const existing = await this.findById(id);

    // Can't delete once billed.
    const invoices = await this.billing.findBySourceIds('first_mile', [id]);
    if (invoices.length) {
      throw new BadRequestException(
        'Cannot delete a first-mile leg after its invoice is generated',
      );
    }

    // Every vehicle this pickup holds — junction + legacy + container rows.
    const [assignments, allocations] = await Promise.all([
      this.dataSource.manager.find(FirstMileVehicleAssignment, {
        where: { firstMileId: id },
      }),
      this.dataSource.manager.find(FirstMileContainerAllocation, {
        where: { firstMileId: id },
      }),
    ]);
    const vehicleIds = [
      ...new Set(
        [
          ...assignments.map((a) => a.vehicleId),
          ...allocations.map((a) => a.vehicleId),
          existing.vehicleId ?? null,
        ].filter((v): v is string => Boolean(v)),
      ),
    ];

    await this.firstMileRepository.softDelete(id);
    if (assignments.length) {
      await this.dataSource.manager.softDelete(FirstMileVehicleAssignment, { firstMileId: id });
    }

    // Free every vehicle no longer held by another active trip and audit release.
    if (vehicleIds.length) {
      await this.vehiclesService.releaseIfUnused(vehicleIds);
      const bookingRef = await this.resolveBookingRef(existing);
      for (const vehicleId of vehicleIds) {
        const info = await this.vehicleInfo(vehicleId);
        await this.history.record({
          eventType: FleetEventType.MILE_VEHICLE_RELEASED,
          vehicleId,
          firstMileId: id,
          driverId: info.driverId,
          metadata: { mile: 'FIRST', bookingRef, vehiclePlate: info.plate, driverName: info.driverName },
        });
      }
    }
  }
}
