/* settings.jsx — Channel category management + scraper status + Phase-2 whitelist */

function ScraperStatusCard({ status, currentServer }) {
  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: "auto 1fr auto",
      alignItems: "center",
      gap: 14,
      padding: "14px 16px",
      background: "var(--bg-2)",
      border: "1px solid var(--border-1)",
      borderRadius: "var(--radius)",
      marginBottom: 14,
    }}>
      <div style={{
        width: 38, height: 38, borderRadius: 8,
        background: "var(--accent-glow)", color: "var(--accent)",
        display: "grid", placeItems: "center",
      }}>{I("zap", { size: 18 })}</div>
      <div>
        <div style={{ fontWeight: 600, fontSize: 14 }}>SinuxMod · trade signal listener</div>
        <div className="mono" style={{ fontSize: 11, color: "var(--fg-2)", marginTop: 2 }}>
          {currentServer ? <>watching <strong style={{ color: "var(--fg-1)" }}>{currentServer.name}</strong> · guild_id <span style={{ color: "var(--fg-1)" }}>{currentServer.guild_id}</span></> : "no server selected"}
        </div>
      </div>
      <StatusPill status={status} />
    </div>
  );
}

function AddChannelForm({ analysts, currentServer, onAdded }) {
  const [open, setOpen] = useState(false);
  const [channelId, setChannelId] = useState("");
  const [name, setName] = useState("");
  const [category, setCategory] = useState("signals");
  const [ownerUserId, setOwnerUserId] = useState("");
  const [instMode, setInstMode] = useState("any");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  if (!currentServer) return null;

  async function submit(e) {
    e.preventDefault();
    setErr(null);
    if (!channelId.trim() || !name.trim()) { setErr("channel ID and name are required"); return; }
    setBusy(true);
    try {
      const r = await tapeFetch(apiBase() + "/api/channels", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          channel_id: channelId.trim(),
          guild_id: currentServer.guild_id,
          name: name.trim(),
          category,
          owner_user_id: ownerUserId || null,
          instrument_mode: instMode,
        }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "request failed");
      onAdded && onAdded(j.channel);
      toast.success("Channel added");
      setChannelId(""); setName(""); setOwnerUserId(""); setCategory("signals"); setInstMode("any");
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }

  if (!open) {
    return (
      <button className="btn primary" style={{ marginBottom: 18 }} onClick={() => setOpen(true)}>
        {I("plus", { size: 14 })} Add channel
      </button>
    );
  }

  return (
    <form onSubmit={submit} className="form-card">
      <div className="form-card-hdr">
        <h3>Add channel</h3>
        <button type="button" className="icon-btn" title="Close" onClick={() => setOpen(false)}>{I("close", { size: 16 })}</button>
      </div>
      <div className="form-grid" style={{ gridTemplateColumns: "1.3fr 1.4fr 1fr 1fr 1.1fr" }}>
        <div className="field">
          <label className="field-label">Channel ID</label>
          <input className="input mono" placeholder="Discord channel ID" value={channelId} onChange={e => setChannelId(e.target.value)} style={{ fontSize: 12 }} />
        </div>
        <div className="field">
          <label className="field-label">Name</label>
          <input className="input" placeholder="e.g. dev-alerts" value={name} onChange={e => setName(e.target.value)} />
        </div>
        <div className="field">
          <label className="field-label">Category</label>
          <select className="select" value={category} onChange={e => setCategory(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
            <option value="signals">signals</option>
            <option value="analysis">analysis</option>
            <option value="chat">chat</option>
            <option value="ignore">ignore</option>
          </select>
        </div>
        <div className="field">
          <label className="field-label">Instrument</label>
          <select className="select" value={instMode} onChange={e => setInstMode(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
            <option value="any">any</option>
            <option value="options">options only</option>
            <option value="shares">shares only</option>
          </select>
        </div>
        <div className="field">
          <label className="field-label">Owner (optional)</label>
          <select className="select" value={ownerUserId} onChange={e => setOwnerUserId(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
            <option value="">any author</option>
            {analysts.map(a => (
              <option key={a.discord_user_id} value={a.discord_user_id}>{a.handle}{a.is_bot ? " (bot)" : ""}</option>
            ))}
          </select>
        </div>
      </div>
      {err && <div className="form-err">{I("alert", { size: 12 })} {err}</div>}
      <div className="form-actions">
        <span className="field-hint">Enable Developer Mode in Discord, then right-click a channel → Copy Channel ID.</span>
        <button type="submit" className="btn primary" disabled={busy}>
          {busy ? "Adding…" : <>{I("plus", { size: 14 })} Add channel</>}
        </button>
      </div>
    </form>
  );
}

function ChannelEditor({ channel, analysts, onCancel, onSave }) {
  const [name, setName] = useState(channel.name || "");
  const [owner, setOwner] = useState(channel.owner_user_id || "");
  const [instMode, setInstMode] = useState(channel.instrument_mode || "any");
  const [enabled, setEnabled] = useState(channel.enabled !== false);

  function save() {
    if (!name.trim()) return;
    onSave({ name: name.trim(), owner_user_id: owner || null, instrument_mode: instMode, enabled });
  }

  return (
    <div className="row-editor">
      <div className="form-grid" style={{ gridTemplateColumns: "1.4fr 1.6fr 1fr 1fr" }}>
        <div className="field">
          <label className="field-label">Channel name</label>
          <input className="input" value={name} onChange={e => setName(e.target.value)} />
        </div>
        <div className="field">
          <label className="field-label">Owner — only this user is parsed</label>
          <select className="select" value={owner} onChange={e => setOwner(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
            <option value="">any author</option>
            {analysts.map(a => (
              <option key={a.discord_user_id} value={a.discord_user_id}>{a.handle}{a.is_bot ? " (bot)" : ""}</option>
            ))}
          </select>
        </div>
        <div className="field">
          <label className="field-label">Instrument</label>
          <select className="select" value={instMode} onChange={e => setInstMode(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
            <option value="any">any</option>
            <option value="options">options only</option>
            <option value="shares">shares only</option>
          </select>
        </div>
        <div className="field">
          <label className="field-label">Status</label>
          <label className="toggle-line">
            <input type="checkbox" checked={enabled} onChange={e => setEnabled(e.target.checked)} />
            {enabled ? "watching (live)" : "paused"}
          </label>
        </div>
      </div>
      <div className="form-actions" style={{ justifyContent: "flex-end" }}>
        <button className="btn" onClick={onCancel}>Cancel</button>
        <button className="btn primary" onClick={save}>{I("check", { size: 14 })} Save changes</button>
      </div>
    </div>
  );
}

function ChannelsScreen({ channels, setChannels, analysts, currentServer }) {
  const [editingId, setEditingId] = useState(null);
  const ownerByUserId = useMemo(() => {
    const m = new Map();
    for (const a of analysts) m.set(a.discord_user_id, a);
    return m;
  }, [analysts]);

  function onChannelAdded(row) {
    // Normalize like app.jsx does
    const norm = {
      id: row.channel_id,
      name: row.name,
      category: row.category,
      instrument_mode: row.instrument_mode || "any",
      msgs: 0,
      owner_user_id: row.owner_user_id,
      enabled: row.enabled !== false,
    };
    // Replace if exists, else prepend
    setChannels(prev => {
      const exists = prev.some(c => c.id === norm.id);
      return exists ? prev.map(c => c.id === norm.id ? norm : c) : [norm, ...prev];
    });
  }

  async function removeChannel(id) {
    if (!(await confirmDialog({ title: "Stop watching channel", message: "The bot will stop parsing this channel.", confirmLabel: "Stop watching", danger: true }))) return;
    setChannels(prev => prev.filter(c => c.id !== id));
    try {
      await tapeFetch(apiBase() + "/api/channels/" + id, { method: "DELETE" });
      toast.success("Channel removed");
    } catch (err) {
      console.error("[sinux-signals] channel delete failed:", err);
      toast.error("Couldn't remove channel");
    }
  }

  async function setCat(id, cat) {
    setChannels(channels.map(c => c.id === id ? { ...c, category: cat } : c));
    try {
      await tapeFetch(apiBase() + "/api/channels/" + id, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ category: cat }),
      });
    } catch (err) {
      console.error("[sinux-signals] channel update failed:", err);
    }
  }

  async function saveChannel(id, patch) {
    setChannels(prev => prev.map(c => c.id === id ? { ...c, ...patch } : c));
    setEditingId(null);
    try {
      await tapeFetch(apiBase() + "/api/channels/" + id, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(patch),
      });
    } catch (err) {
      console.error("[sinux-signals] channel update failed:", err);
    }
  }

  const grouped = {
    signals: channels.filter(c => c.category === "signals"),
    analysis: channels.filter(c => c.category === "analysis"),
    chat: channels.filter(c => c.category === "chat"),
    ignore: channels.filter(c => c.category === "ignore"),
  };

  return (
    <div>
      <h2>Channel categories</h2>
      <p className="sub">Assign each Discord channel to a priority tier. Signals are parsed always · Analysis adds context · Chat is parsed only for whitelisted users.</p>

      <ScraperStatusCard status="ready" currentServer={currentServer} />

      <AddChannelForm analysts={analysts} currentServer={currentServer} onAdded={onChannelAdded} />

      {channels.length === 0 && (
        <div className="empty" style={{ padding: 40 }}>
          No channels registered for this server yet. Use the form above to add one.
        </div>
      )}

      {[
        ["signals",  "Signals",  "Highest priority. Every message is parsed and logged as a trade event.", "var(--accent)"],
        ["analysis", "Analysis", "Parsed with full context. Trade rationale and macro takes go here.", "var(--c-watch)"],
        ["chat",     "Chat",     "High noise. Only whitelisted users parsed.", "var(--fg-2)"],
        ["ignore",   "Ignore",   "Bots, screeners, off-topic. Skipped entirely.", "var(--fg-3)"],
      ].filter(([k]) => grouped[k].length > 0).map(([k, label, desc, col]) => (
        <div key={k} className="cat-group">
          <div className="cat-group-hdr">
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <span className="cat-name">
                <span className="dot" style={{ background: col, color: col, margin: 0 }} />{label}
              </span>
              <span className="cat-count">{grouped[k].length}</span>
            </div>
            <div className="cat-desc">{desc}</div>
          </div>
          <div className="rows">
            {grouped[k].map(c => {
              const owner = c.owner_user_id ? ownerByUserId.get(c.owner_user_id) : null;
              return (
                <Fragment key={c.id}>
                  <div className="channel-row">
                    <span className="hash">#</span>
                    <div style={{ minWidth: 0 }}>
                      <div className="name">{c.name}</div>
                      <div className="id">
                        {c.id}
                        {c.instrument_mode && c.instrument_mode !== "any" && <> · <span style={{ color: "var(--accent)" }}>{c.instrument_mode}</span></>}
                        {owner && <> · owner <span style={{ color: "var(--fg-1)" }}>{owner.handle}{owner.is_bot ? " (bot)" : ""}</span></>}
                      </div>
                    </div>
                    <div className="cat-select">
                      {["signals", "analysis", "chat", "ignore"].map(cat => (
                        <button key={cat} className={cx(c.category === cat && "on")} onClick={() => setCat(c.id, cat)}>{cat}</button>
                      ))}
                    </div>
                    <span className="live-dot" style={{ color: c.enabled ? "var(--c-up)" : "var(--fg-3)" }}>
                      <span className="d" />{c.enabled ? "live" : "off"}
                    </span>
                    <div className="row-actions">
                      <button
                        className={cx("icon-btn", editingId === c.id && "active")}
                        title="Edit channel"
                        onClick={() => setEditingId(editingId === c.id ? null : c.id)}
                      >{I("edit", { size: 15 })}</button>
                      <button
                        className="icon-btn danger"
                        title="Remove channel"
                        onClick={(e) => { e.stopPropagation(); removeChannel(c.id); }}
                      >{I("trash", { size: 15 })}</button>
                    </div>
                  </div>
                  {editingId === c.id && (
                    <ChannelEditor
                      channel={c}
                      analysts={analysts}
                      onCancel={() => setEditingId(null)}
                      onSave={(patch) => saveChannel(c.id, patch)}
                    />
                  )}
                </Fragment>
              );
            })}
          </div>
        </div>
      ))}
    </div>
  );
}

function AddAnalystForm({ currentServer, onAdded }) {
  const [open, setOpen] = useState(false);
  const [discordUserId, setDiscordUserId] = useState("");
  const [handle, setHandle] = useState("");
  const [priority, setPriority] = useState("core");
  const [scope, setScope] = useState(["signals", "analysis"]);
  const [isBot, setIsBot] = useState(false);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  if (!currentServer) return null;

  function toggleScope(cat) {
    setScope(prev => prev.includes(cat) ? prev.filter(c => c !== cat) : [...prev, cat]);
  }

  async function submit(e) {
    e.preventDefault();
    setErr(null);
    if (!discordUserId.trim() || !handle.trim()) { setErr("Discord user ID and handle are required"); return; }
    setBusy(true);
    try {
      const r = await tapeFetch(apiBase() + "/api/analysts", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          guild_id: currentServer.guild_id,
          discord_user_id: discordUserId.trim(),
          handle: handle.trim(),
          priority,
          channel_scope: scope,
          is_bot: isBot,
        }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "request failed");
      onAdded && onAdded(j.analyst);
      toast.success(`Added ${j.analyst.handle}`);
      setDiscordUserId(""); setHandle(""); setPriority("core"); setScope(["signals","analysis"]); setIsBot(false);
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }

  if (!open) {
    return (
      <button className="btn primary" style={{ marginBottom: 18 }} onClick={() => setOpen(true)}>
        {I("plus", { size: 14 })} Add analyst
      </button>
    );
  }

  return (
    <form onSubmit={submit} className="form-card">
      <div className="form-card-hdr">
        <h3>Add analyst</h3>
        <button type="button" className="icon-btn" title="Close" onClick={() => setOpen(false)}>{I("close", { size: 16 })}</button>
      </div>
      <div className="form-grid" style={{ gridTemplateColumns: "1.4fr 1fr 1fr" }}>
        <div className="field">
          <label className="field-label">Discord user ID</label>
          <input className="input mono" placeholder="Discord user ID" value={discordUserId} onChange={e => setDiscordUserId(e.target.value)} style={{ fontSize: 12 }} />
        </div>
        <div className="field">
          <label className="field-label">Handle</label>
          <input className="input" placeholder="e.g. CHARAN" value={handle} onChange={e => setHandle(e.target.value.toUpperCase())} />
        </div>
        <div className="field">
          <label className="field-label">Priority</label>
          <select className="select" value={priority} onChange={e => setPriority(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
            <option value="core">core</option>
            <option value="secondary">secondary</option>
            <option value="watchlist">watchlist</option>
          </select>
        </div>
      </div>
      <div className="form-scope">
        <span className="scope-title">Parses in</span>
        {["signals", "analysis", "chat"].map(cat => (
          <label key={cat}>
            <input type="checkbox" checked={scope.includes(cat)} onChange={() => toggleScope(cat)} />
            {cat}
          </label>
        ))}
        <span style={{ flex: 1 }} />
        <label>
          <input type="checkbox" checked={isBot} onChange={e => setIsBot(e.target.checked)} />
          is a bot
        </label>
      </div>
      {err && <div className="form-err">{I("alert", { size: 12 })} {err}</div>}
      <div className="form-actions">
        <span className="field-hint">Enable Developer Mode in Discord, then right-click a user → Copy User ID.</span>
        <button type="submit" className="btn primary" disabled={busy}>
          {busy ? "Adding…" : <>{I("plus", { size: 14 })} Add analyst</>}
        </button>
      </div>
    </form>
  );
}

function AnalystEditor({ analyst, onCancel, onSave }) {
  const [handle, setHandle] = useState(analyst.handle || "");
  const [scope, setScope] = useState(analyst.channels || []);
  const [isBot, setIsBot] = useState(!!analyst.is_bot);
  const [enabled, setEnabled] = useState(analyst.enabled !== false);

  function toggleScope(cat) {
    setScope(prev => prev.includes(cat) ? prev.filter(c => c !== cat) : [...prev, cat]);
  }
  function save() {
    if (!handle.trim()) return;
    onSave({ handle: handle.trim().toUpperCase(), channel_scope: scope, is_bot: isBot, enabled });
  }

  return (
    <div className="row-editor">
      <div className="form-grid" style={{ gridTemplateColumns: "1fr 1fr" }}>
        <div className="field">
          <label className="field-label">Handle</label>
          <input className="input" value={handle} onChange={e => setHandle(e.target.value.toUpperCase())} />
        </div>
        <div className="field">
          <label className="field-label">Discord user ID (fixed)</label>
          <input className="input mono" value={analyst.discord_user_id} disabled style={{ fontSize: 12, opacity: 0.65 }} />
        </div>
      </div>
      <div className="form-scope">
        <span className="scope-title">Parses in</span>
        {["signals", "analysis", "chat"].map(cat => (
          <label key={cat}>
            <input type="checkbox" checked={scope.includes(cat)} onChange={() => toggleScope(cat)} />
            {cat}
          </label>
        ))}
        <span style={{ flex: 1 }} />
        <label>
          <input type="checkbox" checked={isBot} onChange={e => setIsBot(e.target.checked)} />
          is a bot
        </label>
        <label>
          <input type="checkbox" checked={enabled} onChange={e => setEnabled(e.target.checked)} />
          active
        </label>
      </div>
      <div className="form-actions" style={{ justifyContent: "flex-end" }}>
        <button className="btn" onClick={onCancel}>Cancel</button>
        <button className="btn primary" onClick={save}>{I("check", { size: 14 })} Save changes</button>
      </div>
    </div>
  );
}

function WhitelistScreen({ analysts, events, currentServer, setAnalysts }) {
  const [editingId, setEditingId] = useState(null);
  const [tier, setTier] = useState(() => {
    const m = {};
    analysts.forEach(a => { m[a.handle] = a.priority; });
    return m;
  });

  useEffect(() => {
    const m = {};
    analysts.forEach(a => { m[a.handle] = a.priority; });
    setTier(m);
  }, [analysts]);

  const eventCounts = useMemo(() => {
    const m = new Map();
    for (const e of events) m.set(e.analyst, (m.get(e.analyst) || 0) + 1);
    return m;
  }, [events]);

  async function setTierFor(analyst, t) {
    setTier(prev => ({ ...prev, [analyst.handle]: t }));
    try {
      await tapeFetch(apiBase() + "/api/analysts/" + analyst.id, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ priority: t }),
      });
    } catch (err) {
      console.error("[sinux-signals] analyst update failed:", err);
    }
  }

  async function saveAnalyst(a, patch) {
    // patch may include: handle, channel_scope, is_bot, enabled
    setAnalysts && setAnalysts(prev => prev.map(x => x.id === a.id ? {
      ...x,
      handle: patch.handle !== undefined ? patch.handle : x.handle,
      channels: patch.channel_scope !== undefined ? patch.channel_scope : x.channels,
      is_bot: patch.is_bot !== undefined ? patch.is_bot : x.is_bot,
      enabled: patch.enabled !== undefined ? patch.enabled : x.enabled,
    } : x));
    setEditingId(null);
    try {
      await tapeFetch(apiBase() + "/api/analysts/" + a.id, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(patch),
      });
    } catch (err) {
      console.error("[sinux-signals] analyst update failed:", err);
    }
  }

  function onAnalystAdded(row) {
    const norm = {
      handle: row.handle,
      color: row.color || "var(--a-1)",
      priority: row.priority || "core",
      channels: row.channel_scope || [],
      events: 0,
      discord_user_id: row.discord_user_id,
      is_bot: row.is_bot,
      enabled: row.enabled !== false,
      id: row.id,
    };
    setAnalysts && setAnalysts(prev => {
      const exists = prev.some(a => a.id === norm.id);
      return exists ? prev.map(a => a.id === norm.id ? norm : a) : [...prev, norm];
    });
  }

  async function removeAnalyst(a) {
    if (!(await confirmDialog({ title: "Remove analyst", message: `Remove ${a.handle} from this server's analyst registry?`, confirmLabel: "Remove", danger: true }))) return;
    setAnalysts && setAnalysts(prev => prev.filter(x => x.id !== a.id));
    try {
      await tapeFetch(apiBase() + "/api/analysts/" + a.id, { method: "DELETE" });
      toast.success(`Removed ${a.handle}`);
    } catch (err) {
      console.error("[sinux-signals] analyst delete failed:", err);
      toast.error("Couldn't remove analyst");
    }
  }

  return (
    <div>
      <h2>Analyst registry</h2>
      <p className="sub">Configured analysts for the selected server. Bots are allowed when explicitly registered. Tier controls weighting in chat channels.</p>

      <div style={{ display: "flex", gap: 8, marginBottom: 14, alignItems: "center" }}>
        <span className="mono" style={{ fontSize: 11, color: "var(--fg-2)", letterSpacing: ".10em", textTransform: "uppercase" }}>Tiers</span>
        <span className="pill high">Core — every msg</span>
        <span className="pill medium">Secondary — high-conf only</span>
        <span className="pill low">Watchlist — mute</span>
      </div>

      <AddAnalystForm currentServer={currentServer} onAdded={onAnalystAdded} />

      {analysts.length === 0 && (
        <div className="empty" style={{ padding: 40 }}>
          No analysts registered yet. Use the form above to add one.
        </div>
      )}

      <div className="panel">
        {analysts.map(a => (
          <Fragment key={a.id || a.handle}>
            <div className="user-row">
              <div className="avatar" style={{ background: a.color, color: "var(--bg-0)" }}>{a.handle[0]}</div>
              <div style={{ minWidth: 0 }}>
                <div className="uname">
                  {a.handle}
                  {a.is_bot && <span style={{ marginLeft: 8, fontFamily: "var(--f-mono)", fontSize: 10, color: "var(--c-watch)", letterSpacing: ".08em" }}>BOT</span>}
                  {a.enabled === false && <span style={{ marginLeft: 8, fontFamily: "var(--f-mono)", fontSize: 10, color: "var(--fg-3)", letterSpacing: ".08em" }}>DISABLED</span>}
                </div>
                <div className="mono" style={{ fontSize: 11, color: "var(--fg-3)", marginTop: 2 }}>
                  {a.discord_user_id} · scope: {a.channels.join(" · ") || "—"}
                </div>
              </div>
              <div style={{ display: "flex", gap: 4 }}>
                {["core", "secondary", "watchlist"].map(t => (
                  <button key={t} className="btn" style={{
                    padding: "6px 10px", fontSize: 11,
                    fontFamily: "var(--f-mono)", textTransform: "uppercase", letterSpacing: ".10em",
                    background: tier[a.handle] === t ? "var(--accent-glow)" : "var(--bg-2)",
                    color: tier[a.handle] === t ? "var(--accent)" : "var(--fg-2)",
                    borderColor: tier[a.handle] === t ? "var(--accent-dim)" : "var(--border-1)",
                  }} onClick={() => setTierFor(a, t)}>{t}</button>
                ))}
              </div>
              <span className="mono" style={{ fontSize: 11, color: "var(--fg-2)" }}>{eventCounts.get(a.handle) || 0} ev</span>
              <div className="row-actions">
                <button
                  className={cx("icon-btn", editingId === a.id && "active")}
                  title="Edit analyst"
                  onClick={() => setEditingId(editingId === a.id ? null : a.id)}
                >{I("edit", { size: 15 })}</button>
                <button
                  className="icon-btn danger"
                  title="Remove analyst"
                  onClick={() => removeAnalyst(a)}
                >{I("trash", { size: 15 })}</button>
              </div>
            </div>
            {editingId === a.id && (
              <AnalystEditor
                analyst={a}
                onCancel={() => setEditingId(null)}
                onSave={(patch) => saveAnalyst(a, patch)}
              />
            )}
          </Fragment>
        ))}
      </div>
    </div>
  );
}

function PipelineScreen({ events, status }) {
  const cutoff = Date.now() - 86400000;
  const recent = events.filter(e => e.ts >= cutoff);

  return (
    <div>
      <h2>Pipeline status</h2>
      <p className="sub">Live view of the scrape → parse → store pipeline. Each stage runs as an independent worker.</p>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 10, marginBottom: 24 }}>
        {[
          ["scraper", status?.phase === "polling" ? "live · api" : status?.phase === "error" ? "polling error" : "idle", status?.count ?? 0, "events seen", status?.phase === "error" ? "var(--c-down)" : "var(--accent)"],
          ["parser",  "claude-haiku-4-5", recent.length, "events / 24h", "var(--c-watch)"],
          ["prices",  "Alpha Vantage", "—", "via backend proxy", "var(--c-trim)"],
          ["api",     status?.phase === "error" ? "endpoint error" : "online", "—", "supabase backed", status?.phase === "error" ? "var(--c-down)" : "var(--c-up)"],
        ].map(([k, sub, n, unit, c]) => (
          <div key={k} className="stat" style={{ padding: 14 }}>
            <div className="lbl" style={{ color: c }}><span className="dot" style={{ background: c, color: c, width: 6, height: 6 }} />{k}</div>
            <div className="val">{typeof n === "number" ? n.toLocaleString() : n}</div>
            <div className="delta">{sub} · {unit}</div>
          </div>
        ))}
      </div>

      <div style={{
        background: "var(--bg-2)", border: "1px solid var(--border-1)",
        borderRadius: "var(--radius)", padding: "16px 18px", marginBottom: 24,
      }}>
        <h3 style={{ margin: "0 0 8px", fontSize: 14 }}>Backend keys</h3>
        <p className="sub" style={{ fontSize: 12, marginBottom: 8 }}>
          Both <code className="mono" style={{ color: "var(--fg-1)" }}>SUPABASE_KEY</code> and <code className="mono" style={{ color: "var(--fg-1)" }}>ALPHA_VANTAGE_KEY</code> are set as environment variables on the API server (Vercel) — not in the browser. Update them in your Vercel project Settings → Environment Variables.
        </p>
        {status?.error && (
          <div style={{
            marginTop: 8, padding: "8px 10px",
            background: "color-mix(in oklab, var(--c-down) 12%, transparent)",
            border: "1px solid color-mix(in oklab, var(--c-down) 40%, transparent)",
            borderRadius: 4, fontFamily: "var(--f-mono)", fontSize: 11, color: "var(--c-down)",
          }}>
            {I("alert", { size: 12 })} last poll failed: {status.error}
          </div>
        )}
      </div>

      <div className="panel">
        <div className="panel-hdr"><div className="panel-title">Run log</div><div className="panel-meta">last 12 events</div></div>
        <div className="scroll-y" style={{ maxHeight: 400 }}>
          {events.length === 0 && (
            <div className="empty" style={{ padding: 40 }}>
              No events parsed yet. Once your Discord bot writes the first <code className="mono">tape_trade_events</code> row it will show up here.
            </div>
          )}
          {events.slice(0, 12).map(e => (
            <div key={e.id} style={{
              padding: "10px 18px",
              borderBottom: "1px solid var(--border-1)",
              fontFamily: "var(--f-mono)", fontSize: 11,
              display: "grid", gridTemplateColumns: "100px 1fr",
              gap: 12, color: "var(--fg-1)",
            }}>
              <span style={{ color: "var(--fg-3)" }}>{fmtTime(e.ts, {short:true})}</span>
              <span>
                <span style={{ color: "var(--accent)" }}>scraper</span> received · <span style={{ color: "var(--c-watch)" }}>parser</span> → <span style={{ color: "var(--fg-0)" }}>{e.action.toUpperCase()}</span> <span className="ticker">${e.ticker}</span> · stored as <span style={{ color: "var(--fg-0)" }}>{e.id}</span>
              </span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function AddServerForm({ onAdded, onSelectServer }) {
  const [open, setOpen] = useState(false);
  const [guildId, setGuildId] = useState("");
  const [name, setName] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  async function submit(e) {
    e.preventDefault();
    setErr(null);
    if (!guildId.trim() || !name.trim()) { setErr("server ID and name are required"); return; }
    setBusy(true);
    try {
      const r = await tapeFetch(apiBase() + "/api/servers", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ guild_id: guildId.trim(), name: name.trim() }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "request failed");
      onAdded && onAdded(j.server);
      onSelectServer && onSelectServer(j.server.guild_id);
      toast.success(`Added ${j.server.name}`);
      setGuildId(""); setName(""); setOpen(false);
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }

  if (!open) {
    return (
      <button className="btn primary" style={{ marginBottom: 18 }} onClick={() => setOpen(true)}>
        {I("plus", { size: 14 })} Add server
      </button>
    );
  }

  return (
    <form onSubmit={submit} className="form-card">
      <div className="form-card-hdr">
        <h3>Add Discord server</h3>
        <button type="button" className="icon-btn" title="Close" onClick={() => setOpen(false)}>{I("close", { size: 16 })}</button>
      </div>
      <div className="form-grid" style={{ gridTemplateColumns: "1.2fr 1.6fr" }}>
        <div className="field">
          <label className="field-label">Server (guild) ID</label>
          <input className="input mono" placeholder="e.g. 1423793066107601030" value={guildId} onChange={e => setGuildId(e.target.value)} style={{ fontSize: 12 }} />
        </div>
        <div className="field">
          <label className="field-label">Display name</label>
          <input className="input" placeholder="e.g. Charan Invests" value={name} onChange={e => setName(e.target.value)} />
        </div>
      </div>
      {err && <div className="form-err">{I("alert", { size: 12 })} {err}</div>}
      <div className="form-actions">
        <span className="field-hint">The SinuxMod bot must already be a member of this server. Right-click the server icon → Copy Server ID (Developer Mode on).</span>
        <button type="submit" className="btn primary" disabled={busy}>
          {busy ? "Adding…" : <>{I("plus", { size: 14 })} Add server</>}
        </button>
      </div>
    </form>
  );
}

function ServerEditor({ server, onCancel, onSave }) {
  const [name, setName] = useState(server.name || "");
  const [enabled, setEnabled] = useState(server.enabled !== false);
  function save() { if (!name.trim()) return; onSave({ name: name.trim(), enabled }); }
  return (
    <div className="row-editor">
      <div className="form-grid" style={{ gridTemplateColumns: "1.6fr 1fr" }}>
        <div className="field">
          <label className="field-label">Display name</label>
          <input className="input" value={name} onChange={e => setName(e.target.value)} />
        </div>
        <div className="field">
          <label className="field-label">Status</label>
          <label className="toggle-line">
            <input type="checkbox" checked={enabled} onChange={e => setEnabled(e.target.checked)} />
            {enabled ? "enabled" : "disabled"}
          </label>
        </div>
      </div>
      <div className="form-actions" style={{ justifyContent: "flex-end" }}>
        <button className="btn" onClick={onCancel}>Cancel</button>
        <button className="btn primary" onClick={save}>{I("check", { size: 14 })} Save changes</button>
      </div>
    </div>
  );
}

function ServersScreen({ servers, setServers, onSelectServer }) {
  const [editingId, setEditingId] = useState(null);

  function onServerAdded(row) {
    setServers && setServers(prev => {
      const exists = prev.some(s => s.guild_id === row.guild_id);
      return exists ? prev.map(s => s.guild_id === row.guild_id ? row : s) : [...prev, row];
    });
  }
  async function saveServer(gid, patch) {
    setServers && setServers(prev => prev.map(s => s.guild_id === gid ? { ...s, ...patch } : s));
    setEditingId(null);
    try {
      await tapeFetch(apiBase() + "/api/servers/" + gid, {
        method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(patch),
      });
    } catch (err) { console.error("[sinux-signals] server update failed:", err); }
  }
  async function removeServer(s) {
    if (!(await confirmDialog({ title: `Remove ${s.name}?`, message: "This deletes the server and ALL of its channels, analysts and recorded events. This cannot be undone.", confirmLabel: "Delete server", danger: true }))) return;
    setServers && setServers(prev => prev.filter(x => x.guild_id !== s.guild_id));
    try {
      await tapeFetch(apiBase() + "/api/servers/" + s.guild_id, { method: "DELETE" });
      toast.success(`Removed ${s.name}`);
    } catch (err) {
      console.error("[sinux-signals] server delete failed:", err);
      toast.error("Couldn't remove server");
    }
  }

  return (
    <div>
      <h2>Servers</h2>
      <p className="sub">Discord servers Sinux Signals pulls signals from. Add a server, then configure its channels and analysts. The bot must already be a member of each server.</p>

      <AddServerForm onAdded={onServerAdded} onSelectServer={onSelectServer} />

      {(!servers || servers.length === 0) && (
        <div className="empty" style={{ padding: 40 }}>No servers yet. Use the form above to add one.</div>
      )}

      {servers && servers.length > 0 && (
        <div className="rows">
          {servers.map(s => (
            <Fragment key={s.guild_id}>
              <div className="channel-row" style={{ gridTemplateColumns: "24px minmax(0,1fr) auto auto" }}>
                <span className="hash" style={{ color: "var(--fg-2)" }}>{I("server", { size: 15 })}</span>
                <div style={{ minWidth: 0 }}>
                  <div className="name">{s.name}</div>
                  <div className="id">{s.guild_id}</div>
                </div>
                <span className="live-dot" style={{ color: s.enabled !== false ? "var(--c-up)" : "var(--fg-3)" }}>
                  <span className="d" />{s.enabled !== false ? "enabled" : "off"}
                </span>
                <div className="row-actions">
                  <button className={cx("icon-btn", editingId === s.guild_id && "active")} title="Edit server" onClick={() => setEditingId(editingId === s.guild_id ? null : s.guild_id)}>{I("edit", { size: 15 })}</button>
                  <button className="icon-btn danger" title="Remove server" onClick={() => removeServer(s)}>{I("trash", { size: 15 })}</button>
                </div>
              </div>
              {editingId === s.guild_id && (
                <ServerEditor server={s} onCancel={() => setEditingId(null)} onSave={(patch) => saveServer(s.guild_id, patch)} />
              )}
            </Fragment>
          ))}
        </div>
      )}
    </div>
  );
}

// Role + per-server scope controls, shared by the add and edit user forms.
// `actorRole` = the signed-in user's role (limits which roles they can assign).
function RoleScopeControls({ role, setRole, allServers, setAllServers, scope, setScope, servers, actorRole }) {
  const roleOpts = actorRole === "owner" ? ["admin", "editor", "viewer"]
                 : actorRole === "admin" ? ["editor", "viewer"]
                 : ["viewer"];
  const showScope = role !== "admin";
  function toggleServer(gid) {
    setScope(prev => prev.includes(gid) ? prev.filter(x => x !== gid) : [...prev, gid]);
  }
  return (
    <>
      <div className="form-grid" style={{ gridTemplateColumns: "1fr 1fr", marginTop: 12 }}>
        <div className="field">
          <label className="field-label">Role</label>
          <select className="select" value={role} onChange={e => setRole(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
            {roleOpts.map(r => <option key={r} value={r}>{r}</option>)}
          </select>
        </div>
        {showScope && (
          <div className="field">
            <label className="field-label">Server access</label>
            <label className="toggle-line">
              <input type="checkbox" checked={allServers} onChange={e => setAllServers(e.target.checked)} />
              all servers
            </label>
          </div>
        )}
      </div>
      {showScope && !allServers && (
        <div className="form-scope">
          <span className="scope-title">Servers</span>
          {(servers || []).map(s => (
            <label key={s.guild_id}>
              <input type="checkbox" checked={scope.includes(s.guild_id)} onChange={() => toggleServer(s.guild_id)} />
              {s.name}
            </label>
          ))}
          {(!servers || !servers.length) && <span className="field-hint">No servers available.</span>}
        </div>
      )}
      {role === "admin" && (
        <div className="field-hint" style={{ marginTop: 10 }}>Admins manage all servers, channels, analysts and users (editors/viewers).</div>
      )}
    </>
  );
}

function AddUserForm({ onAdded, servers, role: actorRole }) {
  const [open, setOpen] = useState(false);
  const [uid, setUid] = useState("");
  const [name, setName] = useState("");
  const [role, setRole] = useState("viewer");
  const [allServers, setAllServers] = useState(false);
  const [scope, setScope] = useState([]);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  async function submit(e) {
    e.preventDefault();
    setErr(null);
    if (!uid.trim()) { setErr("Discord user ID is required"); return; }
    setBusy(true);
    try {
      const server_scope = role === "admin" ? [] : (allServers ? ["*"] : scope);
      const r = await tapeFetch(apiBase() + "/api/users", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ discord_user_id: uid.trim(), name: name.trim(), role, server_scope }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "request failed");
      onAdded && onAdded(j.user);
      toast.success("Access granted");
      setUid(""); setName(""); setRole("viewer"); setAllServers(false); setScope([]); setOpen(false);
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }

  if (!open) {
    return (
      <button className="btn primary" style={{ marginBottom: 18 }} onClick={() => setOpen(true)}>
        {I("plus", { size: 14 })} Add user
      </button>
    );
  }

  return (
    <form onSubmit={submit} className="form-card">
      <div className="form-card-hdr">
        <h3>Grant access</h3>
        <button type="button" className="icon-btn" title="Close" onClick={() => setOpen(false)}>{I("close", { size: 16 })}</button>
      </div>
      <div className="form-grid" style={{ gridTemplateColumns: "1.2fr 1.6fr" }}>
        <div className="field">
          <label className="field-label">Discord user ID</label>
          <input className="input mono" placeholder="e.g. 1211348259470053487" value={uid} onChange={e => setUid(e.target.value)} style={{ fontSize: 12 }} />
        </div>
        <div className="field">
          <label className="field-label">Name (optional)</label>
          <input className="input" placeholder="e.g. Charan" value={name} onChange={e => setName(e.target.value)} />
        </div>
      </div>
      <RoleScopeControls role={role} setRole={setRole} allServers={allServers} setAllServers={setAllServers} scope={scope} setScope={setScope} servers={servers} actorRole={actorRole} />
      {err && <div className="form-err">{I("alert", { size: 12 })} {err}</div>}
      <div className="form-actions">
        <span className="field-hint">They sign in with Discord. Right-click a user → Copy User ID (Developer Mode on).</span>
        <button type="submit" className="btn primary" disabled={busy}>
          {busy ? "Adding…" : <>{I("plus", { size: 14 })} Grant access</>}
        </button>
      </div>
    </form>
  );
}

function UserEditor({ user, servers, role: actorRole, onCancel, onSave }) {
  const [name, setName] = useState(user.name || "");
  const [enabled, setEnabled] = useState(user.enabled !== false);
  const initScope = user.server_scope || [];
  const [role, setRole] = useState(user.role || "viewer");
  const [allServers, setAllServers] = useState(initScope.includes("*"));
  const [scope, setScope] = useState(initScope.filter(x => x !== "*"));
  function save() {
    const server_scope = role === "admin" ? [] : (allServers ? ["*"] : scope);
    onSave({ name: name.trim() || null, enabled, role, server_scope });
  }
  return (
    <div className="row-editor">
      <div className="form-grid" style={{ gridTemplateColumns: "1fr 1fr" }}>
        <div className="field">
          <label className="field-label">Name</label>
          <input className="input" value={name} onChange={e => setName(e.target.value)} />
        </div>
        <div className="field">
          <label className="field-label">Discord user ID (fixed)</label>
          <input className="input mono" value={user.discord_user_id} disabled style={{ fontSize: 12, opacity: 0.65 }} />
        </div>
      </div>
      <RoleScopeControls role={role} setRole={setRole} allServers={allServers} setAllServers={setAllServers} scope={scope} setScope={setScope} servers={servers} actorRole={actorRole} />
      <div className="form-scope">
        <span className="scope-title">Status</span>
        <label className="toggle-line">
          <input type="checkbox" checked={enabled} onChange={e => setEnabled(e.target.checked)} />
          {enabled ? "active — can sign in" : "suspended — cannot sign in"}
        </label>
      </div>
      <div className="form-actions" style={{ justifyContent: "flex-end" }}>
        <button className="btn" onClick={onCancel}>Cancel</button>
        <button className="btn primary" onClick={save}>{I("check", { size: 14 })} Save changes</button>
      </div>
    </div>
  );
}

// Access levels a Discord role can grant. "blocked" = deny everyone with the
// role (an individual Active entry still overrides it).
const GRANT_LEVELS = [["viewer", "Viewer"], ["editor", "Editor"], ["admin", "Admin"], ["blocked", "Blocked"]];

function AddRoleGrantForm({ servers, onAdded }) {
  const [open, setOpen] = useState(false);
  const [guildId, setGuildId] = useState(servers[0]?.guild_id || "");
  const [roleId, setRoleId] = useState("");
  const [roleName, setRoleName] = useState("");
  const [level, setLevel] = useState("viewer");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  // Live role picker (read from Discord via the bot). null = loading.
  const [roles, setRoles] = useState(null);
  const [rolesErr, setRolesErr] = useState(null);
  const [manual, setManual] = useState(false);

  useEffect(() => { if (!guildId && servers[0]) setGuildId(servers[0].guild_id); }, [servers]);

  // Pull the server's roles whenever the form opens or the server changes.
  useEffect(() => {
    if (!open || !guildId) return;
    let alive = true;
    setRoles(null); setRolesErr(null); setRoleId(""); setRoleName("");
    tapeFetch(apiBase() + "/api/servers/" + guildId + "/roles")
      .then(async r => { const j = await r.json(); if (!r.ok) throw new Error(j.error || "failed to load roles"); return j; })
      .then(j => { if (alive) setRoles(j.roles || []); })
      .catch(e => { if (alive) { setRolesErr(e.message); setManual(true); } });
    return () => { alive = false; };
  }, [open, guildId]);

  function pickRole(id) {
    setRoleId(id);
    const r = (roles || []).find(x => x.id === id);
    setRoleName(r ? r.name : "");
  }

  async function submit(e) {
    e.preventDefault();
    setErr(null);
    if (!guildId) { setErr("Pick a server"); return; }
    if (!roleId.trim()) { setErr("Pick a role (or enter its ID)"); return; }
    setBusy(true);
    try {
      const r = await tapeFetch(apiBase() + "/api/role-grants", {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ guild_id: guildId, role_id: roleId.trim(), role_name: roleName.trim(), level }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "request failed");
      onAdded && onAdded(j.grant);
      toast.success("Role grant saved");
      setRoleId(""); setRoleName(""); setLevel("viewer"); setOpen(false);
    } catch (e) { setErr(e.message); } finally { setBusy(false); }
  }

  if (!open) {
    return (
      <button className="btn primary" style={{ marginBottom: 18 }} onClick={() => setOpen(true)}>
        {I("plus", { size: 14 })} Add role
      </button>
    );
  }

  const toggleStyle = { background: "none", border: "none", color: "var(--accent)", cursor: "pointer", font: "inherit", fontSize: 11, padding: 0 };

  return (
    <form onSubmit={submit} className="form-card">
      <div className="form-card-hdr">
        <h3>Grant access by role</h3>
        <button type="button" className="icon-btn" title="Close" onClick={() => setOpen(false)}>{I("close", { size: 16 })}</button>
      </div>
      <div className="form-grid" style={{ gridTemplateColumns: "1fr 1fr" }}>
        <div className="field">
          <label className="field-label">Server</label>
          <select className="select" value={guildId} onChange={e => setGuildId(e.target.value)}>
            {servers.map(s => <option key={s.guild_id} value={s.guild_id}>{s.name || s.guild_id}</option>)}
          </select>
        </div>
        <div className="field">
          <label className="field-label">Access level</label>
          <select className="select" value={level} onChange={e => setLevel(e.target.value)}>
            {GRANT_LEVELS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
          </select>
        </div>
        <div className="field" style={{ gridColumn: "1 / -1" }}>
          <label className="field-label" style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
            <span>Role</span>
            <button type="button" style={toggleStyle} onClick={() => setManual(m => !m)}>
              {manual ? "pick from list" : "enter ID manually"}
            </button>
          </label>
          {!manual ? (
            <select className="select" value={roleId} onChange={e => pickRole(e.target.value)} disabled={roles === null}>
              <option value="">{roles === null ? "Loading roles…" : "Select a role…"}</option>
              {(roles || []).map(r => <option key={r.id} value={r.id}>{r.name}{r.managed ? "  (managed)" : ""}</option>)}
            </select>
          ) : (
            <div className="form-grid" style={{ gridTemplateColumns: "1.2fr 1fr", gap: 10, marginTop: 0 }}>
              <input className="input mono" placeholder="Discord role ID" value={roleId} onChange={e => setRoleId(e.target.value)} style={{ fontSize: 12 }} />
              <input className="input" placeholder="Role name (label)" value={roleName} onChange={e => setRoleName(e.target.value)} />
            </div>
          )}
          {rolesErr && <span className="field-hint" style={{ color: "var(--c-down)" }}>Couldn't load roles ({rolesErr}). Enter the ID manually.</span>}
        </div>
      </div>
      {err && <div className="form-err">{I("alert", { size: 12 })} {err}</div>}
      <div className="form-actions">
        <span className="field-hint">{manual
          ? "Server Settings → Roles → right-click a role → Copy Role ID (Developer Mode on)."
          : "Roles are read live from Discord via the bot."}</span>
        <button type="submit" className="btn primary" disabled={busy}>
          {busy ? "Saving…" : <>{I("plus", { size: 14 })} Add role grant</>}
        </button>
      </div>
    </form>
  );
}

function RoleGrants({ servers }) {
  const [grants, setGrants] = useState([]);
  const [loaded, setLoaded] = useState(false);
  const serverName = (gid) => (servers.find(s => s.guild_id === gid)?.name) || gid;

  useEffect(() => {
    tapeFetch(apiBase() + "/api/role-grants")
      .then(r => r.json())
      .then(j => { setGrants(j.grants || []); setLoaded(true); })
      .catch(() => setLoaded(true));
  }, []);

  function onAdded(g) {
    setGrants(prev => {
      const exists = prev.some(x => x.id === g.id);
      return exists ? prev.map(x => x.id === g.id ? g : x) : [...prev, g];
    });
  }
  async function setLevel(g, level) {
    setGrants(prev => prev.map(x => x.id === g.id ? { ...x, level } : x));
    try {
      await tapeFetch(apiBase() + "/api/role-grants/" + g.id, {
        method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ level }),
      });
      toast.success("Level updated");
    } catch (err) { toast.error("Couldn't update level"); }
  }
  async function remove(g) {
    if (!(await confirmDialog({ title: "Remove role grant", message: `Stop granting "${g.role_name || g.role_id}"?`, confirmLabel: "Remove", danger: true }))) return;
    setGrants(prev => prev.filter(x => x.id !== g.id));
    try {
      await tapeFetch(apiBase() + "/api/role-grants/" + g.id, { method: "DELETE" });
      toast.success("Role grant removed");
    } catch (err) { toast.error("Couldn't remove grant"); }
  }

  return (
    <div style={{ marginTop: 36 }}>
      <h2>Role access</h2>
      <p className="sub">Grant portal access to everyone holding a Discord role. The highest matching level wins, scoped to the servers a role covers. An individual entry above always overrides this — add someone as <strong>Active</strong> to allow them past a <strong>Blocked</strong> role, or <strong>Suspended</strong> to block them even though they hold a granting role.</p>

      <AddRoleGrantForm servers={servers} onAdded={onAdded} />

      {loaded && grants.length === 0 && (
        <div className="empty" style={{ padding: 30 }}>No role grants yet. Map a Discord role ID to an access level.</div>
      )}
      {grants.length > 0 && (
        <div className="rows">
          {grants.map(g => (
            <div key={g.id} className="channel-row" style={{ gridTemplateColumns: "24px minmax(0,1fr) auto auto" }}>
              <span className="hash" style={{ color: g.level === "blocked" ? "var(--c-down)" : "var(--fg-2)" }}>{I("shield", { size: 15 })}</span>
              <div style={{ minWidth: 0 }}>
                <div className="name">
                  {g.role_name || "Role"}
                  <span className="mono" style={{ fontSize: 10, color: "var(--fg-3)", marginLeft: 6 }}>{g.role_id}</span>
                </div>
                <div className="id">{serverName(g.guild_id)}</div>
              </div>
              <select className="select" value={g.level} onChange={e => setLevel(g, e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
                {GRANT_LEVELS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
              </select>
              <div className="row-actions">
                <button className="icon-btn danger" title="Remove grant" onClick={() => remove(g)}>{I("trash", { size: 15 })}</button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function AccessScreen({ servers, role }) {
  const [users, setUsers] = useState([]);
  const [loaded, setLoaded] = useState(false);
  const [editingId, setEditingId] = useState(null);
  // Which existing users you may edit/remove (matches the server-side rule).
  const canActOn = (targetRole) =>
    role === "owner" ? true : role === "admin" ? ["editor", "viewer"].includes(targetRole) : targetRole === "viewer";

  useEffect(() => {
    tapeFetch(apiBase() + "/api/users")
      .then(r => r.json())
      .then(j => { setUsers(j.users || []); setLoaded(true); })
      .catch(() => setLoaded(true));
  }, []);

  function onAdded(u) {
    setUsers(prev => {
      const exists = prev.some(x => x.discord_user_id === u.discord_user_id);
      return exists ? prev.map(x => x.discord_user_id === u.discord_user_id ? u : x) : [...prev, u];
    });
  }
  async function saveUser(id, patch) {
    setUsers(prev => prev.map(x => x.discord_user_id === id ? { ...x, ...patch } : x));
    setEditingId(null);
    try {
      await tapeFetch(apiBase() + "/api/users/" + id, {
        method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(patch),
      });
      toast.success("User updated");
    } catch (err) {
      console.error("[sinux-signals] user update failed:", err);
      toast.error("Couldn't update user");
    }
  }
  async function remove(u) {
    if (!(await confirmDialog({ title: "Remove access", message: `Revoke portal access for ${u.name || u.discord_user_id}?`, confirmLabel: "Remove access", danger: true }))) return;
    setUsers(prev => prev.filter(x => x.discord_user_id !== u.discord_user_id));
    try {
      await tapeFetch(apiBase() + "/api/users/" + u.discord_user_id, { method: "DELETE" });
      toast.success("Access removed");
    } catch (err) {
      console.error("[sinux-signals] user delete failed:", err);
      toast.error("Couldn't remove access");
    }
  }

  return (
    <div>
      <h2>Access</h2>
      <p className="sub">Discord users allowed to sign in. Assign a role (Admin / Editor / Viewer) and, for Editors and Viewers, which servers they can access. Bootstrap admins set via the <code className="mono" style={{ color: "var(--fg-1)" }}>TAPE_ALLOWED_USER_IDS</code> env var are owners — always allowed, not listed here.</p>

      <AddUserForm onAdded={onAdded} servers={servers} role={role} />

      {loaded && users.length === 0 && (
        <div className="empty" style={{ padding: 40 }}>No additional users yet. Add a Discord user ID to grant access.</div>
      )}

      {users.length > 0 && (
        <div className="rows">
          {users.map(u => (
            <Fragment key={u.discord_user_id}>
              <div className="channel-row" style={{ gridTemplateColumns: "24px minmax(0,1fr) auto auto" }}>
                <span className="hash" style={{ color: "var(--fg-2)" }}>{I("user", { size: 15 })}</span>
                <div style={{ minWidth: 0 }}>
                  <div className="name">
                    {u.name || "—"}
                    <span style={{ marginLeft: 8, fontFamily: "var(--f-mono)", fontSize: 10, color: "var(--accent)", letterSpacing: ".08em", textTransform: "uppercase" }}>{u.role || "viewer"}</span>
                  </div>
                  <div className="id">
                    {u.discord_user_id}
                    {u.role !== "admin" && <> · {(u.server_scope || []).includes("*") ? "all servers" : `${(u.server_scope || []).length} server${(u.server_scope || []).length === 1 ? "" : "s"}`}</>}
                  </div>
                </div>
                <span className="live-dot" style={{ color: u.enabled !== false ? "var(--c-up)" : "var(--fg-3)" }}>
                  <span className="d" />{u.enabled !== false ? "active" : "suspended"}
                </span>
                <div className="row-actions">
                  {canActOn(u.role) ? (
                    <>
                      <button className={cx("icon-btn", editingId === u.discord_user_id && "active")} title="Edit user" onClick={() => setEditingId(editingId === u.discord_user_id ? null : u.discord_user_id)}>{I("edit", { size: 15 })}</button>
                      <button className="icon-btn danger" title="Remove access" onClick={() => remove(u)}>{I("trash", { size: 15 })}</button>
                    </>
                  ) : (
                    <span style={{ fontFamily: "var(--f-mono)", fontSize: 10, color: "var(--fg-3)" }}>managed by admin</span>
                  )}
                </div>
              </div>
              {editingId === u.discord_user_id && canActOn(u.role) && (
                <UserEditor user={u} servers={servers} role={role} onCancel={() => setEditingId(null)} onSave={(patch) => saveUser(u.discord_user_id, patch)} />
              )}
            </Fragment>
          ))}
        </div>
      )}

      {(role === "owner" || role === "admin") && <RoleGrants servers={servers} />}
    </div>
  );
}

function AuditScreen({ servers }) {
  const [entries, setEntries] = useState([]);
  const [loaded, setLoaded] = useState(false);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(false);
  const [q, setQ] = useState("");
  const [type, setType] = useState("all");
  const [guild, setGuild] = useState("all");

  async function load(reset) {
    setLoading(true);
    const off = reset ? 0 : entries.length;
    const params = new URLSearchParams({ limit: "50", offset: String(off) });
    if (q.trim()) params.set("q", q.trim());
    if (type !== "all") params.set("target_type", type);
    if (guild !== "all") params.set("guild_id", guild);
    try {
      const r = await tapeFetch(apiBase() + "/api/audit?" + params.toString());
      const j = await r.json();
      const list = j.entries || [];
      setEntries(prev => reset ? list : [...prev, ...list]);
      setHasMore(!!j.hasMore);
    } catch (err) {
      console.error("[sinux-signals] audit fetch failed:", err);
    } finally {
      setLoading(false); setLoaded(true);
    }
  }

  // Reload (from the top) whenever a filter changes, debounced.
  useEffect(() => {
    const t = setTimeout(() => load(true), 250);
    return () => clearTimeout(t);
  }, [q, type, guild]);

  return (
    <div>
      <h2>Audit log</h2>
      <p className="sub">Configuration changes — who changed what, and when. Newest first.</p>

      <div className="filterbar" style={{ gridTemplateColumns: "1fr auto auto", marginBottom: 14 }}>
        <input className="input search" placeholder="Search actor, action or target…" value={q} onChange={e => setQ(e.target.value)} />
        <select className="select" value={type} onChange={e => setType(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
          <option value="all">all types</option>
          <option value="server">servers</option>
          <option value="channel">channels</option>
          <option value="analyst">analysts</option>
          <option value="event">events</option>
          <option value="user">users</option>
          <option value="role-grant">role grants</option>
        </select>
        <select className="select" value={guild} onChange={e => setGuild(e.target.value)} style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}>
          <option value="all">all servers</option>
          {(servers || []).map(s => <option key={s.guild_id} value={s.guild_id}>{s.name}</option>)}
        </select>
      </div>

      {loaded && entries.length === 0 && (
        <div className="empty" style={{ padding: 40 }}>{loading ? "Loading…" : "No matching activity."}</div>
      )}

      {entries.length > 0 && (
        <div className="rows">
          {entries.map(e => (
            <div key={e.id} className="audit-row">
              <span className="audit-time mono">{fmtTime(e.ts)}</span>
              <span className="audit-actor">{e.actor_name || e.actor_id || "—"}</span>
              <span className="pill" style={{ color: "var(--accent)", justifySelf: "start" }}>{e.action}</span>
              <span className="audit-target mono">{e.target_label || e.target_id || ""}</span>
            </div>
          ))}
        </div>
      )}

      {hasMore && (
        <div style={{ display: "flex", justifyContent: "center", padding: 14 }}>
          <button className="btn" onClick={() => load(false)} disabled={loading}>{loading ? "Loading…" : "Load more"}</button>
        </div>
      )}
    </div>
  );
}

function Settings({ channels, setChannels, analysts, setAnalysts, events, servers, setServers, currentServerId, onSelectServer, liveStatus, canAdmin, canManageUsers, isOwner, role }) {
  const allSections = [
    { k: "servers", label: "Servers", icon: "server", need: "admin" },
    { k: "channels", label: "Channels", icon: "hash", need: "admin" },
    { k: "whitelist", label: "Analysts", icon: "shield", need: "admin" },
    { k: "access", label: "Access", icon: "user", need: "users" },
    { k: "audit", label: "Audit log", icon: "clock", need: "admin" },
    { k: "pipeline", label: "Integrations", icon: "database", need: "admin" },
  ];
  const allow = (need) => need === "users" ? canManageUsers : canAdmin;
  const sections = allSections.filter(s => allow(s.need));
  const [section, setSection] = useState(() => {
    if (!canAdmin) return "access";                       // editor → users only
    return (!servers || !servers.length) ? "servers" : "channels";
  });
  const currentServer = (servers || []).find(s => s.guild_id === currentServerId);
  return (
    <div className="panel">
      <div className="settings-tabs">
        {sections.map(s => (
          <button key={s.k} className={cx("settings-tab", section === s.k && "active")} onClick={() => setSection(s.k)}>
            {I(s.icon, { size: 15 })} {s.label}
          </button>
        ))}
      </div>
      <div className="settings-content">
        {section === "servers" && canAdmin && <ServersScreen servers={servers} setServers={setServers} onSelectServer={onSelectServer} />}
        {section === "channels" && canAdmin && <ChannelsScreen channels={channels} setChannels={setChannels} analysts={analysts} currentServer={currentServer} />}
        {section === "whitelist" && canAdmin && <WhitelistScreen analysts={analysts} events={events} currentServer={currentServer} setAnalysts={setAnalysts} />}
        {section === "access" && canManageUsers && <AccessScreen servers={servers} isOwner={isOwner} role={role} />}
        {section === "audit" && canAdmin && <AuditScreen servers={servers} />}
        {section === "pipeline" && canAdmin && <PipelineScreen events={events} status={liveStatus} />}
      </div>
    </div>
  );
}

Object.assign(window, { Settings });
