import { Badge, Box, Button, CloseButton, Divider, Group, Paper, ScrollArea, Stack, Text } from "@mantine/core";
import { ChevronLeft, ChevronRight, Gauge, Maximize2, Minimize2, Train, TrainFront, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

import type { TrainScheduleDetail } from "@/types/trainScheduling";

type Wagon = NonNullable<TrainScheduleDetail["trainSet"]>["wagons"][number];

interface Train3DVisualizationProps {
  schedule: TrainScheduleDetail;
  onClose: () => void;
}

// shipping-line style colors — no purple
const CONTAINER_PALETTE = [0xb63a2b, 0x1f6fb2, 0x1e8449, 0xd9a213, 0xd35f1e, 0x11707f, 0x7a3b1e, 0x35506e];

// wagon body color keyed by wagon type — same type = same color across the train
const WAGON_TYPE_PALETTE = [0x2e6f8e, 0xb0722d, 0x5c7f3b, 0x8e3b3b, 0x3b6e63, 0x8a6d2f, 0x505a7d, 0x6d5540];
const wagonTypeColor = (w: Wagon, fallback: number) =>
  w.wagonType?.code ? WAGON_TYPE_PALETTE[Math.abs(hash32(w.wagonType.code)) % WAGON_TYPE_PALETTE.length] : fallback;

const hash32 = (key: string) => {
  let h = 0;
  for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) | 0;
  return h;
};

const hashColor = (key: string) => CONTAINER_PALETTE[Math.abs(hash32(key)) % CONTAINER_PALETTE.length];

type WagonKind = "container" | "bulk" | "tank" | "flat";

const wagonKind = (w: Wagon): WagonKind => {
  const code = `${w.wagonType?.code ?? ""} ${w.wagonType?.name ?? ""}`.toUpperCase();
  if (/TANK|LIQUID|FUEL/.test(code)) return "tank";
  if (/HOPPER|BULK|OPEN|GONDOLA/.test(code) || w.allocations.some((a) => a.bulkLoad)) return "bulk";
  if (/FLAT|CONT/.test(code) || w.allocations.some((a) => a.containerItems?.length)) return "container";
  return "flat";
};

const WAGON_BODY_COLOR: Record<WagonKind, number> = {
  container: 0x5f7d9c,
  bulk: 0xa8683f,
  tank: 0xb8bec8,
  flat: 0x9aa4ae,
};

const M = 1; // 1 unit = 1 meter
const GAUGE = 1.435 * M;
const WHEEL_R = 0.46 * M;
const DECK_H = 1.2 * M;

// deterministic pseudo-random so scenery doesn't jump between rebuilds
const makeRng = (seed: number) => () => {
  seed = (seed * 1664525 + 1013904223) % 4294967296;
  return seed / 4294967296;
};

function makeLabelSprite(text: string, bg: string): THREE.Sprite {
  const canvas = document.createElement("canvas");
  canvas.width = 512;
  canvas.height = 128;
  const ctx = canvas.getContext("2d")!;
  ctx.fillStyle = bg;
  ctx.beginPath();
  ctx.roundRect(6, 6, 500, 116, 28);
  ctx.fill();
  ctx.strokeStyle = "rgba(255,255,255,0.9)";
  ctx.lineWidth = 6;
  ctx.stroke();
  ctx.fillStyle = "#ffffff";
  ctx.font = "bold 58px system-ui, sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText(text, 256, 68);
  const texture = new THREE.CanvasTexture(canvas);
  const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, depthTest: false }));
  sprite.scale.set(7, 1.75, 1);
  return sprite;
}

function buildClouds(scene: THREE.Scene, rng: () => number) {
  const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 1, transparent: true, opacity: 0.92 });
  for (let i = 0; i < 8; i++) {
    const cloud = new THREE.Group();
    const puffs = 3 + Math.floor(rng() * 4);
    for (let p = 0; p < puffs; p++) {
      const puff = new THREE.Mesh(new THREE.SphereGeometry(8 + rng() * 10, 12, 10), mat);
      puff.position.set((p - puffs / 2) * 11, rng() * 4, (rng() - 0.5) * 10);
      puff.scale.y = 0.55;
      cloud.add(puff);
    }
    // high and pushed to the sides so they never sit between camera and train
    cloud.position.set((rng() - 0.5) * 1800, 220 + rng() * 80, (rng() > 0.5 ? 1 : -1) * (350 + rng() * 400));
    scene.add(cloud);
  }
}

function buildAcacia(rng: () => number): THREE.Group {
  // flat-topped acacia
  const tree = new THREE.Group();
  const trunkH = 2.6 + rng() * 2;
  const trunkMat = new THREE.MeshStandardMaterial({ color: 0x7a5230, roughness: 1 });
  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.35, trunkH, 7), trunkMat);
  trunk.position.y = trunkH / 2;
  trunk.rotation.z = (rng() - 0.5) * 0.25;
  tree.add(trunk);
  for (const tilt of [-0.6, 0.6]) {
    const branch = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.14, trunkH * 0.6, 6), trunkMat);
    branch.position.set(tilt * 0.8, trunkH * 0.85, (rng() - 0.5) * 0.6);
    branch.rotation.z = tilt;
    tree.add(branch);
  }
  const canopyMat = new THREE.MeshStandardMaterial({
    color: rng() > 0.5 ? 0x2f7d32 : 0x3c8d40,
    roughness: 1,
  });
  const canopy = new THREE.Mesh(new THREE.SphereGeometry(2.6 + rng() * 1.6, 10, 8), canopyMat);
  canopy.position.y = trunkH + 0.9;
  canopy.scale.set(1.2, 0.32, 1.2); // flat umbrella top
  tree.add(canopy);
  return tree;
}

function buildShrub(rng: () => number): THREE.Mesh {
  const dry = [0x3c7d33, 0x4f9e3f, 0x2e6b28, 0x5da84a];
  const shrub = new THREE.Mesh(
    new THREE.SphereGeometry(0.5 + rng() * 0.8, 7, 6),
    new THREE.MeshStandardMaterial({ color: dry[Math.floor(rng() * dry.length)], roughness: 1 }),
  );
  shrub.scale.y = 0.55;
  shrub.position.y = 0.25;
  return shrub;
}

function buildRock(rng: () => number): THREE.Mesh {
  const rock = new THREE.Mesh(
    new THREE.DodecahedronGeometry(0.5 + rng() * 1.6, 0),
    new THREE.MeshStandardMaterial({ color: rng() > 0.5 ? 0xa08d76 : 0x8d7f6d, roughness: 1 }),
  );
  rock.scale.set(1 + rng(), 0.55 + rng() * 0.4, 1 + rng());
  rock.position.y = 0.3;
  rock.rotation.y = rng() * Math.PI;
  return rock;
}

function buildCamel(rng: () => number): THREE.Group {
  const g = new THREE.Group();
  const mat = new THREE.MeshStandardMaterial({ color: rng() > 0.5 ? 0xc49a5f : 0xb28850, roughness: 1 });
  const body = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.95, 0.85), mat);
  body.position.y = 1.55;
  g.add(body);
  const hump = new THREE.Mesh(new THREE.SphereGeometry(0.55, 10, 8), mat);
  hump.position.set(-0.15, 2.25, 0);
  hump.scale.set(1.15, 0.9, 0.85);
  g.add(hump);
  const neck = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.3, 0.3), mat);
  neck.position.set(1.05, 2.35, 0);
  neck.rotation.z = -0.25;
  g.add(neck);
  const head = new THREE.Mesh(new THREE.BoxGeometry(0.65, 0.32, 0.3), mat);
  head.position.set(1.45, 2.95, 0);
  g.add(head);
  const legGeo = new THREE.BoxGeometry(0.17, 1.15, 0.17);
  for (const [x, z] of [[-0.8, -0.28], [-0.8, 0.28], [0.8, -0.28], [0.8, 0.28]]) {
    const leg = new THREE.Mesh(legGeo, mat);
    leg.position.set(x, 0.58, z);
    g.add(leg);
  }
  g.rotation.y = rng() * Math.PI * 2;
  return g;
}

function buildGoat(rng: () => number): THREE.Group {
  const g = new THREE.Group();
  const colors = [0xe8e4da, 0x4a3b2d, 0x9c8a72];
  const mat = new THREE.MeshStandardMaterial({ color: colors[Math.floor(rng() * colors.length)], roughness: 1 });
  const body = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.5, 0.4), mat);
  body.position.y = 0.62;
  g.add(body);
  const head = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.3, 0.26), mat);
  head.position.set(0.55, 0.85, 0);
  g.add(head);
  const legGeo = new THREE.BoxGeometry(0.09, 0.4, 0.09);
  for (const [x, z] of [[-0.32, -0.13], [-0.32, 0.13], [0.32, -0.13], [0.32, 0.13]]) {
    const leg = new THREE.Mesh(legGeo, mat);
    leg.position.set(x, 0.2, z);
    g.add(leg);
  }
  g.rotation.y = rng() * Math.PI * 2;
  return g;
}

function buildScenery(scene: THREE.Scene, trackLen: number) {
  const rng = makeRng(1234567);
  buildClouds(scene, rng);
  const spread = Math.max(trackLen, 600);
  // grass variation: darker meadow patches
  const patchMat = new THREE.MeshStandardMaterial({ color: 0x4f8f3a, roughness: 1 });
  const patchGeo = new THREE.CircleGeometry(4, 8);
  const patches = new THREE.InstancedMesh(patchGeo, patchMat, 160);
  const m4 = new THREE.Matrix4();
  const q = new THREE.Quaternion().setFromEuler(new THREE.Euler(-Math.PI / 2, 0, 0));
  const v = new THREE.Vector3();
  const s = new THREE.Vector3();
  for (let i = 0; i < 160; i++) {
    const z = (rng() > 0.5 ? 1 : -1) * (6 + rng() * 220);
    v.set((rng() - 0.5) * spread * 1.4, 0.02, z);
    s.setScalar(0.6 + rng() * 2.4);
    m4.compose(v, q, s);
    patches.setMatrixAt(i, m4);
  }
  scene.add(patches);
  // low green hills on the horizon sides
  const duneMat = new THREE.MeshStandardMaterial({ color: 0x4a8a38, roughness: 1 });
  for (let i = 0; i < 12; i++) {
    const dune = new THREE.Mesh(new THREE.SphereGeometry(30 + rng() * 50, 12, 8), duneMat);
    dune.scale.set(1.6 + rng(), 0.12 + rng() * 0.08, 1 + rng());
    dune.position.set((rng() - 0.5) * spread * 1.6, 0, (rng() > 0.5 ? 1 : -1) * (180 + rng() * 200));
    scene.add(dune);
  }
  // sparse acacias, dry shrubs, rocks
  for (let i = 0; i < 34; i++) {
    const tree = buildAcacia(rng);
    tree.position.set((rng() - 0.5) * spread * 1.2, 0, (rng() > 0.5 ? 1 : -1) * (18 + rng() * 170));
    tree.scale.setScalar(0.9 + rng() * 1.1);
    scene.add(tree);
  }
  for (let i = 0; i < 120; i++) {
    const shrub = buildShrub(rng);
    shrub.position.set((rng() - 0.5) * spread * 1.3, 0.15, (rng() > 0.5 ? 1 : -1) * (8 + rng() * 200));
    scene.add(shrub);
  }
  for (let i = 0; i < 40; i++) {
    const rock = buildRock(rng);
    rock.position.x = (rng() - 0.5) * spread * 1.3;
    rock.position.z = (rng() > 0.5 ? 1 : -1) * (10 + rng() * 190);
    scene.add(rock);
  }
  // camel caravans + goat herds
  for (let h = 0; h < 5; h++) {
    const cx = (rng() - 0.5) * spread;
    const cz = (rng() > 0.5 ? 1 : -1) * (30 + rng() * 130);
    const count = 2 + Math.floor(rng() * 3);
    for (let a = 0; a < count; a++) {
      const camel = buildCamel(rng);
      camel.position.set(cx + a * 4 + (rng() - 0.5) * 2, 0, cz + (rng() - 0.5) * 6);
      scene.add(camel);
    }
  }
  for (let h = 0; h < 4; h++) {
    const cx = (rng() - 0.5) * spread;
    const cz = (rng() > 0.5 ? 1 : -1) * (20 + rng() * 120);
    const count = 3 + Math.floor(rng() * 4);
    for (let a = 0; a < count; a++) {
      const goat = buildGoat(rng);
      goat.position.set(cx + (rng() - 0.5) * 10, 0, cz + (rng() - 0.5) * 8);
      scene.add(goat);
    }
  }
}

function buildTrack(scene: THREE.Scene, length: number) {
  const railMat = new THREE.MeshStandardMaterial({ color: 0x9aa0a8, metalness: 0.9, roughness: 0.35 });
  const railGeo = new THREE.BoxGeometry(length, 0.18 * M, 0.08 * M);
  for (const z of [-GAUGE / 2, GAUGE / 2]) {
    const rail = new THREE.Mesh(railGeo, railMat);
    rail.position.set(0, 0.28 * M, z);
    scene.add(rail);
  }
  const sleeperGeo = new THREE.BoxGeometry(0.24 * M, 0.14 * M, 2.4 * M);
  const sleeperMat = new THREE.MeshStandardMaterial({ color: 0x3b2f26, roughness: 1 });
  const count = Math.floor(length / 0.65);
  const sleepers = new THREE.InstancedMesh(sleeperGeo, sleeperMat, count);
  const m4 = new THREE.Matrix4();
  for (let i = 0; i < count; i++) {
    m4.setPosition(-length / 2 + i * 0.65, 0.12 * M, 0);
    sleepers.setMatrixAt(i, m4);
  }
  scene.add(sleepers);
  // ballast bed
  const ballast = new THREE.Mesh(
    new THREE.BoxGeometry(length, 0.1 * M, 4.2 * M),
    new THREE.MeshStandardMaterial({ color: 0x565a5f, roughness: 1 }),
  );
  ballast.position.y = 0.03;
  scene.add(ballast);
  // ground
  const ground = new THREE.Mesh(
    new THREE.PlaneGeometry(4000, 1600),
    new THREE.MeshStandardMaterial({ color: 0x5da043, roughness: 1 }),
  );
  ground.rotation.x = -Math.PI / 2;
  ground.position.y = -0.02;
  scene.add(ground);
}

function addWheels(group: THREE.Group, length: number, wheels: THREE.Mesh[]) {
  const geo = new THREE.CylinderGeometry(WHEEL_R, WHEEL_R, 0.25, 20);
  const mat = new THREE.MeshStandardMaterial({ color: 0x1c1f24, metalness: 0.7, roughness: 0.4 });
  const bogieOffsets = [-length / 2 + 1.8, length / 2 - 1.8];
  for (const x of bogieOffsets) {
    for (const dx of [-0.9, 0.9]) {
      for (const z of [-GAUGE / 2, GAUGE / 2]) {
        const wheel = new THREE.Mesh(geo, mat);
        wheel.rotation.x = Math.PI / 2;
        wheel.position.set(x + dx, WHEEL_R + 0.05, z);
        group.add(wheel);
        wheels.push(wheel);
      }
    }
    const bogie = new THREE.Mesh(
      new THREE.BoxGeometry(2.6, 0.4, GAUGE + 0.4),
      new THREE.MeshStandardMaterial({ color: 0x22262c, roughness: 0.8 }),
    );
    bogie.position.set(x, WHEEL_R + 0.35, 0);
    group.add(bogie);
  }
}

function buildLocomotive(
  loco: { id?: string; code: string } | undefined,
  wheels: THREE.Mesh[],
  pickables: THREE.Object3D[],
): THREE.Group {
  const g = new THREE.Group();
  const len = 20 * M;
  const bodyMat = new THREE.MeshStandardMaterial({ color: 0x1e6f42, metalness: 0.4, roughness: 0.5 });
  const body = new THREE.Mesh(new THREE.BoxGeometry(len, 3.2, 3), bodyMat);
  body.position.y = DECK_H + 1.6;
  g.add(body);
  const cab = new THREE.Mesh(new THREE.BoxGeometry(4.4, 1.1, 3.05), new THREE.MeshStandardMaterial({ color: 0xf2a516, metalness: 0.3, roughness: 0.5 }));
  cab.position.set(len / 2 - 2.6, DECK_H + 3.75, 0);
  g.add(cab);
  const nose = new THREE.Mesh(new THREE.BoxGeometry(1.6, 2.2, 2.6), bodyMat);
  nose.position.set(len / 2 + 0.6, DECK_H + 1.1, 0);
  g.add(nose);
  // headlight
  const light = new THREE.Mesh(
    new THREE.SphereGeometry(0.22, 12, 12),
    new THREE.MeshStandardMaterial({ color: 0xfff6c9, emissive: 0xfff2a8, emissiveIntensity: 2 }),
  );
  light.position.set(len / 2 + 1.4, DECK_H + 1.6, 0);
  g.add(light);
  const spot = new THREE.SpotLight(0xfff2c0, 60, 90, 0.4, 0.6);
  spot.position.copy(light.position);
  spot.target.position.set(len / 2 + 40, 0.5, 0);
  g.add(spot, spot.target);
  addWheels(g, len, wheels);
  g.userData.length = len;
  const locoId = loco?.id ?? loco?.code ?? "loco";
  g.userData.locoId = locoId;
  g.traverse((o) => {
    o.userData.locoId = locoId;
  });
  pickables.push(g);
  return g;
}

function buildWagon(wagon: Wagon, wheels: THREE.Mesh[], pickables: THREE.Object3D[]): THREE.Group {
  const g = new THREE.Group();
  const len = Math.max(8, wagon.lengthMeters || 14) * M;
  const kind = wagonKind(wagon);
  const bodyMat = new THREE.MeshStandardMaterial({
    color: wagonTypeColor(wagon, WAGON_BODY_COLOR[kind]),
    metalness: 0.35,
    roughness: 0.6,
  });

  // frame/deck common to all
  const deck = new THREE.Mesh(new THREE.BoxGeometry(len, 0.35, 3), bodyMat);
  deck.position.y = DECK_H;
  g.add(deck);

  if (kind === "container") {
    const items = wagon.allocations.flatMap((a) =>
      (a.containerItems ?? []).map((c) => ({ ...c, bookingRef: a.bookingReference ?? a.bookingId })),
    );
    const n = Math.max(items.length, 0);
    const slotLen = n > 1 ? len / n - 0.3 : Math.min(12.2, len - 1.5);
    items.forEach((c, i) => {
      const cx = n > 1 ? -len / 2 + (i + 0.5) * (len / n) : 0;
      const box = new THREE.Mesh(
        new THREE.BoxGeometry(slotLen, 2.6, 2.44),
        new THREE.MeshStandardMaterial({ color: hashColor(c.bookingRef), metalness: 0.2, roughness: 0.55 }),
      );
      box.position.set(cx, DECK_H + 0.35 / 2 + 1.3, 0);
      // corrugation hint: thin ribs
      const rib = new THREE.Mesh(
        new THREE.BoxGeometry(slotLen * 0.98, 2.4, 2.5),
        new THREE.MeshStandardMaterial({ color: 0x000000, transparent: true, opacity: 0.12 }),
      );
      rib.position.copy(box.position);
      g.add(box, rib);
    });
  } else if (kind === "bulk") {
    // open hopper walls
    const wallMat = new THREE.MeshStandardMaterial({
      color: wagonTypeColor(wagon, 0x8a5a34),
      metalness: 0.25,
      roughness: 0.7,
    });
    const wallH = 2.2;
    const side = new THREE.BoxGeometry(len, wallH, 0.12);
    for (const z of [-1.45, 1.45]) {
      const wall = new THREE.Mesh(side, wallMat);
      wall.position.set(0, DECK_H + wallH / 2 + 0.17, z);
      g.add(wall);
    }
    const end = new THREE.BoxGeometry(0.12, wallH, 3);
    for (const x of [-len / 2 + 0.06, len / 2 - 0.06]) {
      const wall = new THREE.Mesh(end, wallMat);
      wall.position.set(x, DECK_H + wallH / 2 + 0.17, 0);
      g.add(wall);
    }
    const loaded = wagon.allocations.some((a) => a.bulkLoad) || wagon.assignedWeightTons > 0;
    if (loaded) {
      // cargo mound: squashed bumpy cylinder rows
      const cargoMat = new THREE.MeshStandardMaterial({ color: 0x5c4a33, roughness: 1 });
      const mounds = Math.max(2, Math.floor(len / 4));
      for (let i = 0; i < mounds; i++) {
        const mound = new THREE.Mesh(new THREE.SphereGeometry(1.5, 12, 8), cargoMat);
        mound.scale.set((len / mounds) * 0.42, 0.55, 0.9);
        mound.position.set(-len / 2 + (i + 0.5) * (len / mounds), DECK_H + wallH * 0.85, 0);
        g.add(mound);
      }
    }
  } else if (kind === "tank") {
    const tank = new THREE.Mesh(
      new THREE.CylinderGeometry(1.4, 1.4, len - 1.6, 24),
      new THREE.MeshStandardMaterial({ color: 0xb9bec7, metalness: 0.6, roughness: 0.3 }),
    );
    tank.rotation.z = Math.PI / 2;
    tank.position.y = DECK_H + 1.6;
    g.add(tank);
    const dome = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.45, 0.5, 16), bodyMat);
    dome.position.y = DECK_H + 3.1;
    g.add(dome);
  } else {
    // empty flat: low side rails
    const rail = new THREE.Mesh(new THREE.BoxGeometry(len, 0.35, 0.1), bodyMat);
    for (const z of [-1.45, 1.45]) {
      const r = rail.clone();
      r.position.set(0, DECK_H + 0.35, z);
      g.add(r);
    }
  }

  // floating loaded/empty badge above the wagon
  const loaded = wagon.allocations.length > 0 || wagon.assignedWeightTons > 0;
  const labelText = loaded
    ? `#${wagon.position ?? wagon.sequenceNo} · ${Math.round(wagon.assignedWeightTons)} t`
    : `#${wagon.position ?? wagon.sequenceNo} · EMPTY`;
  const label = makeLabelSprite(labelText, loaded ? "rgba(22, 130, 60, 0.95)" : "rgba(120, 128, 138, 0.9)");
  label.position.set(0, DECK_H + 5.4, 0);
  g.add(label);

  addWheels(g, len, wheels);
  g.userData.length = len;
  g.userData.wagonId = wagon.id;
  g.traverse((o) => {
    o.userData.wagonId = wagon.id;
  });
  pickables.push(g);
  return g;
}

function InfoRow({ label, value }: { label: string; value: string | number | null | undefined }) {
  if (value === null || value === undefined || value === "") return null;
  return (
    <Group justify="space-between" gap="xs" wrap="nowrap">
      <Text size="xs" c="gray.5">{label}</Text>
      <Text size="xs" c="gray.1" fw={600} ta="right">{value}</Text>
    </Group>
  );
}

function LocoDetailPanel({
  locoId,
  schedule,
  onClose,
  onDrive,
}: {
  locoId: string;
  schedule: TrainScheduleDetail;
  onClose: () => void;
  onDrive: () => void;
}) {
  const locos = schedule.trainSet?.locomotives?.length
    ? schedule.trainSet.locomotives
    : schedule.trainSet?.locomotive
      ? [schedule.trainSet.locomotive]
      : [];
  const loco = locos.find((l) => l.id === locoId || l.code === locoId);
  const ts = schedule.trainSet;
  return (
    <Paper
      radius="lg"
      p="md"
      style={{
        position: "absolute",
        top: 60,
        right: 16,
        width: 340,
        maxHeight: "calc(100% - 76px)",
        background: "rgba(13, 17, 23, 0.92)",
        border: "1px solid rgba(255,255,255,0.15)",
        color: "#e6edf3",
        zIndex: 10,
      }}
    >
      <Group justify="space-between" mb="xs">
        <Text fw={700}>Locomotive {loco?.code ?? ""}</Text>
        <CloseButton variant="transparent" c="gray.4" onClick={onClose} />
      </Group>
      <Button fullWidth color="orange" size="xs" mb="sm" leftSection={<Gauge size={14} />} onClick={onDrive}>
        Drive this locomotive
      </Button>
      <ScrollArea.Autosize mah="70vh">
        <Stack gap="xs">
          {loco ? (
            <>
              {loco.name ? <Text size="sm" c="gray.3">{loco.name}</Text> : null}
              <Group gap="xs">
                <Badge color="yellow" variant="light">{loco.status}</Badge>
              </Group>
              <InfoRow label="Max pull weight" value={`${loco.maxPullWeightTons} t`} />
              {loco.maxTrainLengthMeters ? (
                <InfoRow label="Max train length" value={`${loco.maxTrainLengthMeters} m`} />
              ) : null}
            </>
          ) : (
            <Text size="sm" c="gray.5">No locomotive assigned yet.</Text>
          )}
          <Divider color="rgba(255,255,255,0.1)" label="Train" labelPosition="left" />
          <InfoRow label="Voyage / reference" value={schedule.reference} />
          <InfoRow label="Train number" value={schedule.trainNumber} />
          <InfoRow
            label="Train"
            value={schedule.train ? `${schedule.train.code}${schedule.train.trainName ? ` · ${schedule.train.trainName}` : ""}` : null}
          />
          <InfoRow label="Status" value={String(schedule.status)} />
          <InfoRow label="Direction" value={schedule.direction} />
          <InfoRow label="Route" value={schedule.route?.name} />
          <InfoRow
            label="From → To"
            value={
              schedule.originStation || schedule.destinationStation
                ? `${schedule.originStation?.label ?? schedule.originStation?.code ?? "?"} → ${schedule.destinationStation?.label ?? schedule.destinationStation?.code ?? "?"}`
                : null
            }
          />
          <InfoRow
            label="Departure"
            value={schedule.scheduledDepartureDate ? new Date(schedule.scheduledDepartureDate).toLocaleString() : null}
          />
          <InfoRow
            label="Arrival"
            value={schedule.scheduledArrivalDate ? new Date(schedule.scheduledArrivalDate).toLocaleString() : null}
          />
          {ts ? (
            <>
              <Divider color="rgba(255,255,255,0.1)" label="Consist" labelPosition="left" />
              <InfoRow label="Wagons" value={ts.wagonCount} />
              <InfoRow label="Total weight" value={`${ts.totalWeightTons} t`} />
              <InfoRow label="Total length" value={`${ts.totalLengthMeters} m`} />
              {ts.heaviestLeg ? (
                <InfoRow
                  label="Heaviest leg"
                  value={`${ts.heaviestLeg.grossWeightTons} t · ${ts.heaviestLeg.lengthMeters} m`}
                />
              ) : null}
            </>
          ) : null}
        </Stack>
      </ScrollArea.Autosize>
    </Paper>
  );
}

function WagonDetailPanel({ wagon, schedule, onClose }: { wagon: Wagon; schedule: TrainScheduleDetail; onClose: () => void }) {
  const bookings = schedule.bookings ?? [];
  return (
    <Paper
      radius="lg"
      p="md"
      style={{
        position: "absolute",
        top: 60,
        right: 16,
        width: 340,
        maxHeight: "calc(100% - 76px)",
        background: "rgba(13, 17, 23, 0.92)",
        border: "1px solid rgba(255,255,255,0.15)",
        color: "#e6edf3",
        zIndex: 10,
      }}
    >
      <Group justify="space-between" mb="xs">
        <Text fw={700}>
          Wagon {wagon.position ?? wagon.sequenceNo}
          {wagon.physicalWagonNumber ? ` · ${wagon.physicalWagonNumber}` : ""}
        </Text>
        <CloseButton variant="transparent" c="gray.4" onClick={onClose} />
      </Group>
      <ScrollArea.Autosize mah="70vh">
        <Stack gap="xs">
          <Group gap="xs">
            <Badge color="yellow" variant="light">{wagon.wagonType?.code ?? "UNKNOWN"}</Badge>
            <Text size="xs" c="gray.4">{wagon.wagonType?.name}</Text>
          </Group>
          <Text size="xs" c="gray.4">
            Capacity {wagon.capacityTons} t · Loaded {wagon.assignedWeightTons} t
            {wagon.tareWeightTons ? ` · Tare ${wagon.tareWeightTons} t` : ""} · {wagon.lengthMeters} m
          </Text>
          {wagon.allocations.length === 0 ? (
            <Text size="sm" c="gray.5">Empty wagon — no allocations.</Text>
          ) : (
            wagon.allocations.map((alloc) => {
              const booking = bookings.find((b) => b.id === alloc.bookingId);
              return (
                <Paper key={alloc.id} p="sm" radius="md" style={{ background: "rgba(255,255,255,0.06)" }}>
                  <Stack gap={6}>
                    <Group justify="space-between">
                      <Text size="sm" fw={600} ff="monospace">{alloc.bookingReference ?? "—"}</Text>
                      <Badge size="xs" variant="light" color="teal">{alloc.allocatedWeightTons} t</Badge>
                    </Group>
                    {booking ? (
                      <>
                        <Text size="xs" c="gray.3">Customer: {booking.customer ?? "—"}</Text>
                        <Text size="xs" c="gray.4">
                          {booking.origin ?? "?"} → {booking.destination ?? "?"} · {booking.status ?? "—"}
                          {booking.loadingStatus ? ` · ${booking.loadingStatus}` : ""}
                        </Text>
                        {booking.contractReference ? (
                          <Text size="xs" c="gray.5" ff="monospace">Contract {booking.contractReference}</Text>
                        ) : null}
                      </>
                    ) : null}
                    {(alloc.containerItems ?? []).length > 0 ? (
                      <>
                        <Divider color="rgba(255,255,255,0.1)" label="Containers" labelPosition="left" />
                        {(alloc.containerItems ?? []).map((c) => (
                          <Group key={c.id} justify="space-between">
                            <Text size="xs" ff="monospace" c="gray.2">{c.containerNumber ?? "—"}</Text>
                            <Text size="xs" c="gray.5">{c.grossWeightTons ?? "?"} t</Text>
                          </Group>
                        ))}
                      </>
                    ) : null}
                    {alloc.bulkLoad ? (
                      <Text size="xs" c="gray.3">
                        Bulk: {alloc.bulkLoad.cargoDescription ?? "cargo"} · {alloc.bulkLoad.weightTons} t
                      </Text>
                    ) : null}
                  </Stack>
                </Paper>
              );
            })
          )}
        </Stack>
      </ScrollArea.Autosize>
    </Paper>
  );
}

export function Train3DVisualization({ schedule, onClose }: Train3DVisualizationProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const canvasHostRef = useRef<HTMLDivElement>(null);
  const [selectedWagonId, setSelectedWagonId] = useState<string | null>(null);
  const [selectedLocoId, setSelectedLocoId] = useState<string | null>(null);
  const [driveMode, setDriveMode] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const cameraApiRef = useRef<{
    overview: () => void;
    front: () => void;
    rear: () => void;
    focusWagon: (wagonId: string) => void;
  } | null>(null);

  const wagons = schedule.trainSet?.wagons ?? [];
  const dispatched = String(schedule.status).toUpperCase() === "DISPATCHED";
  // drive mode always simulates motion, even before dispatch
  const moving = dispatched || driveMode;
  const selectedWagon = wagons.find((w) => w.id === selectedWagonId) ?? null;
  const orderedWagons = [...wagons].sort(
    (a, b) => (a.position ?? a.sequenceNo) - (b.position ?? b.sequenceNo),
  );
  const selectedIndex = orderedWagons.findIndex((w) => w.id === selectedWagonId);

  const stepWagon = (dir: 1 | -1) => {
    if (orderedWagons.length === 0) return;
    const next =
      selectedIndex < 0
        ? dir === 1
          ? 0
          : orderedWagons.length - 1
        : (selectedIndex + dir + orderedWagons.length) % orderedWagons.length;
    const wagon = orderedWagons[next];
    setSelectedLocoId(null);
    setSelectedWagonId(wagon.id);
    cameraApiRef.current?.focusWagon(wagon.id);
  };

  const firstLocoId =
    schedule.trainSet?.locomotives?.[0]?.id ??
    schedule.trainSet?.locomotive?.id ??
    "LOCO";

  useEffect(() => {
    const host = canvasHostRef.current;
    if (!host) return;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xa7d8f0);
    scene.fog = new THREE.Fog(0xd9e8f0, 900, 3200); // far desert haze — never clouds the train

    const camera = new THREE.PerspectiveCamera(55, host.clientWidth / host.clientHeight, 0.1, 5000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.15;
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(host.clientWidth, host.clientHeight);
    host.appendChild(renderer.domElement);

    // lights: bright sunny day
    scene.add(new THREE.HemisphereLight(0xcfe8ff, 0x6f9c4e, 1.0));
    const sun = new THREE.DirectionalLight(0xfff4d6, 2.4);
    sun.position.set(-120, 180, 90);
    scene.add(sun);
    const fill = new THREE.DirectionalLight(0xdbeaff, 0.6);
    fill.position.set(100, 60, -80);
    scene.add(fill);
    // visible sun disc
    const sunDisc = new THREE.Mesh(
      new THREE.SphereGeometry(18, 16, 16),
      new THREE.MeshBasicMaterial({ color: 0xfff2b0 }),
    );
    sunDisc.position.set(-500, 380, 300);
    scene.add(sunDisc);

    // train assembly
    const wheels: THREE.Mesh[] = [];
    const pickables: THREE.Object3D[] = [];
    const train = new THREE.Group();
    const gap = 1.0 * M;
    let cursor = 0;
    const locos = schedule.trainSet?.locomotives?.length
      ? schedule.trainSet.locomotives
      : schedule.trainSet?.locomotive
        ? [schedule.trainSet.locomotive]
        : [{ code: "LOCO" }];
    const locoGroups: Array<{ id: string; group: THREE.Group }> = [];
    for (const loco of locos) {
      const lg = buildLocomotive(loco, wheels, pickables);
      lg.position.x = cursor - lg.userData.length / 2;
      cursor -= lg.userData.length + gap;
      train.add(lg);
      locoGroups.push({ id: lg.userData.locoId as string, group: lg });
    }
    const ordered = [...wagons].sort((a, b) => (a.position ?? a.sequenceNo) - (b.position ?? b.sequenceNo));
    const wagonGroups: Array<{ id: string; group: THREE.Group }> = [];
    for (const wagon of ordered) {
      const wg = buildWagon(wagon, wheels, pickables);
      wg.position.x = cursor - wg.userData.length / 2;
      cursor -= wg.userData.length + gap;
      train.add(wg);
      wagonGroups.push({ id: wagon.id, group: wg });
    }
    const trainLen = -cursor;
    train.position.x = trainLen / 2; // center train on origin
    scene.add(train);

    const trackLen = Math.max(trainLen * 2.5, 400);
    const finalTrackLen = moving ? Math.max(trackLen, 1600) : trackLen;
    buildTrack(scene, finalTrackLen);
    buildScenery(scene, finalTrackLen);

    camera.position.set(trainLen * 0.12, 14, 42);
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 2, 0);
    controls.maxPolarAngle = Math.PI / 2 - 0.03;
    controls.maxDistance = Math.max(600, trainLen * 1.6);
    controls.enableDamping = true;
    controls.zoomToCursor = true; // wheel zooms toward the point under the pointer
    controls.enabled = !driveMode; // cab ride owns the camera

    // camera fly-to: goal recomputed each frame so it tracks a moving train
    let cameraGoal: (() => { pos: THREE.Vector3; target: THREE.Vector3 }) | null = null;
    const worldPos = new THREE.Vector3();
    const flyToObject = (obj: THREE.Object3D, dist: number, height: number) => {
      cameraGoal = () => {
        obj.getWorldPosition(worldPos);
        return {
          pos: new THREE.Vector3(worldPos.x + dist * 0.35, height, worldPos.z + dist),
          target: new THREE.Vector3(worldPos.x, 2.2, worldPos.z),
        };
      };
    };
    const frontGroup = train.children[0];
    const rearGroup = wagonGroups[wagonGroups.length - 1]?.group ?? frontGroup;
    cameraApiRef.current = {
      overview: () => {
        // side view fitted with the HORIZONTAL fov — much closer than the naive
        // vertical-fov fit, and low to the ground so the train fills the frame
        const vFov = (camera.fov * Math.PI) / 180;
        const hFov = 2 * Math.atan(Math.tan(vFov / 2) * camera.aspect);
        const dist = Math.max(40, (trainLen / 2 / Math.tan(hFov / 2)) * 1.08);
        cameraGoal = () => {
          train.getWorldPosition(worldPos);
          const cx = worldPos.x - trainLen / 2; // train extends in -x from its origin
          return {
            pos: new THREE.Vector3(cx, Math.max(10, dist * 0.1), dist),
            target: new THREE.Vector3(cx, 2.5, 0),
          };
        };
      },
      front: () => flyToObject(frontGroup, 26, 8),
      rear: () => flyToObject(rearGroup, 20, 7),
      focusWagon: (wagonId: string) => {
        const entry = wagonGroups.find((w) => w.id === wagonId);
        if (entry) flyToObject(entry.group, 16, 6);
      },
    };
    // user grabs the mouse → stop auto-flying, hand control back
    controls.addEventListener("start", () => {
      cameraGoal = null;
    });
    if (driveMode) {
      // cab ride: camera on the driver's seat of the lead loco, looking down the line
      const locoLen = frontGroup.userData.length as number;
      cameraGoal = () => {
        frontGroup.getWorldPosition(worldPos);
        return {
          pos: new THREE.Vector3(worldPos.x + locoLen / 2 - 3.2, DECK_H + 3.9, 1.05),
          target: new THREE.Vector3(worldPos.x + locoLen / 2 + 140, 2.2, 0),
        };
      };
    } else if (moving) {
      // dispatched → open following the rolling train so motion is obvious
      cameraApiRef.current.overview();
    }

    // picking
    const raycaster = new THREE.Raycaster();
    const pointer = new THREE.Vector2();
    let highlighted: THREE.Object3D | null = null;
    const setEmissive = (root: THREE.Object3D, on: boolean) => {
      root.traverse((o) => {
        const mesh = o as THREE.Mesh;
        const mat = mesh.material as THREE.MeshStandardMaterial | undefined;
        if (mat?.emissive) {
          mat.emissive.setHex(on ? 0x335588 : 0x000000);
          mat.emissiveIntensity = on ? 0.9 : 1;
        }
      });
    };
    const onClick = (e: MouseEvent) => {
      const rect = renderer.domElement.getBoundingClientRect();
      pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(pointer, camera);
      const hits = raycaster.intersectObjects(pickables, true);
      const wagonId = hits[0]?.object.userData.wagonId as string | undefined;
      const locoId = hits[0]?.object.userData.locoId as string | undefined;
      if (highlighted) setEmissive(highlighted, false);
      highlighted = wagonId
        ? (pickables.find((p) => p.userData.wagonId === wagonId) ?? null)
        : locoId
          ? (locoGroups.find((l) => l.id === locoId)?.group ?? null)
          : null;
      if (highlighted) setEmissive(highlighted, true);
      setSelectedWagonId(wagonId ?? null);
      setSelectedLocoId(locoId && !wagonId ? locoId : null);
      if (wagonId) cameraApiRef.current?.focusWagon(wagonId);
      else if (locoId) {
        const lg = locoGroups.find((l) => l.id === locoId);
        if (lg) flyToObject(lg.group, 30, 9);
      }
    };
    renderer.domElement.addEventListener("click", onClick);

    const onResize = () => {
      camera.aspect = host.clientWidth / host.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(host.clientWidth, host.clientHeight);
    };
    const resizeObserver = new ResizeObserver(onResize);
    resizeObserver.observe(host);

    let raf = 0;
    let last = performance.now();
    let elapsed = 0;
    const speed = 14; // m/s visual speed when dispatched
    const animate = () => {
      raf = requestAnimationFrame(animate);
      const now = performance.now();
      const dt = Math.min((now - last) / 1000, 0.1);
      last = now;
      if (moving) {
        elapsed += dt;
        train.position.x += speed * dt;
        for (const w of wheels) w.rotation.y += (speed * dt) / WHEEL_R;
        // subtle rail-joint sway so motion reads even up close
        train.position.y = Math.sin(elapsed * 9) * 0.03;
        train.rotation.z = Math.sin(elapsed * 4.5) * 0.0025;
        // loop over the long track stretch; jump happens far off-screen
        const half = finalTrackLen / 2 - trainLen - 40;
        if (train.position.x > half + trainLen) train.position.x = -half;
      }
      if (cameraGoal) {
        const goal = cameraGoal();
        const k = Math.min(1, dt * (driveMode ? 7 : 3.2));
        camera.position.lerp(goal.pos, k);
        controls.target.lerp(goal.target, k);
        // static scene: release goal once settled so orbiting feels free again
        if (!moving && camera.position.distanceTo(goal.pos) < 0.15) cameraGoal = null;
      }
      controls.update();
      renderer.render(scene, camera);
    };
    animate();

    return () => {
      cameraApiRef.current = null;
      cancelAnimationFrame(raf);
      resizeObserver.disconnect();
      renderer.domElement.removeEventListener("click", onClick);
      controls.dispose();
      renderer.dispose();
      host.removeChild(renderer.domElement);
      scene.traverse((o) => {
        const mesh = o as THREE.Mesh;
        mesh.geometry?.dispose?.();
        const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
        mats.forEach((m) => m?.dispose?.());
      });
    };
    // rebuild scene only when schedule identity/status changes
  }, [schedule.id, schedule.status, moving, driveMode, wagons.length]); // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    const onFsChange = () => setIsFullscreen(Boolean(document.fullscreenElement));
    const onKey = (e: KeyboardEvent) => {
      if (e.key !== "Escape" || document.fullscreenElement) return;
      if (driveMode) setDriveMode(false);
      else onClose();
    };
    document.addEventListener("fullscreenchange", onFsChange);
    window.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("fullscreenchange", onFsChange);
      window.removeEventListener("keydown", onKey);
    };
  }, [onClose, driveMode]);

  const toggleFullscreen = () => {
    if (document.fullscreenElement) void document.exitFullscreen();
    else void containerRef.current?.requestFullscreen();
  };

  return (
    <Box
      ref={containerRef}
      style={{
        position: "fixed",
        inset: 0,
        zIndex: 400,
        background: "#02040a",
      }}
    >
      <Box ref={canvasHostRef} style={{ position: "absolute", inset: 0 }} />
      <Group
        gap="xs"
        style={{ position: "absolute", top: 16, left: 16, zIndex: 10 }}
      >
        <Badge size="lg" variant="filled" color={driveMode ? "orange" : dispatched ? "green" : "gray"}>
          {driveMode
            ? "DRIVER VIEW — SIMULATION"
            : dispatched
              ? "DISPATCHED — TRAIN IN MOTION"
              : `${schedule.status} — TRAIN STOPPED`}
        </Badge>
        <Badge size="lg" variant="light" color="yellow">
          {schedule.route?.name ?? schedule.reference ?? "Train"} · {wagons.length} wagons
        </Badge>
      </Group>
      <Group
        gap="xs"
        style={{
          position: "absolute",
          bottom: 16,
          left: "50%",
          transform: "translateX(-50%)",
          zIndex: 10,
          background: "rgba(15, 23, 42, 0.75)",
          borderRadius: 12,
          padding: "8px 12px",
        }}
      >
        <Button
          size="compact-sm"
          variant="default"
          leftSection={<Train size={14} />}
          onClick={() => cameraApiRef.current?.overview()}
        >
          Whole train
        </Button>
        <Button
          size="compact-sm"
          variant="default"
          leftSection={<TrainFront size={14} />}
          onClick={() => {
            setSelectedWagonId(null);
            setSelectedLocoId(firstLocoId);
            cameraApiRef.current?.front();
          }}
        >
          Locomotive
        </Button>
        <Button size="compact-sm" variant="default" onClick={() => cameraApiRef.current?.rear()}>
          Last wagon
        </Button>
        <Button
          size="compact-sm"
          variant={driveMode ? "filled" : "default"}
          color={driveMode ? "orange" : undefined}
          leftSection={<Gauge size={14} />}
          onClick={() => setDriveMode((d) => !d)}
        >
          {driveMode ? "Exit drive" : "Drive"}
        </Button>
        <Divider orientation="vertical" color="rgba(255,255,255,0.2)" />
        <Button size="compact-sm" variant="default" onClick={() => stepWagon(-1)} px={8}>
          <ChevronLeft size={16} />
        </Button>
        <Text size="sm" c="white" fw={600} style={{ minWidth: 90, textAlign: "center" }}>
          {selectedIndex >= 0
            ? `Wagon ${orderedWagons[selectedIndex].position ?? orderedWagons[selectedIndex].sequenceNo} / ${orderedWagons.length}`
            : `${orderedWagons.length} wagons`}
        </Text>
        <Button size="compact-sm" variant="default" onClick={() => stepWagon(1)} px={8}>
          <ChevronRight size={16} />
        </Button>
      </Group>
      <Group gap="xs" style={{ position: "absolute", bottom: 16, left: 16, zIndex: 10 }}>
        <Text
          size="xs"
          c="white"
          px="sm"
          py={4}
          style={{ background: "rgba(15, 23, 42, 0.6)", borderRadius: 8 }}
        >
          Drag to orbit · scroll to zoom · click a wagon for details
        </Text>
      </Group>
      <Group gap="xs" style={{ position: "absolute", top: 16, right: 16, zIndex: 11 }}>
        <Button
          size="compact-sm"
          variant="default"
          leftSection={isFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
          onClick={toggleFullscreen}
        >
          {isFullscreen ? "Exit fullscreen" : "Fullscreen"}
        </Button>
        <Button size="compact-sm" color="red" variant="light" leftSection={<X size={14} />} onClick={onClose}>
          Close
        </Button>
      </Group>
      {selectedWagon ? (
        <WagonDetailPanel wagon={selectedWagon} schedule={schedule} onClose={() => setSelectedWagonId(null)} />
      ) : selectedLocoId ? (
        <LocoDetailPanel
          locoId={selectedLocoId}
          schedule={schedule}
          onClose={() => setSelectedLocoId(null)}
          onDrive={() => setDriveMode(true)}
        />
      ) : null}
    </Box>
  );
}
