﻿const { useEffect, useMemo, useRef, useState, useCallback } = React;

const Trash2 = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <polyline points="3 6 5 6 21 6"></polyline>
    <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
    <line x1="10" y1="11" x2="10" y2="17"></line>
    <line x1="14" y1="11" x2="14" y2="17"></line>
  </svg>
);

const Plus = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <line x1="12" y1="5" x2="12" y2="19"></line>
    <line x1="5" y1="12" x2="19" y2="12"></line>
  </svg>
);

const Download = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
    <polyline points="7 10 12 15 17 10"></polyline>
    <line x1="12" y1="15" x2="12" y2="3"></line>
  </svg>
);

const Upload = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <polyline points="16 16 12 12 8 16"></polyline>
    <line x1="12" y1="12" x2="12" y2="21"></line>
    <path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path>
  </svg>
);

const FolderDown = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
    <polyline points="12 11 12 17 9 14"></polyline>
    <polyline points="12 17 15 14"></polyline>
  </svg>
);

const Save = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
    <polyline points="17 21 17 13 7 13 7 21"></polyline>
    <polyline points="7 3 7 8 15 8"></polyline>
  </svg>
);

const FolderOpen = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z"></path>
  </svg>
);

const Info = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <circle cx="12" cy="12" r="10"></circle>
    <line x1="12" y1="16" x2="12" y2="12"></line>
    <line x1="12" y1="8" x2="12.01" y2="8"></line>
  </svg>
);

const Copy = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
    <rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
  </svg>
);

const Edit3 = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
  </svg>
);

const Layers = ({ size = 24, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <polygon points="12 2 20 7 20 17 12 22 4 17 4 7 12 2"></polygon>
    <polyline points="12 22 12 12"></polyline>
    <polyline points="20 7 12 12 4 7"></polyline>
    <polyline points="12 12 20 17"></polyline>
    <polyline points="12 12 4 17"></polyline>
  </svg>
);

function uuid() {
  try { return crypto.randomUUID(); } catch (_) {}
  return Math.random().toString(16).slice(2) + "-" + Date.now().toString(16);
}

function clamp(n, a, b) {
  return Math.max(a, Math.min(b, n));
}

function safeInt(v, fallback = 1) {
  const n = parseInt(String(v ?? "").trim(), 10);
  return Number.isFinite(n) ? n : fallback;
}

function normalizeColor(v, fallback = "#60a5fa") {
  const s = String(v ?? "").trim();
  return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s) ? s : fallback;
}

function slugify(s) {
  return String(s || "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/(^-|-$)/g, "") || "rack";
}

function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

function normalizeSideToken(tok) {
  const t = String(tok || "").trim().toLowerCase();
  if (t === "r" || t === "rear" || t === "back" || t === "b") return "rear";
  return "front";
}

function isInteractive(el) {
  return !!(el && el.closest("button, a, input, label, select, textarea"));
}

class PortGroup {
  constructor(type, count, side = "front") {
    this.type = type;
    this.count = count;
    this.side = side;
  }
}

class Item {
  constructor(name, u, color, notes = "", ports = [], isTemplate = true) {
    this.id = uuid();
    this.name = name;
    this.u = u;
    this.color = color;
    this.notes = notes;
    this.ports = ports;
    this.isTemplate = isTemplate;
  }
}

function parsePorts(input) {
  if (!input) return [];
  const raw = String(input).replace(/\r/g, "");
  return raw
    .split(/;|\n/)
    .map(seg => String(seg).trim())
    .filter(Boolean)
    .map(seg => {
      let side = "front";
      let rest = seg;

      const sideMatch = seg.match(/^(front|rear|f|r)\s*[-\s]\s*(.+)$/i);
      if (sideMatch) {
        side = normalizeSideToken(sideMatch[1]);
        rest = sideMatch[2];
      }

      const parts = String(rest).split(":");
      const rawType = (parts[0] || "Port").trim();
      const rawCount = (parts[1] || "").trim();
      const countRaw = rawCount ? Number(rawCount) : 1;
      const count = Number.isFinite(countRaw) && countRaw > 0 ? Math.floor(countRaw) : 1;

      return new PortGroup(rawType || "Port", count, side);
    });
}

function normalizePortGroups(pg) {
  if (!Array.isArray(pg)) return [];
  return pg
    .filter(Boolean)
    .map(g => ({
      type: String(g.type ?? "Port"),
      count: Math.max(0, safeInt(g.count ?? 0, 0)),
      side: String(g.side ?? "front").toLowerCase().startsWith("r") ? "rear" : "front",
    }))
    .filter(g => g.count > 0);
}

function normalizeItem(x, isTemplate) {
  const ports = Array.isArray(x?.ports) ? normalizePortGroups(x.ports) : parsePorts(x?.Ports ?? x?.ports ?? "");
  return {
    id: x?.id || uuid(),
    name: String(x?.name ?? x?.Name ?? "Device"),
    u: Math.max(1, safeInt(x?.u ?? x?.U ?? 1, 1)),
    color: normalizeColor(x?.color ?? x?.Color ?? "#60a5fa", "#60a5fa"),
    notes: String(x?.notes ?? x?.Notes ?? ""),
    ports,
    isTemplate: !!isTemplate,
  };
}

function normalizeCable(x) {
  return {
    id: x?.id || uuid(),
    fromPortId: String(x?.fromPortId ?? x?.FromPortId ?? "").trim(),
    toPortId: String(x?.toPortId ?? x?.ToPortId ?? "").trim(),
    label: String(x?.label ?? x?.Label ?? ""),
    type: String(x?.type ?? x?.Type ?? "Patch"),
    color: normalizeColor(x?.color ?? x?.Color ?? "#38bdf8", "#38bdf8"),
    notes: String(x?.notes ?? x?.Notes ?? ""),
  };
}

function buildPlacedList(items, placements) {
  return Object.entries(placements || {})
    .map(([itemId, startU]) => {
      const item = (items || []).find(i => i.id === itemId);
      if (!item) return null;
      const endU = startU + item.u - 1;
      return { item, itemId, startU, endU };
    })
    .filter(Boolean)
    .sort((a, b) => b.startU - a.startU);
}

function portTypeKey(type) {
  return slugify(type || "port") || "port";
}

function makePortId(itemId, side, type, index) {
  return `${itemId}::${side === "rear" ? "rear" : "front"}::${portTypeKey(type)}::${safeInt(index, 1)}`;
}

function parsePortId(portId) {
  const [itemId = "", sideRaw = "front", typeKey = "port", indexRaw = "1"] = String(portId || "").split("::");
  return {
    itemId,
    side: sideRaw === "rear" ? "rear" : "front",
    typeKey,
    index: Math.max(1, safeInt(indexRaw, 1)),
  };
}

function buildPortInstances(item) {
  const ports = [];
  const groups = item?.ports || [];
  groups.forEach((group) => {
    const side = group.side === "rear" ? "rear" : "front";
    const type = String(group.type || "Port");
    const count = Math.max(0, safeInt(group.count ?? 0, 0));
    for (let index = 1; index <= count; index += 1) {
      ports.push({
        id: makePortId(item.id, side, type, index),
        itemId: item.id,
        side,
        type,
        index,
        sideLabel: side === "rear" ? "Rear" : "Front",
        sideShort: side === "rear" ? "R" : "F",
        label: `${side === "rear" ? "Rear" : "Front"} ${type} ${String(index).padStart(2, "0")}`,
        shortLabel: `${type} ${String(index).padStart(2, "0")}`,
      });
    }
  });
  return ports;
}

function sanitizeCables(cables, placedItems) {
  const validPorts = new Set((placedItems || []).flatMap(item => buildPortInstances(item).map(port => port.id)));
  const usedPorts = new Set();
  const next = [];

  for (const rawCable of cables || []) {
    const cable = normalizeCable(rawCable);
    if (!cable.fromPortId || !cable.toPortId) continue;
    if (cable.fromPortId === cable.toPortId) continue;
    if (!validPorts.has(cable.fromPortId) || !validPorts.has(cable.toPortId)) continue;
    if (usedPorts.has(cable.fromPortId) || usedPorts.has(cable.toPortId)) continue;
    usedPorts.add(cable.fromPortId);
    usedPorts.add(cable.toPortId);
    next.push(cable);
  }

  return next;
}

function normalizeRack(r) {
  const id = r?.id || uuid();
  const name = String(r?.name ?? "Rack");
  const rackU = clamp(safeInt(r?.rackU ?? 42, 42), 1, 60);

  const placedItemsRaw = Array.isArray(r?.placedItems) ? r.placedItems : [];
  const placedItems = placedItemsRaw.map(x => normalizeItem(x, false));
  const itemById = new Map(placedItems.map(i => [i.id, i]));
  const placementsRaw = r?.placements && typeof r.placements === "object" ? r.placements : {};
  const cleanedPlacements = {};

  for (const [pid, start] of Object.entries(placementsRaw)) {
    const item = itemById.get(pid);
    if (!item) continue;
    const maxStart = Math.max(1, rackU - item.u + 1);
    cleanedPlacements[pid] = clamp(safeInt(start, 1), 1, maxStart);
  }

  const cablesRaw = Array.isArray(r?.cables) ? r.cables : [];
  const cables = sanitizeCables(cablesRaw, placedItems);

  return {
    id,
    name,
    rackU,
    placedItems,
    placements: cleanedPlacements,
    cables,
  };
}

function portsSummary(item) {
  const groups = item?.ports || [];
  if (!groups.length) return "";
  const bySide = { front: [], rear: [] };
  for (const group of groups) {
    bySide[group.side === "rear" ? "rear" : "front"].push(group);
  }
  const fmt = arr => arr.map(g => `${g.type} x ${g.count}`).join(", ");
  const front = bySide.front.length ? `F: ${fmt(bySide.front)}` : "";
  const rear = bySide.rear.length ? `R: ${fmt(bySide.rear)}` : "";
  return [front, rear].filter(Boolean).join(" | ");
}

function isFreeInPlacedList(startU, itemU, placedList, rackU, ignoreId = null) {
  const endU = startU + itemU - 1;
  if (startU < 1 || endU > rackU) return false;

  for (const placed of placedList || []) {
    if (ignoreId && placed.itemId === ignoreId) continue;
    const overlap = !(endU < placed.startU || startU > placed.endU);
    if (overlap) return false;
  }
  return true;
}

function resolveDropPreview(e, rackEl, rackU, itemU, pointerOffsetU, placedSnapshot, ignoreId = null) {
  if (!rackEl) {
    return { hoverU: null, previewStartU: null, valid: false };
  }

  const rect = rackEl.getBoundingClientRect();
  if (!rect.height) {
    return { hoverU: null, previewStartU: null, valid: false };
  }

  const localY = clamp(e.clientY - rect.top, 0, rect.height);
  const hoverU = clamp(Math.ceil((1 - (localY / rect.height)) * rackU), 1, rackU);
  const maxStart = Math.max(1, rackU - itemU + 1);
  let previewStartU = clamp(hoverU - pointerOffsetU, 1, maxStart);

  const hoveredPlaced = (placedSnapshot || []).find(record => record.itemId !== ignoreId && hoverU >= record.startU && hoverU <= record.endU);
  if (hoveredPlaced) {
    const itemTopPx = rect.height - (((hoveredPlaced.startU - 1) + hoveredPlaced.item.u) / rackU) * rect.height;
    const itemBottomPx = rect.height - ((hoveredPlaced.startU - 1) / rackU) * rect.height;
    const midPx = (itemTopPx + itemBottomPx) / 2;
    const preferred = localY < midPx
      ? hoveredPlaced.endU + 1
      : hoveredPlaced.startU - itemU;
    previewStartU = clamp(preferred, 1, maxStart);
  }

  const valid = isFreeInPlacedList(previewStartU, itemU, placedSnapshot, rackU, ignoreId);
  return { hoverU, previewStartU, valid };
}

function getPortAnchorPoint(port, placedRecord, rackU) {
  const sidePorts = buildPortInstances(placedRecord.item).filter(p => p.side === port.side);
  const sideCount = Math.max(1, sidePorts.length);
  const slotIndex = Math.max(0, sidePorts.findIndex(p => p.id === port.id));
  const height = (placedRecord.item.u / rackU) * 100;
  const top = 100 - (((placedRecord.startU - 1) + placedRecord.item.u) / rackU) * 100;
  const y = top + ((slotIndex + 0.5) / sideCount) * height;
  const x = port.side === "rear" ? 92 : 8;
  return { x, y };
}

function buildVisibleSideMarkers(sidePorts, maxVisible) {
  if (!Array.isArray(sidePorts) || !sidePorts.length || maxVisible <= 0) return [];

  const visibleCount = Math.min(sidePorts.length, maxVisible);
  if (visibleCount === 1) {
    const centerPort = sidePorts[Math.floor((sidePorts.length - 1) / 2)];
    return [{
      ...centerPort,
      displayIndex: 0,
      displayCount: 1,
    }];
  }

  const step = (sidePorts.length - 1) / (visibleCount - 1);
  return Array.from({ length: visibleCount }, (_, displayIndex) => {
    const sourceIndex = Math.round(displayIndex * step);
    return {
      ...sidePorts[sourceIndex],
      displayIndex,
      displayCount: visibleCount,
    };
  });
}

function createWorksheet(headers, rows) {
  const table = [headers, ...(rows || []).map(row => headers.map(header => row[header] ?? ""))];
  return XLSX.utils.aoa_to_sheet(table);
}

function App() {
  return <RackPlanner />;
}

function RackPlanner() {
  const rackRef = useRef(null);
  const toastTimer = useRef(null);
  const pointerCaptureRef = useRef(null);
  const dragListenersRef = useRef({ move: null, up: null, cancel: null });

  const [invFilter, setInvFilter] = useState("");
  const [newName, setNewName] = useState("");
  const [newU, setNewU] = useState(1);
  const [newColor, setNewColor] = useState("#60a5fa");
  const [newPorts, setNewPorts] = useState("");
  const [newNotes, setNewNotes] = useState("");
  const [importing, setImporting] = useState(false);
  const [toast, setToast] = useState(null);

  const initialRackId = useMemo(() => uuid(), []);
  const [templates, setTemplates] = useState(() => ([
    new Item("1U Server", 1, "#60a5fa", "General compute server", parsePorts("Front-RJ45:2;Rear-AC:2"), true),
    new Item("2U UPS", 2, "#10b981", "Battery backup", parsePorts("Rear-AC:2"), true),
    new Item("Switch 24p", 1, "#f59e0b", "24 port access switch", parsePorts("Front-RJ45:24;Front-SFP:4;Rear-AC:1"), true),
  ]));

  const [racks, setRacks] = useState(() => ([
    { id: initialRackId, name: "Rack 1", rackU: 42, placedItems: [], placements: {}, cables: [] }
  ]));
  const [activeRackId, setActiveRackId] = useState(initialRackId);

  const [selectedItemId, setSelectedItemId] = useState(null);
  const [portDraft, setPortDraft] = useState(null);
  const [portTargetId, setPortTargetId] = useState("");
  const [cableLabel, setCableLabel] = useState("");
  const [cableType, setCableType] = useState("Patch");
  const [cableColor, setCableColor] = useState("#38bdf8");
  const [cableNotes, setCableNotes] = useState("");

  const [dragSession, setDragSession] = useState(null);

  const showToast = useCallback((msg, kind = "info") => {
    setToast({ msg, kind });
    clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToast(null), 2400);
  }, []);

  const detachDragListeners = useCallback(() => {
    const listeners = dragListenersRef.current;
    if (listeners.move) window.removeEventListener("pointermove", listeners.move);
    if (listeners.up) window.removeEventListener("pointerup", listeners.up);
    if (listeners.cancel) window.removeEventListener("pointercancel", listeners.cancel);
    dragListenersRef.current = { move: null, up: null, cancel: null };
  }, []);

  useEffect(() => {
    return () => {
      detachDragListeners();
      clearTimeout(toastTimer.current);
    };
  }, [detachDragListeners]);

  const activeRack = useMemo(() => racks.find(r => r.id === activeRackId) || racks[0] || null, [racks, activeRackId]);
  const rackU = activeRack?.rackU ?? 42;
  const placedItems = activeRack?.placedItems ?? [];
  const placements = activeRack?.placements ?? {};
  const cables = activeRack?.cables ?? [];
  const placedList = useMemo(() => buildPlacedList(placedItems, placements), [placedItems, placements]);

  useEffect(() => {
    if (!racks.length) return;
    if (!racks.some(r => r.id === activeRackId)) {
      setActiveRackId(racks[0].id);
    }
  }, [racks, activeRackId]);

  useEffect(() => {
    if (!placedList.length) {
      setSelectedItemId(null);
      return;
    }
    if (!placedList.some(record => record.itemId === selectedItemId)) {
      setSelectedItemId(placedList[0].itemId);
    }
  }, [placedList, selectedItemId, activeRackId]);

  const activePorts = useMemo(() => {
    return placedList.flatMap(record =>
      buildPortInstances(record.item).map(port => ({
        ...port,
        rackId: activeRack?.id ?? "",
        rackName: activeRack?.name ?? "Rack",
        deviceId: record.itemId,
        deviceName: record.item.name,
        startU: record.startU,
        endU: record.endU,
      }))
    );
  }, [placedList, activeRack]);

  const portsById = useMemo(() => {
    const map = new Map();
    activePorts.forEach(port => map.set(port.id, port));
    return map;
  }, [activePorts]);

  const cableByPortId = useMemo(() => {
    const map = new Map();
    (cables || []).forEach(cable => {
      map.set(cable.fromPortId, cable);
      map.set(cable.toPortId, cable);
    });
    return map;
  }, [cables]);

  useEffect(() => {
    if (portDraft && !portsById.has(portDraft)) {
      setPortDraft(null);
    }
  }, [portDraft, portsById]);

  useEffect(() => {
    if (portTargetId && !portsById.has(portTargetId)) {
      setPortTargetId("");
    }
  }, [portTargetId, portsById]);

  const selectedPlaced = useMemo(
    () => placedList.find(record => record.itemId === selectedItemId) || null,
    [placedList, selectedItemId]
  );

  const selectedPorts = useMemo(
    () => activePorts.filter(port => port.deviceId === selectedItemId),
    [activePorts, selectedItemId]
  );

  const selectedFrontPorts = useMemo(
    () => selectedPorts.filter(port => port.side === "front"),
    [selectedPorts]
  );

  const selectedRearPorts = useMemo(
    () => selectedPorts.filter(port => port.side === "rear"),
    [selectedPorts]
  );

  const openTargetPorts = useMemo(() => {
    return activePorts.filter(port => port.id !== portDraft && !cableByPortId.has(port.id));
  }, [activePorts, portDraft, cableByPortId]);

  const itemConnectionStats = useMemo(() => {
    const stats = new Map();
    for (const record of placedList) {
      const ports = buildPortInstances(record.item);
      const connected = ports.filter(port => cableByPortId.has(port.id)).length;
      stats.set(record.itemId, {
        totalPorts: ports.length,
        connectedPorts: connected,
      });
    }
    return stats;
  }, [placedList, cableByPortId]);

  const enrichedCables = useMemo(() => {
    return (cables || [])
      .map(cable => {
        const fromPort = portsById.get(cable.fromPortId);
        const toPort = portsById.get(cable.toPortId);
        if (!fromPort || !toPort) return null;
        const fromRecord = placedList.find(record => record.itemId === fromPort.deviceId);
        const toRecord = placedList.find(record => record.itemId === toPort.deviceId);
        if (!fromRecord || !toRecord) return null;

        const fromPoint = getPortAnchorPoint(fromPort, fromRecord, rackU);
        const toPoint = getPortAnchorPoint(toPort, toRecord, rackU);
        const dx = Math.abs(toPoint.x - fromPoint.x);
        const curve = Math.max(12, dx * 0.55);
        const c1x = fromPoint.x < toPoint.x ? fromPoint.x + curve : fromPoint.x - curve;
        const c2x = fromPoint.x < toPoint.x ? toPoint.x - curve : toPoint.x + curve;

        return {
          ...cable,
          fromPort,
          toPort,
          fromRecord,
          toRecord,
          path: `M ${fromPoint.x} ${fromPoint.y} C ${c1x} ${fromPoint.y}, ${c2x} ${toPoint.y}, ${toPoint.x} ${toPoint.y}`,
          fromPoint,
          toPoint,
        };
      })
      .filter(Boolean);
  }, [cables, portsById, placedList, rackU]);

  const activePortDraftInfo = portDraft ? portsById.get(portDraft) || null : null;

  const templateView = useMemo(() => {
    const q = invFilter.trim().toLowerCase();
    return templates
      .filter(item => {
        if (!q) return true;
        return [item.name, item.notes, portsSummary(item)].join(" ").toLowerCase().includes(q);
      })
      .sort((a, b) => (a.name || "").localeCompare(b.name || ""));
  }, [templates, invFilter]);

  const updateRackById = useCallback((rackId, updater) => {
    setRacks(prev => prev.map(rack => rack.id === rackId ? updater(rack) : rack));
  }, []);

  const updateActiveRack = useCallback((updater) => {
    if (!activeRackId) return;
    updateRackById(activeRackId, updater);
  }, [activeRackId, updateRackById]);

  const clearCableDraft = useCallback(() => {
    setPortDraft(null);
    setPortTargetId("");
  }, []);

  function addTemplate({ name, u, color, notes = "", ports = [] }) {
    const template = new Item(name, u, color, notes, ports, true);
    setTemplates(prev => [template, ...prev]);
    showToast("Added template.", "success");
  }

  const removeTemplate = useCallback((templateId) => {
    setTemplates(prev => prev.filter(item => item.id !== templateId));
    showToast("Removed template.", "info");
  }, [showToast]);

  function addRack() {
    const id = uuid();
    const name = `Rack ${racks.length + 1}`;
    const next = { id, name, rackU, placedItems: [], placements: {}, cables: [] };
    setRacks(prev => [...prev, next]);
    setActiveRackId(id);
    clearCableDraft();
    showToast("Added rack.", "success");
  }

  const renameActiveRack = useCallback(() => {
    if (!activeRack) return;
    const nextName = window.prompt("Rename rack:", activeRack.name);
    if (nextName == null) return;
    const name = String(nextName).trim();
    if (!name) return;
    updateActiveRack(rack => ({ ...rack, name }));
    showToast("Renamed rack.", "success");
  }, [activeRack, updateActiveRack, showToast]);

  const duplicateActiveRack = useCallback(() => {
    if (!activeRack) return;
    const id = uuid();
    const name = `${activeRack.name} (Copy)`;
    const idMap = new Map();

    const placedClone = (activeRack.placedItems || []).map(item => {
      const copy = {
        id: uuid(),
        name: String(item.name ?? "Device"),
        u: Math.max(1, safeInt(item.u ?? 1, 1)),
        color: normalizeColor(item.color ?? "#60a5fa", "#60a5fa"),
        notes: String(item.notes ?? ""),
        ports: normalizePortGroups(item.ports),
        isTemplate: false,
      };
      idMap.set(item.id, copy.id);
      return copy;
    });

    const placementsClone = {};
    for (const [oldId, startU] of Object.entries(activeRack.placements || {})) {
      const nextId = idMap.get(oldId);
      if (nextId) placementsClone[nextId] = safeInt(startU, 1);
    }

    const cablesClone = (activeRack.cables || [])
      .map(cable => {
        const from = parsePortId(cable.fromPortId);
        const to = parsePortId(cable.toPortId);
        const nextFromItemId = idMap.get(from.itemId);
        const nextToItemId = idMap.get(to.itemId);
        if (!nextFromItemId || !nextToItemId) return null;
        return {
          ...normalizeCable(cable),
          id: uuid(),
          fromPortId: makePortId(nextFromItemId, from.side, from.typeKey, from.index),
          toPortId: makePortId(nextToItemId, to.side, to.typeKey, to.index),
        };
      })
      .filter(Boolean);

    const next = {
      id,
      name,
      rackU: clamp(activeRack.rackU ?? 42, 1, 60),
      placedItems: placedClone,
      placements: placementsClone,
      cables: sanitizeCables(cablesClone, placedClone),
    };

    setRacks(prev => [...prev, next]);
    setActiveRackId(id);
    clearCableDraft();
    showToast("Duplicated rack.", "success");
  }, [activeRack, showToast, clearCableDraft]);

  const deleteActiveRack = useCallback(() => {
    if (racks.length <= 1) {
      showToast("You must keep at least one rack.", "warn");
      return;
    }
    if (!activeRack) return;
    const ok = window.confirm(`Delete "${activeRack.name}"? This cannot be undone.`);
    if (!ok) return;

    const idx = racks.findIndex(rack => rack.id === activeRackId);
    const nextRacks = racks.filter(rack => rack.id !== activeRackId);
    const nextActive = nextRacks[Math.max(0, idx - 1)] || nextRacks[0];
    setRacks(nextRacks);
    setActiveRackId(nextActive.id);
    clearCableDraft();
    showToast("Deleted rack.", "info");
  }, [racks, activeRack, activeRackId, showToast, clearCableDraft]);

  function placeFromTemplate(templateId, startU) {
    const template = templates.find(item => item.id === templateId);
    if (!template) return null;
    const instance = new Item(
      template.name,
      template.u,
      template.color,
      template.notes,
      normalizePortGroups(template.ports),
      false
    );
    updateActiveRack(rack => ({
      ...rack,
      placedItems: [instance, ...(rack.placedItems || [])],
      placements: { ...(rack.placements || {}), [instance.id]: startU },
      cables: rack.cables || [],
    }));
    return instance.id;
  }

  const movePlaced = useCallback((itemId, startU) => {
    updateActiveRack(rack => ({
      ...rack,
      placements: { ...(rack.placements || {}), [itemId]: startU },
    }));
  }, [updateActiveRack]);

  const removePlaced = useCallback((itemId) => {
    clearCableDraft();
    updateActiveRack(rack => {
      const nextPlaced = (rack.placedItems || []).filter(item => item.id !== itemId);
      const nextPlacements = { ...(rack.placements || {}) };
      delete nextPlacements[itemId];
      const nextCables = (rack.cables || []).filter(cable => {
        const fromItem = parsePortId(cable.fromPortId).itemId;
        const toItem = parsePortId(cable.toPortId).itemId;
        return fromItem !== itemId && toItem !== itemId;
      });
      return {
        ...rack,
        placedItems: nextPlaced,
        placements: nextPlacements,
        cables: nextCables,
      };
    });
  }, [updateActiveRack, clearCableDraft]);

  const clearRack = useCallback(() => {
    clearCableDraft();
    setSelectedItemId(null);
    updateActiveRack(rack => ({ ...rack, placedItems: [], placements: {}, cables: [] }));
    showToast("Cleared active rack.", "info");
  }, [updateActiveRack, showToast, clearCableDraft]);

  const isFree = useCallback((startU, itemU, ignoreId = null) => {
    return isFreeInPlacedList(startU, itemU, placedList, rackU, ignoreId);
  }, [placedList, rackU]);

  const removeCable = useCallback((cableId) => {
    updateActiveRack(rack => ({
      ...rack,
      cables: (rack.cables || []).filter(cable => cable.id !== cableId),
    }));
    showToast("Removed cable.", "info");
  }, [updateActiveRack, showToast]);

  const clearRackCables = useCallback(() => {
    clearCableDraft();
    updateActiveRack(rack => ({ ...rack, cables: [] }));
    showToast("Cleared rack cables.", "info");
  }, [updateActiveRack, showToast, clearCableDraft]);

  const connectPorts = useCallback((fromPortId, toPortId) => {
    if (!fromPortId || !toPortId) {
      showToast("Choose both cable endpoints.", "warn");
      return;
    }
    if (fromPortId === toPortId) {
      showToast("A cable needs two different ports.", "warn");
      return;
    }
    if (!portsById.has(fromPortId) || !portsById.has(toPortId)) {
      showToast("One or more ports are no longer available.", "warn");
      return;
    }
    if (cableByPortId.has(fromPortId) || cableByPortId.has(toPortId)) {
      showToast("Each port can only hold one cable.", "warn");
      return;
    }

    const cable = {
      id: uuid(),
      fromPortId,
      toPortId,
      label: cableLabel.trim() || `Cable ${String(cables.length + 1).padStart(2, "0")}`,
      type: cableType.trim() || "Patch",
      color: normalizeColor(cableColor, "#38bdf8"),
      notes: cableNotes.trim(),
    };

    updateActiveRack(rack => ({
      ...rack,
      cables: [...(rack.cables || []), cable],
    }));
    setCableLabel("");
    setCableNotes("");
    setPortDraft(null);
    setPortTargetId("");
    showToast("Connected ports.", "success");
  }, [portsById, cableByPortId, cableLabel, cableType, cableColor, cableNotes, updateActiveRack, showToast, cables.length]);

  function connectDraftToSelectedTarget() {
    if (!portDraft || !portTargetId) {
      showToast("Pick a target port to finish the cable.", "warn");
      return;
    }
    connectPorts(portDraft, portTargetId);
  }

  function beginPortDraft(portId) {
    if (!portsById.has(portId)) return;
    if (cableByPortId.has(portId)) {
      showToast("That port is already connected.", "warn");
      return;
    }
    if (portDraft === portId) {
      setPortDraft(null);
      setPortTargetId("");
      return;
    }
    setPortDraft(portId);
    setPortTargetId("");
  }

  function handlePortAction(portId) {
    if (!portId) return;
    if (cableByPortId.has(portId)) {
      showToast("Disconnect the existing cable before reusing that port.", "warn");
      return;
    }
    if (!portDraft) {
      beginPortDraft(portId);
      return;
    }
    if (portDraft === portId) {
      setPortDraft(null);
      setPortTargetId("");
      return;
    }
    connectPorts(portDraft, portId);
  }

  function startTemplateDrag(templateId, e) {
    const template = templates.find(item => item.id === templateId);
    if (!template) return;

    const rackEl = rackRef.current;
    const placedSnapshot = [...placedList];
    const rackUSnapshot = rackU;
    let session = {
      kind: "template",
      templateId,
      itemId: templateId,
      itemU: template.u,
      sourceStartU: null,
      hoverU: null,
      previewStartU: null,
      valid: false,
      pointerOffsetU: 0,
    };

    session = { ...session, ...resolveDropPreview(e, rackEl, rackUSnapshot, template.u, 0, placedSnapshot, null) };
    setDragSession(session);

    try {
      if (e?.currentTarget?.setPointerCapture && e.pointerId != null) {
        e.currentTarget.setPointerCapture(e.pointerId);
        pointerCaptureRef.current = e.currentTarget;
      }
    } catch (_) {}

    detachDragListeners();

    const onMove = (evt) => {
      session = { ...session, ...resolveDropPreview(evt, rackEl, rackUSnapshot, template.u, 0, placedSnapshot, null) };
      setDragSession(session);
    };

    const finalize = (evt, cancelled = false) => {
      try {
        if (pointerCaptureRef.current && typeof pointerCaptureRef.current.releasePointerCapture === "function" && evt?.pointerId != null) {
          pointerCaptureRef.current.releasePointerCapture(evt.pointerId);
        }
      } catch (_) {}
      pointerCaptureRef.current = null;
      detachDragListeners();

      const finalSession = session;
      if (!cancelled && finalSession.previewStartU != null && finalSession.valid) {
        const nextId = placeFromTemplate(templateId, finalSession.previewStartU);
        if (nextId) setSelectedItemId(nextId);
        showToast("Placed item.", "success");
      } else if (!cancelled) {
        showToast("That rack space is occupied.", "warn");
      }
      setDragSession(null);
    };

    dragListenersRef.current = {
      move: onMove,
      up: (evt) => finalize(evt, false),
      cancel: (evt) => finalize(evt, true),
    };
    window.addEventListener("pointermove", dragListenersRef.current.move, { passive: true });
    window.addEventListener("pointerup", dragListenersRef.current.up, { passive: true });
    window.addEventListener("pointercancel", dragListenersRef.current.cancel, { passive: true });
  }

  function startPlacedDrag(itemId, e, itemU, startU) {
    const rackEl = rackRef.current;
    const placedSnapshot = [...placedList];
    const rackUSnapshot = rackU;
    const initialHover = resolveDropPreview(e, rackEl, rackUSnapshot, itemU, 0, placedSnapshot, itemId).hoverU;
    const pointerOffsetU = clamp((initialHover ?? startU) - startU, 0, Math.max(0, itemU - 1));
    let session = {
      kind: "placed",
      templateId: null,
      itemId,
      itemU,
      sourceStartU: startU,
      hoverU: null,
      previewStartU: startU,
      valid: true,
      pointerOffsetU,
    };

    session = { ...session, ...resolveDropPreview(e, rackEl, rackUSnapshot, itemU, pointerOffsetU, placedSnapshot, itemId) };
    setSelectedItemId(itemId);
    setDragSession(session);

    try {
      if (e?.currentTarget?.setPointerCapture && e.pointerId != null) {
        e.currentTarget.setPointerCapture(e.pointerId);
        pointerCaptureRef.current = e.currentTarget;
      }
    } catch (_) {}

    detachDragListeners();

    const onMove = (evt) => {
      session = { ...session, ...resolveDropPreview(evt, rackEl, rackUSnapshot, itemU, pointerOffsetU, placedSnapshot, itemId) };
      setDragSession(session);
    };

    const finalize = (evt, cancelled = false) => {
      try {
        if (pointerCaptureRef.current && typeof pointerCaptureRef.current.releasePointerCapture === "function" && evt?.pointerId != null) {
          pointerCaptureRef.current.releasePointerCapture(evt.pointerId);
        }
      } catch (_) {}
      pointerCaptureRef.current = null;
      detachDragListeners();

      const finalSession = session;
      if (!cancelled && finalSession.previewStartU != null && finalSession.valid) {
        if (finalSession.previewStartU !== startU) {
          movePlaced(itemId, finalSession.previewStartU);
          showToast("Moved item.", "success");
        }
      } else if (!cancelled) {
        showToast("That rack space is occupied.", "warn");
      }
      setDragSession(null);
    };

    dragListenersRef.current = {
      move: onMove,
      up: (evt) => finalize(evt, false),
      cancel: (evt) => finalize(evt, true),
    };
    window.addEventListener("pointermove", dragListenersRef.current.move, { passive: true });
    window.addEventListener("pointerup", dragListenersRef.current.up, { passive: true });
    window.addEventListener("pointercancel", dragListenersRef.current.cancel, { passive: true });
  }

  function exportTemplate() {
    const header = ["Name", "U", "Color", "Notes", "Ports"];
    const example = [
      ["Switch 24p", "1", "#f59e0b", "Access switch", "Front-RJ45:24;Front-SFP:4;Rear-AC:1"],
      ["2U UPS", "2", "#10b981", "UPS", "Rear-AC:2"],
      ["Patch Panel", "1", "#a78bfa", "Copper patching", "Front-RJ45:24"],
    ];
    const rows = [header, ...example];
    const csv = rows.map(row => row.map(value => {
      const s = String(value ?? "");
      if (s.includes(",") || s.includes('"') || s.includes("\n")) {
        return `"${s.replace(/"/g, '""')}"`;
      }
      return s;
    }).join(",")).join("\n");
    downloadBlob(new Blob([csv], { type: "text/csv;charset=utf-8" }), "engever-inventory-template.csv");
    showToast("Downloaded inventory template.", "success");
  }

  async function onImportFile(file) {
    if (!file) {
      showToast("No file selected.", "warn");
      return;
    }

    setImporting(true);
    try {
      const isCSV = file.name.toLowerCase().endsWith(".csv");
      let rows = [];

      if (isCSV) {
        const csvText = await file.text();
        if (!csvText) {
          showToast("CSV file is empty.", "warn");
          return;
        }
        const parsed = Papa.parse(csvText, { header: true, skipEmptyLines: true });
        if (parsed.errors?.length) console.warn("CSV parsing errors:", parsed.errors);
        rows = parsed.data || [];
      } else {
        const buf = await file.arrayBuffer();
        if (!buf || buf.byteLength === 0) {
          showToast("Spreadsheet file is empty.", "warn");
          return;
        }
        const workbook = XLSX.read(buf, { type: "array" });
        if (!workbook || !workbook.SheetNames.length) {
          showToast("No sheets found in spreadsheet.", "warn");
          return;
        }
        const sheet = workbook.Sheets[workbook.SheetNames[0]];
        rows = XLSX.utils.sheet_to_json(sheet, { defval: "" });
      }

      const parsedItems = rows
        .filter(row => (row.Name || row.name || row.U || row.u))
        .map((row, index) => {
          const name = String(row.Name ?? row.name ?? `Device ${index + 1}`).trim() || `Device ${index + 1}`;
          const u = Math.max(1, safeInt(row.U ?? row.u ?? 1, 1));
          const color = normalizeColor(row.Color ?? row.color ?? "#60a5fa", "#60a5fa");
          const notes = String(row.Notes ?? row.notes ?? "").trim();
          const ports = parsePorts(row.Ports ?? row.ports ?? "");
          return new Item(name, u, color, notes, ports, true);
        });

      if (!parsedItems.length) {
        showToast("No valid items found. Check your template columns.", "warn");
        return;
      }

      setTemplates(prev => [...parsedItems, ...prev]);
      showToast(`Imported ${parsedItems.length} template(s).`, "success");
    } catch (error) {
      console.error("Import error:", error);
      showToast(`Import failed: ${error.message || "Unknown error"}`, "warn");
    } finally {
      setImporting(false);
    }
  }

  async function exportPNG() {
    const node = rackRef.current;
    if (!node) {
      showToast("Rack element not found.", "warn");
      return;
    }
    if (!activeRack) {
      showToast("No active rack.", "warn");
      return;
    }

    try {
      const dataUrl = await htmlToImage.toPng(node, { pixelRatio: 2, backgroundColor: "#020617" });
      const a = document.createElement("a");
      a.href = dataUrl;
      a.download = `engever-${slugify(activeRack.name)}-${rackU}U-connected.png`;
      a.click();
      showToast("Exported PNG.", "success");
    } catch (error) {
      console.error("PNG export error:", error);
      showToast(`PNG export failed: ${error.message || "Unknown error"}`, "warn");
    }
  }

  function buildBOMRows() {
    const grouped = new Map();
    for (const record of placedList) {
      const stats = itemConnectionStats.get(record.itemId) || { totalPorts: 0, connectedPorts: 0 };
      const key = [
        record.item.name,
        record.item.u,
        record.item.notes || "",
        portsSummary(record.item),
      ].join("||");
      if (!grouped.has(key)) {
        grouped.set(key, {
          Device: record.item.name,
          "Rack Units": record.item.u,
          "Ports Summary": portsSummary(record.item),
          Notes: record.item.notes || "",
          Quantity: 0,
          "Ports Per Device": stats.totalPorts,
        });
      }
      grouped.get(key).Quantity += 1;
    }
    return Array.from(grouped.values()).sort((a, b) => String(a.Device).localeCompare(String(b.Device)));
  }

  function buildPlacementRows() {
    return placedList.map(record => {
      const stats = itemConnectionStats.get(record.itemId) || { totalPorts: 0, connectedPorts: 0 };
      return {
        Rack: activeRack?.name ?? "Rack",
        DeviceId: record.itemId,
        Device: record.item.name,
        U: record.item.u,
        StartU: record.startU,
        EndU: record.endU,
        Ports: stats.totalPorts,
        ConnectedPorts: stats.connectedPorts,
        Notes: record.item.notes || "",
      };
    }).sort((a, b) => b.StartU - a.StartU);
  }

  function buildPortMapRows() {
    return activePorts.map(port => {
      const cable = cableByPortId.get(port.id);
      const oppositePortId = cable ? (cable.fromPortId === port.id ? cable.toPortId : cable.fromPortId) : "";
      const oppositePort = oppositePortId ? portsById.get(oppositePortId) : null;
      return {
        Rack: port.rackName,
        DeviceId: port.deviceId,
        Device: port.deviceName,
        StartU: port.startU,
        EndU: port.endU,
        Side: port.sideLabel,
        PortType: port.type,
        PortIndex: port.index,
        PortLabel: port.label,
        Status: cable ? "Connected" : "Open",
        CableId: cable?.id || "",
        CableLabel: cable?.label || "",
        CableType: cable?.type || "",
        CableColor: cable?.color || "",
        ConnectedToDevice: oppositePort?.deviceName || "",
        ConnectedToSide: oppositePort?.sideLabel || "",
        ConnectedToPort: oppositePort?.label || "",
        CableNotes: cable?.notes || "",
      };
    }).sort((a, b) => {
      if (a.StartU !== b.StartU) return b.StartU - a.StartU;
      if (a.Device !== b.Device) return String(a.Device).localeCompare(String(b.Device));
      if (a.Side !== b.Side) return String(a.Side).localeCompare(String(b.Side));
      return a.PortIndex - b.PortIndex;
    });
  }

  function buildConnectionRows() {
    return enrichedCables.map(cable => ({
      Rack: activeRack?.name ?? "Rack",
      CableId: cable.id,
      CableLabel: cable.label || "",
      CableType: cable.type || "",
      CableColor: cable.color || "",
      FromDevice: cable.fromPort.deviceName,
      FromStartU: cable.fromPort.startU,
      FromSide: cable.fromPort.sideLabel,
      FromPort: cable.fromPort.label,
      ToDevice: cable.toPort.deviceName,
      ToStartU: cable.toPort.startU,
      ToSide: cable.toPort.sideLabel,
      ToPort: cable.toPort.label,
      Notes: cable.notes || "",
    })).sort((a, b) => String(a.CableLabel).localeCompare(String(b.CableLabel)));
  }

  function exportXLSX() {
    if (!activeRack) {
      showToast("No active rack to export.", "warn");
      return;
    }

    try {
      const bomRows = buildBOMRows();
      const placementRows = buildPlacementRows();
      const portMapRows = buildPortMapRows();
      const connectionRows = buildConnectionRows();

      if (!placementRows.length) {
        showToast("No items to export.", "warn");
        return;
      }

      const workbook = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(workbook, createWorksheet(
        ["Device", "Rack Units", "Ports Summary", "Notes", "Quantity", "Ports Per Device"],
        bomRows
      ), "BOM");
      XLSX.utils.book_append_sheet(workbook, createWorksheet(
        ["Rack", "DeviceId", "Device", "U", "StartU", "EndU", "Ports", "ConnectedPorts", "Notes"],
        placementRows
      ), "Placements");
      XLSX.utils.book_append_sheet(workbook, createWorksheet(
        ["Rack", "DeviceId", "Device", "StartU", "EndU", "Side", "PortType", "PortIndex", "PortLabel", "Status", "CableId", "CableLabel", "CableType", "CableColor", "ConnectedToDevice", "ConnectedToSide", "ConnectedToPort", "CableNotes"],
        portMapRows
      ), "Port Map");
      XLSX.utils.book_append_sheet(workbook, createWorksheet(
        ["Rack", "CableId", "CableLabel", "CableType", "CableColor", "FromDevice", "FromStartU", "FromSide", "FromPort", "ToDevice", "ToStartU", "ToSide", "ToPort", "Notes"],
        connectionRows
      ), "Connections");

      XLSX.writeFile(workbook, `engever-${slugify(activeRack.name)}-${rackU}U-connected-export.xlsx`);
      showToast("Exported XLSX.", "success");
    } catch (error) {
      console.error("XLSX export error:", error);
      showToast(`XLSX export failed: ${error.message || "Unknown error"}`, "warn");
    }
  }

  function saveJSON() {
    try {
      const payload = { version: 3, templates, racks, activeRackId };
      downloadBlob(
        new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }),
        "engever-project-connected.json"
      );
      showToast("Saved project JSON.", "success");
    } catch (error) {
      console.error("Save error:", error);
      showToast(`Save failed: ${error.message || "Unknown error"}`, "warn");
    }
  }

  async function loadJSON(file) {
    if (!file) {
      showToast("No file selected.", "warn");
      return;
    }

    try {
      const text = await file.text();
      if (!text) {
        showToast("JSON file is empty.", "warn");
        return;
      }

      const payload = JSON.parse(text);
      if (!payload) {
        showToast("Invalid JSON format.", "warn");
        return;
      }

      if (payload && Array.isArray(payload.racks)) {
        const nextTemplates = Array.isArray(payload.templates)
          ? payload.templates.map(item => normalizeItem(item, true))
          : [];
        const nextRacksRaw = payload.racks.map(rack => normalizeRack(rack)).filter(Boolean);
        const safeRacks = nextRacksRaw.length
          ? nextRacksRaw
          : [{ id: uuid(), name: "Rack 1", rackU: 42, placedItems: [], placements: {}, cables: [] }];
        const nextActiveRackId = payload.activeRackId && safeRacks.some(rack => rack.id === payload.activeRackId)
          ? payload.activeRackId
          : safeRacks[0].id;

        setTemplates(nextTemplates.length ? nextTemplates : templates);
        setRacks(safeRacks);
        setActiveRackId(nextActiveRackId);
        clearCableDraft();
        showToast("Loaded project JSON.", "success");
        return;
      }

      const nextRackU = typeof payload.rackU === "number" ? clamp(payload.rackU, 1, 60) : rackU;
      const legacyItems = Array.isArray(payload.items) ? payload.items : [];
      const normalized = legacyItems.filter(Boolean).map(item => normalizeItem(item, !!item.isTemplate));

      const legacyTemplates = normalized.filter(item => item.isTemplate).map(item => ({ ...item, isTemplate: true }));
      const legacyPlaced = normalized.filter(item => !item.isTemplate).map(item => ({ ...item, isTemplate: false }));
      const placementsRaw = payload.placements && typeof payload.placements === "object" ? payload.placements : {};
      const itemById = new Map(legacyPlaced.map(item => [item.id, item]));
      const cleanedPlacements = {};
      for (const [pid, start] of Object.entries(placementsRaw)) {
        const item = itemById.get(pid);
        if (!item) continue;
        const maxStart = Math.max(1, nextRackU - item.u + 1);
        cleanedPlacements[pid] = clamp(safeInt(start, 1), 1, maxStart);
      }

      const rackId = uuid();
      setTemplates(legacyTemplates.length ? legacyTemplates : templates);
      setRacks([{ id: rackId, name: "Rack 1", rackU: nextRackU, placedItems: legacyPlaced, placements: cleanedPlacements, cables: [] }]);
      setActiveRackId(rackId);
      clearCableDraft();
      showToast("Loaded legacy layout JSON.", "success");
    } catch (error) {
      console.error("Load error:", error);
      showToast(`Failed to load JSON: ${error.message || "Invalid file"}`, "warn");
    }
  }

  function ItemBlock({ record, isGhost = false }) {
    const { item, startU } = record;
    const heightPercent = (item.u / rackU) * 100;
    const bottomPercent = ((startU - 1) / rackU) * 100;
    const stats = itemConnectionStats.get(item.id) || { totalPorts: 0, connectedPorts: 0 };
    const isSelected = !isGhost && selectedItemId === item.id;
    const isDraggingOrigin = dragSession?.kind === "placed" && dragSession?.itemId === item.id && !isGhost;
    const validAtPosition = isGhost ? dragSession?.valid !== false : isFree(startU, item.u, item.id);
    const ports = buildPortInstances(item);
    const frontPorts = ports.filter(port => port.side === "front");
    const rearPorts = ports.filter(port => port.side === "rear");
    const itemHeightPx = Math.max(10, (740 / rackU) * item.u - 4);
    const sizeMode = itemHeightPx < 22 ? "micro" : itemHeightPx < 36 ? "compact" : itemHeightPx < 60 ? "regular" : "tall";
    const dotSizePx = sizeMode === "micro" ? 4 : sizeMode === "compact" ? 5 : 6;
    const markerInsetPx = sizeMode === "micro" ? 3 : 5;
    const markerBudget = clamp(
      Math.floor((itemHeightPx - 4) / (dotSizePx + (sizeMode === "micro" ? 3 : 4))),
      1,
      isSelected ? 10 : 6
    );
    const frontMarkers = buildVisibleSideMarkers(frontPorts, markerBudget);
    const rearMarkers = buildVisibleSideMarkers(rearPorts, markerBudget);
    const nameFontPx = sizeMode === "micro" ? 8 : sizeMode === "compact" ? 9 : sizeMode === "regular" ? 11 : 12;
    const detailFontPx = sizeMode === "compact" ? 8 : sizeMode === "regular" ? 9 : 10;
    const badgeFontPx = sizeMode === "micro" ? 8 : 9;
    const actionSizePx = sizeMode === "micro" ? 18 : 22;
    const iconSizePx = sizeMode === "micro" ? 10 : 13;
    const padY = sizeMode === "micro" ? 1 : sizeMode === "compact" ? 2 : 4;
    const padLeft = frontPorts.length ? dotSizePx + markerInsetPx + 8 : 8;
    const padRight = Math.max(rearPorts.length ? dotSizePx + markerInsetPx + 8 : 8, sizeMode === "micro" ? 52 : 74);
    const canShowSummary = !isGhost && itemHeightPx >= 30;
    const canShowStatus = !isGhost && stats.totalPorts > 0 && itemHeightPx >= 24;
    const compactStatus = stats.totalPorts > 0 ? `${stats.connectedPorts}/${stats.totalPorts}` : "";

    function renderSideMarkers(markers, side) {
      return markers.map(marker => {
        const cable = cableByPortId.get(marker.id);
        const topPercent = ((marker.displayIndex + 0.5) / marker.displayCount) * 100;
        return (
          <span
            key={marker.id}
            className="absolute rounded-full border border-black/20"
            title={marker.label}
            style={{
              [side === "front" ? "left" : "right"]: `${markerInsetPx}px`,
              top: `calc(${topPercent}% - ${dotSizePx / 2}px)`,
              width: `${dotSizePx}px`,
              height: `${dotSizePx}px`,
              backgroundColor: cable ? (cable.color || "#38bdf8") : "rgba(255,255,255,0.78)",
            }}
          />
        );
      });
    }

    return (
      <div
        className={
          "absolute left-[36px] right-1 rounded border shadow-md overflow-hidden no-select touch-none " +
          (isGhost ? "border-cyan-300/60 opacity-85" : "border-white/15") +
          (isSelected ? " ring-2 ring-cyan-400/60" : "") +
          (!validAtPosition ? " ring-2 ring-rose-400/70" : "")
        }
        style={{
          background: item.color,
          height: `calc(${heightPercent}% - 4px)`,
          bottom: `calc(${bottomPercent}% + 2px)`,
          opacity: isDraggingOrigin ? 0.28 : undefined,
        }}
        onPointerDown={(e) => {
          if (isGhost) return;
          if (isInteractive(e.target)) return;
          e.preventDefault();
          setSelectedItemId(item.id);
          startPlacedDrag(item.id, e, item.u, startU);
        }}
        onClick={() => {
          if (!isGhost) setSelectedItemId(item.id);
        }}
      >
        {renderSideMarkers(frontMarkers, "front")}
        {renderSideMarkers(rearMarkers, "rear")}

        <div
          className="h-full text-white"
          style={{
            paddingTop: `${padY}px`,
            paddingBottom: `${padY}px`,
            paddingLeft: `${padLeft}px`,
            paddingRight: `${padRight}px`,
          }}
        >
          {sizeMode === "micro" ? (
            <div className="h-full flex items-center justify-between gap-1">
              <div className="min-w-0 flex-1">
                <div
                  className="font-semibold truncate leading-none"
                  title={`${item.name} (${item.u}U)`}
                  style={{ fontSize: `${nameFontPx}px` }}
                >
                  {item.name}
                </div>
              </div>

              <div className="flex items-center gap-1 shrink-0">
                <span
                  className="bg-black/35 rounded px-1 py-[1px] leading-none"
                  style={{ fontSize: `${badgeFontPx}px` }}
                >
                  {item.u}U
                </span>
                {!!compactStatus && (
                  <span
                    className="bg-black/35 rounded px-1 py-[1px] leading-none"
                    style={{ fontSize: `${badgeFontPx}px` }}
                  >
                    {compactStatus}
                  </span>
                )}
                {!isGhost && (
                  <button
                    onClick={(e) => {
                      e.stopPropagation();
                      removePlaced(item.id);
                    }}
                    className="text-white/80 hover:text-white grid place-items-center shrink-0"
                    title="Remove"
                    style={{ width: `${actionSizePx}px`, height: `${actionSizePx}px` }}
                  >
                    <Trash2 size={iconSizePx} />
                  </button>
                )}
              </div>
            </div>
          ) : (
            <div className="h-full flex flex-col justify-between gap-[2px]">
              <div className="flex items-start justify-between gap-2">
                <div className="min-w-0 flex-1">
                  <div
                    className="font-semibold truncate leading-tight"
                    title={`${item.name} (${item.u}U)`}
                    style={{ fontSize: `${nameFontPx}px` }}
                  >
                    {item.name}
                  </div>
                  {canShowSummary && (
                    <div
                      className="text-white/90 truncate leading-tight"
                      style={{ fontSize: `${detailFontPx}px` }}
                    >
                      {portsSummary(item) || "No defined ports"}
                    </div>
                  )}
                </div>

                <div className="flex items-center gap-1 shrink-0">
                  <span
                    className="bg-black/35 rounded px-1 py-[1px] leading-none"
                    style={{ fontSize: `${badgeFontPx}px` }}
                  >
                    {item.u}U
                  </span>
                  {stats.totalPorts > 0 && (
                    <span
                      className="bg-black/35 rounded px-1 py-[1px] leading-none"
                      style={{ fontSize: `${badgeFontPx}px` }}
                    >
                      {stats.connectedPorts}/{stats.totalPorts}
                    </span>
                  )}
                  {!isGhost && (
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        removePlaced(item.id);
                      }}
                      className="text-white/80 hover:text-white grid place-items-center shrink-0"
                      title="Remove"
                      style={{ width: `${actionSizePx}px`, height: `${actionSizePx}px` }}
                    >
                      <Trash2 size={iconSizePx} />
                    </button>
                  )}
                </div>
              </div>

              {canShowStatus && (
                <div
                  className="text-white/90 truncate leading-none"
                  style={{ fontSize: `${detailFontPx}px` }}
                >
                  {stats.connectedPorts ? `${stats.connectedPorts} connected` : `${stats.totalPorts} open`}
                </div>
              )}
            </div>
          )}
        </div>
      </div>
    );
  }

  function PortCard({ port }) {
    const cable = cableByPortId.get(port.id);
    const oppositePortId = cable ? (cable.fromPortId === port.id ? cable.toPortId : cable.fromPortId) : "";
    const oppositePort = oppositePortId ? portsById.get(oppositePortId) : null;
    const isDraftSource = portDraft === port.id;
    const cardClass = cable
      ? "border-emerald-500/40 bg-emerald-500/10"
      : isDraftSource
        ? "border-cyan-400/60 bg-cyan-500/10"
        : "border-slate-800 bg-slate-950/50 hover:bg-slate-950/70";

    return (
      <div className={`rounded border p-2 ${cardClass}`}>
        <div className="flex items-start justify-between gap-2">
          <div className="min-w-0">
            <div className="text-sm font-medium">
              {port.sideShort} - {port.shortLabel}
            </div>
            <div className="text-[11px] text-slate-300 truncate">
              {cable && oppositePort
                ? `Connected to ${oppositePort.deviceName} / ${oppositePort.label}`
                : isDraftSource
                  ? "Cable source selected"
                  : "Open port"}
            </div>
          </div>

          {cable ? (
            <button
              onClick={() => removeCable(cable.id)}
              className="px-2 py-1 text-[11px] rounded bg-slate-900/80 border border-slate-700 hover:bg-slate-800"
              title="Disconnect cable"
            >
              Unplug
            </button>
          ) : (
            <button
              onClick={() => handlePortAction(port.id)}
              className="px-2 py-1 text-[11px] rounded bg-slate-900/80 border border-slate-700 hover:bg-slate-800"
              title={portDraft && portDraft !== port.id ? "Connect here" : "Start cable from this port"}
            >
              {portDraft && portDraft !== port.id ? "Connect" : isDraftSource ? "Cancel" : "Start"}
            </button>
          )}
        </div>

        {cable && (
          <div className="mt-2 text-[11px] text-slate-200">
            <div className="flex items-center gap-2">
              <span className="inline-block h-2.5 w-2.5 rounded-full" style={{ backgroundColor: cable.color || "#38bdf8" }} />
              <span>{cable.label || cable.id}</span>
            </div>
            {!!cable.notes && (
              <div className="mt-1 text-slate-400">
                {cable.notes}
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  return (
    <div className="w-full min-h-screen p-6 bg-gradient-to-b from-slate-950 via-slate-950 to-slate-900 text-white">
      {toast && (
        <div className="fixed top-4 right-4 z-50">
          <div
            className={
              "px-4 py-3 rounded-lg border shadow-lg bg-slate-900/95 " +
              (toast.kind === "success" ? "border-emerald-500/40" :
                toast.kind === "warn" ? "border-amber-500/40" :
                "border-slate-700")
            }
          >
            <div className="text-sm">{toast.msg}</div>
          </div>
        </div>
      )}

      <div className="max-w-[1600px] mx-auto grid grid-cols-1 xl:grid-cols-[360px_minmax(0,1fr)] gap-6">
        <div className="bg-slate-900/60 border border-slate-800 rounded-lg text-white">
          <div className="p-4 border-b border-slate-800 flex items-center justify-between gap-3">
            <div className="min-w-0">
              <div className="text-lg font-bold">Inventory</div>
              <div className="text-xs text-slate-300">{templateView.length} template(s)</div>
            </div>

            <div className="flex items-center gap-2">
              <button
                onClick={exportTemplate}
                className="p-2 bg-slate-700 rounded hover:bg-slate-600"
                title="Download Inventory CSV Template"
              >
                <Download size={16} />
              </button>

              <label className="inline-flex items-center">
                <input
                  type="file"
                  accept=".csv,.xlsx,.xls"
                  className="hidden"
                  onChange={(e) => {
                    const file = e.target.files?.[0];
                    if (!file) return;
                    onImportFile(file);
                    e.target.value = "";
                  }}
                />
                <span className="p-2 bg-slate-700 rounded hover:bg-slate-600 cursor-pointer" title="Import CSV or XLSX">
                  <Upload size={16} />
                </span>
              </label>
            </div>
          </div>

          <div className="p-4 border-b border-slate-800">
            <label className="block text-xs text-slate-300 mb-2">Filter</label>
            <input
              value={invFilter}
              onChange={(e) => setInvFilter(e.target.value)}
              placeholder="Search templates, notes, or ports..."
              className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500"
            />
          </div>

          <div className="p-4 border-b border-slate-800">
            <div className="text-sm font-semibold mb-3 flex items-center gap-2">
              <Plus size={16} /> Add Template
            </div>

            <div className="grid grid-cols-1 gap-2">
              <input
                value={newName}
                onChange={(e) => setNewName(e.target.value)}
                placeholder="Name"
                className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500"
              />

              <div className="flex gap-2">
                <div className="flex-1">
                  <label className="block text-xs text-slate-300 mb-1">U</label>
                  <input
                    type="number"
                    min="1"
                    max="60"
                    value={newU}
                    onChange={(e) => setNewU(safeInt(e.target.value, 1))}
                    className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500"
                  />
                </div>

                <div className="w-28">
                  <label className="block text-xs text-slate-300 mb-1">Color</label>
                  <input
                    type="color"
                    value={newColor}
                    onChange={(e) => setNewColor(e.target.value)}
                    className="w-full h-[42px] rounded bg-slate-950/50 border border-slate-800"
                  />
                </div>
              </div>

              <textarea
                value={newPorts}
                onChange={(e) => setNewPorts(e.target.value)}
                placeholder='Ports (example: "Front-RJ45:24;Rear-AC:2")'
                className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm"
                rows={2}
              />

              <textarea
                value={newNotes}
                onChange={(e) => setNewNotes(e.target.value)}
                placeholder="Notes (optional)"
                className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm"
                rows={2}
              />

              <button
                onClick={() => {
                  const name = newName.trim();
                  const u = Math.max(1, safeInt(newU, 1));
                  if (!name) {
                    showToast("Template needs a name.", "warn");
                    return;
                  }
                  addTemplate({ name, u, color: newColor, notes: newNotes.trim(), ports: parsePorts(newPorts) });
                  setNewName("");
                  setNewU(1);
                  setNewColor("#60a5fa");
                  setNewPorts("");
                  setNewNotes("");
                }}
                className="px-3 py-2 bg-cyan-600 rounded hover:bg-cyan-500 flex gap-2 items-center justify-center"
              >
                <Plus size={16} /> Add
              </button>
            </div>

            <div className="text-[11px] text-slate-400 mt-3">
              Ports accept multiple groups separated by semicolons or new lines.
              <div className="mt-1 font-mono text-slate-300">Front-RJ45:24;Rear-AC:2</div>
            </div>
          </div>

          <div className="p-4 border-b border-slate-800 text-xs text-slate-300 flex items-center gap-2">
            <Info size={14} />
            <span>{importing ? "Importing templates..." : "Drag templates into the rack. Select placed devices to manage ports and cables."}</span>
          </div>

          <div className="p-4 space-y-3 max-h-[60vh] overflow-auto">
            {templateView.map((item) => (
              <div
                key={item.id}
                className="p-3 rounded border border-slate-800 bg-slate-950/40 hover:bg-slate-950/60 cursor-grab active:cursor-grabbing touch-none"
                onPointerDown={(e) => {
                  if (isInteractive(e.target)) return;
                  e.preventDefault();
                  startTemplateDrag(item.id, e);
                }}
                title="Drag into the rack"
              >
                <div className="flex items-start justify-between gap-2">
                  <div className="min-w-0">
                    <div className="font-semibold truncate">{item.name}</div>
                    <div className="text-xs text-slate-300">{item.u}U {portsSummary(item) ? `- ${portsSummary(item)}` : ""}</div>
                  </div>

                  <div className="flex items-center">
                    <div className="h-4 w-4 rounded" style={{ background: item.color }} />
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        removeTemplate(item.id);
                      }}
                      className="h-7 w-7 ml-2 text-white/80 hover:text-white grid place-items-center"
                      title="Remove template"
                    >
                      <Trash2 size={14} />
                    </button>
                  </div>
                </div>

                {!!item.notes.trim() && (
                  <div className="text-xs text-slate-400 mt-2">
                    {item.notes}
                  </div>
                )}
              </div>
            ))}

            {!templateView.length && (
              <div className="text-sm text-slate-300 border border-slate-800 rounded p-3 bg-slate-900/30">
                No templates match your filter.
              </div>
            )}
          </div>
        </div>

        <div className="bg-slate-900/60 border border-slate-800 rounded-lg">
          <div className="p-4 border-b border-slate-800 flex flex-wrap items-center justify-between gap-3">
            <div className="min-w-0">
              <div className="text-lg font-bold flex items-center gap-2">
                <Layers size={18} /> Rack Workspace
              </div>
              <div className="text-xs text-slate-300">
                Active: <span className="font-semibold">{activeRack?.name ?? "Rack"}</span> | Devices: {placedList.length} | Height: {rackU}U | Cables: {enrichedCables.length}
              </div>
            </div>

            <div className="flex flex-wrap items-center gap-2">
              <div className="flex items-center gap-2 bg-slate-950/40 border border-slate-800 rounded px-2 py-1">
                <span className="text-xs text-slate-300">Rack</span>
                <select
                  value={activeRackId}
                  onChange={(e) => {
                    clearCableDraft();
                    setActiveRackId(e.target.value);
                  }}
                  className="bg-slate-900 text-white text-sm border border-slate-800 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-cyan-500"
                >
                  {racks.map(rack => (
                    <option key={rack.id} value={rack.id}>{rack.name}</option>
                  ))}
                </select>
                <button onClick={addRack} className="px-2 py-1 bg-slate-700 rounded hover:bg-slate-600 flex gap-1 items-center text-sm" title="New Rack">
                  <Plus size={14} /> New
                </button>
                <button onClick={duplicateActiveRack} className="px-2 py-1 bg-slate-700 rounded hover:bg-slate-600 flex gap-1 items-center text-sm" title="Duplicate Rack">
                  <Copy size={14} /> Copy
                </button>
                <button onClick={renameActiveRack} className="px-2 py-1 bg-slate-700 rounded hover:bg-slate-600 flex gap-1 items-center text-sm" title="Rename Rack">
                  <Edit3 size={14} /> Rename
                </button>
                <button
                  onClick={deleteActiveRack}
                  disabled={racks.length <= 1}
                  className={
                    "px-2 py-1 rounded flex gap-1 items-center text-sm " +
                    (racks.length <= 1 ? "bg-slate-800 text-slate-500 cursor-not-allowed" : "bg-rose-700 hover:bg-rose-600")
                  }
                  title={racks.length <= 1 ? "At least one rack is required" : "Delete Rack"}
                >
                  <Trash2 size={14} /> Delete
                </button>
              </div>

              <div className="flex items-center gap-2 bg-slate-950/40 border border-slate-800 rounded px-2 py-1">
                <span className="text-xs text-slate-300">Rack U</span>
                <input
                  type="number"
                  min="1"
                  max="60"
                  value={rackU}
                  onChange={(e) => {
                    const nextRackU = clamp(safeInt(e.target.value, rackU), 1, 60);
                    updateActiveRack(rack => {
                      const nextPlacements = {};
                      for (const [pid, start] of Object.entries(rack.placements || {})) {
                        const item = (rack.placedItems || []).find(x => x.id === pid);
                        if (!item) continue;
                        const maxStart = Math.max(1, nextRackU - item.u + 1);
                        nextPlacements[pid] = clamp(safeInt(start, 1), 1, maxStart);
                      }
                      return { ...rack, rackU: nextRackU, placements: nextPlacements };
                    });
                  }}
                  className="w-20 px-2 py-1 text-sm rounded bg-slate-900 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500"
                />
              </div>

              <button onClick={saveJSON} className="px-3 py-2 bg-slate-700 rounded hover:bg-slate-600 flex gap-2 items-center">
                <Save size={16} />Save JSON
              </button>

              <label className="inline-flex items-center">
                <input
                  type="file"
                  accept=".json"
                  className="hidden"
                  onChange={(e) => {
                    const file = e.target.files?.[0];
                    if (!file) return;
                    loadJSON(file);
                    e.target.value = "";
                  }}
                />
                <span className="px-3 py-2 bg-slate-700 rounded hover:bg-slate-600 cursor-pointer flex gap-2 items-center">
                  <FolderOpen size={16} />Load JSON
                </span>
              </label>

              <button onClick={exportPNG} className="px-3 py-2 bg-indigo-600 rounded hover:bg-indigo-500 flex gap-2 items-center">
                <FolderDown size={16} />Export PNG
              </button>

              <button onClick={exportXLSX} className="px-3 py-2 bg-emerald-600 rounded hover:bg-emerald-500 flex gap-2 items-center">
                <FolderDown size={16} />Export XLSX
              </button>

              <button onClick={clearRack} className="px-3 py-2 bg-rose-700 rounded hover:bg-rose-600 flex gap-2 items-center">
                <Trash2 size={16} />Clear Rack
              </button>
            </div>
          </div>

          <div className="p-4 grid grid-cols-1 2xl:grid-cols-[minmax(0,1fr)_380px] gap-4">
            <div>
              <div
                ref={rackRef}
                className={`relative h-[740px] w-full overflow-hidden rounded border border-slate-800 bg-slate-950/70 ${dragSession ? "no-select" : ""}`}
              >
                <div className="absolute left-0 top-0 bottom-0 w-[34px] border-r border-slate-800">
                  {Array.from({ length: rackU }).map((_, idx) => {
                    const u = rackU - idx;
                    const isHover = dragSession?.hoverU === u;
                    return (
                      <div
                        key={u}
                        className={`h-[calc(100%/${rackU})] text-[11px] leading-none grid place-items-center border-b border-slate-900/60 ${isHover ? "bg-cyan-900/40" : ""}`}
                      >
                        {u}
                      </div>
                    );
                  })}
                </div>

                <div className="absolute left-[34px] right-0 top-0 bottom-0">
                  {Array.from({ length: rackU }).map((_, idx) => (
                    <div
                      key={idx}
                      className="h-[calc(100%/var(--u))] border-b border-slate-900/40"
                      style={{ "--u": rackU }}
                    />
                  ))}

                  {dragSession?.previewStartU != null && (
                    <div
                      className={`absolute left-0 right-0 mx-1 rounded border ${dragSession.valid ? "border-cyan-300/70 bg-cyan-400/10" : "border-rose-300/70 bg-rose-500/10"}`}
                      style={{
                        height: `calc(${(dragSession.itemU / rackU) * 100}% - 4px)`,
                        bottom: `calc(${((dragSession.previewStartU - 1) / rackU) * 100}% + 2px)`,
                      }}
                    />
                  )}

                  <svg className="absolute inset-0 pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="none">
                    {enrichedCables.map(cable => (
                      <g key={cable.id}>
                        <path
                          d={cable.path}
                          fill="none"
                          stroke={cable.color || "#38bdf8"}
                          strokeWidth="1.2"
                          strokeLinecap="round"
                          opacity="0.9"
                        />
                        <circle cx={cable.fromPoint.x} cy={cable.fromPoint.y} r="1.1" fill={cable.color || "#38bdf8"} />
                        <circle cx={cable.toPoint.x} cy={cable.toPoint.y} r="1.1" fill={cable.color || "#38bdf8"} />
                      </g>
                    ))}
                  </svg>

                  {placedList.map(record => (
                    <ItemBlock key={record.itemId} record={record} isGhost={false} />
                  ))}

                  {dragSession?.previewStartU != null && (() => {
                    if (dragSession.kind === "template") {
                      const template = templates.find(item => item.id === dragSession.templateId);
                      if (!template) return null;
                      return (
                        <ItemBlock
                          key="template-ghost"
                          record={{
                            item: { ...template, color: dragSession.valid ? template.color : "#ef4444" },
                            startU: dragSession.previewStartU,
                            endU: dragSession.previewStartU + template.u - 1,
                            itemId: `ghost-${template.id}`,
                          }}
                          isGhost={true}
                        />
                      );
                    }

                    const original = placedItems.find(item => item.id === dragSession.itemId);
                    if (!original) return null;
                    return (
                      <ItemBlock
                        key="placed-ghost"
                        record={{
                          item: { ...original, color: dragSession.valid ? original.color : "#ef4444" },
                          startU: dragSession.previewStartU,
                          endU: dragSession.previewStartU + original.u - 1,
                          itemId: `ghost-${original.id}`,
                        }}
                        isGhost={true}
                      />
                    );
                  })()}
                </div>
              </div>

              <div className="mt-3 text-xs text-slate-300 grid grid-cols-1 lg:grid-cols-3 gap-2">
                <div className="rounded border border-slate-800 bg-slate-950/40 px-3 py-2">
                  Dragging now uses a preview-first drop flow, so moving gear does not jitter around the rack while you drag.
                </div>
                <div className="rounded border border-slate-800 bg-slate-950/40 px-3 py-2">
                  Each placed device exposes individual front and rear ports in the manager panel, and port dots on the rack show live connection status.
                </div>
                <div className="rounded border border-slate-800 bg-slate-950/40 px-3 py-2">
                  XLSX export now includes BOM, Placements, Port Map, and Connections so cable endpoints stay tracked.
                </div>
              </div>
            </div>

            <div className="space-y-4">
              <div className="bg-slate-950/40 border border-slate-800 rounded-lg p-4">
                <div className="flex items-center justify-between gap-3">
                  <div>
                    <div className="text-sm font-semibold">Port Manager</div>
                    <div className="text-xs text-slate-400">
                      {selectedPlaced
                        ? `${selectedPlaced.item.name} at U${selectedPlaced.startU}-${selectedPlaced.endU}`
                        : "Select a placed device to browse ports"}
                    </div>
                  </div>

                  <select
                    value={selectedItemId || ""}
                    onChange={(e) => setSelectedItemId(e.target.value || null)}
                    className="bg-slate-900 text-white text-sm border border-slate-800 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-cyan-500"
                  >
                    <option value="">Select device</option>
                    {placedList.map(record => (
                      <option key={record.itemId} value={record.itemId}>
                        {record.item.name} ({record.startU}-{record.endU})
                      </option>
                    ))}
                  </select>
                </div>

                {selectedPlaced ? (
                  <>
                    <div className="grid grid-cols-3 gap-2 mt-3">
                      <div className="rounded border border-slate-800 bg-slate-900/50 p-2">
                        <div className="text-[11px] text-slate-400">Device Size</div>
                        <div className="text-sm font-medium">{selectedPlaced.item.u}U</div>
                      </div>
                      <div className="rounded border border-slate-800 bg-slate-900/50 p-2">
                        <div className="text-[11px] text-slate-400">Ports</div>
                        <div className="text-sm font-medium">{selectedPorts.length}</div>
                      </div>
                      <div className="rounded border border-slate-800 bg-slate-900/50 p-2">
                        <div className="text-[11px] text-slate-400">Connected</div>
                        <div className="text-sm font-medium">{(itemConnectionStats.get(selectedPlaced.itemId)?.connectedPorts) || 0}</div>
                      </div>
                    </div>

                    <div className="mt-4 space-y-4">
                      <div>
                        <div className="text-xs uppercase tracking-wide text-slate-400 mb-2">Front Ports</div>
                        {selectedFrontPorts.length ? (
                          <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
                            {selectedFrontPorts.map(port => <PortCard key={port.id} port={port} />)}
                          </div>
                        ) : (
                          <div className="text-sm text-slate-400 border border-slate-800 rounded p-3 bg-slate-900/30">
                            No front ports defined for this device.
                          </div>
                        )}
                      </div>

                      <div>
                        <div className="text-xs uppercase tracking-wide text-slate-400 mb-2">Rear Ports</div>
                        {selectedRearPorts.length ? (
                          <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
                            {selectedRearPorts.map(port => <PortCard key={port.id} port={port} />)}
                          </div>
                        ) : (
                          <div className="text-sm text-slate-400 border border-slate-800 rounded p-3 bg-slate-900/30">
                            No rear ports defined for this device.
                          </div>
                        )}
                      </div>
                    </div>
                  </>
                ) : (
                  <div className="mt-3 text-sm text-slate-400 border border-slate-800 rounded p-3 bg-slate-900/30">
                    Place a device in the rack, then select it here to manage its ports.
                  </div>
                )}
              </div>

              <div className="bg-slate-950/40 border border-slate-800 rounded-lg p-4">
                <div className="flex items-center justify-between gap-3">
                  <div>
                    <div className="text-sm font-semibold">Cable Builder</div>
                    <div className="text-xs text-slate-400">
                      Start from any open port, then finish the connection by clicking another open port or using the target selector.
                    </div>
                  </div>

                  {portDraft && (
                    <button
                      onClick={clearCableDraft}
                      className="px-2 py-1 text-xs rounded bg-slate-800 border border-slate-700 hover:bg-slate-700"
                    >
                      Cancel Draft
                    </button>
                  )}
                </div>

                <div className="mt-3 rounded border border-slate-800 bg-slate-900/40 p-3">
                  <div className="text-[11px] uppercase tracking-wide text-slate-400">Source Port</div>
                  <div className="mt-1 text-sm">
                    {activePortDraftInfo
                      ? `${activePortDraftInfo.deviceName} / ${activePortDraftInfo.label}`
                      : "No source port selected yet"}
                  </div>
                </div>

                <div className="grid grid-cols-1 gap-2 mt-3">
                  <div>
                    <label className="block text-xs text-slate-300 mb-1">Cable Label</label>
                    <input
                      value={cableLabel}
                      onChange={(e) => setCableLabel(e.target.value)}
                      placeholder="Optional cable label"
                      className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500"
                    />
                  </div>

                  <div className="grid grid-cols-[minmax(0,1fr)_120px] gap-2">
                    <div>
                      <label className="block text-xs text-slate-300 mb-1">Cable Type</label>
                      <input
                        value={cableType}
                        onChange={(e) => setCableType(e.target.value)}
                        placeholder="Patch / Fiber / Power"
                        className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500"
                      />
                    </div>

                    <div>
                      <label className="block text-xs text-slate-300 mb-1">Color</label>
                      <input
                        type="color"
                        value={cableColor}
                        onChange={(e) => setCableColor(e.target.value)}
                        className="w-full h-[42px] rounded bg-slate-950/50 border border-slate-800"
                      />
                    </div>
                  </div>

                  <div>
                    <label className="block text-xs text-slate-300 mb-1">Target Port</label>
                    <div className="flex gap-2">
                      <select
                        value={portTargetId}
                        onChange={(e) => setPortTargetId(e.target.value)}
                        className="flex-1 bg-slate-950/50 text-white text-sm border border-slate-800 rounded px-2 py-2 focus:outline-none focus:ring-2 focus:ring-cyan-500"
                        disabled={!portDraft}
                      >
                        <option value="">Choose target port</option>
                        {openTargetPorts.map(port => (
                          <option key={port.id} value={port.id}>
                            {port.deviceName} / {port.label}
                          </option>
                        ))}
                      </select>
                      <button
                        onClick={connectDraftToSelectedTarget}
                        disabled={!portDraft || !portTargetId}
                        className={
                          "px-3 py-2 rounded text-sm " +
                          ((!portDraft || !portTargetId)
                            ? "bg-slate-800 text-slate-500 cursor-not-allowed"
                            : "bg-cyan-600 hover:bg-cyan-500")
                        }
                      >
                        Connect
                      </button>
                    </div>
                  </div>

                  <div>
                    <label className="block text-xs text-slate-300 mb-1">Notes</label>
                    <textarea
                      value={cableNotes}
                      onChange={(e) => setCableNotes(e.target.value)}
                      placeholder="Optional cable notes"
                      className="w-full px-3 py-2 rounded bg-slate-950/50 border border-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm"
                      rows={2}
                    />
                  </div>
                </div>
              </div>

              <div className="bg-slate-950/40 border border-slate-800 rounded-lg p-4">
                <div className="flex items-center justify-between gap-3">
                  <div>
                    <div className="text-sm font-semibold">Rack Connections</div>
                    <div className="text-xs text-slate-400">{enrichedCables.length} cable(s) on the active rack</div>
                  </div>

                  <button
                    onClick={clearRackCables}
                    disabled={!enrichedCables.length}
                    className={
                      "px-2 py-1 text-xs rounded border " +
                      (!enrichedCables.length
                        ? "bg-slate-800 text-slate-500 border-slate-700 cursor-not-allowed"
                        : "bg-slate-900 border-slate-700 hover:bg-slate-800")
                    }
                  >
                    Clear Cables
                  </button>
                </div>

                {enrichedCables.length ? (
                  <div className="mt-3 space-y-2 max-h-[320px] overflow-auto">
                    {enrichedCables.map(cable => (
                      <div key={cable.id} className="rounded border border-slate-800 bg-slate-900/40 p-3">
                        <div className="flex items-start justify-between gap-3">
                          <div className="min-w-0">
                            <div className="flex items-center gap-2 text-sm font-medium">
                              <span className="inline-block h-2.5 w-2.5 rounded-full" style={{ backgroundColor: cable.color || "#38bdf8" }} />
                              <span className="truncate">{cable.label || cable.id}</span>
                              <span className="text-xs text-slate-400">({cable.type || "Patch"})</span>
                            </div>
                            <div className="mt-1 text-xs text-slate-300">
                              {cable.fromPort.deviceName} / {cable.fromPort.label}
                            </div>
                            <div className="text-xs text-slate-300">
                              {cable.toPort.deviceName} / {cable.toPort.label}
                            </div>
                            {!!cable.notes && (
                              <div className="mt-1 text-xs text-slate-400">{cable.notes}</div>
                            )}
                          </div>

                          <button
                            onClick={() => removeCable(cable.id)}
                            className="px-2 py-1 text-xs rounded bg-slate-900 border border-slate-700 hover:bg-slate-800"
                          >
                            Remove
                          </button>
                        </div>
                      </div>
                    ))}
                  </div>
                ) : (
                  <div className="mt-3 text-sm text-slate-400 border border-slate-800 rounded p-3 bg-slate-900/30">
                    No cables yet. Start from a port in the port manager to build a tracked connection.
                  </div>
                )}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);

