// =====================================================================
// Character relationship network — Task 2
// - When a play is selected: detailed network of that play
// - When no play: cross-play "constellation" — character archetype clusters
// =====================================================================

function NetworkView({ plays, focusPlay, selectedRole, selectedChar, onSelectChar, themeFilter, hoverChar, onHoverChar, layoutKind = "force" }) {
  const [wrap, size] = useSize();
  const W = size.w || 720, H = size.h || 440;

  // build nodes/links
  const { nodes, links, clusters } = useMemo(() => {
    if (focusPlay) {
      const p = plays.find(x => x.id === focusPlay);
      if (!p) return { nodes: [], links: [], clusters: [] };
      const ns = p.characters.map(c => ({
        id: c.id, key: p.id + ":" + c.id, name: c.name, role: c.role, sub: c.sub,
        themes: c.themes || [], traits: c.traits || [], identity: c.identity, playId: p.id, playTitle: p.title,
        weight: 1 + (c.themes||[]).length, gender: c.gender, age: c.age, presence: c.presence,
      }));
      const ls = p.edges.map(([a,b,w,k]) => ({ source: a, target: b, w, k }));
      return { nodes: ns, links: ls, clusters: [] };
    } else {
      // multi-play "constellation": each play is a small cluster on a 3x3 grid
      const ns = [];
      const cl = [];
      plays.forEach((p, pi) => {
        // sort chars by weight, take top 4
        const top = [...p.characters].sort((a,b) => (b.themes?.length||0) - (a.themes?.length||0)).slice(0, 4);
        cl.push({ playId: p.id, playTitle: p.title, genre: p.genre, era: p.era, count: top.length });
        top.forEach((c, i) => ns.push({
          id: p.id + ":" + c.id, key: p.id + ":" + c.id, name: c.name, role: c.role, sub: c.sub,
          themes: c.themes || [], traits: c.traits, identity: c.identity, playId: p.id, playTitle: p.title,
          weight: 1 + (c.themes||[]).length, gender: c.gender, age: c.age, presence: c.presence,
          cluster: pi, ordInCluster: i,
        }));
      });
      const ls = [];
      // intra-cluster: own edges (limited to top4)
      plays.forEach(p => p.edges.forEach(([a,b,w,k]) => {
        const top4 = [...p.characters].sort((x,y) => (y.themes?.length||0) - (x.themes?.length||0)).slice(0, 4).map(c => c.id);
        if (top4.includes(a) && top4.includes(b))
          ls.push({ source: p.id + ":" + a, target: p.id + ":" + b, w, k, intra: true });
      }));
      // inter-cluster: shared theme of >= 2
      for (let i = 0; i < ns.length; i++) {
        for (let j = i+1; j < ns.length; j++) {
          if (ns[i].playId === ns[j].playId) continue;
          const shared = ns[i].themes.filter(t => ns[j].themes.includes(t));
          if (shared.length >= 2 && ns[i].role === ns[j].role) {
            ls.push({ source: ns[i].key, target: ns[j].key, w: 0.2 + shared.length * 0.12, k: "theme", intra: false, shared });
          }
        }
      }
      return { nodes: ns, links: ls, clusters: cl };
    }
  }, [plays, focusPlay]);

  // simulation
  const simRef = useRef(null);
  const [tick, setTick] = useState(0);
  const positionsRef = useRef({});
  const [ready, setReady] = useState(false);

  useEffect(() => {
    if (!nodes.length || !W) return;
    const rng = mulberry32(focusPlay ? focusPlay.charCodeAt(0) * 17 + nodes.length : 1234 + nodes.length);
    const pos = {};

    // cluster anchors (only when no focusPlay)
    const anchors = {};
    if (!focusPlay && clusters.length) {
      const cols = Math.min(5, Math.ceil(Math.sqrt(clusters.length * (W / H))));
      const rows = Math.ceil(clusters.length / cols);
      const cellW = W / cols;
      const cellH = (H - 40) / rows;
      clusters.forEach((cl, i) => {
        const cx = (i % cols) * cellW + cellW / 2;
        const cy = Math.floor(i / cols) * cellH + cellH / 2 + 20;
        anchors[cl.playId] = { x: cx, y: cy, r: Math.min(cellW, cellH) * 0.32 };
      });
    }

    nodes.forEach((n, i) => {
      if (!focusPlay && anchors[n.playId]) {
        const A = anchors[n.playId];
        const a = (n.ordInCluster / 4) * Math.PI * 2;
        pos[n.id] = {
          x: A.x + Math.cos(a) * A.r * 0.5 + (rng() - 0.5) * 6,
          y: A.y + Math.sin(a) * A.r * 0.5 + (rng() - 0.5) * 6,
          vx: 0, vy: 0,
        };
      } else {
        const a = (i / nodes.length) * Math.PI * 2;
        const r = Math.min(W, H) * 0.30;
        pos[n.id] = {
          x: W/2 + Math.cos(a) * r + (rng() - 0.5) * 30,
          y: H/2 + Math.sin(a) * r + (rng() - 0.5) * 30,
          vx: 0, vy: 0,
        };
      }
    });
    positionsRef.current = pos;
    setReady(true);

    // run a few hundred sim steps in a RAF loop
    let raf;
    let iter = 0;
    const maxIter = focusPlay ? 350 : 220;
    function step() {
      const alpha = Math.max(0.02, 0.6 * (1 - iter / maxIter));
      const ids = nodes.map(n => n.id);
      const nodeById = {};
      nodes.forEach(n => { nodeById[n.id] = n; });

      // repulsion — only within same cluster when multi-play
      for (let i = 0; i < ids.length; i++) {
        for (let j = i+1; j < ids.length; j++) {
          const a = pos[ids[i]], b = pos[ids[j]];
          const sameCluster = focusPlay || (nodeById[ids[i]].playId === nodeById[ids[j]].playId);
          let dx = b.x - a.x, dy = b.y - a.y;
          let d2 = dx*dx + dy*dy + 0.1;
          const d = Math.sqrt(d2);
          // mild global repulsion + strong intra-cluster repulsion
          const rep = (sameCluster ? 1500 : 60) / d2;
          const fx = (dx / d) * rep;
          const fy = (dy / d) * rep;
          a.vx -= fx * alpha; a.vy -= fy * alpha;
          b.vx += fx * alpha; b.vy += fy * alpha;
        }
      }
      // links spring
      links.forEach(L => {
        const a = pos[L.source], b = pos[L.target];
        if (!a || !b) return;
        let dx = b.x - a.x, dy = b.y - a.y;
        let d = Math.sqrt(dx*dx + dy*dy) + 0.1;
        const desired = L.intra === false ? 240 : (focusPlay ? 110 : 60);
        const k = L.intra === false ? 0.005 : 0.06;
        const f = (d - desired) * k * L.w;
        const fx = (dx / d) * f, fy = (dy / d) * f;
        a.vx += fx * alpha; a.vy += fy * alpha;
        b.vx -= fx * alpha; b.vy -= fy * alpha;
      });
      // anchor pull (multi-play only)
      if (!focusPlay) {
        nodes.forEach(n => {
          const A = anchors[n.playId];
          if (!A) return;
          const p = pos[n.id];
          p.vx += (A.x - p.x) * 0.06 * alpha;
          p.vy += (A.y - p.y) * 0.06 * alpha;
        });
      } else {
        // centering
        ids.forEach(id => {
          const n = pos[id];
          n.vx += (W/2 - n.x) * 0.005 * alpha;
          n.vy += (H/2 - n.y) * 0.005 * alpha;
        });
      }
      // damping & integrate
      ids.forEach(id => {
        const n = pos[id];
        n.vx *= 0.78; n.vy *= 0.78;
        n.x += n.vx; n.y += n.vy;
        n.x = Math.max(20, Math.min(W-20, n.x));
        n.y = Math.max(34, Math.min(H-20, n.y));
      });
      iter++;
      setTick(t => t+1);
      if (iter < maxIter) raf = requestAnimationFrame(step);
    }
    raf = requestAnimationFrame(step);
    return () => { if (raf) cancelAnimationFrame(raf); };
  }, [nodes.length, focusPlay, W, H, clusters.length]);

  const pos = positionsRef.current;

  function isDimmed(n) {
    if (selectedRole) {
      if (selectedRole.includes(":")) {
        const [r, s] = selectedRole.split(":");
        if (n.role !== r || n.sub !== s) return true;
      } else {
        if (n.role !== selectedRole) return true;
      }
    }
    if (themeFilter && !(n.themes||[]).includes(themeFilter)) return true;
    return false;
  }

  return (
    <div className="panel">
      <div className="panel-head">
        <div className="panel-title">
          <span className="num">03</span>人 物 关 系 网 络
        </div>
        <div className="panel-meta">
          {focusPlay ? `FOCUSED · ${nodes.length} CHARS · ${links.length} TIES` : `CROSS-PLAY · ${nodes.length} ARCHETYPES`}
        </div>
      </div>
      <div className="panel-body" ref={wrap}>
        <svg width="100%" height="100%" className="no-select">
          {/* edge color legend (top-right corner) */}
          <g transform="translate(14, 18)">
            <text className="mono label-dim" fontSize="9" style={{ letterSpacing:"0.2em" }}>关系</text>
            {[
              ["family","#d4a24c","亲族"],
              ["ally","#b8c8d8","盟友"],
              ["foe","#c8341c","敌对"],
              ["rom","#f0e6d6","婚恋"],
              ["master","#5e8a6a","主仆"],
            ].map(([k, c, lbl], i) => (
              <g key={k} transform={`translate(${36 + i*60}, -8)`}>
                <line x1="0" y1="6" x2="14" y2="6" stroke={c} strokeWidth="1.5" />
                <text x="18" y="9" fontSize="10" fill="var(--paper)">{lbl}</text>
              </g>
            ))}
            {!focusPlay && (
              <text x={345} y="3" fontSize="10" fill="var(--gold)" fontFamily="var(--f-display)" style={{ letterSpacing:"0.1em" }}>
                ※ 灰虚线 = 跨剧目共享主题
              </text>
            )}
          </g>

          {/* cluster labels (multi-play) */}
          {!focusPlay && clusters.map((cl, i) => {
            // approximate centroid from positions
            const ids = nodes.filter(n => n.playId === cl.playId).map(n => n.id);
            const ps = ids.map(id => pos[id]).filter(Boolean);
            if (!ps.length) return null;
            const cx = ps.reduce((s,p)=>s+p.x,0)/ps.length;
            const cy = ps.reduce((s,p)=>s+p.y,0)/ps.length;
            return (
              <g key={cl.playId} transform={`translate(${cx},${cy})`}>
                <text fontSize="11" textAnchor="middle" fill={genreColor(cl.genre)}
                      fontFamily="var(--f-display)"
                      y="-2"
                      style={{ letterSpacing:"0.1em", fontWeight:700, opacity:0.32 }}>
                  《{cl.playTitle}》
                </text>
              </g>
            );
          })}

          {ready && (
            <>
              {/* links */}
              <g className="links" style={{ mixBlendMode:"screen" }}>
                {links.map((L, i) => {
                  const a = pos[L.source], b = pos[L.target];
                  if (!a || !b) return null;
                  const cls = linkClassFromKind(L.k);
                  const involve = selectedChar && (L.source === selectedChar || L.target === selectedChar);
                  const sourceN = nodes.find(n => n.id === L.source);
                  const targetN = nodes.find(n => n.id === L.target);
                  const dimByRole = (sourceN && isDimmed(sourceN)) || (targetN && isDimmed(targetN));
                  const op = involve ? 0.95 : (dimByRole ? 0.08 : (L.intra === false ? 0.22 : 0.55));
                  const sw = involve ? 2.2 + L.w * 1.6 : 0.6 + L.w * 1.4;
                  return (
                    <line key={i} className={"link " + cls}
                          x1={a.x} y1={a.y} x2={b.x} y2={b.y}
                          strokeOpacity={op} strokeWidth={sw}
                          strokeDasharray={L.intra === false ? "3 3" : null} />
                  );
                })}
              </g>
              {/* nodes */}
              <g className="nodes">
                {nodes.map(n => {
                  const p = pos[n.id];
                  if (!p) return null;
                  const dim = isDimmed(n);
                  const sel = selectedChar === n.id;
                  const hov = hoverChar && hoverChar.id === n.id;
                  const r = sel ? 22 : (focusPlay ? 14 + Math.min(8, n.weight) : 10 + Math.min(4, n.weight));
                  return (
                    <g key={n.id} className="node" transform={`translate(${p.x},${p.y})`}
                       onMouseEnter={() => onHoverChar({ ...n })}
                       onMouseLeave={() => onHoverChar(null)}
                       onClick={() => onSelectChar(sel ? null : n.id, n)}
                       style={{ opacity: dim ? 0.2 : 1 }}
                    >
                      {/* face-paint ring */}
                      <circle r={r+3} fill="none"
                              stroke={sel ? "var(--gold)" : "var(--ink-4)"}
                              strokeWidth={sel ? 2 : 1} />
                      <circle r={r} fill={roleColor(n.role)}
                              stroke={n.role === "jing" ? "var(--paper)" : "var(--ink)"}
                              strokeWidth="1.5" />
                      {/* 净 face: ink crossbar */}
                      {n.role === "jing" && (
                        <>
                          <path d={`M ${-r*0.6} ${-r*0.15} L ${r*0.6} ${-r*0.15}`} stroke="var(--paper)" strokeWidth="1.2" />
                          <circle r={r*0.18} fill="var(--vermillion)" cx={-r*0.35} cy={-r*0.25} />
                          <circle r={r*0.18} fill="var(--vermillion)" cx={r*0.35} cy={-r*0.25} />
                        </>
                      )}
                      {/* 丑 face: white nose patch */}
                      {n.role === "chou" && (
                        <ellipse rx={r*0.32} ry={r*0.22} fill="var(--paper)" cy="2" />
                      )}
                      {/* 旦 face: rouge spots */}
                      {n.role === "dan" && (
                        <>
                          <circle r={r*0.22} fill="#c8341c" opacity="0.55" cx={-r*0.40} cy="0" />
                          <circle r={r*0.22} fill="#c8341c" opacity="0.55" cx={r*0.40} cy="0" />
                        </>
                      )}
                      {/* 生 (老生 has beard mark) */}
                      {n.role === "sheng" && n.sub === "老生" && (
                        <path d={`M ${-r*0.35} ${r*0.5} Q 0 ${r*1.1} ${r*0.35} ${r*0.5}`} stroke="var(--ink)" strokeWidth="1.2" fill="none" />
                      )}
                      <text x="0" y={r + 13} textAnchor="middle" fontSize="11"
                            fill="var(--paper)" fontFamily="var(--f-display)"
                            style={{ letterSpacing:"0.05em", textShadow:"0 1px 0 #000" }}>
                        {n.name}
                      </text>
                      {(sel || hov) && (
                        <text x="0" y={r + 25} textAnchor="middle" fontSize="9"
                              fill="var(--gold)" className="mono"
                              style={{ letterSpacing:"0.15em" }}>
                          {n.sub}
                        </text>
                      )}
                    </g>
                  );
                })}
              </g>
            </>
          )}

          {/* placeholder when empty */}
          {!nodes.length && (
            <text x={W/2} y={H/2} fontSize="13" textAnchor="middle" fill="var(--paper-dim)" fontFamily="var(--f-serif)">
              ※ 无匹配剧目 ※
            </text>
          )}
        </svg>
      </div>
    </div>
  );
}

window.NetworkView = NetworkView;
