// screens-admin.jsx — Users, Tokens, Containers, Cluster (graph), Settings

function _normalizeNode(n, x, y) {
  return {
    id: n.id || n.hostname || n.name || 'node',
    role: n.role || 'worker',
    status: n.status || (n.online ? 'online' : 'offline'),
    ip: n.ip || n.ip_address || '—',
    version: n.version || '—',
    uptime: n.uptime || '—',
    cpu: n.cpu || n.cpu_percent || 0,
    mem: n.mem || n.memory_used || n.memory_used_gb || 0,
    memTotal: n.memTotal || n.mem_total || n.memory_total || n.memory_total_gb || 0,
    gpus: n.gpus || (n.gpu_count ? Array(n.gpu_count).fill('GPU') : []),
    devices: n.devices || n.capture_devices || [],
    x, y,
  };
}

function InviteUserModal({ onCreated, onClose }) {
  const [form, setForm] = React.useState({ username: '', display_name: '', password: '', role: 'viewer' });
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState(null);

  const submit = () => {
    if (!form.username || !form.password) { setErr('Username and password are required'); return; }
    setSaving(true); setErr(null);
    window.ZAMPP_API.fetch('/users', { method: 'POST', body: JSON.stringify(form) })
      .then(user => { onCreated(user); onClose(); })
      .catch(e => { setSaving(false); setErr(e.message || 'Failed to create user'); });
  };

  const onKey = e => { if (e.key === 'Enter') submit(); };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ width: 420 }} onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <div style={{ fontSize: 15, fontWeight: 600 }}>Invite user</div>
          <button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
        </div>
        <div className="modal-body">
          <div className="field">
            <label className="field-label">Username</label>
            <input className="field-input" value={form.username} autoFocus
              onChange={e => setForm(p => ({...p, username: e.target.value}))}
              onKeyDown={onKey} placeholder="jsmith" />
          </div>
          <div className="field">
            <label className="field-label">Display name</label>
            <input className="field-input" value={form.display_name}
              onChange={e => setForm(p => ({...p, display_name: e.target.value}))}
              onKeyDown={onKey} placeholder="John Smith" />
          </div>
          <div className="field">
            <label className="field-label">Password</label>
            <input className="field-input" type="password" value={form.password}
              onChange={e => setForm(p => ({...p, password: e.target.value}))}
              onKeyDown={onKey} placeholder="Temporary password" />
          </div>
          <div className="field">
            <label className="field-label">Role</label>
            <select className="field-input" value={form.role}
              onChange={e => setForm(p => ({...p, role: e.target.value}))}
              style={{ appearance: 'auto' }}>
              <option value="viewer">Viewer</option>
              <option value="editor">Editor</option>
              <option value="admin">Admin</option>
            </select>
          </div>
          {err && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>{err}</div>}
        </div>
        <div className="modal-foot">
          <button className="btn ghost sm" onClick={onClose}>Cancel</button>
          <button className="btn primary sm" onClick={submit} disabled={saving}>{saving ? 'Creating…' : 'Create user'}</button>
        </div>
      </div>
    </div>
  );
}

function Users() {
  const [users, setUsers] = React.useState(window.ZAMPP_DATA.USERS || []);
  const [tab, setTab] = React.useState("users");
  const [showInvite, setShowInvite] = React.useState(false);

  const exportCsv = () => {
    const rows = [['Username', 'Name', 'Role', 'Last Seen']].concat(
      users.map(u => [u.username || '', u.name || '', u.role || '', u.lastSeen || ''])
    );
    const csv = rows.map(r => r.map(c => '"' + String(c).replace(/"/g, '""') + '"').join(',')).join('\n');
    const a = document.createElement('a');
    a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
    a.download = 'users.csv';
    a.click();
  };

  const onCreated = (user) => {
    const updated = [...users, user];
    setUsers(updated);
    window.ZAMPP_DATA.USERS = updated;
  };

  return (
    <div className="page">
      <div className="page-header">
        <h1>Users &amp; Groups</h1>
        <div className="spacer" />
        <button className="btn ghost sm" onClick={exportCsv}><Icon name="download" />Export</button>
        <button className="btn primary" onClick={() => setShowInvite(true)}><Icon name="plus" />Invite user</button>
      </div>
      <div className="page-body">
        <div className="tab-group" style={{ width: "fit-content", marginBottom: 12 }}>
          <button className={tab === "users" ? "active" : ""} onClick={() => setTab("users")}>Users · {users.length}</button>
          <button className={tab === "groups" ? "active" : ""} onClick={() => setTab("groups")}>Groups</button>
          <button className={tab === "policies" ? "active" : ""} onClick={() => setTab("policies")}>Policies</button>
        </div>
        <div className="panel">
          <div className="user-row head">
            <div>User</div>
            <div>Role</div>
            <div>Groups</div>
            <div>Last active</div>
            <div></div>
          </div>
          {users.length === 0 && (
            <div style={{ padding: "32px 0", textAlign: "center", color: "var(--text-3)" }}>No users found</div>
          )}
          {users.map(u => (
            <div key={u.id} className="user-row">
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <div className="avatar" style={{ width: 32, height: 32, fontSize: 11, background: avatarColor(u.initials || u.id) }}>{u.initials || '??'}</div>
                <div>
                  <div style={{ fontWeight: 500, fontSize: 13 }}>{u.name}</div>
                  <div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>@{u.username}</div>
                </div>
              </div>
              <div><span className={`badge ${u.role === "admin" ? "purple" : u.role === "service" ? "neutral" : "accent"}`}>{u.role}</span></div>
              <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
                {(u.groups || []).map(g => <span key={g} className="badge outline" style={{ textTransform: "lowercase" }}>{g}</span>)}
              </div>
              <div className="mono" style={{ fontSize: 11.5, color: "var(--text-3)" }}>{u.lastSeen}</div>
              <div><button className="icon-btn"><Icon name="more" /></button></div>
            </div>
          ))}
        </div>
      </div>
      {showInvite && <InviteUserModal onCreated={onCreated} onClose={() => setShowInvite(false)} />}
    </div>
  );
}

function Tokens() {
  const [burned, setBurned] = React.useState(14340);
  const [rate, setRate] = React.useState(2.4);
  const [showCalc, setShowCalc] = React.useState(false);

  React.useEffect(() => {
    const i = setInterval(() => {
      setBurned(b => b + Math.floor(Math.random() * 8) + 1);
      setRate(r => Math.max(0.8, Math.min(8, r + (Math.random() - 0.5) * 0.4)));
    }, 800);
    return () => clearInterval(i);
  }, []);

  const burnSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 100 + i * 8 + Math.sin(i * 0.7) * 12), []);
  const competitorSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 200 + i * 22 + Math.cos(i * 0.5) * 30), []);
  const yourCostSpark = React.useMemo(() => Array.from({ length: 40 }, () => 1), []);

  const [events, setEvents] = React.useState([
    { t: "21:14:02", action: "preview thumbnail generated", cost: 4 },
    { t: "21:14:01", action: "user clicked play", cost: 12 },
    { t: "21:13:58", action: "API health check", cost: 8 },
    { t: "21:13:54", action: "asset metadata read", cost: 2 },
    { t: "21:13:51", action: "session token refreshed", cost: 18 },
    { t: "21:13:47", action: "scrubbed timeline 1 frame", cost: 6 },
    { t: "21:13:42", action: "took a deep breath near the API", cost: 24 },
  ]);

  React.useEffect(() => {
    const actions = [
      "preview thumbnail generated", "user clicked play", "API health check",
      "scrubbed timeline 1 frame", "asset metadata read", "session token refreshed",
      "checked job queue", "rendered a tooltip", "loaded sidebar icon",
      "blinked", "made eye contact with the cluster", "opened a modal (twice)",
      "asset list pagination request", "thought about a comment", "moved cursor near 'Save'",
    ];
    const i = setInterval(() => {
      const now = new Date();
      const t = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
      const a = actions[Math.floor(Math.random() * actions.length)];
      const c = Math.floor(Math.random() * 28) + 1;
      setEvents(ev => [{ t, action: a, cost: c }, ...ev].slice(0, 12));
    }, 1600);
    return () => clearInterval(i);
  }, []);

  const tiers = [
    { name: "Starter", desc: "For \"evaluation only\" — definitely not production", price: "$2,400", per: "/ month", tokens: "100k tokens", popular: false, color: "#6B7280" },
    { name: "Broadcast", desc: "Most teams. Most pain.", price: "$28,000", per: "/ month", tokens: "1.5M tokens", popular: true, color: "#5B7CFA" },
    { name: "Enterprise", desc: "If you have to ask, you can't afford it (but you'll ask anyway)", price: "$call us", per: "and bring lawyers", tokens: "∞ tokens", popular: false, color: "#B57CFA" },
  ];

  return (
    <div className="page">
      <div className="page-header">
        <h1>Tokens</h1>
        <span className="subtitle">Token-metered pricing parody · You actually pay <strong style={{ color: "var(--success)" }}>$0.00</strong></span>
        <div className="spacer" />
        <span className="badge warning"><Icon name="alert" size={10} /> SATIRE</span>
        <button className="btn ghost sm" onClick={() => setShowCalc(!showCalc)}><Icon name="sliders" />Cost calculator</button>
      </div>
      <div className="page-body">

        <div style={{ textAlign: 'center', padding: '8px 0 36px' }}>
          <h2 style={{ fontSize: 30, fontWeight: 700, letterSpacing: '-0.02em', lineHeight: 1.3, margin: 0 }}>
            <span style={{ textDecoration: 'line-through', color: 'var(--text-3)', fontWeight: 500 }}>Per-seat</span>
            {' · '}
            <span style={{ textDecoration: 'line-through', color: 'var(--text-3)', fontWeight: 500 }}>Per-stream</span>
            {' · '}
            <span style={{ textDecoration: 'line-through', color: 'var(--text-3)', fontWeight: 500 }}>Per-month</span>
            <br />
            <span style={{ fontSize: 52, fontWeight: 800, color: 'var(--accent-text)', letterSpacing: '-0.03em' }}>Per Token.</span>
          </h2>
        </div>

        <div className="token-hero">
          <div className="token-burn-card">
            <div className="token-card-label">TOKENS BURNED THIS SESSION</div>
            <div className="token-counter">
              <span className="token-flame">🔥</span>
              <span className="token-big mono">{burned.toLocaleString()}</span>
            </div>
            <div className="token-rate">
              <span className="mono" style={{ color: "var(--danger)" }}>↑ {rate.toFixed(1)}k/sec</span>
              <span style={{ color: "var(--text-3)", marginLeft: 10 }}>burning since you logged in</span>
            </div>
            <div style={{ marginTop: 12 }}>
              <Sparkline data={burnSpark} color="#FF5B5B" />
            </div>
          </div>

          <div className="token-actual-card">
            <div className="token-card-label">WHAT YOU ACTUALLY PAY</div>
            <div className="token-actual-amount">
              <span style={{ fontSize: 48, fontWeight: 700, letterSpacing: "-0.04em" }}>$0</span>
              <span style={{ fontSize: 18, color: "var(--text-3)" }}>.00</span>
            </div>
            <div style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.5 }}>
              Dragonflight is self-hosted. The tokens above are imaginary.<br />
              Imagine them as a stress test for your sanity.
            </div>
            <div style={{ marginTop: 12 }}>
              <Sparkline data={yourCostSpark} color="#2DD4A8" fill={false} />
            </div>
          </div>
        </div>

        <div className="token-comparison">
          <div className="token-card-label" style={{ padding: "16px 16px 0" }}>HOURLY BURN — DRAGONFLIGHT vs. THE OTHER GUYS</div>
          <div className="token-compare-chart">
            <ChartLine
              series={[
                { label: "AMPP-style competitor", data: competitorSpark, color: "#FF5B5B" },
                { label: "Dragonflight (yours)", data: yourCostSpark.map((_, i) => i < 20 ? 1 : 1), color: "#2DD4A8" },
              ]}
            />
            <div className="token-compare-legend">
              <div><span className="dot" style={{ background: "#FF5B5B" }} />Competitor: $1,247/hr and rising</div>
              <div><span className="dot" style={{ background: "#2DD4A8" }} />Dragonflight: $0.00/hr forever</div>
            </div>
          </div>
        </div>

        <div className="token-grid">
          <div>
            <div className="token-card-label" style={{ marginBottom: 8 }}>LIVE BILLING EVENTS</div>
            <div className="panel">
              {events.map((e, i) => (
                <div key={i} className={`token-event ${i === 0 ? "fresh" : ""}`}>
                  <span className="mono" style={{ color: "var(--text-3)", fontSize: 11 }}>{e.t}</span>
                  <span style={{ flex: 1, fontSize: 12.5 }}>{e.action}</span>
                  <span className="mono" style={{ color: "var(--danger)", fontWeight: 600 }}>+{e.cost} tk</span>
                </div>
              ))}
            </div>
          </div>

          <div>
            <div className="token-card-label" style={{ marginBottom: 8 }}>PRICING TIERS WE DIDN'T COPY</div>
            <div className="token-tiers">
              {tiers.map(t => (
                <div key={t.name} className={`token-tier ${t.popular ? "popular" : ""}`}>
                  {t.popular && <span className="token-tier-badge">MOST PAIN</span>}
                  <div className="token-tier-name" style={{ color: t.color }}>{t.name}</div>
                  <div className="token-tier-desc">{t.desc}</div>
                  <div className="token-tier-price">
                    <span style={{ fontSize: 26, fontWeight: 700, letterSpacing: "-0.02em" }}>{t.price}</span>
                    <span style={{ fontSize: 12, color: "var(--text-3)", marginLeft: 4 }}>{t.per}</span>
                  </div>
                  <div className="token-tier-tokens mono">{t.tokens}</div>
                  <button className="btn subtle sm" disabled style={{ width: "100%", marginTop: 8 }}>Not for sale</button>
                </div>
              ))}
            </div>
          </div>
        </div>

        {showCalc && <CostCalculator onClose={() => setShowCalc(false)} />}

        <div className="token-footnote">
          <Icon name="alert" size={14} />
          <div>
            <strong>Disclaimer:</strong> No actual tokens were billed in the making of this page. Wild Dragon's broadcast platform
            is self-hosted infrastructure. Any resemblance to per-API-call broadcast token economies, living or dead, is satirical
            and protected as commentary. If you came here looking for actual API tokens, that page no longer exists — service
            credentials are managed through the cluster's own JWT issuer (see <span style={{ color: "var(--accent-text)" }}>Settings → AMPP Integration</span>).
          </div>
        </div>
      </div>
    </div>
  );
}

function ChartLine({ series }) {
  const w = 600, h = 140;
  return (
    <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ width: "100%", height: 140 }}>
      <defs>
        <pattern id="cgrid" width="60" height="28" patternUnits="userSpaceOnUse">
          <path d="M 60 0 L 0 0 0 28" fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth="1" />
        </pattern>
      </defs>
      <rect width={w} height={h} fill="url(#cgrid)" />
      {series.map((s, si) => {
        const max = Math.max(...series.flatMap(x => x.data), 1);
        const pts = s.data.map((d, i) => {
          const x = (i / (s.data.length - 1)) * w;
          const y = h - (d / max) * (h - 10) - 4;
          return `${x},${y}`;
        }).join(" ");
        const area = `0,${h} ${pts} ${w},${h}`;
        return (
          <g key={si}>
            <polygon points={area} fill={s.color} opacity="0.1" />
            <polyline points={pts} fill="none" stroke={s.color} strokeWidth="2" />
            <circle cx={w} cy={h - (s.data[s.data.length - 1] / max) * (h - 10) - 4} r="4" fill={s.color} />
            <circle cx={w} cy={h - (s.data[s.data.length - 1] / max) * (h - 10) - 4} r="8" fill={s.color} opacity="0.3">
              <animate attributeName="r" values="4;14;4" dur="2s" repeatCount="indefinite" />
              <animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite" />
            </circle>
          </g>
        );
      })}
    </svg>
  );
}

function CostCalculator({ onClose }) {
  const [users, setUsers] = React.useState(12);
  const [assets, setAssets] = React.useState(500);
  const [clicks, setClicks] = React.useState(2000);
  const cost = users * 240 + assets * 8 + clicks * 0.12;
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ width: 520 }} onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <div>
            <div style={{ fontSize: 15, fontWeight: 600 }}>Token Cost Calculator</div>
            <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>What it would cost on AMPP-style pricing</div>
          </div>
          <button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
        </div>
        <div className="modal-body">
          <CalcSlider label="Users" value={users} onChange={setUsers} min={1} max={100} unit=" people" />
          <CalcSlider label="Assets in library" value={assets} onChange={setAssets} min={50} max={10000} step={50} unit="" />
          <CalcSlider label="UI clicks per day" value={clicks} onChange={setClicks} min={100} max={20000} step={100} unit="" />
          <div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: 16, marginTop: 8 }}>
            <div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>You would be paying</div>
            <div style={{ fontSize: 36, fontWeight: 700, color: "var(--danger)", letterSpacing: "-0.02em", marginTop: 4 }}>
              ${cost.toLocaleString("en-US", { maximumFractionDigits: 0 })}<span style={{ fontSize: 14, color: "var(--text-3)", fontWeight: 400, marginLeft: 4 }}>/ month</span>
            </div>
            <div style={{ marginTop: 8, padding: 10, background: "var(--success-soft)", borderRadius: 6, fontSize: 12.5, color: "var(--success)" }}>
              <strong>Your actual Dragonflight cost:</strong> $0.00. You're welcome.
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function CalcSlider({ label, value, onChange, min, max, step = 1, unit }) {
  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6, fontSize: 12 }}>
        <span style={{ color: "var(--text-2)" }}>{label}</span>
        <span className="mono" style={{ color: "var(--text-1)", fontWeight: 600 }}>{value.toLocaleString()}{unit}</span>
      </div>
      <input
        type="range"
        min={min} max={max} step={step}
        value={value}
        onChange={e => onChange(Number(e.target.value))}
        style={{ width: "100%", accentColor: "var(--accent)" }}
      />
    </div>
  );
}

function Containers() {
  const [containers, setContainers] = React.useState(null);

  function load() {
    setContainers(null);
    window.ZAMPP_API.fetch('/cluster/containers')
      .then(data => setContainers(Array.isArray(data) ? data : (data.containers || [])))
      .catch(() => setContainers([]));
  }

  React.useEffect(() => { load(); }, []);

  const running = (containers || []).filter(c => c.state === 'running').length;

  const showLogs = (c) => {
    alert('To view logs for ' + c.name + ', run:\n\ndocker compose logs -f ' + c.name);
  };

  const restartContainer = (c) => {
    if (!window.confirm('Restart container ' + c.name + '?')) return;
    window.ZAMPP_API.fetch('/cluster/containers/' + encodeURIComponent(c.id || c.name) + '/restart', { method: 'POST' })
      .then(() => { alert(c.name + ' restarted.'); load(); })
      .catch(() => alert('No restart endpoint available.\nRun manually:\n\ndocker compose restart ' + c.name));
  };

  return (
    <div className="page">
      <div className="page-header">
        <h1>Containers</h1>
        <span className="subtitle">Docker Compose services across the cluster</span>
        <div className="spacer" />
        {containers !== null && containers.length > 0 && (
          <div className="status-pip">
            <span className="dot" />
            <span>{running} / {containers.length} running</span>
          </div>
        )}
        <button className="btn ghost sm" onClick={load}><Icon name="refresh" />Refresh</button>
      </div>
      <div className="page-body">
        {containers === null && (
          <div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>Loading…</div>
        )}
        {containers !== null && containers.length === 0 && (
          <div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>
            <div style={{ fontSize: 32, marginBottom: 12 }}>🐳</div>
            <div style={{ fontWeight: 500, fontSize: 14 }}>No container data available</div>
            <div style={{ fontSize: 12, marginTop: 6 }}>Container metrics endpoint not yet wired</div>
          </div>
        )}
        {containers !== null && containers.length > 0 && (
          <div className="panel">
            <div className="container-row head">
              <div>Container</div>
              <div>Image</div>
              <div>State</div>
              <div>CPU</div>
              <div>Memory</div>
              <div>Ports</div>
              <div></div>
            </div>
            {containers.map(c => (
              <div key={c.id || c.name} className="container-row">
                <div>
                  <div style={{ fontWeight: 500, fontSize: 13 }}>{c.name}</div>
                  <div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>up {c.uptime}</div>
                </div>
                <div className="mono" style={{ fontSize: 11.5, color: "var(--text-2)" }}>{c.image}</div>
                <div>
                  <span className="badge success"><StatusDot status="online" /> RUNNING</span>
                  {c.healthy && <span style={{ fontSize: 10.5, color: "var(--success)", marginLeft: 6 }}>healthy</span>}
                </div>
                <div className="mono" style={{ fontSize: 11.5 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                    <div style={{ width: 40, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
                      <div style={{ width: `${Math.min((c.cpu || 0) * 4, 100)}%`, height: "100%", background: "var(--accent)" }} />
                    </div>
                    <span>{(c.cpu || 0).toFixed(1)}%</span>
                  </div>
                </div>
                <div className="mono" style={{ fontSize: 11.5 }}>{c.mem} MB</div>
                <div className="mono" style={{ fontSize: 10.5, color: "var(--text-3)" }}>{c.ports}</div>
                <div style={{ display: "flex", gap: 4 }}>
                  <button className="btn ghost sm" onClick={() => showLogs(c)}>Logs</button>
                  <button className="btn ghost sm" onClick={() => restartContainer(c)}>Restart</button>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

function Cluster() {
  const [nodesData, setNodesData] = React.useState(window.ZAMPP_DATA.NODES);
  const [hovered, setHovered] = React.useState(null);

  const refresh = React.useCallback(() => {
    window.ZAMPP_API.fetch('/cluster')
      .then(data => {
        window.ZAMPP_DATA.NODES = data;
        setNodesData(data);
      })
      .catch(() => {});
  }, []);

  const nodesArr = Array.isArray(nodesData) ? nodesData : (nodesData?.nodes || []);

  const NODES = React.useMemo(() => {
    if (!nodesArr.length) return [];
    const primaryRaw = nodesArr.find(n => n.role === 'primary') || nodesArr[0];
    const others = nodesArr.filter(n => n !== primaryRaw);
    const primary = _normalizeNode(primaryRaw, 0.5, 0.46);
    const positioned = others.map((n, i) => {
      const angle = others.length <= 1
        ? Math.PI / 2
        : (i / others.length) * 2 * Math.PI - Math.PI / 2;
      return _normalizeNode(n, 0.5 + 0.32 * Math.cos(angle), 0.46 + 0.35 * Math.sin(angle));
    });
    return [primary, ...positioned];
  }, [nodesData]);

  const [selected, setSelected] = React.useState(null);
  const sel = selected || NODES[0] || null;
  const W = 720, H = 460;

  if (!NODES.length) {
    return (
      <div className="page">
        <div className="page-header">
          <h1>Cluster</h1>
          <div className="spacer" />
          <button className="btn ghost sm" onClick={refresh}><Icon name="refresh" />Refresh</button>
        </div>
        <div className="page-body">
          <div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>No cluster nodes available</div>
        </div>
      </div>
    );
  }

  const primary = NODES.find(n => n.role === 'primary') || NODES[0];
  const edges = NODES.filter(n => n.id !== primary.id).map(n => ({
    from: primary,
    to: n,
    alive: n.status === 'online',
  }));

  const addNode = () => {
    alert('To add a worker node:\n\n1. Install Docker + docker-compose on the target machine\n2. Copy /opt/wild-dragon to that machine\n3. Set NODE_ROLE=worker in the .env file\n4. Run: docker compose up -d\n\nThe node will register with this cluster automatically.');
  };

  const drainNode = (node) => {
    alert('Drain is not yet automated.\n\nTo drain ' + node.id + ':\n1. Stop new jobs from routing to this node\n2. Wait for in-progress jobs to complete\n3. Then remove the node safely');
  };

  const removeNode = (node) => {
    if (!window.confirm('Remove node ' + node.id + ' from the cluster?\nThis does not stop the machine — it only removes it from cluster membership.')) return;
    window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.id), { method: 'DELETE' })
      .then(() => refresh())
      .catch(e => alert('Remove failed: ' + e.message));
  };

  const nodeLogsHint = (node) => {
    alert('To view logs for ' + node.id + ' (' + node.ip + '):\n\nSSH to ' + node.ip + ' and run:\ndocker compose -f /opt/wild-dragon/docker-compose.yml logs -f');
  };

  return (
    <div className="page">
      <div className="page-header">
        <h1>Cluster</h1>
        <span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span>
        <div className="spacer" />
        <div className="status-pip"><span className="dot" /><span>Live</span></div>
        <button className="btn ghost sm" onClick={refresh}><Icon name="refresh" />Refresh</button>
        <button className="btn primary" onClick={addNode}><Icon name="plus" />Add node</button>
      </div>
      <div className="page-body">
        <div className="stat-row" style={{ padding: 0, marginBottom: 16 }}>
          <div className="stat-card">
            <div className="label"><Icon name="cluster" size={12} />Nodes</div>
            <div className="value">{NODES.length}</div>
          </div>
          <div className="stat-card">
            <div className="label"><Icon name="cpu" size={12} />Avg CPU</div>
            <div className="value">{Math.round(NODES.reduce((a, n) => a + (n.cpu || 0), 0) / NODES.length)} <span style={{ fontSize: 14, color: "var(--text-3)" }}>%</span></div>
          </div>
          <div className="stat-card">
            <div className="label"><Icon name="gpu" size={12} />GPUs</div>
            <div className="value">{NODES.reduce((a, n) => a + n.gpus.length, 0)}</div>
          </div>
          <div className="stat-card">
            <div className="label"><Icon name="hdd" size={12} />Avg Memory</div>
            <div className="value">{Math.round(NODES.reduce((a, n) => a + (n.mem || 0), 0) / NODES.length)} <span style={{ fontSize: 14, color: "var(--text-3)" }}>GB</span></div>
          </div>
        </div>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 340px", gap: 16, alignItems: "start" }}>
          <div className="cluster-canvas">
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", borderBottom: "1px solid var(--border)" }}>
              <span style={{ fontWeight: 600, fontSize: 13 }}>Topology</span>
              <div className="tab-group">
                <button className="active">Graph</button>
                <button>List</button>
              </div>
            </div>
            <svg viewBox={`0 0 ${W} ${H}`} style={{ display: "block", width: "100%", height: "auto" }}>
              <defs>
                <radialGradient id="nodeGlow">
                  <stop offset="0%" stopColor="rgba(91,124,250,0.3)" />
                  <stop offset="100%" stopColor="rgba(91,124,250,0)" />
                </radialGradient>
                <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
                  <path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.025)" strokeWidth="1" />
                </pattern>
              </defs>
              <rect width={W} height={H} fill="url(#grid)" />
              {edges.map((e, i) => {
                const x1 = e.from.x * W, y1 = e.from.y * H;
                const x2 = e.to.x * W, y2 = e.to.y * H;
                return (
                  <g key={i}>
                    <line x1={x1} y1={y1} x2={x2} y2={y2}
                      stroke={e.alive ? "var(--accent)" : "var(--text-4)"}
                      strokeWidth="1"
                      strokeDasharray={e.alive ? "0" : "4 3"}
                      opacity={e.alive ? 0.5 : 0.25}
                    />
                    {e.alive && (
                      <circle r="3" fill="var(--accent)">
                        <animateMotion dur={`${2 + i * 0.4}s`} repeatCount="indefinite"
                          path={`M ${x1} ${y1} L ${x2} ${y2}`} />
                      </circle>
                    )}
                  </g>
                );
              })}
              {NODES.map(n => {
                const cx = n.x * W, cy = n.y * H;
                const isSelected = sel && sel.id === n.id;
                const color = n.status === "online" ? "var(--success)" : "var(--text-4)";
                return (
                  <g key={n.id} transform={`translate(${cx}, ${cy})`}
                    style={{ cursor: "pointer" }}
                    onMouseEnter={() => setHovered(n.id)}
                    onMouseLeave={() => setHovered(null)}
                    onClick={() => setSelected(n)}>
                    {n.status === "online" && (
                      <circle r="44" fill="url(#nodeGlow)">
                        <animate attributeName="r" values="34;48;34" dur="3s" repeatCount="indefinite" />
                      </circle>
                    )}
                    <circle r={isSelected ? 26 : 22} fill="var(--bg-2)" stroke={isSelected ? "var(--accent)" : "var(--border-stronger)"} strokeWidth={isSelected ? 2 : 1} />
                    <circle r="6" cx="-13" cy="-13" fill={color} />
                    {n.role === "primary" && <path d="M -4 -2 L 0 2 L 4 -2 L 0 -6 Z" fill="var(--accent)" stroke="none" />}
                    {n.role !== "primary" && <text textAnchor="middle" y="3" fill="var(--text-2)" fontSize="10" fontFamily="var(--font-mono)">{n.role[0].toUpperCase()}</text>}
                    <text textAnchor="middle" y="40" fill={isSelected ? "var(--text-1)" : "var(--text-2)"} fontSize="11" fontWeight={isSelected ? 600 : 500}>{n.id}</text>
                    <text textAnchor="middle" y="54" fill="var(--text-3)" fontSize="10" fontFamily="var(--font-mono)">{n.ip}</text>
                  </g>
                );
              })}
            </svg>
          </div>

          {sel && (
            <div className="panel">
              <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", gap: 8 }}>
                <StatusDot status={sel.status} />
                <span style={{ fontWeight: 600, fontSize: 13 }}>{sel.id}</span>
                <span className={`badge ${sel.role === "primary" ? "accent" : "neutral"}`}>{sel.role}</span>
              </div>
              <div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 10 }}>
                <DetailRow k="Status" v={<span style={{ color: sel.status === "online" ? "var(--success)" : "var(--text-3)" }}>{sel.status}</span>} />
                <DetailRow k="IP" v={sel.ip} mono />
                <DetailRow k="Version" v={sel.version} mono />
                <DetailRow k="Uptime" v={sel.uptime} mono />
                <DetailRow k="CPU" v={
                  <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                    <div style={{ width: 80, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
                      <div style={{ width: `${sel.cpu}%`, height: "100%", background: "var(--accent)" }} />
                    </div>
                    <span className="mono">{sel.cpu}%</span>
                  </div>
                } />
                {sel.memTotal > 0 && (
                  <DetailRow k="Memory" v={
                    <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                      <div style={{ width: 80, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
                        <div style={{ width: `${(sel.mem / sel.memTotal) * 100}%`, height: "100%", background: "var(--purple)" }} />
                      </div>
                      <span className="mono">{sel.mem} / {sel.memTotal} GB</span>
                    </div>
                  } />
                )}
                {sel.gpus.length > 0 && (
                  <div>
                    <div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6 }}>GPUs ({sel.gpus.length})</div>
                    {sel.gpus.map((g, i) => (
                      <div key={i} className="mono" style={{ fontSize: 11.5, padding: "5px 8px", background: "var(--bg-2)", borderRadius: 4, marginBottom: 4, display: "flex", alignItems: "center", gap: 6 }}>
                        <Icon name="gpu" size={11} style={{ color: "var(--text-3)" }} />
                        <span>{g}</span>
                      </div>
                    ))}
                  </div>
                )}
                {sel.devices && sel.devices.length > 0 && (
                  <div>
                    <div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6 }}>Capture devices</div>
                    {sel.devices.map((d, i) => (
                      <div key={i} className="mono" style={{ fontSize: 11.5, padding: "5px 8px", background: "var(--bg-2)", borderRadius: 4 }}>
                        <Icon name="video" size={11} style={{ color: "var(--text-3)", marginRight: 6 }} />{d}
                      </div>
                    ))}
                  </div>
                )}
                <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
                  <button className="btn ghost sm" onClick={() => nodeLogsHint(sel)}>Logs</button>
                  <button className="btn ghost sm" onClick={() => drainNode(sel)}>Drain</button>
                  {sel.role !== "primary" && <button className="btn danger sm" onClick={() => removeNode(sel)}>Remove</button>}
                </div>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function DetailRow({ k, v, mono }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "90px 1fr", alignItems: "center", fontSize: 12 }}>
      <span style={{ color: "var(--text-3)" }}>{k}</span>
      <span className={mono ? "mono" : ""} style={{ fontSize: mono ? 11.5 : 12 }}>{v}</span>
    </div>
  );
}

function Settings() {
  const [s3, setS3] = React.useState({ s3_endpoint: '', s3_bucket: '', s3_access_key: '', s3_secret_key: '', s3_region: 'us-east-1' });
  const [s3Loading, setS3Loading] = React.useState(true);
  const [s3Saving,  setS3Saving]  = React.useState(false);
  const [s3Testing, setS3Testing] = React.useState(false);
  const [s3Msg,     setS3Msg]     = React.useState(null);
  const [secretExists, setSecretExists] = React.useState(false);

  React.useEffect(() => {
    window.ZAMPP_API.fetch('/settings/s3')
      .then(data => {
        setS3({ s3_endpoint: data.s3_endpoint || '', s3_bucket: data.s3_bucket || '', s3_access_key: data.s3_access_key || '', s3_secret_key: '', s3_region: data.s3_region || 'us-east-1' });
        setSecretExists(!!data.s3_secret_key_exists);
        setS3Loading(false);
      })
      .catch(() => setS3Loading(false));
  }, []);

  const saveS3 = () => {
    setS3Saving(true); setS3Msg(null);
    window.ZAMPP_API.fetch('/settings/s3', { method: 'PUT', body: JSON.stringify(s3) })
      .then(() => { setS3Saving(false); setS3Msg({ ok: true, text: 'Saved and applied.' }); if (s3.s3_secret_key) setSecretExists(true); })
      .catch(e  => { setS3Saving(false); setS3Msg({ ok: false, text: e.message }); });
  };

  const testS3 = () => {
    setS3Testing(true); setS3Msg(null);
    window.ZAMPP_API.fetch('/settings/s3/test', { method: 'POST', body: JSON.stringify(s3) })
      .then(r => { setS3Testing(false); setS3Msg({ ok: r.ok !== false, text: r.message || 'Connection OK' }); })
      .catch(e => { setS3Testing(false); setS3Msg({ ok: false, text: e.message }); });
  };

  return (
    <div className="page">
      <div className="page-header">
        <h1>Settings</h1>
        <span className="subtitle">System configuration · changes apply without restart</span>
      </div>
      <div className="page-body">
        <div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 24, alignItems: 'start' }}>
          <nav className="settings-nav">
            {[
              { id: 'storage', label: 'S3 / Object storage', icon: 'hdd' },
              { id: 'gpu',     label: 'GPU / Transcoding',   icon: 'gpu' },
              { id: 'sdi',     label: 'SDI capture',          icon: 'video' },
              { id: 'ampp',    label: 'AMPP integration',     icon: 'link' },
            ].map((s, i) => (
              <a key={s.id} className={`settings-nav-item ${i === 0 ? 'active' : ''}`}>
                <Icon name={s.icon} size={14} />{s.label}
              </a>
            ))}
          </nav>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            <SettingsCard
              icon="hdd"
              title="S3 / Object Storage"
              sub="S3-compatible bucket for media asset storage"
              tag={secretExists ? <span className="badge success">connected</span> : <span className="badge warning">not configured</span>}
            >
              {s3Loading ? (
                <div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Loading…</div>
              ) : (<>
                <SField label="Endpoint URL">
                  <input className="field-input mono" value={s3.s3_endpoint} onChange={e => setS3(p => ({...p, s3_endpoint: e.target.value}))} placeholder="https://s3.example.com" />
                </SField>
                <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
                  <SField label="Region">
                    <input className="field-input mono" value={s3.s3_region} onChange={e => setS3(p => ({...p, s3_region: e.target.value}))} placeholder="us-east-1" />
                  </SField>
                  <SField label="Bucket">
                    <input className="field-input mono" value={s3.s3_bucket} onChange={e => setS3(p => ({...p, s3_bucket: e.target.value}))} placeholder="my-bucket" />
                  </SField>
                </div>
                <SField label="Access key ID">
                  <input className="field-input mono" value={s3.s3_access_key} onChange={e => setS3(p => ({...p, s3_access_key: e.target.value}))} placeholder="Access key ID" />
                </SField>
                <SField label="Secret access key">
                  <input className="field-input mono" type="password" value={s3.s3_secret_key} onChange={e => setS3(p => ({...p, s3_secret_key: e.target.value}))} placeholder={secretExists ? '(saved — type to replace)' : 'Secret key'} />
                </SField>
                {s3Msg && (
                  <div style={{ fontSize: 12, padding: '6px 10px', borderRadius: 5, border: '1px solid', background: s3Msg.ok ? 'var(--success-soft)' : 'var(--danger-soft)', borderColor: s3Msg.ok ? 'var(--success)' : 'var(--danger)', color: s3Msg.ok ? 'var(--success)' : 'var(--danger)' }}>
                    {s3Msg.text}
                  </div>
                )}
                <div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
                  <button className="btn primary sm" onClick={saveS3} disabled={s3Saving}>{s3Saving ? 'Saving…' : 'Save & apply'}</button>
                  <button className="btn ghost sm" onClick={testS3} disabled={s3Testing}>{s3Testing ? 'Testing…' : 'Test connection'}</button>
                </div>
              </>)}
            </SettingsCard>
          </div>
        </div>
      </div>
    </div>
  );
}

function SField({ label, children }) {
  return (
    <div className="field">
      <label className="field-label">{label}</label>
      {children}
    </div>
  );
}

function SettingsCard({ icon, title, sub, tag, children }) {
  return (
    <div className="settings-card">
      <div className="settings-card-head">
        <div className="settings-card-icon"><Icon name={icon} size={16} /></div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 600, fontSize: 14 }}>{title}</div>
          <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>{sub}</div>
        </div>
        {tag}
      </div>
      <div className="settings-card-body">{children}</div>
    </div>
  );
}

Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });
