/* Network map — interactive tailnet topology (showpiece) */
function NetworkMap() {
  const s = useStore();
  const [sel, setSel] = useState(null);          // selected machine id
  const [hover, setHover] = useState(null);
  const [showOffline, setShowOffline] = useState(true);
  const [mode, setMode] = useState("radial");    // radial | clusters
  const [animate, setAnimate] = useState(true);

  const W = 1000, H = 680, cx = W / 2, cy = H / 2;

  // deterministic jitter
  const rng = (seed) => { let x = Math.sin(seed * 99991) * 10000; return x - Math.floor(x); };

  const nodes = useMemo(() => {
    const list = s.machines.filter((m) => showOffline || m.online);
    const userIndex = new Map(s.users.map((u, i) => [String(u.id), i]));
    const role = (m) => s.isExitNode(m) ? 0 : s.subnetPrefixes(m).length ? 1 : 2; // infra inner, clients outer
    const byGroup = (a, b) => role(a) - role(b) || (userIndex.get(String(a.user)) ?? 99) - (userIndex.get(String(b.user)) ?? 99) || String(a.name).localeCompare(String(b.name));
    const uFor = (m) => s.userById(m.user) || { id: m.user, name: (m.tags && m.tags[0]) || "tagged", displayName: "tagged", color: "var(--text-faint)" };
    const out = [];

    if (mode === "radial") {
      const infra = list.filter((m) => role(m) < 2).sort(byGroup);
      const clients = list.filter((m) => role(m) === 2).sort(byGroup);

      // pack an array onto concentric ellipse rings, filling inward-out, evenly by angle
      const pack = (arr, r0, gap, minSpacing) => {
        let i = 0, ring = 0, maxR = r0;
        while (i < arr.length) {
          const R = r0 + ring * gap;
          const cap = Math.max(6, Math.floor((2 * Math.PI * R * 0.92) / minSpacing));
          const remaining = arr.length - i;
          // balance the last two rings so the outer ring isn't nearly empty
          let count = Math.min(cap, remaining);
          if (remaining > cap && remaining < cap * 2) count = Math.ceil(remaining / 2);
          const off = (ring % 2 ? Math.PI / count : 0) - Math.PI / 2;
          for (let k = 0; k < count; k++) {
            const ang = off + (k / count) * Math.PI * 2;
            const m = arr[i++];
            out.push({ m, u: uFor(m), x: cx + Math.cos(ang) * R, y: cy + Math.sin(ang) * R * 0.84, isExit: s.isExitNode(m) });
          }
          maxR = R; ring++;
        }
        return maxR;
      };

      const infraOuter = infra.length ? pack(infra, 118, 64, 50) : 96;
      pack(clients, Math.max(infraOuter + 78, 210), 74, 46);
    } else {
      const users = s.users.filter((u) => list.some((m) => String(m.user) === String(u.id)));
      const ringR = users.length <= 1 ? 0 : Math.min(250, 150 + users.length * 14);
      const GOLDEN = Math.PI * (3 - Math.sqrt(5));
      users.forEach((u, ui) => {
        const um = list.filter((m) => String(m.user) === String(u.id)).sort(byGroup);
        const base = (ui / Math.max(1, users.length)) * Math.PI * 2 - Math.PI / 2;
        const ccx = cx + Math.cos(base) * ringR, ccy = cy + Math.sin(base) * ringR * 0.82;
        const spacing = um.length > 12 ? 17 : 22;
        um.forEach((m, k) => {
          const a = k * GOLDEN;
          const r = um.length === 1 ? 0 : spacing * Math.sqrt(k + 0.5);
          out.push({ m, u, x: ccx + Math.cos(a) * r, y: ccy + Math.sin(a) * r, isExit: s.isExitNode(m), ccx, ccy, cluster: u.id });
        });
      });
    }
    return out;
  }, [s.machines, s.users, s.rev, showOffline, mode]);

  const nodeById = (id) => nodes.find((n) => n.m.id === id);
  const exitNodes = nodes.filter((n) => n.isExit && s.exitApproved(n.m));
  const many = nodes.length > 18;            // declutter threshold
  const nodeR = nodes.length > 30 ? 10 : nodes.length > 20 ? 12 : 13;

  // links: hub spokes + egress to exit nodes
  const links = useMemo(() => {
    const L = [];
    nodes.forEach((n) => L.push({ a: "hub", b: n.m.id, x1: cx, y1: cy, x2: n.x, y2: n.y, type: "spoke", online: n.m.online }));
    // egress links: each online non-exit node routes to nearest enabled exit
    nodes.filter((n) => n.m.online && !n.isExit).forEach((n) => {
      if (!exitNodes.length) return;
      let best = exitNodes[0], bd = Infinity;
      exitNodes.forEach((e) => { const d = (e.x - n.x) ** 2 + (e.y - n.y) ** 2; if (d < bd) { bd = d; best = e; } });
      if (rng(n.m.id * 3) > 0.45) L.push({ a: n.m.id, b: best.m.id, x1: n.x, y1: n.y, x2: best.x, y2: best.y, type: "egress" });
    });
    return L;
  }, [nodes]);

  const focusId = hover || sel;
  const focusNode = focusId ? nodeById(focusId) : null;
  const connected = useMemo(() => {
    if (!focusId) return null;
    const set = new Set(["hub", focusId]);
    links.forEach((l) => { if (l.a === focusId) set.add(l.b); if (l.b === focusId) set.add(l.a); });
    return set;
  }, [focusId, links]);

  const isDim = (id) => connected && !connected.has(id);
  const selM = sel ? s.machines.find((m) => m.id === sel) : null;
  const spokeBase = nodes.length > 30 ? 0.28 : nodes.length > 18 ? 0.4 : 0.5;

  return (
    <div className="page wide fade-up" style={{ paddingBottom: 24 }}>
      <div className="section-head">
        <div>
          <h2 style={{ fontSize: 19 }}>Network map</h2>
          <div className="sub">{nodes.length} nodes · {exitNodes.length} active exit node{exitNodes.length !== 1 ? "s" : ""} · live topology</div>
        </div>
        <div className="spacer" />
        <RefreshBtn />
        <div className="seg">
          <button className={mode === "radial" ? "on" : ""} onClick={() => setMode("radial")}>Radial</button>
          <button className={mode === "clusters" ? "on" : ""} onClick={() => setMode("clusters")}>By user</button>
        </div>
        <button className={"btn sm" + (showOffline ? " " : "")} onClick={() => setShowOffline((v) => !v)}><Ic name={showOffline ? "eye" : "eyeOff"} size={14} />Offline</button>
        <button className="btn sm" onClick={() => setAnimate((v) => !v)}><Ic name={animate ? "activity" : "play"} size={14} />{animate ? "Live" : "Paused"}</button>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: selM ? "1fr 300px" : "1fr", gap: 14, alignItems: "start" }}>
        <div className="card" style={{ overflow: "hidden", position: "relative", background: "var(--map-bg)", height: "min(68vh, 640px)" }}>
          <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet" style={{ width: "100%", height: "100%", display: "block" }} onClick={() => setSel(null)}>
            <defs>
              <radialGradient id="hubGlow" cx="50%" cy="50%" r="50%">
                <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.28" />
                <stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
              </radialGradient>
              <filter id="soft"><feGaussianBlur stdDeviation="2.4" /></filter>
            </defs>

            {/* concentric guide rings */}
            {[130, 215, 300].map((r) => (
              <ellipse key={r} cx={cx} cy={cy} rx={r} ry={r * 0.84} fill="none" stroke="var(--border)" strokeOpacity="0.5" strokeDasharray="2 6" />
            ))}

            {/* links */}
            <g>
              {links.map((l, i) => {
                const dim = connected && !(connected.has(l.a) && connected.has(l.b));
                if (l.type === "spoke") {
                  return <line key={i} x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2}
                    stroke="var(--map-link)" strokeWidth={1} strokeOpacity={dim ? 0.08 : (l.online ? spokeBase : spokeBase * 0.4)} />;
                }
                return <line key={i} x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2}
                  stroke="var(--accent)" strokeWidth={1.4} strokeOpacity={dim ? 0.1 : 0.5} strokeDasharray="4 5" />;
              })}
            </g>

            {/* animated packets on egress links */}
            {animate && links.filter((l) => l.type === "egress").slice(0, 8).map((l, i) => (
              <circle key={"p" + i} r="2.6" fill="var(--accent)" opacity={connected && !(connected.has(l.a) && connected.has(l.b)) ? 0.12 : 0.9}>
                <animateMotion dur={`${1.6 + rng(i) * 1.4}s`} repeatCount="indefinite" path={`M ${l.x1} ${l.y1} L ${l.x2} ${l.y2}`} />
              </circle>
            ))}

            {/* hub */}
            <g style={{ cursor: "default" }} onMouseEnter={() => setHover("hub")} onMouseLeave={() => setHover(null)}>
              <circle cx={cx} cy={cy} r="60" fill="url(#hubGlow)" />
              <circle cx={cx} cy={cy} r="27" fill="var(--surface)" stroke="var(--accent)" strokeWidth="2" />
              <g transform={`translate(${cx - 11}, ${cy - 11})`} style={{ color: "var(--accent)" }} dangerouslySetInnerHTML={{ __html: window.Icon("shield", 22) }} />
              <text x={cx} y={cy + 46} textAnchor="middle" fontSize="12.5" fontWeight="600" fill="var(--text)" fontFamily="var(--font-mono)">headscale</text>
              <text x={cx} y={cy + 61} textAnchor="middle" fontSize="10.5" fill="var(--text-faint)">control plane</text>
            </g>

            {/* nodes */}
            {nodes.map((n) => {
              const dim = isDim(n.m.id);
              const active = focusId === n.m.id;
              const r = n.isExit ? nodeR + 3 : nodeR;
              const col = n.m.online ? (n.isExit ? "var(--info)" : "var(--online)") : "var(--offline)";
              const pulse = animate && n.m.online && (!many || n.isExit || active);
              const showLabel = active || (!many && !connected);
              return (
                <g key={n.m.id} transform={`translate(${n.x}, ${n.y})`} style={{ cursor: "pointer", transition: "opacity .2s" }} opacity={dim ? 0.25 : 1}
                  onMouseEnter={() => setHover(n.m.id)} onMouseLeave={() => setHover(null)}
                  onClick={(e) => { e.stopPropagation(); setSel(n.m.id); }}>
                  {pulse && <circle r={r + 6} fill={col} opacity="0.14"><animate attributeName="r" values={`${r + 3};${r + 9};${r + 3}`} dur="2.8s" repeatCount="indefinite" /><animate attributeName="opacity" values="0.18;0.04;0.18" dur="2.8s" repeatCount="indefinite" /></circle>}
                  <circle r={r} fill="var(--surface)" stroke={col} strokeWidth={active ? 2.6 : 1.8} />
                  <g transform="translate(-8,-8)" style={{ color: col }} dangerouslySetInnerHTML={{ __html: window.Icon(osIcon(n.m.kind, n.m.os), 16) }} />
                  {n.isExit && <g transform={`translate(${r - 4}, ${-r - 4})`}><circle r="7" fill="var(--info)" /><g transform="translate(-5,-5)" style={{ color: "#fff" }} dangerouslySetInnerHTML={{ __html: window.Icon("exit", 10) }} /></g>}
                  {showLabel && (
                    <g>
                      <text y={r + 15} textAnchor="middle" fontSize="11" fontWeight={active ? 600 : 500}
                        fill={active ? "var(--text)" : "var(--text-dim)"} fontFamily="var(--font-mono)"
                        style={active ? { paintOrder: "stroke", stroke: "var(--map-bg)", strokeWidth: 3, strokeLinejoin: "round" } : undefined}>{n.m.name}</text>
                    </g>
                  )}
                </g>
              );
            })}
          </svg>

          {/* legend */}
          <div style={{ position: "absolute", left: 14, bottom: 14, display: "flex", gap: 14, padding: "8px 13px", background: "color-mix(in oklab, var(--surface) 80%, transparent)", backdropFilter: "blur(8px)", border: "1px solid var(--border)", borderRadius: 8, fontSize: 11.5 }}>
            <LegItem color="var(--online)" label="Online" />
            <LegItem color="var(--info)" label="Exit node" />
            <LegItem color="var(--offline)" label="Offline" />
          </div>
          {many && !focusId && (
            <div style={{ position: "absolute", left: "50%", transform: "translateX(-50%)", bottom: 14, fontSize: 11, color: "var(--text-faint)", background: "color-mix(in oklab, var(--surface) 80%, transparent)", backdropFilter: "blur(8px)", padding: "5px 11px", borderRadius: 99, border: "1px solid var(--border)" }}>Hover a node for its name · click to inspect</div>
          )}
          {/* hover tooltip */}
          {hover && hover !== "hub" && nodeById(hover) && (
            <div style={{ position: "absolute", right: 14, top: 14, padding: "9px 12px", background: "var(--surface)", border: "1px solid var(--border-strong)", borderRadius: 8, boxShadow: "var(--shadow-md)", pointerEvents: "none", minWidth: 150 }}>
              <div style={{ fontWeight: 600, fontSize: 13, display: "flex", alignItems: "center", gap: 6 }}><span className={"dot-status " + (nodeById(hover).m.online ? "online" : "offline")} />{nodeById(hover).m.name}</div>
              <div className="ip" style={{ fontSize: 11.5, marginTop: 2 }}>{nodeById(hover).m.ip4}</div>
              <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginTop: 2 }}>{nodeById(hover).u.name} · {osLabel(nodeById(hover).m.os)}</div>
            </div>
          )}
        </div>

        {selM && <MapDetail m={selM} close={() => setSel(null)} />}
      </div>
    </div>
  );
}

function LegItem({ color, label }) {
  return <span style={{ display: "inline-flex", alignItems: "center", gap: 6, color: "var(--text-dim)" }}><span style={{ width: 9, height: 9, borderRadius: 99, background: color }} />{label}</span>;
}

function MapDetail({ m, close }) {
  const s = useStore();
  const u = s.userById(m.user);
  return (
    <div className="card card-pad fade-up" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      <div style={{ display: "flex", alignItems: "flex-start", gap: 11 }}>
        <span style={{ width: 38, height: 38, borderRadius: 10, background: "var(--surface-2)", display: "grid", placeItems: "center", color: "var(--text-dim)" }}><Ic name={osIcon(m.kind, m.os)} size={20} /></span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 600, fontSize: 14.5 }}>{m.name}</div>
          <div style={{ fontSize: 12, color: "var(--text-faint)" }}>{m.online ? "Connected" : "Last seen " + relTime(m.lastSeen)}</div>
        </div>
        <span className="x" style={{ cursor: "pointer", color: "var(--text-faint)" }} onClick={close}><Ic name="x" size={16} /></span>
      </div>
      <div style={{ display: "flex", flexDirection: "column", gap: 9, fontSize: 13 }}>
        <DRow label="Owner">{u && <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}><Avatar user={u} size={20} />{u.name}</span>}</DRow>
        <DRow label="IPv4"><span className="ip">{m.ip4}</span></DRow>
        <DRow label="OS">{osLabel(m.os)}</DRow>
        <DRow label="Role">{s.isExitNode(m) ? <span className="badge info">exit node</span> : s.subnetPrefixes(m).length ? <span className="badge accent">subnet router</span> : <span className="badge">client</span>}</DRow>
      </div>
      {m.tags.length > 0 && <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>{m.tags.map((t) => <span key={t} className="tag">{t}</span>)}</div>}
    </div>
  );
}

window.NetworkMap = NetworkMap;
