import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type KeyboardEvent,
  type ReactNode,
} from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
  ActionIcon,
  Alert,
  Badge,
  Box,
  Button,
  Center,
  Divider,
  FileButton,
  Group,
  Loader,
  Modal,
  Paper,
  Select,
  Stack,
  Switch,
  Text,
  Textarea,
  TextInput,
  ThemeIcon,
  Title,
  Tooltip,
} from "@mantine/core";
import {
  AlertCircle,
  AlertTriangle,
  CalendarDays,
  CheckCircle2,
  ChevronLeft,
  FileDown,
  FileText,
  FileUp,
  Flame,
  Link2,
  MapPin,
  Package,
  Receipt,
  Repeat,
  Snowflake,
  X,
} from "lucide-react";
import type { Freight } from "@edr/types";
import {
  CurrencySelector,
  ExportTrainPicker,
  OperationDatePicker,
} from "@edr/ui-common";

import { api } from "@/services/api";
import { PageContainer } from "@/components/page";
import { PageHeader } from "@/components/page/PageHeader";
import {
  contractsService,
  type ConsolidationCandidate,
} from "@/services/contracts.service";
import { bookingsService } from "@/services/bookings.service";
import {
  useContractCapacity,
  useContractDetail,
  useContractMutations,
} from "@/hooks/contracts/useContracts";
import {
  computeGlShipmentTotal,
  formatRateUnit,
  type GlShipmentQuantities,
} from "./gl-booking-form/total";
import { ContractCapacityNotice } from "./gl-booking-form/ContractCapacityNotice";
import {
  downloadContainerImportTemplate,
  parseContainerExcel,
} from "./gl-booking-form/container-excel";
import {
  fieldStyles,
  StepCard,
  StepHeader,
  StepLabel,
} from "./gl-booking-form/form-ui";
import {
  ConsolidationPartnerPanel,
  emptyPartnerLine,
} from "./gl-booking-form/ConsolidationPartnerPanel";
import { ConsolidationPartnerPicker } from "./gl-booking-form/ConsolidationPartnerPicker";

/**
 * Container sizes offered on the parent-booking panel. Fixed rather than taken
 * from this contract's scope: the parent booking is a different customer on a
 * different contract, so its sizes are its own.
 */
const PARTNER_SIZES = ["20ft", "40ft"];

/** All booking-window times are communicated in East Africa Time. */
const EAT_TZ = "Africa/Addis_Ababa";

// ISO 6346: 4-letter owner/category code + 6-digit serial + check digit.
// Same rule the customer portal shipment form enforces.
const ISO_CONTAINER_NUMBER_REGEX = /^[A-Z]{4}\d{7}$/;

/**
 * Every quantity on this form is a non-negative magnitude. A native number
 * input's `min` only constrains its stepper, so swallow the minus key before it
 * can put a negative into the field at all. Same guard as the portal form.
 */
const blockNegative = (event: KeyboardEvent<HTMLInputElement>) => {
  if (event.key === "-") event.preventDefault();
};

interface UnitErrors {
  containerNumber?: string;
  vgmTons?: string;
}

interface LineErrors {
  quantity?: string;
  hazardousQuantity?: string;
  reeferQuantity?: string;
  returnQuantity?: string;
  units?: string;
}

interface BulkErrors {
  quantity?: string;
  hazardous?: string;
  reefer?: string;
}

function fmtWindowOpensAt(iso: string): string {
  const date = new Date(iso).toLocaleDateString("en-GB", {
    weekday: "short",
    day: "numeric",
    month: "short",
    timeZone: EAT_TZ,
  });
  const time = new Date(iso).toLocaleTimeString("en-GB", {
    hour: "2-digit",
    minute: "2-digit",
    hour12: false,
    timeZone: EAT_TZ,
  });
  return `${date} · ${time}`;
}

interface UnitDraft {
  containerNumber: string;
  sealNumber: string;
  vgmTons: string;
  /** Handling is per physical container; the line counts roll these up. */
  isHazardous: boolean;
  isReefer: boolean;
  isReturn: boolean;
}

/** Mirrors the portal shipment form's container line: line-level quantity +
 * hazardous/reefer counts with per-unit number/seal/VGM details. */
interface ContainerLineDraft {
  containerSize: string;
  quantity: string;
  hazardousQuantity: string;
  reeferQuantity: string;
  /** Units of this line shipping with empty-container return (contract WITH_RETURN only). */
  returnQuantity: string;
  units: UnitDraft[];
}

interface BulkDraft {
  cargoWeightTons: string;
  itemCount: string;
  hazardousQuantity: string;
  reeferQuantity: string;
}

function emptyUnit(): UnitDraft {
  return {
    containerNumber: "",
    sealNumber: "",
    vgmTons: "",
    isHazardous: false,
    isReefer: false,
    isReturn: false,
  };
}

function emptyLine(size: string): ContainerLineDraft {
  return {
    containerSize: size,
    quantity: "0",
    hazardousQuantity: "0",
    reeferQuantity: "0",
    returnQuantity: "0",
    units: [],
  };
}

function bulkUnitOfMeasure(
  contract: Freight.IContract,
): "PER_TON" | "PER_ITEM" {
  const hasPerItem = contract.pricingBreakdown?.lineItems?.some(
    (li) => li.unit === "per_item",
  );
  return hasPerItem ? "PER_ITEM" : "PER_TON";
}

export default function GlCreateBookingForm() {
  // With `bookingId` the form runs in COMPLETION mode: the bare instance
  // (auto-initiated by the customer's shipment request) already finished its
  // per-booking customs clearance, and this form supplies the deferred cargo
  // (container numbers, VGM) + binding shipment day. Same window gate, same
  // validation and price confirmation — the submit completes the existing
  // booking instead of creating a new one.
  const { id, bookingId: completeBookingId } = useParams<{
    id: string;
    bookingId?: string;
  }>();
  const [searchParams] = useSearchParams();
  const requestIdParam = searchParams.get("requestId");
  // Rebook: copy an EXPIRED booking's cargo into a fresh booking on the same
  // contract (GL only picks a new schedule). Set by the clearance Rebook action.
  const copyFromParam = searchParams.get("copyFrom");
  const navigate = useNavigate();
  const { data: contract, isLoading } = useContractDetail(id);
  const mutations = useContractMutations(id ?? "");

  // Completion mode without an explicit ?requestId=: find the shipment request
  // that initiated this instance so the quantities still prefill.
  const { data: contractRequests } = useQuery({
    queryKey: ["shipment-requests-for-contract", id],
    queryFn: () => contractsService.listBookingRequests(id!),
    enabled: Boolean(id) && Boolean(completeBookingId) && !requestIdParam,
  });
  const requestId =
    requestIdParam ??
    (completeBookingId
      ? (contractRequests?.find(
          (r) => r.createdBookingId === completeBookingId,
        )?.id ?? null)
      : null);

  const { data: bookingRequest } = useQuery({
    queryKey: ["shipment-request", requestId],
    queryFn: () => contractsService.getBookingRequest(requestId!),
    enabled: Boolean(requestId),
  });

  // The expired booking a Rebook is copying from (its cargo seeds the form).
  const { data: copyFromBooking } = useQuery({
    queryKey: ["rebook-copy-from", copyFromParam],
    queryFn: () => bookingsService.getById(copyFromParam!),
    enabled: Boolean(copyFromParam),
  });

  // The booking being completed — used to name the customer on the price
  // confirmation when a second booking's price is shown beside it.
  const { data: completeBooking } = useQuery({
    queryKey: ["gl-complete-booking", completeBookingId],
    queryFn: () => bookingsService.getById(completeBookingId!),
    enabled: Boolean(completeBookingId),
  });

  // Same window-gating the customer sees: booking is only allowed while a
  // window is OPEN for one of the contract's routes. Intercity contracts are
  // never window-gated — the shipment rides a passing train staff pick later.
  const contractId = contract?.id ?? id;
  const { data: bookingWindows, isLoading: windowsLoading } = useQuery({
    ...api.trainScheduling.contractBookingWindows.queryOptions({
      input: { contractId: contractId ?? "" },
      refetchInterval: 60_000,
    }),
    enabled: Boolean(contractId),
  });

  const windowOpen = useMemo(
    () => (bookingWindows ?? []).some((w) => w.isOpenNow),
    [bookingWindows],
  );

  // Next future window across all routes, used for the "next window" notice —
  // the next moment booking OPENS (chronological), which may belong to a
  // later-departing train. Departure-first ordering here named the soonest
  // train's later opening as "next" while another lane opened earlier.
  const nextWindow = useMemo(() => {
    const now = Date.now();
    return (bookingWindows ?? [])
      .filter((w) => w.windowOpensAt && new Date(w.windowOpensAt).getTime() > now)
      .sort(
        (a, b) =>
          new Date(a.windowOpensAt!).getTime() - new Date(b.windowOpensAt!).getTime(),
      )[0];
  }, [bookingWindows]);

  const [scheduledDate, setScheduledDate] = useState("");
  // EXPORT rail completion: the specific train GL picks for the shipment day.
  const [trainScheduleId, setTrainScheduleId] = useState("");
  const [contractRouteId, setContractRouteId] = useState<string | null>(null);
  const [notes, setNotes] = useState("");
  // IMPORT bookings pick ETB or USD — starts empty so the choice is
  // deliberate (required before pricing). Everything else is forced to ETB.
  const [paymentCurrency, setPaymentCurrency] = useState<"USD" | "ETB" | "">("");
  // What the containers carry — captured per booking (moved off the contract).
  const [cargoDescription, setCargoDescription] = useState("");
  const [containerLines, setContainerLines] = useState<ContainerLineDraft[]>([]);
  const [bulk, setBulk] = useState<BulkDraft>({
    cargoWeightTons: "",
    itemCount: "",
    hazardousQuantity: "0",
    reeferQuantity: "0",
  });
  const [withReturn, setWithReturn] = useState(false);
  const [prefilled, setPrefilled] = useState(false);
  const [priceOpen, setPriceOpen] = useState(false);
  // ── Odd-20ft shared wagon (customs / Path B) ──────────────────────────────
  // An odd 20ft total leaves one container unpaired. On a customs contract GL
  // resolves that here by linking a second booking that is also odd — two odd
  // counts always sum to even — completing both together onto the shared wagon.
  const [consolidateOdd, setConsolidateOdd] = useState(false);
  // Set once GL flips the toggle by hand, so the auto-on effect below never
  // re-opens a panel GL deliberately closed.
  const consolidateTouchedRef = useRef(false);
  const [partnerPickerOpen, setPartnerPickerOpen] = useState(false);
  const [partner, setPartner] = useState<ConsolidationCandidate | null>(null);
  const [partnerLines, setPartnerLines] = useState<ContainerLineDraft[]>([]);
  const [partnerCargoDescription, setPartnerCargoDescription] = useState("");
  const seededRef = useRef(false);
  const returnSeededRef = useRef(false);

  // Seed the equipment-return toggle from the contract exactly once (also when
  // the form is prefilled from a shipment request); GL can flip it per shipment.
  useEffect(() => {
    if (!contract || returnSeededRef.current) return;
    returnSeededRef.current = true;
    setWithReturn(contract.equipmentReturn === "WITH_RETURN");
  }, [contract]);

  const isContainer = contract?.freightType === "CONTAINER";
  // The contract gates the empty-container return service — like hazardous.
  // WITH_RETURN contracts capture a per-line return quantity instead of the
  // legacy booking-level toggle; other contracts cannot switch it on.
  const contractWithReturn =
    isContainer && contract?.equipmentReturn === "WITH_RETURN";
  // Legacy contracts (no equipment return chosen at creation) keep the old
  // booking-level toggle.
  const legacyReturnToggle = isContainer && !contract?.equipmentReturn;
  /**
   * Handling switches offered on each container row — only the services this
   * contract was created with, since the server rejects the others.
   */
  const handlingColumns = (
    [
      contract?.isHazardous && {
        key: "isHazardous",
        label: "Hazardous",
        icon: <Flame size={14} />,
        color: "#C0392B",
      },
      contract?.isReefer && {
        key: "isReefer",
        label: "Refrigerated",
        icon: <Snowflake size={14} />,
        color: "#2E5B96",
      },
      contractWithReturn && {
        key: "isReturn",
        label: "With return",
        icon: <Repeat size={14} />,
        color: "#0A6F4D",
      },
    ] as Array<
      | false
      | undefined
      | { key: keyof UnitDraft; label: string; icon: ReactNode; color: string }
    >
  ).filter(Boolean) as Array<{
    key: "isHazardous" | "isReefer" | "isReturn";
    label: string;
    icon: ReactNode;
    color: string;
  }>;
  // Intercity shipments ride a passing import/export train staff pick at
  // finalize time — no shipment day is chosen and no window gate applies.
  const isIntercity = contract?.tradeDirection === "DOMESTIC";
  // USD billing is offered on import traffic only — export and domestic
  // shipments are always invoiced in ETB.
  const isImport = contract?.tradeDirection === "IMPORT";

  // ONE_TIME split-remainder mode: a previous booking on this contract was
  // split on train capacity, so the capacity endpoint reports the outstanding
  // remainder — the EXACT quantity this booking must take (API-enforced).
  const { data: splitCapacityLines = [] } = useContractCapacity(
    contract?.contractKind === "ONE_TIME" ? contract?.id : undefined,
  );
  const remainderLines = useMemo(
    () =>
      contract?.contractKind === "ONE_TIME"
        ? splitCapacityLines.filter((l) => (l.remaining ?? 0) > 0)
        : [],
    [contract?.contractKind, splitCapacityLines],
  );
  const remainderMode = remainderLines.length > 0;

  // Bulk: prefill the exact outstanding remainder once — a different amount
  // would be rejected at create anyway.
  const remainderSeededRef = useRef(false);
  useEffect(() => {
    if (!remainderMode || isContainer || remainderSeededRef.current) return;
    remainderSeededRef.current = true;
    setBulk((b) =>
      b.cargoWeightTons || b.itemCount
        ? b
        : { ...b, cargoWeightTons: String(remainderLines[0].remaining) },
    );
  }, [remainderMode, isContainer, remainderLines]);

  const remainderNotice = remainderMode ? (
    <Alert
      color="yellow"
      variant="light"
      radius="md"
      icon={<AlertTriangle size={16} />}
      title="This booking must take the whole split remainder"
    >
      <Text fz={13}>
        The previous booking on this one-time contract was split — the paid
        part is shipping and the rest returned to the contract. This booking
        must cover exactly the remaining{" "}
        {remainderLines
          .map((l) =>
            isContainer
              ? `${l.remaining} × ${l.containerSize ?? "container"}`
              : `${l.remaining} tons`,
          )
          .join(", ")}
        . A different quantity will be rejected, and no other booking can be
        created on this contract.
      </Text>
    </Alert>
  ) : null;
  const routes = useMemo(
    () =>
      [...(contract?.routes ?? [])].sort((a, b) => a.sortOrder - b.sortOrder),
    [contract?.routes],
  );
  const multiRoute = routes.length > 1;

  const containerSizes = useMemo(() => {
    const sizes = new Set<string>();
    (contract?.cargoScope ?? []).forEach((s) => {
      if (s.containerSize) sizes.add(s.containerSize);
    });
    return [...sizes];
  }, [contract?.cargoScope]);

  useEffect(() => {
    if (!bookingRequest || prefilled) return;
    // A rebook (?copyFrom=) seeds from the expired booking's real cargo —
    // richer than the request's bare quantities. Let that seed win the race.
    if (copyFromParam) return;
    setPrefilled(true);
    const lines = bookingRequest.requestedLines ?? {};
    if (lines.containers?.length) {
      setContainerLines(
        lines.containers.map((c) => ({
          containerSize: c.containerSize,
          quantity: String(Math.max(1, c.quantity)),
          hazardousQuantity: String(c.hazardousQuantity ?? 0),
          reeferQuantity: String(c.reeferQuantity ?? 0),
          returnQuantity: "0",
          units: Array.from({ length: Math.max(1, c.quantity) }, emptyUnit),
        })),
      );
    } else if (lines.bulk) {
      setBulk({
        cargoWeightTons:
          lines.bulk.cargoWeightTons != null
            ? String(lines.bulk.cargoWeightTons)
            : "",
        itemCount:
          lines.bulk.itemCount != null ? String(lines.bulk.itemCount) : "",
        hazardousQuantity: String(lines.bulk.hazardousQuantity ?? 0),
        reeferQuantity: "0",
      });
    }
    if (bookingRequest.contractRouteId)
      setContractRouteId(bookingRequest.contractRouteId);
    if (bookingRequest.notes) setNotes(bookingRequest.notes);
  }, [bookingRequest, prefilled]);

  // Rebook seed: copy the source booking's container lines once. (Bulk weight /
  // item count isn't on the booking payload yet, so bulk rebooks fall through to
  // the normal contract seed and GL re-enters the quantity.)
  useEffect(() => {
    if (!copyFromBooking || prefilled) return;
    const lines = copyFromBooking.bookingContainers ?? [];
    if (!lines.length) return;
    // The booking stores a numeric sizeFt (20) but the contract scope — and the
    // create payload the server validates — uses its own size strings ("20ft").
    // Seed with the scope's string so the rebook payload matches what a fresh
    // form entry would send.
    const scopeSizeForFt = (sizeFt: number | null | undefined): string => {
      if (sizeFt == null) return "";
      return (
        containerSizes.find((s) => parseInt(s, 10) === Number(sizeFt)) ??
        `${sizeFt}ft`
      );
    };
    setPrefilled(true);
    // Rebook carries the expired booking's cargo description forward.
    if (copyFromBooking.cargoFreeText) {
      setCargoDescription(copyFromBooking.cargoFreeText);
    }
    setContainerLines(
      lines.map((c) => {
        const qty = Math.max(1, c.quantity);
        // Carry the persisted per-unit details (numbers, seals, VGM, handling)
        // when the source booking has them — a rebooked EXPIRED booking does,
        // and its cargo is fixed server-side anyway.
        const units: UnitDraft[] =
          c.units?.length === qty
            ? c.units.map((u) => ({
                containerNumber: u.containerNumber ?? "",
                sealNumber: u.sealNumber ?? "",
                vgmTons: u.vgmTons != null ? String(u.vgmTons) : "",
                isHazardous: Boolean(u.isHazardous),
                isReefer: Boolean(u.isReefer),
                isReturn: Boolean(u.isReturn),
              }))
            : Array.from({ length: qty }, emptyUnit);
        return {
          containerSize: scopeSizeForFt(c.containerType?.sizeFt),
          quantity: String(qty),
          hazardousQuantity: String(units.filter((u) => u.isHazardous).length),
          reeferQuantity: String(units.filter((u) => u.isReefer).length),
          returnQuantity: String(units.filter((u) => u.isReturn).length),
          units,
        };
      }),
    );
  }, [copyFromBooking, prefilled, containerSizes]);

  // Seed one shipment line per contracted size exactly once — same seeding the
  // portal form does. Subsequent renders reuse the lines.
  useEffect(() => {
    if (!contract || prefilled || seededRef.current) return;
    seededRef.current = true;
    if (isContainer && containerSizes.length > 0 && containerLines.length === 0) {
      setContainerLines(containerSizes.map(emptyLine));
    }
  }, [contract, prefilled, isContainer, containerSizes, containerLines.length]);

  const quantities: GlShipmentQuantities = useMemo(
    () => ({
      isContainer: Boolean(isContainer),
      containers: containerLines.map((l) => ({
        containerSize: l.containerSize,
        quantity: Number(l.quantity || 0),
        hazardousQuantity: Number(l.hazardousQuantity || 0),
        reeferQuantity: Number(l.reeferQuantity || 0),
        returnQuantity: Number(l.returnQuantity || 0),
      })),
      bulkQuantity: Number(bulk.cargoWeightTons || bulk.itemCount || 0),
      bulkHazardousQuantity: Number(bulk.hazardousQuantity || 0),
      bulkReeferQuantity: Number(bulk.reeferQuantity || 0),
    }),
    [isContainer, containerLines, bulk],
  );

  const priceTotal = useMemo(
    () => (contract ? computeGlShipmentTotal(contract, quantities) : null),
    [contract, quantities],
  );

  const selectedRoute = useMemo(
    () => routes.find((r) => r.id === contractRouteId) ?? routes[0],
    [routes, contractRouteId],
  );

  // Read the cargo entered above so the day list reflects what can actually be
  // shipped (matching wagons + open train capacity) — portal parity.
  const cargoQuery = useMemo<Freight.AvailableDaysForCargoQuery | null>(() => {
    if (!selectedRoute?.originYardId || !selectedRoute?.destinationYardId)
      return null;
    if (isContainer) {
      const containers = containerLines
        .map((l) => ({
          containerSize: l.containerSize,
          quantity: Number(l.quantity || 0),
        }))
        .filter((c) => c.quantity >= 1);
      if (containers.length === 0) return null;
      return {
        originYardId: selectedRoute.originYardId,
        destinationYardId: selectedRoute.destinationYardId,
        freightType: "CONTAINER",
        containers,
      };
    }
    const tons = Number(bulk.cargoWeightTons || 0);
    if (tons <= 0) return null;
    return {
      originYardId: selectedRoute.originYardId,
      destinationYardId: selectedRoute.destinationYardId,
      freightType: "BULK",
      cargoTypeCode:
        contract?.pricingBreakdown?.lineItems?.find((li) => li.cargoTypeCode)
          ?.cargoTypeCode ?? undefined,
      totalWeightTons: tons,
    };
  }, [selectedRoute, isContainer, containerLines, bulk.cargoWeightTons, contract?.pricingBreakdown]);

  const { data: availableDays, isLoading: daysLoading } = useQuery({
    ...api.trainScheduling.availableDaysForCargo.queryOptions({
      input: cargoQuery ?? { freightType: "BULK" as const },
    }),
    enabled: cargoQuery !== null && !isIntercity,
  });

  // EXPORT completes pick the TRAIN, not just the day (portal parity). Only
  // when completing an initiated instance — a fresh GL create goes through
  // clearance and picks its train there.
  const isExportPick =
    contract?.tradeDirection === "EXPORT" && Boolean(completeBookingId);
  const wagonsEstimate = useMemo(() => {
    if (!isContainer) return undefined;
    const ft20 = containerLines
      .filter((l) => parseInt(l.containerSize, 10) === 20)
      .reduce((s, l) => s + Number(l.quantity || 0), 0);
    const ft40 = containerLines
      .filter((l) => parseInt(l.containerSize, 10) === 40)
      .reduce((s, l) => s + Number(l.quantity || 0), 0);
    const wagons = Math.ceil(ft20 / 2) + ft40;
    return wagons > 0 ? wagons : undefined;
  }, [isContainer, containerLines]);
  const exportTrainsQuery = useQuery({
    ...api.trainScheduling.exportTrains.queryOptions({
      input: {
        bookingId: completeBookingId ?? "",
        date: scheduledDate,
        cargo: {
          containerSizes: isContainer
            ? containerLines
                .filter((l) => Number(l.quantity || 0) >= 1)
                .map((l) => l.containerSize)
            : undefined,
          cargoTypeCode: !isContainer
            ? (contract?.pricingBreakdown?.lineItems?.find(
                (li) => li.cargoTypeCode,
              )?.cargoTypeCode ?? undefined)
            : undefined,
          wagons: wagonsEstimate,
        },
      },
    }),
    enabled: isExportPick && Boolean(scheduledDate),
  });

  /**
   * Line handling totals are a roll-up of the per-container switches — the
   * count is however many containers ticked each service. Recomputed on every
   * unit change so the price estimate and payload follow the switches.
   */
  const withDerivedCounts = (line: ContainerLineDraft): ContainerLineDraft => ({
    ...line,
    hazardousQuantity: String(line.units.filter((u) => u.isHazardous).length),
    reeferQuantity: String(line.units.filter((u) => u.isReefer).length),
    returnQuantity: String(line.units.filter((u) => u.isReturn).length),
  });

  // Keep the units array length in sync with the entered quantity.
  const syncUnits = (lineIdx: number, qty: number) => {
    setContainerLines((prev) =>
      prev.map((line, i) => {
        if (i !== lineIdx) return line;
        const next = [...line.units];
        while (next.length < qty) next.push(emptyUnit());
        next.length = Math.max(0, qty);
        return withDerivedCounts({ ...line, units: next });
      }),
    );
  };

  const patchLine = (idx: number, patch: Partial<ContainerLineDraft>) =>
    setContainerLines((prev) =>
      prev.map((l, i) => (i === idx ? { ...l, ...patch } : l)),
    );

  const patchUnit = (
    lineIdx: number,
    unitIdx: number,
    patch: Partial<UnitDraft>,
  ) =>
    setContainerLines((prev) =>
      prev.map((l, i) =>
        i === lineIdx
          ? withDerivedCounts({
              ...l,
              units: l.units.map((u, j) => (j === unitIdx ? { ...u, ...patch } : u)),
            })
          : l,
      ),
    );

  // Drop one container row and shrink quantity to match — the inverse of
  // syncUnits growing the array when quantity goes up.
  const removeUnit = (lineIdx: number, unitIdx: number) =>
    setContainerLines((prev) =>
      prev.map((l, i) => {
        if (i !== lineIdx) return l;
        const units = l.units.filter((_, j) => j !== unitIdx);
        return withDerivedCounts({ ...l, quantity: String(units.length), units });
      }),
    );

  // Same client-side validation as the customer portal shipment form
  // (new-shipment-form/schema.ts): ISO container numbers unique within the
  // shipment, positive VGM per unit, hazardous/reefer counts bounded by the
  // line quantity; bulk needs a positive quantity with hazardous/reefer
  // portions bounded by it. Errors only show after a submit attempt.
  const [showErrors, setShowErrors] = useState(false);

  // Excel import: one row per container. All-or-nothing — a file with any bad
  // row is rejected with row-numbered errors so nothing is silently dropped.
  const [importErrors, setImportErrors] = useState<string[]>([]);
  const [importSummary, setImportSummary] = useState<string | null>(null);
  const importResetRef = useRef<(() => void) | null>(null);
  const excelOpts = {
    allowedSizes: containerSizes,
    includeHazardous: contract?.isHazardous ?? false,
    includeReefer: contract?.isReefer ?? false,
    includeReturn: contractWithReturn,
  };

  const handleImportFile = async (file: File | null) => {
    // Reset the hidden input so re-picking the same (fixed) file re-fires.
    importResetRef.current?.();
    if (!file) return;
    const { rows, errors } = await parseContainerExcel(file, excelOpts);
    if (errors.length > 0) {
      setImportSummary(null);
      setImportErrors(errors);
      return;
    }
    // Replace only the lines for sizes present in the file; a contracted size
    // the file omits keeps whatever was already entered for it.
    setContainerLines((prev) =>
      containerSizes.map((size) => {
        const imported = rows.filter((r) => r.containerSize === size);
        if (imported.length === 0) {
          return prev.find((l) => l.containerSize === size) ?? emptyLine(size);
        }
        return {
          containerSize: size,
          quantity: String(imported.length),
          hazardousQuantity: String(imported.filter((r) => r.hazardous).length),
          reeferQuantity: String(imported.filter((r) => r.reefer).length),
          returnQuantity: String(imported.filter((r) => r.withReturn).length),
          // The spreadsheet marks handling per row — carry it onto the
          // container it belongs to rather than collapsing it to a line count.
          units: imported.map((r) => ({
            containerNumber: r.containerNumber,
            sealNumber: r.sealNumber,
            vgmTons: String(r.vgmTons),
            isHazardous: Boolean(r.hazardous),
            isReefer: Boolean(r.reefer),
            isReturn: Boolean(r.withReturn),
          })),
        };
      }),
    );
    setImportErrors([]);
    setShowErrors(false);
    setImportSummary(`Imported ${rows.length} container(s) from ${file.name}.`);
  };

  const unitErrors = useMemo<UnitErrors[][]>(() => {
    if (!isContainer) return [];
    const numberCounts = new Map<string, number>();
    containerLines.forEach((line) =>
      line.units.forEach((u) => {
        const key = u.containerNumber.trim().toUpperCase();
        if (!key) return;
        numberCounts.set(key, (numberCounts.get(key) ?? 0) + 1);
      }),
    );
    return containerLines.map((line) =>
      line.units.map((u) => {
        const errs: UnitErrors = {};
        const key = u.containerNumber.trim().toUpperCase();
        if (!key) {
          errs.containerNumber = "Container number is required.";
        } else if (!ISO_CONTAINER_NUMBER_REGEX.test(key)) {
          errs.containerNumber =
            "Enter a valid ISO container number (e.g. ABCD1234567).";
        } else if ((numberCounts.get(key) ?? 0) > 1) {
          errs.containerNumber = "Duplicate container number in this shipment.";
        }
        const vgm = Number(u.vgmTons);
        if (u.vgmTons.trim() === "" || Number.isNaN(vgm) || vgm <= 0) {
          errs.vgmTons = "Enter a valid VGM.";
        }
        return errs;
      }),
    );
  }, [isContainer, containerLines]);

  const lineErrors = useMemo<LineErrors[]>(() => {
    if (!isContainer || !contract) return [];
    // A line can be 0 (the contract covers both sizes; a booking may only need
    // one) but the booking as a whole needs at least one container — anchor
    // that error on the first line's quantity so it renders in the field.
    const totalQty = containerLines.reduce(
      (sum, l) => sum + Math.max(0, Number(l.quantity) || 0),
      0,
    );
    return containerLines.map((line, idx) => {
      const errs: LineErrors = {};
      const qty = Number(line.quantity || 0);
      if (line.quantity.trim() === "") {
        errs.quantity = "Quantity is required.";
      } else if (Number.isNaN(qty) || qty < 0) {
        errs.quantity = "Enter 0 or more.";
      } else if (idx === 0 && totalQty < 1) {
        errs.quantity = "Book at least one container (either size).";
      } else if (qty >= 1 && line.units.length < qty) {
        errs.units = `Enter details for all ${qty} container(s).`;
      }
      if (contract.isHazardous) {
        const h = Number(line.hazardousQuantity || 0);
        if (Number.isNaN(h) || h < 0) {
          errs.hazardousQuantity = "Enter a valid hazardous quantity.";
        } else if (h > qty) {
          errs.hazardousQuantity = `Can't exceed the ${qty} container(s) in this line.`;
        }
      }
      if (contract.isReefer) {
        const r = Number(line.reeferQuantity || 0);
        if (Number.isNaN(r) || r < 0) {
          errs.reeferQuantity = "Enter a valid refrigerated quantity.";
        } else if (r > qty) {
          errs.reeferQuantity = `Can't exceed the ${qty} container(s) in this line.`;
        }
      }
      if (contractWithReturn) {
        const w = Number(line.returnQuantity || 0);
        if (Number.isNaN(w) || w < 0) {
          errs.returnQuantity = "Enter a valid return quantity.";
        } else if (w > qty) {
          errs.returnQuantity = `Can't exceed the ${qty} container(s) in this line.`;
        }
      }
      return errs;
    });
  }, [isContainer, contract, containerLines, contractWithReturn]);

  // 20ft containers ride two per wagon, so an odd total leaves one unpaired and
  // the booking can never be planned. The server rejects it too (the price
  // modal's `pairingErrors`), but that only lands after GL has filled the whole
  // form — mirror the customer portal (new-booking-form/schema.ts `calcWagons`)
  // and block it inline instead. Size strings arrive as "20ft" from the contract
  // scope but as a bare "20" from the rebook seed, so match on the leading digits.
  const ft20Total = useMemo(() => {
    if (!isContainer) return 0;
    return containerLines
      .filter((l) => parseInt(l.containerSize, 10) === 20)
      .reduce((sum, l) => sum + Number(l.quantity || 0), 0);
  }, [isContainer, containerLines]);
  const hasOdd20ft = ft20Total % 2 === 1;

  // Only a customs (Path B) instance being COMPLETED by GL can use the shared
  // wagon: it is GL, not the customer, who links the two bookings. Anything else
  // falls through to the server's automatic consolidation gate.
  const oddConsolidationAvailable = Boolean(
    completeBookingId && isContainer && contract?.customsClearingEnabled,
  );

  // Auto-on: entering an odd 20ft total opens the consolidation panel by itself,
  // once. GL can still switch it off — then odd is blocked exactly as before.
  useEffect(() => {
    if (!oddConsolidationAvailable) return;
    if (consolidateTouchedRef.current) return;
    if (hasOdd20ft) setConsolidateOdd(true);
  }, [oddConsolidationAvailable, hasOdd20ft]);

  // Clear the partner as soon as the panel closes or stops applying, so a
  // leftover selection can never ride along into a plain single-booking submit.
  useEffect(() => {
    if (consolidateOdd && oddConsolidationAvailable) return;
    setPartner(null);
    setPartnerLines([]);
    setPartnerCargoDescription("");
  }, [consolidateOdd, oddConsolidationAvailable]);

  const consolidationActive =
    oddConsolidationAvailable && consolidateOdd && hasOdd20ft;

  // Once a parent booking is linked, each booking's cargo is entered under its
  // own labelled heading so it is clear which containers belong to whom.
  const splitView = Boolean(consolidationActive && partner);

  const candidatesQuery = useQuery({
    queryKey: ["consolidation-candidates", id, completeBookingId],
    queryFn: () =>
      contractsService.listConsolidationCandidates(
        id ?? "",
        completeBookingId ?? "",
      ),
    enabled:
      partnerPickerOpen && Boolean(id) && Boolean(completeBookingId),
  });

  const bulkUom = contract ? bulkUnitOfMeasure(contract) : "PER_TON";

  const bulkErrors = useMemo<BulkErrors>(() => {
    if (isContainer) return {};
    const errs: BulkErrors = {};
    const qty =
      bulkUom === "PER_ITEM"
        ? Number(bulk.itemCount || 0)
        : Number(bulk.cargoWeightTons || 0);
    if (Number.isNaN(qty) || qty <= 0) {
      errs.quantity = "Enter a quantity greater than 0.";
    }
    const h = Number(bulk.hazardousQuantity || 0);
    if (Number.isNaN(h) || h < 0) {
      errs.hazardous = "Enter a valid hazardous quantity.";
    } else if (qty > 0 && h > qty) {
      errs.hazardous = `Can't exceed the cargo quantity (${qty}).`;
    }
    const r = Number(bulk.reeferQuantity || 0);
    if (Number.isNaN(r) || r < 0) {
      errs.reefer = "Enter a valid refrigerated quantity.";
    } else if (qty > 0 && r > qty) {
      errs.reefer = `Can't exceed the cargo quantity (${qty}).`;
    }
    return errs;
  }, [isContainer, bulk, bulkUom]);

  const dateError =
    !isIntercity && !scheduledDate ? "Select a shipment date." : undefined;
  const routeError =
    multiRoute && !contractRouteId ? "Select a route." : undefined;

  const cargoDescriptionError =
    isContainer && !cargoDescription.trim()
      ? "Describe the cargo carried in the containers."
      : undefined;

  const cargoValid = isContainer
    ? lineErrors.every(
        (e) =>
          !e.quantity &&
          !e.units &&
          !e.hazardousQuantity &&
          !e.reeferQuantity &&
          !e.returnQuantity,
      ) &&
      unitErrors.every((line) =>
        line.every((e) => !e.containerNumber && !e.vgmTons),
      ) &&
      !cargoDescriptionError
    : !bulkErrors.quantity && !bulkErrors.hazardous && !bulkErrors.reefer;

  // COMPLETION never blocks on an odd 20ft total: a customs instance can share
  // the wagon via the manual pair (consolidationActive), and anything else is
  // auto-paired or parked as PENDING_CONSOLIDATION by the server's
  // consolidation gate. Creating a booking from scratch keeps the block.
  const oddBlocksSubmit = hasOdd20ft && !completeBookingId;

  // Partner side: a linked partner must be picked, carry an odd 20ft count of
  // its own (odd + odd = even fills the wagon) and have complete unit details.
  const partnerFt20Total = useMemo(() => {
    if (!consolidationActive) return 0;
    return partnerLines
      .filter((l) => parseInt(l.containerSize, 10) === 20)
      .reduce((sum, l) => sum + Number(l.quantity || 0), 0);
  }, [consolidationActive, partnerLines]);

  const partnerError = useMemo<string | undefined>(() => {
    if (!consolidationActive) return undefined;
    if (!partner) return "Select the booking that shares this wagon.";
    const totalQty = partnerLines.reduce(
      (sum, l) => sum + Math.max(0, Number(l.quantity) || 0),
      0,
    );
    if (totalQty < 1) {
      return `Enter the containers for ${partner.reference}.`;
    }
    if (partnerFt20Total % 2 === 0) {
      return `${partner.reference} must also carry an odd number of 20ft containers so the two bookings fill whole wagons together (it has ${partnerFt20Total}).`;
    }
    const incomplete = partnerLines.some((line) => {
      const qty = Number(line.quantity || 0);
      return qty >= 1 && line.units.length < qty;
    });
    if (incomplete) {
      return `Enter the container details for all of ${partner.reference}'s containers.`;
    }
    const badUnit = partnerLines.some((line) =>
      line.units.some(
        (u) =>
          !ISO_CONTAINER_NUMBER_REGEX.test(u.containerNumber.trim().toUpperCase()) ||
          !(Number(u.vgmTons) > 0),
      ),
    );
    if (badUnit) {
      return `Every ${partner.reference} container needs a valid container number and a VGM above 0.`;
    }
    if (!partnerCargoDescription.trim()) {
      return `Describe the cargo carried in ${partner.reference}'s containers.`;
    }
    return undefined;
  }, [
    consolidationActive,
    partner,
    partnerLines,
    partnerFt20Total,
    partnerCargoDescription,
  ]);

  // Only IMPORT actually chooses — the rest bill ETB regardless of the state.
  const effectiveCurrency: "USD" | "ETB" =
    isImport && paymentCurrency ? paymentCurrency : "ETB";
  const currencyError =
    isImport && !paymentCurrency
      ? "Select the billing currency for this booking."
      : undefined;

  const formValid =
    cargoValid &&
    !oddBlocksSubmit &&
    !dateError &&
    !routeError &&
    !partnerError &&
    !currencyError;

  /** The create-booking DTO from the current form state — shared by the
   * authoritative price preview and the actual submit so what GL confirms is
   * exactly what gets booked. Mirrors the portal form's buildDto. */
  const buildPayload = (): Freight.CreateBookingUnderContractDto | null => {
    if (!contract) return null;

    const payload: Freight.CreateBookingUnderContractDto = {
      ...(contractRouteId ? { contractRouteId } : {}),
      paymentCurrency: effectiveCurrency,
      // Intercity bookings carry no date — staff assign a passing train later.
      ...(scheduledDate
        ? { scheduledDate: new Date(scheduledDate).toISOString() }
        : {}),
      // EXPORT rail: lock the booking onto the picked train.
      ...(trainScheduleId ? { trainScheduleId } : {}),
      ...(notes.trim() ? { notes: notes.trim() } : {}),
      // Equipment return: WITH_RETURN contracts derive it server-side from the
      // per-line return quantities; only legacy contracts (no value chosen at
      // creation) still send the booking-level toggle. Bulk keeps the default.
      ...(legacyReturnToggle
        ? { equipmentReturn: withReturn ? "WITH_RETURN" : "WITHOUT_RETURN" }
        : {}),
    };

    if (isContainer) {
      // What the containers carry — captured per booking, not on the contract.
      if (cargoDescription.trim()) payload.cargoFreeText = cargoDescription.trim();
      payload.containers = containerLines
        .filter((l) => Number(l.quantity) >= 1)
        .map((l) => ({
          containerSize: l.containerSize,
          quantity: Number(l.quantity),
          hazardousQuantity: Number(l.hazardousQuantity || 0) || undefined,
          reeferQuantity: Number(l.reeferQuantity || 0) || undefined,
          ...(contractWithReturn
            ? { returnQuantity: Number(l.returnQuantity || 0) }
            : {}),
          units: l.units.map((u) => ({
            containerNumber: u.containerNumber.trim().toUpperCase(),
            ...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
            vgmTons: Number(u.vgmTons) || 0,
            // Per-container handling — the server rolls these into the line
            // counts and bills each surcharge on the ticked containers only.
            isHazardous: Boolean(u.isHazardous),
            isReefer: Boolean(u.isReefer),
            ...(contractWithReturn ? { isReturn: Boolean(u.isReturn) } : {}),
          })),
        }));
    } else {
      payload.bulkLines = [
        {
          cargoTypeId: contract.cargoScope?.[0]?.cargoTypeId ?? null,
          ...(bulk.cargoWeightTons !== ""
            ? { cargoWeightTons: Number(bulk.cargoWeightTons) }
            : {}),
          ...(bulk.itemCount !== "" ? { itemCount: Number(bulk.itemCount) } : {}),
          hazardousQuantity: Number(bulk.hazardousQuantity || 0) || undefined,
          reeferQuantity: Number(bulk.reeferQuantity || 0) || undefined,
        },
      ];
    }

    return payload;
  };

  /**
   * Completion DTO for the partner half of a shared wagon. Route, day and train
   * are deliberately copied from THIS booking: the two bookings ride the same
   * wagon, so they must ride the same train on the same day. Only the cargo and
   * the billing currency belong to the partner.
   */
  const buildPartnerPayload = (): Freight.CreateBookingUnderContractDto | null => {
    if (!partner || !consolidationActive) return null;

    const payload: Freight.CreateBookingUnderContractDto = {
      paymentCurrency: effectiveCurrency,
      ...(scheduledDate
        ? { scheduledDate: new Date(scheduledDate).toISOString() }
        : {}),
      ...(trainScheduleId ? { trainScheduleId } : {}),
      ...(partnerCargoDescription.trim()
        ? { cargoFreeText: partnerCargoDescription.trim() }
        : {}),
      containers: partnerLines
        .filter((l) => Number(l.quantity) >= 1)
        .map((l) => ({
          containerSize: l.containerSize,
          quantity: Number(l.quantity),
          hazardousQuantity: Number(l.hazardousQuantity || 0) || undefined,
          reeferQuantity: Number(l.reeferQuantity || 0) || undefined,
          units: l.units.map((u) => ({
            containerNumber: u.containerNumber.trim().toUpperCase(),
            ...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
            vgmTons: Number(u.vgmTons) || 0,
            isHazardous: Boolean(u.isHazardous),
            isReefer: Boolean(u.isReefer),
          })),
        })),
    };

    return payload;
  };

  // Authoritative price preview (same pricing pass the booking persists at
  // create): rail freight + first/last mile + overweight + every surcharge,
  // plus the hard-block checks (20ft pairing, max capacity, container numbers
  // already booked on the same train). Fired when the price modal opens; the
  // modal falls back to the contract unit-rate estimate while it loads.
  const validateShipmentMutation = useMutation({
    mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
      contractsService.validateShipment(id ?? "", dto, completeBookingId),
  });
  const validation = validateShipmentMutation.data ?? null;

  // The partner is priced against ITS OWN contract, so the two totals shown in
  // the confirm modal are each customer's real bill — nobody pays for the other.
  const validatePartnerMutation = useMutation({
    mutationFn: (input: {
      contractId: string;
      bookingId: string;
      dto: Freight.CreateBookingUnderContractDto;
    }) =>
      contractsService.validateShipment(
        input.contractId,
        input.dto,
        input.bookingId,
      ),
  });
  const partnerValidation = validatePartnerMutation.data ?? null;

  const serverTotal = useMemo(() => {
    const items = validation?.lineItems;
    if (!items?.length) return null;
    return {
      currency: validation?.currency ?? priceTotal?.currency ?? "ETB",
      lines: items.map((li) => ({
        label: li.description,
        unitPrice: li.unitAmount,
        unit: li.unit.toLowerCase(),
        quantity: li.quantity,
        amount: li.amount,
      })),
      total:
        validation?.totalAmount ?? items.reduce((s, l) => s + l.amount, 0),
    };
  }, [validation, priceTotal]);

  const overweightSurchargeAmount = validation?.overweightSurchargeAmount ?? 0;
  const pairingErrors = validation?.pairingErrors ?? [];
  const capacityErrors = validation?.capacityErrors ?? [];
  const containerClashErrors = validation?.containerClashErrors ?? [];
  const spaceErrors = validation?.spaceErrors ?? [];
  const overweightLines = validation?.overweightLines ?? [];

  // Fallback while the server preview loads: the contract's frozen unit rates
  // with the overweight surcharge folded in — same fallback the portal shows.
  const displayTotal = useMemo(() => {
    if (serverTotal) return serverTotal;
    if (!priceTotal) return null;
    if (!(overweightSurchargeAmount > 0)) return priceTotal;
    return {
      ...priceTotal,
      lines: [
        ...priceTotal.lines,
        {
          label: "Overweight surcharge",
          unitPrice: overweightSurchargeAmount,
          unit: "flat" as const,
          quantity: 1,
          amount: overweightSurchargeAmount,
        },
      ],
      total: priceTotal.total + overweightSurchargeAmount,
    };
  }, [serverTotal, priceTotal, overweightSurchargeAmount]);

  const partnerTotal = useMemo(() => {
    const items = partnerValidation?.lineItems;
    if (!items?.length) return null;
    return {
      currency: partnerValidation?.currency ?? "ETB",
      lines: items.map((li) => ({
        label: li.description,
        unitPrice: li.unitAmount,
        unit: li.unit.toLowerCase(),
        quantity: li.quantity,
        amount: li.amount,
      })),
      total:
        partnerValidation?.totalAmount ?? items.reduce((s, l) => s + l.amount, 0),
    };
  }, [partnerValidation]);

  // The partner half must clear the same hard blocks as this one — the pair is
  // booked all-or-nothing, so a block on either side blocks both.
  const partnerBlockers = useMemo(() => {
    if (!consolidationActive || !partnerValidation) return [];
    return [
      ...(partnerValidation.pairingErrors ?? []),
      ...(partnerValidation.capacityErrors ?? []),
      ...(partnerValidation.containerClashErrors ?? []),
      ...(partnerValidation.spaceErrors ?? []),
    ];
  }, [consolidationActive, partnerValidation]);

  const completePairMutation = useMutation({
    mutationFn: (input: {
      payload: Freight.CreateBookingUnderContractDto;
      partnerPayload: Freight.CreateBookingUnderContractDto;
      partnerBookingId: string;
    }) =>
      contractsService.completeConsolidatedPair(id ?? "", completeBookingId ?? "", {
        partnerBookingId: input.partnerBookingId,
        booking: input.payload,
        partner: input.partnerPayload,
      }),
  });

  const submitPending =
    mutations.createBooking.isPending ||
    mutations.completeBooking.isPending ||
    completePairMutation.isPending;

  // Block confirm until the authoritative server price is in hand — the client
  // estimate is display-only; booking on it would confirm an un-validated,
  // possibly wrong price. Same guard as the portal form.
  const confirmDisabled =
    submitPending ||
    validateShipmentMutation.isPending ||
    pairingErrors.length > 0 ||
    capacityErrors.length > 0 ||
    containerClashErrors.length > 0 ||
    spaceErrors.length > 0 ||
    !serverTotal ||
    // Same bar for the shared-wagon partner: its authoritative price must be in
    // hand and its own hard blocks clear before either booking is confirmed.
    (consolidationActive &&
      (validatePartnerMutation.isPending ||
        !partnerTotal ||
        partnerBlockers.length > 0));

  const openPriceModal = () => {
    // Surface the per-field errors (portal-parity validation) instead of
    // sending an invalid payload to the price preview.
    if (!formValid) {
      setShowErrors(true);
      return;
    }
    setShowErrors(false);
    setPriceOpen(true);
    const payload = buildPayload();
    if (payload) {
      validateShipmentMutation.reset();
      validateShipmentMutation.mutate(payload);
    }
    validatePartnerMutation.reset();
    const partnerPayload = buildPartnerPayload();
    if (partnerPayload && partner?.contractId) {
      validatePartnerMutation.mutate({
        contractId: partner.contractId,
        bookingId: partner.id,
        dto: partnerPayload,
      });
    }
  };

  const handleSubmit = () => {
    if (!contract || !formValid) return;
    // Never book past unresolved 20ft pairing hard-blocks.
    if (pairingErrors.length > 0) return;
    // A line above the container type's max capacity can never book.
    if (capacityErrors.length > 0) return;
    // A container already on another booking of the same train can never book.
    if (containerClashErrors.length > 0) return;
    // An export booking no open train can carry whole can never book.
    if (spaceErrors.length > 0) return;
    const payload = buildPayload();
    if (!payload) return;

    // Shared wagon: both halves complete together, all-or-nothing on the server.
    if (consolidationActive && partner && completeBookingId) {
      // A hard block on the partner's own price preview blocks the pair.
      if (partnerBlockers.length > 0) return;
      const partnerPayload = buildPartnerPayload();
      if (!partnerPayload) return;
      completePairMutation.mutate(
        {
          payload,
          partnerPayload,
          partnerBookingId: partner.id,
        },
        {
          onSuccess: () => navigate(`/dashboard/clearance/${completeBookingId}`),
        },
      );
      return;
    }

    if (completeBookingId) {
      // Completion mode: cargo + day land on the already-cleared instance —
      // the request was linked and accepted at submission time.
      mutations.completeBooking.mutate(
        { bookingId: completeBookingId, payload },
        {
          onSuccess: () => navigate(`/dashboard/clearance/${completeBookingId}`),
        },
      );
      return;
    }

    mutations.createBooking.mutate(payload, {
      onSuccess: async (booking) => {
        if (requestId) {
          try {
            await contractsService.acceptBookingRequest(requestId, booking.id);
          } catch {
            // Non-fatal
          }
        }
        // Clearance is always per booking — land on that booking's clearance
        // detail, the same page the hub opens.
        navigate(`/dashboard/clearance/${booking.id}`);
      },
    });
  };

  if (isLoading || windowsLoading) {
    return (
      <PageContainer>
        <Center mih="50vh">
          <Loader color="edr-green" />
        </Center>
      </PageContainer>
    );
  }

  if (!contract) {
    return (
      <PageContainer>
        <PageHeader
          title="Contract not found"
          backTo="/dashboard/contracts/clearance"
        />
      </PageContainer>
    );
  }

  const header = (
    <Group justify="space-between" align="flex-end" wrap="wrap" gap="md" mb="lg">
      <Box>
        <Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
          {completeBookingId ? "Complete Shipment Booking" : "New Shipment Booking"}
        </Title>
        <Text size="sm" c="dimmed" mt={4}>
          {completeBookingId
            ? `Clearance is finalized — enter the cargo details and shipment day to complete the booking under contract ${contract.reference}.`
            : `Book a shipment on behalf of the customer for contract ${contract.reference}.`}
        </Text>
      </Box>
      <Button
        variant="default"
        radius="md"
        leftSection={<ChevronLeft size={16} />}
        onClick={() =>
          navigate(
            completeBookingId
              ? `/dashboard/clearance/${completeBookingId}`
              : "/dashboard/contracts/clearance",
          )
        }
      >
        Back to clearance
      </Button>
    </Group>
  );

  // Coarse gate — same closed-state notice the customer sees on the portal.
  // Intercity contracts are never window-gated.
  if (!isIntercity && !windowOpen) {
    return (
      <PageContainer>
        {header}
        <Alert
          color="yellow"
          variant="light"
          radius="md"
          icon={<CalendarDays size={18} />}
          title="Booking is not open right now"
        >
          <Text size="sm">
            {nextWindow?.windowOpensAt ? (
              <>
                Next window: <b>{fmtWindowOpensAt(nextWindow.windowOpensAt)} EAT</b>{" "}
                for{" "}
                <b>
                  {nextWindow.origin ?? "Origin"} →{" "}
                  {nextWindow.destination ?? "Destination"}
                </b>
                .
              </>
            ) : (
              <>No upcoming booking window scheduled.</>
            )}
          </Text>
          <Text size="sm" mt="xs">
            Come back when the booking window opens to book this shipment.
          </Text>
        </Alert>
      </PageContainer>
    );
  }

  return (
    <PageContainer>
      {header}

      {bookingRequest ? (
        <Alert
          color="edr-green"
          variant="light"
          radius="md"
          icon={<FileText size={16} />}
          title="From shipment request"
          mb="lg"
        >
          Booking on behalf of the customer for request{" "}
          <b>{bookingRequest.reference}</b>.
          {bookingRequest.scheduledDate ? (
            <>
              {" "}
              Customer requested{" "}
              <b>
                {new Intl.DateTimeFormat("en-GB", {
                  day: "2-digit",
                  month: "short",
                  year: "numeric",
                }).format(new Date(bookingRequest.scheduledDate))}
              </b>{" "}
              — set the binding shipment date below.
            </>
          ) : null}
        </Alert>
      ) : null}

      <Stack gap="lg" maw={896} mx="auto">
        <StepCard>
          <StepHeader
            icon={<MapPin size={22} />}
            title="Route"
            description={
              multiRoute
                ? "Choose which contracted route this shipment ships on."
                : "This shipment ships on the contract's only route."
            }
          />
          {multiRoute ? (
            <Select
              label="Contract route *"
              placeholder="Select a route..."
              value={contractRouteId}
              onChange={setContractRouteId}
              error={showErrors ? routeError : undefined}
              data={routes.map((r) => ({
                value: r.id,
                label: `${r.originYard?.label ?? r.originYard?.code ?? "Origin"} → ${
                  r.destinationYard?.label ??
                  r.destinationYard?.code ??
                  "Destination"
                }`,
              }))}
              radius={10}
              styles={fieldStyles}
            />
          ) : (
            <Paper withBorder radius="md" p="md" style={{ borderColor: "#E6ECF2" }}>
              <Text fz={14} fw={600}>
                {selectedRoute?.originYard?.label ??
                  selectedRoute?.originYard?.code ??
                  "—"}{" "}
                →{" "}
                {selectedRoute?.destinationYard?.label ??
                  selectedRoute?.destinationYard?.code ??
                  "—"}
              </Text>
              <Text fz={12} c="dimmed" mt={2}>
                {contract.tradeDirection}
              </Text>
            </Paper>
          )}
        </StepCard>

        {isContainer ? (
          <StepCard>
            <StepHeader
              icon={<Package size={22} />}
              title="Cargo Details"
              description="Enter the quantity and per-container details for each size in the contract scope."
            />
            <Stack gap={18}>
              {containerSizes.length > 0 && (
                <Paper withBorder radius="md" p="md" style={{ borderColor: "#E6ECF2" }}>
                  <Group justify="space-between" wrap="wrap" gap="sm">
                    <Box>
                      <Text fz={13} fw={600}>
                        Import containers from Excel
                      </Text>
                      <Text fz={12} c="dimmed">
                        One row per container. Importing fills the lines below
                        for the sizes in the file.
                      </Text>
                    </Box>
                    <Group gap="sm">
                      <Button
                        variant="default"
                        size="xs"
                        radius="md"
                        leftSection={<FileDown size={14} />}
                        onClick={() => downloadContainerImportTemplate(excelOpts)}
                      >
                        Download template
                      </Button>
                      <FileButton
                        resetRef={importResetRef}
                        accept=".xlsx,.xls"
                        onChange={handleImportFile}
                      >
                        {(props) => (
                          <Button
                            {...props}
                            size="xs"
                            radius="md"
                            color="edr-green"
                            leftSection={<FileUp size={14} />}
                          >
                            Import Excel
                          </Button>
                        )}
                      </FileButton>
                    </Group>
                  </Group>
                  {importErrors.length > 0 && (
                    <Alert
                      color="red"
                      variant="light"
                      radius="md"
                      icon={<AlertCircle size={16} />}
                      title="Import failed — fix the file and try again"
                      mt="sm"
                    >
                      <Stack gap={4}>
                        {importErrors.slice(0, 8).map((msg, i) => (
                          <Text key={i} fz="xs">
                            {msg}
                          </Text>
                        ))}
                        {importErrors.length > 8 && (
                          <Text fz="xs" c="dimmed">
                            …and {importErrors.length - 8} more.
                          </Text>
                        )}
                      </Stack>
                    </Alert>
                  )}
                  {importSummary && (
                    <Alert
                      color="edr-green"
                      variant="light"
                      radius="md"
                      icon={<CheckCircle2 size={16} />}
                      mt="sm"
                    >
                      <Text fz="xs">{importSummary}</Text>
                    </Alert>
                  )}
                </Paper>
              )}
              {remainderNotice}
              <ContractCapacityNotice contractId={contract.id} isContainer />
              <Textarea
                label="Cargo description *"
                description="What do the containers carry on this shipment?"
                placeholder="e.g. Electronics, garments, machinery spare parts…"
                value={cargoDescription}
                onChange={(e) => setCargoDescription(e.currentTarget.value)}
                error={showErrors ? cargoDescriptionError : undefined}
                radius={10}
                autosize
                minRows={2}
                maxRows={4}
                styles={fieldStyles}
              />
              {/* With a parent booking linked, each booking's containers are
                  entered in its own labelled section, one after the other. */}
              {splitView ? (
                <Group gap={8} align="center">
                  <Badge color="edr-green" variant="light" radius="sm">
                    {completeBooking?.reference ?? "This booking"}
                  </Badge>
                  <Text fz={13} fw={600} c="#10202F">
                    {completeBooking?.company?.name ?? "—"}
                  </Text>
                </Group>
              ) : null}
              {containerLines.length === 0 ? (
                <Text fz="sm" c="dimmed">
                  This contract has no container sizes in scope.
                </Text>
              ) : (
                containerLines.map((line, lineIdx) => (
                  <Box
                    key={`${line.containerSize}-${lineIdx}`}
                    className="rounded-xl"
                    style={{ border: "1px solid #E6ECF2", padding: 16 }}
                  >
                    <Text fz={14} fw={700} mb={10}>
                      {line.containerSize} containers
                    </Text>
                    <Group gap={12} grow mb={12} align="flex-start">
                      <TextInput
                        type="number"
                        onKeyDown={blockNegative}
                        label="Quantity *"
                        min={0}
                        value={line.quantity}
                        error={
                          showErrors
                            ? (lineErrors[lineIdx]?.quantity ??
                              lineErrors[lineIdx]?.units)
                            : undefined
                        }
                        onChange={(e) => {
                          patchLine(lineIdx, { quantity: e.currentTarget.value });
                        }}
                        onBlur={(e) => {
                          syncUnits(lineIdx, Number(e.currentTarget.value || 0));
                        }}
                        radius={10}
                        styles={fieldStyles}
                      />
                    </Group>

                    <StepLabel>Per-container details</StepLabel>
                    {handlingColumns.length > 0 ? (
                      <Text fz={11} c="dimmed" mt={4}>
                        Tick the services each individual container needs —
                        charges apply only to the containers ticked
                        {handlingColumns
                          .map((col) => {
                            const count = line.units.filter(
                              (u) => u[col.key],
                            ).length;
                            return count > 0 ? ` · ${count} ${col.label.toLowerCase()}` : "";
                          })
                          .join("")}
                        .
                      </Text>
                    ) : null}
                    <Stack gap={10} mt={8}>
                      {/* Header row — input labels + handling-service labels,
                          one aligned grid shared by every unit row below.
                          Same layout as the portal shipment form. */}
                      {line.units.length > 0 && (
                        <Group gap={10} wrap="nowrap" align="flex-end">
                          <Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
                            Container number *
                          </Text>
                          <Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
                            Seal number
                          </Text>
                          <Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
                            VGM (tons) *
                          </Text>
                          {handlingColumns.map((col) => (
                            <Group
                              key={col.key}
                              gap={4}
                              wrap="nowrap"
                              justify="center"
                              style={{ width: 96, flexShrink: 0 }}
                            >
                              <span style={{ color: col.color, display: "flex" }}>
                                {col.icon}
                              </span>
                              <Text fz={12} fw={600} c="#10202F">
                                {col.label}
                              </Text>
                            </Group>
                          ))}
                        </Group>
                      )}
                      {line.units.map((unit, unitIdx) => (
                        <Group key={unitIdx} gap={10} wrap="nowrap" align="flex-start">
                          <TextInput
                            placeholder="e.g. MSCU1234567"
                            value={unit.containerNumber}
                            error={
                              showErrors
                                ? unitErrors[lineIdx]?.[unitIdx]?.containerNumber
                                : undefined
                            }
                            onChange={(e) =>
                              patchUnit(lineIdx, unitIdx, {
                                containerNumber:
                                  e.currentTarget.value.toUpperCase(),
                              })
                            }
                            radius={10}
                            styles={fieldStyles}
                            style={{ flex: 1 }}
                          />
                          <TextInput
                            placeholder="Optional"
                            value={unit.sealNumber}
                            onChange={(e) =>
                              patchUnit(lineIdx, unitIdx, {
                                sealNumber: e.currentTarget.value,
                              })
                            }
                            radius={10}
                            styles={fieldStyles}
                            style={{ flex: 1 }}
                          />
                          <TextInput
                            type="number"
                            onKeyDown={blockNegative}
                            placeholder="e.g. 24.5"
                            min={0}
                            step={0.01}
                            value={unit.vgmTons}
                            error={
                              showErrors
                                ? unitErrors[lineIdx]?.[unitIdx]?.vgmTons
                                : undefined
                            }
                            onChange={(e) =>
                              patchUnit(lineIdx, unitIdx, {
                                vgmTons: e.currentTarget.value,
                              })
                            }
                            radius={10}
                            styles={fieldStyles}
                            style={{ flex: 1 }}
                          />
                          {handlingColumns.map((col) => (
                            <Box
                              key={col.key}
                              style={{
                                width: 96,
                                flexShrink: 0,
                                height: 42,
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                              }}
                            >
                              <Switch
                                checked={Boolean(unit[col.key])}
                                aria-label={`${col.label} — container ${unitIdx + 1}`}
                                onChange={(e) =>
                                  patchUnit(lineIdx, unitIdx, {
                                    [col.key]: e.currentTarget.checked,
                                  })
                                }
                                size="sm"
                              />
                            </Box>
                          ))}
                          <ActionIcon
                            variant="subtle"
                            color="red"
                            aria-label={`Remove container ${unitIdx + 1}`}
                            onClick={() => removeUnit(lineIdx, unitIdx)}
                          >
                            <X size={16} />
                          </ActionIcon>
                        </Group>
                      ))}
                    </Stack>
                  </Box>
                ))
              )}

              {hasOdd20ft && oddConsolidationAvailable ? (
                <Alert
                  color={consolidateOdd ? "edr-green" : "red"}
                  variant="light"
                  radius="md"
                  icon={<AlertCircle size={16} />}
                  title={`Odd number of 20ft containers (${ft20Total})`}
                >
                  <Stack gap={10}>
                    <Text fz={13}>
                      20ft containers travel two per wagon, so one container here
                      is unpaired. On a customs booking you can pair it with
                      another customer's odd booking and complete both onto the
                      shared wagon — each booking is still priced and invoiced
                      separately.
                    </Text>
                    <Switch
                      checked={consolidateOdd}
                      color="edr-green"
                      label="Share a wagon with another booking"
                      onChange={(e) => {
                        consolidateTouchedRef.current = true;
                        setConsolidateOdd(e.currentTarget.checked);
                      }}
                    />
                    {consolidateOdd ? (
                      <Group gap={10} align="center" wrap="wrap">
                        <Button
                          size="xs"
                          radius="md"
                          variant="light"
                          color="edr-green"
                          leftSection={<Link2 size={14} />}
                          onClick={() => setPartnerPickerOpen(true)}
                        >
                          {partner
                            ? `Parent booking: ${partner.reference} — change`
                            : "Parent booking"}
                        </Button>
                        {partner ? (
                          <Button
                            size="xs"
                            radius="md"
                            variant="subtle"
                            color="gray"
                            onClick={() => {
                              setPartner(null);
                              setPartnerLines([]);
                              setPartnerCargoDescription("");
                            }}
                          >
                            Remove
                          </Button>
                        ) : null}
                      </Group>
                    ) : (
                      <Text fz={12.5} c="red.7">
                        With sharing off, book an even number of 20ft containers
                        — add one more or remove one (e.g. {ft20Total + 1} or{" "}
                        {ft20Total - 1} instead of {ft20Total}).
                      </Text>
                    )}
                  </Stack>
                </Alert>
              ) : hasOdd20ft ? (
                <Alert
                  color="red"
                  variant="light"
                  radius="md"
                  icon={<AlertCircle size={16} />}
                  title={`Odd number of 20ft containers (${ft20Total})`}
                >
                  20ft containers travel two per wagon, so they must be booked in
                  even numbers. Add one more 20ft container or remove one (e.g.
                  book {ft20Total + 1} or {ft20Total - 1} instead of {ft20Total})
                  — the booking cannot be created with an unpaired 20ft container.
                </Alert>
              ) : null}

              {splitView && partner ? (
                <>
                  <Divider my={4} />
                  <Group gap={8} align="center">
                    <Badge color="blue" variant="light" radius="sm">
                      {partner.reference}
                    </Badge>
                    <Text fz={13} fw={600} c="#10202F">
                      {partner.companyName ?? "—"}
                    </Text>
                  </Group>
                  <Text fz={12.5} c="dimmed">
                    Parent booking — ships on the same day and train, billed to
                    its own customer.
                  </Text>
                  <ConsolidationPartnerPanel
                    lines={partnerLines}
                    onLinesChange={setPartnerLines}
                    cargoDescription={partnerCargoDescription}
                    onCargoDescriptionChange={setPartnerCargoDescription}
                    showHazardous={Boolean(contract.isHazardous)}
                    showReefer={Boolean(contract.isReefer)}
                    showErrors={showErrors}
                    error={partnerError}
                  />
                </>
              ) : null}
            </Stack>
          </StepCard>
        ) : (
          <StepCard>
            <StepHeader
              icon={<Package size={22} />}
              title="Cargo Details"
              description="Enter the amount you are shipping for this booking."
            />
            <Stack gap={14}>
              {remainderNotice}
              <ContractCapacityNotice contractId={contract.id} isContainer={false} />
              <TextInput
                type="number"
                onKeyDown={blockNegative}
                label="Quantity (tons)"
                placeholder="e.g. 1200"
                min={0}
                step={0.01}
                value={bulk.cargoWeightTons}
                error={
                  showErrors && bulkUom === "PER_TON"
                    ? bulkErrors.quantity
                    : undefined
                }
                onChange={(e) =>
                  setBulk((b) => ({ ...b, cargoWeightTons: e.currentTarget.value }))
                }
                radius={10}
                styles={fieldStyles}
              />
              <TextInput
                type="number"
                onKeyDown={blockNegative}
                label="Item count (if applicable)"
                placeholder="e.g. 500"
                min={0}
                step={1}
                value={bulk.itemCount}
                error={
                  showErrors && bulkUom === "PER_ITEM"
                    ? bulkErrors.quantity
                    : undefined
                }
                onChange={(e) =>
                  setBulk((b) => ({ ...b, itemCount: e.currentTarget.value }))
                }
                radius={10}
                styles={fieldStyles}
              />
              {contract.isHazardous && (
                <TextInput
                  type="number"
                  onKeyDown={blockNegative}
                  label="Hazardous quantity"
                  min={0}
                  step={1}
                  value={bulk.hazardousQuantity}
                  error={showErrors ? bulkErrors.hazardous : undefined}
                  onChange={(e) =>
                    setBulk((b) => ({
                      ...b,
                      hazardousQuantity: e.currentTarget.value,
                    }))
                  }
                  radius={10}
                  styles={fieldStyles}
                />
              )}
              {contract.isReefer && (
                <TextInput
                  type="number"
                  onKeyDown={blockNegative}
                  label="Refrigerated quantity"
                  min={0}
                  step={1}
                  value={bulk.reeferQuantity}
                  error={showErrors ? bulkErrors.reefer : undefined}
                  onChange={(e) =>
                    setBulk((b) => ({
                      ...b,
                      reeferQuantity: e.currentTarget.value,
                    }))
                  }
                  radius={10}
                  styles={fieldStyles}
                />
              )}
            </Stack>
          </StepCard>
        )}

        {/* Legacy contracts only — WITH_RETURN contracts capture per-line
            return quantities above, WITHOUT_RETURN contracts locked it off. */}
        {legacyReturnToggle ? (
          <StepCard>
            <StepHeader
              icon={<Repeat size={22} />}
              title="Equipment Return"
              description="Choose whether the empty container(s) come back to EDR after unloading."
            />
            <Paper
              withBorder
              radius="md"
              p="md"
              style={{
                borderColor: withReturn ? "#CDEBDD" : "#E6ECF2",
                background: withReturn ? "#F6FBF8" : "white",
                cursor: "pointer",
                transition: "border-color 150ms ease, background 150ms ease",
              }}
              onClick={() => setWithReturn((v) => !v)}
            >
              <Group justify="space-between" wrap="nowrap" gap="md">
                <Group gap={13} wrap="nowrap" align="flex-start">
                  <Box
                    style={{
                      width: 38,
                      height: 38,
                      flexShrink: 0,
                      borderRadius: 11,
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      background: withReturn ? "#ECF6F1" : "#F1F4F7",
                      color: withReturn ? "#0A6F4D" : "#6B7C8E",
                    }}
                  >
                    <Repeat size={18} />
                  </Box>
                  <Box>
                    <Text fz={14} fw={700}>
                      With return
                    </Text>
                    <Text fz={12} c="dimmed" style={{ lineHeight: 1.4 }}>
                      {withReturn
                        ? "Container(s) returned to EDR after unloading."
                        : "Container(s) retained by the customer after delivery."}
                    </Text>
                  </Box>
                </Group>
                <Switch
                  size="md"
                  color="edr-green"
                  aria-label="With return"
                  checked={withReturn}
                  onChange={(e) => setWithReturn(e.currentTarget.checked)}
                  onClick={(e) => e.stopPropagation()}
                  style={{ flexShrink: 0 }}
                />
              </Group>
            </Paper>
          </StepCard>
        ) : null}

        {isIntercity ? (
          <StepCard>
            <StepHeader
              icon={<CalendarDays size={22} />}
              title="Schedule"
              description="Intercity shipments have no fixed day."
            />
            <Alert color="blue" variant="light" radius="md" icon={<AlertCircle size={16} />}>
              The shipment rides the next import/export train passing through
              the corridor. Operations assign it to a train with free capacity —
              the customer is notified when it is accepted and payment is due.
            </Alert>
          </StepCard>
        ) : (
          <StepCard>
            <StepHeader
              icon={<CalendarDays size={22} />}
              title="Schedule"
              description="Pick the binding shipment day. Only days with an open train that has enough matching wagons for the cargo can be selected."
            />
            <Box>
              <StepLabel>Shipment day *</StepLabel>
              <Box mt={10} w="100%">
                {/* Calendar stays visible before cargo is entered — all days
                    disabled with a hint, since availability depends on cargo. */}
                <OperationDatePicker
                  fullWidth
                  availableDays={cargoQuery === null ? [] : (availableDays ?? [])}
                  isLoading={cargoQuery !== null && daysLoading}
                  emptyMessage={
                    cargoQuery === null
                      ? "Enter the cargo details first — available shipment days depend on the wagons the cargo needs."
                      : undefined
                  }
                  value={scheduledDate}
                  onChange={(d) => {
                    setScheduledDate(d);
                    // A new day invalidates the old train pick.
                    setTrainScheduleId("");
                  }}
                />
              </Box>
              {showErrors && dateError && (
                <Text fz="xs" c="red" mt={6}>
                  {dateError}
                </Text>
              )}
              {isExportPick && scheduledDate ? (
                <ExportTrainPicker
                  options={exportTrainsQuery.data ?? []}
                  loading={exportTrainsQuery.isLoading}
                  value={trainScheduleId}
                  onChange={setTrainScheduleId}
                />
              ) : null}
            </Box>
          </StepCard>
        )}

        <StepCard>
          <Box mb="md">
            <Text size="sm" fw={600} mb={4}>
              Billing currency
            </Text>
            <Text size="xs" c="dimmed" mb={8}>
              {isImport
                ? "Import shipments may be invoiced in ETB or USD. USD is paid by bank transfer, not online."
                : "Shipments are invoiced in ETB."}
            </Text>
            <CurrencySelector
              value={isImport ? paymentCurrency : "ETB"}
              onChange={setPaymentCurrency}
              disabled={!isImport}
              allowUsd={isImport}
              error={currencyError}
            />
          </Box>

          <Textarea
            label="Additional notes"
            placeholder="Any special instructions for this shipment…"
            rows={3}
            radius="md"
            value={notes}
            onChange={(e) => setNotes(e.currentTarget.value)}
          />
        </StepCard>
      </Stack>

      <Box
        style={{
          position: "sticky",
          bottom: 0,
          zIndex: 20,
          borderTop: "1px solid var(--mantine-color-gray-3)",
          backgroundColor: "rgba(255,255,255,0.94)",
          backdropFilter: "blur(14px)",
          padding: "16px 0",
          marginTop: 24,
        }}
      >
        <Box maw={896} mx="auto">
          {/* The review button is disabled on an odd 20ft total, so the click
              that would surface the errors never lands — state the reason here
              rather than leaving it in a tooltip nobody hovers. */}
          {oddBlocksSubmit ? (
            <Alert
              color="red"
              variant="light"
              radius="md"
              icon={<AlertCircle size={16} />}
              mb="sm"
              title={`Odd number of 20ft containers (${ft20Total})`}
            >
              20ft containers travel two per wagon, so they must be booked in
              even numbers. Add one more 20ft container or remove one — book{" "}
              {ft20Total + 1} or {ft20Total - 1} instead of {ft20Total}.
            </Alert>
          ) : showErrors && !formValid ? (
            <Alert
              color="red"
              variant="light"
              radius="md"
              icon={<AlertCircle size={16} />}
              mb="sm"
            >
              Fix the highlighted fields before reviewing the price.
            </Alert>
          ) : partnerError ? (
            // The review button is disabled while the parent booking is
            // incomplete, so the click that would reveal the errors never
            // lands — say what is outstanding without waiting for it.
            <Alert
              color="yellow"
              variant="light"
              radius="md"
              icon={<AlertCircle size={16} />}
              mb="sm"
            >
              {partnerError}
            </Alert>
          ) : null}
          <Group justify="flex-end">
            <Tooltip
              label={
                oddBlocksSubmit
                  ? `Book an even number of 20ft containers — ${ft20Total} is odd and would leave one unpaired.`
                  : (partnerError ?? "")
              }
              withArrow
              // Only explain a block that is actually in force: an odd count
              // linked to a parent booking is resolved by the shared wagon.
              disabled={!oddBlocksSubmit && !partnerError}
            >
              {/* Mantine tooltips get no pointer events from a disabled button,
                  so the wrapper carries the hover target. */}
              <Box>
                <Button
                  color="edr-green"
                  radius="md"
                  leftSection={<Receipt size={16} />}
                  onClick={openPriceModal}
                  // An unpaired 20ft can never be planned onto a wagon — unless
                  // a parent booking is linked to share it, which is what
                  // oddBlocksSubmit accounts for. The parent's own cargo must be
                  // complete too, or there is nothing to price.
                  disabled={oddBlocksSubmit || Boolean(partnerError)}
                >
                  Review price &amp; book
                </Button>
              </Box>
            </Tooltip>
          </Group>
        </Box>
      </Box>

      <ConsolidationPartnerPicker
        opened={partnerPickerOpen}
        onClose={() => setPartnerPickerOpen(false)}
        candidates={candidatesQuery.data ?? []}
        isLoading={candidatesQuery.isLoading}
        isError={candidatesQuery.isError}
        onSelect={(candidate) => {
          setPartner(candidate);
          // Seed a 20ft and a 40ft line. The parent booking sits on its OWN
          // contract, whose size scope need not match this one's, so the panel
          // offers both sizes rather than mirroring this contract's scope; a
          // size the parent does not ship is simply left at 0.
          setPartnerLines(PARTNER_SIZES.map(emptyPartnerLine));
          setPartnerCargoDescription("");
          setPartnerPickerOpen(false);
        }}
      />

      <Modal
        opened={priceOpen}
        onClose={() => {
          if (!submitPending) setPriceOpen(false);
        }}
        closeOnClickOutside={!submitPending}
        closeOnEscape={!submitPending}
        withCloseButton={!submitPending}
        centered
        radius="lg"
        size="lg"
        title={
          <Group gap={10}>
            <ThemeIcon variant="light" color="edr-green" radius="md" size={34}>
              <Receipt size={18} />
            </ThemeIcon>
            <Box>
              <Text fw={800} fz={16}>
                Confirm shipment price
              </Text>
              <Text fz="xs" c="dimmed">
                Booking on behalf of the customer for {contract.reference}.
              </Text>
            </Box>
          </Group>
        }
      >
        {displayTotal ? (
          <Stack gap="md">
            {validateShipmentMutation.isPending && (
              <Group gap={8} c="dimmed">
                <Loader size="xs" color="edr-green" />
                <Text fz="sm" c="dimmed">
                  Computing the final price breakdown and checking container
                  weights…
                </Text>
              </Group>
            )}

            {pairingErrors.length > 0 && (
              <Alert
                color="red"
                variant="light"
                radius="md"
                icon={<AlertCircle size={16} />}
                title="Cannot create booking — 20ft wagon pairing"
              >
                <Stack gap={6}>
                  {pairingErrors.map((msg, i) => (
                    <Text key={i} fz="sm" c="red.8">
                      {msg}
                    </Text>
                  ))}
                  <Text fz="xs" c="red.7" mt={2}>
                    Adjust the 20ft container weights or quantities so pairs
                    differ by no more than 10 tons.
                  </Text>
                </Stack>
              </Alert>
            )}

            {capacityErrors.length > 0 && (
              <Alert
                color="red"
                variant="light"
                radius="md"
                icon={<AlertCircle size={16} />}
                title="Cannot create booking — over maximum capacity"
              >
                <Stack gap={6}>
                  {capacityErrors.map((msg, i) => (
                    <Text key={i} fz="sm" c="red.8">
                      {msg}
                    </Text>
                  ))}
                  <Text fz="xs" c="red.7" mt={2}>
                    Reduce the cargo weight or split it across more containers
                    to book this shipment.
                  </Text>
                </Stack>
              </Alert>
            )}

            {containerClashErrors.length > 0 && (
              <Alert
                color="red"
                variant="light"
                radius="md"
                icon={<AlertCircle size={16} />}
                title="Cannot create booking — container already booked"
              >
                <Stack gap={6}>
                  {containerClashErrors.map((msg, i) => (
                    <Text key={i} fz="sm" c="red.8">
                      {msg}
                    </Text>
                  ))}
                  <Text fz="xs" c="red.7" mt={2}>
                    A container can only be on one booking per train — remove it
                    or pick another shipment day.
                  </Text>
                </Stack>
              </Alert>
            )}

            {spaceErrors.length > 0 && (
              <Alert
                color="red"
                variant="light"
                radius="md"
                icon={<AlertCircle size={16} />}
                title="Cannot create booking — not enough train space"
              >
                <Stack gap={6}>
                  {spaceErrors.map((msg, i) => (
                    <Text key={i} fz="sm" c="red.8">
                      {msg}
                    </Text>
                  ))}
                </Stack>
              </Alert>
            )}

            {overweightLines.length > 0 && (
              <Alert
                color="yellow"
                variant="light"
                radius="md"
                icon={<AlertTriangle size={16} />}
                title="Overweight containers"
              >
                <Stack gap={6}>
                  {overweightLines.map((line, i) => (
                    <Text key={i} fz="sm" c="#9A5B00">
                      {line.containerLabel || line.containerTypeCode}:{" "}
                      {line.totalVgmTons}t exceeds limit {line.maxAllowedTons}t
                      (+{line.excessTons}t overweight)
                    </Text>
                  ))}
                  <Text fz="xs" c="#9A5B00" mt={2}>
                    {overweightSurchargeAmount > 0
                      ? `An overweight surcharge of ${overweightSurchargeAmount.toLocaleString()} ${
                          validation?.currency ?? displayTotal.currency ?? ""
                        } applies (included in the total below). You can still submit, or go back and adjust weights.`
                      : "An overweight surcharge applies. You can still submit, or go back and adjust weights."}
                  </Text>
                </Stack>
              </Alert>
            )}

            <Paper withBorder radius={16} p="lg" style={{ borderColor: "#E6ECF2" }}>
              {/* Whose bill this is. Only worth naming when a second booking is
                  on screen — on a lone booking there is nothing to confuse it with. */}
              {consolidationActive && partner ? (
                <Group gap={8} align="center" mb={12} wrap="wrap">
                  <Badge color="edr-green" variant="light" radius="sm">
                    {completeBooking?.reference ?? "This booking"}
                  </Badge>
                  <Text fz={13} fw={600} c="#10202F">
                    {completeBooking?.company?.name ?? contract.company?.name ?? "—"}
                  </Text>
                </Group>
              ) : null}
              <Stack gap={10}>
                {displayTotal.lines.map((line, i) => (
                  <Group key={i} justify="space-between" wrap="nowrap" gap="sm">
                    <Box style={{ minWidth: 0 }}>
                      <Text fz="sm" fw={500}>
                        {line.label}
                      </Text>
                      <Text fz="xs" c="dimmed">
                        {line.quantity.toLocaleString()} ×{" "}
                        {line.unitPrice.toLocaleString()} {displayTotal.currency} ·{" "}
                        {formatRateUnit(line.unit)}
                      </Text>
                    </Box>
                    <Text fz="sm" fw={600} style={{ whiteSpace: "nowrap" }}>
                      {line.amount.toLocaleString()} {displayTotal.currency}
                    </Text>
                  </Group>
                ))}
                {displayTotal.lines.length === 0 && (
                  <Text fz="sm" c="dimmed">
                    No priced lines — check the cargo details.
                  </Text>
                )}
              </Stack>
              <Divider my="md" />
              <Group justify="space-between" align="flex-end">
                <Text
                  fz="xs"
                  fw={700}
                  tt="uppercase"
                  c="edr-green"
                  style={{ letterSpacing: "0.06em" }}
                >
                  Total
                </Text>
                <Text fw={800} fz={28}>
                  {displayTotal.total.toLocaleString()}{" "}
                  <Text span fz={16} fw={700} c="dimmed">
                    {displayTotal.currency}
                  </Text>
                </Text>
              </Group>
            </Paper>

            {consolidationActive && partner ? (
              <Paper
                withBorder
                radius={16}
                p="lg"
                style={{ borderColor: "#E6ECF2" }}
              >
                <Group gap={8} align="center" mb={12} wrap="wrap">
                  <Badge color="blue" variant="light" radius="sm">
                    {partner.reference}
                  </Badge>
                  <Text fz={13} fw={600} c="#10202F">
                    {partner.companyName ?? "—"}
                  </Text>
                </Group>

                {validatePartnerMutation.isPending ? (
                  <Group gap={8} c="dimmed">
                    <Loader size="xs" color="edr-green" />
                    <Text fz="sm" c="dimmed">
                      Pricing the partner booking…
                    </Text>
                  </Group>
                ) : partnerBlockers.length > 0 ? (
                  <Alert
                    color="red"
                    variant="light"
                    radius="md"
                    icon={<AlertCircle size={16} />}
                    title={`Cannot book ${partner.reference}`}
                  >
                    <Stack gap={6}>
                      {partnerBlockers.map((msg, i) => (
                        <Text key={i} fz="sm" c="red.8">
                          {msg}
                        </Text>
                      ))}
                      <Text fz="xs" c="red.7" mt={2}>
                        Both bookings are confirmed together, so this must be
                        fixed before either can be booked.
                      </Text>
                    </Stack>
                  </Alert>
                ) : partnerTotal ? (
                  <>
                    <Stack gap={10}>
                      {partnerTotal.lines.map((line, i) => (
                        <Group
                          key={i}
                          justify="space-between"
                          wrap="nowrap"
                          gap="sm"
                        >
                          <Box style={{ minWidth: 0 }}>
                            <Text fz="sm" fw={500}>
                              {line.label}
                            </Text>
                            <Text fz="xs" c="dimmed">
                              {line.quantity.toLocaleString()} ×{" "}
                              {line.unitPrice.toLocaleString()}{" "}
                              {partnerTotal.currency} ·{" "}
                              {formatRateUnit(line.unit)}
                            </Text>
                          </Box>
                          <Text
                            fz="sm"
                            fw={600}
                            style={{ whiteSpace: "nowrap" }}
                          >
                            {line.amount.toLocaleString()}{" "}
                            {partnerTotal.currency}
                          </Text>
                        </Group>
                      ))}
                    </Stack>
                    <Divider my="md" />
                    <Group justify="space-between" align="flex-end">
                      <Text
                        fz="xs"
                        fw={700}
                        tt="uppercase"
                        c="blue"
                        style={{ letterSpacing: "0.06em" }}
                      >
                        Total
                      </Text>
                      <Text fw={800} fz={28}>
                        {partnerTotal.total.toLocaleString()}{" "}
                        <Text span fz={16} fw={700} c="dimmed">
                          {partnerTotal.currency}
                        </Text>
                      </Text>
                    </Group>
                  </>
                ) : (
                  <Text fz="sm" c="dimmed">
                    No price yet for the partner booking.
                  </Text>
                )}
              </Paper>
            ) : null}

            {consolidationActive && partner ? (
              <Alert
                color="blue"
                variant="light"
                radius="md"
                icon={<Link2 size={16} />}
              >
                <Text fz="sm">
                  These two bookings share one wagon but stay separate: each is
                  invoiced to its own customer and paid separately. Confirming
                  books both together — if either fails, neither is booked.
                </Text>
              </Alert>
            ) : null}

            <Group justify="space-between" mt="xs">
              <Button
                variant="default"
                radius="md"
                leftSection={<X size={16} />}
                onClick={() => setPriceOpen(false)}
                disabled={submitPending}
              >
                Reject &amp; edit
              </Button>
              <Button
                color="edr-green"
                radius="md"
                leftSection={<CheckCircle2 size={16} />}
                loading={submitPending}
                disabled={confirmDisabled}
                onClick={handleSubmit}
              >
                {consolidationActive && partner
                  ? "Confirm & book both"
                  : completeBookingId
                    ? "Confirm & complete"
                    : "Confirm & book"}
              </Button>
            </Group>
          </Stack>
        ) : null}
      </Modal>
    </PageContainer>
  );
}
