import {
  Alert,
  Badge,
  Button,
  Checkbox,
  Divider,
  Grid,
  Group,
  Modal,
  Progress,
  ScrollArea,
  Select,
  Stack,
  Text,
  Tooltip,
} from "@mantine/core";
import { useMutation, useQuery } from "@tanstack/react-query";
import { isAxiosError } from "axios";
import { AlertTriangle, ArrowLeftRight, History, MapPin, Minus, Plus } from "lucide-react";
import { useEffect, useMemo, useState } from "react";

import { api } from "@/services/api";
import type { ConsistWagonRef, ScheduleConsist } from "@/services/trainBuilder.service";
import { useToast } from "@/hooks/use-toast";

const parseError = (error: unknown, fallback: string) => {
  if (isAxiosError(error)) {
    const message = error.response?.data?.message;
    if (Array.isArray(message)) return message.join(", ");
    if (typeof message === "string") return message;
  }
  return fallback;
};

const tareOf = (w: ConsistWagonRef) => w.wagonType?.tareWeightTons ?? 0;
const lengthOf = (w: ConsistWagonRef) => w.wagonType?.lengthMeters ?? 0;
const round2 = (v: number) => Math.round(v * 100) / 100;

type ConsistWagon = ScheduleConsist["wagons"][number];

/**
 * Adjust the built train's consist from a schedule: trim free wagons (their
 * tare no longer rides — the fix when gross weight beats the pull limit),
 * couple extra yard wagons while weight/length headroom remains, or SWITCH a
 * wagon for a same-type replacement — the replacement inherits the slot, cargo
 * included, which is the only way a loaded wagon leaves the train. Works
 * before departure and mid-route while the train stands at a checkpointed
 * stop. Changes are permanent on the train and logged on the schedule.
 */
export default function AdjustConsistModal({
  scheduleId,
  opened,
  onClose,
}: AdjustConsistModalProps) {
  const { toast } = useToast();
  const [removeIds, setRemoveIds] = useState<string[]>([]);
  const [addIds, setAddIds] = useState<string[]>([]);
  // fromWagonId → toWagonId. A switch is same-type, so it never moves the
  // weight/length/slot projections — it only changes which steel rides.
  const [switchMap, setSwitchMap] = useState<Record<string, string>>({});

  const consistQuery = useQuery(
    api.trainScheduling.scheduleConsist.queryOptions({
      input: { scheduleId },
      enabled: opened && Boolean(scheduleId),
    }),
  );
  const adjust = useMutation(api.trainScheduling.adjustConsist.mutationOptions());
  const data = consistQuery.data;

  useEffect(() => {
    if (opened) {
      setRemoveIds([]);
      setAddIds([]);
      setSwitchMap({});
    }
  }, [opened]);

  const switchCount = Object.keys(switchMap).length;
  const usedReplacementIds = useMemo(
    () => new Set(Object.values(switchMap)),
    [switchMap],
  );

  // Live projection: gross = cargo + tare of (consist − trims + adds), plus
  // the schedule's wagon-slot picture — the consist IS the booking capacity
  // (weight/length only bind while assembling the consist), so trims/adds
  // move the FULL line in real time. Switches are same-type and cancel out.
  const projection = useMemo(() => {
    if (!data) return null;
    const removed = new Set(removeIds);
    const keptTare = data.wagons
      .filter((w) => !removed.has(w.id))
      .reduce((s, w) => s + tareOf(w), 0);
    const keptLength = data.wagons
      .filter((w) => !removed.has(w.id))
      .reduce((s, w) => s + lengthOf(w), 0);
    const addedWagons = data.addableWagons.filter((w) => addIds.includes(w.id));
    const tare = keptTare + addedWagons.reduce((s, w) => s + tareOf(w), 0);
    const length = keptLength + addedWagons.reduce((s, w) => s + lengthOf(w), 0);
    const gross = round2(data.totals.cargoTons + tare);
    const wagonCount = data.totals.wagonCount - removeIds.length + addIds.length;
    const cap = data.scheduleCapacity;
    const freeSlots = cap ? wagonCount - cap.allocatedWagons : null;
    return {
      wagonCount,
      tare: round2(tare),
      gross,
      length: round2(length),
      grossPct: data.limits.pullCapTons
        ? Math.round((gross / data.limits.pullCapTons) * 100)
        : null,
      lengthPct: data.limits.lengthCapMeters
        ? Math.round((length / data.limits.lengthCapMeters) * 100)
        : null,
      overWeight: data.limits.pullCapTons > 0 && gross > data.limits.pullCapTons,
      overLength:
        data.limits.lengthCapMeters > 0 && length > data.limits.lengthCapMeters,
      slots:
        cap && freeSlots != null
          ? {
              allocated: cap.allocatedWagons,
              free: freeSlots,
              pct:
                wagonCount > 0
                  ? Math.round((cap.allocatedWagons / wagonCount) * 100)
                  : null,
              isFullNow: cap.bookingWindowStatus === "FULL",
              willBeFull: freeSlots <= 0,
              overAllocated: freeSlots < 0,
              willReopen: cap.bookingWindowStatus === "FULL" && freeSlots > 0,
            }
          : null,
    };
  }, [data, removeIds, addIds]);

  const hasChanges = removeIds.length > 0 || addIds.length > 0 || switchCount > 0;

  const toggle = (setter: typeof setRemoveIds) => (id: string, checked: boolean) =>
    setter((prev) => (checked ? [...prev, id] : prev.filter((x) => x !== id)));

  // Same-type replacements standing at the current stop, minus wagons already
  // spoken for by another switch or a couple selection.
  const switchOptionsFor = (wagon: ConsistWagon) =>
    (data?.addableWagons ?? [])
      .filter(
        (candidate) =>
          candidate.wagonType?.id === wagon.wagonType?.id &&
          !addIds.includes(candidate.id) &&
          (!usedReplacementIds.has(candidate.id) ||
            switchMap[wagon.id] === candidate.id),
      )
      .map((candidate) => ({ value: candidate.id, label: candidate.wagonNumber }));

  const setSwitch = (fromId: string, toId: string | null) =>
    setSwitchMap((prev) => {
      const next = { ...prev };
      if (toId) next[fromId] = toId;
      else delete next[fromId];
      return next;
    });

  const handleSubmit = async () => {
    if (!hasChanges) return;
    try {
      const result = await adjust.mutateAsync({
        scheduleId,
        payload: {
          ...(addIds.length ? { addWagonIds: addIds } : {}),
          ...(removeIds.length ? { removeWagonIds: removeIds } : {}),
          ...(switchCount
            ? {
                switches: Object.entries(switchMap).map(([fromWagonId, toWagonId]) => ({
                  fromWagonId,
                  toWagonId,
                })),
              }
            : {}),
        },
      });
      toast({
        title: `Consist updated — ${[
          removeIds.length ? `${removeIds.length} trimmed` : "",
          addIds.length ? `${addIds.length} added` : "",
          switchCount ? `${switchCount} switched` : "",
        ]
          .filter(Boolean)
          .join(", ")}`,
      });
      // Schedule-impact warnings from the API: window reopened / now FULL /
      // consist trimmed below what bookings already hold.
      for (const warning of result.warnings ?? []) {
        toast({
          title: "Schedule capacity",
          description: warning,
          duration: 8000,
          ...(warning.includes("over capacity")
            ? { variant: "destructive" as const }
            : {}),
        });
      }
      setRemoveIds([]);
      setAddIds([]);
      setSwitchMap({});
    } catch (err) {
      toast({
        title: "Adjustment failed",
        description: parseError(err, "Could not adjust the consist"),
        variant: "destructive",
      });
    }
  };

  return (
    <Modal
      opened={opened}
      onClose={onClose}
      title={
        <Text fw={600}>
          Adjust consist{data ? ` — train ${data.train.code}` : ""}
        </Text>
      }
      radius="lg"
      size={920}
      centered
    >
      {consistQuery.isLoading || !data ? (
        <Text py="lg" ta="center" c="dimmed" size="sm">
          {consistQuery.isError
            ? "This schedule has no built train to adjust."
            : "Loading consist…"}
        </Text>
      ) : (
        <Stack gap="md">
          {data.currentStop?.isMidRoute ? (
            <Alert color="blue" icon={<MapPin size={16} />}>
              Standing at <strong>{data.currentStop.label}</strong> — mid-route
              consist work is open: couple or switch wagons standing at this
              stop, trim wagons whose cargo was offloaded here. Detached wagons
              stay at {data.currentStop.label}.
            </Alert>
          ) : null}
          {!data.editable ? (
            <Alert color="yellow" icon={<AlertTriangle size={16} />}>
              {data.schedule.status === "DISPATCHED"
                ? "The train is rolling — consist changes are only possible while it stands at a route stop."
                : "The consist can no longer be adjusted — the run is over."}
            </Alert>
          ) : null}

          <Grid gap="md">
            <Grid.Col span={{ base: 12, sm: 6 }}>
              <LimitGauge
                label="Gross weight"
                detail={`${data.totals.cargoTons}T cargo + ${projection?.tare}T tare = ${projection?.gross}T of ${data.limits.pullCapTons}T (limit ${data.limits.maxPullWeightTons}T + ${data.limits.overageToleranceTons}T tolerance)`}
                pct={projection?.grossPct ?? null}
                over={projection?.overWeight ?? false}
              />
            </Grid.Col>
            <Grid.Col span={{ base: 12, sm: 6 }}>
              <LimitGauge
                label="Consist length"
                detail={`${projection?.length}m of ${data.limits.lengthCapMeters}m (limit ${data.limits.maxTrainLengthMeters}m + ${data.limits.overageToleranceMeters}m tolerance)`}
                pct={projection?.lengthPct ?? null}
                over={projection?.overLength ?? false}
              />
            </Grid.Col>
            {projection?.slots ? (
              <Grid.Col span={12}>
                <LimitGauge
                  label="Booking slots — the consist is the schedule's capacity"
                  detail={`${projection.slots.allocated} of ${projection.wagonCount} projected wagon slot(s) held by bookings${
                    projection.slots.free > 0
                      ? ` — ${projection.slots.free} free`
                      : projection.slots.free === 0
                        ? " — none free (FULL)"
                        : ""
                  }`}
                  pct={projection.slots.pct}
                  over={projection.slots.overAllocated}
                />
              </Grid.Col>
            ) : null}
          </Grid>

          {projection?.slots?.isFullNow && !hasChanges ? (
            <Alert color="yellow" icon={<AlertTriangle size={16} />}>
              This schedule is FULL — all {projection.wagonCount} wagon slots are
              taken. You can still edit the train: coupling wagons adds capacity
              and reopens booking; trimming free wagons keeps it FULL.
            </Alert>
          ) : null}
          {hasChanges && projection?.slots?.overAllocated ? (
            <Alert color="red" icon={<AlertTriangle size={16} />}>
              This change leaves {-projection.slots.free} booked wagon(s) without
              a slot — bookings already hold {projection.slots.allocated} of the{" "}
              {projection.wagonCount} remaining. You can apply it, but couple
              wagons back or free bookings before departure.
            </Alert>
          ) : null}
          {hasChanges &&
          projection?.slots &&
          !projection.slots.overAllocated &&
          projection.slots.willBeFull &&
          !projection.slots.isFullNow ? (
            <Alert color="yellow" icon={<AlertTriangle size={16} />}>
              This change takes the last free wagon slot — the schedule becomes
              FULL and stops accepting bookings.
            </Alert>
          ) : null}
          {hasChanges && projection?.slots?.willReopen ? (
            <Alert color="blue" icon={<AlertTriangle size={16} />}>
              This schedule is currently FULL — applying frees{" "}
              {projection.slots.free} wagon slot(s) and reopens its booking
              window.
            </Alert>
          ) : null}

          <Grid gap="md">
            <Grid.Col span={{ base: 12, md: 7 }}>
              <Stack gap="xs">
                <Group gap={6}>
                  <Minus size={14} />
                  <Text size="sm" fw={600}>
                    Coupled wagons ({data.totals.wagonCount})
                  </Text>
                </Group>
                <Text size="xs" c="dimmed">
                  Trim only wagons carrying nothing beyond this stop. A loaded
                  wagon can't leave — but it can be <strong>switched</strong>:
                  the same-type replacement takes its position and its cargo
                  slot. Detaching is permanent.
                </Text>
                <ScrollArea.Autosize mah={280} type="auto">
                  <Stack gap={4}>
                    {data.wagons.map((wagon) => (
                      <CoupledWagonRow
                        key={wagon.id}
                        wagon={wagon}
                        checked={removeIds.includes(wagon.id)}
                        editable={data.editable}
                        switchValue={switchMap[wagon.id] ?? null}
                        switchOptions={switchOptionsFor(wagon)}
                        onToggleRemove={toggle(setRemoveIds)}
                        onSwitch={setSwitch}
                      />
                    ))}
                  </Stack>
                </ScrollArea.Autosize>
              </Stack>
            </Grid.Col>
            <Grid.Col span={{ base: 12, md: 5 }}>
              <Stack gap="xs">
                <Group gap={6}>
                  <Plus size={14} />
                  <Text size="sm" fw={600}>
                    Couple yard wagons ({data.addableWagons.length} available)
                  </Text>
                </Group>
                <Text size="xs" c="dimmed">
                  AVAILABLE wagons standing at{" "}
                  {data.currentStop?.label ?? "the train's yard"}. Blocked when
                  they push gross weight or length past the locomotive limits
                  incl. tolerance.
                </Text>
                <ScrollArea.Autosize mah={280} type="auto">
                  <Stack gap={4}>
                    {data.addableWagons.length ? (
                      data.addableWagons.map((wagon) => {
                        const takenBySwitch = usedReplacementIds.has(wagon.id);
                        return (
                          <AddableWagonRow
                            key={wagon.id}
                            wagon={wagon}
                            checked={addIds.includes(wagon.id)}
                            disabled={!data.editable || takenBySwitch}
                            badge={takenBySwitch ? "Switch target" : null}
                            onToggle={toggle(setAddIds)}
                          />
                        );
                      })
                    ) : (
                      <Text size="sm" c="dimmed" py="sm" ta="center">
                        No available wagons at this stop
                      </Text>
                    )}
                  </Stack>
                </ScrollArea.Autosize>
              </Stack>
            </Grid.Col>
          </Grid>

          {switchCount ? (
            <Alert color="blue" icon={<ArrowLeftRight size={16} />} py={8}>
              {Object.entries(switchMap)
                .map(([fromId, toId]) => {
                  const from = data.wagons.find((w) => w.id === fromId);
                  const to = data.addableWagons.find((w) => w.id === toId);
                  return `${from?.wagonNumber ?? "?"} → ${to?.wagonNumber ?? "?"}`;
                })
                .join(" · ")}{" "}
              — cargo allocations move to the replacement wagon(s).
            </Alert>
          ) : null}

          {data.adjustments.length ? (
            <>
              <Divider />
              <Stack gap={4}>
                <Group gap={6}>
                  <History size={14} />
                  <Text size="sm" fw={600}>
                    Adjustment history
                  </Text>
                </Group>
                <ScrollArea.Autosize mah={120} type="auto">
                  <Stack gap={2}>
                    {data.adjustments.map((log) => (
                      <Group key={log.id} gap="xs">
                        <Badge
                          size="xs"
                          variant="light"
                          color={
                            log.action === "ADD"
                              ? "edr-green"
                              : log.action === "SWITCH"
                                ? "blue"
                                : "red"
                          }
                        >
                          {log.action === "ADD"
                            ? "Added"
                            : log.action === "SWITCH"
                              ? "Switched"
                              : "Trimmed"}
                        </Badge>
                        <Text size="xs" ff="monospace">
                          {log.wagonNumber}
                        </Text>
                        <Text size="xs" c="dimmed">
                          {new Date(log.occurredAt).toLocaleString()}
                        </Text>
                      </Group>
                    ))}
                  </Stack>
                </ScrollArea.Autosize>
              </Stack>
            </>
          ) : null}

          <Group justify="space-between">
            <Text size="xs" c="dimmed">
              Projected consist: {projection?.wagonCount} wagons
            </Text>
            <Group>
              <Button variant="default" onClick={onClose}>
                Close
              </Button>
              <Button
                loading={adjust.isPending}
                disabled={
                  !data.editable ||
                  !hasChanges ||
                  (addIds.length > 0 && (projection?.overWeight || projection?.overLength))
                }
                onClick={handleSubmit}
              >
                Apply{" "}
                {[
                  removeIds.length ? `−${removeIds.length}` : "",
                  addIds.length ? `+${addIds.length}` : "",
                  switchCount ? `⇄${switchCount}` : "",
                ]
                  .filter(Boolean)
                  .join(" / ")}
              </Button>
            </Group>
          </Group>
        </Stack>
      )}
    </Modal>
  );
}

export interface AdjustConsistModalProps {
  scheduleId: string;
  opened: boolean;
  onClose: () => void;
}

function LimitGauge({
  label,
  detail,
  pct,
  over,
}: {
  label: string;
  detail: string;
  pct: number | null;
  over: boolean;
}) {
  return (
    <Stack gap={4}>
      <Group justify="space-between">
        <Text size="xs" fw={600}>
          {label}
        </Text>
        <Text size="xs" fw={700} c={over ? "red.7" : "edr-green.7"}>
          {pct != null ? `${pct}%` : "—"}
        </Text>
      </Group>
      <Progress
        value={Math.min(pct ?? 0, 100)}
        size="md"
        radius="xl"
        color={over ? "red" : (pct ?? 0) > 85 ? "yellow" : "edr-green"}
        striped={over}
        animated={over}
      />
      <Text size="xs" c="dimmed">
        {detail}
      </Text>
    </Stack>
  );
}

/** Coupled row: trim checkbox (reason-badged when blocked) + switch picker. */
function CoupledWagonRow({
  wagon,
  checked,
  editable,
  switchValue,
  switchOptions,
  onToggleRemove,
  onSwitch,
}: {
  wagon: ConsistWagon;
  checked: boolean;
  editable: boolean;
  switchValue: string | null;
  switchOptions: Array<{ value: string; label: string }>;
  onToggleRemove: (id: string, checked: boolean) => void;
  onSwitch: (fromId: string, toId: string | null) => void;
}) {
  const badge = wagon.loaded ? "Loaded" : !wagon.removable ? "Pinned" : null;
  const checkbox = (
    <Checkbox
      size="sm"
      checked={checked}
      disabled={!editable || !wagon.removable || Boolean(switchValue)}
      onChange={(e) => onToggleRemove(wagon.id, e.currentTarget.checked)}
      aria-label={`Trim wagon ${wagon.wagonNumber}`}
    />
  );
  return (
    <Group
      gap="sm"
      wrap="nowrap"
      p={6}
      style={{
        border: switchValue
          ? "1px solid var(--mantine-color-blue-4)"
          : "1px solid var(--mantine-color-gray-2)",
        borderRadius: "var(--mantine-radius-md)",
        background: switchValue ? "var(--mantine-color-blue-0)" : undefined,
      }}
    >
      {wagon.blockReason ? (
        <Tooltip label={wagon.blockReason} withArrow>
          <span>{checkbox}</span>
        </Tooltip>
      ) : (
        checkbox
      )}
      <Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
        <Text size="sm" fw={600} ff="monospace" truncate>
          {wagon.wagonNumber}
        </Text>
        <Text size="xs" c="dimmed" truncate>
          {wagon.wagonType
            ? `${wagon.wagonType.code} · ${wagon.wagonType.tareWeightTons}T tare · ${wagon.wagonType.lengthMeters}m`
            : "Unknown type"}
        </Text>
      </Stack>
      {badge ? (
        <Badge size="xs" variant="light" color={badge === "Loaded" ? "orange" : "gray"}>
          {badge}
        </Badge>
      ) : null}
      {editable && wagon.switchable && switchOptions.length ? (
        <Select
          size="xs"
          w={148}
          placeholder="Switch with…"
          leftSection={<ArrowLeftRight size={12} />}
          data={switchOptions}
          value={switchValue}
          onChange={(toId) => onSwitch(wagon.id, toId)}
          clearable
          searchable
          disabled={checked}
          aria-label={`Switch wagon ${wagon.wagonNumber}`}
        />
      ) : null}
    </Group>
  );
}

function AddableWagonRow({
  wagon,
  checked,
  disabled,
  badge,
  onToggle,
}: {
  wagon: ConsistWagonRef;
  checked: boolean;
  disabled: boolean;
  badge: string | null;
  onToggle: (id: string, checked: boolean) => void;
}) {
  return (
    <Group
      gap="sm"
      wrap="nowrap"
      p={6}
      style={{
        border: "1px solid var(--mantine-color-gray-2)",
        borderRadius: "var(--mantine-radius-md)",
        opacity: disabled && !badge ? 0.7 : 1,
      }}
    >
      <Checkbox
        size="sm"
        checked={checked}
        disabled={disabled}
        onChange={(e) => onToggle(wagon.id, e.currentTarget.checked)}
        aria-label={`Select wagon ${wagon.wagonNumber}`}
      />
      <Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
        <Text size="sm" fw={600} ff="monospace" truncate>
          {wagon.wagonNumber}
        </Text>
        <Text size="xs" c="dimmed" truncate>
          {wagon.wagonType
            ? `${wagon.wagonType.code} · ${wagon.wagonType.tareWeightTons}T tare · ${wagon.wagonType.lengthMeters}m`
            : "Unknown type"}
        </Text>
      </Stack>
      {badge ? (
        <Badge size="xs" variant="light" color="blue">
          {badge}
        </Badge>
      ) : null}
    </Group>
  );
}
