const GRID = 20;
const SNAPSHOTS_KEY = "schemeta:snapshots:v2";
const SCHEMA_URL = "/schemeta.schema.json";
const INSPECTOR_SECTIONS_KEY = "schemeta:inspector-sections:v1";
const NET_CLASSES = ["power", "ground", "signal", "analog", "differential", "clock", "bus"];
const PIN_SIDES = ["left", "right", "top", "bottom"];
const PIN_TYPES = ["power_in", "power_out", "input", "output", "bidirectional", "passive", "analog", "ground"];
const LIST_ROW_HEIGHT = 36;
const LIST_OVERSCAN_ROWS = 8;
const MIN_SCALE = 0.2;
const MAX_SCALE = 5;
const FIT_MARGIN = 56;
const FOCUS_MARGIN = 96;
const THEME_KEY = "schemeta:theme:v1";

const state = {
  model: null,
  compile: null,
  selectedRef: null,
  selectedRefs: [],
  selectedNet: null,
  selectedPin: null,
  scale: 1,
  panX: 40,
  panY: 40,
  isPanning: false,
  dragComponent: null,
  draggingComponentRef: null,
  dragPointerId: null,
  dragPreviewNode: null,
  dragMoved: false,
  showLabels: true,
  isolateNet: false,
  isolateComponent: false,
  renderMode: "explicit",
  theme: "dark",
  userAdjustedView: false,
  spacePan: false,
  schemaText: "",
  boxSelecting: false,
  boxStartX: 0,
  boxStartY: 0,
  boxMoved: false,
  suppressCanvasClick: false,
  compileDebounceId: null,
  historyPast: [],
  historyFuture: [],
  historyLimit: 80,
  historyRestoring: false,
  symbolMigrationAckHash: null,
  commandIndex: 0
};

const el = {
  instanceList: document.getElementById("instanceList"),
  netList: document.getElementById("netList"),
  instanceFilter: document.getElementById("instanceFilter"),
  netFilter: document.getElementById("netFilter"),
  newComponentRefInput: document.getElementById("newComponentRefInput"),
  newComponentTypeSelect: document.getElementById("newComponentTypeSelect"),
  addComponentBtn: document.getElementById("addComponentBtn"),
  newQuickNetNameInput: document.getElementById("newQuickNetNameInput"),
  newQuickNetClassSelect: document.getElementById("newQuickNetClassSelect"),
  addQuickNetBtn: document.getElementById("addQuickNetBtn"),
  canvasViewport: document.getElementById("canvasViewport"),
  canvasInner: document.getElementById("canvasInner"),
  selectionBox: document.getElementById("selectionBox"),
  compileStatus: document.getElementById("compileStatus"),
  selectedSummary: document.getElementById("selectedSummary"),
  componentEditor: document.getElementById("componentEditor"),
  componentSection: document.getElementById("componentSection"),
  symbolEditor: document.getElementById("symbolEditor"),
  symbolSection: document.getElementById("symbolSection"),
  pinEditor: document.getElementById("pinEditor"),
  pinSection: document.getElementById("pinSection"),
  netEditor: document.getElementById("netEditor"),
  netSection: document.getElementById("netSection"),
  instRefInput: document.getElementById("instRefInput"),
  instValueInput: document.getElementById("instValueInput"),
  instNotesInput: document.getElementById("instNotesInput"),
  xInput: document.getElementById("xInput"),
  yInput: document.getElementById("yInput"),
  rotationInput: document.getElementById("rotationInput"),
  lockedInput: document.getElementById("lockedInput"),
  rotateSelectedBtn: document.getElementById("rotateSelectedBtn"),
  updatePlacementBtn: document.getElementById("updatePlacementBtn"),
  duplicateComponentBtn: document.getElementById("duplicateComponentBtn"),
  deleteComponentBtn: document.getElementById("deleteComponentBtn"),
  isolateSelectedComponentBtn: document.getElementById("isolateSelectedComponentBtn"),
  symbolMeta: document.getElementById("symbolMeta"),
  symbolCategoryInput: document.getElementById("symbolCategoryInput"),
  symbolWidthInput: document.getElementById("symbolWidthInput"),
  symbolHeightInput: document.getElementById("symbolHeightInput"),
  addSymbolPinBtn: document.getElementById("addSymbolPinBtn"),
  previewSymbolBtn: document.getElementById("previewSymbolBtn"),
  applySymbolBtn: document.getElementById("applySymbolBtn"),
  symbolValidation: document.getElementById("symbolValidation"),
  symbolMigrationPreview: document.getElementById("symbolMigrationPreview"),
  symbolPinsList: document.getElementById("symbolPinsList"),
  pinMeta: document.getElementById("pinMeta"),
  pinNameInput: document.getElementById("pinNameInput"),
  pinNumberInput: document.getElementById("pinNumberInput"),
  pinSideInput: document.getElementById("pinSideInput"),
  pinTypeInput: document.getElementById("pinTypeInput"),
  pinOffsetInput: document.getElementById("pinOffsetInput"),
  applyPinPropsBtn: document.getElementById("applyPinPropsBtn"),
  showPinNetLabelInput: document.getElementById("showPinNetLabelInput"),
  pinNetSelect: document.getElementById("pinNetSelect"),
  connectPinBtn: document.getElementById("connectPinBtn"),
  newNetNameInput: document.getElementById("newNetNameInput"),
  newNetClassInput: document.getElementById("newNetClassInput"),
  createConnectNetBtn: document.getElementById("createConnectNetBtn"),
  pinConnections: document.getElementById("pinConnections"),
  netNameInput: document.getElementById("netNameInput"),
  netClassInput: document.getElementById("netClassInput"),
  updateNetBtn: document.getElementById("updateNetBtn"),
  isolateSelectedNetBtn: document.getElementById("isolateSelectedNetBtn"),
  netNodeRefInput: document.getElementById("netNodeRefInput"),
  netNodePinInput: document.getElementById("netNodePinInput"),
  addNetNodeBtn: document.getElementById("addNetNodeBtn"),
  netNodesList: document.getElementById("netNodesList"),
  issues: document.getElementById("issues"),
  topology: document.getElementById("topology"),
  jsonEditor: document.getElementById("jsonEditor"),
  jsonFeedback: document.getElementById("jsonFeedback"),
  loadSampleBtn: document.getElementById("loadSampleBtn"),
  resetSampleBtn: document.getElementById("resetSampleBtn"),
  newProjectBtn: document.getElementById("newProjectBtn"),
  importBtn: document.getElementById("importBtn"),
  exportBtn: document.getElementById("exportBtn"),
  fileInput: document.getElementById("fileInput"),
  zoomInBtn: document.getElementById("zoomInBtn"),
  zoomOutBtn: document.getElementById("zoomOutBtn"),
  zoomResetBtn: document.getElementById("zoomResetBtn"),
  fitViewBtn: document.getElementById("fitViewBtn"),
  focusSelectionBtn: document.getElementById("focusSelectionBtn"),
  showLabelsInput: document.getElementById("showLabelsInput"),
  applyJsonBtn: document.getElementById("applyJsonBtn"),
  showSchemaBtn: document.getElementById("showSchemaBtn"),
  validateJsonBtn: document.getElementById("validateJsonBtn"),
  formatJsonBtn: document.getElementById("formatJsonBtn"),
  sortJsonBtn: document.getElementById("sortJsonBtn"),
  copyReproBtn: document.getElementById("copyReproBtn"),
  autoLayoutBtn: document.getElementById("autoLayoutBtn"),
  autoTidyBtn: document.getElementById("autoTidyBtn"),
  unlockAllBtn: document.getElementById("unlockAllBtn"),
  themeToggleBtn: document.getElementById("themeToggleBtn"),
  shortcutsBtn: document.getElementById("shortcutsBtn"),
  undoBtn: document.getElementById("undoBtn"),
  redoBtn: document.getElementById("redoBtn"),
  renderModeSelect: document.getElementById("renderModeSelect"),
  isolateNetBtn: document.getElementById("isolateNetBtn"),
  isolateComponentBtn: document.getElementById("isolateComponentBtn"),
  pinTooltip: document.getElementById("pinTooltip"),
  schemaModal: document.getElementById("schemaModal"),
  schemaViewer: document.getElementById("schemaViewer"),
  closeSchemaBtn: document.getElementById("closeSchemaBtn"),
  copySchemaBtn: document.getElementById("copySchemaBtn"),
  downloadSchemaBtn: document.getElementById("downloadSchemaBtn"),
  shortcutsModal: document.getElementById("shortcutsModal"),
  closeShortcutsBtn: document.getElementById("closeShortcutsBtn"),
  commandModal: document.getElementById("commandModal"),
  closeCommandBtn: document.getElementById("closeCommandBtn"),
  commandInput: document.getElementById("commandInput"),
  commandList: document.getElementById("commandList"),
  selectedNoteHint: null
};

function toGrid(v) {
  return Math.round(v / GRID) * GRID;
}

function clone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

function captureHistorySnapshot() {
  if (!state.model) {
    return null;
  }
  return {
    model: clone(state.model),
    selectedRefs: [...state.selectedRefs],
    selectedNet: state.selectedNet,
    selectedPin: state.selectedPin ? { ...state.selectedPin, nets: [...(state.selectedPin.nets ?? [])] } : null,
    isolateNet: Boolean(state.isolateNet),
    isolateComponent: Boolean(state.isolateComponent)
  };
}

function restoreHistorySnapshot(snapshot) {
  if (!snapshot) {
    return;
  }
  state.model = clone(snapshot.model);
  setSelectedRefs(snapshot.selectedRefs ?? []);
  state.selectedNet = snapshot.selectedNet ?? null;
  state.selectedPin = snapshot.selectedPin ? { ...snapshot.selectedPin } : null;
  state.isolateNet = Boolean(snapshot.isolateNet);
  state.isolateComponent = Boolean(snapshot.isolateComponent);
}

function pushHistory(reason = "edit") {
  const snap = captureHistorySnapshot();
  if (!snap) {
    return;
  }
  state.historyPast.push({ reason, snapshot: snap });
  if (state.historyPast.length > state.historyLimit) {
    state.historyPast.splice(0, state.historyPast.length - state.historyLimit);
  }
  state.historyFuture = [];
}

async function performUndo() {
  if (!state.historyPast.length || !state.model) {
    return;
  }
  const current = captureHistorySnapshot();
  const prev = state.historyPast.pop();
  if (!prev?.snapshot || !current) {
    return;
  }
  state.historyFuture.push({ reason: prev.reason, snapshot: current });
  state.historyRestoring = true;
  restoreHistorySnapshot(prev.snapshot);
  await compileModel(state.model, { keepView: true, source: "undo" });
  state.historyRestoring = false;
}

async function performRedo() {
  if (!state.historyFuture.length || !state.model) {
    return;
  }
  const current = captureHistorySnapshot();
  const next = state.historyFuture.pop();
  if (!next?.snapshot || !current) {
    return;
  }
  state.historyPast.push({ reason: next.reason, snapshot: current });
  state.historyRestoring = true;
  restoreHistorySnapshot(next.snapshot);
  await compileModel(state.model, { keepView: true, source: "redo" });
  state.historyRestoring = false;
}

function hasSelectionModifier(evt) {
  return Boolean(evt?.ctrlKey || evt?.metaKey || evt?.shiftKey);
}

function setSelectedRefs(refs) {
  const uniq = [...new Set((refs ?? []).filter(Boolean))];
  state.selectedRefs = uniq;
  state.selectedRef = uniq.length === 1 ? uniq[0] : null;
}

function selectSingleRef(ref) {
  setSelectedRefs(ref ? [ref] : []);
}

function toggleSelectedRef(ref) {
  const set = new Set(state.selectedRefs);
  if (set.has(ref)) {
    set.delete(ref);
  } else {
    set.add(ref);
  }
  setSelectedRefs([...set]);
}

function selectedRefSet() {
  return new Set(state.selectedRefs);
}

function instanceByRef(ref) {
  return state.model?.instances.find((i) => i.ref === ref) ?? null;
}

function symbolForRef(ref) {
  const inst = instanceByRef(ref);
  if (!inst) {
    return null;
  }
  return state.model?.symbols?.[inst.symbol] ?? null;
}

function pinExists(ref, pinName) {
  const sym = symbolForRef(ref);
  return Boolean(sym?.pins?.some((p) => p.name === pinName));
}

function normalizeRef(raw) {
  return String(raw ?? "")
    .trim()
    .replace(/\s+/g, "_");
}

function normalizeNetName(raw) {
  return String(raw ?? "")
    .trim()
    .replace(/\s+/g, "_");
}

function nextRefLike(baseRef) {
  const base = normalizeRef(baseRef || "X1");
  const m = /^([A-Za-z_]+)(\d+)?$/i.exec(base);
  const prefix = m?.[1] ?? `${base}_`;
  let n = Number(m?.[2] ?? 1);
  let candidate = `${prefix}${n}`;
  while (instanceByRef(candidate)) {
    n += 1;
    candidate = `${prefix}${n}`;
  }
  return candidate;
}

function defaultRefSeedForPart(partName) {
  const part = String(partName ?? "").toLowerCase();
  if (part === "resistor") return "R1";
  if (part === "capacitor") return "C1";
  if (part === "inductor") return "L1";
  if (part === "diode" || part === "led") return "D1";
  if (part === "connector") return "J1";
  if (part === "generic") return "X1";
  return "U1";
}

function escHtml(text) {
  return String(text ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

async function apiPost(path, payload) {
  const res = await fetch(path, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(payload)
  });

  const data = await res.json();
  if (!res.ok) {
    throw new Error(data?.error?.message || "Request failed");
  }

  return data;
}

function setStatus(text, ok = true) {
  el.compileStatus.textContent = text;
  el.compileStatus.className = ok ? "status-ok" : "";
}

function selectThemePreference() {
  const saved = localStorage.getItem(THEME_KEY);
  if (saved === "dark" || saved === "light") {
    return saved;
  }
  return "dark";
}

function applyTheme(theme, persist = true) {
  const next = theme === "light" ? "light" : "dark";
  state.theme = next;
  document.body.classList.toggle("theme-dark", next === "dark");
  if (el.themeToggleBtn) {
    el.themeToggleBtn.textContent = next === "dark" ? "Light" : "Dark";
    el.themeToggleBtn.setAttribute("aria-label", next === "dark" ? "Switch to light mode" : "Switch to dark mode");
  }
  if (persist) {
    localStorage.setItem(THEME_KEY, next);
  }
}

function formatCompileStatus(result) {
  const m = result?.layout_metrics ?? {};
  return `Compiled (${result.errors.length}E, ${result.warnings.length}W | ${m.crossings ?? 0} crossings, ${m.overlap_edges ?? 0} overlaps, ${m.total_bends ?? 0} bends, ${m.label_tie_routes ?? 0} tie-nets, ${(m.detour_ratio ?? 1).toFixed(2)}x detour)`;
}

function defaultProject() {
  return {
    meta: { title: "Untitled Schemeta Project" },
    symbols: {},
    instances: [],
    nets: [],
    constraints: {},
    annotations: []
  };
}

function compileOptions(extra = {}) {
  return {
    render_mode: state.renderMode,
    show_labels: state.showLabels,
    show_annotations: false,
    show_legend: false,
    generic_symbols: true,
    ...extra
  };
}

function applyCompileLayoutToModel(model, compileResult) {
  const next = clone(model);
  const placed = compileResult?.layout?.placed;
  if (!Array.isArray(placed)) {
    return next;
  }

  const byRef = new Map(placed.map((p) => [p.ref, p]));
  for (const inst of next.instances) {
    const p = byRef.get(inst.ref);
    if (!p) {
      continue;
    }
    inst.placement.x = p.x;
    inst.placement.y = p.y;
    inst.placement.rotation = p.rotation ?? inst.placement.rotation ?? 0;
    inst.placement.locked = p.locked ?? inst.placement.locked ?? false;
  }

  return next;
}

function reconcileSelectionWithModel() {
  if (!state.model) {
    setSelectedRefs([]);
    state.selectedNet = null;
    state.selectedPin = null;
    return;
  }

  const refs = new Set(state.model.instances.map((i) => i.ref));
  setSelectedRefs(state.selectedRefs.filter((r) => refs.has(r)));

  if (state.selectedPin) {
    if (!refs.has(state.selectedPin.ref) || !pinExists(state.selectedPin.ref, state.selectedPin.pin)) {
      state.selectedPin = null;
    }
  }

  if (state.selectedNet) {
    const exists = state.model.nets.some((n) => n.name === state.selectedNet);
    if (!exists) {
      state.selectedNet = null;
    }
  }
}

function refreshJsonEditor() {
  if (!state.model) {
    return;
  }
  el.jsonEditor.value = JSON.stringify(state.model, null, 2);
}

function selectedComponentNote() {
  if (!state.model || state.selectedNet || state.selectedPin || state.selectedRefs.length !== 1) {
    return null;
  }
  const inst = instanceByRef(state.selectedRefs[0]);
  if (!inst) {
    return null;
  }
  const raw = String(inst.properties?.note ?? inst.properties?.notes ?? "").trim();
  if (!raw) {
    return null;
  }
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
}

function updateSelectedNoteHint() {
  if (!el.selectedNoteHint) {
    return;
  }
  const note = selectedComponentNote();
  if (!note || !state.compile?.layout || !state.model) {
    el.selectedNoteHint.classList.add("hidden");
    return;
  }
  const ref = state.selectedRefs[0];
  const inst = instanceByRef(ref);
  const sym = inst ? state.model.symbols?.[inst.symbol] : null;
  const placed = (state.compile.layout.placed ?? []).find((p) => p.ref === ref);
  if (!inst || !sym || !placed) {
    el.selectedNoteHint.classList.add("hidden");
    return;
  }

  el.selectedNoteHint.textContent = note;
  el.selectedNoteHint.classList.remove("hidden");
  el.selectedNoteHint.style.visibility = "hidden";

  const viewportRect = el.canvasViewport.getBoundingClientRect();
  const x = (placed.x + sym.body.width / 2) * state.scale + state.panX;
  const y = (placed.y - 12) * state.scale + state.panY;
  el.selectedNoteHint.style.left = `${Math.round(x)}px`;
  el.selectedNoteHint.style.top = `${Math.round(y)}px`;
  el.selectedNoteHint.style.transform = "translate(-50%, -100%)";

  const hintRect = el.selectedNoteHint.getBoundingClientRect();
  const minX = 14 + hintRect.width / 2;
  const maxX = Math.max(minX, viewportRect.width - 14 - hintRect.width / 2);
  const clampedX = Math.max(minX, Math.min(maxX, x));
  el.selectedNoteHint.style.left = `${Math.round(clampedX)}px`;
  el.selectedNoteHint.style.visibility = "";
}

function saveSnapshot() {
  if (!state.model) {
    return;
  }

  const snap = {
    id: `${Date.now()}`,
    ts: new Date().toISOString(),
    model: state.model
  };

  const existing = JSON.parse(localStorage.getItem(SNAPSHOTS_KEY) ?? "[]");
  const next = [snap, ...existing].slice(0, 20);
  localStorage.setItem(SNAPSHOTS_KEY, JSON.stringify(next));
}

function updateTransform() {
  el.canvasInner.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`;
  el.zoomResetBtn.textContent = `${Math.round(state.scale * 100)}%`;
  const svg = el.canvasInner.querySelector("svg");
  if (svg) {
    applyLabelDensityByZoom(svg);
    resolveLabelCollisions(svg);
  }
  updateSelectedNoteHint();
}

function layoutBounds(layout, margin = FIT_MARGIN) {
  const w = layout?.width ?? 0;
  const h = layout?.height ?? 0;
  if (!w || !h) {
    return null;
  }

  let minX = Number.POSITIVE_INFINITY;
  let minY = Number.POSITIVE_INFINITY;
  let maxX = Number.NEGATIVE_INFINITY;
  let maxY = Number.NEGATIVE_INFINITY;

  const byRef = new Map((layout?.placed ?? []).map((p) => [p.ref, p]));
  if (state.model?.instances?.length) {
    for (const inst of state.model.instances) {
      const p = byRef.get(inst.ref);
      const sym = state.model.symbols?.[inst.symbol];
      if (!p || !sym) {
        continue;
      }
      minX = Math.min(minX, p.x);
      minY = Math.min(minY, p.y);
      maxX = Math.max(maxX, p.x + sym.body.width);
      maxY = Math.max(maxY, p.y + sym.body.height);
    }
  }

  if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
    minX = 0;
    minY = 0;
    maxX = w;
    maxY = h;
  }

  return {
    x: Math.max(0, minX - margin),
    y: Math.max(0, minY - margin),
    w: Math.max(1, Math.min(w, maxX - minX + margin * 2)),
    h: Math.max(1, Math.min(h, maxY - minY + margin * 2))
  };
}

function centerOnBBox(bbox, fillRatio = 0.93, stickyAdjusted = false) {
  if (!bbox) {
    return false;
  }
  const viewport = el.canvasViewport.getBoundingClientRect();
  if (!viewport.width || !viewport.height) {
    return false;
  }
  const sx = (viewport.width * fillRatio) / Math.max(1, bbox.w);
  const sy = (viewport.height * fillRatio) / Math.max(1, bbox.h);
  state.scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, Math.min(sx, sy)));
  state.panX = viewport.width / 2 - (bbox.x + bbox.w / 2) * state.scale;
  state.panY = viewport.height / 2 - (bbox.y + bbox.h / 2) * state.scale;
  state.userAdjustedView = stickyAdjusted;
  updateTransform();
  return true;
}

function fitView(layout) {
  const bbox = layoutBounds(layout, FIT_MARGIN);
  if (!bbox) {
    return;
  }
  centerOnBBox(bbox, 0.93, false);
}

function refsBBox(refs, margin = FOCUS_MARGIN) {
  if (!refs?.size || !state.compile?.layout || !state.model) {
    return null;
  }
  const placed = new Map((state.compile.layout.placed ?? []).map((p) => [p.ref, p]));
  let minX = Number.POSITIVE_INFINITY;
  let minY = Number.POSITIVE_INFINITY;
  let maxX = Number.NEGATIVE_INFINITY;
  let maxY = Number.NEGATIVE_INFINITY;
  for (const ref of refs) {
    const inst = instanceByRef(ref);
    const p = placed.get(ref);
    const sym = inst ? state.model.symbols?.[inst.symbol] : null;
    if (!p || !sym?.body) {
      continue;
    }
    minX = Math.min(minX, p.x);
    minY = Math.min(minY, p.y);
    maxX = Math.max(maxX, p.x + sym.body.width);
    maxY = Math.max(maxY, p.y + sym.body.height);
  }
  if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
    return null;
  }
  const w = state.compile.layout.width ?? maxX;
  const h = state.compile.layout.height ?? maxY;
  return {
    x: Math.max(0, minX - margin),
    y: Math.max(0, minY - margin),
    w: Math.max(1, Math.min(w, maxX - minX + margin * 2)),
    h: Math.max(1, Math.min(h, maxY - minY + margin * 2))
  };
}

function selectedFocusBBox() {
  if (!state.model || !state.compile?.layout) {
    return null;
  }
  if (state.selectedPin) {
    return refsBBox(new Set([state.selectedPin.ref]), FOCUS_MARGIN);
  }
  if (state.selectedRefs.length) {
    return refsBBox(new Set(state.selectedRefs), FOCUS_MARGIN);
  }
  if (state.selectedNet) {
    const refs = refsConnectedToNet(state.selectedNet);
    return refsBBox(refs, FOCUS_MARGIN);
  }
  return layoutBounds(state.compile.layout, FIT_MARGIN);
}

function focusSelection() {
  const bbox = selectedFocusBBox();
  if (!bbox) {
    return false;
  }
  return centerOnBBox(bbox, 0.88, true);
}

function zoomToBBox(bbox) {
  if (!bbox) {
    return;
  }
  centerOnBBox(bbox, 0.8, true);
}

function canvasToSvgPoint(clientX, clientY) {
  const rect = el.canvasViewport.getBoundingClientRect();
  return {
    x: (clientX - rect.left - state.panX) / state.scale,
    y: (clientY - rect.top - state.panY) / state.scale
  };
}

function viewportPoint(clientX, clientY) {
  const rect = el.canvasViewport.getBoundingClientRect();
  return {
    x: clientX - rect.left,
    y: clientY - rect.top
  };
}

function beginBoxSelection(clientX, clientY) {
  const p = viewportPoint(clientX, clientY);
  state.boxSelecting = true;
  state.boxMoved = false;
  state.boxStartX = p.x;
  state.boxStartY = p.y;
  el.selectionBox.classList.remove("hidden");
  el.selectionBox.style.left = `${p.x}px`;
  el.selectionBox.style.top = `${p.y}px`;
  el.selectionBox.style.width = "0px";
  el.selectionBox.style.height = "0px";
}

function updateBoxSelection(clientX, clientY) {
  if (!state.boxSelecting) {
    return;
  }
  const p = viewportPoint(clientX, clientY);
  const x = Math.min(state.boxStartX, p.x);
  const y = Math.min(state.boxStartY, p.y);
  const w = Math.abs(p.x - state.boxStartX);
  const h = Math.abs(p.y - state.boxStartY);
  if (w > 4 || h > 4) {
    state.boxMoved = true;
  }
  el.selectionBox.style.left = `${x}px`;
  el.selectionBox.style.top = `${y}px`;
  el.selectionBox.style.width = `${w}px`;
  el.selectionBox.style.height = `${h}px`;
}

function finishBoxSelection() {
  if (!state.boxSelecting) {
    return;
  }

  const box = el.selectionBox.getBoundingClientRect();
  el.selectionBox.classList.add("hidden");
  const moved = state.boxMoved;
  state.boxSelecting = false;
  state.boxMoved = false;
  if (!moved) {
    return;
  }

  state.suppressCanvasClick = true;
  const svg = el.canvasInner.querySelector("svg");
  if (!svg) {
    return;
  }

  const hits = [];
  svg.querySelectorAll("[data-ref]").forEach((node) => {
    const r = node.getBoundingClientRect();
    const intersects = r.left < box.right && r.right > box.left && r.top < box.bottom && r.bottom > box.top;
    if (intersects) {
      const ref = node.getAttribute("data-ref");
      if (ref) {
        hits.push(ref);
      }
    }
  });

  if (hits.length) {
    setSelectedRefs(hits);
    state.selectedNet = null;
    state.selectedPin = null;
    renderAll();
  } else {
    setSelectedRefs([]);
    state.selectedNet = null;
    state.selectedPin = null;
    state.isolateNet = false;
    state.isolateComponent = false;
    renderAll();
  }
}

function incidentNetsForRef(ref) {
  if (!state.model) {
    return new Set();
  }

  return new Set(
    state.model.nets
      .filter((net) => net.nodes.some((n) => n.ref === ref))
      .map((net) => net.name)
  );
}

function refsConnectedToNet(netName) {
  const net = state.model?.nets.find((n) => n.name === netName);
  return new Set((net?.nodes ?? []).map((n) => n.ref));
}

function activeNetSet() {
  if (state.selectedNet) {
    return new Set([state.selectedNet]);
  }

  if (state.selectedRefs.length) {
    const nets = new Set();
    for (const ref of state.selectedRefs) {
      for (const net of incidentNetsForRef(ref)) {
        nets.add(net);
      }
    }
    return nets;
  }

  return null;
}

function setLabelLayerVisibility() {
  const svg = el.canvasInner.querySelector("svg");
  if (!svg) {
    return;
  }

  const layer = svg.querySelector('[data-layer="net-labels"]');
  if (layer) {
    layer.style.display = state.showLabels ? "" : "none";
  }
  applyLabelDensityByZoom(svg);
  resolveLabelCollisions(svg);
}

function boxesOverlap(a, b, pad = 2) {
  return !(a.right + pad < b.left || a.left - pad > b.right || a.bottom + pad < b.top || a.top - pad > b.bottom);
}

function labelPriority(node) {
  if (node.hasAttribute("data-net-label")) return 4;
  if (node.hasAttribute("data-ref-label")) return 3;
  if (node.hasAttribute("data-pin-label")) return 2;
  if (node.hasAttribute("data-value-label")) return 1;
  return 0;
}

function applyLabelDensityByZoom(svg) {
  const pinLabels = svg.querySelectorAll("[data-pin-label]");
  const valueLabels = svg.querySelectorAll("[data-value-label]");
  const refLabels = svg.querySelectorAll("[data-ref-label]");
  const dense = state.scale < 1.05;
  const veryDense = state.scale < 0.8;

  pinLabels.forEach((n) => {
    n.style.display = dense ? "none" : "";
  });
  valueLabels.forEach((n) => {
    n.style.display = veryDense ? "none" : "";
  });
  refLabels.forEach((n) => {
    n.style.display = veryDense ? "none" : "";
  });
}

function resolveLabelCollisions(svg) {
  if (!state.showLabels) {
    return;
  }

  const labels = [
    ...svg.querySelectorAll("[data-net-label], [data-pin-label], [data-ref-label], [data-value-label]")
  ].filter((n) => n.style.display !== "none");

  for (const n of labels) {
    n.style.visibility = "";
  }

  const netSeen = [];
  for (const node of labels) {
    const net = node.getAttribute("data-net-label");
    if (!net) {
      continue;
    }
    const box = node.getBoundingClientRect();
    let dup = false;
    for (const prev of netSeen) {
      if (prev.net !== net) {
        continue;
      }
      const dx = Math.abs((box.left + box.right) / 2 - prev.cx);
      const dy = Math.abs((box.top + box.bottom) / 2 - prev.cy);
      if (dx < 30 && dy < 18) {
        dup = true;
        break;
      }
    }
    if (dup) {
      node.style.visibility = "hidden";
      continue;
    }
    netSeen.push({
      net,
      cx: (box.left + box.right) / 2,
      cy: (box.top + box.bottom) / 2
    });
  }

  const active = labels.filter((n) => n.style.display !== "none" && n.style.visibility !== "hidden");
  const entries = active.map((node) => ({
    node,
    priority: labelPriority(node),
    box: node.getBoundingClientRect()
  }));

  for (let i = 0; i < entries.length; i += 1) {
    const a = entries[i];
    if (a.node.style.visibility === "hidden") {
      continue;
    }
    for (let j = i + 1; j < entries.length; j += 1) {
      const b = entries[j];
      if (b.node.style.visibility === "hidden") {
        continue;
      }
      if (!boxesOverlap(a.box, b.box, 1)) {
        continue;
      }

      if (a.priority === b.priority) {
        b.node.style.visibility = "hidden";
      } else if (a.priority > b.priority) {
        b.node.style.visibility = "hidden";
      } else {
        a.node.style.visibility = "hidden";
        break;
      }
    }
  }
}

function renderVirtualList(listEl, allItems, renderRow, rowHeight = LIST_ROW_HEIGHT) {
  const items = Array.isArray(allItems) ? allItems : [];
  if (!items.length) {
    listEl.innerHTML = "";
    return;
  }

  const viewportHeight = Math.max(rowHeight * 4, listEl.clientHeight || 230);
  const scrollTop = listEl.scrollTop;
  const total = items.length;
  const visibleRows = Math.ceil(viewportHeight / rowHeight);
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - LIST_OVERSCAN_ROWS);
  const end = Math.min(total, start + visibleRows + LIST_OVERSCAN_ROWS * 2);
  const topPad = start * rowHeight;
  const bottomPad = (total - end) * rowHeight;

  const rows = items.slice(start, end).map(renderRow).join("");
  listEl.innerHTML = `<li class="listSpacer" style="height:${topPad}px"></li>${rows}<li class="listSpacer" style="height:${bottomPad}px"></li>`;
}

function renderInstances() {
  if (!state.model) {
    el.instanceList.innerHTML = "";
    return;
  }

  const q = el.instanceFilter.value.trim().toLowerCase();
  const items = state.model.instances.filter((i) => i.ref.toLowerCase().includes(q));

  renderVirtualList(
    el.instanceList,
    items,
    (inst) => {
      const cls = state.selectedRefs.includes(inst.ref) ? "active" : "";
      return `<li class="${cls}" data-ref-item="${inst.ref}" tabindex="0" role="button" aria-label="Instance ${inst.ref}, symbol ${inst.symbol}">${inst.ref} · ${inst.symbol}</li>`;
    },
    LIST_ROW_HEIGHT
  );
}

function renderNets() {
  if (!state.model) {
    el.netList.innerHTML = "";
    return;
  }

  const q = el.netFilter.value.trim().toLowerCase();
  const items = state.model.nets.filter((n) => n.name.toLowerCase().includes(q));

  renderVirtualList(
    el.netList,
    items,
    (net) => {
      const cls = net.name === state.selectedNet ? "active" : "";
      return `<li class="${cls}" data-net-item="${net.name}" tabindex="0" role="button" aria-label="Net ${net.name}, class ${net.class}">${net.name} <small>(${net.class})</small></li>`;
    },
    LIST_ROW_HEIGHT
  );
}

function netByName(name) {
  return state.model?.nets.find((n) => n.name === name) ?? null;
}

function pinUi(inst, pinName) {
  const ui = inst?.properties?.pin_ui;
  if (!ui || typeof ui !== "object") {
    return {};
  }
  const pinObj = ui[pinName];
  if (!pinObj || typeof pinObj !== "object") {
    return {};
  }
  return pinObj;
}

function inferClassForPin(ref, pinName) {
  const sym = symbolForRef(ref);
  const pin = sym?.pins?.find((p) => p.name === pinName);
  const pinType = String(pin?.type ?? "").toLowerCase();
  if (pinType === "ground") {
    return "ground";
  }
  if (pinType === "power_in" || pinType === "power_out") {
    return "power";
  }
  if (pinType === "analog") {
    return "analog";
  }
  return "signal";
}

function nextAutoNetName() {
  const names = new Set((state.model?.nets ?? []).map((n) => n.name));
  let n = 1;
  while (names.has(`NET_${n}`)) {
    n += 1;
  }
  return `NET_${n}`;
}

function renamePinAcrossSymbolInstances(symbolId, oldPinName, newPinName) {
  if (!state.model || !oldPinName || !newPinName || oldPinName === newPinName) {
    return;
  }
  const instances = (state.model.instances ?? []).filter((i) => i.symbol === symbolId);
  const refs = new Set(instances.map((i) => i.ref));
  for (const net of state.model.nets ?? []) {
    for (const node of net.nodes ?? []) {
      if (refs.has(node.ref) && node.pin === oldPinName) {
        node.pin = newPinName;
      }
    }
  }

  for (const inst of instances) {
    const pinUi = inst.properties?.pin_ui;
    if (!pinUi || typeof pinUi !== "object" || Array.isArray(pinUi)) {
      continue;
    }
    if (Object.prototype.hasOwnProperty.call(pinUi, oldPinName) && !Object.prototype.hasOwnProperty.call(pinUi, newPinName)) {
      pinUi[newPinName] = pinUi[oldPinName];
      delete pinUi[oldPinName];
    } else if (Object.prototype.hasOwnProperty.call(pinUi, oldPinName) && Object.prototype.hasOwnProperty.call(pinUi, newPinName)) {
      delete pinUi[oldPinName];
    }
  }

  if (state.selectedPin && refs.has(state.selectedPin.ref) && state.selectedPin.pin === oldPinName) {
    state.selectedPin.pin = newPinName;
  }
}

function symbolPinRowHtml(pin) {
  const sideOptions = PIN_SIDES.map((s) => `<option value="${s}" ${pin.side === s ? "selected" : ""}>${s}</option>`).join("");
  const typeOptions = PIN_TYPES.map((t) => `<option value="${t}" ${pin.type === t ? "selected" : ""}>${t}</option>`).join("");
  return `<div class="miniRow symbolPinRow" data-old-pin="${escHtml(pin.name)}">
    <div class="symbolPinMain">
      <input class="pinCol pinName" type="text" value="${escHtml(pin.name)}" placeholder="name" />
      <input class="pinCol pinNumber" type="text" value="${escHtml(pin.number)}" placeholder="num" />
      <select class="pinCol pinSide">${sideOptions}</select>
      <input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
      <select class="pinCol pinType">${typeOptions}</select>
    </div>
    <div class="symbolPinActions">
      <button type="button" data-move-symbol-pin="up" title="Move pin up">Up</button>
      <button type="button" data-move-symbol-pin="down" title="Move pin down">Down</button>
      <button type="button" data-remove-symbol-pin="${escHtml(pin.name)}">Remove</button>
    </div>
  </div>`;
}

function invalidateSymbolMigrationPreview(message = "") {
  state.symbolMigrationAckHash = null;
  if (el.symbolMigrationPreview) {
    el.symbolMigrationPreview.textContent = message;
    el.symbolMigrationPreview.classList.remove("migrationPreview");
  }
}

function collectSymbolDraft(ref) {
  const inst = instanceByRef(ref);
  const sym = symbolForRef(ref);
  if (!inst || !sym) {
    return { ok: false, message: "No symbol selected." };
  }

  const nextCategory = String(el.symbolCategoryInput.value ?? "").trim() || String(sym.category ?? "generic");
  const nextWidth = Number(el.symbolWidthInput.value);
  const nextHeight = Number(el.symbolHeightInput.value);
  if (!Number.isFinite(nextWidth) || !Number.isFinite(nextHeight) || nextWidth < 20 || nextHeight < 20) {
    return { ok: false, message: "Symbol width/height must be >= 20." };
  }

  const rows = [...el.symbolPinsList.querySelectorAll(".symbolPinRow")];
  clearSymbolRowValidation(rows);
  if (!rows.length) {
    return { ok: false, message: "Symbol must contain at least one pin row." };
  }

  const parsedPins = [];
  for (const row of rows) {
    const name = String(row.querySelector(".pinName")?.value ?? "").trim();
    const number = String(row.querySelector(".pinNumber")?.value ?? "").trim();
    const side = String(row.querySelector(".pinSide")?.value ?? "");
    const offset = Number(row.querySelector(".pinOffset")?.value ?? 0);
    const type = String(row.querySelector(".pinType")?.value ?? "");
    if (!name || !number || !PIN_SIDES.includes(side) || !PIN_TYPES.includes(type) || !Number.isFinite(offset) || offset < 0) {
      row.classList.add("invalidRow");
      return { ok: false, message: "Each pin row needs name, number, valid side/type, and offset >= 0." };
    }
    parsedPins.push({
      oldName: row.getAttribute("data-old-pin") ?? name,
      pin: { name, number, side, offset: Math.round(offset), type }
    });
  }

  const nameCounts = new Map();
  const numberCounts = new Map();
  for (const p of parsedPins) {
    nameCounts.set(p.pin.name, (nameCounts.get(p.pin.name) ?? 0) + 1);
    numberCounts.set(p.pin.number, (numberCounts.get(p.pin.number) ?? 0) + 1);
  }
  let dupName = null;
  let dupNumber = null;
  for (const [name, count] of nameCounts) {
    if (count > 1) {
      dupName = name;
      break;
    }
  }
  for (const [num, count] of numberCounts) {
    if (count > 1) {
      dupNumber = num;
      break;
    }
  }
  if (dupName || dupNumber) {
    for (const row of rows) {
      const name = String(row.querySelector(".pinName")?.value ?? "").trim();
      const num = String(row.querySelector(".pinNumber")?.value ?? "").trim();
      if ((dupName && name === dupName) || (dupNumber && num === dupNumber)) {
        row.classList.add("invalidRow");
      }
    }
    if (dupName) {
      return { ok: false, message: `Duplicate pin name '${dupName}' detected.` };
    }
    return { ok: false, message: `Duplicate pin number '${dupNumber}' detected.` };
  }

  return {
    ok: true,
    inst,
    sym,
    parsedPins,
    nextCategory,
    nextWidth: Math.round(nextWidth),
    nextHeight: Math.round(nextHeight)
  };
}

function buildSymbolMigrationPlan(symbolId, beforePins, parsedPins) {
  const beforeNames = new Set(beforePins.map((p) => p.name));
  const afterNames = new Set(parsedPins.map((p) => p.pin.name));
  const renameMap = new Map();
  for (const entry of parsedPins) {
    if (entry.oldName && beforeNames.has(entry.oldName) && entry.oldName !== entry.pin.name) {
      renameMap.set(entry.oldName, entry.pin.name);
    }
  }

  const removedPins = beforePins
    .map((p) => p.name)
    .filter((name) => !afterNames.has(name) && !renameMap.has(name))
    .sort((a, b) => a.localeCompare(b));
  const renamedPins = [...renameMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
  const refs = new Set((state.model.instances ?? []).filter((i) => i.symbol === symbolId).map((i) => i.ref));

  let droppedNodes = 0;
  const touchedNets = new Set();
  for (const net of state.model.nets ?? []) {
    for (const node of net.nodes ?? []) {
      if (!refs.has(node.ref)) {
        continue;
      }
      const migratedPin = renameMap.get(node.pin) ?? node.pin;
      if (!afterNames.has(migratedPin)) {
        droppedNodes += 1;
        touchedNets.add(net.name);
      }
    }
  }

  let removedPinUiEntries = 0;
  for (const inst of state.model.instances ?? []) {
    if (!refs.has(inst.ref)) {
      continue;
    }
    const pinUi = inst.properties?.pin_ui;
    if (!pinUi || typeof pinUi !== "object" || Array.isArray(pinUi)) {
      continue;
    }
    for (const key of Object.keys(pinUi)) {
      const migratedPin = renameMap.get(key) ?? key;
      if (!afterNames.has(migratedPin)) {
        removedPinUiEntries += 1;
      }
    }
  }

  const hasDestructive = removedPins.length > 0 || droppedNodes > 0 || removedPinUiEntries > 0;
  const hash = JSON.stringify({
    symbolId,
    renamedPins,
    removedPins,
    droppedNodes,
    touchedNets: [...touchedNets].sort(),
    removedPinUiEntries
  });

  return {
    hash,
    hasDestructive,
    renamedPins,
    removedPins,
    droppedNodes,
    touchedNets: [...touchedNets].sort(),
    removedPinUiEntries
  };
}

function renderSymbolMigrationPlan(plan) {
  if (!el.symbolMigrationPreview) {
    return;
  }
  const lines = [];
  lines.push(`Renamed pins: ${plan.renamedPins.length}`);
  lines.push(`Removed pins: ${plan.removedPins.length}`);
  lines.push(`Dropped net nodes: ${plan.droppedNodes}`);
  lines.push(`Removed pin-ui mappings: ${plan.removedPinUiEntries}`);
  if (plan.removedPins.length) {
    lines.push(`Removed: ${plan.removedPins.join(", ")}`);
  }
  if (plan.touchedNets.length) {
    const sample = plan.touchedNets.slice(0, 8).join(", ");
    const suffix = plan.touchedNets.length > 8 ? ` (+${plan.touchedNets.length - 8} more)` : "";
    lines.push(`Affected nets: ${sample}${suffix}`);
  }
  lines.push(plan.hasDestructive ? "Destructive changes detected. Preview must be acknowledged before apply." : "No destructive migration detected.");
  el.symbolMigrationPreview.innerHTML = `<div>${lines.map((line) => escHtml(line)).join("<br/>")}</div>`;
  el.symbolMigrationPreview.classList.add("migrationPreview");
}

function renderSymbolEditorForRef(ref) {
  const inst = instanceByRef(ref);
  const sym = symbolForRef(ref);
  if (!inst || !sym) {
    el.symbolEditor.classList.add("hidden");
    return;
  }
  el.symbolMeta.textContent = `Symbol ${inst.symbol} (${(sym.pins ?? []).length} pins)`;
  el.symbolCategoryInput.value = String(sym.category ?? "");
  el.symbolWidthInput.value = String(Number(sym.body?.width ?? 120));
  el.symbolHeightInput.value = String(Number(sym.body?.height ?? 80));
  el.symbolValidation.textContent = "";
  el.symbolValidation.classList.remove("symbolValidationError");
  invalidateSymbolMigrationPreview("");
  el.symbolPinsList.innerHTML = (sym.pins ?? []).map((pin) => symbolPinRowHtml(pin)).join("");
  el.symbolEditor.classList.remove("hidden");
}

function clearSymbolRowValidation(rows) {
  for (const row of rows) {
    row.classList.remove("invalidRow");
  }
}

function renderPinEditor() {
  if (!state.selectedPin || !state.model) {
    el.pinEditor.classList.add("hidden");
    return;
  }

  const { ref, pin } = state.selectedPin;
  const inst = instanceByRef(ref);
  const sym = symbolForRef(ref);
  const pinDef = sym?.pins?.find((p) => p.name === pin);
  if (!inst) {
    el.pinEditor.classList.add("hidden");
    return;
  }

  const nets = [...new Set((state.model.nets ?? []).filter((n) => n.nodes.some((x) => x.ref === ref && x.pin === pin)).map((n) => n.name))];
  const ui = pinUi(inst, pin);
  el.pinMeta.textContent = `${ref}.${pin} | Nets: ${nets.length ? nets.join(", ") : "(unconnected)"}`;
  el.pinNameInput.value = String(pinDef?.name ?? pin);
  el.pinNumberInput.value = String(pinDef?.number ?? "");
  el.pinSideInput.value = PIN_SIDES.includes(String(pinDef?.side ?? "")) ? String(pinDef.side) : "left";
  el.pinTypeInput.value = PIN_TYPES.includes(String(pinDef?.type ?? "")) ? String(pinDef.type) : "passive";
  el.pinOffsetInput.value = String(Number(pinDef?.offset ?? 0));
  el.showPinNetLabelInput.checked = Boolean(ui.show_net_label ?? inst.properties?.show_net_labels);
  el.pinNetSelect.innerHTML = (state.model.nets ?? [])
    .map((n) => `<option value="${n.name}">${n.name} (${n.class})</option>`)
    .join("");
  el.newNetNameInput.placeholder = nextAutoNetName();
  el.newNetClassInput.value = inferClassForPin(ref, pin);
  el.pinConnections.innerHTML = nets.length
    ? nets
        .map(
          (name) =>
            `<div class="miniRow"><span>${name}</span><button data-disconnect-net="${name}" type="button">Disconnect</button></div>`
        )
        .join("")
    : `<div class="miniRow"><span>No net connections yet.</span></div>`;

  el.pinEditor.classList.remove("hidden");
}

function renderNetEditor() {
  if (!state.selectedNet || !state.model) {
    el.netEditor.classList.add("hidden");
    return;
  }
  const net = netByName(state.selectedNet);
  if (!net) {
    el.netEditor.classList.add("hidden");
    return;
  }

  el.netNameInput.value = net.name;
  el.netClassInput.value = NET_CLASSES.includes(net.class) ? net.class : "signal";
  el.netNodeRefInput.value = state.selectedPin?.ref ?? "";
  el.netNodePinInput.value = state.selectedPin?.pin ?? "";
  el.netNodesList.innerHTML = (net.nodes ?? [])
    .map(
      (node) =>
        `<div class="miniRow"><span>${node.ref}.${node.pin}</span><button data-remove-node="${node.ref}.${node.pin}" type="button">Remove</button></div>`
    )
    .join("");
  el.netEditor.classList.remove("hidden");
}

function renderSelected() {
  el.duplicateComponentBtn.disabled = true;
  el.deleteComponentBtn.disabled = true;
  el.isolateSelectedComponentBtn.disabled = true;
  el.isolateSelectedNetBtn.disabled = true;

  if (!state.model) {
    el.selectedSummary.textContent = "Click a component, net, or pin to inspect it.";
    el.componentEditor.classList.add("hidden");
    el.symbolEditor.classList.add("hidden");
    el.pinEditor.classList.add("hidden");
    el.netEditor.classList.add("hidden");
    return;
  }

  if (state.selectedPin) {
    const nets = (state.model.nets ?? [])
      .filter((n) => n.nodes.some((x) => x.ref === state.selectedPin.ref && x.pin === state.selectedPin.pin))
      .map((n) => n.name);
    el.selectedSummary.textContent = `${state.selectedPin.ref}.${state.selectedPin.pin}\nNets: ${nets.length ? nets.join(", ") : "(no net)"}`;
    el.componentEditor.classList.add("hidden");
    el.symbolEditor.classList.add("hidden");
    renderPinEditor();
    el.netEditor.classList.add("hidden");
    return;
  }

  if (state.selectedRefs.length > 1) {
    el.selectedSummary.textContent = `${state.selectedRefs.length} components selected\n${state.selectedRefs.slice(0, 10).join(", ")}${state.selectedRefs.length > 10 ? "..." : ""}`;
    el.componentEditor.classList.add("hidden");
    el.symbolEditor.classList.add("hidden");
    el.pinEditor.classList.add("hidden");
    el.netEditor.classList.add("hidden");
    return;
  }

  const inst = state.model.instances.find((i) => i.ref === state.selectedRef);
  if (inst) {
    el.selectedSummary.textContent = `${inst.ref} (${inst.symbol})`;
    el.componentEditor.classList.remove("hidden");
    el.xInput.value = String(inst.placement.x ?? 0);
    el.yInput.value = String(inst.placement.y ?? 0);
    el.rotationInput.value = String((Math.round(Number(inst.placement.rotation ?? 0) / 90) * 90 + 360) % 360);
    el.lockedInput.checked = Boolean(inst.placement.locked);
    el.instRefInput.value = inst.ref;
    el.instValueInput.value = String(inst.properties?.value ?? "");
    el.instNotesInput.value = String(inst.properties?.note ?? inst.properties?.notes ?? "");
    el.duplicateComponentBtn.disabled = false;
    el.deleteComponentBtn.disabled = false;
    el.isolateSelectedComponentBtn.disabled = false;
    renderSymbolEditorForRef(inst.ref);
    el.pinEditor.classList.add("hidden");
    el.netEditor.classList.add("hidden");
    return;
  }

  if (state.selectedNet) {
    const net = state.model.nets.find((n) => n.name === state.selectedNet);
    if (net) {
      el.selectedSummary.textContent = `${net.name} (${net.class})\nNodes: ${net.nodes.map((n) => `${n.ref}.${n.pin}`).join(", ")}`;
      el.isolateSelectedNetBtn.disabled = false;
      el.componentEditor.classList.add("hidden");
      el.symbolEditor.classList.add("hidden");
      el.pinEditor.classList.add("hidden");
      renderNetEditor();
      return;
    }
  }

  el.selectedSummary.textContent = "Click a component, net, or pin to inspect it.";
  el.componentEditor.classList.add("hidden");
  el.symbolEditor.classList.add("hidden");
  el.pinEditor.classList.add("hidden");
  el.netEditor.classList.add("hidden");
}

function issueById(issueId) {
  const issues = [...(state.compile?.errors ?? []), ...(state.compile?.warnings ?? [])];
  return issues.find((i) => i.id === issueId) ?? null;
}

function parseRefPinFromIssueMessage(issue) {
  const m = /'([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z0-9_]+)'/.exec(String(issue?.message ?? ""));
  if (m) {
    return { ref: m[1], pin: m[2] };
  }
  return null;
}

function ensureNet(name, netClass = "signal") {
  if (!state.model) {
    return null;
  }
  let net = netByName(name);
  if (!net) {
    net = { name, class: netClass, nodes: [] };
    state.model.nets.push(net);
  }
  return net;
}

function issueFixAction(issue) {
  if (!issue?.code) {
    return null;
  }
  if (issue.code === "ground_net_missing") {
    return { label: "Create GND Net", action: "create_ground" };
  }
  if (issue.code === "floating_input") {
    const rp = parseRefPinFromIssueMessage(issue);
    if (rp) {
      return { label: "Create Signal Net", action: "connect_signal", ref: rp.ref, pin: rp.pin };
    }
  }
  if (issue.code === "required_power_unconnected") {
    const rp = parseRefPinFromIssueMessage(issue);
    if (rp) {
      return { label: "Connect Power", action: "connect_power", ref: rp.ref, pin: rp.pin };
    }
  }
  return null;
}

async function applyIssueFix(issueId) {
  if (!state.model) {
    return;
  }
  const issue = issueById(issueId);
  const fix = issueFixAction(issue);
  if (!issue || !fix) {
    el.jsonFeedback.textContent = "No automatic fix available for this issue.";
    return;
  }

  pushHistory("issue-fix");
  let applied = false;

  if (fix.action === "create_ground") {
    ensureNet("GND", "ground");
    applied = true;
  } else if (fix.action === "connect_signal") {
    const name = normalizeNetName(`NET_${fix.ref}_${fix.pin}`);
    applied = connectPinToNet(fix.ref, fix.pin, name || nextAutoNetName(), { netClass: "signal" }).ok;
  } else if (fix.action === "connect_power") {
    const upperPin = String(fix.pin).toUpperCase();
    if (upperPin.includes("GND")) {
      applied = connectPinToNet(fix.ref, fix.pin, "GND", { netClass: "ground" }).ok;
    } else if (netByName("3V3")) {
      applied = connectPinToNet(fix.ref, fix.pin, "3V3", { netClass: "power" }).ok;
    } else if (netByName("5V")) {
      applied = connectPinToNet(fix.ref, fix.pin, "5V", { netClass: "power" }).ok;
    } else {
      applied = connectPinToNet(fix.ref, fix.pin, normalizeNetName(`PWR_${fix.ref}_${fix.pin}`), { netClass: "power" }).ok;
    }
  }

  if (!applied) {
    el.jsonFeedback.textContent = "Automatic fix could not be applied safely.";
    return;
  }

  await compileModel(state.model, { keepView: true, source: "issue-fix" });
  el.jsonFeedback.textContent = `Applied fix for ${issue.code}.`;
  focusIssue(issueId);
}

function renderIssues() {
  const errors = state.compile?.errors ?? [];
  const warnings = state.compile?.warnings ?? [];

  if (!errors.length && !warnings.length) {
    el.issues.innerHTML = "No issues.\nClick a net/component to inspect relationships.";
    return;
  }

  const rows = [
    ...errors.map(
      (issue) => {
        const fix = issueFixAction(issue);
        return `<div class="issueRow issueErr" data-issue-id="${issue.id}"><div class="issueTitle">[E] ${issue.message}</div><div class="issueMeta">${issue.code} · ${issue.path ?? "-"}</div><div class="issueMeta">${issue.suggestion ?? ""}</div>${fix ? `<div class="issueActions"><button type="button" data-fix-issue="${issue.id}">${fix.label}</button></div>` : ""}</div>`;
      }
    ),
    ...warnings.map(
      (issue) => {
        const fix = issueFixAction(issue);
        return `<div class="issueRow issueWarn" data-issue-id="${issue.id}"><div class="issueTitle">[W] ${issue.message}</div><div class="issueMeta">${issue.code} · ${issue.path ?? "-"}</div><div class="issueMeta">${issue.suggestion ?? ""}</div>${fix ? `<div class="issueActions"><button type="button" data-fix-issue="${issue.id}">${fix.label}</button></div>` : ""}</div>`;
      }
    )
  ];

  el.issues.innerHTML = rows.join("");
}

function renderTopology() {
  const t = state.compile?.topology;
  if (!t) {
    el.topology.textContent = "No topology.";
    return;
  }

  const lines = [];
  lines.push("Power domains:");
  for (const pd of t.power_domain_consumers ?? []) {
    lines.push(`- ${pd.name}: ${pd.count} consumers`);
  }

  lines.push(`Clock sources: ${(t.clock_sources ?? []).join(", ") || "-"}`);
  lines.push(`Clock sinks: ${(t.clock_sinks ?? []).join(", ") || "-"}`);
  lines.push("Buses:");
  if (t.buses?.length) {
    for (const b of t.buses) {
      lines.push(`- ${b.name}: ${b.nets.join(", ")}`);
    }
  } else {
    lines.push("- none");
  }

  lines.push("Signal paths:");
  if (t.signal_paths?.length) {
    for (const p of t.signal_paths) {
      lines.push(`- ${p.join(" -> ")}`);
    }
  } else {
    lines.push("- none");
  }

  el.topology.textContent = lines.join("\n");
}

function parsePinNets(node) {
  const raw = node.getAttribute("data-pin-nets") ?? "";
  if (!raw) {
    return [];
  }
  return raw.split(",").filter(Boolean);
}

function applyVisualHighlight() {
  const svg = el.canvasInner.querySelector("svg");
  if (!svg) {
    return;
  }

  const activeNets = activeNetSet();
  const isolateByNet = state.isolateNet && state.selectedNet;
  const selectedRefs = selectedRefSet();
  const isolateByComp = state.isolateComponent && selectedRefs.size > 0;
  const compIncident = new Set();
  for (const ref of selectedRefs) {
    for (const net of incidentNetsForRef(ref)) {
      compIncident.add(net);
    }
  }
  const netRefs = state.selectedNet ? refsConnectedToNet(state.selectedNet) : new Set();

  svg.querySelectorAll("[data-net], [data-net-label], [data-net-junction], [data-net-tie]").forEach((node) => {
    const net =
      node.getAttribute("data-net") ??
      node.getAttribute("data-net-label") ??
      node.getAttribute("data-net-junction") ??
      node.getAttribute("data-net-tie");

    let on = true;
    if (isolateByNet) {
      on = net === state.selectedNet;
    } else if (isolateByComp) {
      on = compIncident.has(net);
    } else if (activeNets) {
      on = activeNets.has(net);
    }

    node.style.opacity = on ? "1" : activeNets || isolateByNet || isolateByComp ? "0.12" : "1";
  });

  svg.querySelectorAll("[data-ref]").forEach((node) => {
    const ref = node.getAttribute("data-ref");
    let on = true;
    if (isolateByComp) {
      on = selectedRefs.has(ref);
    } else if (isolateByNet) {
      on = netRefs.has(ref);
    }

    node.style.opacity = on ? "1" : "0.1";
    if (selectedRefs.has(ref)) {
      const rect = node.querySelector("rect");
      if (rect) {
        rect.setAttribute("stroke", "#155eef");
        rect.setAttribute("stroke-width", "2.6");
      }
    } else {
      const rect = node.querySelector("rect");
      if (rect) {
        rect.setAttribute("stroke", "#1f2937");
        rect.setAttribute("stroke-width", "2");
      }
    }
  });

  svg.querySelectorAll("[data-pin-ref]").forEach((node) => {
    const ref = node.getAttribute("data-pin-ref");
    const pinNets = parsePinNets(node);

    let on = true;
    if (isolateByComp) {
      on = selectedRefs.has(ref);
    } else if (isolateByNet) {
      on = pinNets.includes(state.selectedNet);
    } else if (activeNets) {
      on = pinNets.some((n) => activeNets.has(n));
    }

    if (state.selectedPin && state.selectedPin.ref === ref && state.selectedPin.pin === node.getAttribute("data-pin-name")) {
      node.setAttribute("r", "4.2");
      node.setAttribute("fill", "#155eef");
      node.style.opacity = "1";
    } else {
      node.setAttribute("r", "3.2");
      node.setAttribute("fill", "#111827");
      node.style.opacity = on ? "1" : activeNets || isolateByNet || isolateByComp ? "0.16" : "1";
    }
  });

  setLabelLayerVisibility();
}

function flashElements(selector) {
  const nodes = [...el.canvasInner.querySelectorAll(selector)];
  for (const n of nodes) {
    n.classList.add("flash");
  }
  setTimeout(() => {
    for (const n of nodes) {
      n.classList.remove("flash");
    }
  }, 1200);
}

function focusIssue(issueId) {
  const target = state.compile?.focus_map?.[issueId];
  if (!target) {
    return;
  }

  if (target.type === "net" && target.net) {
    state.selectedNet = target.net;
    setSelectedRefs([]);
    state.selectedPin = null;
    state.isolateComponent = false;
    renderAll();
    flashElements(`[data-net="${target.net}"], [data-net-label="${target.net}"], [data-net-junction="${target.net}"], [data-net-tie="${target.net}"]`);
  } else if (target.type === "component" && target.ref) {
    selectSingleRef(target.ref);
    state.selectedNet = null;
    state.selectedPin = null;
    state.isolateNet = false;
    renderAll();
    flashElements(`[data-ref="${target.ref}"]`);
  } else if (target.type === "pin" && target.ref && target.pin) {
    selectSingleRef(target.ref);
    state.selectedNet = null;
    state.selectedPin = {
      ref: target.ref,
      pin: target.pin,
      nets: []
    };
    state.isolateNet = false;
    renderAll();
    flashElements(`[data-pin-ref="${target.ref}"][data-pin-name="${target.pin}"]`);
  }

  if (target.bbox) {
    zoomToBBox(target.bbox);
  }
}

function showPinTooltip(clientX, clientY, ref, pin, nets) {
  el.pinTooltip.innerHTML = `<strong>${ref}.${pin}</strong><br/>${nets.length ? `Net: ${nets.join(", ")}` : "Net: (unconnected)"}`;
  el.pinTooltip.classList.remove("hidden");
  const rect = el.canvasViewport.getBoundingClientRect();
  el.pinTooltip.style.left = `${clientX - rect.left + 14}px`;
  el.pinTooltip.style.top = `${clientY - rect.top + 14}px`;
}

function hidePinTooltip() {
  el.pinTooltip.classList.add("hidden");
}

function bindSvgInteractions() {
  const svg = el.canvasInner.querySelector("svg");
  if (!svg) {
    return;
  }

  svg.querySelectorAll("[data-ref]").forEach((node) => {
    node.addEventListener("pointerdown", (evt) => {
      if (
        evt.target.closest("[data-pin-ref]") ||
        evt.target.closest("[data-net]") ||
        evt.target.closest("[data-net-label]") ||
        evt.target.closest("[data-net-junction]") ||
        evt.target.closest("[data-net-tie]")
      ) {
        return;
      }
      evt.stopPropagation();
      evt.preventDefault();
      const ref = node.getAttribute("data-ref");
      if (!ref || !state.model) {
        return;
      }

      if (hasSelectionModifier(evt)) {
        toggleSelectedRef(ref);
        state.selectedNet = null;
        state.selectedPin = null;
        state.isolateNet = false;
        renderAll();
        return;
      }

      selectSingleRef(ref);
      state.selectedNet = null;
      state.selectedPin = null;
      state.isolateNet = false;
      renderInstances();
      renderNets();
      renderSelected();
      applyVisualHighlight();

      const inst = state.model.instances.find((x) => x.ref === ref);
      if (!inst) {
        return;
      }

      const pt = canvasToSvgPoint(evt.clientX, evt.clientY);
      const clickedInst = instanceByRef(ref);
      if (clickedInst?.placement?.locked) {
        pushHistory("auto-unlock-drag");
        clickedInst.placement.locked = false;
        el.jsonFeedback.textContent = `${ref} was locked and has been auto-unlocked for manual move.`;
        renderSelected();
      }

      const dragRefsRaw = state.selectedRefs.length ? [...state.selectedRefs] : [ref];
      const dragRefs = dragRefsRaw.filter((r) => !instanceByRef(r)?.placement?.locked);
      if (!dragRefs.length) {
        el.jsonFeedback.textContent = `${ref} is locked. Uncheck 'Locked' on the component or use 'Unlock All'.`;
        return;
      }
      const baseByRef = {};
      for (const r of dragRefs) {
        const ii = state.model.instances.find((x) => x.ref === r);
        if (!ii) {
          continue;
        }
        baseByRef[r] = {
          x: Number(ii.placement.x ?? 0),
          y: Number(ii.placement.y ?? 0)
        };
      }

      state.draggingComponentRef = ref;
      state.dragPointerId = evt.pointerId;
      state.dragComponent = {
        startPointerX: pt.x,
        startPointerY: pt.y,
        refs: dragRefs,
        baseByRef,
        pendingByRef: clone(baseByRef)
      };
      state.dragPreviewNode = node;
      state.dragMoved = false;

      node.setPointerCapture(evt.pointerId);
    });
  });

  svg.querySelectorAll("[data-net], [data-net-label], [data-net-junction], [data-net-tie]").forEach((node) => {
    node.addEventListener("click", (evt) => {
      evt.stopPropagation();
      const net =
        node.getAttribute("data-net") ??
        node.getAttribute("data-net-label") ??
        node.getAttribute("data-net-junction") ??
        node.getAttribute("data-net-tie");
      state.selectedNet = net;
      setSelectedRefs([]);
      state.selectedPin = null;
      state.isolateComponent = false;
      renderAll();
    });
  });

  svg.querySelectorAll("[data-pin-ref]").forEach((node) => {
    node.addEventListener("mouseenter", (evt) => {
      const ref = node.getAttribute("data-pin-ref");
      const pin = node.getAttribute("data-pin-name");
      const nets = parsePinNets(node);
      showPinTooltip(evt.clientX, evt.clientY, ref, pin, nets);
    });

    node.addEventListener("mousemove", (evt) => {
      const ref = node.getAttribute("data-pin-ref");
      const pin = node.getAttribute("data-pin-name");
      const nets = parsePinNets(node);
      showPinTooltip(evt.clientX, evt.clientY, ref, pin, nets);
    });

    node.addEventListener("mouseleave", hidePinTooltip);

    node.addEventListener("click", (evt) => {
      evt.stopPropagation();
      const ref = node.getAttribute("data-pin-ref");
      const pin = node.getAttribute("data-pin-name");
      const nets = parsePinNets(node);
      state.selectedPin = { ref, pin, nets };
      selectSingleRef(ref);
      state.selectedNet = null;
      state.isolateNet = false;
      renderAll();
    });
  });
}

function renderCanvas() {
  if (!state.compile?.svg) {
    el.canvasInner.innerHTML = "";
    updateSelectedNoteHint();
    return;
  }

  el.canvasInner.innerHTML = state.compile.svg;
  bindSvgInteractions();
  applyVisualHighlight();
  updateTransform();
  const svg = el.canvasInner.querySelector("svg");
  if (svg) {
    applyLabelDensityByZoom(svg);
    resolveLabelCollisions(svg);
  }
  updateSelectedNoteHint();
}

function renderAll() {
  renderInstances();
  renderNets();
  renderSelected();
  renderIssues();
  renderTopology();
  renderCanvas();

  el.isolateNetBtn.classList.toggle("activeChip", state.isolateNet);
  el.isolateComponentBtn.classList.toggle("activeChip", state.isolateComponent);
  el.undoBtn.disabled = state.historyPast.length === 0;
  el.redoBtn.disabled = state.historyFuture.length === 0;
}

async function compileModel(model, opts = {}) {
  if (state.compileDebounceId != null) {
    clearTimeout(state.compileDebounceId);
    state.compileDebounceId = null;
  }
  const source = opts.source ?? "manual";
  const fit = opts.fit ?? false;
  const keepView = opts.keepView ?? false;
  const preservePlacement = opts.preservePlacement ?? false;

  setStatus(source === "drag" ? "Compiling after drag..." : "Compiling...");
  try {
    const result = await apiPost("/compile", {
      payload: model,
      options: compileOptions(
        preservePlacement
          ? {
              preserve_placement: true,
              auto_rotate: false,
              respect_locks: true
            }
          : {}
      )
    });

    state.model = applyCompileLayoutToModel(model, result);
    state.compile = result;
    reconcileSelectionWithModel();
    refreshJsonEditor();
    saveSnapshot();
    renderAll();

    if (fit) {
      fitView(result.layout);
    } else if (!keepView && !state.userAdjustedView) {
      fitView(result.layout);
    }

    setStatus(formatCompileStatus(result));
  } catch (err) {
    setStatus(`Compile failed: ${err.message}`, false);
    el.issues.textContent = `Compile error: ${err.message}`;
  }
}

function clearDragPreview() {
  if (state.dragComponent?.refs?.length) {
    for (const ref of state.dragComponent.refs) {
      const n = el.canvasInner.querySelector(`[data-ref="${ref}"]`);
      if (n) {
        n.removeAttribute("transform");
      }
    }
  } else if (state.dragPreviewNode) {
    state.dragPreviewNode.removeAttribute("transform");
  }
  state.dragPreviewNode = null;
}

function queueCompile(keepView = true, source = "edit") {
  if (!state.model) {
    return;
  }
  if (state.compileDebounceId != null) {
    clearTimeout(state.compileDebounceId);
  }
  state.compileDebounceId = setTimeout(() => {
    state.compileDebounceId = null;
    compileModel(state.model, { keepView, source, preservePlacement: true });
  }, 150);
}

function updateInstance(ref, patch) {
  const inst = instanceByRef(ref);
  if (!inst) {
    return false;
  }
  Object.assign(inst, patch);
  return true;
}

function setPinUi(ref, pinName, patch) {
  const inst = instanceByRef(ref);
  if (!inst) {
    return false;
  }
  inst.properties = inst.properties ?? {};
  const pinUiMap =
    inst.properties.pin_ui && typeof inst.properties.pin_ui === "object" && !Array.isArray(inst.properties.pin_ui)
      ? inst.properties.pin_ui
      : {};
  const pinUiEntry =
    pinUiMap[pinName] && typeof pinUiMap[pinName] === "object" && !Array.isArray(pinUiMap[pinName]) ? pinUiMap[pinName] : {};
  pinUiMap[pinName] = {
    ...pinUiEntry,
    ...patch
  };
  inst.properties.pin_ui = pinUiMap;
  return true;
}

function hasNodeOnNet(net, ref, pin) {
  return (net.nodes ?? []).some((n) => n.ref === ref && n.pin === pin);
}

function refsForSymbol(symbolId) {
  return new Set((state.model?.instances ?? []).filter((i) => i.symbol === symbolId).map((i) => i.ref));
}

function symbolPinUsage(symbolId, pinName) {
  const refs = refsForSymbol(symbolId);
  const nets = new Set();
  let nodes = 0;
  for (const net of state.model?.nets ?? []) {
    for (const node of net.nodes ?? []) {
      if (refs.has(node.ref) && node.pin === pinName) {
        nodes += 1;
        nets.add(net.name);
      }
    }
  }
  return { refs: refs.size, nodes, nets: [...nets].sort() };
}

function disconnectImpact(netName, ref, pin) {
  const net = netByName(netName);
  if (!net) {
    return { remainingNodes: 0, orphaned: false };
  }
  const remainingNodes = (net.nodes ?? []).filter((n) => !(n.ref === ref && n.pin === pin)).length;
  return {
    remainingNodes,
    orphaned: remainingNodes < 2
  };
}

function deleteComponentImpact(ref) {
  const impacted = [];
  let removedNodes = 0;
  for (const net of state.model?.nets ?? []) {
    const before = net.nodes?.length ?? 0;
    const after = (net.nodes ?? []).filter((n) => n.ref !== ref).length;
    if (before !== after) {
      removedNodes += before - after;
      impacted.push({ name: net.name, removed: before - after, orphaned: after < 2 });
    }
  }
  return { removedNodes, impacted };
}

function connectPinToNet(ref, pin, netName, opts = {}) {
  if (!state.model) {
    return { ok: false, message: "No model loaded." };
  }
  const inst = instanceByRef(ref);
  if (!inst) {
    return { ok: false, message: `Unknown component '${ref}'.` };
  }
  if (!pinExists(ref, pin)) {
    const sym = symbolForRef(ref);
    const category = String(sym?.category ?? "").toLowerCase();
    const genericLike = sym?.auto_generated === true || category.includes("generic") || inst.part === "generic";
    if (!genericLike) {
      return { ok: false, message: `Unknown pin ${ref}.${pin}.` };
    }
  }
  const normalized = normalizeNetName(netName);
  if (!normalized) {
    return { ok: false, message: "Net name is required." };
  }
  let net = netByName(normalized);
  if (!net) {
    const guessedClass = opts.netClass && NET_CLASSES.includes(opts.netClass) ? opts.netClass : inferClassForPin(ref, pin);
    net = {
      name: normalized,
      class: guessedClass,
      nodes: []
    };
    state.model.nets.push(net);
  }

  if (!hasNodeOnNet(net, ref, pin)) {
    net.nodes.push({ ref, pin });
  }
  return { ok: true, net: normalized };
}

function removeNetIfOrphaned(netName) {
  if (!state.model) {
    return;
  }
  const net = netByName(netName);
  if (!net) {
    return;
  }
  if ((net.nodes?.length ?? 0) < 2) {
    state.model.nets = state.model.nets.filter((n) => n.name !== netName);
    if (state.selectedNet === netName) {
      state.selectedNet = null;
    }
  }
}

function disconnectPinFromNet(ref, pin, netName) {
  const net = netByName(netName);
  if (!net) {
    return { ok: false, message: `Net '${netName}' not found.` };
  }
  const before = net.nodes.length;
  net.nodes = net.nodes.filter((n) => !(n.ref === ref && n.pin === pin));
  if (net.nodes.length === before) {
    return { ok: false, message: `${ref}.${pin} is not connected to ${netName}.` };
  }
  removeNetIfOrphaned(netName);
  return { ok: true };
}

function renameNet(oldName, newName) {
  if (!state.model) {
    return { ok: false, message: "No model loaded." };
  }
  const current = netByName(oldName);
  if (!current) {
    return { ok: false, message: `Net '${oldName}' not found.` };
  }
  const normalized = normalizeNetName(newName);
  if (!normalized) {
    return { ok: false, message: "Net name is required." };
  }
  if (normalized !== oldName && netByName(normalized)) {
    return { ok: false, message: `Net '${normalized}' already exists.` };
  }
  current.name = normalized;
  if (state.selectedNet === oldName) {
    state.selectedNet = normalized;
  }
  return { ok: true, name: normalized };
}

function setNetClass(netName, netClass) {
  const net = netByName(netName);
  if (!net) {
    return { ok: false, message: `Net '${netName}' not found.` };
  }
  if (!NET_CLASSES.includes(netClass)) {
    return { ok: false, message: `Invalid net class '${netClass}'.` };
  }
  net.class = netClass;
  return { ok: true };
}

function isTypingContext(target) {
  if (!target || !(target instanceof HTMLElement)) {
    return false;
  }
  const tag = target.tagName.toLowerCase();
  return tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable;
}

function parseJsonPositionError(text, err) {
  const msg = String(err?.message ?? "Invalid JSON");
  const m = /position\s+(\d+)/i.exec(msg);
  if (!m) {
    return { message: msg, line: null, col: null };
  }

  const pos = Number(m[1]);
  let line = 1;
  let col = 1;
  for (let i = 0; i < Math.min(pos, text.length); i += 1) {
    if (text[i] === "\n") {
      line += 1;
      col = 1;
    } else {
      col += 1;
    }
  }

  return { message: msg, line, col };
}

function sortKeysDeep(value) {
  if (Array.isArray(value)) {
    return value.map(sortKeysDeep);
  }
  if (value && typeof value === "object") {
    const out = {};
    for (const key of Object.keys(value).sort()) {
      out[key] = sortKeysDeep(value[key]);
    }
    return out;
  }
  return value;
}

function saveInspectorSectionState() {
  const stateObj = {
    component: Boolean(el.componentSection?.open),
    symbol: Boolean(el.symbolSection?.open),
    pin: Boolean(el.pinSection?.open),
    net: Boolean(el.netSection?.open)
  };
  localStorage.setItem(INSPECTOR_SECTIONS_KEY, JSON.stringify(stateObj));
}

function loadInspectorSectionState() {
  try {
    const raw = localStorage.getItem(INSPECTOR_SECTIONS_KEY);
    if (!raw) {
      return;
    }
    const parsed = JSON.parse(raw);
    if (typeof parsed !== "object" || !parsed) {
      return;
    }
    if (el.componentSection) el.componentSection.open = parsed.component !== false;
    if (el.symbolSection) el.symbolSection.open = parsed.symbol !== false;
    if (el.pinSection) el.pinSection.open = parsed.pin !== false;
    if (el.netSection) el.netSection.open = parsed.net !== false;
  } catch {}
}

async function loadSchemaText() {
  if (state.schemaText) {
    return state.schemaText;
  }

  const res = await fetch(SCHEMA_URL);
  if (!res.ok) {
    throw new Error("Schema file unavailable.");
  }

  const text = await res.text();
  state.schemaText = text;
  return text;
}

async function openSchemaModal() {
  try {
    const raw = await loadSchemaText();
    let pretty = raw;
    try {
      pretty = JSON.stringify(JSON.parse(raw), null, 2);
    } catch {}
    el.schemaViewer.value = pretty;
    el.schemaModal.classList.remove("hidden");
    el.schemaViewer.focus();
    el.jsonFeedback.textContent = "Schema loaded.";
  } catch (err) {
    el.jsonFeedback.textContent = `Schema load failed: ${err.message}`;
  }
}

function closeSchemaModal() {
  el.schemaModal.classList.add("hidden");
}

function openShortcutsModal() {
  el.shortcutsModal?.classList.remove("hidden");
  el.closeShortcutsBtn?.focus();
}

function closeShortcutsModal() {
  el.shortcutsModal?.classList.add("hidden");
}

function commandEntries() {
  return [
    { id: "auto-layout", label: "Run Auto Layout", run: () => el.autoLayoutBtn.click() },
    { id: "auto-tidy", label: "Run Auto Tidy", run: () => el.autoTidyBtn.click() },
    { id: "fit-view", label: "Fit View", run: () => el.fitViewBtn.click() },
    { id: "focus-selection", label: "Focus Selection", run: () => el.focusSelectionBtn.click() },
    { id: "toggle-labels", label: "Toggle Net Labels", run: () => el.showLabelsInput.click() },
    { id: "reset-sample", label: "Reset Sample", run: () => el.resetSampleBtn.click() },
    { id: "load-sample", label: "Load Sample", run: () => el.loadSampleBtn.click() },
    { id: "new-project", label: "New Project", run: () => el.newProjectBtn.click() },
    { id: "show-shortcuts", label: "Show Keyboard Shortcuts", run: () => openShortcutsModal() }
  ];
}

function filteredCommands(query) {
  const q = String(query ?? "")
    .trim()
    .toLowerCase();
  const cmds = commandEntries();
  if (!q) {
    return cmds;
  }
  return cmds.filter((c) => c.label.toLowerCase().includes(q) || c.id.includes(q.replace(/\s+/g, "-")));
}

function renderCommandList() {
  const cmds = filteredCommands(el.commandInput?.value ?? "");
  state.commandIndex = Math.max(0, Math.min(state.commandIndex, Math.max(0, cmds.length - 1)));
  if (!cmds.length) {
    el.commandList.innerHTML = `<div class="miniRow"><span>No commands.</span></div>`;
    return cmds;
  }
  el.commandList.innerHTML = cmds
    .map(
      (cmd, idx) =>
        `<button type="button" role="option" class="commandRow ${idx === state.commandIndex ? "active" : ""}" data-command-id="${cmd.id}">${escHtml(cmd.label)}</button>`
    )
    .join("");
  return cmds;
}

function openCommandModal() {
  state.commandIndex = 0;
  el.commandModal?.classList.remove("hidden");
  renderCommandList();
  el.commandInput?.focus();
  el.commandInput?.select();
}

function closeCommandModal() {
  el.commandModal?.classList.add("hidden");
}

function runCommandById(id) {
  const cmd = commandEntries().find((c) => c.id === id);
  if (!cmd) {
    return;
  }
  closeCommandModal();
  cmd.run();
}

function buildMinimalRepro(model) {
  if (!state.selectedRefs.length && !state.selectedNet) {
    return model;
  }

  const refs = new Set();
  const nets = [];

  if (state.selectedNet) {
    const net = model.nets.find((n) => n.name === state.selectedNet);
    if (net) {
      nets.push(net);
      net.nodes.forEach((n) => refs.add(n.ref));
    }
  }

  if (state.selectedRefs.length) {
    for (const ref of state.selectedRefs) {
      refs.add(ref);
      for (const net of model.nets) {
        if (net.nodes.some((n) => n.ref === ref)) {
          nets.push(net);
          net.nodes.forEach((n) => refs.add(n.ref));
        }
      }
    }
  }

  const uniqNet = new Map(nets.map((n) => [n.name, n]));
  const instances = model.instances.filter((i) => refs.has(i.ref));
  const symbols = {};
  for (const inst of instances) {
    symbols[inst.symbol] = model.symbols[inst.symbol];
  }

  return {
    meta: {
      ...(model.meta ?? {}),
      title: `${(model.meta?.title ?? "schemeta")} - minimal repro`
    },
    symbols,
    instances,
    nets: [...uniqNet.values()],
    constraints: {},
    annotations: []
  };
}

function summarizeModelDelta(before, after) {
  if (!before) {
    return "Applied JSON (new project loaded).";
  }

  const beforeRefs = new Set((before.instances ?? []).map((i) => i.ref));
  const afterRefs = new Set((after.instances ?? []).map((i) => i.ref));
  const beforeNets = new Set((before.nets ?? []).map((n) => n.name));
  const afterNets = new Set((after.nets ?? []).map((n) => n.name));
  const beforeSymbols = new Set(Object.keys(before.symbols ?? {}));
  const afterSymbols = new Set(Object.keys(after.symbols ?? {}));

  const countOnlyIn = (a, b) => [...a].filter((x) => !b.has(x)).length;
  const instAdded = countOnlyIn(afterRefs, beforeRefs);
  const instRemoved = countOnlyIn(beforeRefs, afterRefs);
  const netAdded = countOnlyIn(afterNets, beforeNets);
  const netRemoved = countOnlyIn(beforeNets, afterNets);
  const symAdded = countOnlyIn(afterSymbols, beforeSymbols);
  const symRemoved = countOnlyIn(beforeSymbols, afterSymbols);

  const beforeByRef = new Map((before.instances ?? []).map((i) => [i.ref, i]));
  let moved = 0;
  for (const inst of after.instances ?? []) {
    const prev = beforeByRef.get(inst.ref);
    if (!prev) {
      continue;
    }
    const px = Number(prev.placement?.x ?? 0);
    const py = Number(prev.placement?.y ?? 0);
    const nx = Number(inst.placement?.x ?? 0);
    const ny = Number(inst.placement?.y ?? 0);
    if (px !== nx || py !== ny || Boolean(prev.placement?.locked) !== Boolean(inst.placement?.locked)) {
      moved += 1;
    }
  }

  const beforeNetByName = new Map((before.nets ?? []).map((n) => [n.name, n]));
  let netChanged = 0;
  for (const net of after.nets ?? []) {
    const prev = beforeNetByName.get(net.name);
    if (!prev) {
      continue;
    }
    const prevSig = JSON.stringify({
      class: prev.class,
      nodes: [...(prev.nodes ?? [])].map((n) => `${n.ref}.${n.pin}`).sort()
    });
    const nextSig = JSON.stringify({
      class: net.class,
      nodes: [...(net.nodes ?? [])].map((n) => `${n.ref}.${n.pin}`).sort()
    });
    if (prevSig !== nextSig) {
      netChanged += 1;
    }
  }

  const parts = [];
  if (instAdded || instRemoved || moved) {
    parts.push(`instances +${instAdded}/-${instRemoved}, moved ${moved}`);
  }
  if (netAdded || netRemoved || netChanged) {
    parts.push(`nets +${netAdded}/-${netRemoved}, changed ${netChanged}`);
  }
  if (symAdded || symRemoved) {
    parts.push(`symbols +${symAdded}/-${symRemoved}`);
  }

  return parts.length ? `Applied JSON (${parts.join(" | ")}).` : "Applied JSON (no structural changes).";
}

async function validateJsonEditor() {
  const text = el.jsonEditor.value;
  try {
    const parsed = JSON.parse(text);
    const out = await apiPost("/analyze", { payload: parsed });
    const first = out.errors[0] ?? out.warnings[0];
    const firstPath = first?.path ? ` (${first.path})` : "";
    el.jsonFeedback.textContent = `Validation: ${out.errors.length} errors, ${out.warnings.length} warnings.${first ? ` First issue: ${first.message}${firstPath}` : ""}`;
  } catch (err) {
    const p = parseJsonPositionError(text, err);
    if (p.line != null) {
      el.jsonFeedback.textContent = `JSON parse error at line ${p.line}, col ${p.col}: ${p.message}`;
      const lines = text.split("\n");
      let idx = 0;
      for (let i = 0; i < p.line - 1; i += 1) {
        idx += lines[i].length + 1;
      }
      el.jsonEditor.focus();
      el.jsonEditor.setSelectionRange(idx, idx + Math.max(1, lines[p.line - 1]?.length ?? 1));
    } else {
      el.jsonFeedback.textContent = `JSON parse error: ${p.message}`;
    }
  }
}

async function analyzeModelForImport(parsed, sourceName = "JSON") {
  const out = await apiPost("/analyze", { payload: parsed });
  if (out.errors.length) {
    const first = out.errors[0];
    const path = first.path ? ` (${first.path})` : "";
    const msg = `${sourceName} blocked by validation: ${out.errors.length} error(s), ${out.warnings.length} warning(s). First error: ${first.message}${path}`;
    return { ok: false, out, message: msg };
  }

  const firstWarn = out.warnings[0];
  if (firstWarn) {
    const path = firstWarn.path ? ` (${firstWarn.path})` : "";
    return {
      ok: true,
      out,
      message: `${sourceName} validated: ${out.errors.length} errors, ${out.warnings.length} warnings. First warning: ${firstWarn.message}${path}`
    };
  }

  return { ok: true, out, message: `${sourceName} validated: 0 errors, 0 warnings.` };
}

async function runLayoutAction(path) {
  if (!state.model) {
    return;
  }

  pushHistory(path.includes("auto") ? "auto-layout" : "auto-tidy");
  setStatus(path.includes("auto") ? "Auto layout..." : "Auto tidy...");
  try {
    const out = await apiPost(path, {
      payload: state.model,
      options: compileOptions({
        respect_locks: path.includes("/tidy")
      })
    });

    state.model = applyCompileLayoutToModel(out.model, out.compile);
    state.compile = out.compile;
    refreshJsonEditor();
    renderAll();
    fitView(out.compile.layout);
    saveSnapshot();
    setStatus(formatCompileStatus(out.compile));
  } catch (err) {
    setStatus(`Layout action failed: ${err.message}`, false);
  }
}

async function fetchSampleModel() {
  const res = await fetch("/sample.schemeta.json");
  if (!res.ok) {
    throw new Error("Sample missing.");
  }
  return res.json();
}

async function resetToSample(opts = {}) {
  const push = opts.pushHistory !== false;
  const before = state.model ? clone(state.model) : null;
  const model = await fetchSampleModel();
  if (push && state.model) {
    pushHistory("reset-sample");
  }
  setSelectedRefs([]);
  state.selectedNet = null;
  state.selectedPin = null;
  state.isolateNet = false;
  state.isolateComponent = false;
  state.userAdjustedView = false;
  state.renderMode = "explicit";
  el.renderModeSelect.value = "explicit";
  state.showLabels = true;
  el.showLabelsInput.checked = true;
  el.instanceFilter.value = "";
  el.netFilter.value = "";
  el.instanceList.scrollTop = 0;
  el.netList.scrollTop = 0;
  closeSchemaModal();
  await compileModel(model, { fit: true });
  if (before) {
    el.jsonFeedback.textContent = `Reset sample. ${summarizeModelDelta(before, state.model)}`;
  } else {
    el.jsonFeedback.textContent = "Reset sample baseline loaded.";
  }
}

async function loadSample() {
  try {
    await resetToSample({ pushHistory: true });
  } catch (err) {
    setStatus(String(err?.message ?? "Sample missing."), false);
  }
}

function setupEvents() {
  el.instanceFilter.addEventListener("input", () => {
    el.instanceList.scrollTop = 0;
    renderInstances();
  });
  el.netFilter.addEventListener("input", () => {
    el.netList.scrollTop = 0;
    renderNets();
  });
  el.instanceList.addEventListener("scroll", renderInstances, { passive: true });
  el.netList.addEventListener("scroll", renderNets, { passive: true });
  if (el.newComponentRefInput && el.newComponentTypeSelect) {
    const syncRefPlaceholder = () => {
      el.newComponentRefInput.placeholder = defaultRefSeedForPart(el.newComponentTypeSelect.value);
    };
    syncRefPlaceholder();
    el.newComponentTypeSelect.addEventListener("change", syncRefPlaceholder);
  }

  el.addComponentBtn?.addEventListener("click", async () => {
    if (!state.model) {
      return;
    }

    const part = String(el.newComponentTypeSelect?.value ?? "generic").toLowerCase();
    const rawRef = normalizeRef(el.newComponentRefInput?.value ?? "");
    const ref = rawRef || nextRefLike(defaultRefSeedForPart(part));
    if (instanceByRef(ref)) {
      el.jsonFeedback.textContent = `Component '${ref}' already exists.`;
      return;
    }

    pushHistory("add-component");
    state.model.instances.push({
      ref,
      part,
      properties: {},
      placement: { x: null, y: null, rotation: 0, locked: false }
    });
    el.newComponentRefInput.value = "";
    setSelectedRefs([ref]);
    state.selectedNet = null;
    state.selectedPin = null;
    await compileModel(state.model, { keepView: true, source: "add-component" });
    el.jsonFeedback.textContent = `Added component ${ref} (${part}).`;
  });

  el.addQuickNetBtn?.addEventListener("click", async () => {
    if (!state.model) {
      return;
    }
    const rawName = normalizeNetName(el.newQuickNetNameInput?.value ?? "");
    const name = rawName || nextAutoNetName();
    const netClass = NET_CLASSES.includes(el.newQuickNetClassSelect?.value ?? "") ? el.newQuickNetClassSelect.value : "signal";
    if (netByName(name)) {
      el.jsonFeedback.textContent = `Net '${name}' already exists.`;
      return;
    }

    pushHistory("add-net");
    const nodes = state.selectedPin ? [{ ref: state.selectedPin.ref, pin: state.selectedPin.pin }] : [];
    state.model.nets.push({ name, class: netClass, nodes });
    el.newQuickNetNameInput.value = "";
    state.selectedNet = name;
    await compileModel(state.model, { keepView: true, source: "add-net" });
    el.jsonFeedback.textContent = nodes.length
      ? `Added net ${name} (${netClass}) and connected selected pin.`
      : `Added net ${name} (${netClass}).`;
  });

  [el.componentSection, el.symbolSection, el.pinSection, el.netSection].forEach((section) => {
    if (!section) {
      return;
    }
    section.addEventListener("toggle", saveInspectorSectionState);
  });

  el.instanceList.addEventListener("click", (evt) => {
    const item = evt.target.closest("[data-ref-item]");
    if (!item) {
      return;
    }
    const ref = item.getAttribute("data-ref-item");
    if (hasSelectionModifier(evt)) {
      toggleSelectedRef(ref);
    } else {
      selectSingleRef(ref);
    }
    state.selectedNet = null;
    state.selectedPin = null;
    state.isolateNet = false;
    renderAll();
  });

  el.instanceList.addEventListener("keydown", (evt) => {
    const item = evt.target.closest("[data-ref-item]");
    if (!item) {
      return;
    }
    if (evt.key !== "Enter" && evt.key !== " ") {
      return;
    }
    evt.preventDefault();
    item.click();
  });

  el.netList.addEventListener("click", (evt) => {
    const item = evt.target.closest("[data-net-item]");
    if (!item) {
      return;
    }
    state.selectedNet = item.getAttribute("data-net-item");
    setSelectedRefs([]);
    state.selectedPin = null;
    state.isolateComponent = false;
    renderAll();
  });

  el.netList.addEventListener("keydown", (evt) => {
    const item = evt.target.closest("[data-net-item]");
    if (!item) {
      return;
    }
    if (evt.key !== "Enter" && evt.key !== " ") {
      return;
    }
    evt.preventDefault();
    item.click();
  });

  el.issues.addEventListener("click", (evt) => {
    const fixBtn = evt.target.closest("[data-fix-issue]");
    if (fixBtn) {
      evt.stopPropagation();
      void applyIssueFix(fixBtn.getAttribute("data-fix-issue"));
      return;
    }
    const row = evt.target.closest("[data-issue-id]");
    if (!row) {
      return;
    }
    focusIssue(row.getAttribute("data-issue-id"));
  });

  el.updatePlacementBtn.addEventListener("click", async () => {
    if (state.selectedRefs.length !== 1) {
      el.jsonFeedback.textContent = "Select one component to edit.";
      return;
    }
    const inst = instanceByRef(state.selectedRef);
    if (!inst) {
      return;
    }

    const nextRef = normalizeRef(el.instRefInput.value);
    if (!nextRef) {
      el.jsonFeedback.textContent = "Ref cannot be empty.";
      return;
    }
    if (nextRef !== inst.ref && instanceByRef(nextRef)) {
      el.jsonFeedback.textContent = `Ref '${nextRef}' already exists.`;
      return;
    }

    pushHistory("component-edit");
    const oldRef = inst.ref;
    updateInstance(oldRef, {
      ref: nextRef,
      placement: {
        ...inst.placement,
        x: toGrid(Number(el.xInput.value)),
        y: toGrid(Number(el.yInput.value)),
        rotation: Number(el.rotationInput.value),
        locked: el.lockedInput.checked
      },
      properties: {
        ...(inst.properties ?? {}),
        value: el.instValueInput.value,
        note: el.instNotesInput.value,
        notes: el.instNotesInput.value
      }
    });

    if (oldRef !== nextRef) {
      for (const net of state.model.nets ?? []) {
        for (const node of net.nodes ?? []) {
          if (node.ref === oldRef) {
            node.ref = nextRef;
          }
        }
      }
      if (state.selectedPin?.ref === oldRef) {
        state.selectedPin.ref = nextRef;
      }
      selectSingleRef(nextRef);
    }

    el.jsonFeedback.textContent = "Component updated.";
    queueCompile(true, "component-edit");
  });

  el.rotateSelectedBtn.addEventListener("click", () => {
    if (state.selectedRefs.length !== 1 || !state.selectedRef) {
      return;
    }
    const inst = instanceByRef(state.selectedRef);
    if (!inst) {
      return;
    }
    pushHistory("rotate");
    const current = Number(inst.placement.rotation ?? 0);
    inst.placement.rotation = ((Math.round(current / 90) * 90 + 90) % 360 + 360) % 360;
    renderSelected();
    el.jsonFeedback.textContent = "Component rotated.";
    queueCompile(true, "rotate");
  });

  el.duplicateComponentBtn.addEventListener("click", () => {
    if (state.selectedRefs.length !== 1 || !state.selectedRef || !state.model) {
      return;
    }
    const inst = instanceByRef(state.selectedRef);
    if (!inst) {
      return;
    }
    pushHistory("duplicate-component");
    const nextRef = nextRefLike(inst.ref);
    const next = clone(inst);
    next.ref = nextRef;
    next.placement = {
      ...next.placement,
      x: toGrid(Number(next.placement.x ?? 0) + GRID * 2),
      y: toGrid(Number(next.placement.y ?? 0) + GRID * 2),
      locked: false
    };
    state.model.instances.push(next);
    selectSingleRef(nextRef);
    state.selectedNet = null;
    state.selectedPin = null;
    state.isolateNet = false;
    el.jsonFeedback.textContent = `Duplicated ${inst.ref} as ${nextRef}.`;
    queueCompile(true, "duplicate-component");
  });

  el.deleteComponentBtn.addEventListener("click", () => {
    if (state.selectedRefs.length !== 1 || !state.selectedRef || !state.model) {
      return;
    }
    const ref = state.selectedRef;
    const impact = deleteComponentImpact(ref);
    const orphaned = impact.impacted.filter((x) => x.orphaned).map((x) => x.name);
    const summary =
      `Delete ${ref}?\n` +
      `- Net nodes removed: ${impact.removedNodes}\n` +
      `- Nets touched: ${impact.impacted.length}\n` +
      (orphaned.length ? `- Nets that will be removed (<2 nodes): ${orphaned.slice(0, 8).join(", ")}${orphaned.length > 8 ? " ..." : ""}\n` : "");
    if (!window.confirm(summary)) {
      return;
    }
    pushHistory("delete-component");
    state.model.instances = state.model.instances.filter((i) => i.ref !== ref);
    for (const net of state.model.nets ?? []) {
      net.nodes = (net.nodes ?? []).filter((n) => n.ref !== ref);
    }
    state.model.nets = (state.model.nets ?? []).filter((n) => (n.nodes ?? []).length >= 2);
    setSelectedRefs([]);
    state.selectedNet = null;
    state.selectedPin = null;
    state.isolateComponent = false;
    state.isolateNet = false;
    el.jsonFeedback.textContent = `Deleted component ${ref}.`;
    queueCompile(true, "delete-component");
  });

  el.isolateSelectedComponentBtn.addEventListener("click", () => {
    if (!state.selectedRef) {
      return;
    }
    state.isolateComponent = true;
    state.isolateNet = false;
    state.selectedNet = null;
    renderAll();
  });

  el.showPinNetLabelInput.addEventListener("change", () => {
    if (!state.selectedPin) {
      return;
    }
    const { ref, pin } = state.selectedPin;
    pushHistory("pin-ui");
    if (!setPinUi(ref, pin, { show_net_label: el.showPinNetLabelInput.checked })) {
      return;
    }
    el.jsonFeedback.textContent = `Updated ${ref}.${pin} label visibility.`;
    queueCompile(true, "pin-ui");
  });

  el.applyPinPropsBtn.addEventListener("click", () => {
    if (!state.selectedPin) {
      return;
    }
    const { ref, pin: oldPinName } = state.selectedPin;
    const inst = instanceByRef(ref);
    const sym = symbolForRef(ref);
    if (!inst || !sym) {
      return;
    }
    const idx = (sym.pins ?? []).findIndex((p) => p.name === oldPinName);
    if (idx < 0) {
      el.jsonFeedback.textContent = `Pin ${ref}.${oldPinName} not found on symbol.`;
      return;
    }

    const newName = String(el.pinNameInput.value ?? "").trim();
    const newNumber = String(el.pinNumberInput.value ?? "").trim() || String(idx + 1);
    const newSide = el.pinSideInput.value;
    const newType = el.pinTypeInput.value;
    const newOffset = Number(el.pinOffsetInput.value);
    if (!newName) {
      el.jsonFeedback.textContent = "Pin name cannot be empty.";
      return;
    }
    if (!PIN_SIDES.includes(newSide) || !PIN_TYPES.includes(newType) || !Number.isFinite(newOffset) || newOffset < 0) {
      el.jsonFeedback.textContent = "Invalid pin side/type/offset.";
      return;
    }
    const duplicate = (sym.pins ?? []).some((p, i) => i !== idx && p.name === newName);
    if (duplicate) {
      el.jsonFeedback.textContent = `Pin name '${newName}' already exists on symbol '${inst.symbol}'.`;
      return;
    }

    pushHistory("pin-props");
    const beforeName = sym.pins[idx].name;
    sym.pins[idx] = {
      ...sym.pins[idx],
      name: newName,
      number: newNumber,
      side: newSide,
      type: newType,
      offset: Math.round(newOffset)
    };
    renamePinAcrossSymbolInstances(inst.symbol, beforeName, newName);
    state.selectedPin = { ...state.selectedPin, pin: newName };
    el.jsonFeedback.textContent = `Updated pin ${ref}.${newName}.`;
    queueCompile(true, "pin-props");
  });

  el.connectPinBtn.addEventListener("click", () => {
    if (!state.selectedPin) {
      return;
    }
    const netName = el.pinNetSelect.value;
    if (!netName) {
      el.jsonFeedback.textContent = "Choose a net first.";
      return;
    }
    pushHistory("connect-pin");
    const out = connectPinToNet(state.selectedPin.ref, state.selectedPin.pin, netName);
    if (!out.ok) {
      el.jsonFeedback.textContent = out.message;
      return;
    }
    state.selectedNet = out.net;
    el.jsonFeedback.textContent = `Connected ${state.selectedPin.ref}.${state.selectedPin.pin} to ${out.net}.`;
    queueCompile(true, "connect-pin");
  });

  el.createConnectNetBtn.addEventListener("click", () => {
    if (!state.selectedPin) {
      return;
    }
    const name = normalizeNetName(el.newNetNameInput.value || el.newNetNameInput.placeholder || nextAutoNetName());
    const cls = el.newNetClassInput.value;
    pushHistory("create-net");
    const out = connectPinToNet(state.selectedPin.ref, state.selectedPin.pin, name, { netClass: cls });
    if (!out.ok) {
      el.jsonFeedback.textContent = out.message;
      return;
    }
    state.selectedNet = out.net;
    el.newNetNameInput.value = "";
    el.jsonFeedback.textContent = `Created and connected net ${out.net}.`;
    queueCompile(true, "create-net");
  });

  el.pinConnections.addEventListener("click", (evt) => {
    const btn = evt.target.closest("[data-disconnect-net]");
    if (!btn || !state.selectedPin) {
      return;
    }
    const netName = btn.getAttribute("data-disconnect-net");
    const impact = disconnectImpact(netName, state.selectedPin.ref, state.selectedPin.pin);
    const message = impact.orphaned
      ? `Disconnect ${state.selectedPin.ref}.${state.selectedPin.pin} from ${netName}?\nThis will leave fewer than 2 nodes and remove net '${netName}'.`
      : `Disconnect ${state.selectedPin.ref}.${state.selectedPin.pin} from ${netName}?`;
    if (!window.confirm(message)) {
      return;
    }
    pushHistory("disconnect-pin");
    const out = disconnectPinFromNet(state.selectedPin.ref, state.selectedPin.pin, netName);
    if (!out.ok) {
      el.jsonFeedback.textContent = out.message;
      return;
    }
    el.jsonFeedback.textContent = `Disconnected ${state.selectedPin.ref}.${state.selectedPin.pin} from ${netName}.`;
    queueCompile(true, "disconnect-pin");
  });

  el.updateNetBtn.addEventListener("click", () => {
    if (!state.selectedNet) {
      return;
    }
    pushHistory("net-edit");
    const oldName = state.selectedNet;
    const renamed = renameNet(oldName, el.netNameInput.value);
    if (!renamed.ok) {
      el.jsonFeedback.textContent = renamed.message;
      return;
    }
    const clsOut = setNetClass(renamed.name, el.netClassInput.value);
    if (!clsOut.ok) {
      el.jsonFeedback.textContent = clsOut.message;
      return;
    }
    state.selectedNet = renamed.name;
    el.jsonFeedback.textContent = `Updated net ${renamed.name}.`;
    queueCompile(true, "net-edit");
  });

  el.isolateSelectedNetBtn.addEventListener("click", () => {
    if (!state.selectedNet) {
      return;
    }
    state.isolateNet = true;
    state.isolateComponent = false;
    setSelectedRefs([]);
    state.selectedPin = null;
    renderAll();
  });

  el.addNetNodeBtn.addEventListener("click", () => {
    if (!state.selectedNet) {
      return;
    }
    const ref = normalizeRef(el.netNodeRefInput.value);
    const pin = String(el.netNodePinInput.value ?? "").trim();
    if (!ref || !pin) {
      el.jsonFeedback.textContent = "Provide both ref and pin.";
      return;
    }
    pushHistory("net-node-add");
    const out = connectPinToNet(ref, pin, state.selectedNet);
    if (!out.ok) {
      el.jsonFeedback.textContent = out.message;
      return;
    }
    el.netNodePinInput.value = "";
    el.jsonFeedback.textContent = `Added ${ref}.${pin} to ${state.selectedNet}.`;
    queueCompile(true, "net-node-add");
  });

  el.netNodesList.addEventListener("click", (evt) => {
    const btn = evt.target.closest("[data-remove-node]");
    if (!btn || !state.selectedNet) {
      return;
    }
    const netName = state.selectedNet;
    const [ref, ...pinParts] = String(btn.getAttribute("data-remove-node")).split(".");
    const pin = pinParts.join(".");
    const impact = disconnectImpact(netName, ref, pin);
    const message = impact.orphaned
      ? `Remove ${ref}.${pin} from ${netName}?\nThis will leave fewer than 2 nodes and remove net '${netName}'.`
      : `Remove ${ref}.${pin} from ${netName}?`;
    if (!window.confirm(message)) {
      return;
    }
    pushHistory("net-node-remove");
    const out = disconnectPinFromNet(ref, pin, netName);
    if (!out.ok) {
      el.jsonFeedback.textContent = out.message;
      return;
    }
    el.jsonFeedback.textContent = `Removed ${ref}.${pin} from ${netName}.`;
    queueCompile(true, "net-node-remove");
  });

  el.addSymbolPinBtn.addEventListener("click", () => {
    const row = {
      name: `P${el.symbolPinsList.querySelectorAll(".symbolPinRow").length + 1}`,
      number: `${el.symbolPinsList.querySelectorAll(".symbolPinRow").length + 1}`,
      side: "left",
      offset: 20,
      type: "passive"
    };
    el.symbolPinsList.insertAdjacentHTML("beforeend", symbolPinRowHtml(row));
    invalidateSymbolMigrationPreview("Pin rows changed. Preview migration again before apply.");
  });

  el.symbolPinsList.addEventListener("click", (evt) => {
    const moveBtn = evt.target.closest("[data-move-symbol-pin]");
    if (moveBtn) {
      const row = moveBtn.closest(".symbolPinRow");
      if (!row) {
        return;
      }
      const dir = moveBtn.getAttribute("data-move-symbol-pin");
      if (dir === "up" && row.previousElementSibling) {
        row.parentElement.insertBefore(row, row.previousElementSibling);
      } else if (dir === "down" && row.nextElementSibling) {
        row.parentElement.insertBefore(row.nextElementSibling, row);
      }
      invalidateSymbolMigrationPreview("Pin order changed. Preview migration again before apply.");
      return;
    }

    const btn = evt.target.closest("[data-remove-symbol-pin]");
    if (!btn) {
      return;
    }
    const row = btn.closest(".symbolPinRow");
    if (row) {
      const pinName = String(row.querySelector(".pinName")?.value ?? row.getAttribute("data-old-pin") ?? "").trim();
      const inst = state.selectedRef ? instanceByRef(state.selectedRef) : null;
      if (inst && pinName) {
        const usage = symbolPinUsage(inst.symbol, pinName);
        if (usage.nodes > 0) {
          const netSample = usage.nets.slice(0, 8).join(", ");
          const suffix = usage.nets.length > 8 ? " ..." : "";
          const prompt =
            `Remove pin row '${pinName}' from symbol '${inst.symbol}' editor?\n` +
            `Current usage: ${usage.nodes} net node(s) across ${usage.nets.length} net(s): ${netSample}${suffix}\n` +
            "These references will be dropped when you apply symbol changes.";
          if (!window.confirm(prompt)) {
            return;
          }
        }
      }
      row.remove();
      invalidateSymbolMigrationPreview("Pin rows changed. Preview migration again before apply.");
    }
  });

  el.symbolPinsList.addEventListener("input", (evt) => {
    const row = evt.target.closest(".symbolPinRow");
    if (row) {
      row.classList.remove("invalidRow");
    }
    if (el.symbolValidation.textContent) {
      el.symbolValidation.textContent = "";
      el.symbolValidation.classList.remove("symbolValidationError");
    }
    invalidateSymbolMigrationPreview("Symbol changes pending. Preview migration before apply.");
  });

  el.previewSymbolBtn.addEventListener("click", () => {
    if (!state.selectedRef) {
      return;
    }
    const draft = collectSymbolDraft(state.selectedRef);
    if (!draft.ok) {
      el.jsonFeedback.textContent = draft.message;
      el.symbolValidation.textContent = draft.message;
      el.symbolValidation.classList.add("symbolValidationError");
      invalidateSymbolMigrationPreview("");
      return;
    }
    const plan = buildSymbolMigrationPlan(draft.inst.symbol, draft.sym.pins ?? [], draft.parsedPins);
    renderSymbolMigrationPlan(plan);
    state.symbolMigrationAckHash = plan.hash;
    el.jsonFeedback.textContent = plan.hasDestructive
      ? "Migration preview acknowledged. Apply is now enabled for destructive symbol edits."
      : "Preview complete. No destructive migration detected.";
  });

  el.applySymbolBtn.addEventListener("click", () => {
    if (!state.selectedRef) {
      return;
    }
    const draft = collectSymbolDraft(state.selectedRef);
    if (!draft.ok) {
      el.jsonFeedback.textContent = draft.message;
      el.symbolValidation.textContent = draft.message;
      el.symbolValidation.classList.add("symbolValidationError");
      return;
    }
    const { inst, sym, parsedPins, nextCategory, nextWidth, nextHeight } = draft;
    const plan = buildSymbolMigrationPlan(inst.symbol, sym.pins ?? [], parsedPins);
    if (plan.hasDestructive && state.symbolMigrationAckHash !== plan.hash) {
      renderSymbolMigrationPlan(plan);
      el.jsonFeedback.textContent = "Destructive symbol edit detected. Click 'Preview Migration' before applying.";
      el.symbolValidation.textContent = "Preview migration is required for pin removals or dropped net mappings.";
      el.symbolValidation.classList.add("symbolValidationError");
      return;
    }

    pushHistory("symbol-edit");
    const beforePins = new Set((sym.pins ?? []).map((p) => p.name));
    for (const entry of parsedPins) {
      if (entry.oldName && entry.oldName !== entry.pin.name) {
        renamePinAcrossSymbolInstances(inst.symbol, entry.oldName, entry.pin.name);
      }
    }
    sym.category = nextCategory;
    sym.body = {
      ...(sym.body ?? {}),
      width: nextWidth,
      height: nextHeight
    };
    sym.pins = parsedPins.map((p) => p.pin);
    const allowedPins = new Set(sym.pins.map((p) => p.name));
    const refs = new Set((state.model.instances ?? []).filter((i) => i.symbol === inst.symbol).map((i) => i.ref));
    for (const ii of state.model.instances ?? []) {
      if (!refs.has(ii.ref)) {
        continue;
      }
      const pinUi = ii.properties?.pin_ui;
      if (!pinUi || typeof pinUi !== "object" || Array.isArray(pinUi)) {
        continue;
      }
      for (const key of Object.keys(pinUi)) {
        if (!allowedPins.has(key)) {
          delete pinUi[key];
        }
      }
    }
    for (const net of state.model.nets ?? []) {
      net.nodes = (net.nodes ?? []).filter((node) => !refs.has(node.ref) || allowedPins.has(node.pin));
    }
    state.model.nets = (state.model.nets ?? []).filter((net) => (net.nodes ?? []).length >= 2);

    if (state.selectedPin && !pinExists(state.selectedPin.ref, state.selectedPin.pin)) {
      state.selectedPin = null;
    }
    el.symbolValidation.textContent = "";
    el.symbolValidation.classList.remove("symbolValidationError");
    invalidateSymbolMigrationPreview("");
    const removedPinCount = [...beforePins].filter((p) => !allowedPins.has(p)).length;
    el.jsonFeedback.textContent = removedPinCount
      ? `Updated symbol ${inst.symbol}. Removed ${removedPinCount} pin mappings from nets/UI metadata.`
      : `Updated symbol ${inst.symbol}.`;
    queueCompile(true, "symbol-edit");
  });

  [el.symbolCategoryInput, el.symbolWidthInput, el.symbolHeightInput].forEach((input) => {
    input?.addEventListener("input", () => {
      invalidateSymbolMigrationPreview("Symbol changes pending. Preview migration before apply.");
    });
  });

  el.zoomInBtn.addEventListener("click", () => {
    state.scale = Math.min(MAX_SCALE, state.scale + 0.1);
    state.userAdjustedView = true;
    updateTransform();
  });

  el.zoomOutBtn.addEventListener("click", () => {
    state.scale = Math.max(MIN_SCALE, state.scale - 0.1);
    state.userAdjustedView = true;
    updateTransform();
  });

  el.zoomResetBtn.addEventListener("click", () => {
    if (state.compile?.layout) {
      fitView(state.compile.layout);
    } else {
      state.scale = 1;
      state.panX = 40;
      state.panY = 40;
      state.userAdjustedView = false;
      updateTransform();
    }
  });

  el.fitViewBtn.addEventListener("click", () => {
    if (state.compile?.layout) {
      fitView(state.compile.layout);
    }
  });

  el.focusSelectionBtn.addEventListener("click", () => {
    focusSelection();
  });

  el.showLabelsInput.addEventListener("change", () => {
    state.showLabels = el.showLabelsInput.checked;
    setLabelLayerVisibility();
  });

  el.renderModeSelect.addEventListener("change", async () => {
    state.renderMode = el.renderModeSelect.value;
    if (state.model) {
      await compileModel(state.model, { keepView: true, preservePlacement: true });
    }
  });

  el.isolateNetBtn.addEventListener("click", () => {
    state.isolateNet = !state.isolateNet;
    renderAll();
  });

  el.isolateComponentBtn.addEventListener("click", () => {
    state.isolateComponent = !state.isolateComponent;
    renderAll();
  });

  el.canvasViewport.addEventListener(
    "wheel",
    (evt) => {
      evt.preventDefault();
      const oldScale = state.scale;
      state.scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, state.scale + (evt.deltaY < 0 ? 0.08 : -0.08)));

      const rect = el.canvasViewport.getBoundingClientRect();
      const px = evt.clientX - rect.left;
      const py = evt.clientY - rect.top;

      state.panX = px - (px - state.panX) * (state.scale / oldScale);
      state.panY = py - (py - state.panY) * (state.scale / oldScale);
      state.userAdjustedView = true;
      updateTransform();
    },
    { passive: false }
  );

  el.canvasViewport.addEventListener("pointerdown", (evt) => {
    const interactive = evt.target.closest(
      "[data-ref], [data-net], [data-net-label], [data-net-junction], [data-net-tie], [data-pin-ref]"
    );

    if (!interactive && evt.button === 0 && !state.spacePan) {
      beginBoxSelection(evt.clientX, evt.clientY);
      return;
    }

    const allowPan = evt.button === 1 || (evt.button === 0 && state.spacePan);
    if (!allowPan) {
      return;
    }

    if (state.draggingComponentRef) {
      return;
    }

    state.isPanning = true;
    state.panStartX = evt.clientX;
    state.panStartY = evt.clientY;
    state.basePanX = state.panX;
    state.basePanY = state.panY;
    el.canvasViewport.classList.add("dragging");
  });

  el.canvasViewport.addEventListener("click", (evt) => {
    if (state.suppressCanvasClick) {
      state.suppressCanvasClick = false;
      return;
    }

    const interactive = evt.target.closest(
      "[data-ref], [data-net], [data-net-label], [data-net-junction], [data-net-tie], [data-pin-ref]"
    );
    if (interactive) {
      return;
    }

    const hadSelection = Boolean(state.selectedRefs.length || state.selectedNet || state.selectedPin);
    const hadIsolation = Boolean(state.isolateNet || state.isolateComponent);
    if (!hadSelection && !hadIsolation) {
      return;
    }

    setSelectedRefs([]);
    state.selectedNet = null;
    state.selectedPin = null;
    state.isolateNet = false;
    state.isolateComponent = false;
    renderAll();
  });

  window.addEventListener("pointermove", (evt) => {
    if (state.boxSelecting) {
      updateBoxSelection(evt.clientX, evt.clientY);
      return;
    }

    if (state.draggingComponentRef && state.dragComponent && state.model) {
      if (state.dragPointerId != null && evt.pointerId !== state.dragPointerId) {
        return;
      }

      const pt = canvasToSvgPoint(evt.clientX, evt.clientY);
      const dx = pt.x - state.dragComponent.startPointerX;
      const dy = pt.y - state.dragComponent.startPointerY;
      setSelectedRefs(state.dragComponent.refs ?? [state.draggingComponentRef]);
      state.selectedPin = null;
      for (const ref of state.dragComponent.refs ?? []) {
        const base = state.dragComponent.baseByRef?.[ref];
        if (!base) {
          continue;
        }
        const nextX = toGrid(base.x + dx);
        const nextY = toGrid(base.y + dy);
        const moveX = nextX - base.x;
        const moveY = nextY - base.y;
        state.dragComponent.pendingByRef[ref] = { x: nextX, y: nextY };
        if (moveX !== 0 || moveY !== 0) {
          state.dragMoved = true;
        }
        const n = el.canvasInner.querySelector(`[data-ref="${ref}"]`);
        if (n) {
          n.setAttribute("transform", `translate(${moveX} ${moveY})`);
        }
      }
      const primary = state.draggingComponentRef;
      const p = state.dragComponent.pendingByRef?.[primary];
      if (p) {
        el.xInput.value = String(p.x);
        el.yInput.value = String(p.y);
      }
      return;
    }

    if (!state.isPanning) {
      return;
    }

    state.panX = state.basePanX + (evt.clientX - state.panStartX);
    state.panY = state.basePanY + (evt.clientY - state.panStartY);
    state.userAdjustedView = true;
    updateTransform();
  });

  window.addEventListener("pointerup", async (evt) => {
    if (state.dragPointerId != null && evt.pointerId !== state.dragPointerId) {
      return;
    }

    finishBoxSelection();

    state.isPanning = false;
    el.canvasViewport.classList.remove("dragging");

    const moved = state.dragMoved;
    const wasDragging = Boolean(state.draggingComponentRef);
    const dragSnapshot = state.dragComponent ? clone(state.dragComponent.pendingByRef ?? {}) : null;

    clearDragPreview();
    state.draggingComponentRef = null;
    state.dragPointerId = null;
    state.dragComponent = null;
    state.dragMoved = false;

    if (wasDragging && moved && state.model) {
      pushHistory("drag-move");
      for (const [ref, pos] of Object.entries(dragSnapshot ?? {})) {
        const inst = state.model.instances.find((x) => x.ref === ref);
        if (inst) {
          inst.placement.x = pos.x;
          inst.placement.y = pos.y;
        }
      }
      await compileModel(state.model, { source: "drag", keepView: true, preservePlacement: true });
    }
  });

  window.addEventListener("pointercancel", () => {
    finishBoxSelection();
    clearDragPreview();
    state.draggingComponentRef = null;
    state.dragPointerId = null;
    state.dragComponent = null;
    state.dragMoved = false;
    state.isPanning = false;
    el.canvasViewport.classList.remove("dragging");
  });

  window.addEventListener("keydown", (evt) => {
    const mod = evt.ctrlKey || evt.metaKey;
    if (mod && evt.key.toLowerCase() === "z") {
      evt.preventDefault();
      if (evt.shiftKey) {
        void performRedo();
      } else {
        void performUndo();
      }
      return;
    }
    if (mod && evt.key.toLowerCase() === "y") {
      evt.preventDefault();
      void performRedo();
      return;
    }

    if (evt.code === "Space") {
      if (isTypingContext(evt.target)) {
        return;
      }

      if (state.selectedRefs.length && state.model && !evt.repeat) {
        pushHistory("rotate-hotkey");
        for (const ref of state.selectedRefs) {
          const inst = state.model.instances.find((x) => x.ref === ref);
          if (!inst) {
            continue;
          }
          const current = Number(inst.placement.rotation ?? 0);
          inst.placement.rotation = ((Math.round(current / 90) * 90 + 90) % 360 + 360) % 360;
        }
        compileModel(state.model, { source: "rotate", keepView: true, preservePlacement: true });
        evt.preventDefault();
        return;
      }

      state.spacePan = true;
      el.canvasViewport.classList.add("dragging");
      evt.preventDefault();
    }

    if (evt.altKey && evt.key === "Enter") {
      evt.preventDefault();
      if (state.selectedPin) {
        el.applyPinPropsBtn.click();
        return;
      }
      if (state.selectedNet) {
        el.updateNetBtn.click();
        return;
      }
      if (state.selectedRefs.length === 1 && state.selectedRef) {
        el.updatePlacementBtn.click();
        return;
      }
    }

    if (evt.altKey && evt.key.toLowerCase() === "c" && state.selectedPin) {
      evt.preventDefault();
      el.connectPinBtn.click();
      return;
    }

    if (mod && evt.key.toLowerCase() === "k") {
      evt.preventDefault();
      if (el.commandModal?.classList.contains("hidden")) {
        openCommandModal();
      } else {
        closeCommandModal();
      }
      return;
    }

    if (!mod && evt.shiftKey && evt.key === "?") {
      if (isTypingContext(evt.target)) {
        return;
      }
      evt.preventDefault();
      openShortcutsModal();
      return;
    }

    if (!mod && !evt.altKey && !evt.shiftKey && evt.key.toLowerCase() === "f") {
      if (isTypingContext(evt.target)) {
        return;
      }
      evt.preventDefault();
      focusSelection();
    }
  });

  window.addEventListener("resize", () => {
    if (!state.compile?.layout || state.userAdjustedView) {
      return;
    }
    fitView(state.compile.layout);
  });

  window.addEventListener("keyup", (evt) => {
    if (evt.code === "Space") {
      state.spacePan = false;
      if (!state.isPanning) {
        el.canvasViewport.classList.remove("dragging");
      }
    }
    if (evt.code === "Escape") {
      if (!el.commandModal.classList.contains("hidden")) {
        closeCommandModal();
        return;
      }
      if (!el.shortcutsModal.classList.contains("hidden")) {
        closeShortcutsModal();
        return;
      }
      if (!el.schemaModal.classList.contains("hidden")) {
        closeSchemaModal();
        return;
      }
      const hadSelection = Boolean(state.selectedRefs.length || state.selectedNet || state.selectedPin);
      const hadIsolation = Boolean(state.isolateNet || state.isolateComponent);
      if (hadSelection || hadIsolation) {
        setSelectedRefs([]);
        state.selectedNet = null;
        state.selectedPin = null;
        state.isolateNet = false;
        state.isolateComponent = false;
        renderAll();
      }
    }
  });

  el.showSchemaBtn.addEventListener("click", openSchemaModal);
  el.closeSchemaBtn.addEventListener("click", closeSchemaModal);
  el.schemaModal.addEventListener("click", (evt) => {
    if (evt.target === el.schemaModal) {
      closeSchemaModal();
    }
  });

  el.copySchemaBtn.addEventListener("click", async () => {
    try {
      const text = el.schemaViewer.value || (await loadSchemaText());
      await navigator.clipboard.writeText(text);
      el.jsonFeedback.textContent = "Schema copied.";
    } catch (err) {
      el.jsonFeedback.textContent = `Schema copy failed: ${err.message}`;
    }
  });

  el.downloadSchemaBtn.addEventListener("click", async () => {
    try {
      const text = el.schemaViewer.value || (await loadSchemaText());
      const blob = new Blob([text], { type: "application/json" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = "schemeta.schema.json";
      document.body.appendChild(a);
      a.click();
      URL.revokeObjectURL(a.href);
      document.body.removeChild(a);
      el.jsonFeedback.textContent = "Schema downloaded.";
    } catch (err) {
      el.jsonFeedback.textContent = `Schema download failed: ${err.message}`;
    }
  });

  el.shortcutsBtn?.addEventListener("click", openShortcutsModal);
  el.closeShortcutsBtn?.addEventListener("click", closeShortcutsModal);
  el.shortcutsModal?.addEventListener("click", (evt) => {
    if (evt.target === el.shortcutsModal) {
      closeShortcutsModal();
    }
  });

  el.closeCommandBtn?.addEventListener("click", closeCommandModal);
  el.commandModal?.addEventListener("click", (evt) => {
    if (evt.target === el.commandModal) {
      closeCommandModal();
    }
  });
  el.commandInput?.addEventListener("input", () => {
    state.commandIndex = 0;
    renderCommandList();
  });
  el.commandInput?.addEventListener("keydown", (evt) => {
    const cmds = renderCommandList();
    if (!cmds.length) {
      return;
    }
    if (evt.key === "ArrowDown") {
      evt.preventDefault();
      state.commandIndex = Math.min(cmds.length - 1, state.commandIndex + 1);
      renderCommandList();
      return;
    }
    if (evt.key === "ArrowUp") {
      evt.preventDefault();
      state.commandIndex = Math.max(0, state.commandIndex - 1);
      renderCommandList();
      return;
    }
    if (evt.key === "Enter") {
      evt.preventDefault();
      const cmd = cmds[state.commandIndex];
      if (cmd) {
        runCommandById(cmd.id);
      }
    }
  });
  el.commandList?.addEventListener("click", (evt) => {
    const row = evt.target.closest("[data-command-id]");
    if (!row) {
      return;
    }
    runCommandById(row.getAttribute("data-command-id"));
  });

  el.validateJsonBtn.addEventListener("click", validateJsonEditor);

  el.formatJsonBtn.addEventListener("click", () => {
    try {
      const parsed = JSON.parse(el.jsonEditor.value);
      el.jsonEditor.value = JSON.stringify(parsed, null, 2);
      el.jsonFeedback.textContent = "JSON formatted.";
    } catch (err) {
      const p = parseJsonPositionError(el.jsonEditor.value, err);
      el.jsonFeedback.textContent = `Format failed: ${p.message}`;
    }
  });

  el.sortJsonBtn.addEventListener("click", () => {
    try {
      const parsed = JSON.parse(el.jsonEditor.value);
      el.jsonEditor.value = JSON.stringify(sortKeysDeep(parsed), null, 2);
      el.jsonFeedback.textContent = "Keys sorted recursively.";
    } catch (err) {
      const p = parseJsonPositionError(el.jsonEditor.value, err);
      el.jsonFeedback.textContent = `Sort failed: ${p.message}`;
    }
  });

  el.copyReproBtn.addEventListener("click", async () => {
    try {
      const model = JSON.parse(el.jsonEditor.value);
      const repro = buildMinimalRepro(model);
      await navigator.clipboard.writeText(JSON.stringify(repro, null, 2));
      el.jsonFeedback.textContent = "Minimal repro JSON copied.";
    } catch (err) {
      el.jsonFeedback.textContent = `Copy repro failed: ${err.message}`;
    }
  });

  el.applyJsonBtn.addEventListener("click", async () => {
    try {
      const parsed = JSON.parse(el.jsonEditor.value);
      const analyzed = await analyzeModelForImport(parsed, "Apply JSON");
      el.jsonFeedback.textContent = analyzed.message;
      if (!analyzed.ok) {
        return;
      }
      const before = state.model ? clone(state.model) : null;
      if (state.model) {
        pushHistory("apply-json");
      }
      el.jsonFeedback.textContent = "Applying JSON...";
      setSelectedRefs([]);
      state.selectedNet = null;
      state.selectedPin = null;
      await compileModel(parsed, { fit: true });
      el.jsonFeedback.textContent = `${summarizeModelDelta(before, state.model)} ${analyzed.out.warnings.length ? `Validation warnings: ${analyzed.out.warnings.length}.` : "Validation clean."}`;
    } catch (err) {
      const p = parseJsonPositionError(el.jsonEditor.value, err);
      el.jsonFeedback.textContent = `Apply failed: ${p.message}`;
    }
  });

  el.newProjectBtn.addEventListener("click", async () => {
    if (state.model) {
      pushHistory("new-project");
    }
    setSelectedRefs([]);
    state.selectedNet = null;
    state.selectedPin = null;
    await compileModel(defaultProject(), { fit: true });
  });

  el.loadSampleBtn.addEventListener("click", loadSample);
  el.resetSampleBtn.addEventListener("click", async () => {
    try {
      await resetToSample({ pushHistory: true });
    } catch (err) {
      setStatus(`Reset failed: ${err.message}`, false);
    }
  });

  el.autoLayoutBtn.addEventListener("click", async () => {
    await runLayoutAction("/layout/auto");
  });

  el.autoTidyBtn.addEventListener("click", async () => {
    await runLayoutAction("/layout/tidy");
  });

  el.unlockAllBtn?.addEventListener("click", async () => {
    if (!state.model) {
      return;
    }
    pushHistory("unlock-all");
    let changed = 0;
    for (const inst of state.model.instances ?? []) {
      if (inst?.placement?.locked) {
        inst.placement.locked = false;
        changed += 1;
      }
    }
    if (!changed) {
      el.jsonFeedback.textContent = "All components are already unlocked.";
      return;
    }
    el.jsonFeedback.textContent = `Unlocked ${changed} component${changed === 1 ? "" : "s"}.`;
    await compileModel(state.model, { keepView: true, source: "unlock-all", preservePlacement: true });
  });

  el.themeToggleBtn?.addEventListener("click", () => {
    applyTheme(state.theme === "dark" ? "light" : "dark");
  });

  el.undoBtn.addEventListener("click", async () => {
    await performUndo();
  });

  el.redoBtn.addEventListener("click", async () => {
    await performRedo();
  });

  el.importBtn.addEventListener("click", () => {
    el.fileInput.click();
  });

  el.fileInput.addEventListener("change", async (evt) => {
    const file = evt.target.files?.[0];
    if (!file) {
      return;
    }

    try {
      const content = await file.text();
      const parsed = JSON.parse(content);
      const analyzed = await analyzeModelForImport(parsed, "Import JSON");
      if (!analyzed.ok) {
        el.jsonFeedback.textContent = analyzed.message;
        return;
      }
      const before = state.model ? clone(state.model) : null;
      if (state.model) {
        pushHistory("import-json");
      }
      setSelectedRefs([]);
      state.selectedNet = null;
      state.selectedPin = null;
      await compileModel(parsed, { fit: true });
      el.jsonFeedback.textContent = `${summarizeModelDelta(before, state.model)} Imported from file (${analyzed.out.warnings.length} warnings).`;
    } catch (err) {
      setStatus(`Import failed: ${err.message}`, false);
    }

    el.fileInput.value = "";
  });

  el.exportBtn.addEventListener("click", () => {
    if (!state.model) {
      return;
    }

    const blob = new Blob([JSON.stringify(state.model, null, 2)], { type: "application/json" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `${(state.model.meta?.title || "schemeta").toString().replace(/\s+/g, "_").toLowerCase()}.schemeta.json`;
    document.body.appendChild(a);
    a.click();
    URL.revokeObjectURL(a.href);
    document.body.removeChild(a);
  });
}

(async function init() {
  el.selectedNoteHint = document.createElement("div");
  el.selectedNoteHint.className = "selectedNoteHint hidden";
  el.canvasViewport.appendChild(el.selectedNoteHint);
  applyTheme(selectThemePreference());
  setupEvents();
  loadInspectorSectionState();
  updateTransform();

  const snapshots = JSON.parse(localStorage.getItem(SNAPSHOTS_KEY) ?? "[]");
  if (snapshots.length) {
    await compileModel(snapshots[0].model, { fit: true });
  } else {
    await loadSample();
  }
})();
