// screens-dashboard.jsx — staff-only ops cockpit. No user surfaces.

// Sections an admin can see. A code-submitter (can_submit_codes && !is_admin)
// sees only the subset in SUBMITTER_SECTIONS.
const ADMIN_NAV = [
  ["users",        "users",   "Users"],
  ["broadcast",    "code",    "Send Code"],
  ["admin-claims", "bolt",    "Claim History"],
  ["promos",       "credit",  "Promos"],
  ["revenue",      "chart",   "Revenue"],
  ["health",       "shield",  "Health"],
  ["logs",         "code",    "Logs"],
];
const SUBMITTER_SECTIONS = new Set(["broadcast", "logs"]);

const visibleNav = (user) =>
  user?.is_admin
    ? ADMIN_NAV
    : user?.can_submit_codes
      ? ADMIN_NAV.filter(([id]) => SUBMITTER_SECTIONS.has(id))
      : [];

const Sidebar = ({ active, setActive, user }) => {
  const nav = visibleNav(user);
  return (
    <aside className="sidebar">
      <div className="group-label">
        {user?.is_admin ? "Ops · staff" : "Code submitter"}
      </div>
      {nav.map(([id, ic, label]) => (
        <div key={id} className={"nav-item " + (active === id ? "active" : "")} onClick={() => setActive(id)}>
          <Icon name={ic} /> <span>{label}</span>
        </div>
      ))}
    </aside>
  );
};

const NoAccess = () => (
  <div className="container fade-in" style={{ maxWidth: 720, padding: "80px 24px" }}>
    <Card style={{ padding: 40, textAlign: "center" }}>
      <div className="eyebrow" style={{ marginBottom: 12 }}>access not assigned</div>
      <h2 style={{ fontSize: 22, marginBottom: 12 }}>This dashboard is staff-only.</h2>
      <p className="muted" style={{ fontSize: 14, marginBottom: 24 }}>
        Day-to-day claiming lives in the Telegram bot. Open it to manage your pass,
        check balances, and pull recent codes.
      </p>
      <Btn kind="primary" onClick={() => window.open("https://t.me/CCUclaimerbot", "_blank")}>
        Open @CCUclaimerbot
      </Btn>
    </Card>
  </div>
);

const Dashboard = ({ user, plan, goto, addToast }) => {
  const nav = visibleNav(user);
  const [active, setActive] = React.useState(() => nav[0]?.[0] || null);

  // If user/role hydrates after first mount, snap to the first allowed section.
  React.useEffect(() => {
    const allowed = visibleNav(user).map(([id]) => id);
    if (allowed.length && !allowed.includes(active)) setActive(allowed[0]);
  }, [user]);

  if (!nav.length) return <NoAccess />;

  return (
    <div className="container fade-in" style={{ maxWidth: 1300 }}>
      <div className="dash">
        <Sidebar active={active} setActive={setActive} user={user} />
        <AdminPanel active={active} user={user} />
      </div>
    </div>
  );
};

const AdminPanel = ({ active, user }) => {
  if (active === "users")        return <AdminUsers />;
  if (active === "broadcast")    return <AdminBroadcast />;
  if (active === "admin-claims") return <AdminClaims />;
  if (active === "promos")       return <AdminPromos />;
  if (active === "revenue")      return <AdminRevenue />;
  if (active === "logs")         return <AdminLogs />;
  return <AdminHealth />;
};

const AdminLogs = () => {
  const [entries, setEntries] = React.useState([]);
  const [level, setLevel] = React.useState("INFO");
  const [search, setSearch] = React.useState("");
  const [paused, setPaused] = React.useState(false);
  const [headSeq, setHeadSeq] = React.useState(0);
  const [bufferSize, setBufferSize] = React.useState(0);
  const tailRef = React.useRef(null);
  const lastSeqRef = React.useRef(0);
  const authHeader = () => ({ Authorization: `Bearer ${localStorage.getItem("cc_token")}` });

  const fetchLogs = React.useCallback(async (mode) => {
    // mode: "full" = reset + fetch latest 200; "tail" = incremental since last seq
    const params = new URLSearchParams();
    if (mode === "tail" && lastSeqRef.current) {
      params.set("since_seq", String(lastSeqRef.current));
    } else {
      params.set("limit", "300");
    }
    if (level) params.set("level", level);
    if (search.trim()) params.set("search", search.trim());
    try {
      const r = await fetch(`/api/admin/logs?${params.toString()}`, { headers: authHeader() });
      if (!r.ok) return;
      const d = await r.json();
      setHeadSeq(d.head_seq || 0);
      setBufferSize(d.buffer_size || 0);
      if (!d.entries?.length) return;
      lastSeqRef.current = d.entries[d.entries.length - 1].seq;
      if (mode === "tail") {
        setEntries(prev => {
          const merged = [...prev, ...d.entries];
          return merged.length > 800 ? merged.slice(-800) : merged;
        });
      } else {
        setEntries(d.entries);
      }
    } catch {}
  }, [level, search]);

  // Initial + filter-change full reload
  React.useEffect(() => {
    lastSeqRef.current = 0;
    setEntries([]);
    fetchLogs("full");
  }, [level, search]);

  // Tail poll (every 2s) unless paused
  React.useEffect(() => {
    if (paused) return;
    const t = setInterval(() => fetchLogs("tail"), 2000);
    return () => clearInterval(t);
  }, [paused, fetchLogs]);

  // Auto-scroll to bottom on new entries (unless user scrolled up)
  React.useEffect(() => {
    const el = tailRef.current;
    if (!el) return;
    const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
    if (nearBottom) el.scrollTop = el.scrollHeight;
  }, [entries]);

  const levelColor = (lvl) => ({
    DEBUG:    "#6c7280",
    INFO:     "#a3a3a3",
    WARNING:  "#f5a623",
    ERROR:    "#ff5050",
    CRITICAL: "#ff0066",
  }[lvl] || "#a3a3a3");

  const fmtTs = (ts) => {
    const d = new Date(ts * 1000);
    return d.toTimeString().slice(0, 8) + "." + String(d.getMilliseconds()).padStart(3, "0");
  };

  return (
    <div className="dash-main">
      <span className="eyebrow">admin · live logs</span>
      <h2 style={{ fontSize: 24, marginTop: 6, marginBottom: 14 }}>Service log tail</h2>

      <div style={{ display: "flex", gap: 10, marginBottom: 12, flexWrap: "wrap", alignItems: "center" }}>
        <select value={level} onChange={e => setLevel(e.target.value)}
          style={{ background: "#181818", color: "#eee", border: "1px solid #333", borderRadius: 6, padding: "6px 10px" }}>
          <option value="">All</option>
          <option value="DEBUG">DEBUG+</option>
          <option value="INFO">INFO+</option>
          <option value="WARNING">WARNING+</option>
          <option value="ERROR">ERROR+</option>
        </select>
        <input type="text" placeholder="Search…" value={search} onChange={e => setSearch(e.target.value)}
          style={{ flex: 1, minWidth: 180, background: "#181818", color: "#eee", border: "1px solid #333", borderRadius: 6, padding: "6px 10px" }} />
        <button onClick={() => setPaused(p => !p)}
          style={{ background: paused ? "#3a2a00" : "#1a1a1a", color: paused ? "#f5a623" : "#aaa", border: "1px solid #333", borderRadius: 6, padding: "6px 14px", cursor: "pointer" }}>
          {paused ? "▶ Resume" : "⏸ Pause"}
        </button>
        <button onClick={() => { setEntries([]); lastSeqRef.current = 0; fetchLogs("full"); }}
          style={{ background: "#1a1a1a", color: "#aaa", border: "1px solid #333", borderRadius: 6, padding: "6px 14px", cursor: "pointer" }}>
          ↻ Reload
        </button>
        <span className="muted" style={{ fontSize: 11, marginLeft: "auto" }}>
          {entries.length} shown · head seq {headSeq} · buffer {bufferSize}
        </span>
      </div>

      <div ref={tailRef}
        style={{
          background: "#0a0a0a", border: "1px solid #222", borderRadius: 8,
          padding: 10, height: "calc(100vh - 280px)", minHeight: 320,
          overflowY: "auto", fontFamily: "ui-monospace, Menlo, monospace",
          fontSize: 12, lineHeight: 1.5,
        }}>
        {entries.length === 0
          ? <p className="muted" style={{ padding: 12 }}>No log entries yet. The buffer fills as the service writes logs.</p>
          : entries.map(e => (
              <div key={e.seq} style={{ display: "flex", gap: 8, padding: "1px 0", whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                <span style={{ color: "#555", flexShrink: 0 }}>{fmtTs(e.ts)}</span>
                <span style={{ color: levelColor(e.level), flexShrink: 0, width: 60 }}>{e.level}</span>
                <span style={{ color: "#888", flexShrink: 0, maxWidth: 180, overflow: "hidden", textOverflow: "ellipsis" }}>{e.logger}</span>
                <span style={{ color: "#ddd", flex: 1 }}>{e.message}</span>
              </div>
            ))}
      </div>
    </div>
  );
};

const AdminBroadcast = () => {
  const authHeader = () => ({ Authorization: `Bearer ${localStorage.getItem("cc_token")}`, "content-type": "application/json" });

  const PLATFORMS = [
    { id: "both_stakes", label: "Both Stakes", desc: "Fire .com AND .us dispatch simultaneously — same code, both engines", color: "var(--accent)" },
    { id: "stake_com",   label: "Stake.com",   desc: "API claim for all active Stake.com subscribers", color: "var(--accent)" },
    { id: "stake_us",    label: "Stake.us",    desc: "API claim for all active Stake.us subscribers", color: "var(--accent)" },
    { id: "shuffle",     label: "Shuffle",     desc: "Push to all connected userscript clients", color: "var(--ok)" },
    { id: "thrill",      label: "Thrill",      desc: "Push to all connected userscript clients", color: "var(--ok)" },
  ];

  const [platform, setPlatform] = React.useState("both_stakes");
  const [code, setCode]         = React.useState("");
  const [sending, setSending]   = React.useState(false);
  const [result, setResult]     = React.useState(null); // { ok, message }
  const [history, setHistory]   = React.useState([]);   // last few sends

  const postBroadcast = (plat) =>
    fetch("/api/admin/broadcast", {
      method: "POST", headers: authHeader(),
      body: JSON.stringify({ code: code.trim(), platform: plat }),
    }).then(r => r.json().then(d => ({ ok: !!d.ok, error: d.error, platform: plat })));

  const send = async () => {
    if (!code.trim()) return;
    setSending(true); setResult(null);
    try {
      const targets = platform === "both_stakes" ? ["stake_com", "stake_us"] : [platform];
      const results = await Promise.all(targets.map(postBroadcast));
      const allOk = results.every(r => r.ok);
      const plat = PLATFORMS.find(p => p.id === platform);
      const entry = {
        code: code.trim(),
        platform,
        label: plat.label,
        ok: allOk,
        time: new Date().toLocaleTimeString(),
        // When both-stakes, surface a per-target breakdown in the history line
        breakdown: targets.length > 1
          ? results.map(r => `${r.platform === "stake_com" ? ".com" : ".us"}${r.ok ? "✓" : "✕"}`).join(" ")
          : null,
      };
      setHistory(h => [entry, ...h].slice(0, 10));
      if (allOk) {
        setResult({ ok: true, message: targets.length > 1
          ? `Dispatched to Stake.com AND Stake.us — ${results.length} broadcasts fired`
          : `Dispatched — all ${plat.label} users notified` });
        setCode("");
      } else {
        const failed = results.filter(r => !r.ok);
        setResult({
          ok: false,
          message: failed.map(r => `${r.platform}: ${r.error || "error"}`).join(" · "),
        });
      }
    } catch (_) {
      setResult({ ok: false, message: "Network error" });
    }
    setSending(false);
  };

  const plat = PLATFORMS.find(p => p.id === platform);

  return (
    <div className="stack-lg" style={{ maxWidth: 600 }}>
      <div>
        <span className="eyebrow">admin · send code</span>
        <h2 style={{ fontSize: 24, marginTop: 4 }}>Manual broadcast</h2>
        <p className="muted" style={{ marginTop: 4, fontSize: 13 }}>
          Stake codes trigger API claims instantly. Shuffle/Thrill codes push to connected userscript clients.
        </p>
      </div>

      {/* Platform picker — Both Stakes spans full width as the headline option,
          single-platform cards sit underneath in a 2-col grid. */}
      <Card>
        <div style={{ fontWeight: 600, marginBottom: 12 }}>Platform</div>
        {(() => {
          const cardStyle = (p) => ({
            padding: "12px 14px", borderRadius: "var(--radius-sm)", cursor: "pointer",
            border: `2px solid ${platform === p.id ? p.color : "var(--border)"}`,
            background: platform === p.id ? "var(--surf-3)" : "var(--surf-2)",
            transition: "border-color .15s",
          });
          const cardInner = (p) => (
            <>
              <div style={{ fontWeight: 700, fontSize: 14, color: platform === p.id ? p.color : "var(--text)" }}>{p.label}</div>
              <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 3 }}>{p.desc}</div>
            </>
          );
          const both = PLATFORMS.find(p => p.id === "both_stakes");
          const rest = PLATFORMS.filter(p => p.id !== "both_stakes");
          return (
            <div className="stack-sm" style={{ gap: 8 }}>
              <div onClick={() => setPlatform(both.id)} style={cardStyle(both)}>
                {cardInner(both)}
              </div>
              <div className="grid-2" style={{ gap: 8 }}>
                {rest.map(p => (
                  <div key={p.id} onClick={() => setPlatform(p.id)} style={cardStyle(p)}>
                    {cardInner(p)}
                  </div>
                ))}
              </div>
            </div>
          );
        })()}
      </Card>

      {/* Code input */}
      <Card>
        <div style={{ fontWeight: 600, marginBottom: 12 }}>Code</div>
        <div className="row" style={{ gap: 8 }}>
          <Input
            value={code}
            onChange={e => { setCode(e.target.value); setResult(null); }}
            onKeyDown={e => e.key === "Enter" && !sending && send()}
            placeholder={`e.g. BOOST100`}
            style={{ flex: 1, fontFamily: "var(--font-mono)", letterSpacing: ".06em", fontSize: 15 }}
          />
          <Btn kind="primary" disabled={sending || !code.trim()} onClick={send} style={{ minWidth: 100 }}>
            {sending ? "Sending…" : `Send → ${plat.label}`}
          </Btn>
        </div>
        {result && (
          <div style={{ marginTop: 10, padding: "8px 12px", borderRadius: "var(--radius-sm)",
            background: result.ok ? "rgba(0,255,136,.08)" : "rgba(255,80,80,.08)",
            border: `1px solid ${result.ok ? "var(--ok)" : "var(--err)"}`,
            color: result.ok ? "var(--ok)" : "var(--err)", fontSize: 13 }}>
            {result.ok ? "✓" : "✕"} {result.message}
          </div>
        )}
      </Card>

      {/* Recent sends */}
      {history.length > 0 && (
        <Card style={{ padding: 0 }}>
          <div style={{ padding: "10px 14px", borderBottom: "1px solid var(--border)", fontWeight: 600, fontSize: 13 }}>Recent sends</div>
          {history.map((h, i) => (
            <div key={i} className="row between" style={{ padding: "9px 14px", borderBottom: i < history.length - 1 ? "1px solid var(--border)" : "none", fontSize: 13 }}>
              <div className="row" style={{ gap: 10 }}>
                <span style={{ color: h.ok ? "var(--ok)" : "var(--err)" }}>{h.ok ? "✓" : "✕"}</span>
                <code style={{ fontFamily: "var(--font-mono)", fontWeight: 600 }}>{h.code}</code>
                <Pill>{h.label}</Pill>
                {h.breakdown && <span className="muted" style={{ fontSize: 11 }}>{h.breakdown}</span>}
              </div>
              <span className="muted" style={{ fontSize: 11 }}>{h.time}</span>
            </div>
          ))}
        </Card>
      )}
    </div>
  );
};

const AdminUsers = () => {
  const [users, setUsers]     = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [search, setSearch]   = React.useState("");
  const [busy, setBusy]       = React.useState({});
  const [revealed, setRevealed] = React.useState({});
  const [granting, setGranting] = React.useState({}); // userId → { plan, days } | null
  const [grantOk, setGrantOk]   = React.useState({});
  const [slots, setSlots]       = React.useState({}); // userId → slot[] | "loading"
  const [slotBusy, setSlotBusy] = React.useState({}); // slotId → true
  const [details, setDetails]   = React.useState({}); // userId → {stats} | "loading"
  const [flagBusy, setFlagBusy] = React.useState({}); // `${userId}:${flag}` → true

  const authHeader = () => ({ Authorization: `Bearer ${localStorage.getItem("cc_token")}`, "content-type": "application/json" });

  const load = () => {
    fetch("/api/admin/users", { headers: authHeader() })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d?.users) setUsers(d.users); setLoading(false); })
      .catch(() => setLoading(false));
  };
  React.useEffect(load, []);

  // Toggle the per-user slots panel; fetch slots on first open
  const toggleSlots = (userId) => {
    if (slots[userId] !== undefined) {
      setSlots(s => { const n = { ...s }; delete n[userId]; return n; });
      return;
    }
    setSlots(s => ({ ...s, [userId]: "loading" }));
    fetch(`/api/admin/users/${userId}/slots`, { headers: authHeader() })
      .then(r => r.ok ? r.json() : null)
      .then(d => setSlots(s => ({ ...s, [userId]: d?.slots || [] })))
      .catch(() => setSlots(s => ({ ...s, [userId]: [] })));
  };

  // Toggle the per-user details panel — lazy-loads the stats endpoint and
  // caches the user record so flag toggles can rerender without a full reload.
  const toggleDetails = (userId) => {
    if (details[userId] !== undefined) {
      setDetails(s => { const n = { ...s }; delete n[userId]; return n; });
      return;
    }
    setDetails(s => ({ ...s, [userId]: "loading" }));
    fetch(`/api/admin/users/${userId}/stats`, { headers: authHeader() })
      .then(r => r.ok ? r.json() : null)
      .then(d => setDetails(s => ({ ...s, [userId]: d || { error: true } })))
      .catch(() => setDetails(s => ({ ...s, [userId]: { error: true } })));
  };

  // Flip a per-user flag (or set granted_slots). Optimistic update on the
  // visible user list so the pill switches immediately; reverts if the POST
  // fails. Mirrors the bot's /setccu /setadmin / etc commands.
  const setFlag = async (userId, key, value) => {
    const ub = `${userId}:${key}`;
    setFlagBusy(b => ({ ...b, [ub]: true }));
    const prev = users.find(u => u.id === userId);
    setUsers(us => us.map(u => u.id === userId ? { ...u, [key]: value } : u));
    try {
      const r = await fetch(`/api/admin/users/${userId}/flags`, {
        method: "POST", headers: authHeader(),
        body: JSON.stringify({ [key]: value }),
      });
      const d = await r.json();
      if (!d.ok) throw new Error(d.error || "flag update failed");
    } catch (e) {
      // Revert on failure
      if (prev) setUsers(us => us.map(u => u.id === userId ? prev : u));
    }
    setFlagBusy(b => ({ ...b, [ub]: false }));
  };

  const setSlotActive = async (userId, slotId, active) => {
    setSlotBusy(b => ({ ...b, [slotId]: true }));
    try {
      const r = await fetch(`/api/admin/slots/${slotId}/active`, {
        method: "POST", headers: authHeader(),
        body: JSON.stringify({ active }),
      });
      const d = await r.json();
      if (d.ok) {
        setSlots(s => ({
          ...s,
          [userId]: (s[userId] || []).map(sl => sl.id === slotId ? { ...sl, active } : sl),
        }));
      }
    } catch (_) {}
    setSlotBusy(b => ({ ...b, [slotId]: false }));
  };

  const act = async (userId, action) => {
    setBusy(b => ({ ...b, [userId]: action }));
    const url = `/api/admin/users/${userId}/${action === "suspend" ? "suspend" : action + "-extension"}`;
    try {
      const r = await fetch(url, { method: "POST", headers: authHeader(), body: JSON.stringify({}) });
      const d = await r.json();
      if (action === "approve" && d.token) {
        setRevealed(rv => ({ ...rv, [userId]: d.token }));
      }
      load();
    } catch (_) {}
    setBusy(b => ({ ...b, [userId]: null }));
  };

  const grant = async (userId) => {
    const g = granting[userId] || { kind: "browser", days: 30 };
    setBusy(b => ({ ...b, [userId]: "grant" }));
    try {
      const r = await fetch(`/api/admin/users/${userId}/grant`, {
        method: "POST", headers: authHeader(),
        body: JSON.stringify({ kind: g.kind, days: parseInt(g.days) || 30 }),
      });
      const d = await r.json();
      if (d.ok) { setGrantOk(o => ({ ...o, [userId]: true })); load(); }
    } catch (_) {}
    setBusy(b => ({ ...b, [userId]: null }));
    setTimeout(() => setGrantOk(o => ({ ...o, [userId]: false })), 3000);
  };

  const filtered = users.filter(u => {
    if (!search) return true;
    const q = search.toLowerCase();
    return (u.username || "").includes(q) || (u.email || "").includes(q);
  });

  // What the admin can grant. Browser/API correspond to the paid time-pass
  // flow (extends tampermonkey_expires_at / plan_expires_at); Free clears.
  const PLAN_OPTS = [
    { value: "browser", label: "Browser pass (days)" },
    { value: "api",     label: "API pass (days, comps browser)" },
    { value: "free",    label: "Free (clear)" },
  ];

  return (
    <div className="stack-lg">
      <div className="row between">
        <div>
          <span className="eyebrow">admin · users</span>
          <h2 style={{ fontSize: 24, marginTop: 4 }}>{loading ? "…" : `${users.length} users`}</h2>
        </div>
        <Btn sm onClick={load}><Icon name="refresh"/> Refresh</Btn>
      </div>
      <Card style={{ padding: 0 }}>
        <div className="row" style={{ padding: 12, gap: 8, borderBottom: "1px solid var(--border)" }}>
          <Input placeholder="search by username or email…" value={search} onChange={e => setSearch(e.target.value)} style={{ flex: 1 }} />
        </div>
        {loading ? <p className="muted" style={{ padding: 20 }}>Loading…</p> : (
          <div>
            {filtered.map((u) => {
              const hasExt = false; // extension not yet available
              const isBusy = busy[u.id];
              const token  = revealed[u.id];
              const g      = granting[u.id] || { kind: "browser", days: "30" };
              return (
                <div key={u.id} style={{ borderBottom: "1px solid var(--border)", padding: "12px 14px" }}>
                  <div className="row between" style={{ flexWrap: "wrap", gap: 8 }}>
                    <div>
                      <span style={{ fontWeight: 600, fontSize: 14 }}>{u.username || "—"}</span>
                      <span className="muted" style={{ fontSize: 12, marginLeft: 10 }}>{u.email || "telegram"}</span>
                    </div>
                    <div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
                      <Pill kind={u.plan !== "free" ? "ok" : ""} dot={u.plan !== "free"}>{u.plan_display || u.plan}</Pill>
                      {u.plan_expires_at && <span className="muted" style={{ fontSize: 11 }}>exp {new Date(u.plan_expires_at).toLocaleDateString()}</span>}
                      {u.is_admin && <Pill kind="warn" dot>admin</Pill>}
                    </div>
           
                  </div>

                  {/* Grant free subscription */}
                  <div className="row" style={{ marginTop: 10, gap: 8, flexWrap: "wrap", alignItems: "center" }}>
                    <select value={g.kind} onChange={e => setGranting(gr => ({ ...gr, [u.id]: { ...g, kind: e.target.value } }))}
                      style={{ fontSize: 12, padding: "4px 8px", background: "var(--surf-2)", color: "var(--text)", border: "1px solid var(--border-2)", borderRadius: "var(--radius-sm)", cursor: "pointer" }}>
                      {PLAN_OPTS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
                    </select>
                    <Input value={g.days} onChange={e => setGranting(gr => ({ ...gr, [u.id]: { ...g, days: e.target.value } }))}
                      placeholder="days" style={{ width: 60, fontSize: 12, padding: "4px 8px" }} />
                    <Btn sm kind="primary" disabled={!!isBusy} onClick={() => grant(u.id)}>
                      {isBusy === "grant" ? "…" : grantOk[u.id] ? "✓ Granted!" : "Grant free sub"}
                    </Btn>
                    <Btn sm kind="ghost" disabled={!!isBusy} onClick={() => act(u.id, "suspend")}
                      style={{ color: "var(--warn)" }}>
                      {isBusy === "suspend" ? "…" : "Suspend"}
                    </Btn>
                    <Btn sm kind="ghost" onClick={() => toggleSlots(u.id)}>
                      {slots[u.id] !== undefined ? "Hide slots" : "API slots"}
                    </Btn>
                    <Btn sm kind="ghost" onClick={() => toggleDetails(u.id)}>
                      {details[u.id] !== undefined ? "Hide details" : "Details"}
                    </Btn>
                  </div>

                  {/* Operator-visible per-user record — flag toggles + claim stats */}
                  {details[u.id] !== undefined && (
                    <UserDetail
                      user={u}
                      stats={details[u.id]}
                      flagBusy={flagBusy}
                      setFlag={setFlag}
                    />
                  )}

                  {/* Per-slot enable/disable — for wager-requirement enforcement */}
                  {slots[u.id] !== undefined && (
                    <div style={{ marginTop: 8, padding: "8px 10px", background: "var(--surf-2)", borderRadius: "var(--radius-sm)", border: "1px solid var(--border)" }}>
                      {slots[u.id] === "loading" ? (
                        <span className="muted" style={{ fontSize: 12 }}>Loading slots…</span>
                      ) : slots[u.id].length === 0 ? (
                        <span className="muted" style={{ fontSize: 12 }}>No API slots configured.</span>
                      ) : (
                        <div className="stack-sm">
                          {slots[u.id].map(sl => (
                            <div key={sl.id} className="row between" style={{ fontSize: 12, gap: 8 }}>
                              <span className="row" style={{ gap: 8 }}>
                                <Pill kind={sl.active ? "ok" : "err"} dot>{sl.active ? "active" : "disabled"}</Pill>
                                <strong>{PLAT_LABEL[sl.platform] || sl.platform}</strong>
                                <span className="muted">slot {sl.slot_index} · {sl.currency}</span>
                                <code style={{ fontFamily: "var(--font-mono)", color: "var(--text-3)" }}>{sl.api_key_masked}</code>
                              </span>
                              <Btn sm kind="ghost" disabled={!!slotBusy[sl.id]}
                                onClick={() => setSlotActive(u.id, sl.id, !sl.active)}
                                style={{ color: sl.active ? "var(--err)" : "var(--ok)" }}>
                                {slotBusy[sl.id] ? "…" : sl.active ? "Disable" : "Enable"}
                              </Btn>
                            </div>
                          ))}
                        </div>
                      )}
                    </div>
                  )}

                  {/* Extension controls — only for ext-capable plans */}
                  {hasExt && (
                    <div className="row" style={{ marginTop: 8, gap: 8, flexWrap: "wrap" }}>
                      <Btn sm kind="ghost" disabled={!!isBusy} onClick={() => act(u.id, "approve")}>
                        {isBusy === "approve" ? "…" : <><Icon name="key"/> Approve ext token</>}
                      </Btn>
                      <Btn sm kind="ghost" disabled={!!isBusy} onClick={() => act(u.id, "revoke")}
                        style={{ color: "var(--err)", borderColor: "var(--err)" }}>
                        {isBusy === "revoke" ? "…" : "Revoke token"}
                      </Btn>
                    </div>
                  )}

                  {token && (
                    <div style={{ marginTop: 8, padding: "8px 10px", background: "var(--surf-3)", borderRadius: "var(--radius-sm)", border: "1px solid var(--accent-3)" }}>
                      <div className="row between" style={{ gap: 8 }}>
                        <span style={{ fontSize: 11, color: "var(--text-3)" }}>
                          Extension token — copy and share with {u.username}. Not shown again.
                        </span>
                        <Btn sm kind="ghost" onClick={() => navigator.clipboard.writeText(token)}><Icon name="copy"/></Btn>
                      </div>
                      <code style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--accent)", wordBreak: "break-all" }}>{token}</code>
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </Card>
    </div>
  );
};

const PLAT_LABEL = { stake_com: "Stake.com", stake_us: "Stake.us", shuffle: "Shuffle", thrill: "Thrill" };

// ── Admin user detail (flags + lifetime/7d metrics) ─────────────────────
// Mounted inline under an AdminUsers row when the operator clicks "Details".
// Shows the full operator-visible record from the rollout checklist: flags,
// access windows, slot grants, lifetime and rolling-7d claim totals broken
// out by platform / category / currency.

const FLAG_DEFS = [
  ["is_admin",          "Admin"],
  ["can_submit_codes",  "Can submit codes"],
  ["is_legacy",         "Legacy"],
  ["beta_tester",       "Beta tester"],
  ["ccu_member",        "CCU member"],
  ["ccu_vip",           "CCU VIP"],
  ["ccu_good_standing", "Good standing"],
  ["disabled",          "Disabled"],
  ["reloads_paused",    "Reloads paused"],
];

const CATEGORY_LABEL = { bonus: "bonus", reload: "reloads", daily_dollar: "daily $" };

const FlagToggle = ({ on, label, busy, onClick }) => (
  <button
    onClick={onClick}
    disabled={busy}
    style={{
      background: on ? "var(--accent)" : "var(--surf-2)",
      color: on ? "var(--text-on-accent, #fff)" : "var(--text-3)",
      border: `1px solid ${on ? "var(--accent)" : "var(--border-2)"}`,
      borderRadius: "var(--radius-sm)",
      padding: "4px 10px",
      fontSize: 11,
      cursor: busy ? "wait" : "pointer",
      opacity: busy ? 0.6 : 1,
    }}
  >
    {on ? "✓" : "○"} {label}
  </button>
);

// Re-shape the stats array into a {platform: {category: [{currency, amount, count}]}} map
// for compact table rendering.
const groupStats = (rows) => {
  const out = {};
  for (const r of rows || []) {
    out[r.platform] = out[r.platform] || {};
    out[r.platform][r.category] = out[r.platform][r.category] || [];
    out[r.platform][r.category].push(r);
  }
  return out;
};

const StatsTable = ({ rows, header }) => {
  const grouped = groupStats(rows);
  const platforms = Object.keys(grouped).sort();
  if (!platforms.length) {
    return (
      <div>
        <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>{header}</div>
        <div className="muted" style={{ fontSize: 12 }}>No claimed events yet.</div>
      </div>
    );
  }
  return (
    <div>
      <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>{header}</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
        {platforms.map(plat => (
          <div key={plat} style={{ fontSize: 12 }}>
            <strong>{PLAT_LABEL[plat] || plat}</strong>
            {Object.entries(grouped[plat]).map(([cat, entries]) => (
              <div key={cat} className="muted" style={{ paddingLeft: 12 }}>
                {CATEGORY_LABEL[cat] || cat}:{" "}
                {entries.map(e => `${e.amount % 1 === 0 ? e.amount : e.amount.toFixed(4).replace(/0+$/, "").replace(/\.$/, "")} ${e.currency} (${e.count})`).join(", ")}
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
};

const UserDetail = ({ user, stats, flagBusy, setFlag }) => {
  const tmExp = user.tampermonkey_expires_at ? new Date(user.tampermonkey_expires_at) : null;
  const tmLeft = tmExp ? Math.max(0, Math.round((tmExp - Date.now()) / 86400000)) : null;
  const planExp = user.plan_expires_at ? new Date(user.plan_expires_at) : null;
  const planLeft = planExp ? Math.max(0, Math.round((planExp - Date.now()) / 86400000)) : null;

  return (
    <div
      style={{
        marginTop: 10,
        padding: "10px 12px",
        background: "var(--surf-2)",
        borderRadius: "var(--radius-sm)",
        border: "1px solid var(--border)",
        display: "flex",
        flexDirection: "column",
        gap: 12,
      }}
    >
      {/* Identity */}
      <div style={{ fontSize: 12 }}>
        <strong>{user.telegram_username || user.username || "—"}</strong>
        <span className="muted" style={{ marginLeft: 8 }}>
          tg: {user.telegram_id || "—"} · email: {user.email || "—"}
        </span>
      </div>

      {/* Access */}
      <div style={{ fontSize: 12, display: "flex", gap: 12, flexWrap: "wrap" }}>
        <span><strong>Browser pass:</strong> {tmLeft != null ? `${tmLeft}d left` : "—"}</span>
        <span><strong>Plan:</strong> {user.plan} {planLeft != null ? `(${planLeft}d)` : ""}</span>
        <span><strong>Slots:</strong> {user.granted_slots ?? 1} granted</span>
      </div>

      {/* Flag toggles */}
      <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
        {FLAG_DEFS.map(([key, label]) => (
          <FlagToggle
            key={key}
            on={!!user[key]}
            label={label}
            busy={!!flagBusy[`${user.id}:${key}`]}
            onClick={() => setFlag(user.id, key, !user[key])}
          />
        ))}
      </div>

      {/* Stats */}
      {stats === "loading" ? (
        <span className="muted" style={{ fontSize: 12 }}>Loading stats…</span>
      ) : stats?.error ? (
        <span className="muted" style={{ fontSize: 12, color: "var(--err)" }}>Failed to load stats.</span>
      ) : (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
          <StatsTable rows={stats?.lifetime || []} header="Lifetime claimed" />
          <StatsTable rows={stats?.rolling_7d || []} header="Last 7 days" />
        </div>
      )}

      <div className="muted" style={{ fontSize: 11 }}>
        Thrill is "coming soon" — its rows are excluded from totals above.
      </div>
    </div>
  );
};

const AdminClaims = () => {
  const [claims, setClaims]     = React.useState([]);
  const [loading, setLoading]   = React.useState(true);
  const [platform, setPlatform] = React.useState("");
  const [search, setSearch]     = React.useState("");

  const authHeader = () => ({ Authorization: `Bearer ${localStorage.getItem("cc_token")}` });

  const load = (plat) => {
    setLoading(true);
    const qs = plat ? `?platform=${plat}` : "";
    fetch(`/api/admin/claims${qs}`, { headers: authHeader() })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d?.claims) setClaims(d.claims); setLoading(false); })
      .catch(() => setLoading(false));
  };
  React.useEffect(() => load(platform), [platform]);

  const ok    = claims.filter(c => c.status === "claimed").length;
  const total = claims.length;
  const rate  = total ? Math.round(ok / total * 100) : 0;

  const filtered = claims.filter(c => {
    if (!search) return true;
    const q = search.toLowerCase();
    return (c.code || "").includes(q) || (c.username || "").includes(q) || (c.platform || "").includes(q);
  });

  return (
    <div className="stack-lg">
      <div className="row between" style={{ flexWrap: "wrap", gap: 12 }}>
        <div>
          <span className="eyebrow">admin · claim history</span>
          <h2 style={{ fontSize: 24, marginTop: 4 }}>
            {loading ? "…" : `${ok} / ${total} claimed · ${rate}%`}
          </h2>
        </div>
        <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
          {["", "stake_com", "stake_us", "shuffle", "thrill"].map(p => (
            <Btn key={p} sm kind={platform === p ? "primary" : "ghost"} onClick={() => setPlatform(p)}>
              {p ? PLAT_LABEL[p] : "All"}
            </Btn>
          ))}
          <Btn sm onClick={() => load(platform)}><Icon name="refresh"/> Refresh</Btn>
        </div>
      </div>

      <Card style={{ padding: 0 }}>
        <div style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)" }}>
          <Input placeholder="search code, user, platform…" value={search}
            onChange={e => setSearch(e.target.value)} style={{ width: "100%" }} />
        </div>
        {loading ? <p className="muted" style={{ padding: 20 }}>Loading…</p> : (
          <div style={{ overflowX: "auto" }}>
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
              <thead>
                <tr style={{ borderBottom: "1px solid var(--border)", textAlign: "left" }}>
                  {["Status", "Code", "User", "Platform", "Value", "Speed", "Time"].map(h => (
                    <th key={h} style={{ padding: "8px 12px", color: "var(--text-3)", fontWeight: 500, whiteSpace: "nowrap" }}>{h}</th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {filtered.map(c => {
                  const ok = c.status === "claimed";
                  const ts = c.time ? new Date(c.time).toLocaleString([], { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "—";
                  const speed = c.elapsed_ms ? `${c.elapsed_ms}ms` : "—";
                  const val = c.value && c.currency ? `${c.value} ${c.currency}` : (c.message && !ok ? c.message : "—");
                  return (
                    <tr key={c.id} style={{ borderBottom: "1px solid var(--border)" }}>
                      <td style={{ padding: "8px 12px" }}>
                        <Pill kind={ok ? "ok" : "err"} dot>{ok ? "claimed" : "failed"}</Pill>
                      </td>
                      <td style={{ padding: "8px 12px", fontFamily: "var(--font-mono)", fontSize: 12 }}>{c.code}</td>
                      <td style={{ padding: "8px 12px", color: "var(--text-2)" }}>{c.username}</td>
                      <td style={{ padding: "8px 12px", color: "var(--text-3)" }}>{PLAT_LABEL[c.platform] || c.platform}</td>
                      <td style={{ padding: "8px 12px", color: ok ? "var(--ok)" : "var(--err)", fontSize: 12 }}>{val}</td>
                      <td style={{ padding: "8px 12px", color: c.elapsed_ms < 400 ? "var(--ok)" : "var(--text-3)", fontSize: 12 }}>{speed}</td>
                      <td style={{ padding: "8px 12px", color: "var(--text-3)", fontSize: 12, whiteSpace: "nowrap" }}>{ts}</td>
                    </tr>
                  );
                })}
                {!filtered.length && (
                  <tr><td colSpan={7} style={{ padding: 20, color: "var(--text-3)", textAlign: "center" }}>No claims found.</td></tr>
                )}
              </tbody>
            </table>
          </div>
        )}
      </Card>
    </div>
  );
};

const AdminPromos = () => {
  const [promos, setPromos]   = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [form, setForm]       = React.useState({ code: "", promo_type: "free_days", value: "7", max_uses: "", note: "" });
  const [saving, setSaving]   = React.useState(false);
  const [err, setErr]         = React.useState("");

  const authHeader = () => ({ Authorization: `Bearer ${localStorage.getItem("cc_token")}`, "content-type": "application/json" });

  const load = () => {
    setLoading(true);
    fetch("/api/admin/promos", { headers: authHeader() })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d?.promos) setPromos(d.promos); setLoading(false); })
      .catch(() => setLoading(false));
  };
  React.useEffect(load, []);

  const create = async () => {
    setErr(""); setSaving(true);
    try {
      const r = await fetch("/api/admin/promos", {
        method: "POST", headers: authHeader(),
        body: JSON.stringify({
          code: form.code.toUpperCase(),
          promo_type: form.promo_type,
          value: parseInt(form.value) || 0,
          max_uses: form.max_uses ? parseInt(form.max_uses) : null,
          note: form.note,
        }),
      });
      const d = await r.json();
      if (d.error) { setErr(d.error); } else { setForm({ code: "", promo_type: "free_days", value: "7", max_uses: "", note: "" }); load(); }
    } catch (_) { setErr("Network error"); }
    setSaving(false);
  };

  const deactivate = async (id) => {
    await fetch(`/api/admin/promos/${id}`, { method: "DELETE", headers: authHeader() });
    load();
  };

  const PRESET_TYPES = [
    { type: "free_days", value: 7,  label: "7 days free" },
    { type: "discount_pct", value: 50, label: "50% off" },
  ];

  return (
    <div className="stack-lg">
      <div>
        <span className="eyebrow">admin · promos</span>
        <h2 style={{ fontSize: 24, marginTop: 4 }}>Promo Codes</h2>
      </div>

      <Card>
        <div style={{ fontWeight: 600, marginBottom: 14 }}>Create promo</div>
        <div className="row" style={{ gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
          {PRESET_TYPES.map(p => (
            <Btn key={p.label} sm kind={form.promo_type === p.type && parseInt(form.value) === p.value ? "primary" : "ghost"}
              onClick={() => setForm(f => ({ ...f, promo_type: p.type, value: String(p.value) }))}>
              {p.label}
            </Btn>
          ))}
        </div>
        <div className="row" style={{ gap: 8, flexWrap: "wrap", alignItems: "flex-end" }}>
          <div style={{ flex: "1 1 130px" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>Code</div>
            <Input value={form.code} onChange={e => setForm(f => ({ ...f, code: e.target.value.toUpperCase() }))}
              placeholder="e.g. WELCOME7" style={{ textTransform: "uppercase", letterSpacing: ".06em" }} />
          </div>
          <div style={{ flex: "0 0 120px" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>Type</div>
            <select value={form.promo_type} onChange={e => setForm(f => ({ ...f, promo_type: e.target.value }))}
              style={{ width: "100%", fontSize: 13, padding: "8px 10px", background: "var(--surf-2)", color: "var(--text)", border: "1px solid var(--border-2)", borderRadius: "var(--radius-sm)" }}>
              <option value="free_days">Free days</option>
              <option value="discount_pct">% Discount</option>
            </select>
          </div>
          <div style={{ flex: "0 0 80px" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>{form.promo_type === "free_days" ? "Days" : "% off"}</div>
            <Input value={form.value} onChange={e => setForm(f => ({ ...f, value: e.target.value }))} type="number" min="1" />
          </div>
          <div style={{ flex: "0 0 80px" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>Max uses</div>
            <Input value={form.max_uses} onChange={e => setForm(f => ({ ...f, max_uses: e.target.value }))} placeholder="∞" type="number" min="1" />
          </div>
          <div style={{ flex: "1 1 150px" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>Note (admin only)</div>
            <Input value={form.note} onChange={e => setForm(f => ({ ...f, note: e.target.value }))} placeholder="optional memo" />
          </div>
          <Btn kind="primary" disabled={saving || !form.code || !form.value} onClick={create}>
            {saving ? "…" : "Create"}
          </Btn>
        </div>
        {err && <p style={{ color: "var(--err)", fontSize: 13, marginTop: 8 }}>{err}</p>}
      </Card>

      <Card style={{ padding: 0 }}>
        {loading ? <p className="muted" style={{ padding: 20 }}>Loading…</p> : (
          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
            <thead>
              <tr style={{ borderBottom: "1px solid var(--border)", textAlign: "left" }}>
                {["Code", "Type", "Value", "Used", "Max", "Note", ""].map(h => (
                  <th key={h} style={{ padding: "8px 12px", color: "var(--text-3)", fontWeight: 500 }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {promos.map(p => (
                <tr key={p.id} style={{ borderBottom: "1px solid var(--border)", opacity: p.active ? 1 : 0.45 }}>
                  <td style={{ padding: "8px 12px", fontFamily: "var(--font-mono)", letterSpacing: ".06em", fontWeight: 600 }}>{p.code}</td>
                  <td style={{ padding: "8px 12px" }}>
                    <Pill kind={p.promo_type === "free_days" ? "ok" : "warn"}>
                      {p.promo_type === "free_days" ? "free days" : "% off"}
                    </Pill>
                  </td>
                  <td style={{ padding: "8px 12px", color: "var(--accent)" }}>
                    {p.promo_type === "free_days" ? `${p.value}d` : `${p.value}%`}
                  </td>
                  <td style={{ padding: "8px 12px", color: "var(--text-2)" }}>{p.used_count}</td>
                  <td style={{ padding: "8px 12px", color: "var(--text-3)" }}>{p.max_uses ?? "∞"}</td>
                  <td style={{ padding: "8px 12px", color: "var(--text-3)", fontSize: 12 }}>{p.note || "—"}</td>
                  <td style={{ padding: "8px 12px" }}>
                    {p.active && (
                      <Btn sm kind="ghost" onClick={() => deactivate(p.id)} style={{ color: "var(--err)", borderColor: "var(--err)" }}>
                        Deactivate
                      </Btn>
                    )}
                    {!p.active && <span className="muted" style={{ fontSize: 11 }}>inactive</span>}
                  </td>
                </tr>
              ))}
              {!promos.length && (
                <tr><td colSpan={7} style={{ padding: 20, color: "var(--text-3)", textAlign: "center" }}>No promos yet.</td></tr>
              )}
            </tbody>
          </table>
        )}
      </Card>
    </div>
  );
};

// Display-only label map for the admin revenue / claims views. The bot
// stores time-pass SKUs in cc_subscriptions.plan; rolling them up under
// "Browser pass" / "API pass" / "Legacy" matches the customer-facing model.
// We don't show House Cut / Prime Cut / Grand Feast names anywhere visible.
const PLAN_LABELS = {
  free:                "Free",
  api:                 "API pass",
  // Time-pass SKUs from payments.py
  tampermonkey_24h:    "Browser pass · 24h",
  tampermonkey_7d:     "Browser pass · 7d",
  tampermonkey_30d:    "Browser pass · 30d",
  api_slot_7d:         "API pass · 7d",
  api_slot_30d:        "API pass · 30d",
  // Server-granted (no payment)
  browser_pass_grant:  "Browser pass · grant",
  api_pass_grant:      "API pass · grant",
  free_reset:          "Free (reset)",
  // Legacy tier IDs — bucketed so revenue rows still tally but don't surface
  // House Cut / Prime Cut / Grand Feast names anywhere visible.
  house_cut:    "Legacy plan",
  prime_cut:    "Legacy plan",
  grand_feast:  "Legacy plan",
  stake_com:    "Legacy plan",
  stake_us:     "Legacy plan",
  bundle:       "Legacy plan",
  shuffle:      "Legacy plan",
  wild_card:    "Legacy plan",
  stake_runner: "Legacy plan",
  god_mode:     "Legacy plan",
};
// Used by AdminRevenue to attribute MRR to a plan. Time-pass SKUs are
// one-shots, not monthly, so they show actual sticker price. Legacy tiers
// were per-slot monthly.
const PLAN_PRICES = {
  tampermonkey_24h: 5,  tampermonkey_7d: 15, tampermonkey_30d: 30,
  api_slot_7d:      20, api_slot_30d:     60,
  house_cut:        10, prime_cut:        15, grand_feast:      30,
};

const AdminRevenue = () => {
  const [rev, setRev]     = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const token = localStorage.getItem("cc_token");

  React.useEffect(() => {
    fetch("/api/admin/revenue", { headers: { Authorization: `Bearer ${token}` } })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) setRev(d); setLoading(false); })
      .catch(() => setLoading(false));
  }, []);

  if (loading) return <p className="muted" style={{ padding: 20 }}>Loading…</p>;
  if (!rev) return <p className="muted" style={{ padding: 20 }}>Failed to load revenue data.</p>;

  const byPlan = rev.by_plan || {};

  return (
    <div className="stack-lg">
      <div>
        <span className="eyebrow">admin · revenue</span>
        <h2 style={{ fontSize: 24, marginTop: 4 }}>
          ${rev.mrr.toFixed(2)} <span style={{ fontSize: 16, color: "var(--text-3)", fontWeight: 400 }}>MRR</span>
        </h2>
        <p className="muted" style={{ fontSize: 13, marginTop: 4 }}>{rev.active_subs} active subscribers · ${rev.arpu} ARPU</p>
      </div>
      <div className="grid-3">
        <Stat k="MRR" n={`$${rev.mrr.toFixed(2)}`} />
        <Stat k="active subs" n={rev.active_subs} />
        <Stat k="ARPU" n={`$${rev.arpu}`} />
      </div>
      <Card>
        <strong style={{ display: "block", marginBottom: 14 }}>Subscribers by plan</strong>
        <div className="stack">
          {Object.entries(byPlan).map(([plan, count]) => {
            const price = PLAN_PRICES[plan] || 0;
            const label = PLAN_LABELS[plan] || plan;
            const planMrr = count * price;
            return (
              <div key={plan} className="row between" style={{ fontSize: 13 }}>
                <span className="muted">{label}</span>
                <span className="row" style={{ gap: 10 }}>
                  <Pill kind={count > 0 ? "ok" : ""} dot={count > 0}>{count} users</Pill>
                  <strong>${planMrr}/mo</strong>
                </span>
              </div>
            );
          })}
        </div>
      </Card>
    </div>
  );
};

const AdminHealth = () => {
  const [stats, setStats]   = React.useState(null);
  const [services, setServices] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  const authHeader = () => ({ Authorization: `Bearer ${localStorage.getItem("cc_token")}` });

  const load = () => {
    setLoading(true);
    Promise.all([
      fetch("/api/admin/stats",    { headers: authHeader() }).then(r => r.ok ? r.json() : null),
      fetch("/api/admin/services", { headers: authHeader() }).then(r => r.ok ? r.json() : null),
    ]).then(([s, sv]) => {
      if (s)  setStats(s);
      if (sv) setServices(sv);
      setLoading(false);
    }).catch(() => setLoading(false));
  };

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

  if (loading) return <p className="muted" style={{ padding: 20 }}>Loading…</p>;
  if (!stats)  return <p className="muted" style={{ padding: 20 }}>Not authorised or failed to load.</p>;

  // turnstile pool — shape: { backend, solver_enabled, stake_com:{tokens,max},
  //   stake_us:{tokens,max}, solves:{ok,failed,last_ok_at,last_error,last_error_at} }
  const ts      = stats.turnstile || {};
  const comPool = ts.stake_com?.tokens ?? 0;
  const comMax  = ts.stake_com?.max    ?? 0;
  const usPool  = ts.stake_us?.tokens  ?? 0;
  const usMax   = ts.stake_us?.max     ?? 0;
  // Solver is "erroring" only if its most recent failure is newer than its
  // most recent success — a stale error after recovery shouldn't alarm.
  const solverErroring = !!ts.solves?.last_error &&
    (ts.solves.last_error_at || 0) > (ts.solves.last_ok_at || 0);

  // services — shape: { summary:{running,dead,cancelled}, cache_age_s, reload_users, daily_users, tasks:[…] }
  const sum   = services?.summary || { running: 0, dead: 0, cancelled: 0 };
  const tasks = services?.tasks || [];
  const fmtAgo = (epoch) => {
    if (!epoch) return "never";
    const s = Math.max(0, Math.round(Date.now() / 1000 - epoch));
    if (s < 60)    return `${s}s ago`;
    if (s < 3600)  return `${Math.round(s / 60)}m ago`;
    return `${Math.round(s / 3600)}h ago`;
  };

  return (
    <div className="stack-lg">
      <div className="row between">
        <div>
          <span className="eyebrow">admin · service health</span>
          <h2 style={{ fontSize: 24, marginTop: 4 }}>Service health</h2>
        </div>
        <span className="row" style={{ gap: 6 }}>
          <Pill kind="ok" dot>claimer · live</Pill>
          <Pill kind={stats.sse_live > 0 ? "ok" : ""} dot={stats.sse_live > 0}>{stats.sse_live} SSE</Pill>
          <Pill kind={sum.dead > 0 ? "err" : "ok"} dot>
            {sum.dead > 0 ? `${sum.dead} task(s) dead` : `${sum.running} task(s) live`}
          </Pill>
          <Pill kind={solverErroring ? "err" : "ok"} dot>
            {solverErroring ? "solver erroring" : "solver ok"}
          </Pill>
          <Btn sm onClick={load}><Icon name="refresh"/> Refresh</Btn>
        </span>
      </div>

      <div className="grid-4">
        <Stat k="total users"  n={stats.total_users} />
        <Stat k="active subs"  n={stats.active_users} />
        <Stat k="total claims" n={stats.total_claims} />
        <Stat k="success rate" n={`${stats.success_rate}%`} />
      </div>

      <Card>
        <strong>Claims by platform</strong>
        <div className="grid-4" style={{ marginTop: 12, gap: 10 }}>
          {["stake_com", "stake_us", "shuffle", "thrill"].map(p => {
            const row = (stats.by_platform || {})[p] || { claimed: 0, failed: 0 };
            const total = row.claimed + row.failed;
            const rate = total ? Math.round(row.claimed / total * 100) : 0;
            return (
              <Stat
                key={p}
                k={PLAT_LABEL[p] || p}
                n={`${row.claimed} / ${total}`}
                d={total ? `${rate}% success` : "no claims yet"}
              />
            );
          })}
        </div>
      </Card>

      <div className="grid-2">
        <Card>
          <strong>Turnstile pool</strong>
          <div className="grid-2" style={{ marginTop: 12, gap: 10 }}>
            <Stat k=".com tokens" n={`${comPool} / ${comMax}`} />
            <Stat k=".us tokens"  n={`${usPool} / ${usMax}`} />
          </div>
          <div className="divider" style={{ margin: "10px 0" }}></div>
          <div className="stack-sm" style={{ fontSize: 12 }}>
            <div className="row between">
              <span className="muted">solver backend</span>
              <strong>{ts.backend || "—"}</strong>
            </div>
            <div className="row between">
              <span className="muted">solver enabled</span>
              <Pill kind={ts.solver_enabled ? "ok" : "warn"} dot>{ts.solver_enabled ? "yes" : "no"}</Pill>
            </div>
            <div className="row between">
              <span className="muted">solves (ok / failed)</span>
              <strong>{(ts.solves?.ok ?? 0)} / {(ts.solves?.failed ?? 0)}</strong>
            </div>
            <div className="row between">
              <span className="muted">last successful solve</span>
              <strong>{fmtAgo(ts.solves?.last_ok_at)}</strong>
            </div>
            {ts.solves?.last_error && (
              <div className="row between">
                <span className="muted">last solver error</span>
                <Pill kind="err" dot>{ts.solves.last_error}</Pill>
              </div>
            )}
            <div className="row between">
              <span className="muted">user cache age</span>
              <strong>{services?.cache_age_s != null ? `${services.cache_age_s}s` : "—"}</strong>
            </div>
          </div>
        </Card>
        <Card>
          <strong>Plans breakdown</strong>
          <div className="stack" style={{ marginTop: 10 }}>
            {Object.entries(stats.by_plan || {}).map(([plan, count]) => (
              <div key={plan} className="row between" style={{ fontSize: 13 }}>
                <span className="muted">{PLAN_LABELS[plan] || plan}</span>
                <strong>{count}</strong>
              </div>
            ))}
          </div>
        </Card>
      </div>

      {/* Claim engines — reload + daily-dollar background tasks */}
      <div>
        <div className="row between" style={{ marginBottom: 10 }}>
          <div className="eyebrow">Claim engines · reload + daily-dollar</div>
          <span className="row" style={{ gap: 6 }}>
            <Pill kind="ok" dot>{sum.running} running</Pill>
            {sum.dead > 0 && <Pill kind="err" dot>{sum.dead} dead</Pill>}
            <span className="muted" style={{ fontSize: 11 }}>
              {services?.reload_users ?? 0} reload · {services?.daily_users ?? 0} daily
            </span>
          </span>
        </div>
        <Card style={{ padding: 0 }}>
          {tasks.length === 0 ? (
            <p className="muted" style={{ padding: 20, fontSize: 13 }}>
              No background tasks running — no users have reload or daily-dollar enabled.
            </p>
          ) : (
            <div style={{ overflowX: "auto" }}>
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
                <thead>
                  <tr style={{ borderBottom: "1px solid var(--border)", textAlign: "left" }}>
                    {["Engine", "Platform", "User", "State", "Last claim"].map(h => (
                      <th key={h} style={{ padding: "8px 12px", color: "var(--text-3)", fontWeight: 500, whiteSpace: "nowrap" }}>{h}</th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {tasks.map((t, i) => (
                    <tr key={i} style={{ borderBottom: "1px solid var(--border)" }}>
                      <td style={{ padding: "8px 12px", fontWeight: 600 }}>
                        {t.engine === "daily_dollar" ? "Daily dollar" : "Reload"}
                      </td>
                      <td style={{ padding: "8px 12px" }}>{PLAT_LABEL[t.platform] || t.platform}</td>
                      <td style={{ padding: "8px 12px", fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--text-3)" }}>
                        {(t.user_id || "").slice(0, 8)}
                      </td>
                      <td style={{ padding: "8px 12px" }}>
                        <Pill kind={t.state === "running" ? "ok" : t.state === "dead" ? "err" : "warn"} dot>
                          {t.state}
                        </Pill>
                      </td>
                      <td style={{ padding: "8px 12px", color: "var(--text-3)" }}>{fmtAgo(t.last_claim)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </Card>
      </div>
    </div>
  );
};

Object.assign(window, { Dashboard });
