import { resolveElkRuntime } from "./layout-elk.js";

const GRID = 20;
const MARGIN_X = 140;
const MARGIN_Y = 140;
const COLUMN_GAP = 320;
const ROW_GAP = 190;
const OBSTACLE_PADDING = 14;
const CONNECTIVITY_COMPACT_PASSES = 8;
const CONNECTIVITY_MOVE_LIMIT = GRID * 6;

const NET_CLASS_PRIORITY = {
  power: 0,
  ground: 1,
  clock: 2,
  signal: 3,
  analog: 4,
  bus: 5,
  differential: 6
};

const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]);
const DEFAULT_RENDER_MODE = "schematic_stub";
export const DEFAULT_LAYOUT_ENGINE = "schemeta-v2";
const ROTATION_STEPS = [0, 90, 180, 270];
const MIN_CHANNEL_SPACING_STEPS = 3;
const LANE_ORDER = ["power", "clock", "signal", "analog", "ground", "bus", "differential"];

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

function pointKey(p) {
  return `${p.x},${p.y}`;
}

function edgeKey(a, b) {
  const ka = pointKey(a);
  const kb = pointKey(b);
  return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
}

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

function pinTypeFor(model, ref, pinName) {
  const inst = model.instances.find((x) => x.ref === ref);
  if (!inst) {
    return "passive";
  }
  const sym = model.symbols[inst.symbol];
  const pin = sym?.pins.find((p) => p.name === pinName);
  return pin?.type ?? "passive";
}

function pinPoint(inst, pin, width, height) {
  const x0 = inst.placement.x;
  const y0 = inst.placement.y;
  const rotation = normalizeRotation(inst.placement.rotation ?? 0);
  let base = { x: x0, y: y0 };

  switch (pin.side) {
    case "left":
      base = { x: x0, y: y0 + pin.offset };
      break;
    case "right":
      base = { x: x0 + width, y: y0 + pin.offset };
      break;
    case "top":
      base = { x: x0 + pin.offset, y: y0 };
      break;
    case "bottom":
      base = { x: x0 + pin.offset, y: y0 + height };
      break;
    default:
      base = { x: x0, y: y0 };
  }

  if (!rotation) {
    return base;
  }

  const cx = x0 + width / 2;
  const cy = y0 + height / 2;
  return rotatePoint(base, { x: cx, y: cy }, rotation);
}

function normalizeRotation(value) {
  const n = Number(value ?? 0);
  if (!Number.isFinite(n)) {
    return 0;
  }
  const snapped = Math.round(n / 90) * 90;
  let rot = snapped % 360;
  if (rot < 0) {
    rot += 360;
  }
  return rot;
}

function rotatePoint(point, center, rotation) {
  const rad = (rotation * Math.PI) / 180;
  const cos = Math.round(Math.cos(rad));
  const sin = Math.round(Math.sin(rad));
  const dx = point.x - center.x;
  const dy = point.y - center.y;
  return {
    x: Math.round(center.x + dx * cos - dy * sin),
    y: Math.round(center.y + dx * sin + dy * cos)
  };
}

function rotateSide(side, rotation) {
  const steps = normalizeRotation(rotation) / 90;
  const order = ["top", "right", "bottom", "left"];
  const idx = order.indexOf(side);
  if (idx < 0) {
    return side;
  }
  return order[(idx + steps) % 4];
}

function hasValidPlacement(inst) {
  return Number.isFinite(inst?.placement?.x) && Number.isFinite(inst?.placement?.y);
}

function preservePlacedInstances(model, options = {}) {
  const respectLocks = options.respectLocks ?? true;
  const autoRotate = options.autoRotate ?? false;
  const fallback = placeInstances(model, { respectLocks, autoRotate });
  const fallbackByRef = new Map((fallback?.placed ?? []).map((inst) => [inst.ref, inst]));
  const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref));

  return instances.map((inst) => {
    if (hasValidPlacement(inst)) {
      return {
        ...inst,
        placement: {
          x: toGrid(Number(inst.placement.x)),
          y: toGrid(Number(inst.placement.y)),
          rotation: normalizeRotation(inst.placement.rotation ?? 0),
          locked: respectLocks ? Boolean(inst.placement.locked) : false
        }
      };
    }
    return fallbackByRef.get(inst.ref) ?? {
      ...inst,
      placement: {
        x: toGrid(MARGIN_X),
        y: toGrid(MARGIN_Y),
        rotation: normalizeRotation(inst.placement?.rotation ?? 0),
        locked: respectLocks ? Boolean(inst.placement?.locked) : false
      }
    };
  });
}

function getNodePin(model, placedMap, node) {
  const inst = placedMap.get(node.ref);
  if (!inst) {
    return null;
  }

  const sym = model.symbols[inst.symbol];
  const pin = sym.pins.find((x) => x.name === node.pin);
  if (!pin) {
    return null;
  }

  const point = pinPoint(inst, pin, sym.body.width, sym.body.height);
  const exit = {
    left: { x: point.x - GRID, y: point.y },
    right: { x: point.x + GRID, y: point.y },
    top: { x: point.x, y: point.y - GRID },
    bottom: { x: point.x, y: point.y + GRID }
  }[rotateSide(pin.side, inst.placement.rotation ?? 0)];

  return {
    ref: node.ref,
    pin: node.pin,
    pinType: pin.type,
    side: pin.side,
    point,
    exit: { x: toGrid(exit.x), y: toGrid(exit.y) }
  };
}

function buildDirectedEdges(model) {
  const edges = [];

  for (const net of model.nets) {
    const sources = net.nodes.filter((n) => {
      const t = pinTypeFor(model, n.ref, n.pin);
      return t === "output" || t === "power_out";
    });

    const sinks = net.nodes.filter((n) => {
      const t = pinTypeFor(model, n.ref, n.pin);
      return t === "input" || t === "power_in" || t === "analog" || t === "bidirectional";
    });

    for (const s of sources) {
      for (const d of sinks) {
        if (s.ref !== d.ref) {
          edges.push([s.ref, d.ref]);
        }
      }
    }
  }

  const dedup = new Map();
  for (const [a, b] of edges) {
    dedup.set(`${a}->${b}`, [a, b]);
  }

  return [...dedup.values()];
}

function isLikelySourceSymbol(sym) {
  const category = String(sym?.category ?? "").toLowerCase();
  return (
    category.includes("power") ||
    category.includes("mcu") ||
    category.includes("microcontroller") ||
    category.includes("processor") ||
    category.includes("clock")
  );
}

function fallbackUndirectedRanks(model, directedRank) {
  const refs = model.instances.map((x) => x.ref).sort();
  if (!refs.length) {
    return directedRank;
  }

  const graph = connectivityGraph(model);
  const degree = new Map(refs.map((ref) => [ref, 0]));
  for (const ref of refs) {
    let sum = 0;
    for (const w of graph.get(ref)?.values() ?? []) {
      sum += w;
    }
    degree.set(ref, sum);
  }

  const seeds = [];
  for (const inst of model.instances) {
    const sym = model.symbols[inst.symbol];
    if (!sym) {
      continue;
    }
    if (sym.pins.some((p) => p.type === "power_out" || p.type === "output")) {
      seeds.push(inst.ref);
      continue;
    }
    if (isLikelySourceSymbol(sym)) {
      seeds.push(inst.ref);
    }
  }

  if (!seeds.length) {
    const strongest = refs
      .slice()
      .sort((a, b) => (degree.get(b) ?? 0) - (degree.get(a) ?? 0) || a.localeCompare(b))[0];
    if (strongest) {
      seeds.push(strongest);
    }
  }

  const dist = new Map(refs.map((ref) => [ref, Number.POSITIVE_INFINITY]));
  const queue = [];
  for (const s of seeds) {
    dist.set(s, 0);
    queue.push(s);
  }

  while (queue.length) {
    const ref = queue.shift();
    const base = dist.get(ref) ?? Number.POSITIVE_INFINITY;
    for (const [nbr, weight] of graph.get(ref)?.entries() ?? []) {
      const step = Math.max(0.35, 1.4 - Math.min(1.1, weight * 0.55));
      const next = base + step;
      if (next + 1e-6 < (dist.get(nbr) ?? Number.POSITIVE_INFINITY)) {
        dist.set(nbr, next);
        queue.push(nbr);
      }
    }
  }

  let minFinite = Number.POSITIVE_INFINITY;
  let maxFinite = 0;
  for (const d of dist.values()) {
    if (!Number.isFinite(d)) {
      continue;
    }
    minFinite = Math.min(minFinite, d);
    maxFinite = Math.max(maxFinite, d);
  }

  const normalized = new Map(directedRank);
  for (const ref of refs) {
    const d = dist.get(ref);
    if (!Number.isFinite(d)) {
      continue;
    }
    const shifted = d - (Number.isFinite(minFinite) ? minFinite : 0);
    const fallbackRank = Math.max(0, Math.round(shifted * 1.6));
    const directed = directedRank.get(ref) ?? 1;
    normalized.set(ref, Math.max(directed, fallbackRank));
  }

  if (maxFinite - minFinite < 0.6) {
    refs
      .slice()
      .sort((a, b) => {
        const da = degree.get(a) ?? 0;
        const db = degree.get(b) ?? 0;
        if (da !== db) {
          return db - da;
        }
        return a.localeCompare(b);
      })
      .forEach((ref, idx) => {
        const directed = normalized.get(ref) ?? 1;
        normalized.set(ref, Math.max(directed, Math.floor(idx / 3)));
      });
  }

  return normalized;
}

function computeRanks(model) {
  const refs = model.instances.map((x) => x.ref).sort();
  const rank = new Map(refs.map((r) => [r, 1]));

  const powerRefs = new Set(
    model.instances
      .filter((inst) => {
        const sym = model.symbols[inst.symbol];
        return (
          sym.category.toLowerCase().includes("power") ||
          sym.pins.some((p) => p.type === "power_out")
        );
      })
      .map((x) => x.ref)
  );

  for (const r of powerRefs) {
    rank.set(r, 0);
  }

  const edges = buildDirectedEdges(model);

  for (let i = 0; i < refs.length; i += 1) {
    let changed = false;
    for (const [from, to] of edges) {
      const next = (rank.get(from) ?? 1) + 1;
      if (!powerRefs.has(to) && next > (rank.get(to) ?? 1)) {
        rank.set(to, next);
        changed = true;
      }
    }
    if (!changed) {
      break;
    }
  }

  for (const r of powerRefs) {
    rank.set(r, 0);
  }

  const uniqueDirectedRanks = new Set(refs.map((ref) => rank.get(ref) ?? 1));
  const needsFallback =
    edges.length < Math.max(2, Math.floor(refs.length * 0.25)) ||
    uniqueDirectedRanks.size <= 2;
  const ranked = needsFallback ? fallbackUndirectedRanks(model, rank) : rank;

  return { rank: ranked, edges, powerRefs };
}

function computeBaryOrder(columns, edges) {
  const predecessors = new Map();
  for (const [a, b] of edges) {
    const list = predecessors.get(b) ?? [];
    list.push(a);
    predecessors.set(b, list);
  }

  const orderByRef = new Map();
  for (const refs of columns.values()) {
    refs.forEach((ref, idx) => orderByRef.set(ref, idx));
  }

  const sortedColumns = [...columns.entries()].sort((a, b) => Number(a[0]) - Number(b[0]));
  for (const [, refs] of sortedColumns) {
    refs.sort((a, b) => {
      const pa = predecessors.get(a) ?? [];
      const pb = predecessors.get(b) ?? [];
      const ba = pa.length
        ? pa.reduce((sum, r) => sum + (orderByRef.get(r) ?? 0), 0) / pa.length
        : Number.MAX_SAFE_INTEGER;
      const bb = pb.length
        ? pb.reduce((sum, r) => sum + (orderByRef.get(r) ?? 0), 0) / pb.length
        : Number.MAX_SAFE_INTEGER;

      if (ba !== bb) {
        return ba - bb;
      }
      return a.localeCompare(b);
    });

    refs.forEach((ref, idx) => orderByRef.set(ref, idx));
  }
}

function applyAlignmentConstraints(placedMap, constraints) {
  const alignments = constraints?.alignment ?? [];
  for (const rule of alignments) {
    const left = placedMap.get(rule.left_of);
    const right = placedMap.get(rule.right_of);
    if (!left || !right) {
      continue;
    }

    const targetRightX = left.placement.x + COLUMN_GAP;
    if (right.placement.x < targetRightX) {
      right.placement.x = toGrid(targetRightX);
    }
  }
}

function applyNearConstraints(model, placedMap, constraints) {
  const rules = constraints?.near ?? [];
  for (const rule of rules) {
    const comp = placedMap.get(rule.component);
    const target = placedMap.get(rule.target_pin?.ref);
    if (!comp || !target || comp.placement.locked) {
      continue;
    }

    const targetSym = model.symbols[target.symbol];
    const targetPin = targetSym.pins.find((p) => p.name === rule.target_pin.pin);
    if (!targetPin) {
      continue;
    }

    const tp = pinPoint(target, targetPin, targetSym.body.width, targetSym.body.height);
    comp.placement.x = toGrid(tp.x + GRID * 4);
    comp.placement.y = toGrid(tp.y - GRID * 3);
  }
}

function buildInstanceMap(instances) {
  return new Map(instances.map((inst) => [inst.ref, inst]));
}

function buildConstraintGroups(model, rank) {
  const allRefs = new Set(model.instances.map((i) => i.ref));
  const consumed = new Set();
  const out = [];

  for (const g of model.constraints?.groups ?? []) {
    const members = (g.members ?? []).filter((ref) => allRefs.has(ref) && !consumed.has(ref));
    if (!members.length) {
      continue;
    }
    for (const ref of members) {
      consumed.add(ref);
    }
    out.push({
      name: g.name ?? `group_${out.length + 1}`,
      members,
      synthetic: false
    });
  }

  const leftovers = model.instances
    .map((i) => i.ref)
    .filter((ref) => !consumed.has(ref))
    .sort((a, b) => {
      const ra = rank.get(a) ?? 1;
      const rb = rank.get(b) ?? 1;
      if (ra !== rb) {
        return ra - rb;
      }
      return a.localeCompare(b);
    });

  const autoGroups = autoClusterLeftovers(model, leftovers, rank);
  for (const g of autoGroups) {
    out.push(g);
  }

  return out;
}

function autoClusterLeftovers(model, leftovers, rank) {
  if (!leftovers.length) {
    return [];
  }
  const graph = connectivityGraph(model);
  const leftoverSet = new Set(leftovers);
  const visited = new Set();
  const clusters = [];
  const EDGE_THRESHOLD = 0.35;

  for (const start of leftovers) {
    if (visited.has(start)) {
      continue;
    }
    const queue = [start];
    visited.add(start);
    const comp = [];
    while (queue.length) {
      const ref = queue.shift();
      comp.push(ref);
      for (const [nbr, w] of graph.get(ref)?.entries() ?? []) {
        if (!leftoverSet.has(nbr) || visited.has(nbr)) {
          continue;
        }
        if (w < EDGE_THRESHOLD) {
          continue;
        }
        visited.add(nbr);
        queue.push(nbr);
      }
    }
    clusters.push(comp);
  }

  clusters.sort((a, b) => {
    const ar = Math.min(...a.map((r) => rank.get(r) ?? 1));
    const br = Math.min(...b.map((r) => rank.get(r) ?? 1));
    if (ar !== br) {
      return ar - br;
    }
    return a[0].localeCompare(b[0]);
  });

  return clusters.map((members, idx) => {
    const sorted = [...members].sort((a, b) => {
      const ra = rank.get(a) ?? 1;
      const rb = rank.get(b) ?? 1;
      if (ra !== rb) {
        return ra - rb;
      }
      return a.localeCompare(b);
    });
    if (sorted.length === 1) {
      return { name: `solo_${sorted[0]}`, members: sorted, synthetic: true };
    }
    return { name: `auto_cluster_${idx + 1}`, members: sorted, synthetic: true };
  });
}

function rankColumnsForRefs(refs, rank) {
  const minRank = Math.min(...refs.map((r) => rank.get(r) ?? 1));
  const cols = new Map();

  for (const ref of refs) {
    const localRank = Math.max(0, (rank.get(ref) ?? 1) - minRank);
    const list = cols.get(localRank) ?? [];
    list.push(ref);
    cols.set(localRank, list);
  }

  const uniqueCols = [...cols.keys()].sort((a, b) => a - b);
  if (uniqueCols.length === 1 && refs.length >= 4) {
    cols.clear();
    const targetCols = Math.min(3, Math.max(2, Math.ceil(refs.length / 3)));
    refs.forEach((ref, idx) => {
      const col = idx % targetCols;
      const list = cols.get(col) ?? [];
      list.push(ref);
      cols.set(col, list);
    });
  }

  return cols;
}

function connectivityDegree(model) {
  const deg = new Map(model.instances.map((i) => [i.ref, 0]));
  for (const net of model.nets) {
    const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))];
    for (const ref of refs) {
      deg.set(ref, (deg.get(ref) ?? 0) + Math.max(1, refs.length - 1));
    }
  }
  return deg;
}

function connectivityGraph(model) {
  const graph = new Map(model.instances.map((i) => [i.ref, new Map()]));
  const classWeight = (netClass) => {
    if (netClass === "clock") return 4.6;
    if (netClass === "signal") return 4.2;
    if (netClass === "analog") return 3.8;
    if (netClass === "bus") return 3.4;
    if (netClass === "differential") return 3.2;
    if (netClass === "power") return 1.6;
    if (netClass === "ground") return 1.4;
    return 2.4;
  };

  for (const net of model.nets ?? []) {
    const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))].filter((ref) => graph.has(ref));
    if (refs.length < 2) {
      continue;
    }
    const weight = classWeight(String(net.class ?? "signal")) / Math.max(1, refs.length - 1);
    for (let i = 0; i < refs.length; i += 1) {
      for (let j = i + 1; j < refs.length; j += 1) {
        const a = refs[i];
        const b = refs[j];
        const aAdj = graph.get(a);
        const bAdj = graph.get(b);
        aAdj.set(b, (aAdj.get(b) ?? 0) + weight);
        bAdj.set(a, (bAdj.get(a) ?? 0) + weight);
      }
    }
  }

  return graph;
}

function centerForPlacement(model, inst) {
  const sym = model.symbols[inst.symbol];
  const w = sym?.body?.width ?? 120;
  const h = sym?.body?.height ?? 80;
  return {
    x: inst.placement.x + w / 2,
    y: inst.placement.y + h / 2
  };
}

function clampStep(delta, maxStep) {
  if (delta > maxStep) {
    return maxStep;
  }
  if (delta < -maxStep) {
    return -maxStep;
  }
  return delta;
}

function compactPlacementByConnectivity(model, placedMap, options = {}) {
  const respectLocks = options.respectLocks ?? true;
  const laneProfiles = options.laneProfiles ?? new Map();
  const rank = options.rank ?? new Map();
  const graph = connectivityGraph(model);
  const refs = [...placedMap.keys()].sort();
  if (!refs.length) {
    return;
  }
  const passCount = refs.length > 140 ? 2 : refs.length > 80 ? 4 : CONNECTIVITY_COMPACT_PASSES;
  const minRank = Math.min(...refs.map((r) => rank.get(r) ?? 1), 1);
  const rankBuckets = new Map();
  for (const ref of refs) {
    const r = rank.get(ref) ?? 1;
    const list = rankBuckets.get(r) ?? [];
    list.push(ref);
    rankBuckets.set(r, list);
  }
  const rankOffsetByRef = new Map();
  for (const [, bucketRefs] of rankBuckets.entries()) {
    bucketRefs.sort();
    const cols = Math.min(4, Math.max(1, Math.ceil(bucketRefs.length / 2)));
    const stride = GRID * 6;
    for (let i = 0; i < bucketRefs.length; i += 1) {
      const slot = i % cols;
      const centered = slot - (cols - 1) / 2;
      rankOffsetByRef.set(bucketRefs[i], centered * stride);
    }
  }

  const centersByRef = () => {
    const out = new Map();
    for (const ref of refs) {
      const inst = placedMap.get(ref);
      if (!inst) continue;
      out.set(ref, centerForPlacement(model, inst));
    }
    return out;
  };

  for (let pass = 0; pass < passCount; pass += 1) {
    const centers = centersByRef();
    for (const ref of refs) {
      const inst = placedMap.get(ref);
      if (!inst) {
        continue;
      }
      if (respectLocks && inst.placement.locked) {
        continue;
      }
      const sym = model.symbols[inst.symbol];
      const w = sym?.body?.width ?? 120;
      const h = sym?.body?.height ?? 80;
      const currentCenter = centers.get(ref);
      if (!currentCenter) {
        continue;
      }

      const neighbors = [...(graph.get(ref)?.entries() ?? [])];
      if (!neighbors.length) {
        continue;
      }

      let sumW = 0;
      let tx = 0;
      let ty = 0;
      for (const [nbr, weight] of neighbors) {
        const c = centers.get(nbr);
        if (!c) {
          continue;
        }
        sumW += weight;
        tx += c.x * weight;
        ty += c.y * weight;
      }
      if (!sumW) {
        continue;
      }
      tx /= sumW;
      ty /= sumW;

      const localRank = Math.max(0, (rank.get(ref) ?? 1) - minRank);
      const rankTargetX =
        MARGIN_X + localRank * (COLUMN_GAP * 0.82) + (rankOffsetByRef.get(ref) ?? 0);
      const lane = laneProfiles.get(ref)?.laneIndex ?? 2;
      const laneY = MARGIN_Y + lane * (ROW_GAP * 0.65);
      let repelX = 0;
      let repelY = 0;
      for (const [otherRef, otherCenter] of centers.entries()) {
        if (otherRef === ref) {
          continue;
        }
        const ddx = currentCenter.x - otherCenter.x;
        const ddy = currentCenter.y - otherCenter.y;
        const ax = Math.abs(ddx);
        const ay = Math.abs(ddy);
        if (ax > GRID * 10 || ay > GRID * 10) {
          continue;
        }
        const px = (GRID * 10 - ax) * 0.035;
        const py = (GRID * 10 - ay) * 0.03;
        repelX += ddx >= 0 ? px : -px;
        repelY += ddy >= 0 ? py : -py;
      }

      tx = tx * 0.54 + rankTargetX * 0.31 + currentCenter.x * 0.15 + repelX * 0.55;
      ty = ty * 0.62 + laneY * 0.3 + currentCenter.y * 0.08 + repelY * 0.35;

      const dx = clampStep(tx - currentCenter.x, CONNECTIVITY_MOVE_LIMIT);
      const dy = clampStep(ty - currentCenter.y, CONNECTIVITY_MOVE_LIMIT);
      const nextX = toGrid(Math.max(MARGIN_X, inst.placement.x + dx));
      const rankCorridor = toGrid(COLUMN_GAP * 0.62);
      const minRankX = toGrid(Math.max(MARGIN_X, rankTargetX - rankCorridor));
      const maxRankX = toGrid(rankTargetX + rankCorridor);
      inst.placement.x = Math.max(minRankX, Math.min(maxRankX, nextX));
      inst.placement.y = toGrid(Math.max(MARGIN_Y, inst.placement.y + dy));
      placedMap.set(ref, inst);
    }

    const shouldResolve =
      pass === passCount - 1 || (refs.length <= 80 && pass % 2 === 1);
    if (shouldResolve) {
      resolvePlacementOverlaps(model, placedMap, { respectLocks });
    }
  }
}

function isRailNetName(name) {
  const n = String(name ?? "").toUpperCase();
  return n === "GND" || n === "GROUND" || n === "3V3" || n === "5V" || n === "VCC" || n === "VIN";
}

function symbolKind(sym) {
  const t = String(sym?.template_name ?? "").toLowerCase();
  if (t) {
    return t;
  }
  const c = String(sym?.category ?? "").toLowerCase();
  if (c.includes("resistor")) return "resistor";
  if (c.includes("capacitor")) return "capacitor";
  if (c.includes("inductor")) return "inductor";
  if (c.includes("diode")) return "diode";
  if (c.includes("led")) return "led";
  return c;
}

function tightenPassiveAdjacency(model, placedMap, options = {}) {
  const respectLocks = options.respectLocks ?? true;
  const refToNets = new Map();
  for (const net of model.nets ?? []) {
    for (const node of net.nodes ?? []) {
      const list = refToNets.get(node.ref) ?? [];
      list.push(net);
      refToNets.set(node.ref, list);
    }
  }

  const refs = [...placedMap.keys()].sort();
  for (const ref of refs) {
    const inst = placedMap.get(ref);
    if (!inst) continue;
    if (respectLocks && inst.placement.locked) continue;
    const sym = model.symbols[inst.symbol];
    const kind = symbolKind(sym);
    if (!["resistor", "capacitor", "inductor", "diode", "led"].includes(kind)) {
      continue;
    }

    const nets = refToNets.get(ref) ?? [];
    const preferred = nets.find((n) => !["power", "ground"].includes(String(n.class ?? "")) && !isRailNetName(n.name));
    if (!preferred) {
      continue;
    }

    const anchors = [];
    for (const node of preferred.nodes ?? []) {
      if (node.ref === ref) continue;
      const other = placedMap.get(node.ref);
      if (!other) continue;
      anchors.push(centerForPlacement(model, other));
    }
    if (anchors.length < 2) {
      continue;
    }

    const cx = anchors.reduce((s, p) => s + p.x, 0) / anchors.length;
    const cy = anchors.reduce((s, p) => s + p.y, 0) / anchors.length;
    const spread =
      anchors.length > 1
        ? Math.max(...anchors.map((p) => Math.hypot(p.x - cx, p.y - cy)))
        : 0;
    if (spread < GRID * 5) {
      continue;
    }
    const current = centerForPlacement(model, inst);
    const tx = cx * 0.86 + current.x * 0.14;
    const ty = cy * 0.86 + current.y * 0.14;
    const rawX = tx - (sym?.body?.width ?? 120) / 2;
    const rawY = ty - (sym?.body?.height ?? 80) / 2;
    const maxShift = GRID * 7;
    const jitter = (((ref.charCodeAt(0) ?? 65) + (ref.charCodeAt(ref.length - 1) ?? 90)) % 3) - 1;
    const nx = toGrid(
      Math.max(MARGIN_X, Math.min(inst.placement.x + maxShift, Math.max(inst.placement.x - maxShift, rawX + jitter * GRID)))
    );
    const ny = toGrid(
      Math.max(MARGIN_Y, Math.min(inst.placement.y + maxShift, Math.max(inst.placement.y - maxShift, rawY)))
    );
    inst.placement.x = nx;
    inst.placement.y = ny;
    placedMap.set(ref, inst);
  }
}

function tightenConstraintGroups(model, placedMap, options = {}) {
  const respectLocks = options.respectLocks ?? true;
  for (const g of model.constraints?.groups ?? []) {
    const members = (g.members ?? []).map((ref) => placedMap.get(ref)).filter(Boolean);
    if (members.length < 2) {
      continue;
    }

    const centers = members.map((inst) => centerForPlacement(model, inst));
    const cx = centers.reduce((s, p) => s + p.x, 0) / centers.length;
    const cy = centers.reduce((s, p) => s + p.y, 0) / centers.length;
    const maxRadius = 320;

    for (const inst of members) {
      if (respectLocks && inst.placement.locked) {
        continue;
      }
      const sym = model.symbols[inst.symbol];
      const c = centerForPlacement(model, inst);
      const dx = c.x - cx;
      const dy = c.y - cy;
      const dist = Math.hypot(dx, dy);
      if (dist <= maxRadius) {
        continue;
      }

      const pull = Math.min(CONNECTIVITY_MOVE_LIMIT, (dist - maxRadius) * 0.55);
      const ux = dist > 0 ? dx / dist : 0;
      const uy = dist > 0 ? dy / dist : 0;
      const tx = c.x - ux * pull;
      const ty = c.y - uy * pull;
      inst.placement.x = toGrid(Math.max(MARGIN_X, tx - (sym?.body?.width ?? 120) / 2));
      inst.placement.y = toGrid(Math.max(MARGIN_Y, ty - (sym?.body?.height ?? 80) / 2));
      placedMap.set(inst.ref, inst);
    }
  }
}

function refLaneProfiles(model) {
  const profiles = new Map(model.instances.map((inst) => [inst.ref, { total: 0, byClass: {} }]));
  for (const net of model.nets ?? []) {
    const netClass = String(net.class ?? "signal");
    const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))];
    for (const ref of refs) {
      const p = profiles.get(ref);
      if (!p) {
        continue;
      }
      p.total += 1;
      p.byClass[netClass] = (p.byClass[netClass] ?? 0) + 1;
    }
  }

  const out = new Map();
  for (const [ref, profile] of profiles.entries()) {
    const ranked = Object.entries(profile.byClass).sort((a, b) => {
      if (a[1] !== b[1]) {
        return b[1] - a[1];
      }
      return (NET_CLASS_PRIORITY[a[0]] ?? 99) - (NET_CLASS_PRIORITY[b[0]] ?? 99);
    });
    const dominantClass = ranked[0]?.[0] ?? "signal";
    const laneIndex = Math.max(0, LANE_ORDER.indexOf(dominantClass));
    out.set(ref, {
      dominantClass,
      laneIndex,
      profile
    });
  }
  return out;
}

function placeGroup(model, group, start, context) {
  const { rank, degree, instanceByRef, respectLocks, laneProfiles } = context;
  const refs = [...group.members].sort((a, b) => a.localeCompare(b));
  const cols = rankColumnsForRefs(refs, rank);
  const colOrder = [...cols.keys()].sort((a, b) => a - b);

  const colWidths = new Map();
  for (const col of colOrder) {
    const w = Math.max(
      ...cols.get(col).map((ref) => {
        const inst = instanceByRef.get(ref);
        const sym = inst ? model.symbols[inst.symbol] : null;
        return sym?.body?.width ?? 120;
      }),
      120
    );
    colWidths.set(col, w);
  }

  const colX = new Map();
  let xCursor = start.x;
  for (const col of colOrder) {
    colX.set(col, toGrid(xCursor));
    xCursor += (colWidths.get(col) ?? 120) + 110;
  }

  const placed = [];
  let minX = Number.POSITIVE_INFINITY;
  let minY = Number.POSITIVE_INFINITY;
  let maxX = Number.NEGATIVE_INFINITY;
  let maxY = Number.NEGATIVE_INFINITY;

  for (const col of colOrder) {
    const refsInCol = [...(cols.get(col) ?? [])].sort((a, b) => {
      const la = laneProfiles.get(a)?.laneIndex ?? 2;
      const lb = laneProfiles.get(b)?.laneIndex ?? 2;
      if (la !== lb) {
        return la - lb;
      }
      const da = degree.get(a) ?? 0;
      const db = degree.get(b) ?? 0;
      if (da !== db) {
        return db - da;
      }
      return a.localeCompare(b);
    });

    const byLane = new Map();
    for (const ref of refsInCol) {
      const lane = laneProfiles.get(ref)?.laneIndex ?? 2;
      const list = byLane.get(lane) ?? [];
      list.push(ref);
      byLane.set(lane, list);
    }

    let yCursor = start.y;
    const laneSequence = [...byLane.keys()].sort((a, b) => a - b);
    for (const lane of laneSequence) {
      const laneRefs = byLane.get(lane) ?? [];
      for (const ref of laneRefs) {
      const inst = instanceByRef.get(ref);
      if (!inst) {
        continue;
      }
      const sym = model.symbols[inst.symbol];
      const locked = respectLocks ? (inst.placement.locked ?? false) : false;
      let x = inst.placement.x;
      let y = inst.placement.y;

      if (x == null || y == null || !locked) {
        x = toGrid(colX.get(col) ?? start.x);
        y = toGrid(yCursor);
      }

      const next = {
        ...inst,
        placement: {
          x,
          y,
          rotation: normalizeRotation(inst.placement.rotation ?? 0),
          locked
        }
      };
      placed.push(next);

      minX = Math.min(minX, x);
      minY = Math.min(minY, y);
      maxX = Math.max(maxX, x + sym.body.width);
      maxY = Math.max(maxY, y + sym.body.height);

      yCursor = y + sym.body.height + 48;
      }
      yCursor += 12;
    }
  }

  if (!placed.length) {
    return {
      placed,
      bbox: { x: start.x, y: start.y, w: 320, h: 240 }
    };
  }

  return {
    placed,
    bbox: {
      x: minX,
      y: minY,
      w: maxX - minX,
      h: maxY - minY
    }
  };
}

function rectForPlacement(model, inst) {
  const sym = model.symbols[inst.symbol];
  return {
    x: inst.placement.x,
    y: inst.placement.y,
    w: sym?.body?.width ?? 120,
    h: sym?.body?.height ?? 80
  };
}

function boxesOverlap(a, b, pad = 12) {
  return !(
    a.x + a.w + pad <= b.x ||
    b.x + b.w + pad <= a.x ||
    a.y + a.h + pad <= b.y ||
    b.y + b.h + pad <= a.y
  );
}

function resolvePlacementOverlaps(model, placedMap, options = {}) {
  const respectLocks = options.respectLocks ?? true;
  const refs = [...placedMap.keys()].sort();
  const iterations = Math.max(1, refs.length * 3);

  for (let pass = 0; pass < iterations; pass += 1) {
    let moved = false;
    for (let i = 0; i < refs.length; i += 1) {
      const aRef = refs[i];
      const aInst = placedMap.get(aRef);
      if (!aInst) {
        continue;
      }
      const aLocked = respectLocks ? Boolean(aInst.placement.locked) : false;
      let aBox = rectForPlacement(model, aInst);
      for (let j = i + 1; j < refs.length; j += 1) {
        const bRef = refs[j];
        const bInst = placedMap.get(bRef);
        if (!bInst) {
          continue;
        }
        const bLocked = respectLocks ? Boolean(bInst.placement.locked) : false;
        const bBox = rectForPlacement(model, bInst);
        if (!boxesOverlap(aBox, bBox, 14)) {
          continue;
        }

        const target = !aLocked ? aInst : !bLocked ? bInst : null;
        if (!target) {
          continue;
        }
        const oldX = target.placement.x;
        const oldY = target.placement.y;
        const overlapX = Math.max(
          0,
          Math.min(aBox.x + aBox.w, bBox.x + bBox.w) - Math.max(aBox.x, bBox.x)
        );
        const overlapY = Math.max(
          0,
          Math.min(aBox.y + aBox.h, bBox.y + bBox.h) - Math.max(aBox.y, bBox.y)
        );
        const targetBox = rectForPlacement(model, target);
        const otherBox = target === aInst ? bBox : aBox;
        const targetCx = targetBox.x + targetBox.w / 2;
        const targetCy = targetBox.y + targetBox.h / 2;
        const otherCx = otherBox.x + otherBox.w / 2;
        const otherCy = otherBox.y + otherBox.h / 2;

        if (overlapX <= overlapY) {
          const dir = targetCx >= otherCx ? 1 : -1;
          const push = toGrid(overlapX + 64) * dir;
          target.placement.x = Math.max(MARGIN_X, toGrid(target.placement.x + push));
        } else {
          const dir = targetCy >= otherCy ? 1 : -1;
          const push = toGrid(overlapY + 64) * dir;
          target.placement.y = Math.max(MARGIN_Y, toGrid(target.placement.y + push));
        }
        if (target.placement.x === oldX && target.placement.y === oldY) {
          const fallbackDir = targetCy >= otherCy ? 1 : -1;
          const fallbackPush = toGrid(Math.max(overlapY, overlapX) + 84) * fallbackDir;
          target.placement.y = Math.max(MARGIN_Y, toGrid(target.placement.y + fallbackPush));
        }
        if (target === aInst) {
          aBox = rectForPlacement(model, aInst);
        }
        moved = true;
      }
    }
    if (!moved) {
      break;
    }
  }
}

function enforceFinalComponentSeparation(model, placedMap, options = {}) {
  const respectLocks = options.respectLocks ?? true;
  const refs = [...placedMap.keys()].sort();
  const maxPasses = Math.max(1, refs.length * 2);

  for (let pass = 0; pass < maxPasses; pass += 1) {
    let changed = false;
    for (let i = 0; i < refs.length; i += 1) {
      const aRef = refs[i];
      const aInst = placedMap.get(aRef);
      if (!aInst) continue;
      const aLocked = respectLocks ? Boolean(aInst.placement.locked) : false;
      const aBox = rectForPlacement(model, aInst);
      for (let j = i + 1; j < refs.length; j += 1) {
        const bRef = refs[j];
        const bInst = placedMap.get(bRef);
        if (!bInst) continue;
        const bLocked = respectLocks ? Boolean(bInst.placement.locked) : false;
        const bBox = rectForPlacement(model, bInst);
        if (!boxesOverlap(aBox, bBox, 8)) {
          continue;
        }

        let mover = null;
        let anchor = null;
        if (!aLocked && bLocked) {
          mover = aInst;
          anchor = bInst;
        } else if (aLocked && !bLocked) {
          mover = bInst;
          anchor = aInst;
        } else if (!aLocked && !bLocked) {
          mover = bRef.localeCompare(aRef) >= 0 ? bInst : aInst;
          anchor = mover === bInst ? aInst : bInst;
        } else {
          continue;
        }

        const mBox = rectForPlacement(model, mover);
        const kBox = rectForPlacement(model, anchor);
        const moveRight = mBox.x >= kBox.x;
        const stepX = toGrid(Math.max(mBox.w, kBox.w) + 70) * (moveRight ? 1 : -1);
        const oldX = mover.placement.x;
        mover.placement.x = Math.max(MARGIN_X, toGrid(mover.placement.x + stepX));
        if (mover.placement.x === oldX) {
          mover.placement.y = Math.max(MARGIN_Y, toGrid(mover.placement.y + toGrid(Math.max(mBox.h, kBox.h) + 70)));
        }
        changed = true;
      }
    }
    if (!changed) {
      break;
    }
  }
}

function buildNodeNetMap(model) {
  const map = new Map();
  for (const net of model.nets) {
    for (const node of net.nodes ?? []) {
      const key = `${node.ref}.${node.pin}`;
      const list = map.get(key) ?? [];
      list.push(net);
      map.set(key, list);
    }
  }
  return map;
}

function scoreInstanceRotation(model, placedMap, inst, rotation, nodeNetMap) {
  const sym = model.symbols[inst.symbol];
  if (!sym || !Array.isArray(sym.pins)) {
    return Number.POSITIVE_INFINITY;
  }

  const temp = {
    ...inst,
    placement: {
      ...inst.placement,
      rotation
    }
  };

  let score = 0;
  for (const pin of sym.pins) {
    const key = `${inst.ref}.${pin.name}`;
    const nets = nodeNetMap.get(key) ?? [];
    if (!nets.length) {
      continue;
    }

    const p = pinPoint(temp, pin, sym.body.width, sym.body.height);
    for (const net of nets) {
      const others = (net.nodes ?? []).filter((n) => !(n.ref === inst.ref && n.pin === pin.name));
      if (!others.length) {
        continue;
      }

      let cx = 0;
      let cy = 0;
      let count = 0;

      for (const n of others) {
        const oi = placedMap.get(n.ref);
        if (!oi) {
          continue;
        }
        const os = model.symbols[oi.symbol];
        const op = os?.pins?.find((pp) => pp.name === n.pin);
        if (!os || !op) {
          continue;
        }
        const q = pinPoint(oi, op, os.body.width, os.body.height);
        cx += q.x;
        cy += q.y;
        count += 1;
      }

      if (!count) {
        continue;
      }

      const tx = cx / count;
      const ty = cy / count;
      score += Math.abs(p.x - tx) + Math.abs(p.y - ty);
    }
  }

  return score;
}

function applyAutoRotation(model, placedMap, options = {}) {
  const allow = options.autoRotate !== false;
  if (!allow) {
    return;
  }

  const nodeNetMap = buildNodeNetMap(model);
  const refs = [...placedMap.keys()].sort();
  for (const ref of refs) {
    const inst = placedMap.get(ref);
    if (!inst || inst.placement.locked) {
      continue;
    }

    let bestRot = normalizeRotation(inst.placement.rotation ?? 0);
    let bestScore = scoreInstanceRotation(model, placedMap, inst, bestRot, nodeNetMap);

    for (const rot of ROTATION_STEPS) {
      const s = scoreInstanceRotation(model, placedMap, inst, rot, nodeNetMap);
      if (s < bestScore - 0.001) {
        bestScore = s;
        bestRot = rot;
      }
    }

    inst.placement.rotation = bestRot;
    placedMap.set(ref, inst);
  }
}

function groupLaneIndex(group, laneProfiles) {
  const counts = new Map();
  for (const ref of group.members ?? []) {
    const lane = laneProfiles.get(ref)?.laneIndex ?? 2;
    counts.set(lane, (counts.get(lane) ?? 0) + 1);
  }
  const sorted = [...counts.entries()].sort((a, b) => {
    if (a[1] !== b[1]) {
      return b[1] - a[1];
    }
    return a[0] - b[0];
  });
  return sorted[0]?.[0] ?? 2;
}

function estimateGroupFootprint(group, rank, degree) {
  const refs = [...(group.members ?? [])].sort((a, b) => {
    const ra = rank.get(a) ?? 1;
    const rb = rank.get(b) ?? 1;
    if (ra !== rb) {
      return ra - rb;
    }
    const da = degree.get(a) ?? 0;
    const db = degree.get(b) ?? 0;
    if (da !== db) {
      return db - da;
    }
    return a.localeCompare(b);
  });
  const cols = rankColumnsForRefs(refs, rank);
  const colCount = Math.max(1, cols.size);
  const maxRowsInCol = Math.max(1, ...[...cols.values()].map((list) => list.length));
  const width = toGrid(240 + colCount * 220);
  const height = toGrid(180 + maxRowsInCol * 120);
  return { width, height };
}

function groupRectsOverlap(a, b, pad = 80) {
  return !(
    a.x + a.w + pad <= b.x ||
    b.x + b.w + pad <= a.x ||
    a.y + a.h + pad <= b.y ||
    b.y + b.h + pad <= a.y
  );
}

function computeGroupOrigins(groups, rank, laneProfiles, degree) {
  if (!groups.length) {
    return new Map();
  }

  const globalMinRank = Math.min(
    ...groups.flatMap((group) => (group.members ?? []).map((ref) => rank.get(ref) ?? 1)),
    0
  );
  const strideX = 420;
  const laneStrideY = 120;
  const origins = new Map();
  const usedRects = [];

  const planned = groups
    .map((group, idx) => {
      const refs = group.members ?? [];
      const minRank = Math.min(...refs.map((ref) => rank.get(ref) ?? 1), globalMinRank);
      const lane = groupLaneIndex(group, laneProfiles);
      const laneBand = Math.max(0, Math.min(3, lane));
      const footprint = estimateGroupFootprint(group, rank, degree);
      return {
        group,
        idx,
        minRank,
        lane,
        laneBand,
        footprint,
        targetX: toGrid(MARGIN_X + Math.max(0, minRank - globalMinRank) * strideX),
        targetY: toGrid(MARGIN_Y + laneBand * laneStrideY)
      };
    })
    .sort((a, b) => {
      if (a.targetX !== b.targetX) {
        return a.targetX - b.targetX;
      }
      if (a.targetY !== b.targetY) {
        return a.targetY - b.targetY;
      }
      return a.group.name.localeCompare(b.group.name);
    });

  const targetColumnSlots = new Map();
  for (const item of planned) {
    const slotKey = `${item.targetX}`;
    const slot = targetColumnSlots.get(slotKey) ?? 0;
    targetColumnSlots.set(slotKey, slot + 1);
    const fanoutStep = planned.length <= 3 ? Math.floor(strideX * 0.78) : Math.floor(strideX * 0.42);
    const fanoutX = toGrid(slot * fanoutStep);
    let x = toGrid(item.targetX + fanoutX);
    let y = item.targetY;
    let tries = 0;
    while (tries < 28) {
      const rect = { x, y, w: item.footprint.width, h: item.footprint.height };
      const collides = usedRects.some((other) => groupRectsOverlap(rect, other));
      if (!collides) {
        usedRects.push(rect);
        origins.set(item.group.name, { x, y });
        break;
      }
      y = toGrid(y + laneStrideY);
      if (tries > 0 && tries % 6 === 0) {
        x = toGrid(x + Math.floor(strideX * 0.5));
        y = item.targetY;
      }
      tries += 1;
    }
    if (!origins.has(item.group.name)) {
      origins.set(item.group.name, { x: item.targetX, y: toGrid(item.targetY + laneStrideY * 2) });
    }
  }

  return origins;
}

function placeInstances(model, options = {}) {
  const respectLocks = options.respectLocks ?? true;
  const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref));
  const { rank } = computeRanks(model);
  const degree = connectivityDegree(model);
  const laneProfiles = refLaneProfiles(model);
  const instanceByRef = buildInstanceMap(instances);
  const groups = buildConstraintGroups(model, rank);

  const placed = [];
  const placedMap = new Map();
  const groupOrigins = computeGroupOrigins(groups, rank, laneProfiles, degree);

  for (let i = 0; i < groups.length; i += 1) {
    const group = groups[i];
    const origin = groupOrigins.get(group.name) ?? { x: toGrid(MARGIN_X), y: toGrid(MARGIN_Y + i * 220) };

    const out = placeGroup(model, group, origin, {
      rank,
      degree,
      laneProfiles,
      instanceByRef,
      respectLocks
    });

    for (const inst of out.placed) {
      placed.push(inst);
      placedMap.set(inst.ref, inst);
    }
  }

  applyAutoRotation(model, placedMap, {
    autoRotate: options.autoRotate ?? true
  });

  compactPlacementByConnectivity(model, placedMap, {
    respectLocks,
    laneProfiles,
    rank
  });
  if (model.instances.length >= 8 && model.instances.length <= 220) {
    tightenPassiveAdjacency(model, placedMap, { respectLocks });
    tightenConstraintGroups(model, placedMap, { respectLocks });
  }

  applyAlignmentConstraints(placedMap, model.constraints);
  applyNearConstraints(model, placedMap, model.constraints);
  resolvePlacementOverlaps(model, placedMap, { respectLocks });
  enforceFinalComponentSeparation(model, placedMap, { respectLocks });

  const normalizedPlaced = instances
    .map((inst) => placedMap.get(inst.ref) ?? inst)
    .sort((a, b) => a.ref.localeCompare(b.ref));

  return { placed: normalizedPlaced, placedMap };
}

function buildObstacles(model, placed) {
  return placed.map((inst) => {
    const sym = model.symbols[inst.symbol];
    return {
      ref: inst.ref,
      x: inst.placement.x - OBSTACLE_PADDING,
      y: inst.placement.y - OBSTACLE_PADDING,
      w: sym.body.width + OBSTACLE_PADDING * 2,
      h: sym.body.height + OBSTACLE_PADDING * 2
    };
  });
}

function between(value, min, max) {
  return value >= min && value <= max;
}

function segmentIntersectsBox(a, b, box) {
  if (a.x === b.x) {
    if (!between(a.x, box.x, box.x + box.w)) {
      return false;
    }
    const minY = Math.min(a.y, b.y);
    const maxY = Math.max(a.y, b.y);
    return !(maxY < box.y || minY > box.y + box.h);
  }

  if (a.y === b.y) {
    if (!between(a.y, box.y, box.y + box.h)) {
      return false;
    }
    const minX = Math.min(a.x, b.x);
    const maxX = Math.max(a.x, b.x);
    return !(maxX < box.x || minX > box.x + box.w);
  }

  return false;
}

function buildBounds(model, placed) {
  const maxX = Math.max(...placed.map((p) => p.placement.x + model.symbols[p.symbol].body.width), MARGIN_X * 3);
  const maxY = Math.max(...placed.map((p) => p.placement.y + model.symbols[p.symbol].body.height), MARGIN_Y * 3);

  return {
    minX: 0,
    minY: 0,
    maxX: toGrid(maxX + MARGIN_X * 2),
    maxY: toGrid(maxY + MARGIN_Y * 2)
  };
}

function segmentBlocked(a, b, obstacles, allowedRefs) {
  for (const box of obstacles) {
    if (allowedRefs.has(box.ref)) {
      continue;
    }
    if (segmentIntersectsBox(a, b, box)) {
      return true;
    }
  }
  return false;
}

function heuristic(a, b) {
  return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}

function reconstructPath(cameFrom, currentKey) {
  const out = [];
  let key = currentKey;
  while (key) {
    const node = cameFrom.get(key)?.node;
    if (!node) {
      const [x, y] = key.split(",").map(Number);
      out.push({ x, y });
      break;
    }
    out.push(node);
    key = cameFrom.get(key)?.prev;
  }
  out.reverse();
  return out;
}

function hasForeignEdgeUsage(edgeUsage, netName, a, b) {
  const usage = edgeUsage.get(edgeKey(a, b));
  if (!usage) {
    return false;
  }

  for (const existingNet of usage.byNet.keys()) {
    if (existingNet !== netName) {
      return true;
    }
  }
  return false;
}

function hasForeignPointUsage(pointUsage, netName, point) {
  const usage = pointUsage.get(pointKey(point));
  if (!usage) {
    return false;
  }

  for (const existingNet of usage.keys()) {
    if (existingNet !== netName) {
      return true;
    }
  }
  return false;
}

function foreignUsageCount(usageMap, key, ownNet) {
  const usage = usageMap.get(key);
  if (!usage) {
    return 0;
  }

  let count = 0;
  for (const [net, n] of usage.entries()) {
    if (net !== ownNet) {
      count += n;
    }
  }
  return count;
}

function channelCrowdingPenalty(current, next, context) {
  const { netName, hLineUsage, vLineUsage } = context;
  if (current.x !== next.x && current.y !== next.y) {
    return 0;
  }

  const isHorizontal = current.y === next.y;
  const lineUsage = isHorizontal ? hLineUsage : vLineUsage;
  const lineCoord = isHorizontal ? current.y : current.x;

  let penalty = 0;
  for (let step = -MIN_CHANNEL_SPACING_STEPS; step <= MIN_CHANNEL_SPACING_STEPS; step += 1) {
    const coord = lineCoord + step * GRID;
    const foreign = foreignUsageCount(lineUsage, coord, netName);
    if (!foreign) {
      continue;
    }

    const distance = Math.abs(step);
    const weight = distance === 0 ? 34 : distance === 1 ? 17 : 7;
    penalty += foreign * weight;
  }

  return penalty;
}

function pointCongestionPenalty(point, context) {
  const { pointUsage, netName } = context;
  let penalty = 0;
  for (const nb of [
    { x: point.x, y: point.y },
    { x: point.x + GRID, y: point.y },
    { x: point.x - GRID, y: point.y },
    { x: point.x, y: point.y + GRID },
    { x: point.x, y: point.y - GRID }
  ]) {
    const foreign = foreignUsageCount(pointUsage, pointKey(nb), netName);
    if (foreign) {
      penalty += foreign * 10;
    }
  }
  return penalty;
}

function proximityPenalty(point, obstacles, allowedRefs) {
  let penalty = 0;
  for (const box of obstacles) {
    if (allowedRefs.has(box.ref)) {
      continue;
    }

    const dx = Math.max(box.x - point.x, 0, point.x - (box.x + box.w));
    const dy = Math.max(box.y - point.y, 0, point.y - (box.y + box.h));
    const dist = dx + dy;
    if (dist < GRID * 4) {
      penalty += (GRID * 4 - dist) * 0.7;
    }
  }
  return penalty;
}

function aStar(start, goal, context) {
  const { bounds, obstacles, allowedRefs, edgeUsage, pointUsage, netName } = context;

  const startNode = { x: toGrid(start.x), y: toGrid(start.y) };
  const goalNode = { x: toGrid(goal.x), y: toGrid(goal.y) };

  const open = new Map();
  const gScore = new Map();
  const cameFrom = new Map();

  const startKey = pointKey(startNode);
  open.set(startKey, { ...startNode, dir: null, f: heuristic(startNode, goalNode) });
  gScore.set(startKey, 0);
  cameFrom.set(startKey, { prev: null, node: startNode, dir: null });

  const maxIterations = 30000;
  let iterations = 0;

  while (open.size > 0 && iterations < maxIterations) {
    iterations += 1;

    let current = null;
    for (const cand of open.values()) {
      if (!current || cand.f < current.f) {
        current = cand;
      }
    }

    if (!current) {
      break;
    }

    const currentKey = pointKey(current);
    if (current.x === goalNode.x && current.y === goalNode.y) {
      return reconstructPath(cameFrom, currentKey);
    }

    open.delete(currentKey);

    const neighbors = [
      { x: current.x + GRID, y: current.y, dir: "h" },
      { x: current.x - GRID, y: current.y, dir: "h" },
      { x: current.x, y: current.y + GRID, dir: "v" },
      { x: current.x, y: current.y - GRID, dir: "v" }
    ];

    for (const nb of neighbors) {
      if (nb.x < bounds.minX || nb.x > bounds.maxX || nb.y < bounds.minY || nb.y > bounds.maxY) {
        continue;
      }

      if (segmentBlocked(current, nb, obstacles, allowedRefs)) {
        continue;
      }

      if (hasForeignEdgeUsage(edgeUsage, netName, current, nb)) {
        continue;
      }

      const isGoal = nb.x === goalNode.x && nb.y === goalNode.y;
      if (!isGoal && hasForeignPointUsage(pointUsage, netName, nb)) {
        continue;
      }

      const nbKey = pointKey(nb);
      const prevCost = gScore.get(currentKey) ?? Number.POSITIVE_INFINITY;
      const turnPenalty = current.dir && current.dir !== nb.dir ? 24 : 0;
      const obstaclePenalty = proximityPenalty(nb, obstacles, allowedRefs);
      const channelPenalty = channelCrowdingPenalty(current, nb, context);
      const congestionPenalty = pointCongestionPenalty(nb, context);
      const tentative = prevCost + GRID + turnPenalty + obstaclePenalty + channelPenalty + congestionPenalty;

      if (tentative >= (gScore.get(nbKey) ?? Number.POSITIVE_INFINITY)) {
        continue;
      }

      gScore.set(nbKey, tentative);
      open.set(nbKey, {
        ...nb,
        f: tentative + heuristic(nb, goalNode)
      });
      cameFrom.set(nbKey, {
        prev: currentKey,
        node: { x: nb.x, y: nb.y },
        dir: nb.dir
      });
    }
  }

  return null;
}

function pointsToSegments(points) {
  const segments = [];
  for (let i = 1; i < points.length; i += 1) {
    const a = points[i - 1];
    const b = points[i];
    if (a.x === b.x && a.y === b.y) {
      continue;
    }
    segments.push({ a: { ...a }, b: { ...b } });
  }
  return simplifySegments(segments);
}

function simplifySegments(segments) {
  const out = [];

  for (const seg of segments) {
    const prev = out[out.length - 1];
    if (!prev) {
      out.push(seg);
      continue;
    }

    const prevVertical = prev.a.x === prev.b.x;
    const currVertical = seg.a.x === seg.b.x;
    if (prevVertical === currVertical && prev.b.x === seg.a.x && prev.b.y === seg.a.y) {
      prev.b = { ...seg.b };
      continue;
    }

    out.push(seg);
  }

  return out;
}

function segmentStepPoints(a, b) {
  const points = [];

  if (a.x === b.x) {
    const step = a.y < b.y ? GRID : -GRID;
    for (let y = a.y; step > 0 ? y <= b.y : y >= b.y; y += step) {
      points.push({ x: a.x, y });
    }
    return points;
  }

  if (a.y === b.y) {
    const step = a.x < b.x ? GRID : -GRID;
    for (let x = a.x; step > 0 ? x <= b.x : x >= b.x; x += step) {
      points.push({ x, y: a.y });
    }
    return points;
  }

  return [a, b];
}

function addLineUsage(lineUsage, coord, netName) {
  const usage = lineUsage.get(coord) ?? new Map();
  usage.set(netName, (usage.get(netName) ?? 0) + 1);
  lineUsage.set(coord, usage);
}

function addUsageForSegments(edgeUsage, pointUsage, hLineUsage, vLineUsage, netName, segments) {
  for (const seg of segments) {
    const stepPoints = segmentStepPoints(seg.a, seg.b);

    for (let i = 1; i < stepPoints.length; i += 1) {
      const p0 = stepPoints[i - 1];
      const p1 = stepPoints[i];
      const eKey = edgeKey(p0, p1);
      const edge = edgeUsage.get(eKey) ?? { total: 0, byNet: new Map() };
      edge.total += 1;
      edge.byNet.set(netName, (edge.byNet.get(netName) ?? 0) + 1);
      edgeUsage.set(eKey, edge);
    }

    if (seg.a.y === seg.b.y) {
      addLineUsage(hLineUsage, seg.a.y, netName);
    } else if (seg.a.x === seg.b.x) {
      addLineUsage(vLineUsage, seg.a.x, netName);
    }

    for (const p of stepPoints) {
      const pKey = pointKey(p);
      const usage = pointUsage.get(pKey) ?? new Map();
      usage.set(netName, (usage.get(netName) ?? 0) + 1);
      pointUsage.set(pKey, usage);
    }
  }
}

function computeJunctionPoints(routes) {
  const degree = new Map();

  for (const route of routes) {
    for (const seg of route) {
      const aKey = pointKey(seg.a);
      const bKey = pointKey(seg.b);
      degree.set(aKey, (degree.get(aKey) ?? 0) + 1);
      degree.set(bKey, (degree.get(bKey) ?? 0) + 1);
    }
  }

  const out = [];
  for (const [key, deg] of degree.entries()) {
    if (deg >= 3) {
      const [x, y] = key.split(",").map(Number);
      out.push({ x, y });
    }
  }

  out.sort((a, b) => a.x - b.x || a.y - b.y);
  return out;
}

function labelPointForPin(pin) {
  if (pin.side === "left") {
    return { x: pin.exit.x - GRID * 2.2, y: pin.exit.y - GRID * 0.4 };
  }
  if (pin.side === "right") {
    return { x: pin.exit.x + GRID * 0.6, y: pin.exit.y - GRID * 0.4 };
  }
  if (pin.side === "top") {
    return { x: pin.exit.x + GRID * 0.3, y: pin.exit.y - GRID * 0.8 };
  }
  return { x: pin.exit.x + GRID * 0.3, y: pin.exit.y + GRID * 0.9 };
}

function chooseEndpoints(pinNodes) {
  if (pinNodes.length < 2) {
    return null;
  }

  const priority = (pinType) => {
    if (pinType === "power_out") return 0;
    if (pinType === "output") return 1;
    if (pinType === "bidirectional") return 2;
    if (pinType === "analog") return 3;
    return 4;
  };

  const sorted = [...pinNodes].sort((a, b) => {
    const pa = priority(a.pinType);
    const pb = priority(b.pinType);
    if (pa !== pb) {
      return pa - pb;
    }
    return a.exit.x - b.exit.x || a.exit.y - b.exit.y;
  });

  return { source: sorted[0], target: sorted[1] };
}

function pinRoutePriority(pin) {
  if (pin.pinType === "power_out") return 0;
  if (pin.pinType === "output") return 1;
  if (pin.pinType === "bidirectional") return 2;
  if (pin.pinType === "analog") return 3;
  return 4;
}

function uniquePoints(points) {
  const map = new Map();
  for (const p of points) {
    map.set(pointKey(p), p);
  }
  return [...map.values()];
}

function routeLabelTieNet(net, pinNodes, context, fallbackReason = null) {
  const routes = [];
  const tiePoints = [];

  for (const pin of pinNodes) {
    const stub = pointsToSegments([pin.point, pin.exit]);
    if (stub.length) {
      routes.push(stub);
      addUsageForSegments(
        context.edgeUsage,
        context.pointUsage,
        context.hLineUsage,
        context.vLineUsage,
        net.name,
        stub
      );
    }
    tiePoints.push({ x: pin.exit.x, y: pin.exit.y });
  }

  const labelPoints = [];
  if (tiePoints.length) {
    const centroid = {
      x: tiePoints.reduce((sum, p) => sum + p.x, 0) / tiePoints.length,
      y: tiePoints.reduce((sum, p) => sum + p.y, 0) / tiePoints.length
    };
    labelPoints.push({
      x: toGrid(centroid.x + GRID * 0.6),
      y: toGrid(centroid.y - GRID * 0.8)
    });

    const anchorPin = [...pinNodes].sort((a, b) => a.exit.x - b.exit.x || a.exit.y - b.exit.y)[0];
    if (anchorPin) {
      labelPoints.push(labelPointForPin(anchorPin));
    }
  }

  const tieLength = routeLengthFromSegments(routes);

  return {
    mode: "label_tie",
    routes,
    labelPoints,
    tiePoints,
    junctionPoints: [],
    route_stats: {
      total_length: tieLength,
      direct_length: tieLength,
      total_bends: 0,
      detour_ratio: 1,
      used_label_tie: true,
      fallback_reason: fallbackReason
    }
  };
}

function pathLength(points) {
  let length = 0;
  for (let i = 1; i < points.length; i += 1) {
    length += Math.abs(points[i].x - points[i - 1].x) + Math.abs(points[i].y - points[i - 1].y);
  }
  return length;
}

function routeLengthFromSegments(routes) {
  let length = 0;
  for (const route of routes) {
    for (const seg of route) {
      length += Math.abs(seg.a.x - seg.b.x) + Math.abs(seg.a.y - seg.b.y);
    }
  }
  return length;
}

function countBendsInRoute(routes) {
  let bends = 0;
  for (const route of routes) {
    for (let i = 1; i < route.length; i += 1) {
      const prev = route[i - 1];
      const curr = route[i];
      const prevH = prev.a.y === prev.b.y;
      const currH = curr.a.y === curr.b.y;
      if (prevH !== currH) {
        bends += 1;
      }
    }
  }
  return bends;
}

function routePointToPointNet(net, pinNodes, context) {
  if (pinNodes.length < 2) {
    return {
      mode: "routed",
      routes: [],
      labelPoints: [],
      tiePoints: [],
      junctionPoints: [],
      route_stats: {
        total_length: 0,
        direct_length: 0,
        total_bends: 0,
        detour_ratio: 1,
        used_label_tie: false,
        fallback_reason: null
      }
    };
  }

  const sorted = [...pinNodes].sort((a, b) => {
    const pa = pinRoutePriority(a);
    const pb = pinRoutePriority(b);
    if (pa !== pb) {
      return pa - pb;
    }
    return a.exit.x - b.exit.x || a.exit.y - b.exit.y;
  });

  const source = sorted[0];
  const routes = [];
  const sourceStub = pointsToSegments([source.point, source.exit]);
  if (sourceStub.length) {
    routes.push(sourceStub);
    addUsageForSegments(
      context.edgeUsage,
      context.pointUsage,
      context.hLineUsage,
      context.vLineUsage,
      net.name,
      sourceStub
    );
  }

  const treePoints = new Map();
  treePoints.set(pointKey(source.exit), source.exit);

  const allowedRefs = new Set(sorted.map((p) => p.ref));
  const remaining = sorted.slice(1);
  let accumulatedPath = 0;
  let accumulatedDirect = 0;

  for (const target of remaining) {
    const candidates = uniquePoints([...treePoints.values()])
      .sort((a, b) => heuristic(target.exit, a) - heuristic(target.exit, b))
      .slice(0, 12);

    let best = null;
    for (const attach of candidates) {
      const path = aStar(target.exit, attach, {
        ...context,
        allowedRefs,
        netName: net.name
      });
      if (!path) {
        continue;
      }

      const cost = pathLength(path);
      if (!best || cost < best.cost) {
        best = { path, attach, cost };
      }
    }

    if (!best) {
      return {
        ...routeLabelTieNet(net, sorted, context),
        route_stats: {
          total_length: 0,
          direct_length: 0,
          total_bends: 0,
          detour_ratio: 1,
          used_label_tie: true,
          fallback_reason: "no_path"
        }
      };
    }

    const direct = heuristic(target.exit, best.attach);
    accumulatedPath += best.cost;
    accumulatedDirect += Math.max(GRID, direct);
    if (context.renderMode === "schematic_stub" && best.cost > direct * 1.6) {
      return {
        ...routeLabelTieNet(net, sorted, context),
        route_stats: {
          total_length: accumulatedPath,
          direct_length: accumulatedDirect,
          total_bends: 0,
          detour_ratio: accumulatedPath / Math.max(GRID, accumulatedDirect),
          used_label_tie: true,
          fallback_reason: "branch_detour"
        }
      };
    }

    const branchPoints = [target.point, target.exit, ...best.path.slice(1)];
    const branchSegments = pointsToSegments(branchPoints);
    if (branchSegments.length) {
      routes.push(branchSegments);
      addUsageForSegments(
        context.edgeUsage,
        context.pointUsage,
        context.hLineUsage,
        context.vLineUsage,
        net.name,
        branchSegments
      );
      for (const seg of branchSegments) {
        for (const p of segmentStepPoints(seg.a, seg.b)) {
          treePoints.set(pointKey(p), p);
        }
      }
    }
  }

  const allPathPoints = routes.flatMap((route) => route.flatMap((seg) => [seg.a, seg.b]));
  const centroid = allPathPoints.length
    ? {
        x: allPathPoints.reduce((sum, p) => sum + p.x, 0) / allPathPoints.length,
        y: allPathPoints.reduce((sum, p) => sum + p.y, 0) / allPathPoints.length
      }
    : labelPointForPin(source);

  const labelPoints = [labelPointForPin(source)];
  if (sorted.length >= 3) {
    labelPoints.push({ x: toGrid(centroid.x + GRID * 0.4), y: toGrid(centroid.y - GRID * 0.6) });
  }
  const junctionPoints = computeJunctionPoints(routes);
  if (sorted.length >= 3 && junctionPoints.length === 0) {
    junctionPoints.push({ x: source.exit.x, y: source.exit.y });
  }

  const totalLength = routeLengthFromSegments(routes);
  const totalBends = countBendsInRoute(routes);
  const directLength = Math.max(GRID, accumulatedDirect || heuristic(source.exit, sorted[sorted.length - 1].exit));
  const detourRatio = totalLength / directLength;
  const maxAllowedBends = Math.max(6, sorted.length * 3);
  if (
    context.renderMode === "schematic_stub" &&
    (detourRatio > 1.9 || totalBends > maxAllowedBends || totalLength > GRID * 180)
  ) {
    return {
      ...routeLabelTieNet(net, sorted, context),
      route_stats: {
        total_length: totalLength,
        direct_length: directLength,
        total_bends: totalBends,
        detour_ratio: detourRatio,
        used_label_tie: true,
        fallback_reason: "global_quality"
      }
    };
  }

  return {
    mode: "routed",
    routes,
    labelPoints,
    tiePoints: [],
    junctionPoints,
    route_stats: {
      total_length: totalLength,
      direct_length: directLength,
      total_bends: totalBends,
      detour_ratio: detourRatio,
      used_label_tie: false,
      fallback_reason: null
    }
  };
}

function detectBusGroups(nets) {
  const groups = new Map();
  for (const net of nets) {
    const match = /^([A-Za-z0-9]+)_/.exec(net.name);
    if (!match) {
      continue;
    }
    const key = match[1].toUpperCase();
    const list = groups.get(key) ?? [];
    list.push(net.name);
    groups.set(key, list);
  }

  const out = [];
  for (const [name, members] of groups.entries()) {
    if (members.length >= 2) {
      out.push({ name, nets: members.sort() });
    }
  }

  out.sort((a, b) => a.name.localeCompare(b.name));
  return out;
}

function shouldUseLabelTie(net, pinNodes, context) {
  if (!pinNodes.length) {
    return false;
  }
  const xs = pinNodes.map((p) => p.exit.x);
  const ys = pinNodes.map((p) => p.exit.y);
  const minX = Math.min(...xs);
  const maxX = Math.max(...xs);
  const minY = Math.min(...ys);
  const maxY = Math.max(...ys);
  const span = Math.abs(maxX - minX) + Math.abs(maxY - minY);

  if (context.renderMode === "explicit") {
    if (context.busNetNames.has(net.name) && (pinNodes.length >= 3 || span > GRID * 36)) {
      return true;
    }
    return LABEL_TIE_CLASSES.has(net.class) && pinNodes.length > 2;
  }

  if (LABEL_TIE_CLASSES.has(net.class)) {
    return true;
  }

  if (context.busNetNames.has(net.name)) {
    return true;
  }

  // Prefer readable schematic stubs for dense multi-node nets.
  if (pinNodes.length >= 4) {
    return true;
  }

  // Long-distributed analog/signal nets become noisy when fully routed.
  if ((net.class === "analog" || net.class === "signal") && pinNodes.length >= 3 && span > GRID * 34) {
    return true;
  }

  return false;
}

function shouldFallbackToTieByQuality(net, pinNodes, routed) {
  if (!routed || routed.mode !== "routed") {
    return false;
  }
  const stats = routed.route_stats ?? {};
  const detour = Number(stats.detour_ratio ?? 1);
  const bends = Number(stats.total_bends ?? 0);
  const totalLength = Number(stats.total_length ?? 0);
  const directLength = Number(stats.direct_length ?? totalLength);
  const spanRatio = directLength > 0 ? totalLength / directLength : 1;
  const xs = pinNodes.map((p) => p.exit.x);
  const ys = pinNodes.map((p) => p.exit.y);
  const span = Math.abs(Math.max(...xs) - Math.min(...xs)) + Math.abs(Math.max(...ys) - Math.min(...ys));
  const hasSpread = span >= GRID * 24;

  if (pinNodes.length >= 4 && hasSpread && (detour > 2.05 || bends >= 6)) {
    return true;
  }
  if (
    hasSpread &&
    (net.class === "analog" || net.class === "signal") &&
    pinNodes.length >= 3 &&
    spanRatio > 2.7
  ) {
    return true;
  }
  return false;
}

function routeAllNets(model, placed, placedMap, bounds, options) {
  const obstacles = buildObstacles(model, placed);
  const edgeUsage = new Map();
  const pointUsage = new Map();
  const hLineUsage = new Map();
  const vLineUsage = new Map();
  const busGroups = detectBusGroups(model.nets);
  const busNetNames = new Set(busGroups.flatMap((g) => g.nets));

  const nets = [...model.nets].sort((a, b) => {
    const pa = NET_CLASS_PRIORITY[a.class] ?? 99;
    const pb = NET_CLASS_PRIORITY[b.class] ?? 99;
    if (pa !== pb) {
      return pa - pb;
    }
    return a.name.localeCompare(b.name);
  });

  const routedByName = new Map();

  for (const net of nets) {
    const pinNodes = net.nodes.map((node) => getNodePin(model, placedMap, node)).filter(Boolean);

    const routeContext = {
      bounds,
      obstacles,
      edgeUsage,
      pointUsage,
      hLineUsage,
      vLineUsage,
      renderMode: options.renderMode,
      busNetNames
    };

    let routed = shouldUseLabelTie(net, pinNodes, routeContext)
      ? routeLabelTieNet(net, pinNodes, routeContext)
      : routePointToPointNet(net, pinNodes, routeContext);

    if (shouldFallbackToTieByQuality(net, pinNodes, routed)) {
      routed = routeLabelTieNet(net, pinNodes, routeContext, "quality_policy");
    }

    routedByName.set(net.name, {
      net,
      isBusMember: busNetNames.has(net.name),
      ...routed
    });
  }

  const routed = model.nets.map(
    (net) =>
      routedByName.get(net.name) ?? {
        net,
        mode: "routed",
        routes: [],
        labelPoints: [],
        tiePoints: [],
        junctionPoints: [],
        route_stats: {
          total_length: 0,
          direct_length: 0,
          total_bends: 0,
          detour_ratio: 1,
          used_label_tie: false,
          fallback_reason: null
        }
      }
  );

  return { routed, busGroups };
}

function lineIntersection(a1, a2, b1, b2) {
  const aVertical = a1.x === a2.x;
  const bVertical = b1.x === b2.x;

  if (aVertical === bVertical) {
    return null;
  }

  const v = aVertical ? [a1, a2] : [b1, b2];
  const h = aVertical ? [b1, b2] : [a1, a2];

  const vx = v[0].x;
  const hy = h[0].y;
  const vMinY = Math.min(v[0].y, v[1].y);
  const vMaxY = Math.max(v[0].y, v[1].y);
  const hMinX = Math.min(h[0].x, h[1].x);
  const hMaxX = Math.max(h[0].x, h[1].x);

  if (vx >= hMinX && vx <= hMaxX && hy >= vMinY && hy <= vMaxY) {
    return { x: vx, y: hy };
  }

  return null;
}

function collectSegmentsByNet(routed) {
  const out = [];
  for (const rn of routed) {
    for (const route of rn.routes) {
      for (const seg of route) {
        out.push({ net: rn.net.name, seg });
      }
    }
  }
  return out;
}

function countOverlapEdges(routed) {
  const edgeNets = new Map();
  for (const rn of routed) {
    for (const route of rn.routes) {
      for (const seg of route) {
        const steps = segmentStepPoints(seg.a, seg.b);
        for (let i = 1; i < steps.length; i += 1) {
          const k = edgeKey(steps[i - 1], steps[i]);
          const set = edgeNets.get(k) ?? new Set();
          set.add(rn.net.name);
          edgeNets.set(k, set);
        }
      }
    }
  }

  let overlaps = 0;
  for (const nets of edgeNets.values()) {
    if (nets.size > 1) {
      overlaps += 1;
    }
  }
  return overlaps;
}

function countCrossings(routed) {
  const segments = collectSegmentsByNet(routed);
  let crossings = 0;

  for (let i = 0; i < segments.length; i += 1) {
    const a = segments[i];
    for (let j = i + 1; j < segments.length; j += 1) {
      const b = segments[j];
      if (a.net === b.net) {
        continue;
      }
      const hit = lineIntersection(a.seg.a, a.seg.b, b.seg.a, b.seg.b);
      if (!hit) {
        continue;
      }
      const atEndpoint =
        pointKey(hit) === pointKey(a.seg.a) ||
        pointKey(hit) === pointKey(a.seg.b) ||
        pointKey(hit) === pointKey(b.seg.a) ||
        pointKey(hit) === pointKey(b.seg.b);
      if (!atEndpoint) {
        crossings += 1;
      }
    }
  }

  return crossings;
}

function countLabelCollisions(routed) {
  const labels = routed.flatMap((rn) => {
    if (rn.isBusMember && rn.mode === "label_tie") {
      return [];
    }

    const points = rn.labelPoints?.length ? [rn.labelPoints[0]] : [];
    return points.map((p) => ({ ...p, net: rn.net.name }));
  });
  let collisions = 0;

  for (let i = 0; i < labels.length; i += 1) {
    for (let j = i + 1; j < labels.length; j += 1) {
      const a = labels[i];
      const b = labels[j];
      if (Math.abs(a.x - b.x) < GRID * 2 && Math.abs(a.y - b.y) < GRID * 1.2) {
        collisions += 1;
      }
    }
  }

  return collisions;
}

function computeLayoutMetrics(routed, busGroups) {
  const segmentCount = routed.reduce(
    (total, rn) => total + rn.routes.reduce((sum, route) => sum + route.length, 0),
    0
  );
  const tiePoints = routed.reduce((total, rn) => total + rn.tiePoints.length, 0);
  const totalBends = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_bends ?? 0), 0);
  const totalLength = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_length ?? 0), 0);
  const totalDirect = routed.reduce((total, rn) => total + Number(rn.route_stats?.direct_length ?? 0), 0);
  const labelTieFallbacks = routed.reduce(
    (total, rn) => total + (rn.route_stats?.used_label_tie && rn.route_stats?.fallback_reason ? 1 : 0),
    0
  );
  const labelTieRoutes = routed.reduce((total, rn) => total + (rn.route_stats?.used_label_tie ? 1 : 0), 0);

  return {
    segment_count: segmentCount,
    overlap_edges: countOverlapEdges(routed),
    crossings: countCrossings(routed),
    label_collisions: countLabelCollisions(routed),
    tie_points_used: tiePoints,
    bus_groups: busGroups.length,
    total_bends: totalBends,
    total_length: totalLength,
    direct_length: totalDirect,
    detour_ratio: totalDirect > 0 ? totalLength / totalDirect : 1,
    label_tie_fallbacks: labelTieFallbacks,
    label_tie_routes: labelTieRoutes
  };
}

export function applyLayoutToModel(model, options = {}) {
  const working = clone(model);
  const respectLocks = options.respectLocks ?? true;
  const autoRotate = options.autoRotate ?? true;
  const layoutOptions =
    options.layoutOptions && typeof options.layoutOptions === "object" ? options.layoutOptions : {};

  if (!respectLocks) {
    for (const inst of working.instances) {
      inst.placement.x = null;
      inst.placement.y = null;
      inst.placement.locked = false;
    }
  }

  const { placed } = layoutAndRoute(working, {
    ...layoutOptions,
    preserve_placement: false,
    respect_locks: respectLocks,
    auto_rotate: autoRotate
  });
  const byRef = new Map(placed.map((inst) => [inst.ref, inst]));

  for (const inst of working.instances) {
    const p = byRef.get(inst.ref);
    if (!p) {
      continue;
    }
    inst.placement.x = p.placement.x;
    inst.placement.y = p.placement.y;
    inst.placement.rotation = p.placement.rotation;
    inst.placement.locked = p.placement.locked;
  }

  return working;
}

export function requestedLayoutEngine(options = {}) {
  const explicitEngine = typeof options.layout_engine === "string" ? options.layout_engine.trim().toLowerCase() : "";
  if (explicitEngine === "elk" || options.use_elk_layout === true) {
    return "elk";
  }
  return DEFAULT_LAYOUT_ENGINE;
}

function layoutAndRouteNative(model, options = {}) {
  const renderMode = options.render_mode === "explicit" ? "explicit" : DEFAULT_RENDER_MODE;
  const respectLocks = options.respect_locks ?? true;
  const autoRotate = options.auto_rotate ?? true;
  const preservePlacement = options.preserve_placement !== false;
  const placed = preservePlacement
    ? preservePlacedInstances(model, { respectLocks, autoRotate })
    : placeInstances(model, { respectLocks, autoRotate }).placed;
  const placedMap = new Map(placed.map((inst) => [inst.ref, inst]));
  const bounds = buildBounds(model, placed);
  const { routed, busGroups } = routeAllNets(model, placed, placedMap, bounds, {
    renderMode
  });

  return {
    placed,
    routed,
    width: bounds.maxX,
    height: bounds.maxY,
    bus_groups: busGroups,
    metrics: computeLayoutMetrics(routed, busGroups),
    render_mode_used: renderMode
  };
}

function buildElkEdges(model) {
  const directed = buildDirectedEdges(model);
  if (directed.length) {
    return directed;
  }
  const undirected = new Map();
  for (const net of model.nets ?? []) {
    const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))].sort();
    if (refs.length < 2) {
      continue;
    }
    const source = refs[0];
    for (let i = 1; i < refs.length; i += 1) {
      const target = refs[i];
      if (source === target) continue;
      undirected.set(`${source}->${target}`, [source, target]);
    }
  }
  return [...undirected.values()];
}

function buildElkGraph(model) {
  const instances = [...(model.instances ?? [])].sort((a, b) => a.ref.localeCompare(b.ref));
  const edges = buildElkEdges(model);
  return {
    id: "schemeta-root",
    layoutOptions: {
      "elk.algorithm": "layered",
      "elk.direction": "RIGHT",
      "elk.spacing.nodeNode": "90",
      "elk.layered.spacing.nodeNodeBetweenLayers": "170",
      "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
      "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
      "elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
    },
    children: instances.map((inst) => {
      const sym = model.symbols[inst.symbol];
      return {
        id: inst.ref,
        width: sym?.body?.width ?? 120,
        height: sym?.body?.height ?? 80
      };
    }),
    edges: edges.map(([source, target], idx) => ({
      id: `e${idx}_${source}_${target}`,
      sources: [source],
      targets: [target]
    }))
  };
}

function resolveElkLayoutResult(elkRuntime, graph) {
  const ElkCtor = elkRuntime.ElkCtor;
  if (typeof ElkCtor !== "function") {
    return {
      ok: false,
      reason: "invalid_runtime",
      message: "ELK runtime did not provide a constructor."
    };
  }
  let instance;
  try {
    instance = new ElkCtor();
  } catch (err) {
    return {
      ok: false,
      reason: "constructor_failed",
      message: `ELK constructor failed: ${err instanceof Error ? err.message : String(err)}`
    };
  }

  let laidOut;
  if (typeof instance.layoutSync === "function") {
    laidOut = instance.layoutSync(graph);
  } else if (typeof instance.layout === "function") {
    laidOut = instance.layout(graph);
    if (laidOut && typeof laidOut.then === "function") {
      return {
        ok: false,
        reason: "async_runtime",
        message:
          "ELK runtime returned an asynchronous layout promise; synchronous backend placement is required. Using default engine."
      };
    }
  } else {
    return {
      ok: false,
      reason: "invalid_runtime",
      message: "ELK runtime did not expose layout/layoutSync."
    };
  }

  if (!laidOut || !Array.isArray(laidOut.children)) {
    return {
      ok: false,
      reason: "invalid_result",
      message: "ELK runtime returned an invalid graph result."
    };
  }
  return { ok: true, graph: laidOut };
}

function layoutAndRouteElk(model, options = {}, nativeLayout) {
  const elkRuntime = resolveElkRuntime(options.elk_runtime_module);
  if (!elkRuntime.ok) {
    return {
      ok: false,
      warning: {
        code: "elk_layout_unavailable_fallback",
        message: elkRuntime.message
      }
    };
  }

  const graph = buildElkGraph(model);
  const elkResult = resolveElkLayoutResult(elkRuntime, graph);
  if (!elkResult.ok) {
    return {
      ok: false,
      warning: {
        code:
          elkResult.reason === "async_runtime"
            ? "elk_layout_async_runtime_fallback"
            : "elk_layout_invalid_result_fallback",
        message: elkResult.message
      }
    };
  }

  const renderMode = options.render_mode === "explicit" ? "explicit" : DEFAULT_RENDER_MODE;
  const respectLocks = options.respect_locks ?? true;
  const autoRotate = options.auto_rotate ?? true;
  const preservePlacement = options.preserve_placement !== false;
  const rank = computeRanks(model).rank;
  const laneProfiles = refLaneProfiles(model);
  const minRank = Math.min(...[...rank.values(), 1]);
  const nativeByRef = new Map((nativeLayout?.placed ?? []).map((inst) => [inst.ref, inst]));
  const elkByRef = new Map(
    elkResult.graph.children
      .filter((child) => child && typeof child.id === "string")
      .map((child) => [child.id, child])
  );

  const placed = [...model.instances]
    .sort((a, b) => a.ref.localeCompare(b.ref))
    .map((inst) => {
      const sym = model.symbols[inst.symbol];
      const locked = respectLocks ? Boolean(inst.placement?.locked) : false;
      const keepExisting = preservePlacement && hasValidPlacement(inst);
      const keepLocked = locked && hasValidPlacement(inst);
      const native = nativeByRef.get(inst.ref);
      const elkNode = elkByRef.get(inst.ref);
      let x = hasValidPlacement(inst) ? Number(inst.placement.x) : Number.NaN;
      let y = hasValidPlacement(inst) ? Number(inst.placement.y) : Number.NaN;

      if (!keepExisting && !keepLocked) {
        if (elkNode && Number.isFinite(elkNode.x) && Number.isFinite(elkNode.y)) {
          const localRank = Math.max(0, (rank.get(inst.ref) ?? 1) - minRank);
          const lane = laneProfiles.get(inst.ref)?.laneIndex ?? 2;
          const rankTargetX = MARGIN_X + localRank * (COLUMN_GAP * 0.84);
          const laneTargetY = MARGIN_Y + lane * (ROW_GAP * 0.66);
          const rawX = toGrid(MARGIN_X + Number(elkNode.x));
          const rawY = toGrid(MARGIN_Y + Number(elkNode.y));
          x = toGrid(rawX * 0.7 + rankTargetX * 0.3);
          y = toGrid(rawY * 0.62 + laneTargetY * 0.38);
        } else if (native?.placement && Number.isFinite(native.placement.x) && Number.isFinite(native.placement.y)) {
          x = native.placement.x;
          y = native.placement.y;
        }
      }

      if (!Number.isFinite(x) || !Number.isFinite(y)) {
        if (native?.placement && Number.isFinite(native.placement.x) && Number.isFinite(native.placement.y)) {
          x = native.placement.x;
          y = native.placement.y;
        } else {
          x = toGrid(MARGIN_X);
          y = toGrid(MARGIN_Y);
        }
      }

      return {
        ...inst,
        placement: {
          x: toGrid(Math.max(MARGIN_X, x)),
          y: toGrid(Math.max(MARGIN_Y, y)),
          rotation: normalizeRotation(inst.placement?.rotation ?? 0),
          locked
        }
      };
    });

  const placedMap = new Map(placed.map((inst) => [inst.ref, inst]));
  applyAutoRotation(model, placedMap, { autoRotate });
  applyAlignmentConstraints(placedMap, model.constraints);
  applyNearConstraints(model, placedMap, model.constraints);
  resolvePlacementOverlaps(model, placedMap, { respectLocks });
  enforceFinalComponentSeparation(model, placedMap, { respectLocks });
  const normalizedPlaced = [...model.instances]
    .sort((a, b) => a.ref.localeCompare(b.ref))
    .map((inst) => placedMap.get(inst.ref) ?? inst);

  const bounds = buildBounds(model, normalizedPlaced);
  const normalizedMap = new Map(normalizedPlaced.map((inst) => [inst.ref, inst]));
  const { routed, busGroups } = routeAllNets(model, normalizedPlaced, normalizedMap, bounds, {
    renderMode
  });

  return {
    ok: true,
    layout: {
      placed: normalizedPlaced,
      routed,
      width: bounds.maxX,
      height: bounds.maxY,
      bus_groups: busGroups,
      metrics: computeLayoutMetrics(routed, busGroups),
      render_mode_used: renderMode
    }
  };
}

export function layoutAndRoute(model, options = {}) {
  const requestedEngine = requestedLayoutEngine(options);
  const nativeLayout = layoutAndRouteNative(model, options);

  if (requestedEngine !== "elk") {
    return {
      ...nativeLayout,
      layout_engine_requested: requestedEngine,
      layout_engine_used: DEFAULT_LAYOUT_ENGINE,
      layout_warnings: []
    };
  }

  const elkLayout = layoutAndRouteElk(model, options, nativeLayout);
  if (!elkLayout.ok) {
    return {
      ...nativeLayout,
      layout_engine_requested: "elk",
      layout_engine_used: DEFAULT_LAYOUT_ENGINE,
      layout_warnings: [elkLayout.warning]
    };
  }

  return {
    ...elkLayout.layout,
    layout_engine_requested: "elk",
    layout_engine_used: "elk",
    layout_warnings: []
  };
}

export function netAnchorPoint(net, model, placed) {
  const first = net.nodes[0];
  if (!first) {
    return null;
  }

  const inst = placed.find((x) => x.ref === first.ref);
  if (!inst) {
    return null;
  }

  const sym = model.symbols[inst.symbol];
  const p = sym.pins.find((x) => x.name === first.pin);
  if (!p) {
    return null;
  }

  return pinPoint(inst, p, sym.body.width, sym.body.height);
}
