// Causal graph renderer for DAEV Arbiter
// - Manual coords (0..1 normalized) from fixture, upgraded with dagre-like tweaks
// - Progressive node/edge reveal animation (SSE sim)
// - Taint path highlight with single-pulse
// - Supports LR (left-right) and TB (top-bottom) orientation

const { useState: uS, useEffect: uE, useRef: uR, useMemo: uM, useCallback: uC, useLayoutEffect: uLE } = React;

// Entity type styling — a visual vocabulary per kind
const ENTITY_STYLE = {
  source:     { glyph: 'e_source',     label: 'SOURCE',     semTone: 'entry'  },
  sink:       { glyph: 'e_sink',       label: 'SINK',       semTone: 'exit'   },
  sanitizer:  { glyph: 'e_sanitizer',  label: 'SANITIZER',  semTone: 'guard'  },
  gate:       { glyph: 'e_gate',       label: 'GATE',       semTone: 'mode'   },
  transform:  { glyph: 'e_transform',  label: 'TRANSFORM',  semTone: 'op'     },
  passthrough:{ glyph: 'e_passthrough',label: 'PASS',       semTone: 'op'     },
  use:        { glyph: 'e_use',        label: 'USE',        semTone: 'ref'    },
  definition: { glyph: 'e_definition', label: 'DEF',        semTone: 'bind'   },
};

// Edge stroke pattern by kind
function edgeDash(kind) {
  if (kind === 'def-use') return null;
  if (kind === 'control-dep') return '6 4';
  if (kind === 'synthetic-flow') return '1 4';
  if (kind === 'cycle-broken') return '3 3';
  return null;
}

// Smooth cubic path between two points
function cubicPath(x1, y1, x2, y2, dir = 'LR') {
  const dx = x2 - x1, dy = y2 - y1;
  if (dir === 'LR') {
    const k = Math.max(40, Math.abs(dx) * 0.45);
    return `M ${x1} ${y1} C ${x1+k} ${y1}, ${x2-k} ${y2}, ${x2} ${y2}`;
  } else {
    const k = Math.max(30, Math.abs(dy) * 0.45);
    return `M ${x1} ${y1} C ${x1} ${y1+k}, ${x2} ${y2-k}, ${x2} ${y2}`;
  }
}

// For TB (top-down) orientation, swap normalized x,y
function coord(pos, dir, W, H, padX, padY) {
  const nx = dir === 'TB' ? pos.y : pos.x;
  const ny = dir === 'TB' ? pos.x : pos.y;
  return {
    x: padX + nx * (W - padX * 2),
    y: padY + ny * (H - padY * 2),
  };
}

// ─────────────────────────────────────────────────────────────────────────
// Slice 6.4.5.2 Phase 9 (BUG-73): real layout for wired-fixture graphs.
//
// The wiring transform leaves `positions: {}` so static fixtures (which
// carry authored positions) keep rendering exactly as they did. Wired
// fixtures get an automatic layout — Dagre when its vendor script loaded,
// topological-layered fallback otherwise (no flat-collapse to 0.5/0.5).
// ─────────────────────────────────────────────────────────────────────────
function computeAutoLayout(nodes, edges, direction, size, padX, padY) {
  if (!nodes || nodes.length === 0) return [];
  if (nodes.length === 1) {
    const p = coord({ x: 0.5, y: 0.5 }, direction, size.w, size.h, padX, padY);
    return [{ ...nodes[0], x: p.x, y: p.y }];
  }
  if (typeof window !== 'undefined' && window.dagre && window.dagre.graphlib) {
    return computeDagreLayout(nodes, edges, direction, size, padX, padY);
  }
  return computeLayeredLayoutFallback(nodes, edges, direction, size, padX, padY);
}

function computeDagreLayout(nodes, edges, direction, size, padX, padY) {
  const g = new window.dagre.graphlib.Graph();
  g.setGraph({
    rankdir: direction === 'TB' ? 'TB' : 'LR',
    nodesep: 40,
    ranksep: 80,
    marginx: 20,
    marginy: 20,
  });
  g.setDefaultEdgeLabel(() => ({}));

  const NODE_W = 140;
  const NODE_H = 50;
  nodes.forEach(n => g.setNode(n.id, { width: NODE_W, height: NODE_H }));
  const nodeIds = new Set(nodes.map(n => n.id));
  (edges || []).forEach(e => {
    if (nodeIds.has(e.from) && nodeIds.has(e.to)) {
      g.setEdge(e.from, e.to);
    }
  });

  try {
    window.dagre.layout(g);
  } catch (err) {
    console.warn('[daev-graph] dagre layout threw; falling back to layered:', err && err.message);
    return computeLayeredLayoutFallback(nodes, edges, direction, size, padX, padY);
  }

  const dim = g.graph();
  const totalW = Math.max(1, dim.width || 1);
  const totalH = Math.max(1, dim.height || 1);

  return nodes.map(n => {
    const dn = g.node(n.id);
    if (!dn) {
      const p = coord({ x: 0.5, y: 0.5 }, direction, size.w, size.h, padX, padY);
      return { ...n, x: p.x, y: p.y };
    }
    const xFrac = dn.x / totalW;
    const yFrac = dn.y / totalH;
    const p = coord({ x: xFrac, y: yFrac }, direction, size.w, size.h, padX, padY);
    return { ...n, x: p.x, y: p.y };
  });
}

function computeLayeredLayoutFallback(nodes, edges, direction, size, padX, padY) {
  // Kahn's algorithm for layered topological layout — used when Dagre is
  // unavailable. Spreads nodes by depth (left→right) and rank (top→bottom).
  const inDegree = {};
  const outAdj = {};
  nodes.forEach(n => { inDegree[n.id] = 0; outAdj[n.id] = []; });
  (edges || []).forEach(e => {
    if (inDegree[e.to] != null) inDegree[e.to]++;
    if (outAdj[e.from]) outAdj[e.from].push(e.to);
  });

  const layer = {};
  let queue = nodes.filter(n => inDegree[n.id] === 0).map(n => n.id);
  let depth = 0;
  while (queue.length > 0) {
    const next = [];
    queue.forEach(id => {
      layer[id] = depth;
      (outAdj[id] || []).forEach(neighbor => {
        if (--inDegree[neighbor] === 0) next.push(neighbor);
      });
    });
    queue = next;
    depth++;
  }
  // Cycle-broken edges may leave some nodes unranked; pin them at the back.
  nodes.forEach(n => { if (layer[n.id] == null) layer[n.id] = depth; });

  const byLayer = {};
  nodes.forEach(n => {
    const L = layer[n.id];
    (byLayer[L] = byLayer[L] || []).push(n);
  });
  const maxLayer = Math.max(...Object.values(layer));
  const layerCount = Math.max(1, maxLayer + 1);

  return nodes.map(n => {
    const L = layer[n.id];
    const peers = byLayer[L] || [];
    const idx = peers.findIndex(p => p.id === n.id);
    const xFrac = layerCount === 1 ? 0.5 : (L + 0.5) / layerCount;
    const yFrac = peers.length === 1 ? 0.5 : (idx + 0.5) / peers.length;
    const p = coord({ x: xFrac, y: yFrac }, direction, size.w, size.h, padX, padY);
    return { ...n, x: p.x, y: p.y };
  });
}

function CausalGraph({
  t,
  graph,
  revealedNodes = null, // array of ids, or null for all
  revealedEdges = null,
  taintPath = null,     // array of ids to highlight
  pulseKey = 0,         // bump to trigger single pulse
  direction = 'LR',
  onHoverNode,
  hoveredNode,
  nodeStyle = 'dots',   // 'dots' | 'shapes' | 'cards'
}) {
  const boxRef = uR(null);
  const [size, setSize] = uS({ w: 800, h: 500 });

  uLE(() => {
    if (!boxRef.current) return;
    const ro = new ResizeObserver(entries => {
      for (const e of entries) {
        const cr = e.contentRect;
        setSize({ w: cr.width, h: cr.height });
      }
    });
    ro.observe(boxRef.current);
    return () => ro.disconnect();
  }, []);

  const padX = 80, padY = 60;
  const nodesMap = uM(() => Object.fromEntries(graph.nodes.map(n => [n.id, n])), [graph]);
  const taintSet = uM(() => new Set(taintPath || []), [taintPath]);
  const taintEdges = uM(() => {
    if (!taintPath) return new Set();
    const s = new Set();
    for (let i=0;i<taintPath.length-1;i++) {
      const a = taintPath[i], b = taintPath[i+1];
      // match edge from..to (or either direction)
      const e = graph.edges.find(e => (e.from===a && e.to===b));
      if (e) s.add(e.id);
    }
    return s;
  }, [taintPath, graph]);

  const revealedN = uM(() => revealedNodes ? new Set(revealedNodes) : new Set(graph.nodes.map(n=>n.id)), [revealedNodes, graph]);
  const revealedE = uM(() => revealedEdges ? new Set(revealedEdges) : new Set(graph.edges.map(e=>e.id)), [revealedEdges, graph]);

  // Slice 6.4.5.2 Phase 9 (BUG-73): when graph.positions is empty (every wired
  // audit, since the transform sets positions: {}), compute a real layout via
  // Dagre — falling back to a topological-layered layout if the vendor script
  // didn't load. Static fixtures retain their authored positions.
  const positioned = uM(() => {
    const hasAuthoredPositions = graph.positions && Object.keys(graph.positions).length > 0;
    if (hasAuthoredPositions) {
      return graph.nodes.map(n => {
        const p = coord(
          graph.positions[n.id] || { x: 0.5, y: 0.5 },
          direction, size.w, size.h, padX, padY,
        );
        return { ...n, x: p.x, y: p.y };
      });
    }
    return computeAutoLayout(graph.nodes, graph.edges, direction, size, padX, padY);
  }, [graph, direction, size]);

  const nodeRadius = nodeStyle === 'cards' ? 0 : nodeStyle === 'shapes' ? 14 : 10;

  return (
    <div ref={boxRef} style={{ position:'relative', width:'100%', height:'100%', overflow:'hidden' }}>
      {/* background grid */}
      <svg width="100%" height="100%" style={{ position:'absolute', inset:0, pointerEvents:'none' }}>
        <defs>
          <pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse">
            <path d="M 32 0 L 0 0 0 32" fill="none" stroke={t.line} strokeOpacity="0.35" strokeWidth="1"/>
          </pattern>
          <pattern id="gridMinor" width="8" height="8" patternUnits="userSpaceOnUse">
            <circle cx="1" cy="1" r="0.6" fill={t.line} fillOpacity="0.5"/>
          </pattern>
          <radialGradient id="taintGlow">
            <stop offset="0%" stopColor={t.semantic.red} stopOpacity="0.5"/>
            <stop offset="70%" stopColor={t.semantic.red} stopOpacity="0.05"/>
            <stop offset="100%" stopColor={t.semantic.red} stopOpacity="0"/>
          </radialGradient>
          <filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
            <feGaussianBlur stdDeviation="4"/>
          </filter>
        </defs>
        <rect width="100%" height="100%" fill="url(#gridMinor)"/>
      </svg>

      {/* Edges SVG layer */}
      <svg width="100%" height="100%" style={{ position:'absolute', inset:0, overflow:'visible' }}>
        {graph.edges.map(e => {
          if (!revealedE.has(e.id)) return null;
          const a = positioned.find(n => n.id === e.from);
          const b = positioned.find(n => n.id === e.to);
          if (!a || !b) return null;
          const isTaint = taintEdges.has(e.id);
          const dash = edgeDash(e.kind);
          const d = cubicPath(a.x, a.y, b.x, b.y, direction);
          const color = isTaint ? t.semantic.red : t.fg3;

          return (
            <g key={e.id} className="edge" style={{
              animation: `daev-edge-draw 260ms ease-out both`,
            }}>
              {isTaint && (
                <path d={d} stroke={t.semantic.red} strokeWidth="6" fill="none"
                  opacity="0.35" filter="url(#softGlow)" strokeLinecap="round"/>
              )}
              <path d={d}
                stroke={color}
                strokeWidth={isTaint ? 2 : 1}
                strokeDasharray={dash || undefined}
                fill="none"
                strokeLinecap="round"
                opacity={isTaint ? 1 : 0.75}
              />
              {/* arrow head */}
              <ArrowHead x={b.x} y={b.y} toward={{x:a.x,y:a.y}} color={color} taint={isTaint} />
              {/* cycle-broken decoration */}
              {e.kind === 'cycle-broken' && (
                <g transform={`translate(${(a.x+b.x)/2},${(a.y+b.y)/2})`}>
                  <rect x="-8" y="-7" width="16" height="14" fill={t.bg0} stroke={t.fg3} strokeWidth="0.5"/>
                  <text textAnchor="middle" y="4" fontSize="9" fontFamily={t.font.mono} fill={t.fg2}>⇌</text>
                </g>
              )}
            </g>
          );
        })}

        {/* Nodes */}
        {positioned.map((n, idx) => {
          if (!revealedN.has(n.id)) return null;
          const style = ENTITY_STYLE[n.kind] || ENTITY_STYLE.definition;
          const onPath = taintSet.has(n.id);
          const isHover = hoveredNode === n.id;

          const nodeColor = onPath ? t.semantic.red : (nodeStyle === 'dots' ? t.fg1 : t.fg0);
          const nodeBg = onPath
            ? `color-mix(in oklch, ${t.semantic.red} 30%, ${t.bg1})`
            : t.bg1;

          return (
            <g key={n.id}
              onMouseEnter={() => onHoverNode && onHoverNode(n.id)}
              onMouseLeave={() => onHoverNode && onHoverNode(null)}
              style={{
                cursor: 'pointer',
                animation: `daev-node-in 220ms ${idx*30}ms ease-out both`,
              }}
              transform={`translate(${n.x},${n.y})`}
              className={onPath && pulseKey ? 'daev-pulse' : ''}
              data-pulse={pulseKey}
            >
              {/* Taint glow */}
              {onPath && (
                <circle r="26" fill="url(#taintGlow)" style={{ pointerEvents:'none' }}/>
              )}

              {/* Node shape based on style */}
              {nodeStyle === 'cards' ? (
                <NodeCard t={t} node={n} style={style} color={nodeColor} bg={nodeBg} onPath={onPath} hover={isHover}/>
              ) : nodeStyle === 'shapes' ? (
                <NodeShape t={t} kind={n.kind} r={nodeRadius} color={nodeColor} bg={nodeBg} onPath={onPath} hover={isHover}/>
              ) : (
                <NodeDot t={t} kind={n.kind} r={nodeRadius} color={nodeColor} bg={nodeBg} onPath={onPath} hover={isHover}/>
              )}

              {/* Label — taint nodes get above-anchor placement to escape edge gutter;
                   regular nodes stay right of the dot. paint-order halo prevents
                   edge crossings from bleeding through glyphs. */}
              {nodeStyle !== 'cards' && (() => {
                const labelAbove = onPath;
                const tx = labelAbove ? 0 : nodeRadius + 10;
                const ty = labelAbove ? -(nodeRadius + 16) : 4;
                const anchor = labelAbove ? 'middle' : 'start';
                const haloStyle = {
                  paintOrder: 'stroke fill',
                  stroke: t.bg0,
                  strokeWidth: 4,
                  strokeLinejoin: 'round',
                };
                return (
                  <g transform={`translate(${tx}, ${ty})`}>
                    <text fontFamily={t.font.mono} fontSize="11.5" fontWeight="600"
                      textAnchor={anchor}
                      style={haloStyle}
                      fill={onPath ? t.semantic.red : t.fg0}>
                      {n.label}
                    </text>
                    <text fontFamily={t.font.mono} fontSize="9" y="12"
                      textAnchor={anchor}
                      style={haloStyle}
                      fill={t.fg2} letterSpacing="0.5">
                      {style.label} · L{n.line}
                    </text>
                  </g>
                );
              })()}

              {/* Hover ring */}
              {isHover && nodeStyle !== 'cards' && (
                <circle r={nodeRadius + 4} fill="none" stroke={t.accent.base} strokeWidth="1" opacity="0.8"/>
              )}
            </g>
          );
        })}
      </svg>

      {/* keyframes */}
      <style>{`
        @keyframes daev-node-in {
          from { opacity: 0; }
          to   { opacity: 1; }
        }
        @keyframes daev-edge-draw {
          from { opacity: 0; }
          to   { opacity: 1; }
        }
        @keyframes daev-pulse-once {
          0%   { filter: drop-shadow(0 0 0 ${t.semantic.red}); }
          30%  { filter: drop-shadow(0 0 16px ${t.semantic.red}); }
          100% { filter: drop-shadow(0 0 0 ${t.semantic.red}); }
        }
        .daev-pulse[data-pulse]:not([data-pulse="0"]) {
          animation: daev-pulse-once 700ms ease-out;
        }
      `}</style>
    </div>
  );
}

function ArrowHead({ x, y, toward, color, taint }) {
  const dx = x - toward.x, dy = y - toward.y;
  const len = Math.hypot(dx, dy) || 1;
  const ux = dx/len, uy = dy/len;
  // Pull back so arrow tip lands at node edge (offset 14)
  const off = 14;
  const tipX = x - ux * off, tipY = y - uy * off;
  const s = 6;
  const lx = tipX - ux*s - uy*s*0.6;
  const ly = tipY - uy*s + ux*s*0.6;
  const rx = tipX - ux*s + uy*s*0.6;
  const ry = tipY - uy*s - ux*s*0.6;
  return <path d={`M ${tipX} ${tipY} L ${lx} ${ly} M ${tipX} ${tipY} L ${rx} ${ry}`}
    stroke={color} strokeWidth={taint ? 2 : 1} fill="none" strokeLinecap="round"/>;
}

// Node renderers
function NodeDot({ t, kind, r, color, bg, onPath, hover }) {
  // Small filled dots, with a ring variant for sources
  const style = ENTITY_STYLE[kind] || ENTITY_STYLE.definition;
  const fillMap = {
    source:    'none',
    sink:      color,
    sanitizer: bg,
    gate:      bg,
    transform: color,
    passthrough: bg,
    use:       'none',
    definition: color,
  };
  const strokeMap = {
    source: color, sink: color, sanitizer: color, gate: color,
    transform: color, passthrough: color, use: color, definition: color,
  };
  if (kind === 'gate') {
    return <rect x={-r} y={-r} width={r*2} height={r*2}
      fill={fillMap[kind]} stroke={strokeMap[kind]} strokeWidth="1.25"/>;
  }
  if (kind === 'transform') {
    return <polygon points={`0,${-r} ${r},0 0,${r} ${-r},0`}
      fill={fillMap[kind]} stroke={strokeMap[kind]} strokeWidth="1.25"/>;
  }
  if (kind === 'sanitizer') {
    return <g>
      <circle r={r} fill={fillMap[kind]} stroke={strokeMap[kind]} strokeWidth="1.25"/>
      <circle r={r*0.45} fill={color} stroke="none"/>
    </g>;
  }
  if (kind === 'use') {
    return <circle r={r} fill={fillMap[kind]} stroke={strokeMap[kind]} strokeWidth="1.25" strokeDasharray="2 2"/>;
  }
  if (kind === 'source') {
    return <g>
      <circle r={r} fill="none" stroke={color} strokeWidth="1.25"/>
      <circle r={r*0.4} fill={color}/>
    </g>;
  }
  return <circle r={r} fill={fillMap[kind]} stroke={strokeMap[kind]} strokeWidth="1.25"/>;
}

function NodeShape({ t, kind, r, color, bg, onPath, hover }) {
  // Bigger/more distinct shape variants
  return <NodeDot t={t} kind={kind} r={r} color={color} bg={bg} onPath={onPath} hover={hover}/>;
}

function NodeCard({ t, node, style, color, bg, onPath, hover }) {
  const W = 156, H = 42;
  return (
    <g transform={`translate(${-W/2}, ${-H/2})`}>
      <rect width={W} height={H} rx={t.radius.md}
        fill={bg}
        stroke={onPath ? t.semantic.red : (hover ? t.accent.base : t.line2)}
        strokeWidth={onPath ? 1.5 : 1}/>
      <g transform="translate(11, 15)">
        <Icon name={style.glyph} size={13} color={color}/>
      </g>
      <text x={32} y={18} fontFamily={t.font.mono} fontSize="11.5" fontWeight="600" fill={color}>
        {node.label.length > 18 ? node.label.slice(0,17)+'…' : node.label}
      </text>
      <text x={32} y={31} fontFamily={t.font.mono} fontSize="9" fill={t.fg2} letterSpacing="0.4">
        {style.label} · L{node.line}
      </text>
    </g>
  );
}

// Legend
function GraphLegend({ t }) {
  return (
    <div style={{
      display:'flex', gap: 18, alignItems:'center',
      padding:'8px 14px',
      fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
      letterSpacing: 0.5, textTransform:'uppercase',
    }}>
      <LegendEdge t={t} label="def-use" dash={null}/>
      <LegendEdge t={t} label="control-dep" dash="6 4"/>
      <LegendEdge t={t} label="synthetic-flow" dash="1 4"/>
      <LegendEdge t={t} label="cycle-broken" dash="3 3"/>
      <span style={{ flex:1 }}/>
      <div style={{ display:'flex', alignItems:'center', gap: 6 }}>
        <span style={{
          width: 8, height: 8, borderRadius: '50%',
          background: t.semantic.red,
          boxShadow: `0 0 10px ${t.semantic.red}`,
        }}/>
        on taint path
      </div>
    </div>
  );
}

function LegendEdge({ t, label, dash }) {
  return (
    <div style={{ display:'flex', alignItems:'center', gap:6 }}>
      <svg width="24" height="8">
        <line x1="0" y1="4" x2="24" y2="4" stroke={t.fg2} strokeWidth="1" strokeDasharray={dash || undefined}/>
      </svg>
      {label}
    </div>
  );
}

Object.assign(window, { CausalGraph, GraphLegend, ENTITY_STYLE });
