/* GraphPage.jsx — Token relationship graph (graph · resolution table · lineage) */
const { useState: useStateG, useMemo: useMemoG, useRef: useRefG, useEffect: useEffectG, useCallback: useCbG } = React;

const LAYER_BAR = { primitive:'var(--text-3)', semantic:'var(--accent)', component:'#7C7CE0', consumer:'var(--success)' };
const LAYER_LABEL = { primitive:'Primitive', semantic:'Semantic', component:'Component', consumer:'Component' };
const LAYER_LABEL_ZH = { primitive:'原始', semantic:'语义', component:'组件', consumer:'组件' };
function layerLabel(l){ return tr(LAYER_LABEL[l]||l, LAYER_LABEL_ZH[l]||l); }
/* family key = id with its LAST dotted segment dropped (parent namespace).
   color.gray.500 → color.gray · space.400 → space · single-segment → itself (won't fold). */
function familyKey(id){ const p = String(id).split('.'); return p.length<=1 ? id : p.slice(0,-1).join('.'); }
const FAM_MIN = 3; // families with ≥ this many members in a column fold into one chip
/* namespaced expand-key for a folded group so primitive families can't collide with consumer categories */
function famExpandKey(col, key){ return 'fam.'+col+'.'+key; }
function catExpandKey(cat){ return 'grp.'+cat; }
const GKINDS = [
  { value:'color', label:'Color', labelZh:'颜色' }, { value:'spacing', label:'Spacing', labelZh:'间距' },
  { value:'radius', label:'Radius', labelZh:'圆角' }, { value:'type', label:'Typography', labelZh:'字体' }, { value:'shadow', label:'Shadow', labelZh:'阴影' },
];
const TIER_DEFS = [
  { key:'primitive', en:'Primitive',   zh:'原始' },
  { key:'semantic',  en:'Semantic',    zh:'语义' },
  { key:'component', en:'Comp. token', zh:'组件 Token' },
  { key:'consumer',  en:'Components',  zh:'组件' },
];

/* node card content — LOD-aware (dot / chip / full) + category meta-node variant */
function NodeCard({ n, ctx, accents, dim, active, onPick, onHover, badge, lod='full', onMeta }) {
  const ref = n.ref || {};
  const isConsumer = n.layer === 'consumer';
  const isMeta = !!n.meta;
  const val = (isConsumer || isMeta) ? null : gValue(n.id, ctx, accents);
  const label = ref.name || (n.id.split('.').slice(-2).join('.'));
  const click = () => { if (isMeta) onMeta && onMeta(n); else onPick(n.id); };
  return (
    <div onMouseEnter={()=>!isMeta&&onHover(n.id)} onMouseLeave={()=>!isMeta&&onHover(null)} onClick={click}
      className="absolute cursor-pointer select-none"
      style={{ left:n.x, top:n.y, width:NODE_W, height:NODE_H,
        opacity: dim?0.22:1, transition:'opacity 160ms ease, box-shadow 160ms ease, transform 160ms ease',
        transform: active?'translateZ(0) scale(1.015)':'none' }}>
      <div className={cx('w-full h-full flex items-center gap-2.5 pl-0 pr-2.5 rounded-[8px] bg-panel border overflow-hidden', isMeta&&'border-dashed', lod==='dot'&&'justify-center pr-0')}
        style={{ borderColor: active?'var(--accent)':'var(--border)',
          boxShadow: active?'0 0 0 1px var(--accent), 0 6px 18px rgba(0,0,0,0.10)':'0 1px 2px rgba(0,0,0,0.05)' }}>
        {lod!=='dot' && <span className="h-full w-[3px] shrink-0" style={{ background:LAYER_BAR[n.layer] }} />}
        {isMeta ? (
          <span className="w-7 h-7 rounded-[7px] bg-accentWeak text-accent flex items-center justify-center shrink-0 ml-1"><Icon name="grid" size={15}/></span>
        ) : isConsumer ? (
          <span className="w-7 h-7 rounded-[7px] bg-sunken border border-line flex items-center justify-center text-ink shrink-0 ml-1"><Icon name={ref.icon||'component'} size={15}/></span>
        ) : val && val.kind==='color' ? (
          <Swatch color={val.value} size={26} radius={6} />
        ) : (
          <span className="w-7 h-7 rounded-[4px] bg-sunken border border-line flex items-center justify-center shrink-0 ml-1">
            <KindPreview token={{ id:n.id, kind:val?val.kind:'color', source:ref.source||{type:'value',value:0} }} ctx={ctx} size={18} />
          </span>
        )}
        {lod!=='dot' && (
          <span className="min-w-0 flex-1 leading-tight">
            <span className="block font-mono text-[11.5px] text-ink truncate">{isMeta ? ref.name : label}</span>
            {lod==='full' && (
              <span className="block text-[10px] text-ink3 truncate">
                {isMeta ? (ref.subtitle || (ref.count + ' ' + tr('components','组件'))) : isConsumer ? tr('consumes','消费') : val && val.kind==='color' ? <span className="font-mono">{val.value}</span> : val ? <span className="font-mono">{fmtVal(val)}</span> : layerLabel(n.layer)}
              </span>
            )}
          </span>
        )}
        {isMeta ? (
          lod!=='dot' && <span title={tr('Drill into this group','钻取该分组')} className="shrink-0 inline-flex items-center gap-0.5 h-[18px] pl-1.5 pr-1 rounded-full text-[10px] font-medium bg-accentWeak text-accent">{ref.count}<Icon name="chevronR" size={11}/></span>
        ) : lod==='full' && badge && (
          <span title={badge.tip}
            className={cx('shrink-0 inline-flex items-center gap-1 h-[18px] px-1.5 rounded-full text-[10px] font-medium border',
              badge.accent ? 'bg-accentWeak text-accent border-transparent' : 'bg-sunken text-ink3 border-line')}>
            <Icon name={badge.icon} size={10} />{badge.n}
          </span>
        )}
        {!isMeta && lod==='full' && n.ref && n.ref.virtual && <span className="text-[9px] uppercase tracking-wide text-ink3 shrink-0">{n.ref.brand?.slice(0,3)}</span>}
      </div>
    </div>
  );
}
function fmtVal(v){ if(v.kind==='spacing'||v.kind==='radius') return v.value+'px'; if(v.kind==='type') return v.value+'/'+(v.lh||'—'); return String(v.value); }

/* curved connector — bundled edges share a short trunk leaving the source hub */
function edgePath(a, b, bundle) {
  const x1=a.x+NODE_W, y1=a.y+NODE_H/2, x2=b.x, y2=b.y+NODE_H/2;
  if (bundle) {
    const cx = x1 + 26;            // overlapping stubs from one hub read as a single trunk that splays
    const mx = (cx + x2)/2;
    return `M${x1},${y1} L${cx},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`;
  }
  const mx=(x1+x2)/2;
  return `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`;
}

/* ============ GRAPH CANVAS ============ */
function GraphCanvas({ g, kind, ctx, focusId, onClearFocus, selected, setSelected, tiers, setTiers }) {
  const [hover, setHover] = useStateG(null);
  const [view, setView] = useStateG({ x:40, y:30, k:1 });
  const wrapRef = useRefG(null);
  const pan = useRefG(null);
  const movedRef = useRefG(false);

  // GROUP-FOCUS (drill-in): clicking a collapsed family/category meta chip in overview enters
  // a focused view of that ONE group — its members ∪ everything their edges touch — with a
  // breadcrumb header. Only one group is ever open at a time; "collapse" = go back to overview.
  // Shape: { key, label, count, members:[ids] } | null.  Reuses the node-focus visible plumbing.
  const [focusGroup, setFocusGroup] = useStateG(null);
  const enterGroup = useCbG((metaNode) => {
    const mem = (metaNode.ref && metaNode.ref.members) || [];
    setFocusGroup({ key: metaNode.expandKey, label: (metaNode.ref && metaNode.ref.name) || metaNode.id, count: mem.length, members: mem.map(m=>m.id) });
    setHover(null);
  }, []);
  const clearGroup = useCbG(() => setFocusGroup(null), []);
  // a fresh category resets every focus/expand state
  useEffectG(() => { setFocusGroup(null); }, [kind]);
  // group-focus is invalid while a single node is focused (chips only exist in overview)
  useEffectG(() => { if (focusId) setFocusGroup(null); }, [focusId]);

  // visible node set: subgraph when a node is focused, OR members∪neighbors when a group is
  // focused (union of each member's full ancestor+descendant subgraph — reuses subgraphIds).
  const visible = useMemoG(() => {
    if (focusId && g.nodesMap[focusId]) return subgraphIds(focusId, g);
    if (focusGroup) {
      const set = new Set();
      focusGroup.members.forEach(id => { if (g.nodesMap[id]) subgraphIds(id, g).forEach(x=>set.add(x)); });
      return set.size ? set : null;
    }
    return null;
  }, [focusId, focusGroup, g]);

  // group aggregation — in overview, consumers collapse into per-category meta-nodes AND
  // tall primitive/semantic columns collapse into per-family chips. One Set tracks every
  // expanded group; keys are namespaced (catExpandKey vs famExpandKey) so they can't collide.
  const [expandedGroups, setExpandedGroups] = useStateG(() => new Set());
  useEffectG(() => { setExpandedGroups(new Set()); }, [kind]);
  // tall non-consumer tiers we fold by id-family (primitive col 0, semantic col 1)
  const FAMILY_COLS = [0, 1];

  // display node + edge set: tier/focus filter; then (overview) fold consumers into category
  // meta-nodes AND fold tall primitive/semantic columns into id-family chips, deduping their
  // edges into one weighted edge per (source → group) pair. memberToGroup maps ANY folded
  // member (primitive/semantic/consumer) → its visible representative (family chip / category).
  const display = useMemoG(() => {
    const inScope = (n) => visible ? visible.has(n.id)
      : (n.layer === 'consumer' ? !!tiers.consumer : tiers[n.layer] !== false);
    const inScopeBase = g.nodes.filter(n => n.layer !== 'consumer' && inScope(n));
    const consumers = g.nodes.filter(n => n.layer === 'consumer');
    let baseNodes = [], consumerNodes = [], metaNodes = [];
    const memberToGroup = {};
    let famGroupCount = 0;

    if (visible) {
      baseNodes = inScopeBase;                                    // focus: individuals, no aggregation
      consumerNodes = consumers.filter(n => visible.has(n.id));
    } else {
      // --- fold tall non-consumer tiers by id-family (generalizes the consumer fold) ---
      const byColFam = {};   // col → familyKey → members
      inScopeBase.forEach(n => {
        if (!FAMILY_COLS.includes(n.col)) { baseNodes.push(n); return; }
        const fk = familyKey(n.id);
        const fam = (byColFam[n.col] = byColFam[n.col] || {});
        (fam[fk] = fam[fk] || []).push(n);
      });
      Object.keys(byColFam).forEach(col => {
        const fams = byColFam[col];
        Object.keys(fams).forEach(fk => {
          const members = fams[fk];
          const ekey = famExpandKey(col, fk);
          if (members.length < FAM_MIN || expandedGroups.has(ekey)) { baseNodes.push(...members); return; }
          const avgY = members.reduce((s,m)=>s+(m.y||0),0)/members.length;
          const gid = ekey;   // chip id doubles as its expand key
          metaNodes.push({ id:gid, expandKey:ekey, col:+col, layer:members[0].layer, meta:true, y:avgY,
            ref:{ name:fk, count:members.length, members, subtitle: members.length + ' ' + tr('tokens','token') } });
          members.forEach(m => { memberToGroup[m.id] = gid; });
          famGroupCount++;
        });
      });
      // --- fold consumers into per-category meta-nodes (unchanged behavior) ---
      if (tiers.consumer) {
        const byCat = {};
        consumers.forEach(n => { const c = (n.ref && n.ref.category) || tr('Other','其他'); (byCat[c] = byCat[c] || []).push(n); });
        Object.keys(byCat).forEach(cat => {
          const members = byCat[cat];
          const ekey = catExpandKey(cat);
          if (expandedGroups.has(ekey)) { consumerNodes.push(...members); return; }
          const avgY = members.reduce((s,m)=>s+(m.y||0),0)/members.length;
          metaNodes.push({ id:ekey, expandKey:ekey, col:3, layer:'consumer', meta:true, y:avgY, ref:{ name:cat, count:members.length, members } });
          members.forEach(m => { memberToGroup[m.id] = ekey; });
        });
      }
    }
    const allNodes = baseNodes.concat(consumerNodes).concat(metaNodes);
    const ids = new Set(allNodes.map(n=>n.id));
    const eMap = {};
    g.edges.forEach(e => {
      const from = memberToGroup[e.from] || e.from, to = memberToGroup[e.to] || e.to;
      if (from === to) return;                 // both endpoints folded into the same chip
      if (!ids.has(from) || !ids.has(to)) return;
      (eMap[from+'>'+to] = eMap[from+'>'+to] || { from, to, weight:0 }).weight++;
    });
    const consumerGroupCount = metaNodes.filter(m=>m.layer==='consumer').length;
    return { allNodes, edges: Object.values(eMap), groupCount: consumerGroupCount, famGroupCount, totalConsumers: consumers.length };
  }, [g, visible, tiers, expandedGroups]);

  // re-pack display nodes into compact, centered columns (restores a tight lattice)
  const { laidNodes, vpos, shownCols } = useMemoG(() => {
    const STEP = NODE_H + ROW_GAP, byCol = {};
    display.allNodes.forEach(n => { (byCol[n.col] = byCol[n.col] || []).push(n); });
    let maxH = NODE_H;
    Object.values(byCol).forEach(arr => { arr.sort((a,b)=>(a.y||0)-(b.y||0)); maxH = Math.max(maxH, arr.length*STEP - ROW_GAP); });
    const vpos = {};
    Object.keys(byCol).forEach(col => {
      const arr = byCol[col], off = (maxH - (arr.length*STEP - ROW_GAP))/2;
      arr.forEach((n,i) => { vpos[n.id] = { x: COL_X[+col], y: off + i*STEP }; });
    });
    return { laidNodes: display.allNodes.map(n => ({ ...n, x: vpos[n.id].x, y: vpos[n.id].y })), vpos, shownCols: new Set(display.allNodes.map(n=>n.col)) };
  }, [display]);
  const vedges = useMemoG(() => { const s = new Set(laidNodes.map(n=>n.id)); return display.edges.filter(e => s.has(e.from) && s.has(e.to)); }, [laidNodes, display]);

  // highlight path from hover or selection
  const activeId = hover || selected;
  const highlight = useMemoG(() => {
    if (!activeId || !g.nodesMap[activeId]) return null;
    const s = new Set([activeId]);
    ancestorsOf(activeId, g.parents).forEach(x=>s.add(x));
    descendantsOf(activeId, g.children).forEach(x=>s.add(x));
    return s;
  }, [activeId, g]);

  // fit the compact visible bbox — hiding consumers now actually zooms the lattice in
  const fit = useCbG(() => {
    const el = wrapRef.current; if (!el || !laidNodes.length) return;
    const minX = Math.min(...laidNodes.map(n=>n.x)), maxX = Math.max(...laidNodes.map(n=>n.x+NODE_W));
    const minY = Math.min(...laidNodes.map(n=>n.y)), maxY = Math.max(...laidNodes.map(n=>n.y+NODE_H));
    const w = Math.max(maxX-minX, 1), h = Math.max(maxY-minY, 1);
    const padX = 56, padTop = 48, padBot = 28;
    const kx = (el.clientWidth - padX*2) / w;
    const ky = (el.clientHeight - padTop - padBot) / h;
    const k = Math.max(0.08, Math.min(1.2, Math.min(kx, ky))); // tiny floor so fit-to-view can frame tall/large graphs (LOD renders dots at low zoom)
    const x = (el.clientWidth - w*k)/2 - minX*k;
    const y = padTop + ((el.clientHeight - padTop - padBot) - h*k)/2 - minY*k;
    setView({ k, x, y });
  }, [laidNodes]);
  useEffectG(() => { const r = requestAnimationFrame(()=>fit()); return ()=>cancelAnimationFrame(r); }, [focusId, focusGroup, tiers, g, expandedGroups]);

  function onWheel(e){ e.preventDefault(); const d=-e.deltaY*0.0016; setView(v=>{ const k=Math.max(0.35,Math.min(2,v.k*(1+d))); const r=wrapRef.current.getBoundingClientRect(); const cx=e.clientX-r.left,cy=e.clientY-r.top; return { k, x:cx-(cx-v.x)*(k/v.k), y:cy-(cy-v.y)*(k/v.k) }; }); }
  function onDown(e){ movedRef.current=false; if(e.target.closest('[data-node]')) return; pan.current={ sx:e.clientX, sy:e.clientY, ox:view.x, oy:view.y }; }
  function onMove(e){ if(!pan.current) return; movedRef.current=true; setView(v=>({ ...v, x:pan.current.ox+(e.clientX-pan.current.sx), y:pan.current.oy+(e.clientY-pan.current.sy) })); }
  function onUp(){ pan.current=null; }
  function onBgClick(e){ if(!movedRef.current && !e.target.closest('[data-node]')){ setSelected(null); clearGroup(); onClearFocus(); } }
  const zoom = f => setView(v=>{ const k=Math.max(0.35,Math.min(2,v.k*f)); const el=wrapRef.current; const cx=el.clientWidth/2,cy=el.clientHeight/2; return { k, x:cx-(cx-v.x)*(k/v.k), y:cy-(cy-v.y)*(k/v.k) }; });

  const nodes = laidNodes;
  const posOf = vpos;
  const edges = vedges;
  // out-degree in the current view → drives edge bundling + fan-out badges
  const outDeg = useMemoG(() => { const d = {}; vedges.forEach(e => { d[e.from] = (d[e.from] || 0) + 1; }); return d; }, [vedges]);
  const HUB = 4;
  const lod = view.k < 0.6 ? 'dot' : view.k < 0.9 ? 'chip' : 'full';   // zoom-driven level-of-detail
  const nIsDim = id => (highlight && !highlight.has(id));
  const eState = e => {
    if (highlight && highlight.has(e.from) && highlight.has(e.to)) return 'hot';
    if (highlight) return 'dim';
    return 'base';
  };

  return (
    <div className="relative h-full">
      {/* column headers — opaque clipped band so nodes never overlap labels */}
      <div className="absolute top-0 left-0 right-0 h-8 z-[4] bg-panel/92 backdrop-blur-sm border-b border-line pointer-events-none overflow-hidden">
        {[tr('Primitive · value','原始 · 值'),tr('Semantic','语义'),tr('Component token','组件 token'),tr('Component','组件')].map((t,i)=>(
          shownCols.has(i) ? (
          <div key={t} className="absolute top-1/2 -translate-y-1/2 text-[10.5px] uppercase tracking-wide text-ink3 font-medium whitespace-nowrap"
            style={{ left: view.x + COL_X[i]*view.k + 2 }}>{t}</div>
          ) : null
        ))}
      </div>

      <div ref={wrapRef} onWheel={onWheel} onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp} onClick={onBgClick}
        className="absolute inset-0 overflow-hidden cursor-grab active:cursor-grabbing"
        style={{ background:'var(--bg-sunken)', backgroundImage:'radial-gradient(var(--border) 0.7px, transparent 0.7px)', backgroundSize:`${22*view.k}px ${22*view.k}px`, backgroundPosition:`${view.x}px ${view.y}px` }}>
        <div className="absolute origin-top-left" style={{ transform:`translate(${view.x}px,${view.y}px) scale(${view.k})`, width:g.width, height:g.height }}>
          <svg className="absolute overflow-visible pointer-events-none" style={{ left:0, top:0, width:g.width, height:g.height }}>
            {edges.map((e,i) => {
              const a=posOf[e.from], b=posOf[e.to]; if(!a||!b) return null;
              const st=eState(e);
              return <path key={i} d={edgePath(a,b,(outDeg[e.from]||0)>=HUB)} fill="none"
                stroke={st==='hot'?'var(--accent)':'var(--text-3)'}
                strokeWidth={st==='hot'?2.4:Math.min(4, 1.25 + Math.log2((e.weight||1))*0.85)} strokeOpacity={st==='dim'?0.18:st==='hot'?1:0.7}
                style={{ transition:'stroke 160ms, stroke-opacity 160ms, stroke-width 160ms' }} />;
            })}
          </svg>
          {nodes.map(n => {
            let badge = null;
            if (n.layer === 'component') {
              const cc = (g.children[n.id]||[]).filter(c=>g.nodesMap[c] && g.nodesMap[c].layer==='consumer').length;
              if (cc>0) badge = { n: cc, icon:'component', accent: !!tiers.consumer, tip: tr('Components referencing this token','引用此 token 的组件数') };
            } else if ((outDeg[n.id]||0) >= HUB) {
              badge = { n: outDeg[n.id], icon:'network', accent:false, tip: tr('Tokens this drives downstream','它向下游驱动的数量') };
            }
            return <div key={n.id} data-node><NodeCard n={n} ctx={ctx} accents={g.accents}
              dim={nIsDim(n.id)} active={activeId===n.id} onPick={setSelected} onHover={setHover} badge={badge}
              lod={lod} onMeta={enterGroup} /></div>;
          })}
        </div>
      </div>

      {/* zoom + scale — horizontal cluster, bottom-right; the % reading is the last item */}
      <div className="absolute bottom-4 right-4 z-[3] flex flex-row items-center gap-0.5 p-1 rounded-[12px] bg-panel border border-line shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
        <IconButton name="zoomOut" title="Zoom out" onClick={()=>zoom(0.83)} />
        <IconButton name="zoomIn" title="Zoom in" onClick={()=>zoom(1.2)} />
        <IconButton name="fit" title="Fit to view" onClick={fit} />
        <span className="px-2 font-mono text-[11px] text-ink3 tabular-nums">{Math.round(view.k*100)}%</span>
      </div>

      {/* tier toggles + grouping — bottom-left, single row, left-aligned (overview only) */}
      {!focusId && !focusGroup && (
        <div className="absolute bottom-4 left-4 z-[5] inline-flex items-center gap-1 p-1 rounded-[12px] bg-panel/90 backdrop-blur border border-line shadow-[0_2px_8px_rgba(0,0,0,0.06)] whitespace-nowrap">
          {TIER_DEFS.map(t=>(
            <button key={t.key} onClick={()=>setTiers(s=>({ ...s, [t.key]: !s[t.key] }))} title={tr(t.en,t.zh)}
              className={cx('inline-flex items-center gap-1.5 h-7 px-2 rounded-[7px] text-[12px] font-medium transition', tiers[t.key]?'text-ink hover:bg-hover':'text-ink3 hover:bg-hover')}>
              <span className="w-2 h-2 rounded-full" style={{ background:LAYER_BAR[t.key], opacity: tiers[t.key]?1:0.25 }} />
              {tr(t.en, t.zh)}
            </button>
          ))}
          {(() => {
            const folded = display.groupCount + display.famGroupCount; // family chips + consumer categories
            if (folded === 0) return null;
            return (
              <>
                <div className="w-px h-5 bg-line mx-0.5" />
                <span title={tr('Click a group chip to drill in','点击分组卡片即可钻取')} className="inline-flex items-center gap-1.5 h-7 px-2 text-[12px] text-ink3"><Icon name="grid" size={13}/>{tr(`${folded} groups · click to open`, `${folded} 个分组 · 点击展开`)}</span>
              </>
            );
          })()}
        </div>
      )}

      {/* node-focus breadcrumb */}
      {focusId && (
        <div className="absolute top-11 left-1/2 -translate-x-1/2 z-[5] inline-flex items-center gap-2 h-8 px-3 rounded-full bg-panel border border-accent text-[12px] text-ink shadow-[0_2px_10px_rgba(0,0,0,0.08)]">
          <Icon name="crosshair" size={13} className="text-accent" />{tr('Focused on','已聚焦')} <span className="font-mono text-accent">{focusId}</span>
          <button onClick={onClearFocus} className="text-ink3 hover:text-ink"><Icon name="close" size={13}/></button>
        </div>
      )}

      {/* group-focus breadcrumb — top-left pill: ← back · group name · member count */}
      {!focusId && focusGroup && (
        <div className="absolute top-11 left-4 z-[5] inline-flex items-center gap-1.5 h-8 pl-1.5 pr-3 rounded-full bg-panel border border-accent text-[12px] text-ink shadow-[0_2px_10px_rgba(0,0,0,0.08)]">
          <button onClick={clearGroup} title={tr('Back to overview','返回总览')} className="inline-flex items-center gap-1 h-6 pl-1 pr-2 rounded-full text-ink2 hover:text-ink hover:bg-hover transition">
            <Icon name="arrowR" size={13} className="rotate-180" />{tr('Overview','总览')}
          </button>
          <span className="w-px h-4 bg-line" />
          <Icon name="grid" size={13} className="text-accent" />
          <span className="font-mono text-accent truncate max-w-[200px]">{focusGroup.label}</span>
          <span className="text-ink3">· {focusGroup.count} {tr('tokens','token')}</span>
          <button onClick={clearGroup} title={tr('Exit group','退出分组')} className="text-ink3 hover:text-ink ml-0.5"><Icon name="close" size={13}/></button>
        </div>
      )}
    </div>
  );
}

/* ============ DETAIL PANEL ============ */
function DetailPanel({ g, id, kind, ctx, onClose, onFocus, onOpenComponent }) {
  if (!id || !g.nodesMap[id]) return (
    <div className="h-full flex flex-col items-center justify-center text-center px-6 text-ink3">
      <div className="w-10 h-10 rounded-full bg-sunken border border-line flex items-center justify-center mb-3"><Icon name="crosshair" size={18}/></div>
      <p className="text-[13px] text-ink2 font-medium">{tr('Select a node','选择一个节点')}</p>
      <p className="text-[12px] mt-1 max-w-[200px]">{tr('Hover to preview a path; click any node to inspect its value, chain, and impact.','悬停预览路径;点击任意节点查看其值、引用链与影响。')}</p>
    </div>
  );
  const node = g.nodesMap[id];
  const ref = node.ref || {};
  const isConsumer = node.layer==='consumer';
  const val = isConsumer ? null : gValue(id, ctx, g.accents);
  const chain = isConsumer ? [] : upstreamChain(id, ctx, g.accents);
  const down = descendantsOf(id, g.children);
  const downTokens = [...down].filter(x=>g.nodesMap[x] && g.nodesMap[x].layer!=='consumer');
  const downComp = [...down].filter(x=>g.nodesMap[x] && g.nodesMap[x].layer==='consumer');

  return (
    <div className="h-full flex flex-col">
      <div className="px-4 pt-3.5 pb-3 border-b border-line flex items-start justify-between gap-2">
        <div className="min-w-0">
          <div className="flex items-center gap-1.5 mb-1">
            <span className="w-2 h-2 rounded-full" style={{ background:LAYER_BAR[node.layer] }} />
            <span className="text-[10.5px] uppercase tracking-wide text-ink3">{isConsumer?tr('Component','组件'):layerLabel(node.layer)}</span>
          </div>
          <h3 className="font-mono text-[13.5px] text-ink truncate">{id}</h3>
        </div>
        <IconButton name="close" title="Close" onClick={onClose} />
      </div>
      <div className="flex-1 overflow-y-auto ds-scroll p-4 flex flex-col gap-4">
        {!isConsumer && (
          <div className="flex items-center gap-3 rounded-[12px] border border-line bg-sunken p-3">
            {val.kind==='color' ? <Swatch color={val.value} size={40} radius={8}/> :
              <span className="w-10 h-10 rounded-[8px] bg-panel border border-line flex items-center justify-center"><KindPreview token={{id, kind:val.kind, source:ref.source||{type:'value',value:0}}} ctx={ctx} size={26}/></span>}
            <div className="min-w-0">
              <div className="text-[10.5px] uppercase tracking-wide text-ink3">{tr('Resolved','解析值')} · {ctx.theme.name} · {ctx.mode}</div>
              <div className="font-mono text-[14px] text-ink">{val.kind==='color'?val.value:fmtVal(val)}</div>
            </div>
          </div>
        )}
        {ref.description && <p className="text-[12.5px] text-ink2 leading-relaxed">{ref.description}</p>}

        {!isConsumer && chain.length>1 && (
          <div>
            <div className="text-[10.5px] uppercase tracking-wide text-ink3 mb-1.5">{tr('Resolution chain','引用链')}</div>
            <div className="rounded-[12px] border border-line bg-panel p-2.5 flex flex-col gap-1">
              {chain.map((cid,i)=>{
                const cn=g.nodesMap[cid]; const cv=gValue(cid,ctx,g.accents);
                return (
                  <div key={cid} className="flex items-center gap-2" style={{ paddingLeft:i*10 }}>
                    {i>0 && <Icon name="arrowR" size={11} className="text-ink3 -ml-3 rotate-90" style={{ marginLeft:i*10-14 }}/>}
                    {cv && cv.kind==='color' && <Swatch color={cv.value} size={14}/>}
                    <span className="font-mono text-[11.5px] text-ink truncate">{cid}</span>
                    {cn && <span className="text-[9.5px] uppercase tracking-wide text-ink3 ml-auto">{cn.layer}</span>}
                  </div>
                );
              })}
              <div className="flex items-center gap-2 mt-0.5 pt-1.5 border-t border-line" style={{ paddingLeft:chain.length*10 }}>
                <span className="font-mono text-[11.5px] text-ink2">= {val.kind==='color'?val.value:fmtVal(val)}</span>
              </div>
            </div>
          </div>
        )}

        {/* impact */}
        <div>
          <div className="text-[10.5px] uppercase tracking-wide text-ink3 mb-1.5">{tr('Impact if changed','改动影响')}</div>
          <div className="grid grid-cols-2 gap-2">
            <div className="rounded-[12px] border border-line bg-sunken p-2.5">
              <div className="text-[20px] font-semibold text-ink tabular-nums leading-none">{downTokens.length}</div>
              <div className="text-[11px] text-ink3 mt-1">{tr('downstream tokens','下游 token')}</div>
            </div>
            <div className="rounded-[12px] border border-line bg-sunken p-2.5">
              <div className="text-[20px] font-semibold text-ink tabular-nums leading-none">{downComp.length}</div>
              <div className="text-[11px] text-ink3 mt-1">{tr('components affected','受影响组件')}</div>
            </div>
          </div>
          {downComp.length>0 && (
            <div className="flex flex-wrap gap-1.5 mt-2">
              {downComp.map(c=> <span key={c} className="inline-flex items-center gap-1 h-[22px] px-2 rounded-[4px] bg-panel border border-line text-[11.5px] text-ink2"><Icon name={g.nodesMap[c].ref.icon||'component'} size={12}/>{g.nodesMap[c].ref.name}</span>)}
            </div>
          )}
        </div>

        {isConsumer && onOpenComponent && (
          <Button variant="primary" icon="component" onClick={()=>onOpenComponent(id.replace(/^cmp\./,''))} className="w-full">{tr('Open component','打开组件')}</Button>
        )}
        <Button variant="secondary" icon="crosshair" onClick={()=>onFocus(id)} className="w-full">{tr('Focus subgraph','聚焦子图')}</Button>
      </div>
    </div>
  );
}

/* ============ RESOLUTION TABLE ============ */
function ResolutionTable({ g, kind, ctx, onJump }) {
  const Lk = ({ id, cls }) => <button onClick={()=>onJump&&onJump(id)} title={tr('Open in graph','在图谱中打开')}
    className={cx('font-mono text-left hover:text-accent hover:underline transition', cls)}>{id}</button>;
  const rows = useMemoG(() => {
    return TOKENS.filter(t=>t.layer==='component' && t.kind===kind && t.status!=='deprecated').map(c => {
      const chain = upstreamChain(c.id, ctx, g.accents);
      const prim = chain[chain.length-1];
      const sem = chain.find(x=>g.nodesMap[x] && g.nodesMap[x].layer==='semantic');
      const val = gValue(c.id, ctx, g.accents);
      const cons = consumerFor(c.id);
      return { comp:c.id, sem, prim, val, cons };
    });
  }, [kind, ctx.theme.id, ctx.mode]);

  return (
    <div className="h-full overflow-auto ds-scroll">
      <table className="w-full border-collapse text-[12.5px]">
        <thead className="sticky top-0 z-[1]">
          <tr className="bg-sunken border-b border-line text-left">
            {[tr('Resolved value','解析值'),tr('Primitive token','原始 token'),tr('Semantic token','语义 token'),tr('Component token','组件 token'),tr('Used in','用于')].map(h=>(
              <th key={h} className="px-4 h-9 font-medium text-[11px] uppercase tracking-wide text-ink3 whitespace-nowrap">{h}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map(r=>(
            <tr key={r.comp} className="border-b border-[color:var(--border)] hover:bg-hover transition-colors">
              <td className="px-4 py-2.5">
                <span className="inline-flex items-center gap-2">
                  {r.val.kind==='color' ? <Swatch color={r.val.value} size={16}/> : null}
                  <span className="font-mono text-ink">{r.val.kind==='color'?r.val.value:fmtVal(r.val)}</span>
                </span>
              </td>
              <td className="px-4 py-2.5">{r.prim ? <Lk id={r.prim} cls="text-ink2" /> : <span className="text-ink3">—</span>}</td>
              <td className="px-4 py-2.5">{r.sem ? <Lk id={r.sem} cls="text-ink2" /> : <span className="text-ink3">{tr('direct','直接')}</span>}</td>
              <td className="px-4 py-2.5"><Lk id={r.comp} cls="text-ink" /></td>
              <td className="px-4 py-2.5">{r.cons ? <span className="inline-flex items-center gap-1.5 text-ink2"><Icon name={r.cons.icon} size={13}/>{r.cons.name}</span> : <span className="text-ink3">—</span>}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

/* ============ SINGLE-TOKEN LINEAGE ============ */
function LineagePane({ g, kind, ctx, focusId, setFocusId, onJump }) {
  const pickables = g.nodes.filter(n=>n.layer!=='consumer');
  const id = focusId && g.nodesMap[focusId] ? focusId : (pickables[0] && pickables[0].id);
  const [q, setQ] = useStateG('');
  if (!id) return <EmptyState icon="route" title={tr('No tokens','暂无 token')} body={tr('Pick another category.','换一个分类试试。')} />;

  const up = upstreamChain(id, ctx, g.accents).slice().reverse(); // primitive → … → self
  const downDirect = (g.children[id]||[]);
  const val = gValue(id, ctx, g.accents);

  const filtered = pickables.filter(n=>!q || n.id.toLowerCase().includes(q.toLowerCase()));

  return (
    <div className="h-full flex">
      <div className="w-[230px] shrink-0 border-r border-line flex flex-col">
        <div className="p-2.5 border-b border-line"><SearchInput value={q} onChange={setQ} placeholder={tr('Find a token…','查找 token…')} size="sm"/></div>
        <div className="flex-1 overflow-y-auto ds-scroll py-1">
          {filtered.map(n=>{ const v=gValue(n.id,ctx,g.accents); return (
            <button key={n.id} onClick={()=>setFocusId(n.id)} className={cx('w-full flex items-center gap-2 px-3 h-9 text-left transition', id===n.id?'bg-accentWeak':'hover:bg-hover')}>
              {v.kind==='color'?<Swatch color={v.value} size={14}/>:<span className="w-3.5 h-3.5 rounded-[3px]" style={{background:LAYER_BAR[n.layer]}}/>}
              <span className="font-mono text-[11.5px] text-ink truncate flex-1">{n.id}</span>
            </button>
          );})}
        </div>
      </div>
      <div className="flex-1 overflow-auto ds-scroll p-8">
        <div className="max-w-[560px] mx-auto">
          <div className="text-[10.5px] uppercase tracking-wide text-ink3 mb-1">{tr('Lineage','血缘')} · {ctx.theme.name} · {ctx.mode}</div>
          <div className="flex items-center justify-between gap-3 mb-6">
            <h2 className="font-mono text-[16px] text-ink truncate">{id}</h2>
            <Button variant="secondary" size="sm" icon="network" onClick={()=>onJump&&onJump(id)} className="shrink-0">{tr('View in graph','在图谱中查看')}</Button>
          </div>

          <div className="text-[11px] uppercase tracking-wide text-ink3 mb-2">{tr('Upstream — resolves from','上游 — 解析来源')}</div>
          <div className="flex flex-col">
            {up.map((cid,i)=>{ const cn=g.nodesMap[cid]; const cv=gValue(cid,ctx,g.accents); const isSelf=cid===id; return (
              <div key={cid} className="flex items-stretch gap-3">
                <div className="flex flex-col items-center">
                  <div className={cx('w-9 h-9 rounded-[8px] border flex items-center justify-center shrink-0', isSelf?'border-accent bg-accentWeak':'border-line bg-panel')}>
                    {cv.kind==='color'?<Swatch color={cv.value} size={18}/>:<span className="w-4 h-4 rounded" style={{background:LAYER_BAR[cn.layer]}}/>}
                  </div>
                  {i<up.length-1 && <span className="w-px flex-1 my-1" style={{ background:'var(--border-strong)' }}/>}
                </div>
                <div className={cx('flex-1 mb-2 rounded-[8px] border px-3 py-2', isSelf?'border-accent':'border-line bg-panel')}>
                  <div className="flex items-center gap-2">
                    <span className="font-mono text-[12.5px] text-ink truncate">{cid}</span>
                    <span className="text-[9.5px] uppercase tracking-wide text-ink3 ml-auto">{cn.layer}</span>
                  </div>
                  <div className="font-mono text-[11px] text-ink3 mt-0.5">{cv.kind==='color'?cv.value:fmtVal(cv)}</div>
                </div>
              </div>
            );})}
          </div>

          <div className="text-[11px] uppercase tracking-wide text-ink3 mt-6 mb-2">{tr('Downstream — consumed by','下游 — 被谁引用')}</div>
          {downDirect.length===0 ? <p className="text-[12.5px] text-ink3">{tr('Nothing references this token directly.','没有 token 直接引用它。')}</p> : (
            <div className="flex flex-col gap-1.5">
              {downDirect.map(cid=>{ const cn=g.nodesMap[cid]; const isC=cn.layer==='consumer'; const cv=isC?null:gValue(cid,ctx,g.accents); return (
                <button key={cid} onClick={()=>!isC&&setFocusId(cid)} className={cx('flex items-center gap-2.5 rounded-[8px] border border-line bg-panel px-3 py-2 text-left', !isC&&'hover:border-line2 hover:bg-hover transition')}>
                  {isC ? <span className="w-7 h-7 rounded-[7px] bg-sunken border border-line flex items-center justify-center"><Icon name={cn.ref.icon||'component'} size={14}/></span> : cv.kind==='color'?<Swatch color={cv.value} size={16}/>:<span className="w-4 h-4 rounded" style={{background:LAYER_BAR[cn.layer]}}/>}
                  <span className="font-mono text-[12px] text-ink truncate flex-1">{isC?cn.ref.name:cid}</span>
                  <span className="text-[9.5px] uppercase tracking-wide text-ink3">{cn.layer}</span>
                </button>
              );})}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ============ PAGE SHELL ============ */
function GraphPage({ ctx, theme, setTheme, mode, onOpenComponent, initialFocus, clearInitialFocus }) {
  const [tab, setTab] = useStateG('graph');
  const [kind, setKind] = useStateG('color');
  const [q, setQ] = useStateG('');
  const [focusId, setFocusId] = useStateG(initialFocus || null);
  const [selected, setSelected] = useStateG(initialFocus || null);
  const [tiers, setTiers] = useStateG({ primitive:true, semantic:true, component:true, consumer: initialFocus ? String(initialFocus).startsWith('cmp.') : false });

  useEffectG(() => {
    if (initialFocus) {
      setKind('color'); setTab('graph'); setFocusId(initialFocus); setSelected(initialFocus);
      if (String(initialFocus).startsWith('cmp.')) setTiers(t => ({ ...t, consumer:true })); // deep-link guard
    }
  }, [initialFocus]);

  const g = useMemoG(() => buildGraph(kind, ctx), [kind, ctx.theme.id, ctx.mode]);
  const matches = useMemoG(() => {
    if (!q) return [];
    return g.nodes.filter(n=>n.id.toLowerCase().includes(q.toLowerCase())).slice(0,6);
  }, [q, g]);

  function pickSearch(id){ setFocusId(id); setSelected(id); setQ(''); }
  // cross-link: jump from the Resolution table / Lineage into a focused graph view
  function jumpTo(id){ if(!id) return; setSelected(id); setFocusId(id); setTab('graph'); }

  return (
    <div className="h-full flex flex-col">
      {/* header */}
      <div className="shrink-0 border-b border-line px-5 pt-4 pb-3 bg-panel">
        <div className="flex items-center justify-between gap-3 mb-3">
          <div className="flex items-center gap-2.5 min-w-0">
            <h1 className="text-[19px] font-semibold text-ink tracking-tight">{tr('Relationships','关系图谱')}</h1>
            <span className="text-[13px] text-ink3 hidden md:inline">{tr('value → primitive → semantic → component → UI','值 → 原始 → 语义 → 组件 → 界面')}</span>
          </div>
          <span className="shrink-0 inline-flex items-center gap-1.5 text-[12px] text-ink3">
            <span className="w-4 h-4 rounded-[4px] flex items-center justify-center text-white text-[9px] font-semibold border border-line2 shrink-0 overflow-hidden" style={{ background:theme.accent[500] }}>{theme.logo ? <img src={theme.logo} alt="" className="w-full h-full object-cover" /> : (theme.name||'?').trim().charAt(0).toUpperCase()}</span>
            {tr('Previewing','预览')} <b className="font-medium text-ink2">{theme.name}</b> · {mode}
          </span>
        </div>
        <div className="flex items-center gap-2 flex-wrap">
          {tab==='graph' && (
            <div className="relative">
              <SearchInput value={q} onChange={setQ} placeholder={tr('Search to focus a token…','搜索以聚焦 token…')} className="w-[240px]" size="sm" />
              {matches.length>0 && (
                <div className="absolute top-9 left-0 z-30 w-[280px] bg-panel border border-line rounded-[12px] py-1 shadow-[0_8px_28px_rgba(0,0,0,0.16)]">
                  {matches.map(m=>{ const v=gValue(m.id,ctx,g.accents); return (
                    <button key={m.id} onClick={()=>pickSearch(m.id)} className="w-full flex items-center gap-2 px-3 h-9 text-left hover:bg-hover transition">
                      {v.kind==='color'?<Swatch color={v.value} size={14}/>:<span className="w-3.5 h-3.5 rounded-[3px]" style={{background:LAYER_BAR[m.layer]}}/>}
                      <span className="font-mono text-[12px] text-ink truncate flex-1">{m.id}</span>
                      <span className="text-[9.5px] uppercase tracking-wide text-ink3">{m.layer}</span>
                    </button>
                  );})}
                </div>
              )}
            </div>
          )}
          {tab==='graph' && <div className="w-px h-6 bg-line mx-0.5" />}
          <Segmented value={tab} onChange={t=>{setTab(t);}} options={[
            {value:'graph',label:tr('Graph','图谱'),icon:'network'},
            {value:'table',label:tr('Resolution','解析表'),icon:'table'},
            {value:'lineage',label:tr('Lineage','血缘'),icon:'route'},
          ]} />
          <div className="w-px h-6 bg-line mx-0.5" />
          <Segmented value={kind} onChange={k=>{setKind(k);setFocusId(null);setSelected(null);}} options={GKINDS.map(k=>({value:k.value,label:tr(k.label,k.labelZh)}))} />
        </div>
      </div>

      {/* body */}
      <div className="flex-1 min-h-0">
        {tab==='graph' && (
          <div className="h-full flex">
            <div className="flex-1 min-w-0 relative">
              <GraphCanvas g={g} kind={kind} ctx={ctx} focusId={focusId} onClearFocus={()=>setFocusId(null)} selected={selected} setSelected={setSelected} tiers={tiers} setTiers={setTiers} />
            </div>
            <div className="w-[300px] shrink-0 border-l border-line bg-panel">
              <DetailPanel g={g} id={selected} kind={kind} ctx={ctx} onClose={()=>setSelected(null)} onFocus={(id)=>{setFocusId(id);}} onOpenComponent={onOpenComponent} />
            </div>
          </div>
        )}
        {tab==='table' && <ResolutionTable g={g} kind={kind} ctx={ctx} onJump={jumpTo} />}
        {tab==='lineage' && <LineagePane g={g} kind={kind} ctx={ctx} focusId={focusId} setFocusId={setFocusId} onJump={jumpTo} />}
      </div>
    </div>
  );
}

Object.assign(window, { GraphPage });
