import React, { useEffect, useMemo, useRef, useState } from 'react';
import cytoscape from 'cytoscape';
// @ts-ignore cytoscape-fcose ships no type declarations
import fcose from 'cytoscape-fcose';
import type { Ontology, NavNode } from '../ontology/types';

// 用成熟的 Cytoscape.js + fCoSE 力导向布局渲染本体网络图。
// fCoSE 是官方推荐的“最小化交叉、聚类清晰”的力导向布局。
// 节点模型:
//   - 实体 = compound(复合)框;框内只保留自身「标识(主键)」子节点(显示自己模块内的名字)。
//   - 普通属性 = 独立节点(卫星),用连线挂到实体框外,不再嵌进框内。
//   - 跨聚合引用 = 独立「引用节点」(专色 C 紫),落在引用方与被引用实体之间,
//     显示的是「引用地的表述」(引用方 fk 字段的 zh,如 客户编号2),常驻可见。
//   - 标识被隐藏后,实体框变空,模块名回到框内居中(不再浮在空框上方)。
// 左侧目录内置为组件自身的左栏(不再 portal 到外部侧边栏),严格按本体的 navigations(M3 导航树)
// 组织;无导航时退化为按聚合平铺。
cytoscape.use(fcose);

const COL: Record<string, string[]> = {
  entity: ['#eef2fd', '#5b7fe0', '#33529e'],
  behavior: ['#e9f7ee', '#4fae6e', '#2f7d4c'],
  rule: ['#fdf3e8', '#e0944a', '#9a5e1d'],
  event: ['#fbe3ef', '#e8579b', '#b83b7e'],
  nav: ['#f2f3f5', '#8e8e96', '#3f3f46'],
  identifier: ['#fdf3d6', '#caa011', '#7a5c00'],   // 标识 A(金)
  attribute: ['#eef0f3', '#9298a3', '#4b5058'],    // 属性 B(灰)——与标识同形不同色
  bridge: ['#efe7fb', '#7c4dcf', '#4d2a94'],       // 引用节点 C(紫)——跨实体引用的落点
};
const TYPE_CN: Record<string, string> = { entity: '实体', behavior: '行为', rule: '规则', event: '事件', nav: '目录', identifier: '标识', attribute: '属性', bridge: '引用' };

// 属性 / 标识作为独立节点:合成 id = attr::<对象id>::<字段名>(字段名仅在对象内唯一,故带上对象 id 前缀)。
// 标识 = is_primary_key 的属性(M1 导入时被放到 attributes 首位),其余为普通属性。
const attrNodeId = (oid: string, name: string) => `attr::${oid}::${name}`;
const attrNodeType = (a: any) => (a.is_primary_key ? 'identifier' : 'attribute');
// 跨聚合引用列(relationship.fk_field):不单独成普通属性节点,而是生成一个「引用节点」,
// 显示引用方对该外键列的表述(如订单里的 客户编号2),落在引用方与被引用实体之间。
const fkFields = (o: any) => new Set((o.relationships || []).filter((r: any) => r.fk_field).map((r: any) => r.fk_field));
const attrNodeIds = (o: any) => { const fk = fkFields(o); return (o.attributes || []).filter((a: any) => !fk.has(a.name)).map((a: any) => attrNodeId(o.id, a.name)); };
// 引用节点 id:按(引用方对象, 关系)唯一。每条跨聚合引用一个,互不共享。
const refNodeId = (ownerId: string, rel: any) => `ref::${ownerId}::${rel.fk_field || rel.name}`;
// 引用方对该外键列的表述(引用地的 zh);取不到时退回关系名。
const refLabel = (o: any, rel: any) => {
  const a = (o.attributes || []).find((x: any) => x.name === rel.fk_field);
  return a?.label || rel.label || rel.name;
};

// M3 导航节点是“空节点”:自身无实体/行为内容,只作为界面目录入口。
function findNav(nodes: NavNode[] | undefined, id: string): NavNode | null {
  for (const n of nodes || []) { if (`nav__${n.id}` === id) return n; const f = findNav(n.children, id); if (f) return f; }
  return null;
}

function nameOf(ontology: Ontology, id: string): string {
  if (typeof id === 'string' && id.startsWith('nav__')) { const n = findNav(ontology.navigations, id); if (n) return n.zh || n.name || n.id; }
  return ontology.objects.find((o) => o.id === id)?.name
    || ontology.behaviors.find((b) => b.id === id)?.name
    || ontology.rules.find((r) => r.id === id)?.name
    || ontology.events.find((e) => e.id === id)?.name || id;
}
function describe(ontology: Ontology, id: string): any {
  // 引用节点:显示引用地的表述 + 引用方 / 指向
  if (typeof id === 'string' && id.startsWith('ref::')) {
    const [, ownerId, fk] = id.split('::');
    const owner = ontology.objects.find((x) => x.id === ownerId);
    const rel = owner && (owner.relationships || []).find((r) => (r.fk_field || r.name) === fk);
    const target = rel && ontology.objects.find((x) => x.id === rel.target_object_id);
    return {
      type: 'bridge',
      title: owner && rel ? refLabel(owner, rel) : fk,
      lines: [
        '跨实体引用(连接点)',
        owner ? `引用方: ${owner.name}` : null,
        target ? `指向: ${target.name}` : null,
        rel?.fk_field ? `字段 ${rel.fk_field}` : null,
      ].filter(Boolean),
    };
  }
  if (typeof id === 'string' && id.startsWith('attr::')) {
    const rest = id.slice('attr::'.length);
    const sep = rest.indexOf('::');
    const oid = rest.slice(0, sep), fname = rest.slice(sep + 2);
    const o = ontology.objects.find((x) => x.id === oid);
    const a: any = o && (o.attributes || []).find((x) => x.name === fname);
    if (a) {
      const referenced = a.is_primary_key && ontology.objects.some((x) => (x.relationships || []).some((r) => r.fk_field && r.target_object_id === oid));
      return {
        type: attrNodeType(a),
        title: a.label || a.name,
        lines: [
          `字段 ${a.name} · ${a.type}`,
          a.is_primary_key ? '主键(标识)' : null,
          referenced ? '被其他实体引用' : null,
          [a.required ? '必填' : null, a.unique ? '唯一' : null].filter(Boolean).join(' · ') || null,
          a.enum_values?.length ? `枚举: ${a.enum_values.join('、')}` : null,
          o ? `属于: ${o.name}` : null,
          a.description,
        ].filter(Boolean),
      };
    }
  }
  if (typeof id === 'string' && id.startsWith('nav__')) {
    const n = findNav(ontology.navigations, id);
    if (n) { const kids = (n.children || []).map((c) => c.zh || c.name).filter(Boolean);
      return { type: 'nav', title: n.zh || n.name || n.id, lines: [
        n.link ? `入口 → ${nameOf(ontology, n.link)}` : '分组节点(空节点)',
        kids.length ? `子项: ${kids.join('、')}` : null,
      ].filter(Boolean) }; }
  }
  const o = ontology.objects.find((x) => x.id === id);
  if (o) {
    // 关系已由图上的连线表达,卡片内不再重复列出
    return { type: 'entity', title: `${o.name} (${o.alias})`, lines: [o.description].filter(Boolean) };
  }
  const b = ontology.behaviors.find((x) => x.id === id);
  if (b) return { type: 'behavior', title: b.name, lines: [
    `作用于: ${nameOf(ontology, b.owner_object_id)}`,
    b.applied_rules?.length ? `规则: ${b.applied_rules.map((r) => nameOf(ontology, r)).join(', ')}` : null,
    b.produced_events?.length ? `事件: ${b.produced_events.map((e) => nameOf(ontology, e)).join(', ')}` : null,
  ].filter(Boolean) };
  const r = ontology.rules.find((x) => x.id === id);
  if (r) return { type: 'rule', title: r.name, lines: [`${r.type} · ${r.enforced ? '可强制' : '仅说明'}`, r.expression].filter(Boolean) };
  const e = ontology.events.find((x) => x.id === id);
  if (e) { const by = ontology.behaviors.filter((x) => (x.produced_events || []).includes(id)).map((x) => x.name);
    return { type: 'event', title: e.name, lines: [by.length ? `由 ${by.join(', ')} 产出` : null, e.description].filter(Boolean) }; }
  return null;
}

function buildElements(ontology: Ontology): any[] {
  const els: any[] = [];
  const clen = (s: string) => [...s].length;
  ontology.objects.forEach((o) => {
    const bw = Math.max(56, Math.min(120, clen(o.name) * 14 + 22));
    els.push({ data: { id: o.id, label: o.name, type: 'entity', bw, bh: 34, bfont: 12, btw: bw - 14, bmy: 0 } });
    const fk = fkFields(o);
    (o.attributes || []).forEach((a) => {
      if (fk.has(a.name)) return;                       // 外键引用列不成普通属性节点(由引用节点表达)
      const nid = attrNodeId(o.id, a.name);
      const lb = a.label || a.name;
      const w = Math.max(44, Math.min(116, clen(lb) * 11 + 16));
      const base = { id: nid, label: lb, type: attrNodeType(a), bw: w, bh: 24, bfont: 9, btw: w - 12, bmy: 0 };
      // 标识(主键)→ 嵌进实体框(compound 子节点);普通属性 → 独立卫星节点(连线在下方补)
      els.push({ data: a.is_primary_key ? { ...base, parent: o.id } : base });
    });
  });
  ontology.behaviors.forEach((b) => els.push({ data: { id: b.id, label: b.name, type: 'behavior', bw: 58, bh: 58, bfont: 9.5, btw: 46, bmy: 0 } }));
  ontology.rules.forEach((r) => els.push({ data: { id: r.id, label: r.name, type: 'rule', bw: 68, bh: 68, bfont: 8.5, btw: 42, bmy: 0 } }));
  ontology.events.forEach((e) => els.push({ data: { id: e.id, label: e.name, type: 'event', bw: 56, bh: 56, bfont: 8.5, btw: 34, bmy: 8 } }));
  // M3 导航:每个目录节点作为“空节点”入图,按层级相连,叶子连到所引用的聚合根。
  // 层级连线标注级别:顶层→子=一级,再往下=二级……(取子节点深度)
  const CN_NUM = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
  const levelLabel = (d: number) => `${CN_NUM[d - 1] || d}级`;
  const navMeta: any[] = [];
  const walkNav = (n: NavNode, parentGid: string | null, depth: number) => {
    const gid = `nav__${n.id}`;
    const label = n.zh || n.name || n.id;
    const bw = Math.max(56, Math.min(150, clen(label) * 14 + 22));
    els.push({ data: { id: gid, label, type: 'nav', bw, bh: 32, bfont: 11, btw: bw - 14, bmy: 0 } });
    navMeta.push({ gid, parentGid, link: n.link || null, depth });
    (n.children || []).forEach((c) => walkNav(c, gid, depth + 1));
  };
  (ontology.navigations || []).forEach((n) => walkNav(n, null, 0));
  const ids = new Set(els.map((e) => e.data.id));
  const edge = (s: string, t: string, label: string, kind: string, i: any) => els.push({ data: { id: `${s}_${kind}_${t}_${i}`, source: s, target: t, label, hlcolor: COL[kind][1] } });
  ontology.behaviors.forEach((b, i) => {
    if (ids.has(b.owner_object_id)) edge(b.id, b.owner_object_id, 'ownerEntity', 'behavior', i);
    (b.applied_rules || []).forEach((r, j) => ids.has(r) && edge(b.id, r, 'appliedRules', 'rule', `${i}${j}`));
    (b.produced_events || []).forEach((e, j) => ids.has(e) && edge(b.id, e, 'produces', 'event', `${i}${j}`));
  });
  // 普通属性(卫星)→ 实体框:连线(标识在框内由 compound 包含表达,不连线)
  ontology.objects.forEach((o, i) => (o.attributes || []).forEach((a, j) => {
    if (a.is_primary_key || fkFields(o).has(a.name)) return;
    const nid = attrNodeId(o.id, a.name);
    if (ids.has(nid)) edge(o.id, nid, '', 'attribute', `a${i}_${j}`);
  }));
  ontology.objects.forEach((o, i) => (o.relationships || []).forEach((rel, j) => {
    if (!ids.has(rel.target_object_id)) return;
    // 跨聚合引用:引用方 →〔引用节点(引用地表述,如 客户编号2)〕→ 被引用实体
    if (rel.fk_field) {
      const rid = refNodeId(o.id, rel);
      const lb = refLabel(o, rel);
      const w = Math.max(44, Math.min(120, clen(lb) * 11 + 16));
      els.push({ data: { id: rid, label: lb, type: 'attribute', bridge: true, bw: w, bh: 24, bfont: 9, btw: w - 12, bmy: 0 } });
      edge(o.id, rid, '引用', 'bridge', `${i}${j}`);
      edge(rid, rel.target_object_id, '', 'bridge', `${i}${j}b`);
      return;
    }
    edge(o.id, rel.target_object_id, rel.label || rel.name, 'entity', `${i}${j}`);
  }));
  navMeta.forEach((m, i) => {
    if (m.parentGid) edge(m.parentGid, m.gid, levelLabel(m.depth), 'nav', `nv${i}`);
    if (m.link && ids.has(m.link)) edge(m.gid, m.link, '入口', 'nav', `nl${i}`);
  });
  return els;
}

// 聚合层级:聚合根 → 子实体 / 跨聚合引用 / 行为(→ 规则、事件)。
function buildTree(ontology: Ontology) {
  const roots = ontology.objects.filter((o) => !o.parent_id);
  const usedRules = new Set<string>(); const usedEvents = new Set<string>();
  const tree = roots.map((root) => {
    const subs = ontology.objects.filter((o) => o.parent_id === root.id);
    const subIds = new Set(subs.map((s) => s.id));
    const refs: any[] = [];
    [root, ...subs].forEach((o) => (o.relationships || []).forEach((rel) => {
      if (subIds.has(rel.target_object_id) || rel.target_object_id === root.id) return; // 组合已列为子实体
      const t = ontology.objects.find((x) => x.id === rel.target_object_id);
      if (t) refs.push({ key: `${o.id}_${rel.name}`, target: t.id, label: `${rel.fk_field || rel.name} → ${t.name}`, refNode: rel.fk_field ? refNodeId(o.id, rel) : null });
    }));
    const behaviors = ontology.behaviors
      .filter((b) => b.owner_object_id === root.id || subIds.has(b.owner_object_id))
      .map((b) => {
        const rules = (b.applied_rules || []).map((id) => ontology.rules.find((r) => r.id === id)).filter(Boolean) as any[];
        const events = (b.produced_events || []).map((id) => ontology.events.find((e) => e.id === id)).filter(Boolean) as any[];
        rules.forEach((r) => usedRules.add(r.id)); events.forEach((e) => usedEvents.add(e.id));
        return { b, rules, events };
      });
    return { root, subs, refs, behaviors };
  });
  // 挂不上任何聚合的节点兜底展示,保证目录覆盖图上每个节点
  const knownObj = new Set(ontology.objects.map((o) => o.id));
  const others = [
    ...ontology.behaviors.filter((b) => !knownObj.has(b.owner_object_id)).map((b) => ({ id: b.id, label: b.name, type: 'behavior' })),
    ...ontology.rules.filter((r) => !usedRules.has(r.id)).map((r) => ({ id: r.id, label: r.name, type: 'rule' })),
    ...ontology.events.filter((e) => !usedEvents.has(e.id)).map((e) => ({ id: e.id, label: e.name, type: 'event' })),
  ];
  return { tree, others };
}

// 聚合簇覆盖的全部节点 id(根 + 子实体 + 跨聚合引用节点/目标 + 行为 + 规则 + 事件)。
// 引用目标(如 订单明细→产品)纳入本簇:选中本模块时,即使产品属于别的模块也一并显示。
function clusterIds(t: any): any[] {
  const ids = [t.root.id, ...attrNodeIds(t.root),
    ...t.subs.flatMap((s: any) => [s.id, ...attrNodeIds(s)]),
    ...t.refs.map((r: any) => r.target), ...t.refs.map((r: any) => r.refNode).filter(Boolean)];
  t.behaviors.forEach(({ b, rules, events }: any) => ids.push(b.id, ...rules.map((r: any) => r.id), ...events.map((e: any) => e.id)));
  return ids;
}

// 一行:缩进由外层 nest() 容器提供,这里只管内容与选中态。
function Row({ type = 'entity', label, alias, note, bold, selected, chev, onChev, onPick }: any) {
  const c = COL[type] || COL.entity;
  return (
    <div onClick={onPick} title={label}
      style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '4px 8px', borderRadius: 7, cursor: 'pointer', background: selected ? c[0] : 'transparent', fontSize: 12, color: '#3f3f46', lineHeight: 1.45, userSelect: 'none' }}>
      <span onClick={onChev ? (e: any) => { e.stopPropagation(); onChev(); } : undefined}
        style={{ width: 12, textAlign: 'center', color: '#b4b4ba', fontSize: 9, flex: 'none', cursor: onChev ? 'pointer' : 'default' }}>
        {chev === 'open' ? '▾' : chev === 'closed' ? '▸' : ''}
      </span>
      <span style={{ width: 8, height: 8, borderRadius: type === 'entity' || type === 'nav' ? 2 : 8, background: c[1], flex: 'none' }} />
      <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: bold ? 600 : 400 }}>
        {label}{alias && <span style={{ color: '#b8b8be', fontWeight: 400, marginLeft: 6, fontSize: 11 }}>{alias}</span>}
      </span>
      {note && <span style={{ fontSize: 10, color: '#b4b4ba', flex: 'none' }}>{note}</span>}
    </div>
  );
}

const GroupLabel = ({ text, count }: any) => (
  <div style={{ padding: '5px 6px 2px 8px', fontSize: 10, fontWeight: 600, letterSpacing: '.03em', color: '#aeaeb4', userSelect: 'none' }}>{text}{count != null ? ` · ${count}` : ''}</div>
);

// 按类型 + 孤儿 + 目录折叠 过滤:用 display:none(可逆),连线随端点自动隐藏。
// 孤儿 = 没有任何连到“类型可见”邻居的边(含 0 边的独立节点)。
// dirHidden = 目录里被折叠的聚合/导航分组所覆盖的全部节点 id。
function applyFilter(cy: any, hiddenTypes: Record<string, boolean>, hideOrphans: boolean, dirHidden: Set<any> = new Set()) {
  if (!cy) return;
  cy.batch(() => {
    cy.nodes().forEach((n: any) => {
      const t = n.data('type');
      let disp = 'element';
      // 引用节点常驻显示——即使隐藏了“标识/属性”类型,也保留两实体之间的连接点
      if (n.data('bridge')) { n.style('display', 'element'); return; }
      if (hiddenTypes[t] || dirHidden.has(n.id())) disp = 'none';
      else if (hideOrphans) {
        const hasNbr = n.connectedEdges().some((e: any) => {
          const other = e.source().id() === n.id() ? e.target() : e.source();
          return !hiddenTypes[other.data('type')] && !dirHidden.has(other.id());
        });
        if (!hasNbr) disp = 'none';
      }
      n.style('display', disp);
    });
    // 标识(框内)被隐藏后,实体框变空 → 让模块名回到框内居中(不再浮在空框上方)
    cy.nodes('[type = "entity"]').forEach((n: any) => {
      const hasVisibleChild = n.children().some((c: any) => c.style('display') !== 'none');
      n.toggleClass('entity-empty', !hasVisibleChild);
    });
  });
  // 不调用 fit:筛选时保持当前视图位置与缩放不变
}

const STYLE: any[] = [
  { selector: 'node', style: { label: 'data(label)', 'text-wrap': 'wrap', 'border-width': 1.6, 'transition-property': 'opacity', 'transition-duration': '0.15s' } },
  // 叶子节点(属性/标识/行为/规则/事件/目录)用固定基准尺寸,标签居中
  { selector: 'node[type != "entity"]', style: { width: 'data(bw)', height: 'data(bh)', 'font-size': 'data(bfont)', 'text-max-width': 'data(btw)', 'text-valign': 'center', 'text-halign': 'center' } },
  // 实体 = compound 父节点:自动裹住框内标识,半透明底,标签置顶
  { selector: 'node[type="entity"]', style: { shape: 'round-rectangle', 'background-color': COL.entity[0], 'background-opacity': 0.4, 'border-color': COL.entity[1], color: COL.entity[2], 'text-valign': 'top', 'text-halign': 'center', 'font-size': 12, 'font-weight': 700, 'text-margin-y': 3, padding: '12px', 'min-width': 44, 'min-height': 22 } },
  { selector: 'node[type="behavior"]', style: { shape: 'ellipse', 'background-color': COL.behavior[0], 'border-color': COL.behavior[1], color: COL.behavior[2] } },
  { selector: 'node[type="rule"]', style: { shape: 'diamond', 'background-color': COL.rule[0], 'border-color': COL.rule[1], color: COL.rule[2] } },
  { selector: 'node[type="event"]', style: { shape: 'triangle', 'background-color': COL.event[0], 'border-color': COL.event[1], color: COL.event[2], 'text-margin-y': 'data(bmy)' } },
  // 目录节点=空节点:白底虚线边框,视觉上“空”
  { selector: 'node[type="nav"]', style: { shape: 'round-rectangle', 'background-color': '#ffffff', 'border-color': COL.nav[1], 'border-style': 'dashed', color: COL.nav[2] } },
  // 标识 A(金)与属性 B(灰)同形(圆角矩形),只靠颜色区分
  { selector: 'node[type="identifier"]', style: { shape: 'round-rectangle', 'background-color': COL.identifier[0], 'border-color': COL.identifier[1], color: COL.identifier[2] } },
  { selector: 'node[type="attribute"]', style: { shape: 'round-rectangle', 'background-color': COL.attribute[0], 'border-color': COL.attribute[1], color: COL.attribute[2] } },
  // 引用节点用属性灰(type=attribute),不再单独着色
  // 实体框内标识被隐藏 → 空框:回到固定尺寸、实底、标签居中(模块名回到框内)
  { selector: 'node.entity-empty', style: { 'min-width': 'data(bw)', 'min-height': 'data(bh)', 'text-valign': 'center', 'text-halign': 'center', 'text-margin-y': 0, 'background-opacity': 1, padding: '0px' } },
  { selector: 'edge', style: { width: 1.3, 'line-color': '#dfe1e8', 'curve-style': 'bezier', label: 'data(label)', 'font-size': 8, color: '#b8bac4', 'text-rotation': 'autorotate', 'text-background-color': '#fbfbfc', 'text-background-opacity': 0.85, 'text-background-padding': 1 } },
  { selector: '.faded', style: { opacity: 0.12, 'text-opacity': 0.06 } },
  { selector: 'node.hl', style: { 'border-width': 3.5 } },
  { selector: 'edge.hl', style: { 'line-color': 'data(hlcolor)', color: 'data(hlcolor)' } },
];

export default function NetworkGraph({ ontology }: { ontology: Ontology }) {
  const boxRef = useRef<HTMLDivElement | null>(null);
  const cyRef = useRef<any>(null);
  const tipRef = useRef<HTMLDivElement | null>(null);
  const activeRef = useRef<any>(null);
  const selRef = useRef(false);
  const [tip, setTip] = useState<any>(null);
  const [hiddenTypes, setHiddenTypes] = useState<Record<string, boolean>>({ nav: true });   // 默认隐藏目录节点(可在筛选面板点「目录」显示)
  const [hideOrphans, setHideOrphans] = useState(false);
  const filterRef = useRef<any>({ hiddenTypes: { nav: true }, hideOrphans: false, dirHidden: new Set() });

  // ---- 目录数据:M3 导航树 + 聚合层级 ----
  const { tree, others } = useMemo(() => buildTree(ontology), [ontology]);
  const navs = useMemo(() => ontology.navigations || [], [ontology]);
  const byRoot = useMemo(() => Object.fromEntries(tree.map((t) => [t.root.id, t])), [tree]);
  // 未被任何导航叶子引用的聚合,兜底列在目录底部
  const restTree = useMemo(() => {
    if (!navs.length) return [];
    const used = new Set<string>();
    const mark = (n: NavNode) => { if (n.link) used.add(n.link); (n.children || []).forEach(mark); };
    navs.forEach(mark);
    return tree.filter((t) => !used.has(t.root.id));
  }, [navs, tree]);

  const [closedNavs, setClosedNavs] = useState<Set<string>>(() => new Set());   // 折叠的导航节点(含叶子)
  const [closedRoots, setClosedRoots] = useState<Set<string>>(() => new Set()); // 无导航退化模式下折叠的聚合
  const [openBehs, setOpenBehs] = useState<Set<string>>(() => new Set());
  const [selId, setSelId] = useState<string | null>(null);
  const pickRef = useRef<((id: string) => void) | null>(null);
  const pick = (id: string) => pickRef.current && pickRef.current(id);
  // 每个节点 → 它的“底层全部节点”id 列表(点击 → 高亮整簇);图与目录共用同一份。
  const descRef = useRef<Map<any, any>>(new Map());
  useEffect(() => {
    const m = new Map<any, any>();
    tree.forEach((t) => {
      m.set(t.root.id, clusterIds(t));
      t.subs.forEach((s) => {
        const obj = ontology.objects.find((o) => o.id === s.id);
        const refT = (obj?.relationships || []).map((r) => r.target_object_id).filter((id) => id && id !== t.root.id);
        const refNodes = (obj?.relationships || []).filter((r) => r.fk_field).map((r) => refNodeId(s.id, r));
        m.set(s.id, [s.id, ...attrNodeIds(s), ...refT, ...refNodes]);
      });
      t.behaviors.forEach(({ b, rules, events }) => {
        m.set(b.id, [b.id, ...rules.map((r) => r.id), ...events.map((e) => e.id)]);
        rules.forEach((r) => m.has(r.id) || m.set(r.id, [r.id]));
        events.forEach((e) => m.has(e.id) || m.set(e.id, [e.id]));
      });
    });
    const walk = (n: NavNode) => {
      const ids: any[] = [];
      const w2 = (node: NavNode) => { ids.push(`nav__${node.id}`); if (node.link && byRoot[node.link]) ids.push(...clusterIds(byRoot[node.link])); (node.children || []).forEach(w2); };
      w2(n);
      m.set(`nav__${n.id}`, ids);
      (n.children || []).forEach(walk);
    };
    navs.forEach(walk);
    descRef.current = m;
  }, [ontology, tree, navs, byRoot]);

  // 目录折叠只作用于目录本身(展开/合并树行),不再隐藏图上节点;
  // 图的聚焦改由“点击任意节点 → 高亮它旗下的全部底层节点”实现(见 descRef / highlightById)。
  const dirHidden = useMemo(() => new Set(), []);

  useEffect(() => {
    const cy = cytoscape({
      container: boxRef.current,
      elements: buildElements(ontology),
      style: STYLE,
      minZoom: 0.2, maxZoom: 4, wheelSensitivity: 0.25,
    });
    cyRef.current = cy;

    // 布局在挂载后(容器与节点尺寸就绪)才跑,避免首帧节点重叠/漏画
    const LAYOUT: any = { name: 'fcose', quality: 'proof', randomize: true, animate: false, fit: true, padding: 45, nodeSeparation: 110, idealEdgeLength: 120, nodeRepulsion: 8000, gravity: 0.28, packComponents: true, nestingFactor: 0.6 };
    const runLayout = () => { cy.resize(); cy.layout(LAYOUT).run(); };
    requestAnimationFrame(runLayout);
    cy.on('layoutstop', () => { applyFilter(cy, filterRef.current.hiddenTypes, filterRef.current.hideOrphans, filterRef.current.dirHidden); });
    const ro = new ResizeObserver(() => cy.resize());
    if (boxRef.current) ro.observe(boxRef.current);

    const place = () => {
      const n = activeRef.current, el = tipRef.current;
      if (!n || !el || !boxRef.current) return;
      const p = n.renderedPosition();
      const flip = p.x > boxRef.current.clientWidth * 0.58;
      el.style.left = `${flip ? p.x - 300 : p.x + 30}px`;
      el.style.top = `${Math.max(8, p.y - 12)}px`;
    };
    const showTip = (n: any) => { activeRef.current = n; setTip(describe(ontology, n.id())); requestAnimationFrame(place); };
    const hideTip = () => { activeRef.current = null; setTip(null); };

    // 高亮一组节点(整簇):淡化其余,连线仅保留簇内。
    const highlightCluster = (idsArr: any[], focusId: string | null, doFit?: boolean) => {
      const eles = cy.collection();
      idsArr.forEach((id) => { const n = cy.$id(id); if (n && n.nonempty() && n.style('display') !== 'none') eles.merge(n); });
      if (eles.empty()) return;
      selRef.current = true;
      cy.elements().removeClass('faded hl');
      cy.elements().addClass('faded');
      eles.removeClass('faded');
      const inner = eles.edgesWith(eles);
      inner.removeClass('faded').addClass('hl');
      eles.addClass('hl');
      setSelId(focusId || null);
      hideTip();
      if (doFit && eles.length > 1) cy.animate({ fit: { eles: eles.union(inner), padding: 60 }, duration: 260 });
      else if (focusId) { const f = cy.$id(focusId); if (f.nonempty()) cy.animate({ center: { eles: f }, duration: 220 }); }
    };
    // 任意节点点击(图内或目录)→ 高亮它的“底层全部节点”,而非仅相邻节点
    const highlightById = (id: string, doFit?: boolean) => highlightCluster(descRef.current.get(id) || [id], id, doFit);
    cy.on('tap', 'node', (evt: any) => highlightById(evt.target.id(), true));
    cy.on('tap', (evt: any) => { if (evt.target === cy) { selRef.current = false; cy.elements().removeClass('faded hl'); setSelId(null); hideTip(); } });
    pickRef.current = (id: string) => highlightById(id, true);
    cy.on('mouseover', 'node', (evt: any) => { if (!selRef.current) showTip(evt.target); });
    cy.on('mouseout', 'node', () => { if (!selRef.current) hideTip(); });
    cy.on('render', place);

    return () => { ro.disconnect(); cy.destroy(); };
  }, [ontology]);

  // 过滤变化时重新应用(类型筛选 / 孤儿 / 目录折叠)
  useEffect(() => {
    filterRef.current = { hiddenTypes, hideOrphans, dirHidden };
    if (cyRef.current) applyFilter(cyRef.current, hiddenTypes, hideOrphans, dirHidden);
  }, [hiddenTypes, hideOrphans, dirHidden]);

  const zoom = (f: number) => { const cy = cyRef.current; if (cy) cy.zoom({ level: cy.zoom() * f, renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 } }); };
  const fit = () => { const cy = cyRef.current; if (cy) { const v = cy.elements(':visible'); cy.fit(v.length ? v : undefined, 40); } };
  const TYPES: [string, string][] = [['entity', '实体'], ['identifier', '标识'], ['attribute', '属性'], ['behavior', '行为'], ['rule', '规则'], ['event', '事件'], ['nav', '目录']];
  const toggleType = (t: string) => setHiddenTypes((h) => ({ ...h, [t]: !h[t] }));
  const btn: React.CSSProperties = { width: 30, height: 30, borderRadius: 8, border: '1px solid #e6e6e6', background: '#fff', color: '#52525b', fontSize: 16, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' };
  const toggleSet = (setter: React.Dispatch<React.SetStateAction<Set<string>>>) => (id: string) => setter((s) => { const n = new Set(s); if (n.has(id)) n.delete(id); else n.add(id); return n; });
  const toggleNav = toggleSet(setClosedNavs);
  const toggleRoot = toggleSet(setClosedRoots);
  const toggleBeh = toggleSet(setOpenBehs);
  const allNavIds = useMemo(() => { const s: string[] = []; const walk = (n: NavNode) => { s.push(n.id); (n.children || []).forEach(walk); }; navs.forEach(walk); return s; }, [navs]);
  const expandAll = () => { setClosedNavs(new Set()); setClosedRoots(new Set()); setOpenBehs(new Set(ontology.behaviors.map((b) => b.id))); };
  const collapseAll = () => { setClosedNavs(new Set(allNavIds)); setClosedRoots(new Set(tree.map((t) => t.root.id))); setOpenBehs(new Set()); };

  // ---- 目录渲染(树线样式) ----
  // 一层缩进容器,画竖直引导线
  const nest = (children: React.ReactNode) => (
    <div style={{ marginLeft: 9, paddingLeft: 7, borderLeft: '1px solid #e8e8ea' }}>{children}</div>
  );
  // 顶层模块标题:点标题=图上高亮整簇;点三角=展开/合并目录
  const moduleHeader = (label: string, open: boolean, onToggle: () => void, onPick: () => void, note: any) => (
    <div onClick={onPick} style={{ display: 'flex', alignItems: 'center', gap: 7, cursor: 'pointer', userSelect: 'none', padding: '6px 8px', margin: '3px 0 1px', borderRadius: 6, fontSize: 13, fontWeight: 700, color: '#27272a' }}>
      <span onClick={(e) => { e.stopPropagation(); onToggle(); }} style={{ width: 12, textAlign: 'center', color: '#9a9aa0', fontSize: 10, cursor: 'pointer' }}>{open ? '▾' : '▸'}</span>
      <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
      {note && <span style={{ fontSize: 10, color: '#b4b4ba', fontWeight: 400 }}>{note}</span>}
    </div>
  );

  // 聚合内部:子实体 / 引用 / 行为(→ 规则、事件),不含根行(根由上层承载)。
  // 这些是具体节点,点击走 pick():居中并高亮邻域(看单节点关系)。
  const renderAggInner = (t: any) => (
    <>
      {t.subs.length > 0 && <GroupLabel text="子实体" count={t.subs.length} />}
      {t.subs.map((s: any) => <Row key={s.id} type="entity" label={s.name} selected={selId === s.id} onPick={() => pick(s.id)} />)}
      {t.refs.length > 0 && <GroupLabel text="引用" count={t.refs.length} />}
      {t.refs.map((r: any) => <Row key={r.key} type="entity" label={r.label} selected={selId === r.target} onPick={() => pick(r.target)} />)}
      {t.behaviors.length > 0 && <GroupLabel text="行为" count={t.behaviors.length} />}
      {t.behaviors.map(({ b, rules, events }: any) => {
        const kids = rules.length + events.length;
        const bOpen = openBehs.has(b.id);
        return (
          <React.Fragment key={b.id}>
            <Row type="behavior" label={b.name} chev={kids ? (bOpen ? 'open' : 'closed') : null} onChev={kids ? () => toggleBeh(b.id) : null} selected={selId === b.id} onPick={() => pick(b.id)} />
            {bOpen && nest(
              <>
                {rules.map((r: any) => <Row key={r.id} type="rule" label={r.name} selected={selId === r.id} onPick={() => pick(r.id)} />)}
                {events.map((e: any) => <Row key={e.id} type="event" label={e.name} selected={selId === e.id} onPick={() => pick(e.id)} />)}
              </>
            )}
          </React.Fragment>
        );
      })}
    </>
  );

  // 聚合根实体行 + 内部。根实体(如 订单)在图里是独立节点,目录里也单列一行,
  // 作为叶子目录(如 销售订单明细)的子节点,与图完全对齐。
  const renderAggRootAndInner = (t: any) => {
    const open = !closedRoots.has(t.root.id);
    return (
      <>
        <Row type="entity" label={t.root.name} bold note={`${t.subs.length + t.refs.length}子·${t.behaviors.length}行为`}
          chev={open ? 'open' : 'closed'} onChev={() => toggleRoot(t.root.id)}
          selected={selId === t.root.id} onPick={() => pick(t.root.id)} />
        {open && nest(renderAggInner(t))}
      </>
    );
  };

  // 非顶层导航节点:点标题=高亮该目录旗下全部底层节点;点三角=展开/合并目录
  const renderNav = (n: NavNode) => {
    const open = !closedNavs.has(n.id);
    const t = n.link ? byRoot[n.link] : null;
    const label = n.zh || n.name || n.id;
    return (
      <React.Fragment key={n.id}>
        <Row type="nav" label={label} bold
          note={t ? `${t.subs.length + t.refs.length}子·${t.behaviors.length}行为` : null}
          chev={open ? 'open' : 'closed'} onChev={() => toggleNav(n.id)}
          selected={selId === `nav__${n.id}`} onPick={() => pick(`nav__${n.id}`)} />
        {open && nest(t ? renderAggRootAndInner(t) : (n.children || []).map(renderNav))}
      </React.Fragment>
    );
  };

  // 顶层模块(M3 第一层)
  const renderTopModule = (n: NavNode) => {
    const open = !closedNavs.has(n.id);
    const t = n.link ? byRoot[n.link] : null;
    const label = n.zh || n.name || n.id;
    const note = t ? `${t.subs.length + t.refs.length}子·${t.behaviors.length}行为` : null;
    return (
      <React.Fragment key={n.id}>
        {moduleHeader(label, open, () => toggleNav(n.id), () => pick(`nav__${n.id}`), note)}
        {open && nest(t ? renderAggRootAndInner(t) : (n.children || []).map(renderNav))}
      </React.Fragment>
    );
  };

  // 顶层聚合(无 M3 导航 / 未入目录时的兜底)
  const renderAggTop = (t: any) => (
    <div key={t.root.id} style={{ marginBottom: 2 }}>{renderAggRootAndInner(t)}</div>
  );

  const dirLink: React.CSSProperties = { cursor: 'pointer', color: '#6b6b72', userSelect: 'none' };
  const directory = (
    <div style={{ padding: '2px 4px 12px' }}>
      <div style={{ display: 'flex', gap: 12, padding: '2px 8px 8px', fontSize: 11 }}>
        <span onClick={expandAll} style={dirLink}>全部展开</span>
        <span onClick={collapseAll} style={dirLink}>全部合并</span>
      </div>
      {navs.length ? (
        <>
          {navs.map(renderTopModule)}
          {restTree.length > 0 && <GroupLabel text="未入目录" count={restTree.length} />}
          {restTree.map(renderAggTop)}
        </>
      ) : tree.map(renderAggTop)}
      {others.length > 0 && <GroupLabel text="未归组" count={others.length} />}
      {others.map((o) => <Row key={o.id} type={o.type} label={o.label} selected={selId === o.id} onPick={() => pick(o.id)} />)}
    </div>
  );

  return (
    <div style={{ position: 'absolute', inset: 0, display: 'flex', overflow: 'hidden' }}>
      {/* 左侧目录(内置为组件自身的左栏,不再 portal 到外部侧边栏) */}
      <div style={{ width: 260, flex: 'none', overflow: 'auto', borderRight: '1px solid #ececec', background: '#fff' }}>
        {directory}
      </div>

      {/* 右侧:本体网络图 + 悬浮控件 */}
      <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
        <div ref={boxRef} style={{ position: 'absolute', inset: 0, background: 'radial-gradient(circle at 55% 42%,#fdfdfe,#f6f7f9)' }} />

        {/* 筛选面板:按类型显示/隐藏 + 隐藏孤儿节点 */}
        <div style={{ position: 'absolute', top: 12, left: 14, width: 236, zIndex: 4 }}>
          <div style={{ background: '#fff', border: '1px solid #ececec', borderRadius: 10, padding: '8px 10px', boxShadow: '0 1px 8px rgba(0,0,0,.06)' }}>
            <div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
              <span style={{ fontSize: 11, color: '#a6a6ac' }}>显示</span>
              {TYPES.map(([t, label]) => {
                const hid = !!hiddenTypes[t]; const c = COL[t];
                return (
                  <span key={t} onClick={() => toggleType(t)} title={hid ? '点击显示' : '点击隐藏'}
                    style={{ cursor: 'pointer', fontSize: 11.5, padding: '3px 9px', borderRadius: 12, userSelect: 'none', border: `1px solid ${hid ? '#e6e6e6' : c[1]}`, background: hid ? '#f4f4f5' : c[0], color: hid ? '#b4b4ba' : c[2], textDecoration: hid ? 'line-through' : 'none' }}>{label}</span>
                );
              })}
            </div>
            <label style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 7, fontSize: 12, color: '#52525b', cursor: 'pointer', userSelect: 'none' }}>
              <input type="checkbox" checked={hideOrphans} onChange={(e) => setHideOrphans(e.target.checked)} style={{ cursor: 'pointer' }} /> 隐藏孤儿节点
            </label>
          </div>
        </div>

        <div style={{ position: 'absolute', bottom: 16, left: 14, fontSize: 11, color: '#c4c4cc', pointerEvents: 'none' }}>滚轮缩放 · 拖拽平移 · 单击节点看关系</div>
        <div style={{ position: 'absolute', top: 12, right: 14, display: 'flex', flexDirection: 'column', gap: 6 }}>
          <div style={btn} onClick={() => zoom(1.25)}>＋</div>
          <div style={btn} onClick={() => zoom(1 / 1.25)}>－</div>
          <div style={{ ...btn, fontSize: 13 }} onClick={fit} title="适应画布">⤢</div>
        </div>

        <div ref={tipRef} style={{ position: 'absolute', width: 282, background: '#fff', border: '1px solid #ececec', borderRadius: 12, padding: '12px 14px', boxShadow: '0 6px 24px rgba(0,0,0,.12)', pointerEvents: 'none', display: tip ? 'block' : 'none', zIndex: 5 }}>
          {tip && (
            <>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
                <span style={{ fontSize: 10.5, fontWeight: 600, color: '#fff', background: COL[tip.type][1], padding: '2px 8px', borderRadius: 5 }}>{TYPE_CN[tip.type]}</span>
                <span style={{ fontSize: 14.5, fontWeight: 700 }}>{tip.title}</span>
              </div>
              {tip.lines.map((l: any, i: number) => (<div key={i} style={{ fontSize: 12, color: '#52525b', lineHeight: 1.65 }}>{l}</div>))}
            </>
          )}
        </div>
      </div>
    </div>
  );
}
