/* AcquiCup — Admin panel: matches, scoring, AI players, users, companies, exports */
const { useState, useEffect, useRef, useMemo } = React;

/* ---------------- sortable table headers (shared) ----------------
 * useSort() holds the active {key,dir}; .apply(list, cmps) returns a sorted COPY using the
 * per-column comparator map (each comparator sorts ASCENDING — dir flips it; an unknown key
 * leaves the list untouched). <SortTH> renders a clickable header cell with the active-direction
 * arrow. Used by every admin table. (UsersAdmin keeps its own inline variant.) */
function useSort(defaultKey, defaultDir = "asc") {
  const [st, setSt] = useState({ key: defaultKey, dir: defaultDir });
  return {
    key: st.key, dir: st.dir,
    by: (key, dir = "asc") => setSt((s) => (s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir })),
    arrow: (key) => (st.key === key ? (st.dir === "asc" ? " ▲" : " ▼") : ""),
    apply: (list, cmps) => {
      const cmp = cmps[st.key];
      if (!cmp) return list;
      const d = st.dir === "asc" ? 1 : -1;
      return list.slice().sort((a, b) => d * cmp(a, b));
    },
  };
}
function SortTH({ s, k, label, align, dir = "asc", style }) {
  const st = { cursor: "pointer", userSelect: "none", ...(align ? { textAlign: align } : null), ...style };
  return <th className="hov" style={st} onClick={() => s.by(k, dir)}>{label}{s.arrow(k)}</th>;
}
// Status display order for sorting match/result tables (lifecycle order, not alphabetical).
const MATCH_STATUS_ORDER = ["upcoming", "locked", "live", "finished"];

/* ---------------- AI Players (Claude / ChatGPT / Gemini) ----------------
 * Enter or import predictions for the bot players. Writes go to the admin
 * /ai-players endpoints; standings recompute live on refresh. A "force" toggle
 * bypasses the kickoff lock so a missed (locked/finished) match can be backfilled. */
function AiPlayersAdmin({ refresh }) {
  const A = window.ACQ;
  const [bots, setBots] = useState(null);        // [{id,name,company,logo,predictions:{matchId:{home,away,x2}}}]
  const [botId, setBotId] = useState(null);
  const [entries, setEntries] = useState({});    // matchId -> {home,away,x2} (local edit buffer)
  const [force, setForce] = useState(false);
  const [status, setStatus] = useState({ busy: false, msg: null, err: null });
  const [csv, setCsv] = useState("");
  const [importBot, setImportBot] = useState(""); // "" = use the CSV bot column
  const [importForce, setImportForce] = useState(false);
  const [importRes, setImportRes] = useState(null);

  const matches = A.MATCHES;
  const sort = useSort("kickoff", "asc");
  const matchCmps = {
    match: (a, b) => (T(a.home).code || a.home).localeCompare(T(b.home).code || b.home),
    kickoff: (a, b) => ((a.date || "") + (a.time || "")).localeCompare((b.date || "") + (b.time || "")),
    status: (a, b) => MATCH_STATUS_ORDER.indexOf(a.status) - MATCH_STATUS_ORDER.indexOf(b.status),
    result: (a, b) => (a.homeScore == null ? -1 : a.homeScore + a.awayScore) - (b.homeScore == null ? -1 : b.homeScore + b.awayScore),
  };
  const userById = useMemo(() => { const m = {}; for (const u of A.USERS) m[u.id] = u; return m; }, [A.USERS]);

  async function load() {
    try {
      const list = await window.API.adminGetAiPlayers();
      setBots(list);
      setBotId((cur) => cur || (list[0] && list[0].id) || null);
    } catch (e) {
      setBots([]);
      setStatus({ busy: false, msg: null, err: (e && e.error) || "Couldn't load AI players" });
    }
  }
  useEffect(() => { load(); }, []);

  const selBot = bots && bots.find((b) => b.id === botId);
  // Reset the edit buffer from the selected bot's stored predictions.
  useEffect(() => {
    if (!selBot) return;
    const e = {};
    for (const m of matches) {
      const p = selBot.predictions[m.id];
      e[m.id] = p ? { home: String(p.home), away: String(p.away), x2: !!p.x2 } : { home: "", away: "", x2: false };
    }
    setEntries(e);
  }, [botId, bots]);

  function setEntry(mid, patch) { setEntries((s) => ({ ...s, [mid]: { ...(s[mid] || {}), ...patch } })); }

  async function saveRow(m) {
    const e = entries[m.id] || {};
    const home = parseInt(e.home, 10), away = parseInt(e.away, 10);
    if (!Number.isInteger(home) || !Number.isInteger(away)) {
      setStatus({ busy: false, msg: null, err: `Enter both scores for ${T(m.home).code}–${T(m.away).code}.` });
      return;
    }
    setStatus({ busy: true, msg: "Saving…", err: null });
    try {
      const r = await window.API.adminSetAiPrediction(botId, m.id, { home, away, x2: !!e.x2, force });
      let msg = `Saved ${selBot.name}: ${T(m.home).code} ${home}–${away} ${T(m.away).code}.`;
      if (r.x2LimitReached) msg += " (X2 not added — bot already has 2)";
      setStatus({ busy: false, msg, err: null });
      await load(); await refresh();
    } catch (err) {
      setStatus({ busy: false, msg: null, err: (err && err.error) || "Save failed." });
    }
  }

  async function clearRow(m) {
    setStatus({ busy: true, msg: "Removing…", err: null });
    try {
      await window.API.adminDeleteAiPrediction(botId, m.id, force);
      setStatus({ busy: false, msg: `Removed ${selBot.name}'s pick on ${T(m.home).code}–${T(m.away).code}.`, err: null });
      await load(); await refresh();
    } catch (err) {
      setStatus({ busy: false, msg: null, err: (err && err.error) || "Remove failed." });
    }
  }

  async function doImport() {
    if (!csv.trim()) { setStatus({ busy: false, msg: null, err: "Paste CSV rows first." }); return; }
    setStatus({ busy: true, msg: "Importing…", err: null });
    try {
      const r = await window.API.adminImportAiPredictions({ bot: importBot || undefined, csv, force: importForce });
      setImportRes(r);
      const nErr = (r.errors || []).length;
      setStatus({ busy: false, err: null,
        msg: `Imported ${r.applied} prediction${r.applied === 1 ? "" : "s"}` + (nErr ? ` · ${nErr} row${nErr === 1 ? "" : "s"} skipped` : "") + "." });
      await load(); await refresh();
    } catch (err) {
      setImportRes(null);
      setStatus({ busy: false, msg: null, err: (err && err.error) || "Import failed." });
    }
  }

  // One-click bootstrap: create the bots + AI companies server-side (idempotent,
  // non-destructive). Works on a live project with no local credentials (App Hosting
  // has ADC), and re-syncs/heals if some already exist.
  async function ensureBots() {
    setStatus({ busy: true, msg: "Creating AI players…", err: null });
    try {
      const r = await window.API.adminEnsureAiPlayers(false);
      const nU = r.created.users.length, nC = r.created.companies.length;
      const nHeal = r.healed.users.length + r.healed.companies.length;
      const msg = (nU || nC)
        ? `Created ${nU} bot${nU === 1 ? "" : "s"} + ${nC} compan${nC === 1 ? "y" : "ies"}.`
        : nHeal ? `Already present — fixed ${nHeal} flag${nHeal === 1 ? "" : "s"}.`
        : "AI players are already present.";
      setStatus({ busy: false, msg, err: null });
      await load(); await refresh();
    } catch (e) {
      setStatus({ busy: false, msg: null, err: (e && e.error) || "Couldn't create AI players." });
    }
  }

  const statusBar = (status.busy || status.msg || status.err) ? (
    <div className="card card-pad" style={{ padding: "9px 14px", fontSize: 12.5 }}>
      {status.busy ? <span className="muted">{status.msg}</span>
        : status.err ? <span style={{ color: "var(--neg-tx)" }}><Icon name="x" size={12} /> {status.err}</span>
        : <span style={{ color: "var(--accent)" }}><Icon name="check" size={12} /> {status.msg}</span>}
    </div>
  ) : null;

  if (bots === null) return <div className="card card-pad muted" style={{ padding: 18 }}>Loading AI players…</div>;
  if (!bots.length) return (
    <div className="col gap-16">
      <div className="card card-pad col gap-12" style={{ padding: 18 }}>
        <div className="col gap-2">
          <span className="eyebrow">AI players</span>
          <span className="muted" style={{ fontSize: 13 }}>The AI bots (Claude, ChatGPT, Gemini) and their companies aren't in this database yet. Create them here — it's idempotent and touches no other data (no accounts, no predictions). Then enter their picks from this tab.</span>
        </div>
        <div><button className="btn btn-primary" disabled={status.busy} onClick={ensureBots}><Icon name="plus" size={15} />Create AI players</button></div>
      </div>
      {statusBar}
    </div>
  );

  return (
    <div className="col gap-16">
      {/* intro + bot selector */}
      <div className="card card-pad col gap-12" style={{ padding: 18 }}>
        <div className="row between wrap gap-8" style={{ alignItems: "flex-start" }}>
          <div className="col gap-2" style={{ flex: "1 1 320px", minWidth: 0 }}>
            <span className="eyebrow">AI players</span>
            <span className="muted" style={{ fontSize: 13 }}>Enter or import predictions for the AI bots. They rank on the individual and company boards exactly like human players; saving recalculates the standings.</span>
          </div>
          <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={ensureBots} title="Create any missing bots / companies (idempotent)"><Icon name="plus" size={13} />Re-sync</button>
        </div>
        <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
          {bots.map((b) => {
            const su = userById[b.id];
            const on = b.id === botId;
            return (
              <button key={b.id} onClick={() => setBotId(b.id)} className="row gap-10 pointer"
                style={{ alignItems: "center", padding: "8px 12px", borderRadius: 12, cursor: "pointer",
                  border: on ? "1.5px solid var(--accent)" : "1px solid var(--line)",
                  background: on ? "var(--accent-soft)" : "var(--surface)" }}>
                <CompanyLogo id={b.company} size={26} />
                <div className="col" style={{ lineHeight: 1.15, alignItems: "flex-start" }}>
                  <span className="row gap-6" style={{ fontWeight: 700, fontSize: 13.5, alignItems: "center" }}>{b.name}<AiBadge /></span>
                  <span className="muted mono" style={{ fontSize: 11 }}>{su ? `#${su.rank} · ${su.points} pts` : "—"}</span>
                </div>
              </button>
            );
          })}
          <label className="row gap-6 pointer" style={{ alignItems: "center", marginLeft: "auto", fontSize: 12.5 }}>
            <input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} />
            <span className="muted">Force (bypass kickoff locks — backfill)</span>
          </label>
        </div>
      </div>

      {statusBar}

      {/* manual entry grid for the selected bot */}
      {selBot && (
        <div className="card" style={{ overflow: "hidden" }}>
          <div className="row between wrap gap-8" style={{ padding: "12px 16px", borderBottom: "1px solid var(--line)" }}>
            <span style={{ fontWeight: 700 }} className="row gap-8"><CompanyLogo id={selBot.company} size={22} />{selBot.name}'s predictions</span>
            <span className="muted" style={{ fontSize: 12 }}>{Object.keys(selBot.predictions).length} set</span>
          </div>
          <div style={{ overflowX: "auto" }}>
            <table className="lb">
              <thead><tr><SortTH s={sort} k="match" label="Match" /><SortTH s={sort} k="status" label="Status" align="center" /><th style={{ textAlign: "center" }}>Prediction</th><th style={{ textAlign: "center" }}>X2</th><SortTH s={sort} k="result" label="Result" align="center" dir="desc" /><th></th></tr></thead>
              <tbody>
                {sort.apply(matches, matchCmps).map((m) => {
                  const e = entries[m.id] || { home: "", away: "", x2: false };
                  const editable = m.status === "upcoming" || force;
                  const has = selBot.predictions[m.id] != null;
                  return (
                    <tr key={m.id} className="hov" style={{ opacity: editable ? 1 : 0.5 }}>
                      <td><div className="row gap-8"><TeamBadge code={m.home} size="sm" /><span className="mono" style={{ fontSize: 12.5 }}>{T(m.home).code}–{T(m.away).code}</span><TeamBadge code={m.away} size="sm" /></div></td>
                      <td style={{ textAlign: "center" }}><StatusPill status={m.status} minute={m.minute} /></td>
                      <td>
                        <div className="row gap-4 center">
                          <input className="field mono" disabled={!editable} style={{ width: 42, padding: "5px 4px", textAlign: "center", fontSize: 12.5 }}
                            value={e.home} onChange={(ev) => setEntry(m.id, { home: ev.target.value.replace(/[^0-9]/g, "") })} placeholder="–" />
                          <span className="muted">:</span>
                          <input className="field mono" disabled={!editable} style={{ width: 42, padding: "5px 4px", textAlign: "center", fontSize: 12.5 }}
                            value={e.away} onChange={(ev) => setEntry(m.id, { away: ev.target.value.replace(/[^0-9]/g, "") })} placeholder="–" />
                        </div>
                      </td>
                      <td style={{ textAlign: "center" }}>
                        <input type="checkbox" disabled={!editable} checked={!!e.x2} onChange={(ev) => setEntry(m.id, { x2: ev.target.checked })} />
                      </td>
                      <td style={{ textAlign: "center" }} className="mono">{m.homeScore != null ? `${m.homeScore}–${m.awayScore}` : <span className="muted">—</span>}</td>
                      <td style={{ textAlign: "right" }}>
                        <div className="row gap-6" style={{ justifyContent: "flex-end" }}>
                          <button className="btn btn-primary btn-sm" disabled={!editable || status.busy} onClick={() => saveRow(m)}>Save</button>
                          {has && <button className="btn btn-ghost btn-sm" disabled={!editable || status.busy} onClick={() => clearRow(m)}>Clear</button>}
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {/* CSV import */}
      <div className="card card-pad col gap-12" style={{ padding: 18 }}>
        <div className="col gap-2">
          <span className="eyebrow">Import from CSV</span>
          <span className="muted" style={{ fontSize: 12.5 }}>One row per prediction: <span className="mono">matchId,homeGoals,awayGoals</span>. Pick a bot below, or add a <span className="mono">bot</span> column (claude / chatgpt / gemini) to import all three at once. A header row is optional.</span>
        </div>
        <textarea className="field mono" rows={6} value={csv} onChange={(e) => setCsv(e.target.value)} spellCheck={false}
          placeholder={"matchId,homeGoals,awayGoals\nm07,2,1\nm08,0,0"} style={{ fontSize: 12.5, resize: "vertical" }} />
        <div className="row gap-10 wrap center">
          <select className="field" style={{ width: 200 }} value={importBot} onChange={(e) => setImportBot(e.target.value)}>
            <option value="">Use “bot” column</option>
            {bots.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
          </select>
          <label className="row gap-6 pointer" style={{ alignItems: "center", fontSize: 12.5 }}>
            <input type="checkbox" checked={importForce} onChange={(e) => setImportForce(e.target.checked)} />
            <span className="muted">Force (backfill locked matches)</span>
          </label>
          <button className="btn btn-primary btn-sm" disabled={status.busy} onClick={doImport}><Icon name="download" size={14} />Import predictions</button>
        </div>
        {importRes && importRes.errors && importRes.errors.length > 0 && (
          <div className="card card-pad col gap-4" style={{ padding: "10px 14px", background: "var(--surface-2)", maxHeight: 180, overflowY: "auto" }}>
            <span style={{ fontWeight: 700, fontSize: 12.5, color: "var(--neg-tx)" }}>{importRes.errors.length} row{importRes.errors.length === 1 ? "" : "s"} skipped</span>
            {importRes.errors.map((er, i) => (
              <span key={i} className="mono muted" style={{ fontSize: 11.5 }}>{er.line != null ? `line ${er.line}: ` : ""}{er.message}</span>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

/* ---------------- shared admin helpers ---------------- */
// Busy / success / error status line reused by the CRUD sub-panels (same look as the
// AiPlayersAdmin status bar).
function AdminStatus({ status }) {
  if (!status || (!status.busy && !status.msg && !status.err)) return null;
  return (
    <div className="card card-pad" style={{ padding: "9px 14px", fontSize: 12.5 }}>
      {status.busy ? <span className="muted">{status.msg || "Working…"}</span>
        : status.err ? <span style={{ color: "var(--neg-tx)" }}><Icon name="x" size={12} /> {status.err}</span>
        : <span style={{ color: "var(--accent)" }}><Icon name="check" size={12} /> {status.msg}</span>}
  </div>
  );
}
// Labelled form field wrapper.
function Field({ label, hint, children, flex = 1 }) {
  return (
    <label className="col gap-4" style={{ flex, minWidth: 0 }}>
      <span className="muted" style={{ fontSize: 12 }}>{label}</span>
      {children}
      {hint && <span className="muted" style={{ fontSize: 11 }}>{hint}</span>}
    </label>
  );
}
const coNameOf = (id) => (window.ACQ.COMPANIES.find((c) => c.id === id) || {}).name || id || "—";
const CATEGORY_LABELS = { acquisit: "Acquisit", client: "Client", partner: "Partner", friend: "Friend of Acquisit", other: "Other" };
const catLabelOf = (cat) => CATEGORY_LABELS[cat] || cat;

/* ---------------- Match form modal (create + edit) ---------------- */
// Create a new match (POST /matches) OR fully edit an existing one (PUT /matches/:id) — incl. its
// teams, venue, kickoff, stage, group and odds points. Pass `match` (a client match) to edit; omit
// it to create. Team options come from the bootstrap TEAMS map (an existing match's teams are
// always present there); venues are fetched from GET /admin/venues (the bootstrap VENUES map only
// carries venues a match already references). Native date/time inputs emit exactly the
// YYYY-MM-DD / HH:MM the server validates. Every change is recorded server-side in the audit log.
function MatchFormModal({ match, onClose, onSaved }) {
  const isEdit = !!match;
  const teams = useMemo(
    () => Object.values(window.ACQ.TEAMS).slice().sort((a, b) => String(a.name).localeCompare(String(b.name))),
    []
  );
  const [venues, setVenues] = useState([]);
  const [f, setF] = useState(() =>
    isEdit
      ? {
          home: match.home || "", away: match.away || "", venue: match.venue || "",
          date: match.date || "", time: match.time || "",
          stage: match.stage || "Group Stage", group: match.group || "",
          ph: String(match.points ? match.points.home : 0),
          pd: String(match.points ? match.points.draw : 0),
          pa: String(match.points ? match.points.away : 0),
          manual: !!match.manual,
        }
      : { home: "", away: "", venue: "", date: "", time: "", stage: "Group Stage", group: "", ph: "0", pd: "0", pa: "0", manual: false }
  );
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  useEffect(() => {
    window.API.adminGetVenues().then(setVenues).catch((e) => { setVenues([]); setErr((e && e.error) || "Couldn't load venues — you can still set one later."); });
  }, []);
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
  async function submit() {
    setErr(null);
    if (!f.home || !f.away) return setErr("Pick both teams.");
    if (f.home === f.away) return setErr("Home and away must be different teams.");
    if (!f.date || !f.time) return setErr("Pick a kickoff date and time.");
    setBusy(true);
    // On edit, send null to CLEAR an optional field; on create, undefined to omit it (the server
    // defaults absent fields). points are always sent so an inline-edited slip stays consistent.
    const body = {
      home: f.home, away: f.away, venue: f.venue || (isEdit ? null : undefined),
      date: f.date, time: f.time, stage: f.stage || undefined, group: f.group || (isEdit ? null : undefined),
      points: { home: +f.ph || 0, draw: +f.pd || 0, away: +f.pa || 0 },
      // Pin: only meaningful on an existing match — carries the bypass-live-API flag through.
      ...(isEdit ? { manual: f.manual } : {}),
    };
    try {
      if (isEdit) await window.API.adminUpdateMatch(match.id, body);
      else await window.API.adminCreateMatch(body);
      onSaved();
    } catch (e) { setBusy(false); setErr((e && e.error) || (isEdit ? "Couldn't save the match." : "Couldn't create the match.")); }
  }
  return (
    <AdminModal title={isEdit ? "Edit match" : "Add match"} onClose={onClose} width={540}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-primary" onClick={submit} disabled={busy}>{busy ? (isEdit ? "Saving…" : "Creating…") : (isEdit ? "Save changes" : "Create match")}</button>
      </>}>
      <div className="col gap-12">
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        <div className="row gap-10 wrap">
          <Field label="Home team">
            <select className="field" value={f.home} onChange={(e) => set("home", e.target.value)}>
              <option value="">Select…</option>
              {teams.map((t) => <option key={t.code} value={t.code}>{t.code} — {t.name}</option>)}
            </select>
          </Field>
          <Field label="Away team">
            <select className="field" value={f.away} onChange={(e) => set("away", e.target.value)}>
              <option value="">Select…</option>
              {teams.map((t) => <option key={t.code} value={t.code}>{t.code} — {t.name}</option>)}
            </select>
          </Field>
        </div>
        <div className="row gap-10 wrap">
          <Field label="Kickoff date"><input className="field" type="date" value={f.date} onChange={(e) => set("date", e.target.value)} /></Field>
          <Field label="Kickoff time"><input className="field" type="time" value={f.time} onChange={(e) => set("time", e.target.value)} /></Field>
        </div>
        <Field label="Venue (optional)">
          <select className="field" value={f.venue} onChange={(e) => set("venue", e.target.value)}>
            <option value="">No venue</option>
            {venues.map((v) => <option key={v.key} value={v.key}>{v.city} — {v.stadium}</option>)}
          </select>
        </Field>
        <div className="row gap-10 wrap">
          <Field label="Stage" flex={2}><input className="field" value={f.stage} onChange={(e) => set("stage", e.target.value)} placeholder="Group Stage" /></Field>
          <Field label="Group" flex={1}><input className="field" value={f.group} onChange={(e) => set("group", e.target.value)} placeholder="A" /></Field>
        </div>
        <Field label="Odds points (1 / X / 2)" hint="Leave 0/0/0 to auto-estimate from team tiers on the next boot.">
          <div className="row gap-8">
            {[["ph", "1"], ["pd", "X"], ["pa", "2"]].map(([k, l]) => (
              <div key={k} className="row gap-6 center" style={{ flex: 1 }}>
                <span className="mono muted" style={{ fontSize: 12, width: 12 }}>{l}</span>
                <input className="field mono" style={{ width: "100%", textAlign: "center" }} inputMode="numeric"
                  value={f[k]} onChange={(e) => set(k, e.target.value.replace(/[^0-9]/g, ""))} />
              </div>
            ))}
          </div>
        </Field>
        {isEdit && (
          <Field label="Live API" hint="When pinned, the automatic Sportmonks sync will NOT overwrite this match's result. Entering a result by hand pins it automatically; uncheck to hand it back to auto-sync.">
            <label className="row gap-8 center" style={{ cursor: "pointer", fontSize: 13 }}>
              <input type="checkbox" checked={f.manual} onChange={(e) => set("manual", e.target.checked)} />
              <span>Pin — ignore live API (manual result)</span>
            </label>
          </Field>
        )}
      </div>
    </AdminModal>
  );
}

/* ---------------- Enter / edit result modal ---------------- */
// Enter or correct a match's final score. Saving sets the score AND marks the match 'finished' so
// it scores (the per-row status dropdown handles other status changes). A centered modal — so it
// works regardless of how far down a long match list the row sits — that SURFACES server errors
// (e.g. a TBD-knockout match whose teams aren't drawn yet, or scores out of range) instead of
// failing silently the way the old inline panel did.
function ResultModal({ match, onClose, onSaved }) {
  const [home, setHome] = useState(match.homeScore != null ? String(match.homeScore) : "");
  const [away, setAway] = useState(match.awayScore != null ? String(match.awayScore) : "");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  async function submit() {
    setErr(null);
    if (home === "" || away === "") return setErr("Enter both scores.");
    setBusy(true);
    try {
      await window.API.adminUpdateMatch(match.id, { homeScore: +home, awayScore: +away, status: "finished" });
      onSaved();
    } catch (e) { setBusy(false); setErr((e && e.error) || "Couldn't save the result."); }
  }
  return (
    <AdminModal title={`Result — ${T(match.home).code}–${T(match.away).code}`} onClose={onClose} width={420}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-primary" onClick={submit} disabled={busy}>{busy ? "Saving…" : "Save & recalculate"}</button>
      </>}>
      <div className="col gap-14">
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        <span style={{ fontWeight: 700, fontSize: 14 }}>{T(match.home).name} vs {T(match.away).name}</span>
        <div className="row gap-12 center" style={{ justifyContent: "center" }}>
          <div className="col gap-4 center">
            <span className="muted" style={{ fontSize: 12 }}>{T(match.home).code}</span>
            <input className="field mono" style={{ width: 64, textAlign: "center", fontSize: 18 }} inputMode="numeric" placeholder="–"
              value={home} onChange={(e) => setHome(e.target.value.replace(/[^0-9]/g, ""))} />
          </div>
          <span className="mono muted" style={{ fontSize: 20, marginTop: 16 }}>:</span>
          <div className="col gap-4 center">
            <span className="muted" style={{ fontSize: 12 }}>{T(match.away).code}</span>
            <input className="field mono" style={{ width: 64, textAlign: "center", fontSize: 18 }} inputMode="numeric" placeholder="–"
              value={away} onChange={(e) => setAway(e.target.value.replace(/[^0-9]/g, ""))} />
          </div>
        </div>
        <span className="muted" style={{ fontSize: 11.5 }}>Saving marks the match <strong>finished</strong> and recalculates rankings.</span>
      </div>
    </AdminModal>
  );
}

/* ---------------- Edit user modal ---------------- */
// Wires the formerly-dead Users "Edit" button to PUT /api/admin/users/:id. AI bots have no
// Auth account, so the admin/disable toggles are hidden for them (the server skips those
// side-effects anyway).
// Office buckets (work_location) + department buckets, mirroring server/src/orgMap.js
// (OFFICES + OFFICE_LABELS, DEPARTMENTS). The values stored on the user doc are these raw
// strings; orgMap normalises them at read time for the AcquiBoard.
const OFFICE_OPTS = [["UAE", "United Arab Emirates"], ["KSA", "Saudi Arabia"], ["LEB", "Lebanon"], ["INT", "International"]];
const DEPARTMENT_OPTS = ["UAE", "KSA", "LOREAL", "SEO", "CRO", "CRM", "Marketplace", "Measurement", "Marketing", "COE", "Admin", "Timed", "BD", "Other"];

function EditUserModal({ user, onClose, onSaved }) {
  const companies = window.ACQ.COMPANIES;
  const [f, setF] = useState({ first: user.first || "", last: user.last || "", role: user.role || "", company: user.company || "", category: user.category || "", nationality: user.nationality || "", country: user.country || "", workLocation: user.workLocation || "", department: user.department || "", isAdmin: !!user.isAdmin, disabled: !!user.disabled, newPassword: "" });
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
  // Office/department are Acquisit-staff org fields — show them for @acquisit.io accounts or
  // anyone the admin is (re)classifying as Acquisit in this same form.
  const isAcquisit = (user.email || "").toLowerCase().endsWith("@acquisit.io") || f.category === "acquisit";
  async function submit() {
    setErr(null);
    if (!f.first.trim()) { setErr("First name can't be empty."); return; }
    setBusy(true);
    try {
      const patch = { first: f.first.trim(), last: f.last.trim(), role: f.role, companyId: f.company || null, category: f.category || null, nationality: f.nationality || null, country: f.country || "" };
      if (isAcquisit) {
        patch.workLocation = f.workLocation || null;
        patch.department = f.department || null;
      }
      if (!user.isAi) {
        patch.isAdmin = !!f.isAdmin;
        patch.disabled = !!f.disabled;
        if (f.newPassword) patch.newPassword = f.newPassword;
      }
      await window.API.adminUpdateUser(user.id, patch);
      onSaved();
    } catch (e) { setBusy(false); setErr((e && e.error) || "Couldn't save the user."); }
  }
  return (
    <AdminModal title={`Edit ${userName(user)}`} onClose={onClose}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-primary" onClick={submit} disabled={busy}>{busy ? "Saving…" : "Save changes"}</button>
      </>}>
      <div className="col gap-12">
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        <div className="row gap-12">
          <Field label="First name"><input className="field" value={f.first} onChange={(e) => set("first", e.target.value)} placeholder="First" /></Field>
          <Field label="Last name"><input className="field" value={f.last} onChange={(e) => set("last", e.target.value)} placeholder="Last" /></Field>
        </div>
        <Field label="Role"><input className="field" value={f.role} onChange={(e) => set("role", e.target.value)} placeholder="Player" /></Field>
        <Field label="Company">
          <select className="field" value={f.company} onChange={(e) => set("company", e.target.value)}>
            <option value="">— No company —</option>
            {companies.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
        </Field>
        <Field label="Category">
          <select className="field" value={f.category} onChange={(e) => set("category", e.target.value)}>
            <option value="">— None —</option>
            <option value="acquisit">Acquisit</option>
            <option value="client">Client</option>
            <option value="partner">Partner</option>
            <option value="friend">Friend of Acquisit</option>
            <option value="other">Other</option>
          </select>
        </Field>
        <Field label="Nationality"><CountryCombo value={f.nationality} onChange={(v) => set("nationality", v)} ariaLabel="Nationality" placeholder="Select a nationality…" /></Field>
        <Field label="Country of residence"><CountryCombo value={f.country} onChange={(v) => set("country", v)} ariaLabel="Country of residence" placeholder="Select a country…" /></Field>
        {isAcquisit && (
          <>
            <Field label="Office (AcquiBoard)">
              <select className="field" value={f.workLocation} onChange={(e) => set("workLocation", e.target.value)}>
                <option value="">— Not set —</option>
                {OFFICE_OPTS.map(([k, l]) => <option key={k} value={k}>{l}</option>)}
                {f.workLocation && !OFFICE_OPTS.some(([k]) => k === f.workLocation) && <option value={f.workLocation}>{f.workLocation} (current)</option>}
              </select>
            </Field>
            <Field label="Department (AcquiBoard)">
              <select className="field" value={f.department} onChange={(e) => set("department", e.target.value)}>
                <option value="">— Not set —</option>
                {DEPARTMENT_OPTS.map((d) => <option key={d} value={d}>{d}</option>)}
                {f.department && !DEPARTMENT_OPTS.includes(f.department) && <option value={f.department}>{f.department} (current)</option>}
              </select>
            </Field>
          </>
        )}
        {user.isAi ? (
          <span className="muted" style={{ fontSize: 12 }}>This is an AI bot — it has no sign-in account, so admin rights and account lock don't apply.</span>
        ) : (
          <div className="col gap-8">
            <Field label="Force new password">
              <input className="field" type="text" autoComplete="new-password" value={f.newPassword} onChange={(e) => set("newPassword", e.target.value)} placeholder="Leave blank to keep current" />
              <span className="muted" style={{ fontSize: 11.5 }}>Min 6 chars incl. a number and a special character. Sets the player's sign-in password and revokes their active sessions.</span>
            </Field>
            <label className="row gap-8 pointer" style={{ alignItems: "center", fontSize: 13 }}>
              <input type="checkbox" checked={f.isAdmin} onChange={(e) => set("isAdmin", e.target.checked)} />
              <span>Administrator (full admin panel access)</span>
            </label>
            <label className="row gap-8 pointer" style={{ alignItems: "center", fontSize: 13 }}>
              <input type="checkbox" checked={f.disabled} onChange={(e) => set("disabled", e.target.checked)} />
              <span>Disabled — sign-in locked (existing sessions revoked)</span>
            </label>
          </div>
        )}
      </div>
    </AdminModal>
  );
}

/* ---------------- Company icon picker ---------------- */
// Rasterise a chosen image file to a small square-bounded PNG data URI. Rasterising (rather
// than storing the raw upload) normalises dimensions, strips any SVG scripting, and keeps the
// data URI small enough to live in the company doc + bootstrap payload. The server stores the
// string verbatim in `logo` and the client renders it as <img src> (CSP allows data: images).
function fileToLogoDataUri(file, max) {
  max = max || 128;
  return new Promise(function (resolve, reject) {
    if (!file) return reject(new Error("No file selected."));
    if (!/^image\//.test(file.type || "")) return reject(new Error("Please choose an image file."));
    var reader = new FileReader();
    reader.onerror = function () { reject(new Error("Couldn't read that file.")); };
    reader.onload = function () {
      var img = new Image();
      img.onerror = function () { reject(new Error("That image couldn't be loaded.")); };
      img.onload = function () {
        var iw = img.naturalWidth || img.width || max;
        var ih = img.naturalHeight || img.height || max;
        var scale = Math.min(1, max / Math.max(iw, ih));
        var w = Math.max(1, Math.round(iw * scale));
        var h = Math.max(1, Math.round(ih * scale));
        var canvas = document.createElement("canvas");
        canvas.width = w; canvas.height = h;
        try {
          canvas.getContext("2d").drawImage(img, 0, 0, w, h);
          resolve(canvas.toDataURL("image/png"));
        } catch (e) { reject(new Error("Couldn't process that image.")); }
      };
      img.src = reader.result;
    };
    reader.readAsDataURL(file);
  });
}

// Icon picker for the company create/edit modals. `value` is the current logo (a data URI, a
// seeded asset path, or null); `onChange(next)` gets a new data URI, or null when cleared. The
// preview reflects the pending value (not window.ACQ), so Remove shows the fallback immediately.
// `canRemove` (default true) hides the Remove button — set false for AI companies whose logo is
// a seeded BRAND asset path: clearing it to null is irreversible from the UI (the picker only
// emits data URIs, and the server rejects a bare asset path), so it would orphan the bot's mark.
function LogoPicker({ value, onChange, companyId, name, canRemove = true }) {
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  async function pick(e) {
    setErr(null);
    const file = e.target.files && e.target.files[0];
    e.target.value = ""; // let the same file be re-picked after a Remove
    if (!file) return;
    setBusy(true);
    try {
      const uri = await fileToLogoDataUri(file, 128);
      if (uri.length > 200000) throw new Error("Image is too detailed — try a simpler one.");
      onChange(uri);
    } catch (e2) { setErr((e2 && e2.message) || "Couldn't use that image."); }
    finally { setBusy(false); }
  }
  const initial = ((name || companyId || "?").trim().charAt(0) || "?").toUpperCase();
  return (
    <div className="col gap-8">
      <div className="row gap-12" style={{ alignItems: "center" }}>
        <div style={{ width: 56, height: 56, borderRadius: 16, background: value ? "#fff" : companyColor(companyId),
          border: "1px solid var(--line)", display: "flex", alignItems: "center", justifyContent: "center",
          overflow: "hidden", flex: "none", boxShadow: "0 1px 2px rgba(15,45,82,.16)" }}>
          {value
            ? <img src={value} alt="" style={{ width: "76%", height: "76%", objectFit: "contain", display: "block" }} />
            : <span style={{ color: "#fff", fontWeight: 800, fontSize: 22 }}>{initial}</span>}
        </div>
        <div className="row gap-6 wrap">
          <label className="btn btn-ghost btn-sm" style={{ cursor: busy ? "default" : "pointer", opacity: busy ? 0.6 : 1 }}>
            <Icon name="plus" size={13} />{busy ? "Processing…" : value ? "Replace" : "Upload image"}
            <input type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" disabled={busy}
              onChange={pick} style={{ display: "none" }} />
          </label>
          {value && canRemove && <button type="button" className="btn btn-ghost btn-sm" disabled={busy}
            onClick={() => { setErr(null); onChange(null); }} style={{ color: "var(--neg-tx)" }}>Remove</button>}
        </div>
      </div>
      {err && <span style={{ color: "var(--neg-tx)", fontSize: 12 }}><Icon name="x" size={11} /> {err}</span>}
    </div>
  );
}

/* ---------------- Merge company modal ---------------- */
// Wires the formerly-dead "Merge" button to POST /api/admin/companies/merge. Members of the
// source company move into the target; the source is then hidden. Targets exclude the host,
// hidden companies, and the source itself (server enforces the same).
function MergeCompanyModal({ from, companies, onClose, onSaved }) {
  const targets = companies.filter((c) => c.id !== from.id && !c.hidden && !c.isHost);
  const [into, setInto] = useState(targets[0] ? targets[0].id : "");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  async function submit() {
    setErr(null);
    if (!into) return setErr("Pick a destination company.");
    setBusy(true);
    try { await window.API.adminMergeCompanies(from.id, into); onSaved(); }
    catch (e) { setBusy(false); setErr((e && e.error) || "Merge failed."); }
  }
  return (
    <AdminModal title={`Merge “${from.name}”`} onClose={onClose}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-primary" onClick={submit} disabled={busy || !targets.length}>{busy ? "Merging…" : "Merge"}</button>
      </>}>
      <div className="col gap-12">
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        {targets.length ? (
          <Field label="Move all members into" hint={`“${from.name}” will be hidden after the merge.`}>
            <select className="field" value={into} onChange={(e) => setInto(e.target.value)}>
              {targets.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
            </select>
          </Field>
        ) : (
          <span className="muted" style={{ fontSize: 13 }}>No eligible destination companies (need a visible, non-host company).</span>
        )}
      </div>
    </AdminModal>
  );
}

/* ---------------- Join codes editor (shared by create + edit company modals) ---------------- */
// A company may carry several join codes; they're edited as an in-place list (blank rows dropped
// on save). The server normalizes/dedupes whatever we send, so this is purely the editing UI.
const JOIN_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // mirrors src/joinCode.js (no 0/O/1/I/L)
function genJoinCode(len = 6) {
  let out = "";
  for (let i = 0; i < len; i += 1) out += JOIN_CODE_ALPHABET[Math.floor(Math.random() * JOIN_CODE_ALPHABET.length)];
  return out;
}
function JoinCodesField({ codes, setCodes, busy }) {
  return (
    <div className="col gap-6">
      {codes.length === 0 && <div className="muted" style={{ fontSize: 12.5 }}>No codes yet — add or generate one to let members join with a code.</div>}
      {codes.map((c, i) => (
        <div key={i} className="row gap-8" style={{ alignItems: "center" }}>
          <input className="field grow" value={c} placeholder="ABC123" style={{ textTransform: "uppercase", letterSpacing: ".06em" }}
            onChange={(e) => setCodes((arr) => arr.map((v, j) => (j === i ? e.target.value : v)))} />
          <button type="button" className="btn btn-ghost btn-sm" aria-label="Remove code" disabled={busy}
            onClick={() => setCodes((arr) => arr.filter((_, j) => j !== i))}><Icon name="x" size={13} /></button>
        </div>
      ))}
      <div className="row gap-8">
        <button type="button" className="btn btn-ghost btn-sm" disabled={busy} onClick={() => setCodes((arr) => [...arr, ""])}><Icon name="plus" size={13} />Add code</button>
        <button type="button" className="btn btn-ghost btn-sm" disabled={busy} onClick={() => setCodes((arr) => [...arr, genJoinCode()])}><Icon name="refresh" size={13} />Generate</button>
      </div>
    </div>
  );
}

/* ---------------- Create company modal ---------------- */
function CreateCompanyModal({ onClose, onSaved }) {
  const [name, setName] = useState("");
  const [shortName, setShortName] = useState("");
  const [logo, setLogo] = useState(null);
  const [type, setType] = useState("");
  const [joinCodes, setJoinCodes] = useState([]);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  async function submit() {
    setErr(null);
    if (!name.trim()) return setErr("Enter a company name.");
    const codes = [...new Set(joinCodes.map((c) => c.trim().toUpperCase()).filter(Boolean))];
    setBusy(true);
    try {
      await window.API.adminCreateCompany({
        name: name.trim(),
        shortName: shortName.trim() || undefined,
        logo: logo || undefined,
        type: type || undefined,
        joinCodes: codes,
      });
      onSaved();
    } catch (e) { setBusy(false); setErr((e && e.error) || "Couldn't create the company."); }
  }
  return (
    <AdminModal title="New company" onClose={onClose} width={420}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-primary" onClick={submit} disabled={busy}>{busy ? "Creating…" : "Create"}</button>
      </>}>
      <div className="col gap-12">
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        <Field label="Company name"><input className="field" value={name} autoFocus onChange={(e) => setName(e.target.value)} placeholder="Acme Corp" /></Field>
        <Field label="Short name" hint="Optional — used as the label on rank charts (e.g. “Acme” instead of “Acme Corp”). Leave blank to use the first word of the full name.">
          <input className="field" value={shortName} maxLength={30} onChange={(e) => setShortName(e.target.value)} placeholder="Optional" />
        </Field>
        <Field label="Type" hint="Client / Partner companies show in the sign-up dropdown. Leave as None for an internal/demo company (it can still have join codes).">
          <select className="field" value={type} onChange={(e) => setType(e.target.value)}>
            <option value="">None (not in sign-up dropdown)</option>
            <option value="client">Client</option>
            <option value="partner">Partner</option>
          </select>
        </Field>
        <Field label="Join codes" hint={type ? "Members join with any of these codes. Leave empty to auto-generate one." : "Members can join with any of these codes (works even without a type)."}>
          <JoinCodesField codes={joinCodes} setCodes={setJoinCodes} busy={busy} />
        </Field>
        <Field label="Icon" hint="Optional — upload a square image; it's resized to a 128px icon.">
          <LogoPicker value={logo} onChange={setLogo} name={name} />
        </Field>
      </div>
    </AdminModal>
  );
}

/* ---------------- Edit company modal (name + icon) ---------------- */
// Replaces the bare rename prompt: rename and/or upload-replace-clear the company icon in one
// place. Sends a minimal patch (only changed fields) to PUT /admin/companies/:id; `logo: null`
// clears the icon back to the generated mark, an unchanged icon is omitted (so a seeded AI
// company's asset-path logo is preserved across a rename).
function EditCompanyModal({ company, onClose, onSaved }) {
  const [name, setName] = useState(company.name);
  const [shortName, setShortName] = useState(company.shortName || "");
  const [logo, setLogo] = useState(company.logo || null);
  const [type, setType] = useState(company.type || "");
  // Multiple join codes per company; edited as an in-place list (blank rows are dropped on save).
  const [joinCodes, setJoinCodes] = useState(() => (company.joinCodes || []).slice());
  const [hsCategory, setHsCategory] = useState(company.hsCategory || "");
  // Multiple auto-join domains per company; edited as an in-place list (blank rows are dropped on save).
  const [emailDomains, setEmailDomains] = useState(() => (company.emailDomains || []).slice());
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const typeable = !company.isAi && !company.isHost; // AI/host companies aren't managed at sign-up
  async function submit() {
    setErr(null);
    const nm = name.trim();
    if (!nm) return setErr("Enter a company name.");
    const patch = {};
    if (nm !== company.name) patch.name = nm;
    if ((shortName.trim() || "") !== (company.shortName || "")) patch.shortName = shortName.trim();
    if ((logo || null) !== (company.logo || null)) patch.logo = logo; // string sets, null clears
    if (typeable) {
      if ((type || "") !== (company.type || "")) patch.type = type || null;
      const cleanCodes = [...new Set(joinCodes.map((c) => c.trim().toUpperCase()).filter(Boolean))];
      const origCodes = (company.joinCodes || []).map((c) => c.toUpperCase());
      if (cleanCodes.join(",") !== origCodes.join(",")) patch.joinCodes = cleanCodes;
    }
    if ((hsCategory.trim() || "") !== (company.hsCategory || "")) patch.hsCategory = hsCategory.trim();
    const cleanDomains = [...new Set(emailDomains.map((d) => d.trim().toLowerCase()).filter(Boolean))];
    const origDomains = (company.emailDomains || []).map((d) => d.toLowerCase());
    if (cleanDomains.join(",") !== origDomains.join(",")) patch.emailDomains = cleanDomains;
    if (!Object.keys(patch).length) { onClose(); return; }
    setBusy(true);
    try { await window.API.adminUpdateCompany(company.id, patch); onSaved(); }
    catch (e) { setBusy(false); setErr((e && e.error) || "Couldn't save the company."); }
  }
  return (
    <AdminModal title={`Edit “${company.name}”`} onClose={onClose} width={420}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-primary" onClick={submit} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
      </>}>
      <div className="col gap-12">
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        <Field label="Company name"><input className="field" value={name} autoFocus onChange={(e) => setName(e.target.value)} placeholder="Acme Corp" /></Field>
        <Field label="Short name" hint="Optional — used as the label on rank charts (e.g. “Acme” instead of “Acme Corp”). Leave blank to use the first word of the full name.">
          <input className="field" value={shortName} maxLength={30} onChange={(e) => setShortName(e.target.value)} placeholder="Optional" />
        </Field>
        {typeable && (
          <Field label="Type" hint="Client / Partner companies show in the sign-up dropdown. Join codes work regardless of type.">
            <select className="field" value={type} onChange={(e) => setType(e.target.value)}>
              <option value="">None (not in sign-up dropdown)</option>
              <option value="client">Client</option>
              <option value="partner">Partner</option>
            </select>
          </Field>
        )}
        {typeable && (
          <Field label="Join codes" hint="Members can join with ANY of these codes. Remove a code to invalidate it; add or generate more to hand out alongside it.">
            <JoinCodesField codes={joinCodes} setCodes={setJoinCodes} busy={busy} />
          </Field>
        )}
        <Field label="Email domains" hint="Employees signing up with ANY of these email domains auto-join this company (e.g. talabat.com, talabat.ae).">
          <div className="col gap-6">
            {emailDomains.length === 0 && <div className="muted" style={{ fontSize: 12.5 }}>No domains yet — add one to enable email-domain auto-join.</div>}
            {emailDomains.map((d, i) => (
              <div key={i} className="row gap-8" style={{ alignItems: "center" }}>
                <input className="field grow" value={d} placeholder="company.com" style={{ textTransform: "lowercase" }}
                  onChange={(e) => setEmailDomains((arr) => arr.map((v, j) => (j === i ? e.target.value : v)))} />
                <button type="button" className="btn btn-ghost btn-sm" aria-label="Remove domain" disabled={busy}
                  onClick={() => setEmailDomains((arr) => arr.filter((_, j) => j !== i))}><Icon name="x" size={13} /></button>
              </div>
            ))}
            <div><button type="button" className="btn btn-ghost btn-sm" disabled={busy} onClick={() => setEmailDomains((arr) => [...arr, ""])}><Icon name="plus" size={13} />Add domain</button></div>
          </div>
        </Field>
        <Field label="HS category" hint="HubSpot lifecycle stage. Admin-only — shown only here and in the Companies filters.">
          <input className="field" value={hsCategory} onChange={(e) => setHsCategory(e.target.value)} placeholder="e.g. Prospect, Lead, Client" />
        </Field>
        <Field label="Icon" hint="Upload a square image (PNG/JPG/SVG); it's resized to a 128px icon.">
          <LogoPicker value={logo} onChange={setLogo} companyId={company.id} name={name} canRemove={!company.isAi} />
        </Field>
      </div>
    </AdminModal>
  );
}

/* ---------------- Team form modal (create + edit) ---------------- */
function TeamFormModal({ team, onClose, onSaved }) {
  const editing = !!team;
  const [f, setF] = useState({ code: team ? team.code : "", name: team ? team.name : "", tier: String(team ? team.tier || 3 : 3), iso: team ? team.iso || "" : "", color: team ? team.color || "#888888" : "#888888" });
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
  async function submit() {
    setErr(null);
    if (!editing && !f.code.trim()) return setErr("Enter a team code.");
    if (!f.name.trim()) return setErr("Enter a team name.");
    setBusy(true);
    try {
      if (editing) {
        await window.API.adminUpdateTeam(team.code, { name: f.name.trim(), tier: +f.tier, iso: f.iso.trim(), color: f.color });
      } else {
        await window.API.adminCreateTeam({ code: f.code.trim().toUpperCase(), name: f.name.trim(), tier: +f.tier, iso: f.iso.trim(), color: f.color });
      }
      onSaved();
    } catch (e) { setBusy(false); setErr((e && e.error) || "Couldn't save the team."); }
  }
  return (
    <AdminModal title={editing ? `Edit ${team.code}` : "Add team"} onClose={onClose} width={440}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-primary" onClick={submit} disabled={busy}>{busy ? "Saving…" : editing ? "Save" : "Create"}</button>
      </>}>
      <div className="col gap-12">
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        <div className="row gap-10 wrap">
          <Field label="Code" hint={editing ? "Code can't be changed" : "2–8 letters/digits, e.g. BRA"}>
            <input className="field mono" value={f.code} disabled={editing} onChange={(e) => set("code", e.target.value.toUpperCase())} placeholder="BRA" />
          </Field>
          <Field label="Tier" flex={1}>
            <select className="field" value={f.tier} onChange={(e) => set("tier", e.target.value)}>
              {[1, 2, 3, 4, 5].map((t) => <option key={t} value={t}>Tier {t}</option>)}
            </select>
          </Field>
        </div>
        <Field label="Name"><input className="field" value={f.name} onChange={(e) => set("name", e.target.value)} placeholder="Brazil" /></Field>
        <div className="row gap-10 wrap">
          <Field label="Flag ISO" hint="flagcdn code, e.g. br / gb-eng (empty = TBD)"><input className="field mono" value={f.iso} onChange={(e) => set("iso", e.target.value.toLowerCase())} placeholder="br" /></Field>
          <Field label="Colour" flex={0}>
            <input type="color" value={/^#([0-9a-fA-F]{6})$/.test(f.color) ? f.color : "#888888"} onChange={(e) => set("color", e.target.value)}
              style={{ width: 46, height: 36, padding: 2, border: "1px solid var(--line)", borderRadius: 8, background: "var(--surface)" }} />
          </Field>
        </div>
      </div>
    </AdminModal>
  );
}

/* ---------------- Users tab ---------------- */
// Loads the admin-gated GET /admin/users (authoritative; carries isAdmin + the live Auth
// disabled state, which the bootstrap USERS payload omits). Edit/Delete/Reset-X2 act on it.
function UsersAdmin({ refresh }) {
  const [users, setUsers] = useState(null);
  const [status, setStatus] = useState({});
  const [editing, setEditing] = useState(null);
  const [selected, setSelected] = useState(() => new Set()); // user ids checked for a bulk action
  const [q, setQ] = useState(""); // free-text filter over name / email / company / country
  const [sort, setSort] = useState({ key: "createdAt", dir: "desc" }); // default: newest signups first
  async function load() {
    try { const list = await window.API.adminGetUsers(); setUsers(list); return true; }
    catch (e) { setUsers([]); setStatus({ err: (e && e.error) || "Couldn't load users." }); return false; }
  }
  useEffect(() => { load(); }, []);
  // Reload FIRST, then flash success only if the reload actually succeeded (otherwise the
  // load()'s error stands — no misleading "saved" over stale data).
  async function afterChange(msg) { const ok = await load(); if (ok) { setSelected(new Set()); setStatus({ msg }); await refresh(); } }
  function toggleOne(id) { setSelected((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); }
  // Select-all operates on the CURRENTLY VISIBLE (filtered) rows, not the whole roster, so a
  // search + "select all" + delete only ever touches what the admin can actually see.
  function toggleAll() {
    setSelected((prev) => {
      const ids = visible.map((u) => u.id);
      const allSel = ids.length > 0 && ids.every((id) => prev.has(id));
      const n = new Set(prev);
      ids.forEach((id) => (allSel ? n.delete(id) : n.add(id)));
      return n;
    });
  }
  // Click a sortable header: toggle direction if it's the active key, else switch to it.
  function sortBy(key, defaultDir) { setSort((s) => s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: defaultDir }); }
  const sortArrow = (key) => sort.key === key ? (sort.dir === "asc" ? " ▲" : " ▼") : "";
  async function onResetX2(u) {
    setStatus({ busy: true, msg: "Resetting X2…" });
    try { await window.API.adminResetX2(u.id); await afterChange(`Cleared ${capName(u.first)}'s X2 picks.`); }
    catch (e) { setStatus({ err: (e && e.error) || "Reset failed." }); }
  }
  // Approve a pending membership: promotes their requested company to a confirmed one.
  async function onApprove(u) {
    setStatus({ busy: true, msg: "Approving…" });
    try { await window.API.adminUpdateUser(u.id, { approve: true }); await afterChange(`Approved ${userName(u)} into ${coNameOf(u.pendingCompany)}.`); }
    catch (e) { setStatus({ err: (e && e.error) || "Approve failed." }); }
  }
  async function onDelete(u) {
    if (!window.confirm(`Delete ${userName(u)}? This permanently removes their account, predictions and X2 picks. This cannot be undone.`)) return;
    setStatus({ busy: true, msg: "Deleting…" });
    try { await window.API.adminDeleteUser(u.id); await afterChange(`Deleted ${userName(u)}.`); }
    catch (e) { setStatus({ err: (e && e.error) || "Delete failed." }); }
  }
  // Bulk delete: the API has no batch endpoint, so delete each selected user in turn (one
  // DELETE /admin/users/:id apiece), tallying failures so a mid-run error doesn't hide the rest.
  async function onDeleteSelected() {
    const targets = users.filter((u) => selected.has(u.id));
    if (!targets.length) return;
    if (!window.confirm(`Delete ${targets.length} selected user${targets.length > 1 ? "s" : ""}? This permanently removes their accounts, predictions and X2 picks. This cannot be undone.`)) return;
    setStatus({ busy: true, msg: `Deleting ${targets.length} users…` });
    let ok = 0; const failed = [];
    for (const u of targets) {
      try { await window.API.adminDeleteUser(u.id); ok++; }
      catch (e) { failed.push(userName(u)); }
    }
    await load(); setSelected(new Set()); await refresh();
    if (failed.length) setStatus({ err: `Deleted ${ok}, failed ${failed.length}: ${failed.join(", ")}.` });
    else setStatus({ msg: `Deleted ${ok} user${ok > 1 ? "s" : ""}.` });
  }
  if (users === null) return <div className="card card-pad muted" style={{ padding: 18 }}>Loading users…</div>;
  // Compact join date; createdAt is epoch millis (null for legacy docs with no created_at).
  const fmtJoined = (ms) => { if (!ms) return "—"; try { return new Date(ms).toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" }); } catch (e) { return "—"; } };
  const needle = q.trim().toLowerCase();
  const matches = (u) => !needle || [userName(u), u.email, u.country, u.affiliation, u.company && coNameOf(u.company), u.pendingCompany && coNameOf(u.pendingCompany)]
    .some((f) => (f || "").toLowerCase().includes(needle));
  const dir = sort.dir === "asc" ? 1 : -1;
  // What the Company column actually shows (company, else pending company, else affiliation) —
  // so sorting matches the visible text.
  const coDisplay = (u) => u.pending ? coNameOf(u.pendingCompany) : (u.company ? coNameOf(u.company) : (u.affiliation || ""));
  const cmp = (a, b) => {
    if (sort.key === "name") return dir * userName(a).localeCompare(userName(b));
    if (sort.key === "company") return dir * coDisplay(a).localeCompare(coDisplay(b));
    if (sort.key === "category") return dir * catLabelOf(a.category || "").localeCompare(catLabelOf(b.category || ""));
    if (sort.key === "points") return dir * ((a.points ?? 0) - (b.points ?? 0));
    if (sort.key === "preds") return dir * ((a.preds ?? 0) - (b.preds ?? 0));
    if (sort.key === "x2") return dir * ((a.x2Count ?? 0) - (b.x2Count ?? 0));
    return dir * ((a.createdAt ?? 0) - (b.createdAt ?? 0)); // createdAt
  };
  const visible = users.filter(matches).slice().sort(cmp);
  return (
    <div className="col gap-12">
      <AdminStatus status={status} />
      <div className="card" style={{ overflow: "hidden" }}>
        <div className="row between" style={{ padding: "12px 16px", borderBottom: "1px solid var(--line)" }}>
          {selected.size > 0
            ? <span className="row gap-10" style={{ alignItems: "center" }}>
                <span style={{ fontWeight: 700 }}>{selected.size} selected</span>
                <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => setSelected(new Set())}>Clear</button>
              </span>
            : <span style={{ fontWeight: 700 }}>{needle ? `${visible.length} of ${users.length} users` : `${users.length} users`}</span>}
          <span className="row gap-6" style={{ alignItems: "center" }}>
            <input className="field" style={{ padding: "7px 10px", fontSize: 13, minWidth: 200 }} type="search" placeholder="Search name, email, company…" aria-label="Search users" value={q} onChange={(e) => setQ(e.target.value)} />
            {selected.size > 0 && <button className="btn btn-sm" disabled={status.busy} onClick={onDeleteSelected} style={{ background: "var(--neg-bg)", color: "var(--neg-tx)" }}><Icon name="trash" size={13} />Delete selected</button>}
            <button className="btn btn-ghost btn-sm" onClick={load} disabled={status.busy}>Refresh</button>
          </span>
        </div>
        <div style={{ overflowX: "auto" }}>
          <table className="lb">
            <thead><tr><th style={{ width: 34, textAlign: "center" }}><input type="checkbox" aria-label="Select all users" checked={visible.length > 0 && visible.every((u) => selected.has(u.id))} ref={(el) => { if (el) { const n = visible.filter((u) => selected.has(u.id)).length; el.indeterminate = n > 0 && n < visible.length; } }} onChange={toggleAll} disabled={status.busy} /></th><th className="hov" style={{ cursor: "pointer", userSelect: "none" }} onClick={() => sortBy("name", "asc")}>User{sortArrow("name")}</th><th className="hov" style={{ cursor: "pointer", userSelect: "none" }} onClick={() => sortBy("company", "asc")}>Company{sortArrow("company")}</th><th className="hov" style={{ cursor: "pointer", userSelect: "none" }} onClick={() => sortBy("category", "asc")}>Category{sortArrow("category")}</th><th className="hov" style={{ textAlign: "right", cursor: "pointer", userSelect: "none" }} onClick={() => sortBy("points", "desc")}>Points{sortArrow("points")}</th><th className="hov" style={{ textAlign: "right", cursor: "pointer", userSelect: "none" }} onClick={() => sortBy("preds", "desc")}>Preds{sortArrow("preds")}</th><th className="hov" style={{ textAlign: "center", cursor: "pointer", userSelect: "none" }} onClick={() => sortBy("x2", "desc")}>X2{sortArrow("x2")}</th><th className="hov" style={{ textAlign: "right", cursor: "pointer", userSelect: "none" }} onClick={() => sortBy("createdAt", "desc")}>Joined{sortArrow("createdAt")}</th><th style={{ textAlign: "right" }}>Actions</th></tr></thead>
            <tbody>
              {visible.length === 0 && <tr><td colSpan={9} className="muted" style={{ padding: 18, textAlign: "center" }}>{needle ? `No users match “${q.trim()}”.` : "No users."}</td></tr>}
              {visible.map((u) => (
                <tr key={u.id} className="hov" style={{ opacity: u.disabled ? 0.55 : 1, background: selected.has(u.id) ? "var(--accent-soft)" : undefined }}>
                  <td style={{ textAlign: "center" }}><input type="checkbox" aria-label={`Select ${userName(u)}`} checked={selected.has(u.id)} onChange={() => toggleOne(u.id)} disabled={status.busy} /></td>
                  <td><div className="row gap-10">{(u.company || u.pendingCompany) ? <CompanyLogo id={u.company || u.pendingCompany} size={30} /> : <Avatar user={u} size={30} />}
                    <div className="col" style={{ lineHeight: 1.15 }}>
                      <span className="row gap-6" style={{ fontWeight: 600, fontSize: 13.5, alignItems: "center" }}>{userName(u)}{u.isAi && <AiBadge />}{u.isAdmin && <span className="badge" style={{ background: "var(--neg-bg)", color: "var(--neg-tx)" }}>Admin</span>}{u.pending && <span className="badge" style={{ background: "var(--gold-soft, #fdf3d8)", color: "var(--gold, #9a6a00)" }}>Pending</span>}{u.disabled && <span className="badge">Disabled</span>}</span>
                      <span className="muted" style={{ fontSize: 11 }}>{u.country || u.email}</span>
                    </div></div></td>
                  <td style={{ fontSize: 13 }}>{u.pending ? <span className="muted">{coNameOf(u.pendingCompany)} · awaiting approval</span> : (u.company ? coNameOf(u.company) : (u.affiliation ? <span className="muted">{u.affiliation}</span> : "—"))}</td>
                  <td>{u.category ? <span className="badge" style={u.category === "acquisit" ? { background: "var(--accent-soft)", color: "var(--accent)" } : {}}>{catLabelOf(u.category)}</span> : <span className="muted">—</span>}</td>
                  <td className="mono" style={{ textAlign: "right", fontWeight: 600 }}>{u.points}</td>
                  <td className="mono" style={{ textAlign: "right" }}>{u.preds ?? 0}</td>
                  <td style={{ textAlign: "center" }}>{u.x2Count > 0 ? <span className="row gap-2 mono" style={{ justifyContent: "center", color: "var(--gold)", fontWeight: 700, fontSize: 12.5 }}><Icon name="bolt" size={13} fill />{u.x2Count}/2</span> : <span className="muted">–</span>}</td>
                  <td className="muted" style={{ textAlign: "right", fontSize: 12, whiteSpace: "nowrap" }}>{fmtJoined(u.createdAt)}</td>
                  <td style={{ textAlign: "right" }}>
                    <div className="row gap-6" style={{ justifyContent: "flex-end" }}>
                      {u.pending && <button className="btn btn-primary btn-sm" disabled={status.busy} onClick={() => onApprove(u)} title="Approve this pending membership"><Icon name="check" size={13} />Approve</button>}
                      <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => setEditing(u)}><Icon name="pencil" size={13} />Edit</button>
                      {u.x2Used && <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => onResetX2(u)}>Reset X2</button>}
                      <button className="btn btn-ghost btn-sm" disabled={status.busy} title="Delete user" onClick={() => onDelete(u)} style={{ color: "var(--neg-tx)" }}><Icon name="trash" size={13} /></button>
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
      {editing && <EditUserModal user={editing} onClose={() => setEditing(null)} onSaved={async () => { setEditing(null); await afterChange("User updated."); }} />}
    </div>
  );
}

/* ---------------- Companies tab ---------------- */
// Loads GET /admin/companies (ALL companies incl. hidden + member counts) so unhide/delete
// of hidden/empty companies is reachable; cross-references COMPANY_RANK for the standings
// stats. Rename / Merge / Hide-Unhide / Delete + a "New company" action.
function CompaniesAdmin({ refresh }) {
  const [companies, setCompanies] = useState(null);
  const [status, setStatus] = useState({});
  const [merging, setMerging] = useState(null);
  const [creating, setCreating] = useState(false);
  const [editing, setEditing] = useState(null);
  const [selected, setSelected] = useState(() => new Set()); // company ids checked for a bulk action
  // Filters. Default to ONLY companies with at least one signed-up member — the imported client
  // roster is ~1000 strong and mostly empty until employees join, so the unfiltered list is noise.
  const [onlySignedUp, setOnlySignedUp] = useState(true);
  const [hsFilter, setHsFilter] = useState(""); // "" = all HS categories
  const [query, setQuery] = useState(""); // free-text search over name / domain / join code
  // Standings stats by company id, computed each render (NOT memoized on []) so a refresh()
  // after a rename/merge reflects the new COMPANY_RANK, not the mount-time snapshot.
  const rankById = {};
  for (const c of window.ACQ.COMPANY_RANK) rankById[c.id] = c;
  const sort = useSort("name", "asc");
  const cmps = {
    name: (a, b) => (a.name || "").localeCompare(b.name || ""),
    shortName: (a, b) => (a.shortName || "").localeCompare(b.shortName || ""),
    type: (a, b) => (a.type || "").localeCompare(b.type || ""),
    emailDomain: (a, b) => ((a.emailDomains || [])[0] || "").localeCompare((b.emailDomains || [])[0] || ""),
    joinCode: (a, b) => ((a.joinCodes || [])[0] || "").localeCompare((b.joinCodes || [])[0] || ""),
    players: (a, b) => (a.count ?? 0) - (b.count ?? 0),
    avgTop3: (a, b) => ((rankById[a.id] || {}).avgTop3 ?? -1) - ((rankById[b.id] || {}).avgTop3 ?? -1),
  };
  async function load() {
    try { setCompanies(await window.API.adminGetCompanies()); return true; }
    catch (e) { setCompanies([]); setStatus({ err: (e && e.error) || "Couldn't load companies." }); return false; }
  }
  useEffect(() => { load(); }, []);
  async function afterChange(msg) { const ok = await load(); if (ok) { setSelected(new Set()); setStatus({ msg }); await refresh(); } }
  // Only empty, non-host companies can be deleted (host companies and ones with members are protected).
  const canDelete = (c) => !c.isHost && c.count === 0;
  function toggleOne(id) { setSelected((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); }
  async function onToggleHide(c) {
    setStatus({ busy: true, msg: c.hidden ? "Unhiding…" : "Hiding…" });
    try { await window.API.adminUpdateCompany(c.id, { hidden: !c.hidden }); await afterChange(c.hidden ? `${c.name} is visible again.` : `${c.name} hidden.`); }
    catch (e) { setStatus({ err: (e && e.error) || "Update failed." }); }
  }
  async function onDelete(c) {
    if (!window.confirm(`Delete company “${c.name}”? This cannot be undone.`)) return;
    setStatus({ busy: true, msg: "Deleting…" });
    try { await window.API.adminDeleteCompany(c.id); await afterChange(`Deleted ${c.name}.`); }
    catch (e) { setStatus({ err: (e && e.error) || "Delete failed." }); }
  }
  // Bulk delete: the API has no batch endpoint, so delete each selected company in turn (one
  // DELETE /admin/companies/:id apiece), tallying failures so a mid-run error doesn't hide the rest.
  async function onDeleteSelected() {
    const targets = companies.filter((c) => selected.has(c.id) && canDelete(c));
    if (!targets.length) return;
    if (!window.confirm(`Delete ${targets.length} selected compan${targets.length > 1 ? "ies" : "y"}? This cannot be undone.`)) return;
    setStatus({ busy: true, msg: `Deleting ${targets.length} companies…` });
    let ok = 0; const failed = [];
    for (const c of targets) {
      try { await window.API.adminDeleteCompany(c.id); ok++; }
      catch (e) { failed.push(c.name); }
    }
    await load(); setSelected(new Set()); await refresh();
    if (failed.length) setStatus({ err: `Deleted ${ok}, failed ${failed.length}: ${failed.join(", ")}.` });
    else setStatus({ msg: `Deleted ${ok} compan${ok > 1 ? "ies" : "y"}.` });
  }
  if (companies === null) return <div className="card card-pad muted" style={{ padding: 18 }}>Loading companies…</div>;
  // Distinct HS categories present, for the filter dropdown (alphabetical).
  const hsOptions = [...new Set(companies.map((c) => c.hsCategory).filter(Boolean))].sort((a, b) => a.localeCompare(b));
  const q = query.trim().toLowerCase();
  const filtered = companies.filter((c) =>
    (!onlySignedUp || (c.count || 0) > 0) &&
    (!hsFilter || c.hsCategory === hsFilter) &&
    (!q || `${c.name || ""} ${(c.emailDomains || []).join(" ")} ${(c.joinCodes || []).join(" ")}`.toLowerCase().includes(q))
  );
  const rows = sort.apply(filtered, cmps);
  const deletable = rows.filter(canDelete); // only these get a checkbox / are reachable by "select all"
  const allChecked = deletable.length > 0 && deletable.every((c) => selected.has(c.id));
  function toggleAll() { setSelected((prev) => { if (deletable.every((c) => prev.has(c.id))) return new Set(); return new Set(deletable.map((c) => c.id)); }); }
  return (
    <div className="col gap-12">
      <AdminStatus status={status} />
      <div className="card" style={{ overflow: "hidden" }}>
        <div className="row between" style={{ padding: "12px 16px", borderBottom: "1px solid var(--line)" }}>
          {selected.size > 0
            ? <span className="row gap-10" style={{ alignItems: "center" }}>
                <span style={{ fontWeight: 700 }}>{selected.size} selected</span>
                <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => setSelected(new Set())}>Clear</button>
              </span>
            : <span style={{ fontWeight: 700 }}>{rows.length}{rows.length !== companies.length ? ` of ${companies.length}` : ""} companies</span>}
          <div className="row gap-8">
            {selected.size > 0 && <button className="btn btn-sm" disabled={status.busy} onClick={onDeleteSelected} style={{ background: "var(--neg-bg)", color: "var(--neg-tx)" }}><Icon name="trash" size={13} />Delete selected</button>}
            <button className="btn btn-soft btn-sm" onClick={() => window.API.adminDownloadExport("companies")} disabled={status.busy}><Icon name="download" size={14} />Export CSV</button>
            <button className="btn btn-primary btn-sm" onClick={() => setCreating(true)} disabled={status.busy}><Icon name="plus" size={14} />New company</button>
          </div>
        </div>
        <div className="row gap-14" style={{ padding: "10px 16px", borderBottom: "1px solid var(--line)", alignItems: "center", flexWrap: "wrap" }}>
          <input className="field" style={{ height: 30, padding: "0 10px", width: 240 }} type="search" placeholder="Search name, domain, join code…" value={query} onChange={(e) => { setQuery(e.target.value); setSelected(new Set()); }} />
          <label className="row gap-6" style={{ alignItems: "center", fontSize: 13, cursor: "pointer" }}>
            <input type="checkbox" checked={onlySignedUp} onChange={(e) => { setOnlySignedUp(e.target.checked); setSelected(new Set()); }} />
            Only with a sign-up
          </label>
          <label className="row gap-6" style={{ alignItems: "center", fontSize: 13 }}>
            <span className="muted">HS category</span>
            <select className="field" style={{ height: 30, padding: "0 8px", width: "auto" }} value={hsFilter} onChange={(e) => { setHsFilter(e.target.value); setSelected(new Set()); }}>
              <option value="">All</option>
              {hsOptions.map((h) => <option key={h} value={h}>{h}</option>)}
            </select>
          </label>
        </div>
        <div style={{ overflowX: "auto" }}>
          <table className="lb">
            <thead><tr><th style={{ width: 34, textAlign: "center" }}><input type="checkbox" aria-label="Select all deletable companies" disabled={status.busy || deletable.length === 0} checked={allChecked} ref={(el) => { if (el) { const n = deletable.filter((c) => selected.has(c.id)).length; el.indeterminate = n > 0 && n < deletable.length; } }} onChange={toggleAll} /></th><SortTH s={sort} k="name" label="Company" /><SortTH s={sort} k="shortName" label="Short name" /><SortTH s={sort} k="type" label="Type" /><SortTH s={sort} k="emailDomain" label="Domain" /><SortTH s={sort} k="joinCode" label="Join code" /><SortTH s={sort} k="players" label="Players" align="center" dir="desc" /><SortTH s={sort} k="avgTop3" label="Avg top 3" align="right" dir="desc" /><th style={{ textAlign: "right" }}>Actions</th></tr></thead>
            <tbody>
              {rows.map((c) => {
                const r = rankById[c.id];
                return (
                  <tr key={c.id} className="hov" style={{ opacity: c.hidden ? 0.5 : 1, background: selected.has(c.id) ? "var(--accent-soft)" : undefined }}>
                    <td style={{ textAlign: "center" }}>{canDelete(c) ? <input type="checkbox" aria-label={`Select ${c.name}`} checked={selected.has(c.id)} onChange={() => toggleOne(c.id)} disabled={status.busy} /> : null}</td>
                    <td><div className="row gap-8" style={{ alignItems: "center" }}><CompanyLogo id={c.id} size={22} /><span style={{ fontWeight: 600 }}>{c.name}</span>{c.isHost && <span className="badge blue">Host</span>}{c.isAi && <AiBadge />}{c.hidden && <span className="badge">Hidden</span>}</div></td>
                    <td>{c.shortName ? c.shortName : <span className="muted">—</span>}</td>
                    <td>{c.type ? <span className="badge">{c.type === "client" ? "Client" : "Partner"}</span> : <span className="muted">—</span>}</td>
                    <td className="mono" style={{ fontSize: 12 }}>{(c.emailDomains && c.emailDomains.length)
                      ? <span title={c.emailDomains.join(", ")}>{c.emailDomains[0]}{c.emailDomains.length > 1 ? ` +${c.emailDomains.length - 1}` : ""}</span>
                      : <span className="muted">—</span>}</td>
                    <td className="mono" style={{ fontSize: 12, letterSpacing: ".05em" }}>{(c.joinCodes && c.joinCodes.length)
                      ? <span title={c.joinCodes.join(", ")}>{c.joinCodes[0]}{c.joinCodes.length > 1 ? ` +${c.joinCodes.length - 1}` : ""}</span>
                      : <span className="muted">—</span>}</td>
                    <td className="mono" style={{ textAlign: "center" }}>{c.count}</td>
                    <td className="mono" style={{ textAlign: "right", fontWeight: 600 }}>{r ? r.avgTop3 : "—"}</td>
                    <td style={{ textAlign: "right" }}>
                      <div className="row gap-6" style={{ justifyContent: "flex-end" }}>
                        <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => setEditing(c)}><Icon name="pencil" size={13} />Edit</button>
                        {!c.isHost && <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => setMerging(c)}>Merge</button>}
                        {!c.isHost && <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => onToggleHide(c)}>{c.hidden ? "Unhide" : "Hide"}</button>}
                        {!c.isHost && c.count === 0 && <button className="btn btn-ghost btn-sm" disabled={status.busy} title="Delete company" onClick={() => onDelete(c)} style={{ color: "var(--neg-tx)" }}><Icon name="trash" size={13} /></button>}
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
      {merging && <MergeCompanyModal from={merging} companies={companies} onClose={() => setMerging(null)} onSaved={async () => { setMerging(null); await afterChange("Companies merged."); }} />}
      {creating && <CreateCompanyModal onClose={() => setCreating(false)} onSaved={async () => { setCreating(false); await afterChange("Company created."); }} />}
      {editing && <EditCompanyModal company={editing} onClose={() => setEditing(null)} onSaved={async () => { setEditing(null); await afterChange("Company updated."); }} />}
    </div>
  );
}

/* ---------------- Teams tab ---------------- */
function TeamsAdmin({ refresh }) {
  const [teams, setTeams] = useState(null);
  const [status, setStatus] = useState({});
  const [editing, setEditing] = useState(undefined); // undefined=closed, null=create, obj=edit
  const sort = useSort("code", "asc");
  const cmps = {
    code: (a, b) => (a.name || a.code || "").localeCompare(b.name || b.code || ""),
    tier: (a, b) => (a.tier ?? 99) - (b.tier ?? 99),
    iso: (a, b) => (a.iso || "").localeCompare(b.iso || ""),
  };
  async function load() {
    try { setTeams(await window.API.adminGetTeams()); return true; }
    catch (e) { setTeams([]); setStatus({ err: (e && e.error) || "Couldn't load teams." }); return false; }
  }
  useEffect(() => { load(); }, []);
  async function afterChange(msg) { const ok = await load(); if (ok) { setStatus({ msg }); await refresh(); } }
  async function onDelete(t) {
    if (!window.confirm(`Delete team ${t.code} (${t.name})?`)) return;
    setStatus({ busy: true, msg: "Deleting…" });
    try { await window.API.adminDeleteTeam(t.code); await afterChange(`Deleted ${t.code}.`); }
    catch (e) { setStatus({ err: (e && e.error) || "Delete failed." }); }
  }
  if (teams === null) return <div className="card card-pad muted" style={{ padding: 18 }}>Loading teams…</div>;
  return (
    <div className="col gap-12">
      <AdminStatus status={status} />
      <div className="card" style={{ overflow: "hidden" }}>
        <div className="row between" style={{ padding: "12px 16px", borderBottom: "1px solid var(--line)" }}>
          <span style={{ fontWeight: 700 }}>{teams.length} teams</span>
          <button className="btn btn-primary btn-sm" onClick={() => setEditing(null)} disabled={status.busy}><Icon name="plus" size={14} />Add team</button>
        </div>
        <div style={{ overflowX: "auto" }}>
          <table className="lb">
            <thead><tr><SortTH s={sort} k="code" label="Team" /><SortTH s={sort} k="tier" label="Tier" align="center" /><SortTH s={sort} k="iso" label="Flag" /><th>Colour</th><th style={{ textAlign: "right" }}>Actions</th></tr></thead>
            <tbody>
              {sort.apply(teams, cmps).map((t) => (
                <tr key={t.code} className="hov">
                  <td><div className="row gap-8" style={{ alignItems: "center" }}>
                    {t.iso ? <img src={"https://flagcdn.com/" + t.iso + ".svg"} alt="" style={{ width: 23, height: 16, borderRadius: 3, objectFit: "cover" }} />
                      : <span className="team-flag fallback" style={{ width: 23, height: 16, borderRadius: 3, background: t.color || "#888", fontSize: 8 }}>{t.code}</span>}
                    <span className="mono" style={{ fontWeight: 700 }}>{t.code}</span><span style={{ fontSize: 13 }}>{t.name}</span></div></td>
                  <td className="mono" style={{ textAlign: "center" }}>{t.tier ?? "—"}</td>
                  <td className="mono muted" style={{ fontSize: 12 }}>{t.iso || "—"}</td>
                  <td><span className="row gap-6" style={{ alignItems: "center" }}><span style={{ width: 16, height: 16, borderRadius: 4, background: t.color || "#888", border: "1px solid var(--line)" }} /><span className="mono muted" style={{ fontSize: 11.5 }}>{t.color}</span></span></td>
                  <td style={{ textAlign: "right" }}><div className="row gap-6" style={{ justifyContent: "flex-end" }}>
                    <button className="btn btn-ghost btn-sm" disabled={status.busy} onClick={() => setEditing(t)}><Icon name="pencil" size={13} />Edit</button>
                    <button className="btn btn-ghost btn-sm" disabled={status.busy} title="Delete team" onClick={() => onDelete(t)} style={{ color: "var(--neg-tx)" }}><Icon name="trash" size={13} /></button>
                  </div></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
      {editing !== undefined && <TeamFormModal team={editing} onClose={() => setEditing(undefined)} onSaved={async () => { setEditing(undefined); await afterChange("Team saved."); }} />}
    </div>
  );
}

/* ---------------- Audit log tab ---------------- */
// Read-only viewer for the append-only audit trail (GET /admin/audit-log).
// Format an audit `at` (epoch millis, or null while a server timestamp is still pending).
const fmtAuditWhen = (ms) => { if (!ms) return "—"; try { return new Date(ms).toLocaleString(); } catch (e) { return "—"; } };

// Shared audit-entry table (When / Actor / Action / Target / Details). Used by the Audit tab and
// the per-match History modal. `showTarget` hides the redundant Target column in the per-match
// view (every row targets the same match).
function AuditTable({ rows, showTarget = true }) {
  const cols = showTarget ? 5 : 4;
  const sort = useSort("when", "desc"); // default: most recent first
  const cmps = {
    when: (a, b) => (a.at ?? 0) - (b.at ?? 0),
    actor: (a, b) => (a.actorEmail || a.actorId || "").localeCompare(b.actorEmail || b.actorId || ""),
    action: (a, b) => (a.action || "").localeCompare(b.action || ""),
    target: (a, b) => (a.targetId || "").localeCompare(b.targetId || ""),
  };
  return (
    <div style={{ overflowX: "auto" }}>
      <table className="lb">
        <thead><tr><SortTH s={sort} k="when" label="When" dir="desc" /><SortTH s={sort} k="actor" label="Actor" /><SortTH s={sort} k="action" label="Action" />{showTarget && <SortTH s={sort} k="target" label="Target" />}<th>Details</th></tr></thead>
        <tbody>
          {rows.length === 0 && <tr><td colSpan={cols} className="muted" style={{ padding: 16 }}>No audit entries yet.</td></tr>}
          {sort.apply(rows, cmps).map((r) => (
            <tr key={r.id} className="hov">
              <td className="muted nowrap" style={{ fontSize: 12 }}>{fmtAuditWhen(r.at)}</td>
              <td style={{ fontSize: 12.5 }}>{r.actorEmail || r.actorId || "—"}</td>
              <td><span className="badge mono" style={{ fontSize: 11 }}>{r.action}</span></td>
              {showTarget && <td className="mono muted" style={{ fontSize: 11.5 }}>{r.targetId || "—"}</td>}
              <td className="mono muted" style={{ fontSize: 11, maxWidth: 320, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.details ? JSON.stringify(r.details) : "—"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function AuditAdmin() {
  const [rows, setRows] = useState(null);
  const [err, setErr] = useState(null);
  async function load() {
    try { setRows(await window.API.adminGetAuditLog(150)); setErr(null); }
    catch (e) { setRows([]); setErr((e && e.error) || "Couldn't load the audit log."); }
  }
  useEffect(() => { load(); }, []);
  if (rows === null) return <div className="card card-pad muted" style={{ padding: 18 }}>Loading audit log…</div>;
  return (
    <div className="col gap-12">
      <div className="card card-pad col gap-2" style={{ padding: 18 }}>
        <div className="row between"><span className="eyebrow">Audit log</span><button className="btn btn-ghost btn-sm" onClick={load}>Refresh</button></div>
        <span className="muted" style={{ fontSize: 13 }}>Append-only record of privileged actions (match create/update/delete/sync, odds sync, role changes, merges, deletes, AI ops). Most recent first.</span>
      </div>
      {err && <div className="card card-pad" style={{ padding: "9px 14px", fontSize: 12.5, color: "var(--neg-tx)" }}><Icon name="x" size={12} /> {err}</div>}
      <div className="card" style={{ overflow: "hidden" }}><AuditTable rows={rows} /></div>
    </div>
  );
}

/* ---------------- Per-match change history modal ---------------- */
// Read-only view of ONE match's change history (GET /audit-log?targetId=<matchId>): every create,
// edit, result entry, odds change, and Sportmonks sync recorded for it. Powers the row "History"
// button.
function MatchHistoryModal({ match, onClose }) {
  const [rows, setRows] = useState(null);
  const [err, setErr] = useState(null);
  useEffect(() => {
    window.API.adminGetAuditLog(100, match.id)
      .then((r) => { setRows(r); setErr(null); })
      .catch((e) => { setRows([]); setErr((e && e.error) || "Couldn't load this match's history."); });
  }, [match.id]);
  return (
    <AdminModal title={`History — ${T(match.home).code}–${T(match.away).code}`} onClose={onClose} width={640}
      footer={<button className="btn btn-ghost" onClick={onClose}>Close</button>}>
      <div className="col gap-10">
        <span className="muted" style={{ fontSize: 12.5 }}>
          Append-only record of every change to this match (<span className="mono">{match.id}</span>) — create, edits, result entry, odds, and Sportmonks syncs. Most recent first.
        </span>
        {err && <div style={{ color: "var(--neg-tx)", fontSize: 12.5 }}><Icon name="x" size={12} /> {err}</div>}
        {rows === null
          ? <div className="muted" style={{ fontSize: 13, padding: "8px 0" }}>Loading history…</div>
          : <div className="card" style={{ overflow: "hidden" }}><AuditTable rows={rows} showTarget={false} /></div>}
      </div>
    </AdminModal>
  );
}

function Admin({ store, refresh }) {
  const tz = useTimezone();
  const A = window.ACQ;
  const [tab, setTab] = useState("users");
  const [config, setConfig] = useState({ ...A.CONFIG });
  const [matches, setMatches] = useState(A.MATCHES.map((m) => ({ ...m, points: { ...m.points } })));
  const [resultMatch, setResultMatch] = useState(null); // match whose final-score modal is open
  const [oddsSync, setOddsSync] = useState({ busy: false, msg: null, err: null }); // Sportmonks odds push
  const [oddsEdit, setOddsEdit] = useState({ msg: null }); // confirmation after an inline odds-cell edit
  const [fixturesSync, setFixturesSync] = useState({ busy: false, msg: null, err: null }); // bulk Sportmonks fixtures/results sync
  const [matchSync, setMatchSync] = useState({ id: null, busy: false, msg: null, err: null }); // per-match Sportmonks sync
  const [matchModal, setMatchModal] = useState(null); // null=closed | 'add' | <match> (edit)
  const [historyMatch, setHistoryMatch] = useState(null); // match whose change history is shown
  const matchSort = useSort("kickoff", "asc"); // default: chronological (≈ fixture order)
  const matchCmps = {
    match: (a, b) => (T(a.home).code || a.home).localeCompare(T(b.home).code || b.home),
    kickoff: (a, b) => ((a.date || "") + (a.time || "")).localeCompare((b.date || "") + (b.time || "")),
    status: (a, b) => MATCH_STATUS_ORDER.indexOf(a.status) - MATCH_STATUS_ORDER.indexOf(b.status),
    result: (a, b) => (a.homeScore == null ? -1 : a.homeScore + a.awayScore) - (b.homeScore == null ? -1 : b.homeScore + b.awayScore),
  };
  const tabs = [
    ["users", "Users"], ["companies", "Companies"], ["matches", "Matches"], ["scoring", "Scoring"],
    ["ai", "AI Players"], ["teams", "Teams"], ["audit", "Audit"], ["exports", "Exports"],
    ["maintenance", "Maintenance"],
  ];

  // Keep local matches/config in sync when fresh data arrives from the server (e.g. after a
  // refresh, or a concurrent admin changing the bonus / tournament name).
  useEffect(() => { setMatches(A.MATCHES.map((m) => ({ ...m, points: { ...m.points } }))); }, [A.MATCHES]);
  useEffect(() => { setConfig({ ...A.CONFIG }); }, [A.CONFIG]);

  function updMatch(id, patch) { setMatches((ms) => ms.map((m) => (m.id === id ? { ...m, ...patch } : m))); }

  // Status change → confirm (it locks/unlocks picks and feeds scoring immediately), persist,
  // refresh so standings update live. Cancelling leaves the controlled <select> on its old value.
  async function onStatusChange(id, status) {
    const m = matches.find((x) => x.id === id);
    if (!m || m.status === status) return;
    if (!window.confirm(`Change ${T(m.home).code}–${T(m.away).code} status from “${m.status}” to “${status}”? This locks/unlocks predictions and affects scoring immediately.`)) return;
    updMatch(id, { status });
    await window.API.adminUpdateMatch(id, { status });
    await refresh();
  }

  // Odds change keeps local state only; persist on blur.
  function onOddsChange(id, k, value) {
    setMatches((ms) => ms.map((m) => (m.id === id ? { ...m, points: { ...m.points, [k]: +value || 0 } } : m)));
  }
  async function onOddsBlur(id) {
    const m = matches.find((x) => x.id === id);
    if (!m) return;
    const { home, draw, away } = m.points;
    // 0/0/0 is the reserved "no odds" sentinel: it's re-estimated on every boot
    // (see src/odds.js), so saving an all-zero slip would just be undone on the
    // next restart. Block it — restore the last persisted odds instead.
    if (!(+home) && !(+draw) && !(+away)) {
      const orig = A.MATCHES.find((x) => x.id === id);
      if (orig) updMatch(id, { points: { ...orig.points } });
      return;
    }
    // Unchanged vs the persisted slip → nothing to save, no confirmation to show.
    const orig = A.MATCHES.find((x) => x.id === id);
    if (orig && +orig.points.home === +home && +orig.points.draw === +draw && +orig.points.away === +away) return;
    await window.API.adminUpdateMatch(id, { points: { home, draw, away } });
    setOddsEdit({ msg: `Odds saved for ${T(m.home).code}–${T(m.away).code} · ${home}/${draw}/${away} (1/X/2).` });
    await refresh();
  }

  // Push a Sportmonks odds update (scope: 'upcoming' | 'all') → refresh so the
  // new odds appear in every slip. Odds are static and shared by all users; this
  // just re-pulls them from the provider for the chosen scope.
  async function onPushOdds(scope) {
    if (oddsSync.busy) return;
    if (!window.confirm(scope === "all"
      ? "Re-pull odds for ALL imported matches from Sportmonks? This overwrites the current 1 / X / 2 points on every one."
      : "Re-pull odds for UPCOMING matches from Sportmonks? This overwrites their current 1 / X / 2 points.")) return;
    setOddsSync({ busy: true, msg: scope === "all" ? "Updating all odds…" : "Updating upcoming odds…", err: null });
    try {
      const r = await window.API.adminSyncOdds(scope);
      const n = r.withOdds || 0;
      setOddsSync({
        busy: false, err: null,
        msg: `Updated ${n} match${n === 1 ? "" : "es"} from Sportmonks` + (r.empty ? ` · ${r.empty} had no odds` : "") + ".",
      });
      await refresh();
    } catch (e) {
      setOddsSync({ busy: false, msg: null, err: (e && e.error) || "Odds update failed." });
    }
  }

  // Bulk-sync fixtures/results from Sportmonks → refresh so the new scores/statuses/kickoffs
  // appear. Scopes: 'upcoming' leaves already-finished matches untouched; 'all' re-pulls every
  // match; 'knockout' re-pulls every bracket fixture; 'knockout-new' pulls only brand-new bracket
  // fixtures (existing knockout matches untouched). Odds are NOT touched here (use the odds buttons).
  const SYNC_CONFIRM = {
    upcoming: "Pull fixtures + results for UPCOMING matches from Sportmonks? Already-finished matches are kept.",
    all: "Re-pull EVERY match's fixtures + results from Sportmonks? This can overwrite teams, kickoffs, statuses and scores across all matches.",
    knockout: "Re-pull EVERY knockout-bracket match from Sportmonks? This can overwrite the bracket's teams, kickoffs, statuses and scores.",
    "knockout-new": "Pull only NEW knockout-bracket fixtures from Sportmonks? Existing bracket matches are left untouched.",
  };
  const SYNC_BUSY = {
    upcoming: "Syncing upcoming matches…", all: "Syncing all matches…",
    knockout: "Syncing knockout matches…", "knockout-new": "Syncing new knockout matches…",
  };
  async function onSyncMatches(scope) {
    if (fixturesSync.busy) return;
    if (!window.confirm(SYNC_CONFIRM[scope] || "Sync matches from Sportmonks?")) return;
    setFixturesSync({ busy: true, msg: SYNC_BUSY[scope] || "Syncing matches…", err: null });
    try {
      const r = await window.API.adminSyncMatches(scope);
      const bits = [];
      if (r.inserted) bits.push(`${r.inserted} added`);
      if (r.updated) bits.push(`${r.updated} updated`);
      if (r.skipped) bits.push(`${r.skipped} finished kept`);
      setFixturesSync({ busy: false, err: null, msg: `Synced from Sportmonks` + (bits.length ? ` · ${bits.join(" · ")}` : " · no changes") + "." });
      await refresh();
    } catch (e) {
      setFixturesSync({ busy: false, msg: null, err: (e && e.error) || "Match sync failed." });
    }
  }

  // Refresh ONE match from Sportmonks → persist + refresh. `which` selects what to pull:
  // 'result' (scores + status), 'odds' (1X2 points), or 'both'. Only Sportmonks-imported
  // matches (id 'sm…') expose these buttons; the server records it in the audit log.
  async function onSyncMatch(m, which) {
    if (matchSync.busy) return;
    if (!window.confirm(`Sync ${T(m.home).code}–${T(m.away).code} (result + odds) from Sportmonks? This overwrites its current result and odds.`)) return;
    const parts = which === "result" ? { results: true, odds: false }
      : which === "odds" ? { results: false, odds: true }
      : { results: true, odds: true };
    const label = which === "result" ? "result" : which === "odds" ? "odds" : "result + odds";
    setMatchSync({ id: m.id, busy: true, msg: `Syncing ${T(m.home).code}–${T(m.away).code} ${label} from Sportmonks…`, err: null });
    try {
      const r = await window.API.adminSyncMatch(m.id, parts);
      const bits = [];
      if (r.results) bits.push(r.results.home_score != null ? `result ${r.results.home_score}–${r.results.away_score} (${r.results.status})` : `status ${r.results.status}`);
      if (r.odds && r.odds.empty) bits.push("no odds available");
      else if (r.odds) bits.push(`odds ${r.odds.home}/${r.odds.draw}/${r.odds.away}`);
      setMatchSync({ id: m.id, busy: false, err: null, msg: `Synced ${T(m.home).code}–${T(m.away).code}` + (bits.length ? ` · ${bits.join(" · ")}` : "") + "." });
      await refresh();
    } catch (e) {
      setMatchSync({ id: m.id, busy: false, msg: null, err: (e && e.error) || "Sportmonks sync failed." });
    }
  }

  // Delete a match (cascades its predictions + X2) → confirm, persist, refresh.
  async function onDeleteMatch(m) {
    if (!window.confirm(`Delete ${T(m.home).code}–${T(m.away).code}? This removes the match and every prediction/X2 on it. This cannot be undone.`)) return;
    await window.API.adminDeleteMatch(m.id);
    if (resultMatch && resultMatch.id === m.id) setResultMatch(null);
    await refresh();
  }

  // Scoring config save → persist + refresh.
  async function onSaveConfig() {
    await window.API.adminUpdateConfig({
      exactScoreMode: config.exactScoreMode || "multiply",
      exactScoreBonus: config.exactScoreBonus,
      exactScoreMultiplier: config.exactScoreMultiplier ?? 2,
      tournamentName: config.tournamentName,
    });
    await refresh();
  }

  // Maintenance (pause) mode. `paused`/`pauseMessage` live on the shared config doc, so we
  // reuse the same adminUpdateConfig endpoint. Pausing sends the current message too, so a
  // "Pause" or "Save message" both persist the latest notice; resuming flips paused off.
  const [maint, setMaint] = useState({ busy: false, msg: null, err: null });
  async function setPaused(paused) {
    setMaint({ busy: true, msg: null, err: null });
    try {
      await window.API.adminUpdateConfig({ paused, pauseMessage: config.pauseMessage || "" });
      await refresh();
      setMaint({
        busy: false,
        err: null,
        msg: paused ? "App paused — players now see the maintenance screen." : "App resumed — players are back in.",
      });
    } catch (e) {
      setMaint({ busy: false, msg: null, err: (e && e.error) || "Couldn't update maintenance mode." });
    }
  }

  // Exports — download server-generated CSV with auth.
  function onExport(type) { window.API.adminDownloadExport(type); }

  return (
    <div className="col gap-20">
      <PageHead eyebrow="Restricted" title="Admin panel"
        sub="Manage matches, odds, results, users and companies. Changes here drive the scoring engine and rankings."
        right={<span className="badge" style={{ background: "var(--neg-bg)", color: "var(--neg-tx)" }}><Icon name="lock" size={12} />Admin access</span>} />
      <div className="row" style={{ overflowX: "auto" }}><div className="tabs">{tabs.map(([id, l]) => <button key={id} className={tab === id ? "on" : ""} onClick={() => setTab(id)}>{l}</button>)}</div></div>

      {/* ---------------- MATCHES ---------------- */}
      {tab === "matches" && (
        <div className="card" style={{ overflow: "hidden" }}>
          <div className="row between wrap gap-8" style={{ padding: "14px 18px", borderBottom: "1px solid var(--line)" }}>
            <span style={{ fontWeight: 700 }}>{matches.length} matches</span>
            <div className="row gap-8 wrap center">
              <button className="btn btn-soft btn-sm" disabled={fixturesSync.busy} onClick={() => onSyncMatches("upcoming")} title="Pull fixtures + results for upcoming matches from Sportmonks (finished matches kept)"><Icon name="refresh" size={14} />Sync upcoming</button>
              <button className="btn btn-soft btn-sm" disabled={fixturesSync.busy} onClick={() => onSyncMatches("all")} title="Re-pull every match's fixture + result from Sportmonks"><Icon name="refresh" size={14} />Sync all matches</button>
              <button className="btn btn-soft btn-sm" disabled={fixturesSync.busy} onClick={() => onSyncMatches("knockout-new")} title="Pull only NEW knockout-bracket fixtures from Sportmonks (existing bracket matches untouched — preserves entered results/odds)"><Icon name="refresh" size={14} />Sync new KO matches</button>
              <button className="btn btn-soft btn-sm" disabled={fixturesSync.busy} onClick={() => onSyncMatches("knockout")} title="Re-pull every knockout-bracket match from Sportmonks"><Icon name="refresh" size={14} />Sync all KO matches</button>
              <button className="btn btn-soft btn-sm" disabled={oddsSync.busy} onClick={() => onPushOdds("upcoming")}><Icon name="download" size={14} />Update upcoming odds</button>
              <button className="btn btn-soft btn-sm" disabled={oddsSync.busy} onClick={() => onPushOdds("all")}><Icon name="download" size={14} />Update all odds</button>
              <button className="btn btn-primary btn-sm" onClick={() => setMatchModal("add")}><Icon name="plus" size={14} />Add match</button>
            </div>
          </div>
          {(fixturesSync.busy || fixturesSync.msg || fixturesSync.err) && (
            <div style={{ padding: "9px 18px", borderBottom: "1px solid var(--line)", background: "var(--surface-2)", fontSize: 12.5 }}>
              {fixturesSync.busy
                ? <span className="muted">{fixturesSync.msg}</span>
                : fixturesSync.err
                  ? <span style={{ color: "var(--neg-tx)" }}><Icon name="x" size={12} /> {fixturesSync.err}</span>
                  : <span style={{ color: "var(--accent)" }}><Icon name="check" size={12} /> {fixturesSync.msg}</span>}
            </div>
          )}
          {(oddsSync.busy || oddsSync.msg || oddsSync.err) && (
            <div style={{ padding: "9px 18px", borderBottom: "1px solid var(--line)", background: "var(--surface-2)", fontSize: 12.5 }}>
              {oddsSync.busy
                ? <span className="muted">{oddsSync.msg}</span>
                : oddsSync.err
                  ? <span style={{ color: "var(--neg-tx)" }}><Icon name="x" size={12} /> {oddsSync.err}</span>
                  : <span style={{ color: "var(--accent)" }}><Icon name="check" size={12} /> {oddsSync.msg}</span>}
            </div>
          )}
          {oddsEdit.msg && (
            <div style={{ padding: "9px 18px", borderBottom: "1px solid var(--line)", background: "var(--surface-2)", fontSize: 12.5 }}>
              <span style={{ color: "var(--accent)" }}><Icon name="check" size={12} /> {oddsEdit.msg}</span>
            </div>
          )}
          {(matchSync.busy || matchSync.msg || matchSync.err) && (
            <div style={{ padding: "9px 18px", borderBottom: "1px solid var(--line)", background: "var(--surface-2)", fontSize: 12.5 }}>
              {matchSync.busy
                ? <span className="muted">{matchSync.msg}</span>
                : matchSync.err
                  ? <span style={{ color: "var(--neg-tx)" }}><Icon name="x" size={12} /> {matchSync.err}</span>
                  : <span style={{ color: "var(--accent)" }}><Icon name="check" size={12} /> {matchSync.msg}</span>}
            </div>
          )}
          <div style={{ overflowX: "auto" }}>
            <table className="lb">
              <thead><tr><SortTH s={matchSort} k="match" label="Match" /><SortTH s={matchSort} k="kickoff" label="Kickoff" /><SortTH s={matchSort} k="status" label="Status" align="center" /><th style={{ textAlign: "center" }}>Odds (1 / X / 2)</th><SortTH s={matchSort} k="result" label="Result" align="center" dir="desc" /><th></th></tr></thead>
              <tbody>
                {matchSort.apply(matches, matchCmps).map((m) => (
                  <tr key={m.id} className="hov">
                    <td><div className="row gap-10"><TeamBadge code={m.home} size="sm" /><span className="mono" style={{ fontSize: 12.5 }}>{T(m.home).code}–{T(m.away).code}</span><TeamBadge code={m.away} size="sm" /></div></td>
                    <td className="muted" style={{ fontSize: 12.5 }}>{fmtMatchDate(m, tz)} {fmtMatchTime(m, tz)}</td>
                    <td style={{ textAlign: "center" }}>
                      <select className="field" style={{ padding: "5px 8px", width: 112, fontSize: 12.5 }} value={m.status} onChange={(e) => onStatusChange(m.id, e.target.value)}>
                        {["upcoming", "locked", "live", "finished"].map((s) => <option key={s} value={s}>{s}</option>)}
                      </select>
                    </td>
                    <td>
                      <div className="row gap-4 center">
                        {["home", "draw", "away"].map((k) => (
                          <input key={k} className="field mono" style={{ width: 46, padding: "5px 4px", textAlign: "center", fontSize: 12.5 }}
                            value={m.points[k]} onChange={(e) => onOddsChange(m.id, k, e.target.value)} onBlur={() => onOddsBlur(m.id)} />
                        ))}
                      </div>
                    </td>
                    <td style={{ textAlign: "center" }}>
                      {m.homeScore != null ? <span className="mono" style={{ fontWeight: 700 }}>{m.homeScore}–{m.awayScore}</span> : <span className="muted">—</span>}
                    </td>
                    <td style={{ textAlign: "right" }}>
                      <div className="row gap-6" style={{ justifyContent: "flex-end" }}>
                        <button className="btn btn-ghost btn-sm" title="Enter or edit the final score" onClick={() => setResultMatch(m)}>{m.homeScore != null ? "Result" : "Enter result"}</button>
                        {String(m.id).startsWith("sm") && (
                          <button className="btn btn-ghost btn-sm" title="Sync this match's result + odds from Sportmonks" disabled={matchSync.busy} onClick={() => onSyncMatch(m, "both")}><Icon name="refresh" size={13} /></button>
                        )}
                        <button className="btn btn-ghost btn-sm" title="Edit match (teams, venue, kickoff, stage, odds)" onClick={() => setMatchModal(m)}><Icon name="pencil" size={13} /></button>
                        <button className="btn btn-ghost btn-sm" title="Change history" onClick={() => setHistoryMatch(m)}><Icon name="book" size={13} /></button>
                        <button className="btn btn-ghost btn-sm" title="Delete match" onClick={() => onDeleteMatch(m)} style={{ color: "var(--neg-tx)" }}><Icon name="trash" size={13} /></button>
                      </div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          {resultMatch && (
            <ResultModal
              match={resultMatch}
              onClose={() => setResultMatch(null)}
              onSaved={async () => { setResultMatch(null); await refresh(); }}
            />
          )}
          {matchModal !== null && (
            <MatchFormModal
              match={matchModal === "add" ? null : matchModal}
              onClose={() => setMatchModal(null)}
              onSaved={async () => { setMatchModal(null); await refresh(); }}
            />
          )}
          {historyMatch && (
            <MatchHistoryModal match={historyMatch} onClose={() => setHistoryMatch(null)} />
          )}
        </div>
      )}

      {/* ---------------- SCORING ---------------- */}
      {tab === "scoring" && (
        <div className="col gap-16">
          <div className="card card-pad col gap-4" style={{ padding: 20 }}>
            <span className="eyebrow">Scoring bonus configuration</span>
            <span className="muted" style={{ fontSize: 13 }}>Nailing the exact scoreline either <strong>multiplies</strong> the odds-based outcome points (×N) or adds a <strong>flat bonus</strong> (+X) — your choice below. The scoring engine recalculates all rankings on save.</span>
          </div>
          <div className="card card-pad" style={{ padding: "16px 20px" }}>
            <Field label="Tournament name" hint="The contest label shown across the app. Saved with the button below.">
              <input className="field" value={config.tournamentName || ""} onChange={(e) => setConfig((c) => ({ ...c, tournamentName: e.target.value }))} placeholder="World Cup 2026" style={{ maxWidth: 360 }} />
            </Field>
          </div>
          <div className="card card-pad col gap-12" style={{ padding: "16px 18px", maxWidth: 460 }}>
            <div className="col gap-2">
              <span style={{ fontWeight: 700, fontSize: 14 }}>Exact predicted scoreline</span>
              <span className="muted" style={{ fontSize: 12 }}>Reward for nailing the precise score (not just the winner).</span>
            </div>
            {/* mode toggle: multiply the outcome points (×N) or add a flat bonus (+X) */}
            <div className="row gap-6">
              {[["multiply", "Multiply ×N"], ["add", "Flat bonus +X"]].map(([m, lbl]) => (
                <button key={m}
                  className={(config.exactScoreMode || "multiply") === m ? "btn btn-primary btn-sm" : "btn btn-soft btn-sm"}
                  onClick={() => setConfig((c) => ({ ...c, exactScoreMode: m }))}>{lbl}</button>
              ))}
            </div>
            {/* value stepper — multiplier (multiply mode) or flat bonus (add mode) */}
            {(config.exactScoreMode || "multiply") === "add" ? (
              <div className="row gap-8 center">
                <button className="btn btn-ghost btn-sm" style={{ width: 34, padding: 0, justifyContent: "center" }} onClick={() => setConfig((c) => ({ ...c, exactScoreBonus: Math.max(0, (c.exactScoreBonus ?? 20) - 1) }))}>–</button>
                <span className="mono" style={{ fontWeight: 700, fontSize: 22, width: 54, textAlign: "center", color: "var(--accent)" }}>+{config.exactScoreBonus ?? 20}</span>
                <button className="btn btn-ghost btn-sm" style={{ width: 34, padding: 0, justifyContent: "center" }} onClick={() => setConfig((c) => ({ ...c, exactScoreBonus: (c.exactScoreBonus ?? 20) + 1 }))}>+</button>
                <span className="muted" style={{ fontSize: 12 }}>points added to the outcome points.</span>
              </div>
            ) : (
              <div className="row gap-8 center">
                <button className="btn btn-ghost btn-sm" style={{ width: 34, padding: 0, justifyContent: "center" }} onClick={() => setConfig((c) => ({ ...c, exactScoreMultiplier: Math.max(1, (c.exactScoreMultiplier ?? 2) - 1) }))}>–</button>
                <span className="mono" style={{ fontWeight: 700, fontSize: 22, width: 54, textAlign: "center", color: "var(--accent)" }}>×{config.exactScoreMultiplier ?? 2}</span>
                <button className="btn btn-ghost btn-sm" style={{ width: 34, padding: 0, justifyContent: "center" }} onClick={() => setConfig((c) => ({ ...c, exactScoreMultiplier: Math.min(10, (c.exactScoreMultiplier ?? 2) + 1) }))}>+</button>
                <span className="muted" style={{ fontSize: 12 }}>× the outcome points on an exact score.</span>
              </div>
            )}
          </div>
          <div className="row gap-10"><button className="btn btn-primary" onClick={onSaveConfig}>Save & recalculate rankings</button><button className="btn btn-ghost" onClick={() => setConfig({ ...window.ACQ.CONFIG })}>Reset to defaults</button></div>
        </div>
      )}

      {/* ---------------- MAINTENANCE ---------------- */}
      {tab === "maintenance" && (
        <div className="col gap-16" style={{ maxWidth: 560 }}>
          <div className="card card-pad col gap-4" style={{ padding: 20 }}>
            <span className="eyebrow">Maintenance mode</span>
            <span className="muted" style={{ fontSize: 13 }}>Pause the app while you push an update. Players see a friendly “we’ll be right back” screen and can’t save predictions or X2. <strong>You (and other admins) keep full access</strong> the whole time.</span>
          </div>

          {/* Current status */}
          <div className="card card-pad row between center" style={{ padding: "16px 20px" }}>
            <div className="col gap-2">
              <span style={{ fontWeight: 700, fontSize: 14 }}>Status</span>
              <span className="muted" style={{ fontSize: 12 }}>{config.paused ? "Players are currently locked out." : "The app is live for everyone."}</span>
            </div>
            {config.paused
              ? <span className="badge" style={{ background: "var(--brand-yellow)", color: "#5a4400", fontWeight: 700 }}><Icon name="lock" size={12} />Paused</span>
              : <span className="badge" style={{ background: "var(--pos-bg)", color: "var(--pos-tx)", fontWeight: 700 }}><Icon name="check" size={12} />Live</span>}
          </div>

          {/* Editable notice */}
          <div className="card card-pad" style={{ padding: "16px 20px" }}>
            <Field label="Message shown to players" hint="Optional — leave blank to use the default “we’ll be right back” copy. Max 500 characters.">
              <textarea className="field" rows={3} maxLength={500}
                value={config.pauseMessage || ""}
                onChange={(e) => setConfig((c) => ({ ...c, pauseMessage: e.target.value }))}
                placeholder="AcquiCup is briefly offline for an update. We'll be back shortly — thanks for your patience!"
                style={{ resize: "vertical", lineHeight: 1.5 }} />
            </Field>
          </div>

          {(maint.msg || maint.err) && (
            <div style={{ fontSize: 12.5 }}>
              {maint.err
                ? <span style={{ color: "var(--neg-tx)" }}><Icon name="x" size={12} /> {maint.err}</span>
                : <span style={{ color: "var(--accent)" }}><Icon name="check" size={12} /> {maint.msg}</span>}
            </div>
          )}

          {/* Actions */}
          <div className="row gap-10">
            {config.paused ? (
              <React.Fragment>
                <button className="btn btn-primary" disabled={maint.busy} onClick={() => setPaused(false)}>{maint.busy ? "Working…" : "Resume app"}</button>
                <button className="btn btn-soft" disabled={maint.busy} onClick={() => setPaused(true)} title="Persist the message above without resuming">Save message</button>
              </React.Fragment>
            ) : (
              <button className="btn btn-primary" disabled={maint.busy} onClick={() => setPaused(true)}
                style={{ background: "var(--neg-tx)", borderColor: "var(--neg-tx)" }}>
                <Icon name="lock" size={14} />{maint.busy ? "Pausing…" : "Pause app"}
              </button>
            )}
          </div>
        </div>
      )}

      {/* ---------------- TEAMS ---------------- */}
      {tab === "teams" && <TeamsAdmin refresh={refresh} />}

      {/* ---------------- AI PLAYERS ---------------- */}
      {tab === "ai" && <AiPlayersAdmin refresh={refresh} />}

      {/* ---------------- USERS ---------------- */}
      {tab === "users" && <UsersAdmin refresh={refresh} />}

      {/* ---------------- COMPANIES ---------------- */}
      {tab === "companies" && <CompaniesAdmin refresh={refresh} />}

      {/* ---------------- AUDIT ---------------- */}
      {tab === "audit" && <AuditAdmin />}

      {/* ---------------- EXPORTS ---------------- */}
      {tab === "exports" && (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(240px,1fr))", gap: 14 }}>
          {[
            ["Individual leaderboard", "individual"],
            ["Company leaderboard", "company"],
            ["Companies (all dimensions)", "companies"],
            ["Match results", "matches"],
            ["Match results + odds", "matches-odds"],
            ["User list", "users"],
          ].map(([label, type]) => (
            <div key={type} className="card card-pad col gap-12" style={{ padding: 18 }}>
              <div className="row between"><Icon name="download" size={18} style={{ color: "var(--ink-soft)" }} /><span className="badge mono" style={{ fontSize: 10.5 }}>CSV</span></div>
              <span style={{ fontWeight: 700, fontSize: 14.5 }}>{label}</span>
              <button className="btn btn-soft btn-sm full" style={{ justifyContent: "center" }} onClick={() => onExport(type)}>Export</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { Admin });
