/* ============================================================
   DEPENDENCY GRAPH
   ============================================================ */
const NODE_W = 168, NODE_H = 44, LAYER_GAP = 108;

function layoutGraph(nodes, links, width, height){
  const n = nodes.length;
  if (!n) return [];
  const pos = nodes.map((nd, i) => {
    const angle = (i / n) * Math.PI * 2;
    return {
      id: nd.id, node: nd,
      x: width/2 + Math.cos(angle) * (60 + n * 5) + (Math.random()-0.5)*30,
      y: height/2 - nd.layer * LAYER_GAP + (Math.random()-0.5)*20,
      vx:0, vy:0
    };
  });
  const byId = Object.fromEntries(pos.map(p=>[p.id,p]));
  const cx = width/2, cy = height/2;
  const idealLink = 150;

  for (let step=0; step<420; step++){
    const cool = 1 - step/460;
    for (let i=0;i<pos.length;i++){
      for (let j=i+1;j<pos.length;j++){
        const a = pos[i], b = pos[j];
        let dx = b.x - a.x, dy = (b.y - a.y) * 1.7;
        let d2 = dx*dx + dy*dy;
        if (d2 < 1) { dx = (Math.random()-0.5); dy = (Math.random()-0.5); d2 = 1; }
        const d = Math.sqrt(d2);
        const min = 210;
        if (d < min){
          const f = ((min - d) / d) * 0.45 * cool;
          a.vx -= dx*f; a.vy -= dy*f*0.35;
          b.vx += dx*f; b.vy += dy*f*0.35;
        }
        const rep = (5200 / d2) * cool;
        a.vx -= (dx/d)*rep; b.vx += (dx/d)*rep;
      }
    }
    links.forEach(l => {
      const a = byId[l.source], b = byId[l.target];
      if (!a || !b) return;
      const dx = b.x - a.x, dy = b.y - a.y;
      const d = Math.max(1, Math.sqrt(dx*dx + dy*dy));
      const f = ((d - idealLink) / d) * 0.045 * cool;
      a.vx += dx*f; a.vy += dy*f*0.18;
      b.vx -= dx*f; b.vy -= dy*f*0.18;
    });
    pos.forEach(p => {
      const ty = cy - p.node.layer * LAYER_GAP;
      p.vy += (ty - p.y) * 0.22;
      p.vx += (cx - p.x) * 0.006;
      p.x += (p.vx *= 0.80);
      p.y += (p.vy *= 0.80);
    });
  }

  // Snap onto layer lines and guarantee horizontal clearance, keeping the
  // left-to-right order the simulation settled on.
  const groups = {};
  pos.forEach(p => { (groups[p.node.layer] = groups[p.node.layer] || []).push(p); });
  Object.keys(groups).forEach(k => {
    const g = groups[k].sort((a,b) => a.x - b.x);
    const gap = NODE_W + 28;
    for (let i=1; i<g.length; i++) if (g[i].x - g[i-1].x < gap) g[i].x = g[i-1].x + gap;
    const mean = g.reduce((s,p)=>s+p.x, 0) / g.length;
    g.forEach(p => { p.x += cx - mean; p.y = cy - p.node.layer * LAYER_GAP; });
  });

  return pos.map(p => ({...p.node, x:Math.round(p.x), y:Math.round(p.y)}));
}

function DependencyGraph({nodes, links, height=460, onSelect, focusId}){
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({x:0,y:0});
  const [drag, setDrag] = useState(null);
  const [override, setOverride] = useState({});
  const [hover, setHover] = useState(null);
  const svgRef = useRef(null);
  const W = 1100;

  const laid = useMemo(()=>layoutGraph(nodes, links, W, height),
    [JSON.stringify(nodes.map(n=>[n.id,n.layer])), JSON.stringify(links.map(l=>l.id)), height]);

  useEffect(()=>{ setOverride({}); setPan({x:0,y:0}); setZoom(1); }, [focusId, nodes.length]);

  const placed = laid.map(n => override[n.id] ? {...n, ...override[n.id]} : n);
  const byId = Object.fromEntries(placed.map(n=>[n.id,n]));

  const bounds = useMemo(()=>{
    if (!placed.length) return {minX:0,minY:0,maxX:W,maxY:height};
    const xs = placed.map(n=>n.x), ys = placed.map(n=>n.y);
    return {
      minX: Math.min(...xs) - NODE_W/2 - 40, maxX: Math.max(...xs) + NODE_W/2 + 40,
      minY: Math.min(...ys) - NODE_H/2 - 40, maxY: Math.max(...ys) + NODE_H/2 + 40
    };
  }, [placed]);
  const vbW = Math.max(320, bounds.maxX - bounds.minX);
  const vbH = Math.max(240, bounds.maxY - bounds.minY);
  const view = {
    w: vbW / zoom, h: vbH / zoom,
    x: bounds.minX + (vbW - vbW/zoom)/2 - pan.x,
    y: bounds.minY + (vbH - vbH/zoom)/2 - pan.y
  };

  const scale = () => {
    const r = svgRef.current ? svgRef.current.getBoundingClientRect() : null;
    return r && r.width ? {sx: view.w / r.width, sy: view.h / r.height, r} : null;
  };
  const toSvg = (evt) => {
    const s = scale(); if (!s) return {x:0,y:0};
    return {x: view.x + (evt.clientX - s.r.left) * s.sx, y: view.y + (evt.clientY - s.r.top) * s.sy};
  };
  const onDown = (e, id) => {
    e.stopPropagation();
    const p = toSvg(e); const n = byId[id];
    setDrag({id, dx: n.x - p.x, dy: n.y - p.y, moved:false});
  };
  const onPanStart = (e) => setDrag({pan:true, cx:e.clientX, cy:e.clientY, from:{...pan}, moved:false});
  const onMove = (e) => {
    if (!drag) return;
    if (drag.pan){
      const s = scale(); if (!s) return;
      setPan({x: drag.from.x + (e.clientX - drag.cx) * s.sx, y: drag.from.y + (e.clientY - drag.cy) * s.sy});
      return;
    }
    const p = toSvg(e);
    setOverride(o => ({...o, [drag.id]: {x: p.x + drag.dx, y: p.y + drag.dy}}));
    setDrag(d => d && {...d, moved:true});
  };
  const onUp = (id) => {
    if (drag && !drag.pan && !drag.moved && onSelect) onSelect(id);
    setDrag(null);
  };

  const toneOf = (n) => {
    if (n.id === focusId) return {fill:'#4f46e5', stroke:'#4338ca', text:'#fff', sub:'#c9c9f7'};
    if (n.dir === 'up')   return {fill:'#eef6ff', stroke:'#a9cbf0', text:'#15406b', sub:'#5c7d9e'};
    if (n.dir === 'down') return {fill:'#f0fbf5', stroke:'#a6dcc2', text:'#0d5c40', sub:'#5c8a76'};
    return {fill:'#ffffff', stroke:'#d3dae4', text:'#1b2230', sub:'#7c8798'};
  };

  if (!nodes.length) {
    return <div className="graph-wrap" style={{height}}>
      <EmptyState icon={<I.graph size={30}/>} title="Nothing to plot" body="No relationships recorded yet."/>
    </div>;
  }

  return (
    <div className="graph-wrap" style={{height}}>
      <div className="graph-toolbar">
        <button className="btn sm" onClick={()=>setZoom(z=>Math.min(2.4, z*1.25))} title="Zoom in">+</button>
        <button className="btn sm" onClick={()=>setZoom(z=>Math.max(0.4, z/1.25))} title="Zoom out">&minus;</button>
        <button className="btn sm" onClick={()=>{setZoom(1);setPan({x:0,y:0});setOverride({});}}>Reset</button>
      </div>
      <div className="graph-legend">
        {focusId && <>
          <div className="lg"><i style={{background:'#cfe4fb'}}/> Depends on this</div>
          <div className="lg"><i style={{background:'#4f46e5'}}/> This component</div>
          <div className="lg"><i style={{background:'#c9eddb'}}/> This depends on</div></>}
        {!focusId && <>
          <div className="lg"><i style={{background:'#fff',border:'1px solid #d3dae4'}}/> Higher layers sit above their dependencies</div>
          <div className="lg" style={{color:'var(--text-3)'}}>Drag to rearrange &middot; click to open</div></>}
      </div>
      <svg ref={svgRef} className={'graph' + (drag?' dragging':'')} width="100%" height={height}
           viewBox={`${view.x} ${view.y} ${view.w} ${view.h}`} preserveAspectRatio="xMidYMid meet"
           onMouseDown={onPanStart} onMouseMove={onMove}
           onMouseUp={()=>setDrag(null)} onMouseLeave={()=>setDrag(null)}>
        <defs>
          <marker id="ah" 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="#9aa5b4"/>
          </marker>
          <marker id="ah-hot" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
            <path d="M0 0 L10 5 L0 10 z" fill="#4f46e5"/>
          </marker>
        </defs>
        <g>
          {links.map((l,i) => {
            const a = byId[l.source], b = byId[l.target];
            if (!a || !b) return null;
            const hot = focusId && (l.source === focusId || l.target === focusId);
            const ax = a.x, ay = a.y + (b.y > a.y ? NODE_H/2 : -NODE_H/2);
            const bx = b.x, by = b.y + (b.y > a.y ? -NODE_H/2 : NODE_H/2);
            const mx = (ax+bx)/2, my = (ay+by)/2;
            const cxp = mx + (ay===by ? 0 : (bx-ax)*0.06);
            return (
              <g key={l.id||i} opacity={hover && !hot && hover!==l.source && hover!==l.target ? .35 : 1}>
                <path d={`M${ax},${ay} Q${cxp},${my} ${bx},${by}`} fill="none"
                      stroke={hot ? '#8b85f0' : '#c4ccd8'} strokeWidth={hot?2:1.4}
                      strokeDasharray={l.criticality==='Soft' ? '5 4' : undefined}
                      markerEnd={hot ? 'url(#ah-hot)' : 'url(#ah)'}>
                  <title>{l.label}</title>
                </path>
              </g>
            );
          })}
          {placed.map(n => {
            const t = toneOf(n);
            return (
              <g key={n.id} className="gnode" transform={`translate(${n.x},${n.y})`}
                 onMouseDown={(e)=>onDown(e,n.id)} onMouseUp={()=>onUp(n.id)}
                 onMouseEnter={()=>setHover(n.id)} onMouseLeave={()=>setHover(null)}>
                <rect className="gnode-shape" x={-NODE_W/2} y={-NODE_H/2} width={NODE_W} height={NODE_H} rx="9"
                      fill={t.fill} stroke={t.stroke} strokeWidth="1.4"
                      style={{filter:'drop-shadow(0 1px 2px rgba(16,24,40,.10))'}}/>
                <rect x={-NODE_W/2} y={-NODE_H/2} width="4" height={NODE_H} rx="2"
                      fill={n.tier===1?'#c0392b':n.tier===2?'#d99117':n.tier===3?'#3b82c4':'#98a2b3'}/>
                <text x={-NODE_W/2+13} y={-4} fontSize="11.5" fontWeight="650" fill={t.text}>
                  {n.name.length > 22 ? n.name.slice(0,21) + '…' : n.name}
                </text>
                <text x={-NODE_W/2+13} y={11} fontSize="9.5" fill={t.sub} letterSpacing=".04em">
                  {n.identifier} · {n.typeName}
                </text>
                <title>{n.name + ' (' + n.identifier + ') — Tier ' + n.tier + ', ' + n.status}</title>
              </g>
            );
          })}
        </g>
      </svg>
    </div>
  );
}

function buildGraphData(state, idx, focusId, depth, filterFn){
  const {out, inc} = depGraph(state);
  let ids, dirs = {}, layers = {};
  if (focusId){
    const down = traverse(focusId, out, 'out', depth);
    const up   = traverse(focusId, inc, 'in',  depth);
    ids = new Set([...down.keys(), ...up.keys()]);
    ids.forEach(id => {
      const dl = down.has(id) ? down.get(id) : null;
      const ul = up.has(id) ? up.get(id) : null;
      if (id === focusId){ dirs[id]='self'; layers[id]=0; }
      else if (dl != null && (ul == null || dl <= ul)){ dirs[id]='down'; layers[id] = -dl; }
      else { dirs[id]='up'; layers[id] = ul; }
    });
  } else {
    const list = state.components.filter(filterFn || (()=>true));
    ids = new Set(list.map(c=>c.id));
    const memo = {};
    const calc = (id, stack) => {
      if (memo[id] != null) return memo[id];
      if (stack.has(id)) return 0;
      stack.add(id);
      let best = 0;
      (out[id]||[]).forEach(d => { if (ids.has(d.toId)) best = Math.max(best, 1 + calc(d.toId, stack)); });
      stack.delete(id);
      return (memo[id] = best);
    };
    ids.forEach(id => { layers[id] = calc(id, new Set()); dirs[id] = 'none'; });
  }
  const nodes = [...ids].filter(id => idx.comp[id]).map(id => {
    const c = idx.comp[id];
    return {
      id, name:c.name, identifier:c.identifier, tier:c.tier, status:c.status,
      typeName: (idx.type[c.typeId] && idx.type[c.typeId].name) || 'Component',
      dir: dirs[id], layer: layers[id] || 0
    };
  });
  const links = state.dependencies
    .filter(d => ids.has(d.fromId) && ids.has(d.toId))
    .map(d => ({id:d.id, source:d.fromId, target:d.toId, criticality:d.criticality,
      label:`${idx.comp[d.fromId] ? idx.comp[d.fromId].name : '?'} — ${d.type} → ${idx.comp[d.toId] ? idx.comp[d.toId].name : '?'}`}));
  return {nodes, links};
}
