// Force-directed graph — the centerpiece.
// Uses d3-force (loaded from CDN in index.html) for the simulation. SVG render
// for the visual layer so state transitions are CSS-driven.

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

window.ForgeGraph = function ForgeGraph({
  nodes,
  edges,
  resolved,
  selected,
  hoveredCanon,
  onToggleSelect,
  onHoverNode,
  onHoverEdge,
}) {
  const svgRef = useRef(null);
  const containerRef = useRef(null);
  const simRef = useRef(null);
  const [size, setSize] = useState({ w: 1000, h: 700 });
  const [transform, setTransform] = useState({ x: 0, y: 0, k: 1 });
  const transformRef = useRef(transform);
  transformRef.current = transform;

  // resize observer
  useEffect(() => {
    if (!containerRef.current) return;
    const ro = new ResizeObserver(([e]) => {
      const r = e.contentRect;
      setSize({ w: Math.max(400, r.width), h: Math.max(400, r.height) });
    });
    ro.observe(containerRef.current);
    return () => ro.disconnect();
  }, []);

  // Build simulation. Pin nodes by canonical so they keep identity across re-renders.
  const simNodesRef = useRef(new Map());

  useEffect(() => {
    // (re)initialise simulation when node list size changes
    const w = size.w, h = size.h;
    const simNodes = nodes.map(n => {
      const existing = simNodesRef.current.get(n.canonical);
      if (existing) {
        existing.node = n;
        return existing;
      }
      const fresh = {
        canonical: n.canonical,
        node: n,
        x: w / 2 + (Math.random() - 0.5) * 200,
        y: h / 2 + (Math.random() - 0.5) * 200,
        vx: 0, vy: 0,
      };
      simNodesRef.current.set(n.canonical, fresh);
      return fresh;
    });

    const simEdges = edges.map(e => ({
      source: e.from,
      target: e.to,
      relation: e.relation,
    }));

    const sim = d3.forceSimulation(simNodes)
      .force("link", d3.forceLink(simEdges)
        .id(d => d.canonical)
        .distance(l => {
          // Tight cluster for compatible/requires, push apart for breaks
          if (l.relation === "BREAKS") return 230;
          if (l.relation === "REQUIRES") return 140;
          if (l.relation === "CONDITIONAL") return 160;
          if (l.relation === "DEGRADES") return 200;
          return 170;
        })
        .strength(l => {
          if (l.relation === "REQUIRES") return 0.6;
          if (l.relation === "BREAKS") return 0.15;
          return 0.25;
        })
      )
      .force("charge", d3.forceManyBody().strength(-580).distanceMax(450))
      .force("center", d3.forceCenter(w / 2, h / 2).strength(0.04))
      .force("collide", d3.forceCollide().radius(54).strength(0.9))
      // Group similar types together vertically
      .force("typeY", d3.forceY(d => typeYTarget(d.node.type, h)).strength(0.06))
      .alpha(1)
      .alphaDecay(0.025);

    simRef.current = sim;

    const renderTick = () => {
      // direct DOM update for perf
      const svg = svgRef.current;
      if (!svg) return;
      for (const n of simNodes) {
        const el = svg.querySelector(`g.node[data-canon="${cssEscape(n.canonical)}"]`);
        if (el) el.setAttribute("transform", `translate(${n.x.toFixed(2)},${n.y.toFixed(2)})`);
      }
      for (let i = 0; i < simEdges.length; i++) {
        const e = simEdges[i];
        const path = svg.querySelector(`g.edge[data-id="${i}"] path.edge-path`);
        if (path && e.source.x != null) {
          const x1 = e.source.x, y1 = e.source.y;
          const x2 = e.target.x, y2 = e.target.y;
          const dx = x2 - x1, dy = y2 - y1;
          const len = Math.sqrt(dx*dx + dy*dy) || 1;
          // perpendicular unit vector × offset amount (slight molten droop)
          const nx = -dy / len, ny = dx / len;
          const sag = Math.min(40, len * 0.18) * ((i % 2) ? 1 : -1) * (e.relation === "BREAKS" ? 0.6 : 1);
          const cx = (x1 + x2) / 2 + nx * sag;
          const cy = (y1 + y2) / 2 + ny * sag;
          const d = `M ${x1.toFixed(1)} ${y1.toFixed(1)} Q ${cx.toFixed(1)} ${cy.toFixed(1)} ${x2.toFixed(1)} ${y2.toFixed(1)}`;
          path.setAttribute("d", d);
          const glow = svg.querySelector(`g.edge[data-id="${i}"] path.edge-glow`);
          if (glow) glow.setAttribute("d", d);
        }
      }
    };
    sim.on("tick", renderTick);

    // Pre-tick a chunk so the layout settles before the first paint,
    // and so we have a fallback layout even if d3.timer doesn't fire
    // in this preview iframe.
    for (let i = 0; i < 80; i++) sim.tick();
    renderTick();

    // Belt-and-braces: drive ticks via a manual rAF loop in case d3.timer
    // is paused (some preview iframes throttle d3-timer).
    let rafHandle;
    const loop = () => {
      if (sim.alpha() > sim.alphaMin()) {
        sim.tick();
        renderTick();
      }
      rafHandle = requestAnimationFrame(loop);
    };
    rafHandle = requestAnimationFrame(loop);

    return () => { sim.stop(); cancelAnimationFrame(rafHandle); };
  }, [nodes.length, edges.length, size.w, size.h]);

  // Kick the simulation when selection changes so layout adapts to highlighted edges.
  useEffect(() => {
    if (simRef.current) {
      simRef.current.alpha(0.35).restart();
    }
  }, [selected.length]);

  // Pan & zoom (basic)
  useEffect(() => {
    const svg = svgRef.current;
    if (!svg) return;
    let dragging = false;
    let last = null;
    const onDown = (ev) => {
      // Only pan when target is not a node card
      if (ev.target.closest("g.node")) return;
      dragging = true;
      last = { x: ev.clientX, y: ev.clientY };
      svg.style.cursor = "grabbing";
    };
    const onMove = (ev) => {
      if (!dragging) return;
      const dx = ev.clientX - last.x;
      const dy = ev.clientY - last.y;
      last = { x: ev.clientX, y: ev.clientY };
      setTransform(t => ({ ...t, x: t.x + dx, y: t.y + dy }));
    };
    const onUp = () => { dragging = false; svg.style.cursor = "grab"; };
    const onWheel = (ev) => {
      ev.preventDefault();
      const dir = ev.deltaY < 0 ? 1.1 : 1 / 1.1;
      setTransform(t => {
        const k = Math.max(0.4, Math.min(2.2, t.k * dir));
        // zoom centered on cursor
        const rect = svg.getBoundingClientRect();
        const cx = ev.clientX - rect.left;
        const cy = ev.clientY - rect.top;
        const dx = (cx - t.x) * (1 - dir);
        const dy = (cy - t.y) * (1 - dir);
        return { x: t.x + dx, y: t.y + dy, k };
      });
    };
    svg.addEventListener("mousedown", onDown);
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    svg.addEventListener("wheel", onWheel, { passive: false });
    return () => {
      svg.removeEventListener("mousedown", onDown);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      svg.removeEventListener("wheel", onWheel);
    };
  }, []);

  const resetView = () => setTransform({ x: 0, y: 0, k: 1 });
  const zoom = (dir) => setTransform(t => ({ ...t, k: Math.max(0.4, Math.min(2.2, t.k * dir)) }));

  const selectedSet = useMemo(() => new Set(selected), [selected]);

  // map edges to their indices so the simulation can find them
  const edgeData = useMemo(() => edges.map((e, i) => ({ ...e, _i: i })), [edges]);

  // Which edges are "hot" — incident to a selected node
  const hotEdge = (e) => selectedSet.has(e.from) || selectedSet.has(e.to);

  // For "needs-by" rendering: which nodes are amber because some selected node requires them?
  const neededBy = useMemo(() => {
    const m = {};
    for (const e of edges) {
      if (e.relation === "REQUIRES" && selectedSet.has(e.from) && !selectedSet.has(e.to)) {
        (m[e.to] = m[e.to] || []).push(e.from);
      }
    }
    return m;
  }, [edges, selectedSet]);

  // Helpers
  function classFor(canon) {
    const r = resolved[canon];
    if (!r) return "s-available";
    return "s-" + r.status;
  }

  return (
    <div className="stage" ref={containerRef}>
      <svg
        ref={svgRef}
        className="graph"
        width={size.w} height={size.h}
        viewBox={`0 0 ${size.w} ${size.h}`}
        preserveAspectRatio="xMidYMid meet"
      >
        <defs>
          {/* Molten plate gradient — the "just-forged" red-hot interior */}
          <radialGradient id="grad-molten-plate" cx="50%" cy="55%" r="65%">
            <stop offset="0%"  stopColor="#5a1f0a" />
            <stop offset="35%" stopColor="#3a1408" />
            <stop offset="70%" stopColor="#1a0807" />
            <stop offset="100%" stopColor="#0a0303" />
          </radialGradient>
          {/* Forge ember glow filter (used on selected plates) */}
          <filter id="node-ember-glow" x="-50%" y="-50%" width="200%" height="200%">
            <feGaussianBlur stdDeviation="2.6" result="blur" />
            <feFlood floodColor="#ff6a1f" floodOpacity="0.55" />
            <feComposite in2="blur" operator="in" />
            <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
          </filter>
          {/* Painterly noise overlay for plate backgrounds */}
          <filter id="plate-grain" x="0" y="0" width="100%" height="100%">
            <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="3" />
            <feColorMatrix values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.15 0" />
            <feComposite in2="SourceGraphic" operator="in" />
          </filter>
          {/* Arrow for REQUIRES edges */}
          <marker id="arrow-requires" viewBox="0 0 10 10" refX="9" refY="5"
                  markerWidth="6" markerHeight="6" orient="auto-start-reverse">
            <path d="M0 0 L10 5 L0 10 z" fill="rgba(212,164,68,0.7)" />
          </marker>
          {/* Tier-rarity coin gradients — T1 legendary, T2 rare, T3 common */}
          <radialGradient id="brass-t1" cx="40%" cy="35%" r="75%">
            <stop offset="0%"  stopColor="#ffd09a" />
            <stop offset="45%" stopColor="#ff8a3d" />
            <stop offset="100%" stopColor="#6a2a08" />
          </radialGradient>
          <radialGradient id="brass-t2" cx="40%" cy="35%" r="75%">
            <stop offset="0%"  stopColor="#b8d4f0" />
            <stop offset="45%" stopColor="#5b9bd8" />
            <stop offset="100%" stopColor="#1c3a5a" />
          </radialGradient>
          <radialGradient id="brass-t3" cx="40%" cy="35%" r="75%">
            <stop offset="0%"  stopColor="#cac4b6" />
            <stop offset="45%" stopColor="#9a9285" />
            <stop offset="100%" stopColor="#3a3528" />
          </radialGradient>
        </defs>

        <g transform={`translate(${transform.x},${transform.y}) scale(${transform.k})`}>
          {/* Edges */}
          <g className="edges">
            {edgeData.map((e, i) => {
              const hot = hotEdge(e);
              const anySelected = selectedSet.size > 0;
              const faded = anySelected && !hot && e.relation !== "BREAKS";
              const relCls = e.relation.toLowerCase();
              return (
                <g key={i}
                   className={`edge ${relCls} ${hot ? "hot" : ""} ${faded ? "faded" : ""}`}
                   data-id={i}
                   onMouseEnter={ev => onHoverEdge && onHoverEdge(e, ev)}
                   onMouseLeave={() => onHoverEdge && onHoverEdge(null)}
                >
                  {/* glow overlay drawn first (under) for hot edges */}
                  <path className="edge-glow" />
                  <path className="edge-path"
                        markerEnd={e.relation === "REQUIRES" ? "url(#arrow-requires)" : undefined} />
                </g>
              );
            })}
          </g>

          {/* Nodes — ornate brass-framed runestone plaques */}
          <g className="nodes">
            {nodes.map(n => {
              const meta = window.TYPE_META[n.type] || window.TYPE_META.technique;
              const status = (resolved[n.canonical] || {}).status || "available";
              const needs = neededBy[n.canonical] || [];
              const isSel = selectedSet.has(n.canonical);
              const cls = isSel ? "s-selected" : ("s-" + status);

              // Octagon vertices (flat-top, 64×64). Computed once below.
              const OCT_OUTER = "-13.3,-32 13.3,-32 32,-13.3 32,13.3 13.3,32 -13.3,32 -32,13.3 -32,-13.3";
              const OCT_BRASS = "-11.2,-27 11.2,-27 27,-11.2 27,11.2 11.2,27 -11.2,27 -27,11.2 -27,-11.2";
              const OCT_HILT  = "-9.5,-23 9.5,-23 23,-9.5 23,9.5 9.5,23 -9.5,23 -23,9.5 -23,-9.5";
              // Four corner stud positions (on the diagonal short edges)
              const STUDS = [[-22.6,-22.6], [22.6,-22.6], [22.6,22.6], [-22.6,22.6]];
              const tierGrad = `url(#brass-t${n.tier})`;

              const needsLabel = needs.length > 0 ? `needs ${truncate(displayName(needs[0]), 14)}` : "";
              const chipW = Math.max(70, needsLabel.length * 5 + 16);

              return (
                <g
                  key={n.canonical}
                  className={`node ${cls}`}
                  data-canon={n.canonical}
                  style={{ ["--type-color"]: meta.color }}
                  onClick={() => onToggleSelect(n.canonical)}
                  onMouseEnter={ev => onHoverNode(n, ev)}
                  onMouseLeave={() => onHoverNode(null)}
                >
                  {/* outer pulse halo (only visible per state) */}
                  <polygon className="node-aura" points={OCT_OUTER}
                           transform="scale(1.25)" />

                  {/* outer black/iron bezel */}
                  <polygon className="node-frame-outer" points={OCT_OUTER} />

                  {/* the stone plate (interior) */}
                  <polygon className="node-plate" points={OCT_BRASS} />

                  {/* type-colored gem glow behind icon */}
                  <ellipse className="node-gem" cx="0" cy="-2" rx="14" ry="11" />

                  {/* brass frame ring */}
                  <polygon className="node-frame" points={OCT_BRASS} />
                  <polygon className="node-frame-inner" points={OCT_HILT} />

                  {/* brass corner studs */}
                  {STUDS.map(([sx,sy], si) => (
                    <circle key={si} className="node-stud" cx={sx} cy={sy} r="1.8" />
                  ))}

                  {/* icon — type glyph */}
                  <g className="node-icon" transform="translate(-12,-14) scale(1.0)"
                     dangerouslySetInnerHTML={{ __html: meta.icon }} />

                  {/* name ribbon below the plate */}
                  <path className="node-ribbon"
                        d="M -33 33 L -38 47 L -28 44 L -22 48 L -16 44 L -10 48 L -4 44 L 2 48 L 8 44 L 14 48 L 20 44 L 26 48 L 34 47 L 33 33 Z" />
                  <text className="node-name" y="44.5">{truncate(n.name, 14)}</text>

                  {/* tier wax-seal medallion top-right */}
                  <g className="node-seal" transform="translate(22,-22)">
                    <circle className="node-seal-disc" r="8.5" fill={tierGrad} />
                    <circle r="6.5" fill="none" stroke="rgba(0,0,0,0.4)" strokeWidth="0.5" />
                    <text y="2.5">T{n.tier}</text>
                  </g>

                  {/* lock graphic for blocked state — over the icon, ominous */}
                  <g className="node-lock" transform="translate(0,-2)">
                    <path className="lock-shackle" d="M -5 -2 v -4 a 5 5 0 0 1 10 0 v 4" />
                    <rect className="lock-body" x="-7" y="-2" width="14" height="11" rx="1.5" />
                    <circle className="lock-keyhole" cx="0" cy="2" r="1.4" />
                    <rect className="lock-keyhole" x="-0.5" y="2" width="1" height="3.5" />
                  </g>

                  {/* "needs X" pinned scroll-tag for conditional */}
                  {(status === "conditional" && needs.length > 0) && (
                    <g className="needs-chip" transform="translate(0,60)">
                      <rect x={-chipW/2} y="-7" width={chipW} height="14" rx="2" />
                      <circle className="chip-pin" cx={-chipW/2} cy="0" r="1.5" />
                      <circle className="chip-pin" cx={chipW/2}  cy="0" r="1.5" />
                      <text y="2.5">{needsLabel}</text>
                    </g>
                  )}
                </g>
              );
            })}
          </g>
        </g>
      </svg>

      {/* corner controls */}
      <div className="stage-controls">
        <button className="ctrl" onClick={() => zoom(1.15)} title="Zoom in">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3M8 11h6M11 8v6"/></svg>
        </button>
        <button className="ctrl" onClick={() => zoom(1/1.15)} title="Zoom out">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3M8 11h6"/></svg>
        </button>
        <button className="ctrl" onClick={resetView} title="Reset view">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5"/></svg>
        </button>
      </div>

      <div className="stage-toolbar">
        <span className="pill"><span className="dot" style={{ background: "var(--ember-1)", boxShadow: "0 0 6px var(--ember-1)"}}/>The Forge</span>
        <span className="pill" title="API base URL">
          api · localhost:8010
        </span>
      </div>

      {selectedSet.size === 0 && (
        <div className="stage-hint">
          <div className="ring" />
          <h3>Lay an ingredient on the anvil</h3>
          <p>Pick a component from the inventory.<br/>Incompatible nodes will grey out with citations.</p>
        </div>
      )}

      <div className="anvil-watermark">forge · 2026</div>
    </div>
  );
};

// helpers
function typeYTarget(type, h) {
  const order = ["architecture", "technique", "optimizer", "scheduler", "quantization", "inference"];
  const i = order.indexOf(type);
  if (i === -1) return h / 2;
  // distribute around vertical center with mild attraction
  const span = h * 0.55;
  return (h / 2) + (i - (order.length - 1) / 2) * (span / order.length);
}

function cssEscape(s) {
  if (window.CSS && window.CSS.escape) return window.CSS.escape(s);
  return String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
}

function displayName(canon) {
  const n = (window.FORGE_NODES || []).find(x => x.canonical === canon);
  return n ? n.name : canon;
}
function truncate(s, n) { return s && s.length > n ? s.slice(0, n - 1) + "…" : s; }
