import { BadRequestException, ConflictException } from '@nestjs/common';
import type { DataSource } from 'typeorm';

/** Two 20ft containers fit a truck bed; one 40ft fills it. */
export const MAX_CONTAINERS_PER_TRUCK = 2;

/**
 * What one truck is being asked to carry, and the booking context to judge it
 * against. `sizes` are the container_size labels of `containers`, in any order —
 * only whether a 40ft is present matters.
 */
export interface TruckLoadCheck {
  containers: string[];
  /** Every container number on the booking. Empty means nothing to validate against. */
  bookingContainers: string[];
  sizes: string[];
  /** Containers already riding another truck on this booking. */
  assignedElsewhere?: string[];
}

/**
 * The physical rule for loading one truck, shared by both haulage paths.
 *
 * A customer's own truck and an EDR last-mile truck obey the same physics, but
 * the rule was implemented twice — once in CustomerTruckService, once in
 * LastMileService — along with a byte-identical container-size query. Two copies
 * of one rule drift, and that is exactly how the self-haul guard ended up
 * enforced on one side only.
 */
export function assertTruckLoad({
  containers,
  bookingContainers,
  sizes,
  assignedElsewhere = [],
}: TruckLoadCheck): void {
  if (containers.length > MAX_CONTAINERS_PER_TRUCK) {
    throw new BadRequestException(
      `A truck carries at most ${MAX_CONTAINERS_PER_TRUCK} containers`,
    );
  }

  // With no container list on the booking there is nothing to check membership
  // against — bulk bookings take this path.
  if (!bookingContainers.length) return;

  for (const number of containers) {
    if (!bookingContainers.includes(number)) {
      throw new BadRequestException(
        `Container ${number} is not one of this booking's containers`,
      );
    }
    if (assignedElsewhere.includes(number)) {
      throw new ConflictException(`Container ${number} is already loaded onto another truck`);
    }
  }

  // A 40ft fills the bed, so it travels alone.
  if (containers.length > 1 && sizes.some((size) => size.includes('40'))) {
    throw new BadRequestException(
      'A 40ft container fills the truck — assign only 1 container to this truck',
    );
  }
}

/** Never put more trucks on a booking than it has containers to fill them. */
export function assertTruckCountWithinContainers(
  truckCount: number,
  bookingContainerCount: number,
): void {
  if (bookingContainerCount > 0 && truckCount > bookingContainerCount) {
    throw new BadRequestException(
      `Cannot assign more trucks than containers — this booking has ${bookingContainerCount} container(s) and ${truckCount} truck(s) requested.`,
    );
  }
}

/**
 * How much of a bulk booking is still to be hauled. Counts trucks from BOTH
 * haulage paths — a booking uses one or the other, and the rule ("trucks until
 * no tonnage is left") is the same either way, so a single sum keeps them from
 * disagreeing.
 *
 * Only departed trucks count: tonnage is known once the truck is weighed out.
 */
export async function remainingBulkTons(
  dataSource: DataSource,
  bookingId: string,
): Promise<{ totalTons: number; hauledTons: number; remainingTons: number; complete: boolean }> {
  const [row]: Array<{ totalTons: string | null; hauledTons: string | null }> =
    await dataSource.query(
      `SELECT COALESCE(b.cargo_total_weight_vgm, 0) AS "totalTons",
              COALESCE((
                SELECT SUM(va.net_weight_tons)
                  FROM freight.last_mile_vehicle_assignments va
                  JOIN freight.last_mile lm
                    ON lm.id = va.last_mile_id AND lm.deleted_at IS NULL
                 WHERE lm.booking_id = b.id
                   AND va.deleted_at IS NULL
                   AND va.departed_at IS NOT NULL
              ), 0)
            + COALESCE((
                SELECT SUM(a.net_weight_tons)
                  FROM freight.customer_truck_assignments a
                 WHERE a.booking_id = b.id
                   AND a.deleted_at IS NULL
                   AND a.departed_at IS NOT NULL
              ), 0) AS "hauledTons"
         FROM freight.bookings b
        WHERE b.id = $1 AND b.deleted_at IS NULL`,
      [bookingId],
    );
  const totalTons = Number(row?.totalTons ?? 0);
  const hauledTons = Number(row?.hauledTons ?? 0);
  const remainingTons = Math.max(0, Math.round((totalTons - hauledTons) * 1000) / 1000);
  return { totalTons, hauledTons, remainingTons, complete: totalTons > 0 && remainingTons <= 0 };
}

/** A fully-hauled bulk booking has nothing left for another truck to carry. */
export function assertBulkTonnageRemains(totalTons: number, remainingTons: number): void {
  if (totalTons > 0 && remainingTons <= 0) {
    throw new BadRequestException(
      'This bulk booking is fully hauled — no tonnage left to assign trucks for',
    );
  }
}

/**
 * container_size labels for the given container numbers on a booking. Shared so
 * the two haulage paths read sizes the same way.
 */
export async function bookingContainerSizes(
  dataSource: DataSource,
  bookingId: string,
  numbers: string[],
): Promise<string[]> {
  if (!numbers.length) return [];
  const rows: Array<{ size: string | null }> = await dataSource.query(
    `SELECT bc.container_size AS "size"
       FROM freight.booking_container_units bcu
       JOIN freight.booking_container bc
         ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
      WHERE bc.booking_id = $1
        AND UPPER(bcu.container_number) = ANY($2)
        AND bcu.deleted_at IS NULL`,
    [bookingId, numbers],
  );
  return rows.map((row) => (row.size ?? '').trim());
}
