import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type KeyboardEvent,
  type ReactNode,
} from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigate, useParams } from "react-router-dom";
import {
  Alert,
  Box,
  Button,
  Center,
  Divider,
  FileButton,
  Group,
  Loader,
  Modal,
  Paper,
  SegmentedControl,
  Stack,
  Switch,
  Text,
  TextInput,
  Textarea,
  ThemeIcon,
  Title,
  Tooltip,
} from "@mantine/core";
import {
  AlertCircle,
  AlertTriangle,
  CalendarDays,
  CheckCircle2,
  ChevronLeft,
  FileDown,
  FileUp,
  Flame,
  MapPin,
  Package,
  Receipt,
  Repeat,
  Snowflake,
  X,
} from "lucide-react";

import type { Freight } from "@edr/types";
import { OperationDatePicker } from "@edr/ui-common";
import { api } from "@/services/api";
import {
  contractsService,
  type ShipmentValidation,
} from "@/services/contracts.service";
import {
  SelectField,
  StepCard,
  StepHeader,
  StepLabel,
  fieldStyles,
} from "./new-contract-form/shared";
import { formatRateUnit } from "./new-contract-form/unit-rates";
import {
  ShipmentFormInputValues,
  ShipmentFormValues,
  createShipmentFormSchema,
  initialShipmentFormValues,
} from "./new-shipment-form/schema";
import { computeShipmentTotal } from "./new-shipment-form/total";
import {
  downloadContainerImportTemplate,
  parseContainerExcel,
} from "./new-shipment-form/container-excel";
import { ContractCapacityNotice } from "./new-shipment-form/ContractCapacityNotice";
import { closedWindowMessage, hasOpenWindow } from "./booking-window";

type ShipmentForm = ReturnType<
  typeof useForm<ShipmentFormInputValues, any, ShipmentFormValues>
>;

/**
 * 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.
 */
const blockNegative = (event: KeyboardEvent<HTMLInputElement>) => {
  if (event.key === "-") event.preventDefault();
};

export default function NewShipmentPage() {
  // With `bookingId` the page runs in COMPLETION mode: the bare booking
  // instance (created by "Initiate booking") already passed per-booking
  // clearance, and this form supplies the deferred cargo + shipment day. Same
  // window gates, same validation, same price confirmation — the submit just
  // completes the existing booking instead of creating a new one.
  const { id, bookingId } = useParams<{ id: string; bookingId?: string }>();
  const navigate = useNavigate();

  const { data: contract, isLoading } = useQuery(
    api.contracts.get.queryOptions({ input: { id: id! }, enabled: !!id }),
  );

  // Coarse booking-window gate: block the form entirely when no window is
  // currently open for the contract's routes. The day-picker inside the form
  // still narrows to bookable days; this is the outer "is booking open at all"
  // check that mirrors the contract detail page.
  const { data: bookingWindows = [], isLoading: windowsLoading } = useQuery({
    ...api.bookings.getContractBookingWindows.queryOptions({
      input: { contractId: id! },
      refetchInterval: 60_000,
    }),
    enabled: !!id,
  });

  if (isLoading || windowsLoading) {
    return (
      <Center mih={400} p="xl">
        <Loader color="edr-green" />
      </Center>
    );
  }

  if (!contract) {
    return (
      <Box p="xl">
        <Paper withBorder radius="lg" p="xl">
          <Text fw={700} mb="xs">
            Contract not found
          </Text>
          <Button variant="default" onClick={() => navigate("/contracts")}>
            Back to contracts
          </Button>
        </Paper>
      </Box>
    );
  }

  if (contract.customsClearingEnabled) {
    return (
      <Box p="xl">
        <Alert color="blue" icon={<AlertCircle size={18} />} radius="md">
          <Text fw={700} mb="xs">
            Global Logistics handles bookings for this contract
          </Text>
          <Text size="sm" mb="md">
            This contract includes customs clearance. Upload your clearance
            documents — once Global Logistics finalizes the clearance, they
            create the booking on your behalf and you will be notified when
            payment is due.
          </Text>
          <Button
            color="edr-green"
            radius="md"
            onClick={() => navigate(`/contracts/${contract.id}/clearance`)}
          >
            Go to clearance
          </Button>
        </Alert>
      </Box>
    );
  }

  // Coarse gate: if the customer deep-links here while no booking window is
  // open, show the same closed-state notice as the contract page instead of the
  // form. Still allowed the moment any window isOpenNow. Applies to ONE_TIME
  // and GENERAL alike. Intercity contracts are never window-gated — the
  // shipment rides a passing train that staff pick at finalize time, so
  // booking is always open.
  if (
    contract.tradeDirection !== "DOMESTIC" &&
    !hasOpenWindow(bookingWindows)
  ) {
    return (
      <Box style={{ padding: "28px 0 0" }}>
        <Group
          justify="space-between"
          px="24px"
          align="flex-end"
          wrap="wrap"
          gap="md"
          mb="lg"
        >
          <Box>
            <Title
              order={1}
              fw={800}
              fz={26}
              style={{ letterSpacing: "-0.01em" }}
            >
              New Shipment Booking
            </Title>
            <Text size="sm" c="edr-muted" mt={4}>
              Book a shipment against contract {contract.reference}.
            </Text>
          </Box>
          <Button
            variant="default"
            radius="md"
            leftSection={<ChevronLeft size={16} />}
            onClick={() => navigate(`/contracts/${contract.id}`)}
          >
            Back to contract
          </Button>
        </Group>
        <Box px="24px">
          <Alert
            color="yellow"
            variant="light"
            radius="md"
            icon={<CalendarDays size={18} />}
            title="Booking is not open right now"
          >
            <Text size="sm">{closedWindowMessage(bookingWindows)}</Text>
            <Text size="sm" mt="xs">
              Come back when the booking window opens to book your shipment.
            </Text>
          </Alert>
        </Box>
      </Box>
    );
  }

  return (
    <NewShipmentBookingForm
      contract={contract}
      contractId={id!}
      completeBookingId={bookingId}
    />
  );
}

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";
}

function NewShipmentBookingForm({
  contract,
  contractId,
  completeBookingId,
}: {
  contract: Freight.IContract;
  contractId: string;
  /** Set when completing an initiated (bare) booking after clearance. */
  completeBookingId?: string;
}) {
  const navigate = useNavigate();
  const queryClient = useQueryClient();
  const [pendingValues, setPendingValues] = useState<ShipmentFormValues | null>(
    null,
  );

  const form = useForm<ShipmentFormInputValues, any, ShipmentFormValues>({
    defaultValues: {
      ...initialShipmentFormValues,
      // Seed the equipment-return toggle from the contract; the customer can
      // still flip it per shipment.
      withReturn: contract.equipmentReturn === "WITH_RETURN",
      // The contract quotes USD; the customer bills this shipment in the
      // currency they pick here. Intercity is always ETB.
      paymentCurrency:
        contract.tradeDirection === "DOMESTIC" ? "ETB" : "USD",
    },
    resolver: zodResolver(
      createShipmentFormSchema({
        isContainer: contract.freightType === "CONTAINER",
        isHazardous: contract.isHazardous ?? false,
        isReefer: contract.isReefer ?? false,
        withReturnService:
          contract.freightType === "CONTAINER" &&
          contract.equipmentReturn === "WITH_RETURN",
        unitOfMeasure: bulkUnitOfMeasure(contract),
        // Intercity rides a passing train staff pick later — no date to choose.
        requiresDate: contract.tradeDirection !== "DOMESTIC",
      }),
    ),
    mode: "onChange",
  });

  // 20ft containers ride two per wagon, so an odd total leaves one unpaired and
  // the booking can never be planned. The server's shipment validation reports
  // it too, but only once the price modal opens — block it inline instead, the
  // same way the direct-booking wizard does (new-booking-form `calcWagons`).
  const watchedContainers = form.watch("containers");
  const ft20Total =
    contract.freightType === "CONTAINER"
      ? (watchedContainers ?? [])
          .filter((l) => l.containerSize === "20ft")
          .reduce((sum, l) => sum + Number(l.quantity || 0), 0)
      : 0;
  const hasOdd20ft = ft20Total % 2 === 1;

  const submitMutation = useMutation({
    mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
      completeBookingId
        ? api.contracts.completeBookingUnderContract.call({
            id: contractId,
            bookingId: completeBookingId,
            dto,
          })
        : api.contracts.createBookingUnderContract.call({ id: contractId, dto }),
    onSuccess: (booking) => {
      queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
      queryClient.invalidateQueries({
        queryKey: api.contracts.get.queryKey({ id: contractId }),
      });
      navigate(`/bookings/${booking.id}`);
    },
  });

  // Pre-submit validation (container contracts only): warns on overweight
  // containers and HARD-BLOCKS on 20ft wagon-pairing errors. Runs each time the
  // price modal opens so re-reviewing after an edit re-checks.
  const validateMutation = useMutation({
    mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
      api.contracts.validateShipment.call({ id: contractId, dto }),
  });

  function buildDto(
    values: ShipmentFormValues,
  ): Freight.CreateBookingUnderContractDto {
    const isContainer = contract.freightType === "CONTAINER";
    // WITH_RETURN contracts carry a per-line return quantity and the server
    // derives the booking's equipment return from it; only legacy contracts
    // (no equipment return chosen at creation) still send the toggle.
    const withReturnService =
      isContainer && contract.equipmentReturn === "WITH_RETURN";
    const legacyReturnToggle = isContainer && !contract.equipmentReturn;
    return {
      ...(values.contractRouteId
        ? { contractRouteId: values.contractRouteId }
        : {}),
      paymentCurrency: values.paymentCurrency,
      // Intercity bookings carry no date — staff assign a passing train later.
      ...(values.scheduledDate
        ? { scheduledDate: new Date(values.scheduledDate).toISOString() }
        : {}),
      ...(legacyReturnToggle
        ? {
            equipmentReturn: values.withReturn
              ? "WITH_RETURN"
              : "WITHOUT_RETURN",
          }
        : {}),
      // Equipment return is a container concern — bulk keeps the contract default.
      ...(isContainer
        ? {
            // What the containers carry — captured per booking, not on the contract.
            ...(values.cargoDescription?.trim()
              ? { cargoFreeText: values.cargoDescription.trim() }
              : {}),
            containers: values.containers
              .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,
                ...(withReturnService
                  ? { returnQuantity: Number(l.returnQuantity || 0) }
                  : {}),
                units: l.units.map((u) => ({
                  containerNumber: u.containerNumber,
                  sealNumber: u.sealNumber || undefined,
                  vgmTons: Number(u.vgmTons),
                  // Per-container handling — the server rolls these up into the
                  // line counts and bills each surcharge on the ticked containers.
                  isHazardous: Boolean(u.isHazardous),
                  isReefer: Boolean(u.isReefer),
                  ...(withReturnService ? { isReturn: Boolean(u.isReturn) } : {}),
                })),
              })),
          }
        : {
            bulkLines: [
              {
                cargoTypeId: contract.cargoScope?.[0]?.cargoTypeId ?? null,
                cargoWeightTons: values.cargoWeightTons
                  ? Number(values.cargoWeightTons)
                  : undefined,
                itemCount: values.itemCount
                  ? Number(values.itemCount)
                  : undefined,
                hazardousQuantity:
                  Number(values.bulkHazardousQuantity || 0) || undefined,
                reeferQuantity:
                  Number(values.bulkReeferQuantity || 0) || undefined,
              },
            ],
          }),
      ...(values.notes ? { notes: values.notes } : {}),
    };
  }

  // Submit validates the whole form, then opens the price modal for
  // confirmation. The server-side shipment validation also returns the
  // authoritative price breakdown (rail + first/last mile + every surcharge) —
  // run it for every freight type; container contracts additionally get
  // overweight warnings + 20ft pairing hard-blocks surfaced in the modal.
  const handleReview = form.handleSubmit((values) => {
    // An unpaired 20ft can never be planned onto a wagon — don't even price it.
    if (hasOdd20ft) return;
    setPendingValues(values);
    validateMutation.reset();
    validateMutation.mutate(buildDto(values));
  });

  const handleConfirm = () => {
    if (!pendingValues) return;
    // Guard: never let a booking with unresolved 20ft pairing errors submit.
    if ((validateMutation.data?.pairingErrors.length ?? 0) > 0) return;
    // Guard: a line above the container type's max capacity can never book.
    if ((validateMutation.data?.capacityErrors?.length ?? 0) > 0) return;
    // Guard: a container already on another booking of the same train can never book.
    if ((validateMutation.data?.containerClashErrors?.length ?? 0) > 0) return;
    // Guard: an export booking no open train can carry whole can never book.
    if ((validateMutation.data?.spaceErrors?.length ?? 0) > 0) return;
    submitMutation.mutate(buildDto(pendingValues));
  };

  // Reject — close the modal and let the customer edit and re-book.
  const handleReject = () => {
    if (submitMutation.isPending) return;
    setPendingValues(null);
    validateMutation.reset();
  };

  const routes = contract.routes ?? [];

  return (
    <Box
      style={{
        padding: "28px 0 0",
        minHeight: "calc(100dvh - var(--app-shell-header-height, 56px))",
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Group
        justify="space-between"
        px="24px"
        align="flex-end"
        wrap="wrap"
        gap="md"
        mb="lg"
      >
        <Box>
          <Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
            {completeBookingId ? "Complete Your Booking" : "New Shipment Booking"}
          </Title>
          <Text size="sm" c="edr-muted" mt={4}>
            {completeBookingId
              ? `Clearance is finalized — enter the cargo details and shipment day to complete your booking under contract ${contract.reference}.`
              : `Book a shipment against contract ${contract.reference}.`}
          </Text>
        </Box>
        <Button
          variant="default"
          radius="md"
          leftSection={<ChevronLeft size={16} />}
          onClick={() => navigate(`/contracts/${contract.id}`)}
        >
          Back to contract
        </Button>
      </Group>

      <form
        className="flex flex-col"
        style={{ flex: 1 }}
        onSubmit={(e) => e.preventDefault()}
      >
        <Box flex={1} p="24px">
          {submitMutation.isError && (
            <Alert
              color="red"
              icon={<AlertCircle size={16} />}
              radius="md"
              mb="lg"
            >
              <Text size="sm" fw={600}>
                {completeBookingId
                  ? "Failed to complete the booking"
                  : "Failed to create the shipment booking"}
              </Text>
              <Text size="sm" mt={4} c="red.7">
                {submitMutation.error instanceof Error
                  ? submitMutation.error.message
                  : "An unexpected error occurred. Please try again."}
              </Text>
            </Alert>
          )}

          {/* Single-step form — all sections on one page. */}
          <Stack gap="lg" className="mx-auto max-w-4xl">
            <RouteStep form={form} contract={contract} routes={routes} />
            <CargoStep form={form} contract={contract} />
            {/* Legacy contracts only — WITH_RETURN contracts capture per-line
                return quantities in the cargo step; WITHOUT_RETURN locked it off. */}
            {contract.freightType === "CONTAINER" &&
              !contract.equipmentReturn && <EquipmentReturnStep form={form} />}
            <ScheduleStep form={form} contract={contract} routes={routes} />
            <NotesSection form={form} />
          </Stack>
        </Box>

        <Box
          style={{
            position: "sticky",
            bottom: 0,
            zIndex: 20,
            borderTop: "1px solid var(--mantine-color-edr-border-0)",
            backgroundColor: "rgba(255,255,255,0.94)",
            backdropFilter: "blur(14px)",
            padding: "16px 24px",
            marginTop: "auto",
          }}
        >
          <Group justify="flex-end" className="mx-auto max-w-4xl">
            <Tooltip
              label={`Book an even number of 20ft containers — ${ft20Total} is odd and would leave one unpaired.`}
              withArrow
              disabled={!hasOdd20ft}
            >
              {/* Mantine tooltips get no pointer events from a disabled button,
                  so the wrapper carries the hover target. */}
              <Box>
                <Button
                  type="button"
                  color="edr-green"
                  radius="md"
                  leftSection={<Receipt size={16} />}
                  onClick={handleReview}
                  disabled={hasOdd20ft}
                >
                  Review price &amp; book
                </Button>
              </Box>
            </Tooltip>
          </Group>
        </Box>
      </form>

      <PriceConfirmModal
        contract={contract}
        values={pendingValues}
        loading={submitMutation.isPending}
        validation={validateMutation.data ?? null}
        validationLoading={validateMutation.isPending}
        onConfirm={handleConfirm}
        onReject={handleReject}
      />
    </Box>
  );
}

function PriceConfirmModal({
  contract,
  values,
  loading,
  validation,
  validationLoading,
  onConfirm,
  onReject,
}: {
  contract: Freight.IContract;
  values: ShipmentFormValues | null;
  loading: boolean;
  validation: ShipmentValidation | null;
  validationLoading: boolean;
  onConfirm: () => void;
  onReject: () => void;
}) {
  const baseTotal = useMemo(
    () => (values ? computeShipmentTotal(contract, values) : null),
    [contract, values],
  );

  const overweightLines = validation?.overweightLines ?? [];
  const overweightSurchargeAmount = validation?.overweightSurchargeAmount ?? 0;
  const pairingErrors = validation?.pairingErrors ?? [];
  const hasPairingBlock = pairingErrors.length > 0;
  const capacityErrors = validation?.capacityErrors ?? [];
  const hasCapacityBlock = capacityErrors.length > 0;
  const containerClashErrors = validation?.containerClashErrors ?? [];
  const hasClashBlock = containerClashErrors.length > 0;
  const spaceErrors = validation?.spaceErrors ?? [];
  const hasSpaceBlock = spaceErrors.length > 0;

  // Authoritative server breakdown — the SAME BookingPricingService pass that
  // prices the booking on create, so it carries every line the booking will be
  // charged: rail freight, first/last mile trucking, overweight, hazard/reefer
  // and any other rule-engine surcharge.
  const serverTotal = useMemo(() => {
    const items = validation?.lineItems;
    if (!items?.length) return null;
    return {
      currency: validation?.currency ?? baseTotal?.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, baseTotal]);

  // Block confirm until the authoritative server price is in hand. The client
  // baseTotal fallback is display-only; booking on it (e.g. after a validation
  // error clears validationLoading with no data) would let the customer confirm
  // an un-validated, possibly wrong price.
  const confirmDisabled =
    loading ||
    validationLoading ||
    hasPairingBlock ||
    hasCapacityBlock ||
    hasClashBlock ||
    hasSpaceBlock ||
    !serverTotal;

  // Fallback while the server preview loads: the contract's frozen unit rates
  // (container/bulk + hazard/reefer only) with the overweight surcharge folded
  // in. Replaced by the full server breakdown the moment it arrives.
  const total = useMemo(() => {
    if (serverTotal) return serverTotal;
    if (!baseTotal) return null;
    if (!(overweightSurchargeAmount > 0)) return baseTotal;
    return {
      ...baseTotal,
      lines: [
        ...baseTotal.lines,
        {
          label: "Overweight surcharge",
          unitPrice: overweightSurchargeAmount,
          unit: "flat" as const,
          quantity: 1,
          amount: overweightSurchargeAmount,
        },
      ],
      total: baseTotal.total + overweightSurchargeAmount,
    };
  }, [serverTotal, baseTotal, overweightSurchargeAmount]);

  return (
    <Modal
      opened={Boolean(values)}
      onClose={onReject}
      closeOnClickOutside={!loading}
      closeOnEscape={!loading}
      withCloseButton={!loading}
      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} c="#10202F">
              Confirm shipment price
            </Text>
            <Text fz="xs" c="dimmed">
              Review the total before booking this shipment.
            </Text>
          </Box>
        </Group>
      }
    >
      {total ? (
        <Stack gap="md">
          {validationLoading && (
            <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>
          )}

          {hasPairingBlock && (
            <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>
          )}

          {hasCapacityBlock && (
            <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>
          )}

          {hasClashBlock && (
            <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>
          )}

          {hasSpaceBlock && (
            <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.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 ?? total?.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" }}>
            <Stack gap={10}>
              {total.lines.map((line, i) => (
                <Group key={i} justify="space-between" wrap="nowrap" gap="sm">
                  <Box style={{ minWidth: 0 }}>
                    <Text fz="sm" c="#10202F" fw={500}>
                      {line.label}
                    </Text>
                    <Text fz="xs" c="dimmed">
                      {line.quantity.toLocaleString()} ×{" "}
                      {line.unitPrice.toLocaleString()} {total.currency} ·{" "}
                      {formatRateUnit(line.unit)}
                    </Text>
                  </Box>
                  <Text
                    fz="sm"
                    fw={600}
                    c="#10202F"
                    style={{ whiteSpace: "nowrap" }}
                  >
                    {line.amount.toLocaleString()} {total.currency}
                  </Text>
                </Group>
              ))}
              {total.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} c="#10202F">
                {total.total.toLocaleString()}{" "}
                <Text span fz={16} fw={700} c="edr-muted">
                  {total.currency}
                </Text>
              </Text>
            </Group>
          </Paper>

          <Group justify="space-between" mt="xs">
            <Button
              variant="default"
              radius="md"
              leftSection={<X size={16} />}
              onClick={onReject}
              disabled={loading}
            >
              Reject &amp; edit
            </Button>
            <Button
              color="edr-green"
              radius="md"
              leftSection={<CheckCircle2 size={16} />}
              onClick={onConfirm}
              loading={loading}
              disabled={confirmDisabled}
            >
              Confirm &amp; book
            </Button>
          </Group>
        </Stack>
      ) : null}
    </Modal>
  );
}

function RouteStep({
  form,
  contract,
  routes,
}: {
  form: ShipmentForm;
  contract: Freight.IContract;
  routes: Freight.IContractRoute[];
}) {
  const multiRoute = routes.length > 1;
  return (
    <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 ? (
        <Controller
          name="contractRouteId"
          control={form.control}
          render={({ field, fieldState }) => (
            <SelectField
              field={field}
              error={fieldState.error}
              label="Contract route *"
              placeholder="Select a route..."
              data={routes.map((r) => ({
                value: r.id,
                label: `${r.originYard?.label ?? r.originYardId} → ${
                  r.destinationYard?.label ?? r.destinationYardId
                }`,
              }))}
            />
          )}
        />
      ) : (
        <Paper withBorder radius="md" p="md" style={{ borderColor: "#E6ECF2" }}>
          <Text fz={14} fw={600} c="#10202F">
            {routes[0]?.originYard?.label ?? "—"} →{" "}
            {routes[0]?.destinationYard?.label ?? "—"}
          </Text>
          <Text fz={12} c="dimmed" mt={2}>
            {contract.tradeDirection}
          </Text>
        </Paper>
      )}
    </StepCard>
  );
}

function ScheduleStep({
  form,
  contract,
  routes,
}: {
  form: ShipmentForm;
  contract: Freight.IContract;
  routes: Freight.IContractRoute[];
}) {
  const contractRouteId = form.watch("contractRouteId");
  const route = routes.find((r) => r.id === contractRouteId) ?? routes[0];

  // Read the cargo entered in the previous step so the day list reflects what
  // can actually be shipped (matching wagons + open train capacity).
  const isContainer = contract.freightType === "CONTAINER";
  const containerLines = form.watch("containers");
  const cargoWeightTons = form.watch("cargoWeightTons");
  const itemCount = form.watch("itemCount");

  const cargoQuery = useMemo<Freight.AvailableDaysForCargoQuery | null>(() => {
    if (!route?.originYardId || !route?.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: route.originYardId,
        destinationYardId: route.destinationYardId,
        freightType: "CONTAINER",
        containers,
      };
    }
    const tons = Number(cargoWeightTons || 0);
    if (tons <= 0) return null;
    return {
      originYardId: route.originYardId,
      destinationYardId: route.destinationYardId,
      freightType: "BULK",
      cargoTypeCode:
        contract.pricingBreakdown?.lineItems?.find((li) => li.cargoTypeCode)
          ?.cargoTypeCode ?? undefined,
      totalWeightTons: tons,
    };
    // itemCount is referenced so the query refreshes when a PER_ITEM cargo
    // amount changes (weight is the sizing input the backend uses).
  }, [
    route,
    isContainer,
    containerLines,
    cargoWeightTons,
    itemCount,
    contract.pricingBreakdown,
  ]);

  const isIntercity = contract.tradeDirection === "DOMESTIC";
  const { data: availableDays, isLoading } = useQuery({
    ...api.bookings.getAvailableDaysForCargo.queryOptions({
      input: cargoQuery ?? ({ freightType: "BULK" } as Freight.AvailableDaysForCargoQuery),
    }),
    enabled: cargoQuery !== null && !isIntercity,
  });

  if (isIntercity) {
    return (
      <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} />}>
          Your shipment rides the next import/export train passing through your
          corridor. Operations assign it to a train with free capacity — you
          will be notified when it is accepted and payment is due.
        </Alert>
        <Box mt="md">
          <StepLabel>Billing currency</StepLabel>
          <Text fz={13} c="dimmed" mt={6}>
            Intercity shipments are invoiced in <strong>ETB</strong>.
          </Text>
        </Box>
      </StepCard>
    );
  }

  return (
    <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 your cargo can be selected."
      />
      <Controller
        name="paymentCurrency"
        control={form.control}
        render={({ field }) => (
          <Box mb="lg">
            <StepLabel>Billing currency *</StepLabel>
            <Text fz={12.5} c="dimmed" mt={4} mb={10}>
              Your contract is quoted in USD. Pick the currency this shipment is
              invoiced in — the total is converted for you.
            </Text>
            <SegmentedControl
              value={field.value ?? "USD"}
              onChange={(v) => field.onChange(v)}
              data={[
                { label: "USD", value: "USD" },
                { label: "ETB", value: "ETB" },
              ]}
              color="edr-green"
              radius={10}
            />
          </Box>
        )}
      />
      {cargoQuery === null ? (
        <Alert color="yellow" variant="light" radius="md" icon={<AlertCircle size={16} />}>
          Enter your cargo details first — available shipment days depend on the
          wagons your cargo needs.
        </Alert>
      ) : (
        <Controller
          name="scheduledDate"
          control={form.control}
          render={({ field, fieldState }) => (
            <Box>
              <StepLabel>Shipment day *</StepLabel>
              <Box mt={10} w="100%">
                <OperationDatePicker
                  fullWidth
                  availableDays={availableDays ?? []}
                  isLoading={isLoading}
                  value={field.value ?? ""}
                  onChange={(d) => field.onChange(d)}
                />
              </Box>
              {fieldState.error?.message && (
                <Text fz="xs" c="red" mt={6}>
                  {fieldState.error.message}
                </Text>
              )}
            </Box>
          )}
        />
      )}
    </StepCard>
  );
}

function CargoStep({
  form,
  contract,
}: {
  form: ShipmentForm;
  contract: Freight.IContract;
}) {
  const isContainer = contract.freightType === "CONTAINER";
  // Sizes enabled by the contract scope.
  const sizes = useMemo(
    () =>
      (contract.cargoScope ?? [])
        .map((s) => s.containerSize)
        .filter((s): s is "20ft" | "40ft" => s === "20ft" || s === "40ft"),
    [contract],
  );

  // ONE_TIME split-remainder mode: the previous booking was split on train
  // capacity, so the capacity endpoint reports the outstanding remainder — the
  // EXACT quantity this booking must take (the API rejects anything else).
  const { data: capacityLines = [] } = useQuery({
    queryKey: ["contract-capacity", contract.id],
    queryFn: () => contractsService.getCapacity(contract.id),
    enabled: contract.contractKind === "ONE_TIME",
  });
  const remainderLines = useMemo(
    () =>
      contract.contractKind === "ONE_TIME"
        ? capacityLines.filter((l) => (l.remaining ?? 0) > 0)
        : [],
    [contract.contractKind, capacityLines],
  );
  const remainderMode = remainderLines.length > 0;

  // Bulk: prefill the exact outstanding remainder once (the customer cannot
  // book a different amount anyway). Containers keep manual entry — each unit
  // still needs its container number.
  useEffect(() => {
    if (!remainderMode || isContainer) return;
    const field =
      bulkUnitOfMeasure(contract) === "PER_ITEM" ? "itemCount" : "cargoWeightTons";
    if (!form.getValues(field)) {
      form.setValue(field, String(remainderLines[0].remaining), {
        shouldValidate: false,
      });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [remainderMode]);

  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}>
        Your 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} ${
                  bulkUnitOfMeasure(contract) === "PER_ITEM" ? "items" : "tons"
                }`,
          )
          .join(", ")}
        . A different quantity will be rejected, and no other booking can be
        created on this contract.
      </Text>
    </Alert>
  ) : null;

  // Seed one shipment line per contracted size exactly once (never during
  // render — appending in render loops). Subsequent renders reuse the lines.
  const seededRef = useRef(false);
  useEffect(() => {
    if (seededRef.current || !isContainer) return;
    seededRef.current = true;
    const existing = form.getValues("containers") ?? [];
    if (existing.length > 0) return;
    form.setValue(
      "containers",
      sizes.map((size) => ({
        containerSize: size,
        quantity: "1",
        hazardousQuantity: "0",
        reeferQuantity: "0",
        returnQuantity: "0",
        units: [emptyUnit()],
      })),
      { shouldValidate: false },
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const lines = form.watch("containers") ?? [];

  // 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: sizes,
    includeHazardous: contract.isHazardous ?? false,
    includeReefer: contract.isReefer ?? false,
    includeReturn: contract.equipmentReturn === "WITH_RETURN",
  };

  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.
    const current = form.getValues("containers") ?? [];
    const next = sizes.map((size) => {
      const imported = rows.filter((r) => r.containerSize === size);
      if (imported.length === 0) {
        return (
          current.find((l) => l.containerSize === size) ?? {
            containerSize: size,
            quantity: "1",
            hazardousQuantity: "0",
            reeferQuantity: "0",
            returnQuantity: "0",
            units: [emptyUnit()],
          }
        );
      }
      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 already 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: r.vgmTons,
          isHazardous: Boolean(r.hazardous),
          isReefer: Boolean(r.reefer),
          isReturn: Boolean(r.withReturn),
        })),
      };
    });
    form.setValue("containers", next, { shouldValidate: true, shouldDirty: true });
    setImportErrors([]);
    setImportSummary(`Imported ${rows.length} container(s) from ${file.name}.`);
  };

  if (isContainer) {
    return (
      <StepCard>
        <StepHeader
          icon={<Package size={22} />}
          title="Cargo Details"
          description="Enter the quantity and per-container details for each size in your contract scope."
        />
        <Stack gap={18}>
          {sizes.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} c="#10202F">
                    Import containers from Excel
                  </Text>
                  <Text fz={12} c="dimmed">
                    One row per container. Importing fills the lines below for
                    the sizes in your 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 />
          <Controller
            name="cargoDescription"
            control={form.control}
            render={({ field, fieldState }) => (
              <Textarea
                label="Cargo description *"
                description="What do the containers carry on this shipment?"
                placeholder="e.g. Electronics, garments, machinery spare parts…"
                value={field.value ?? ""}
                onChange={(e) => field.onChange(e.currentTarget.value)}
                onBlur={field.onBlur}
                error={fieldState.error?.message}
                radius={10}
                autosize
                minRows={2}
                maxRows={4}
                styles={fieldStyles}
              />
            )}
          />
          {lines.map((line, index) => (
            <ContainerLineEditor
              key={line.containerSize}
              form={form}
              index={index}
              size={line.containerSize}
              isHazardous={contract.isHazardous}
              isReefer={contract.isReefer}
              withReturnService={contract.equipmentReturn === "WITH_RETURN"}
            />
          ))}
          {sizes.length === 0 && (
            <Text fz="sm" c="dimmed">
              This contract has no container sizes in scope.
            </Text>
          )}
          {(() => {
            const ft20 = lines
              .filter((l) => l.containerSize === "20ft")
              .reduce((sum, l) => sum + Number(l.quantity || 0), 0);
            if (ft20 % 2 !== 1) return null;
            return (
              <Alert
                color="red"
                variant="light"
                radius="md"
                icon={<AlertCircle size={16} />}
                title={`Odd number of 20ft containers (${ft20})`}
              >
                <Text fz={13}>
                  20ft containers travel two per wagon, so they must be booked in
                  even numbers. Please add one more 20ft container or remove one
                  (e.g. book {ft20 + 1} or {ft20 - 1} instead of {ft20}) — the
                  booking cannot be submitted with an unpaired 20ft container.
                </Text>
              </Alert>
            );
          })()}
        </Stack>
      </StepCard>
    );
  }

  return (
    <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} />
        <Controller
          name="cargoWeightTons"
          control={form.control}
          render={({ field, fieldState }) => (
            <TextInput
              {...field}
              type="number"
              onKeyDown={blockNegative}
              label="Quantity (tons)"
              placeholder="e.g. 1200"
              min={0}
              step={0.01}
              error={fieldState.error?.message}
              radius={10}
              styles={fieldStyles}
            />
          )}
        />
        <Controller
          name="itemCount"
          control={form.control}
          render={({ field, fieldState }) => (
            <TextInput
              {...field}
              type="number"
              onKeyDown={blockNegative}
              label="Item count (if applicable)"
              placeholder="e.g. 500"
              min={0}
              step={1}
              error={fieldState.error?.message}
              radius={10}
              styles={fieldStyles}
            />
          )}
        />
        {contract.isHazardous && (
          <Controller
            name="bulkHazardousQuantity"
            control={form.control}
            render={({ field, fieldState }) => (
              <TextInput
                {...field}
                type="number"
                onKeyDown={blockNegative}
                label="Hazardous quantity"
                min={0}
                step={1}
                error={fieldState.error?.message}
                radius={10}
                styles={fieldStyles}
              />
            )}
          />
        )}
        {contract.isReefer && (
          <Controller
            name="bulkReeferQuantity"
            control={form.control}
            render={({ field, fieldState }) => (
              <TextInput
                {...field}
                type="number"
                onKeyDown={blockNegative}
                label="Refrigerated quantity"
                min={0}
                step={1}
                error={fieldState.error?.message}
                radius={10}
                styles={fieldStyles}
              />
            )}
          />
        )}
      </Stack>
    </StepCard>
  );
}

function EquipmentReturnStep({ form }: { form: ShipmentForm }) {
  return (
    <StepCard>
      <StepHeader
        icon={<Repeat size={22} />}
        title="Equipment Return"
        description="Choose whether the empty container(s) come back to EDR after unloading."
      />
      <Controller
        name="withReturn"
        control={form.control}
        render={({ field }) => {
          const on = field.value ?? false;
          return (
            <Paper
              withBorder
              radius="md"
              p="md"
              style={{
                borderColor: on ? "#CDEBDD" : "#E6ECF2",
                background: on ? "#F6FBF8" : "white",
                cursor: "pointer",
                transition: "border-color 150ms ease, background 150ms ease",
              }}
              onClick={() => field.onChange(!on)}
            >
              <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: on ? "#ECF6F1" : "#F1F4F7",
                      color: on ? "#0A6F4D" : "#6B7C8E",
                    }}
                  >
                    <Repeat size={18} />
                  </Box>
                  <Box>
                    <Text fz={14} fw={700} c="#10202F">
                      With return
                    </Text>
                    <Text fz={12} c="dimmed" style={{ lineHeight: 1.4 }}>
                      {on
                        ? "Container(s) returned to EDR after unloading."
                        : "Container(s) retained by you after delivery."}
                    </Text>
                  </Box>
                </Group>
                <Switch
                  size="md"
                  color="edr-green"
                  aria-label="With return"
                  checked={on}
                  onChange={(e) => field.onChange(e.currentTarget.checked)}
                  onClick={(e) => e.stopPropagation()}
                  style={{ flexShrink: 0 }}
                />
              </Group>
            </Paper>
          );
        }}
      />
    </StepCard>
  );
}

function NotesSection({ form }: { form: ShipmentForm }) {
  return (
    <StepCard>
      <Controller
        name="notes"
        control={form.control}
        render={({ field }) => (
          <Textarea
            {...field}
            label="Additional notes"
            placeholder="Any special instructions for this shipment…"
            rows={3}
            radius="md"
          />
        )}
      />
    </StepCard>
  );
}

/** A blank container row — handling switches start off. */
const emptyUnit = () => ({
  containerNumber: "",
  sealNumber: "",
  vgmTons: "",
  isHazardous: false,
  isReefer: false,
  isReturn: false,
});

function ContainerLineEditor({
  form,
  index,
  size,
  isHazardous,
  isReefer,
  withReturnService,
}: {
  form: ShipmentForm;
  index: number;
  size: "20ft" | "40ft";
  isHazardous: boolean;
  isReefer: boolean;
  /** Contract opted into empty-container return — capture the per-line quantity. */
  withReturnService?: boolean;
}) {
  const line = form.watch(`containers.${index}`);
  const quantity = Number(line?.quantity || 0);
  const units = line?.units ?? [];

  // Keep the units array length in sync with the entered quantity.
  const syncUnits = (qty: number) => {
    const current = form.getValues(`containers.${index}.units`) ?? [];
    const next = [...current];
    while (next.length < qty)
      next.push({
        containerNumber: "",
        sealNumber: "",
        vgmTons: "",
        isHazardous: false,
        isReefer: false,
        isReturn: false,
      });
    next.length = Math.max(0, qty);
    form.setValue(`containers.${index}.units`, next, { shouldValidate: false });
    syncHandlingCounts(next);
  };

  /**
   * Line totals are a roll-up of the per-container switches — the count is
   * however many containers ticked each service. Kept in form state so the
   * price estimate and the submitted payload stay in step with the switches.
   */
  const syncHandlingCounts = (
    units: Array<{ isHazardous?: boolean; isReefer?: boolean; isReturn?: boolean }>,
  ) => {
    const set = (
      key: "hazardousQuantity" | "reeferQuantity" | "returnQuantity",
      count: number,
    ) =>
      form.setValue(`containers.${index}.${key}`, String(count), {
        shouldDirty: true,
        shouldValidate: true,
      });
    set("hazardousQuantity", units.filter((u) => u.isHazardous).length);
    set("reeferQuantity", units.filter((u) => u.isReefer).length);
    set("returnQuantity", units.filter((u) => u.isReturn).length);
  };

  /** Flip one container's handling switch, then re-roll the line totals. */
  const toggleUnitHandling = (
    unitIndex: number,
    key: "isHazardous" | "isReefer" | "isReturn",
    on: boolean,
  ) => {
    form.setValue(`containers.${index}.units.${unitIndex}.${key}`, on, {
      shouldDirty: true,
    });
    syncHandlingCounts(form.getValues(`containers.${index}.units`) ?? []);
  };

  /**
   * The handling columns offered on each container row — only the services this
   * contract was created with, since the server rejects quantities for the others.
   */
  const handlingColumns = [
    isHazardous && {
      key: "isHazardous" as const,
      label: "Hazardous",
      icon: <Flame size={14} />,
      color: "#C0392B",
    },
    isReefer && {
      key: "isReefer" as const,
      label: "Refrigerated",
      icon: <Snowflake size={14} />,
      color: "#2E5B96",
    },
    withReturnService && {
      key: "isReturn" as const,
      label: "With return",
      icon: <Repeat size={14} />,
      color: "#0A6F4D",
    },
  ].filter(Boolean) as Array<{
    key: "isHazardous" | "isReefer" | "isReturn";
    label: string;
    icon: ReactNode;
    color: string;
  }>;

  return (
    <Box
      className="rounded-xl"
      style={{ border: "1px solid #E6ECF2", padding: 16 }}
    >
      <Text fz={14} fw={700} c="#10202F" mb={10}>
        {size} containers
      </Text>
      <Box maw={220} mb={14}>
        <Controller
          name={`containers.${index}.quantity`}
          control={form.control}
          render={({ field, fieldState }) => (
            <TextInput
              {...field}
              type="number"
              onKeyDown={blockNegative}
              label="Quantity *"
              min={0}
              error={fieldState.error?.message}
              radius={10}
              styles={fieldStyles}
              onChange={(e) => {
                field.onChange(e.currentTarget.value);
                const qty = Number(e.currentTarget.value || 0);
                syncUnits(qty);
              }}
            />
          )}
        />
      </Box>

      <StepLabel>Per-container details</StepLabel>
      {handlingColumns.length > 0 && quantity > 0 ? (
        <Text fz={11} c="#5B6B7B" mt={4}>
          Tick the services each individual container needs — charges apply only
          to the containers you tick.
        </Text>
      ) : null}
      <Stack gap={10} mt={8}>
        {/* Header row — input labels + handling-service labels, one aligned
            grid shared by every unit row below. */}
        {Math.max(quantity, 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>
        )}
        {Array.from({ length: Math.max(quantity, units.length) }).map((_, u) => (
          <Group key={u} gap={10} wrap="nowrap" align="flex-start">
            <Controller
              name={`containers.${index}.units.${u}.containerNumber`}
              control={form.control}
              render={({ field, fieldState }) => (
                <TextInput
                  {...field}
                  onChange={(e) =>
                    field.onChange(e.currentTarget.value.toUpperCase())
                  }
                  placeholder="e.g. MSCU1234567"
                  error={fieldState.error?.message}
                  radius={10}
                  styles={fieldStyles}
                  style={{ flex: 1 }}
                />
              )}
            />
            <Controller
              name={`containers.${index}.units.${u}.sealNumber`}
              control={form.control}
              render={({ field }) => (
                <TextInput
                  {...field}
                  placeholder="Optional"
                  radius={10}
                  styles={fieldStyles}
                  style={{ flex: 1 }}
                />
              )}
            />
            <Controller
              name={`containers.${index}.units.${u}.vgmTons`}
              control={form.control}
              render={({ field, fieldState }) => (
                <TextInput
                  {...field}
                  type="number"
                  onKeyDown={blockNegative}
                  placeholder="e.g. 24.5"
                  min={0}
                  step={0.01}
                  error={fieldState.error?.message}
                  radius={10}
                  styles={fieldStyles}
                  style={{ flex: 1 }}
                />
              )}
            />
            {handlingColumns.map((col) => (
              <Controller
                key={col.key}
                name={`containers.${index}.units.${u}.${col.key}`}
                control={form.control}
                render={({ field }) => (
                  <Box
                    style={{
                      width: 96,
                      flexShrink: 0,
                      height: 42,
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                    }}
                  >
                    <Switch
                      checked={Boolean(field.value)}
                      aria-label={`${col.label} — container ${u + 1}`}
                      onChange={(e) =>
                        toggleUnitHandling(u, col.key, e.currentTarget.checked)
                      }
                      size="sm"
                    />
                  </Box>
                )}
              />
            ))}
          </Group>
        ))}
      </Stack>
    </Box>
  );
}
