/* ============================================================
   Toy Collector — single-file React app (loaded via Babel standalone).
   Features: auth, per-user state, catalog admin, photo upload,
             search, list/grid view, alpha index bar, CSV export,
             series dropdown, UPC field, brand-series management,
             UPC barcode scanner.
   ============================================================ */

const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ---------- Constants ------------------------------------------------
const CURRENCY_SYMBOL = { USD: "$", EUR: "€", GBP: "£", AUD: "A$", CAD: "C$", JPY: "¥" };
const CONDITIONS = [
  { id: "mint",      label: "Mint",         color: "#34c759" },
  { id: "near_mint", label: "Near Mint",    color: "#9be07a" },
  { id: "loose",     label: "Loose",        color: "#ffb627" },
  { id: "worn",      label: "Worn",         color: "#ff9500" },
  { id: "damaged",   label: "Damaged",      color: "#ff3b30" },
  { id: "moc",       label: "MOC/MIB",      color: "#007aff" },
  { id: "graded",    label: "Graded",       color: "#af52de" },
];
const CONDITION_BY_ID = Object.fromEntries(CONDITIONS.map((c) => [c.id, c]));
const ICON_OPTIONS = ["star","saber","shield","gear","sai","axe","diamond","crown"];
const GRADING_COMPANIES = ["AFA","CAS","CGC","UKG","VGA"];
const PERMISSION_KEYS = ["canManageCatalog", "canManageBrands", "canManageUsers"];
const PERMISSION_LABELS = {
  canManageCatalog: "Manage catalog (figures + photos)",
  canManageBrands:  "Manage brands + series",
  canManageUsers:   "Manage users",
};
const ALPHA = ["0-9","A","B","C","D","E","F","G","H","I","J","K","L","M",
               "N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];

// ---------- Helpers --------------------------------------------------
function fmtMoney(n, currency) {
  currency = currency || "USD";
  const sym = CURRENCY_SYMBOL[currency] || "$";
  return sym + Math.round(Number(n) || 0).toLocaleString("en-US");
}
// Same as fmtMoney but preserves the exact value the user entered (no rounding).
// Used for fields like original retail where $2.99 should not become $3.
function fmtMoneyExact(n, currency) {
  currency = currency || "USD";
  const sym = CURRENCY_SYMBOL[currency] || "$";
  const v = Number(n);
  if (!isFinite(v)) return sym + "0";
  // Preserve up to 4 decimals if present; strip trailing zeros.
  const fixed = (Math.round(v * 10000) / 10000).toString();
  return sym + Number(fixed).toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 4 });
}
function rarityLabel(r) {
  if (r >= 9) return "Ultra Rare";
  if (r >= 7) return "Very Rare";
  if (r >= 5) return "Uncommon";
  if (r >= 3) return "Common";
  return "Very Common";
}
function openExternal(url) { window.open(url, "_blank", "noopener"); }
function coverUrl(fig, userEntry) {
  if (userEntry && userEntry.customCoverUrl) return userEntry.customCoverUrl;
  if (fig.customCoverUrl) return fig.customCoverUrl;
  return "/covers/" + fig.id + ".svg";
}
function firstChar(name) {
  const c = (name || "").trim().charAt(0).toUpperCase();
  if (/[A-Z]/.test(c)) return c;
  return "0-9";
}
function downloadFile(filename, content, mime) {
  const blob = new Blob([content], { type: mime || "text/csv" });
  const url  = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click(); a.remove();
  URL.revokeObjectURL(url);
}
function csvEscape(v) {
  const s = String(v == null ? "" : v);
  return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
}
function buildCsv(figures, state) {
  const headers = ["id","name","brand","series","group","year","region","upc","figure_number","condition","quantity","loose_value","complete_value","moc_mib_value","graded_value","grading","notes"];
  const lines = [headers.join(",")];
  for (const f of figures) {
    const e = state.collection[f.id] || {};
    lines.push([
      f.id, f.name, f.brand, f.series, f.group || "", f.year, f.region, f.upc || "", f.figure_number || "",
      e.condition || "", e.quantity == null ? "" : e.quantity,
      f.loose_value, f.complete_value, f.moc_value, f.graded_value || 0,
      e.grading ? (e.grading.company + " " + e.grading.grade) : "",
      e.notes || "",
    ].map(csvEscape).join(","));
  }
  return lines.join("\n");
}

// ---------- API client ----------------------------------------------
const api = {
  me:        () => fetch("/api/auth/me").then((r) => r.ok ? r.json() : null),
  login:     (b) => fetch("/api/auth/login",  { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  signup:    (b) => fetch("/api/auth/signup", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  logout:    () => fetch("/api/auth/logout",  { method: "POST" }),
  state:     () => fetch("/api/state").then((r) => r.json()),
  saveState: (s) => fetch("/api/state", { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(s) }),
  catalog:   () => fetch("/api/catalog").then((r) => r.json()),
  upload:    (file) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = async () => {
      const r = await fetch("/api/upload", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({ dataUrl: reader.result, name: file.name }),
      });
      if (!r.ok) return reject(await r.json().catch(() => ({})));
      resolve(await r.json());
    };
    reader.readAsDataURL(file);
  }),
  removeUpload: (url) => fetch("/api/upload", { method: "DELETE", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ url }) }),
  createFigure: (b) => fetch("/api/catalog/figures", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  updateFigure: (id, b) => fetch("/api/catalog/figures/" + id, { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  deleteFigure: (id) => fetch("/api/catalog/figures/" + id, { method: "DELETE" }),
  createBrand:  (b) => fetch("/api/catalog/brands", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  updateBrand:  (n, b) => fetch("/api/catalog/brands/" + encodeURIComponent(n), { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  deleteBrand:  (n, cascade) => fetch("/api/catalog/brands/" + encodeURIComponent(n) + (cascade ? "?cascade=true" : ""), { method: "DELETE" }),
  addSeries:    (brand, series) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/series", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ series }) }),
  delSeries:    (brand, series) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/series/" + encodeURIComponent(series), { method: "DELETE" }),
  addGroup:     (brand, group) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/groups", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ group }) }),
  delGroup:     (brand, group) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/groups/" + encodeURIComponent(group), { method: "DELETE" }),
  toggleHiddenSeries: (brand, series, hidden) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/hiddenSeries", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ series, hidden }) }),
  toggleHiddenGroup:  (brand, group, hidden) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/hiddenGroups", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ group, hidden }) }),
  addManufacturer: (brand, manufacturer) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/manufacturers", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ manufacturer }) }),
  delManufacturer: (brand, manufacturer) => fetch("/api/catalog/brands/" + encodeURIComponent(brand) + "/manufacturers/" + encodeURIComponent(manufacturer), { method: "DELETE" }),
  bulkDelete:   (ids) => fetch("/api/catalog/figures/bulk-delete", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ ids }) }),
  importCsv:    (csv) => fetch("/api/catalog/figures/import", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ csv }) }),
  share:        (token) => fetch("/api/share/" + encodeURIComponent(token)).then((r) => r.ok ? r.json() : null),
  manageShare:  (username, kind, action) => fetch("/api/users/" + encodeURIComponent(username) + "/share", { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ kind, action }) }),
  addRegion:    (region) => fetch("/api/catalog/regions", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ region }) }),
  delRegion:    (region) => fetch("/api/catalog/regions/" + encodeURIComponent(region), { method: "DELETE" }),
  postMessage:  (b) => fetch("/api/messages", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  myMessages:   () => fetch("/api/messages/mine").then((r) => r.json()),
  allMessages:  () => fetch("/api/messages").then((r) => r.json()),
  updateMessage:(id, b) => fetch("/api/messages/" + id, { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  deleteMessage:(id) => fetch("/api/messages/" + id, { method: "DELETE" }),
  // AI
  aiConfig:     () => fetch("/api/admin/ai-config").then((r) => r.json()),
  aiSetConfig:  (b) => fetch("/api/admin/ai-config", { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  aiTest:       () => fetch("/api/admin/ai-test", { method: "POST" }).then((r) => r.json()),
  aiSuggestFromName:  (name) => fetch("/api/ai/suggest-figure-from-name", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ name }) }).then((r) => r.json()),
  aiSuggestFromPhoto: (dataUrl, name) => fetch("/api/ai/suggest-figure-from-photo", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ dataUrl, name }) }).then((r) => r.json()),
  aiSuggestDescription:  (b) => fetch("/api/ai/suggest-description",  { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }).then((r) => r.json()),
  aiSuggestAccessories: (b) => fetch("/api/ai/suggest-accessories", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }).then((r) => r.json()),
  // SerpAPI
  serpapiConfig:    () => fetch("/api/admin/serpapi-config").then((r) => r.json()),
  serpapiSetConfig: (b) => fetch("/api/admin/serpapi-config", { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  serpapiSearch:    (id, force) => fetch("/api/admin/serpapi-search/" + id, { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ force: !!force }) }).then((r) => r.json()),
  serpapiSave:      (id, url, slot) => fetch("/api/admin/serpapi-save/" + id, { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ url, slot }) }).then((r) => r.json()),
  serpapiClearCache:(id) => fetch("/api/admin/serpapi-cache/" + id, { method: "DELETE" }).then((r) => r.json()),
  // User management (admin only)
  listUsers:    () => fetch("/api/users").then((r) => r.json()),
  createUser:   (b) => fetch("/api/users", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  updateUser:   (u, b) => fetch("/api/users/" + encodeURIComponent(u), { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(b) }),
  deleteUser:   (u) => fetch("/api/users/" + encodeURIComponent(u), { method: "DELETE" }),
};

// ---------- Sync hook (per-user state) -------------------------------
function useSyncedState(authed) {
  const [state, setState] = useState(null);
  const [status, setStatus] = useState("idle");
  const dirty = useRef(false);
  const timer = useRef(null);

  useEffect(() => {
    if (!authed) { setState(null); return; }
    setStatus("loading");
    api.state().then((s) => { setState(s); setStatus("saved"); }).catch(() => setStatus("error"));
  }, [authed]);

  useEffect(() => {
    if (!state || !dirty.current) return;
    setStatus("saving");
    clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      api.saveState(state)
        .then((r) => setStatus(r.ok ? "saved" : "error"))
        .catch(() => setStatus("error"));
      dirty.current = false;
    }, 500);
  }, [state]);

  const update = useCallback((fn) => {
    setState((prev) => { const n = typeof fn === "function" ? fn(prev) : fn; dirty.current = true; return n; });
  }, []);

  return [state, update, status];
}

function useCatalog() {
  const [data, setData] = useState({ figures: [], brands: {} });
  const refresh = useCallback(() => api.catalog().then(setData), []);
  useEffect(() => { refresh(); }, [refresh]);
  return [data, refresh];
}

// ---------- Auth screen --------------------------------------------
function AuthScreen({ onAuthed }) {
  const [mode, setMode] = useState("login");
  const [username, setU] = useState("");
  const [password, setP] = useState("");
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setErr(""); setBusy(true);
    try {
      const r = await (mode === "login" ? api.login({ username, password }) : api.signup({ username, password }));
      if (!r.ok) { setErr((await r.json().catch(()=>({}))).error || "failed"); return; }
      // Pull the full /api/auth/me payload (which includes permissions).
      onAuthed(await api.me());
    } finally { setBusy(false); }
  };

  return (
    <div className="auth-shell">
      <div className="auth-card">
        <div className="auth-brand">
          <div className="auth-logo">T</div>
          <div>
            <div style={{ fontWeight: 700, fontSize: 18 }}>Toy Collector</div>
            <div style={{ fontSize: 13, color: "var(--ink-muted)" }}>Action figure collection manager</div>
          </div>
        </div>
        <div className="sub-tabs" style={{ margin: "12px 0" }}>
          <button className={mode === "login" ? "active" : ""} onClick={() => setMode("login")}>Sign in</button>
          <button className={mode === "signup" ? "active" : ""} onClick={() => setMode("signup")}>Create account</button>
        </div>
        <form onSubmit={submit} className="auth-form">
          <label>Username<input type="text" autoComplete="username" value={username} onChange={(e) => setU(e.target.value)} required /></label>
          <label>Password<input type="password" autoComplete={mode === "login" ? "current-password" : "new-password"} value={password} onChange={(e) => setP(e.target.value)} required minLength={4} /></label>
          {err && <div className="auth-err">{err}</div>}
          <button className="btn" disabled={busy}>{busy ? "..." : (mode === "login" ? "Sign in" : "Sign up")}</button>
        </form>
        {mode === "signup" && (
          <div style={{ fontSize: 12, marginTop: 12, color: "var(--ink-muted)" }}>
            The first account becomes the <strong>admin</strong>; later accounts are customers.
          </div>
        )}
      </div>
    </div>
  );
}

// ---------- Gauge / PieChart ---------------------------------------
function Gauge({ percent }) {
  const p = Math.max(0, Math.min(100, percent));
  const start = -Math.PI;
  const end = start + (Math.PI * p) / 100;
  const r = 85, cx = 100, cy = 100;
  const x1 = cx + r * Math.cos(start), y1 = cy + r * Math.sin(start);
  const x2 = cx + r * Math.cos(end),   y2 = cy + r * Math.sin(end);
  const largeArc = p > 50 ? 1 : 0;
  const fillPath = p <= 0.1 ? "" : "M " + x1 + " " + y1 + " A " + r + " " + r + " 0 " + largeArc + " 1 " + x2 + " " + y2 + " L " + cx + " " + cy + " Z";
  return (
    <svg viewBox="0 0 200 110" width="220" height="120">
      <path d={"M " + (cx - r) + " " + cy + " A " + r + " " + r + " 0 0 1 " + (cx + r) + " " + cy + " L " + cx + " " + cy + " Z"} fill="rgba(0,0,0,0.08)" />
      {fillPath && <path d={fillPath} fill="var(--accent)" />}
      <text x="100" y="88" textAnchor="middle" fontSize="22" fontWeight="700" fill="var(--ink)">{p.toFixed(2)}%</text>
    </svg>
  );
}

function PieChart({ slices, total, label }) {
  const r = 110, cx = 130, cy = 130;
  let acc = -Math.PI / 2;
  const paths = slices.map((s) => {
    const angle = (s.value / (total || 1)) * Math.PI * 2;
    const x1 = cx + r * Math.cos(acc), y1 = cy + r * Math.sin(acc);
    acc += angle;
    const x2 = cx + r * Math.cos(acc), y2 = cy + r * Math.sin(acc);
    const large = angle > Math.PI ? 1 : 0;
    const d = angle < 0.001 ? "" : "M " + cx + " " + cy + " L " + x1 + " " + y1 + " A " + r + " " + r + " 0 " + large + " 1 " + x2 + " " + y2 + " Z";
    return { d: d, color: s.color };
  });
  return (
    <svg viewBox="0 0 260 260" width="260" height="260">
      {paths.map((p, i) => p.d && <path key={i} d={p.d} fill={p.color} stroke="var(--panel)" strokeWidth="2" />)}
      <circle cx={cx} cy={cy} r="46" fill="var(--panel)" />
      <text x={cx} y={cy - 4} textAnchor="middle" fontSize="11" fill="var(--ink-muted)">{label || "Total"}</text>
      <text x={cx} y={cy + 14} textAnchor="middle" fontSize="16" fontWeight="700" fill="var(--ink)">{total}</text>
    </svg>
  );
}

// ---------- Sidebar ------------------------------------------------
function Sidebar({ figures, state, onOpenFigure }) {
  const currency = state.settings.currency;
  const collection = state.collection;
  const owned = figures.filter((f) => collection[f.id] && collection[f.id].owned);
  const wanted = figures.filter((f) => collection[f.id] && collection[f.id].wanted);
  const ownedValue = owned.reduce((a, f) => {
    const e = collection[f.id] || {};
    const qty = e.quantity || 1;
    const isComplete = e.condition === "mint" || e.condition === "near_mint" || e.condition === "moc";
    const base = e.condition === "moc" ? f.moc_value : (isComplete ? f.complete_value : f.loose_value);
    return a + base * qty;
  }, 0);
  const wantedValue = wanted.reduce((a, f) => a + f.loose_value, 0);
  const gradedOwned = owned.filter((f) => collection[f.id] && collection[f.id].grading && collection[f.id].grading.company);
  const gradedValue = gradedOwned.reduce((a, f) => a + (f.graded_value || 0), 0);
  const completion = figures.length ? (owned.length / figures.length) * 100 : 0;
  const mostRare      = [...owned].sort((a, b) => b.rarity - a.rarity)[0];
  const mostValuable  = [...owned].sort((a, b) => b.complete_value - a.complete_value)[0];
  return (
    <aside className="sidebar">
      <div className="sidebar-inner">
        <h3>Collection analysis</h3>
        <div className="gauge-wrap">
          <Gauge percent={completion} />
          <div className="gauge-range"><span>0</span><span>{figures.length}</span></div>
        </div>
        <div className="totals">
          <div className="total-card"><div className="count">{owned.length} figures</div><div className="label">Collection</div><div className="value">{fmtMoney(ownedValue, currency)}</div></div>
          <div className="total-card"><div className="count">{wanted.length} figures</div><div className="label">Wanted</div><div className="value">{fmtMoney(wantedValue, currency)}</div></div>
        </div>
        {gradedOwned.length > 0 && (
          <div className="total-card" style={{ marginBottom: 16 }}>
            <div className="count">{gradedOwned.length} graded</div>
            <div className="label">Graded avg eBay</div>
            <div className="value">{fmtMoney(gradedValue, currency)}</div>
          </div>
        )}
        <div className="showcase">
          <div className="item">
            <span className="kicker">Most rare</span>
            {mostRare ? <img src={coverUrl(mostRare, collection[mostRare.id])} alt={mostRare.name} onClick={() => onOpenFigure(mostRare.id)} /> : <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>--</div>}
          </div>
          <div className="item">
            <span className="kicker">Most valuable</span>
            {mostValuable ? <img src={coverUrl(mostValuable, collection[mostValuable.id])} alt={mostValuable.name} onClick={() => onOpenFigure(mostValuable.id)} /> : <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>--</div>}
          </div>
        </div>
      </div>
    </aside>
  );
}

// ---------- Alpha bar ----------------------------------------------
function AlphaBar({ figures, active, onPick }) {
  const present = useMemo(() => new Set(figures.map((f) => firstChar(f.name))), [figures]);
  return (
    <div className="alpha-bar">
      {ALPHA.map((c) => (
        <button key={c}
                className={(active === c ? "active " : "") + (!present.has(c) ? "empty" : "")}
                disabled={!present.has(c) && active !== c}
                onClick={() => onPick(active === c ? null : c)}>
          {c}
        </button>
      ))}
    </div>
  );
}

// ---------- Grid view ---------------------------------------------
function Grid({ figures, state, onOpen }) {
  return (
    <div className="grid">
      {figures.map((f) => {
        const e = state.collection[f.id] || {};
        const cls = "card" + (e.owned ? " owned" : "") + (!e.owned && e.wanted ? " wanted" : "") + (state.excluded.indexOf(f.id) >= 0 ? " excluded" : "");
        const cond = CONDITION_BY_ID[e.condition];
        return (
          <div key={f.id} className={cls} onClick={() => onOpen(f.id)}>
            <div className="cover"><img src={coverUrl(f, e)} alt={f.name} loading="lazy" /></div>
            <div className="caption">{f.name}</div>
            {e.quantity > 1 && <span className="pill">x{e.quantity}</span>}
            {!e.owned && e.wanted && <span className="pill" style={{ background: "var(--gold)", color: "#000" }}>WANT</span>}
            {e.owned && cond && <span className="pill" style={{ background: cond.color, color: "#000" }}>{cond.label}</span>}
          </div>
        );
      })}
      {figures.length === 0 && <div style={{ color: "var(--ink-muted)", padding: 30, gridColumn: "1 / -1", textAlign: "center" }}>No figures match this view.</div>}
    </div>
  );
}

// ---------- List view ---------------------------------------------
function ListView({ figures, state, onOpen }) {
  const currency = state.settings.currency;
  return (
    <div className="list-view">
      {figures.map((f) => {
        const e = state.collection[f.id] || {};
        const cond = CONDITION_BY_ID[e.condition];
        return (
          <div key={f.id} className="list-row" onClick={() => onOpen(f.id)}>
            <div className="thumb"><img src={coverUrl(f, e)} alt="" /></div>
            <div className="meta">
              <div className="name">{f.name}{e.quantity > 1 ? " x" + e.quantity : ""}</div>
              <div className="sub">{f.brand}{f.series ? " - " + f.series : ""}{f.figure_number ? " - #" + f.figure_number : ""} - {f.year} - {f.region}</div>
            </div>
            <div>{cond ? <span className="condition-chip" style={{ background: cond.color, color: "#000" }}>{cond.label}</span> : (e.wanted ? <span className="condition-chip" style={{ background: "var(--gold)", color: "#000" }}>Wanted</span> : "")}</div>
            <div className="price">{fmtMoney(e.condition === "moc" ? f.moc_value : f.complete_value, currency)}</div>
          </div>
        );
      })}
      {figures.length === 0 && <div style={{ padding: 30, textAlign: "center", color: "var(--ink-muted)" }}>No figures match this view.</div>}
    </div>
  );
}

// ---------- Photo uploader ----------------------------------------
function PhotoUploader({ currentUrl, onUploaded, onRemoved, label }) {
  label = label || "Photo";
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const ref = useRef(null);
  const onChange = async (e) => {
    const file = e.target.files[0]; if (!file) return;
    setBusy(true); setErr("");
    try { onUploaded((await api.upload(file)).url); }
    catch (ex) { setErr(ex.error || "upload failed"); }
    finally { setBusy(false); e.target.value = ""; }
  };
  return (
    <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
      <input ref={ref} type="file" accept="image/*" style={{ display: "none" }} onChange={onChange} />
      <button className="btn secondary" onClick={() => ref.current && ref.current.click()} disabled={busy}>{busy ? "..." : "Upload " + label}</button>
      {currentUrl && <button className="btn danger" onClick={() => { onRemoved(); api.removeUpload(currentUrl); }}>Remove</button>}
      {err && <span style={{ fontSize: 12, color: "var(--red)" }}>{err}</span>}
    </div>
  );
}

// ---------- UPC scanner --------------------------------------------
function ScannerModal({ onClose, onScan }) {
  useEffect(() => {
    let html5 = null; let cancelled = false;
    const start = async () => {
      if (!window.Html5Qrcode) { alert("Scanner library failed to load."); return; }
      html5 = new window.Html5Qrcode("qr-reader");
      try {
        await html5.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 150 } },
          (text) => { if (!cancelled) { cancelled = true; html5.stop().then(() => onScan(text)); } },
          () => {});
      } catch (e) {
        alert("Could not start camera: " + (e && e.message ? e.message : e) + "\n\nYou can still type the UPC manually.");
      }
    };
    start();
    return () => { cancelled = true; if (html5 && html5.isScanning) html5.stop().catch(() => {}); };
  }, []);
  const [manual, setManual] = useState("");
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ maxWidth: 520 }} onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <div style={{ fontWeight: 700 }}>Scan UPC</div>
          <button className="close" onClick={onClose}>x</button>
        </div>
        <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
          <div className="scan-area"><div id="qr-reader" /></div>
          <div style={{ marginTop: 12 }}>
            <div style={{ fontSize: 13, color: "var(--ink-muted)", marginBottom: 6 }}>Or enter UPC manually:</div>
            <div style={{ display: "flex", gap: 8 }}>
              <input style={{ flex: 1 }} value={manual} onChange={(e) => setManual(e.target.value)} placeholder="0123456789012" />
              <button className="btn" onClick={() => onScan(manual.trim())} disabled={!manual.trim()}>Search</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- Figure detail ------------------------------------------
function FigureDetail({ figure, state, update, role, brands, onClose, onCatalogChange }) {
  const [lightbox, setLightbox] = useState(null); // number (index into lightboxItems) | null
  const e = (figure && state.collection[figure.id]) || {};
  const seriesOptions = (figure && brands[figure.brand] && brands[figure.brand].series) || [];
  const groupOptions  = (figure && brands[figure.brand] && brands[figure.brand].groups) || [];
  const [reportOpen, setReportOpen] = useState(false);
  const [desc, setDesc] = useState((figure && figure.description) || "");
  const [series, setSeries] = useState((figure && figure.series) || "");
  const [group, setGroup] = useState((figure && figure.group) || "");
  const [upc, setUpc] = useState((figure && figure.upc) || "");
  useEffect(() => {
    if (!figure) return;
    setDesc(figure.description || ""); setSeries(figure.series || ""); setGroup(figure.group || ""); setUpc(figure.upc || "");
  }, [figure && figure.id]);

  if (!figure) return null;

  // Collect every image associated with this figure in a consistent order.
  const lightboxItems = (() => {
    const items = [];
    const cv = coverUrl(figure, e);
    if (cv) items.push({ url: cv, label: "Cover" });
    const photos = figure.photos || {};
    if (photos.front)       items.push({ url: photos.front,       label: "Front" });
    if (photos.back)        items.push({ url: photos.back,        label: "Back" });
    if (photos.loose)       items.push({ url: photos.loose,       label: "Loose" });
    if (photos.accessories) items.push({ url: photos.accessories, label: "Accessories photo" });
    if (figure.variantImage) items.push({ url: figure.variantImage, label: "Variant" });
    (figure.accessories || []).forEach((a) => { if (a.imageUrl) items.push({ url: a.imageUrl, label: a.name }); });
    return items;
  })();
  const openLightbox = (url) => {
    const i = lightboxItems.findIndex((item) => item.url === url);
    setLightbox(i >= 0 ? i : 0);
  };

  const currency = state.settings.currency;
  const setEntry = (patch) => update((s) => {
    const next = { ...s, collection: { ...s.collection, [figure.id]: { ...(s.collection[figure.id] || {}), ...patch } } };
    return next;
  });
  const addTxn = (kind) => {
    const amt = Number(prompt((kind === "purchase" ? "Purchase" : "Sale") + " amount (" + currency + ")", String(figure.loose_value))) || 0;
    if (!amt) return;
    const note = prompt("Note (optional)", "") || "";
    update((s) => ({ ...s, budget: [{ id: Date.now(), figureId: figure.id, kind: kind, amount: amt, note: note, date: new Date().toISOString().slice(0,10) }, ...s.budget] }));
  };
  const youtubeUrl = "https://www.youtube.com/results?search_query=" + encodeURIComponent(figure.youtube_query);
  const ebayDomain = ({ US:"com", UK:"co.uk", DE:"de", AU:"com.au", CA:"ca", FR:"fr" })[state.settings.ebayRegion] || "com";
  const ebayUrl = "https://www.ebay." + ebayDomain + "/sch/i.html?_nkw=" + encodeURIComponent(figure.ebay_query);
  const myTxns = state.budget.filter((b) => b.figureId === figure.id);
  const saveAdmin = async () => { await api.updateFigure(figure.id, { description: desc, series: series, group: group, upc: upc }); onCatalogChange(); };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(ev) => ev.stopPropagation()}>
        <div className="modal-head">
          <div>
            <div style={{ fontWeight: 700 }}>{figure.name}</div>
            <div style={{ fontSize: 13, color: "var(--ink-muted)" }}>{figure.brand}{figure.series ? " - " + figure.series : ""}{figure.group ? " - " + figure.group : ""} - {figure.year} - {figure.region}{figure.figure_number ? " - #" + figure.figure_number : ""}{figure.upc ? " - UPC " + figure.upc : ""}{figure.variantOf ? " - Variant" : ""}</div>
          </div>
          <button className="close" onClick={onClose}>x</button>
        </div>
        <div className="modal-body">
          <div>
            <img className="detail-cover" src={coverUrl(figure, e)} alt={figure.name}
                 onClick={() => openLightbox(coverUrl(figure, e))}
                 style={{ cursor: "zoom-in" }}
                 title="Click to enlarge" />
            <PhotoGallery photos={figure.photos} onOpen={openLightbox} />
            {role === "admin" && (
              <div style={{ marginTop: 8, fontSize: 12, color: "var(--ink-muted)" }}>
                Manage photos for this figure in <strong>More -&gt; Manage Catalog</strong>.
              </div>
            )}
          </div>
          <div>
            <table className="pricing-table">
              <thead><tr><th></th><th>Main Listing</th><th>My Listing</th><th>Other users</th></tr></thead>
              <tbody>
                <tr><td>Rarity</td><td>{figure.rarity}/10 - {rarityLabel(figure.rarity)}</td><td>--</td><td>{figure.user_rating} *</td></tr>
                <tr><td>Original Retail Price</td><td>{fmtMoneyExact(figure.original_retail_value || 0, currency)}</td><td>--</td><td>--</td></tr>
                <tr><td>Loose</td><td>{fmtMoney(figure.loose_value, currency)}</td><td>--</td><td>{fmtMoney(figure.loose_value, currency)}</td></tr>
                <tr><td>Complete</td><td>{fmtMoney(figure.complete_value, currency)}</td><td>--</td><td>{fmtMoney(figure.complete_value, currency)}</td></tr>
                <tr><td>MOC/MIB</td><td>{fmtMoney(figure.moc_value, currency)}</td><td>--</td><td>{fmtMoney(figure.moc_value, currency)}</td></tr>
                <tr><td>Graded avg eBay</td><td>{fmtMoney(figure.graded_value || 0, currency)}</td><td>{e.grading ? (e.grading.company + " " + e.grading.grade) : "--"}</td><td>{fmtMoney(figure.graded_value || 0, currency)}</td></tr>
              </tbody>
            </table>

            <div className="ext-btns">
              <button className="ext-btn youtube" onClick={() => openExternal(youtubeUrl)}>YouTube</button>
              <button className="ext-btn ebay" onClick={() => openExternal(ebayUrl)}>eBay ({state.settings.ebayRegion})</button>
            </div>

            <div className="toggle-row">
              <div style={{ display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 200 }}>
                <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Series</span>
                {role === "admin" ? (
                  <select value={series} onChange={(ev) => setSeries(ev.target.value)}>
                    <option value="">-- none --</option>
                    {seriesOptions.map((s) => <option key={s}>{s}</option>)}
                  </select>
                ) : (
                  <span>{figure.series || <em style={{ color: "var(--ink-muted)" }}>not assigned</em>}</span>
                )}
              </div>
              <div style={{ display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 160 }}>
                <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Group / Wave</span>
                {role === "admin" ? (
                  <select value={group} onChange={(ev) => setGroup(ev.target.value)}>
                    <option value="">-- none --</option>
                    {groupOptions.map((g) => <option key={g}>{g}</option>)}
                  </select>
                ) : (
                  <span>{figure.group || <em style={{ color: "var(--ink-muted)" }}>not assigned</em>}</span>
                )}
              </div>
              <div style={{ display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 160 }}>
                <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>UPC</span>
                {role === "admin" ? (
                  <input value={upc} onChange={(ev) => setUpc(ev.target.value)} placeholder="UPC barcode digits" />
                ) : (
                  <span>{figure.upc || <em style={{ color: "var(--ink-muted)" }}>--</em>}</span>
                )}
              </div>
              {role === "admin" && <button className="btn" onClick={saveAdmin}>Save admin fields</button>}
            </div>

            <div className="toggle-row">
              <label className="switch"><input type="checkbox" checked={!!e.owned} onChange={(ev) => setEntry({ owned: ev.target.checked, quantity: e.quantity || 1 })} /><span className="track" /> In Collection</label>
              <label className="switch"><input type="checkbox" checked={!!e.wanted} onChange={(ev) => setEntry({ wanted: ev.target.checked })} /><span className="track" /> Wanted</label>
              <label className="switch"><input type="checkbox" checked={!!e.forSale} onChange={(ev) => setEntry({ forSale: ev.target.checked })} /><span className="track" /> For sale</label>
              <label className="switch"><input type="checkbox" checked={state.excluded.indexOf(figure.id) >= 0} onChange={(ev) => update((s) => ({ ...s, excluded: ev.target.checked ? Array.from(new Set([...s.excluded, figure.id])) : s.excluded.filter((id) => id !== figure.id) }))} /><span className="track" /> Exclude</label>
            </div>

            <div className="condition-row">
              <span style={{ fontSize: 13 }}>Condition:</span>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 6, flex: 1 }}>
                {CONDITIONS.map((c) => (
                  <button key={c.id} onClick={() => setEntry({ condition: c.id })} className="condition-chip"
                          style={{ background: e.condition === c.id ? c.color : "var(--panel)", color: e.condition === c.id ? "#000" : "var(--ink)", border: "1.5px solid " + c.color }}>
                    {c.label}
                  </button>
                ))}
              </div>
              <span style={{ fontSize: 13, marginLeft: 8 }}># Copies:</span>
              <input type="number" min="0" value={e.quantity == null ? 1 : e.quantity} style={{ width: 70 }} onChange={(ev) => setEntry({ quantity: Math.max(0, Number(ev.target.value)) })} />
              <button className="btn" onClick={() => setEntry({ quantity: (e.quantity == null ? 1 : e.quantity) + 1, owned: true })}>Duplicate</button>
            </div>

            {(figure.variantOf || figure.variantNote || figure.variantImage) && (
              <div style={{ padding: 12, background: "var(--panel-alt)", borderRadius: "var(--radius)", fontSize: 14, marginBottom: 10, display: "flex", gap: 12, alignItems: "flex-start" }}>
                {figure.variantImage && (
                  <img src={figure.variantImage} alt="variant" style={{ width: 90, height: 90, objectFit: "cover", borderRadius: 8, cursor: "zoom-in" }} onClick={() => openLightbox(figure.variantImage)} />
                )}
                <div style={{ flex: 1 }}>
                  <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 4 }}>Variant</div>
                  {figure.variantOf != null && <div style={{ fontSize: 12 }}>Variant of figure id <strong>{figure.variantOf}</strong></div>}
                  {figure.variantNote && <div style={{ marginTop: 4 }}>{figure.variantNote}</div>}
                </div>
              </div>
            )}
            <FigureVariantsList figure={figure} />
            {figure.description && (
              <div style={{ padding: 12, background: "var(--panel-alt)", borderRadius: "var(--radius)", fontSize: 14, marginBottom: 10 }}>
                <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 4 }}>About this figure</div>
                {figure.description}
              </div>
            )}

            {(figure.accessories && figure.accessories.length > 0) && (
              <div style={{ padding: 12, background: "var(--panel-alt)", borderRadius: "var(--radius)", fontSize: 14, marginBottom: 10 }}>
                <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 8 }}>
                  Accessories ({figure.accessories.length})
                  {e.owned && (
                    <span style={{ marginLeft: 8, color: "var(--green)" }}>
                      — {((e.accessories || []).length)} / {figure.accessories.length} checked
                    </span>
                  )}
                </div>
                <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
                  {figure.accessories.map((a, i) => {
                    const owned = e.owned;
                    const haveIt = owned && (e.accessories || []).includes(a.name);
                    return (
                      <div key={i}
                        onClick={() => {
                          if (!owned) return;
                          const cur = e.accessories || [];
                          const next = haveIt ? cur.filter((n) => n !== a.name) : [...cur, a.name];
                          setEntry({ accessories: next });
                        }}
                        title={owned ? (haveIt ? "Click to uncheck" : "Click to mark as owned") : "Mark figure as owned to track accessories"}
                        style={{
                          display: "flex", alignItems: "center", gap: 5,
                          padding: "4px 10px", borderRadius: 20, fontSize: 13,
                          border: "1.5px solid " + (haveIt ? "var(--green)" : "var(--bar-border)"),
                          background: haveIt ? "rgba(52,199,89,0.12)" : "var(--panel)",
                          cursor: owned ? "pointer" : "default",
                          opacity: owned ? 1 : 0.75,
                        }}>
                        {a.imageUrl && (
                          <img src={a.imageUrl} alt={a.name}
                            style={{ width: 22, height: 22, borderRadius: 4, objectFit: "cover" }}
                            onClick={(ev) => { ev.stopPropagation(); openLightbox(a.imageUrl); }}
                          />
                        )}
                        {owned && (
                          <span style={{ fontSize: 14, lineHeight: 1, color: haveIt ? "var(--green)" : "var(--ink-muted)" }}>
                            {haveIt ? "✓" : "○"}
                          </span>
                        )}
                        <span>{a.name}</span>
                      </div>
                    );
                  })}
                </div>
                {e.owned && (
                  <div style={{ fontSize: 11, color: "var(--ink-muted)", marginTop: 8 }}>
                    Tap an accessory to mark whether you have it. Helps you know if your copy is truly complete.
                  </div>
                )}
              </div>
            )}

            {role === "admin" && (
              <div style={{ marginBottom: 10 }}>
                <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 4 }}>Catalog description (admin)</div>
                <textarea className="notes" value={desc} onChange={(ev) => setDesc(ev.target.value)} />
                <button className="btn secondary" onClick={saveAdmin}>Save description</button>
              </div>
            )}

            <div className="toggle-row">
              <div style={{ display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 160 }}>
                <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Grading company</span>
                <select value={(e.grading && e.grading.company) || ""}
                        onChange={(ev) => setEntry({ grading: ev.target.value ? { company: ev.target.value, grade: (e.grading && e.grading.grade) || 85 } : null })}>
                  <option value="">-- not graded --</option>
                  {GRADING_COMPANIES.map((g) => <option key={g}>{g}</option>)}
                </select>
              </div>
              <div style={{ display: "flex", flexDirection: "column", gap: 4, minWidth: 90 }}>
                <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Grade</span>
                <input type="number" min="0" max="100" step="1"
                       value={(e.grading && e.grading.grade) || ""}
                       disabled={!(e.grading && e.grading.company)}
                       onChange={(ev) => setEntry({ grading: { company: (e.grading && e.grading.company) || "AFA", grade: Number(ev.target.value) } })}
                       style={{ width: 80 }} />
              </div>
              {e.grading && e.grading.company && (
                <span style={{ fontSize: 13, color: "var(--ink-muted)" }}>
                  Average graded eBay value: <strong>{fmtMoney(figure.graded_value || 0, currency)}</strong>
                </span>
              )}
            </div>
            <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 4 }}>My notes</div>
            <textarea className="notes" placeholder="Personal notes about this figure..." value={e.notes || ""} onChange={(ev) => setEntry({ notes: ev.target.value })} />

            <div style={{ display: "flex", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
              <button className="btn danger" onClick={() => addTxn("purchase")}>+ Log Purchase</button>
              <button className="btn" style={{ background: "var(--green)" }} onClick={() => addTxn("sale")}>+ Log Sale</button>
              <button className="btn secondary" onClick={() => setReportOpen(true)}>Report issue</button>
            </div>

            {myTxns.length > 0 && (
              <div className="txn-log">
                <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 4 }}>Transactions</div>
                {myTxns.map((t) => (
                  <div key={t.id} className={"txn " + t.kind}>
                    <span>{t.date}</span>
                    <span>{t.kind === "purchase" ? "-" : "+"} {fmtMoney(t.amount, currency)}</span>
                    <span style={{ flex: 1, textAlign: "right", opacity: 0.92 }}>{t.note}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>
      </div>
      {lightbox !== null && <FigureLightbox items={lightboxItems} startIdx={lightbox} onClose={() => setLightbox(null)} />}
      {reportOpen && <ReportIssueModal figure={figure} onClose={() => setReportOpen(false)} />}
    </div>
  );
}

// ---------- FigureLightbox: multi-image viewer with nav arrows ----
function FigureLightbox({ items, startIdx, onClose }) {
  const [idx, setIdx] = useState(typeof startIdx === "number" ? startIdx : 0);
  const hasMultiple = items.length > 1;

  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      if (!hasMultiple) return;
      if (e.key === "ArrowLeft")  setIdx((i) => (i - 1 + items.length) % items.length);
      if (e.key === "ArrowRight") setIdx((i) => (i + 1) % items.length);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [items.length, hasMultiple, onClose]);

  if (!items.length) return null;
  const cur = items[idx];

  const arrowStyle = {
    position: "fixed", top: "50%", transform: "translateY(-50%)",
    background: "rgba(0,0,0,0.6)", border: "none", color: "#fff",
    fontSize: 42, lineHeight: 1, width: 52, height: 72,
    borderRadius: 8, cursor: "pointer", zIndex: 10001,
    display: "flex", alignItems: "center", justifyContent: "center",
    userSelect: "none",
  };

  return (
    <div className="lightbox-backdrop" onClick={onClose}>
      {/* Close button — clear label so user knows it returns to figure detail */}
      <button
        className="lightbox-close"
        onClick={onClose}
        aria-label="Close"
        style={{ display: "flex", alignItems: "center", gap: 5, fontSize: 14,
                 padding: "5px 14px", borderRadius: 20,
                 background: "rgba(0,0,0,0.6)", border: "none", color: "#fff", cursor: "pointer" }}
      >
        ✕&nbsp;Close
      </button>

      {/* Prev arrow */}
      {hasMultiple && (
        <button style={{ ...arrowStyle, left: 14 }}
                onClick={(ev) => { ev.stopPropagation(); setIdx((i) => (i - 1 + items.length) % items.length); }}
                aria-label="Previous image">
          ‹
        </button>
      )}

      {/* Image + label + thumbnails */}
      <div onClick={(ev) => ev.stopPropagation()}
           style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10, maxWidth: "90vw" }}>
        {hasMultiple && (
          <div style={{ color: "#fff", fontSize: 13, opacity: 0.85, textAlign: "center" }}>
            <strong>{cur.label}</strong>&ensp;·&ensp;{idx + 1}&thinsp;/&thinsp;{items.length}
            <span style={{ marginLeft: 10, fontSize: 11, opacity: 0.7 }}>← → or click arrows to navigate</span>
          </div>
        )}

        <img className="lightbox-img" src={cur.url} alt={cur.label || ""}
             style={{ maxHeight: hasMultiple ? "calc(82vh - 80px)" : undefined }} />

        {/* Thumbnail strip */}
        {hasMultiple && (
          <div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "center", maxWidth: "90vw", paddingBottom: 4 }}>
            {items.map((item, i) => (
              <div key={i} onClick={() => setIdx(i)} title={item.label}
                   style={{ width: 46, height: 46, borderRadius: 6, overflow: "hidden",
                            cursor: "pointer", flexShrink: 0,
                            border: i === idx ? "2.5px solid #fff" : "2.5px solid rgba(255,255,255,0.25)",
                            opacity: i === idx ? 1 : 0.5, transition: "opacity 0.15s, border-color 0.15s" }}>
                <img src={item.url} alt={item.label}
                     style={{ width: "100%", height: "100%", objectFit: "cover" }} />
              </div>
            ))}
          </div>
        )}
      </div>

      {/* Next arrow */}
      {hasMultiple && (
        <button style={{ ...arrowStyle, right: 14 }}
                onClick={(ev) => { ev.stopPropagation(); setIdx((i) => (i + 1) % items.length); }}
                aria-label="Next image">
          ›
        </button>
      )}
    </div>
  );
}

// ---------- FigureVariantsList: lists figures whose variantOf == this figure.id
function FigureVariantsList({ figure }) {
  const [variants, setVariants] = useState([]);
  useEffect(() => {
    api.catalog().then((c) => setVariants((c.figures || []).filter((f) => f.variantOf === figure.id)));
  }, [figure.id]);
  if (!variants.length) return null;
  return (
    <div style={{ padding: 10, background: "var(--panel-alt)", borderRadius: "var(--radius)", marginBottom: 10 }}>
      <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 6 }}>{variants.length} variant(s) of this figure</div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        {variants.map((v) => (
          <div key={v.id} style={{ width: 100, textAlign: "center" }}>
            <img src={v.variantImage || coverUrl(v)} alt={v.name} style={{ width: 100, height: 100, objectFit: "cover", borderRadius: 8, background: "var(--panel)" }} />
            <div style={{ fontSize: 11, marginTop: 2 }}>{v.name}</div>
            {v.variantNote && <div style={{ fontSize: 10, color: "var(--ink-muted)" }}>{v.variantNote}</div>}
          </div>
        ))}
      </div>
    </div>
  );
}

// ---------- ReportIssueModal -------------------------------------
function ReportIssueModal({ figure, onClose }) {
  const [subject, setSubject] = useState(figure ? "Issue with " + figure.name : "");
  const [bodyText, setBody] = useState("");
  const [photoUrl, setPhotoUrl] = useState(null);
  const [busy, setBusy] = useState(false);
  const submit = async () => {
    if (!subject.trim() || !bodyText.trim()) return alert("Subject and body required");
    setBusy(true);
    try {
      const r = await api.postMessage({ subject: subject.trim(), body: bodyText.trim(), figureId: figure ? figure.id : null, photoUrl });
      if (!r.ok) return alert((await r.json()).error || "failed");
      alert("Message sent. Admin will review it.");
      onClose();
    } finally { setBusy(false); }
  };
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ maxWidth: 720, width: "94vw" }} onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <div style={{ fontWeight: 700 }}>Report issue / Suggest correction</div>
          <button className="close" onClick={onClose}>x</button>
        </div>
        <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
          <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
            {figure && <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>About figure: <strong>{figure.name}</strong> (id {figure.id})</div>}
            <label style={{ fontSize: 12, color: "var(--ink-muted)", display: "flex", flexDirection: "column", gap: 4 }}>
              Subject
              <input value={subject} onChange={(e) => setSubject(e.target.value)} maxLength={200} style={{ width: "100%", fontSize: 15, padding: "10px 12px" }} />
            </label>
            <label style={{ fontSize: 12, color: "var(--ink-muted)" }}>Details<textarea className="notes" value={bodyText} onChange={(e) => setBody(e.target.value)} maxLength={4000} placeholder="What's the issue? e.g. wrong year, missing variant, broken UPC..." style={{ minHeight: 140 }} /></label>
            <PhotoUploader currentUrl={photoUrl} onUploaded={(u) => setPhotoUrl(u)} onRemoved={() => setPhotoUrl(null)} label="optional photo" />
            <div className="btn-row"><button className="btn" disabled={busy} onClick={submit}>Send</button><button className="btn secondary" onClick={onClose}>Cancel</button></div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- MyMessages (any signed-in user) ---------------------
function MyMessages() {
  const [messages, setMessages] = useState([]);
  useEffect(() => { api.myMessages().then((j) => setMessages(j.messages || [])); }, []);
  return (
    <div className="page">
      <h2>My messages</h2>
      {messages.length === 0 && <p style={{ color: "var(--ink-muted)" }}>No messages yet. Use the "Report issue" button on any figure.</p>}
      {messages.map((m) => (
        <div key={m.id} style={{ padding: 12, background: "var(--panel-alt)", borderRadius: "var(--radius)", marginBottom: 8 }}>
          <div style={{ display: "flex", justifyContent: "space-between" }}>
            <strong>{m.subject}</strong>
            <span className="role-tag" style={{ background: m.status === "resolved" ? "var(--green)" : m.status === "dismissed" ? "var(--ink-muted)" : "var(--gold)", color: "#000" }}>{m.status}</span>
          </div>
          <div style={{ fontSize: 11, color: "var(--ink-muted)" }}>{new Date(m.createdAt).toLocaleString()}</div>
          <div style={{ marginTop: 6, whiteSpace: "pre-wrap" }}>{m.body}</div>
          {m.photoUrl && <img src={m.photoUrl} style={{ maxWidth: 220, marginTop: 8, borderRadius: 8 }} />}
          {m.adminReply && <div style={{ marginTop: 8, padding: 8, background: "var(--panel)", borderRadius: 8, fontSize: 13 }}><strong>Admin reply:</strong> {m.adminReply}</div>}
        </div>
      ))}
    </div>
  );
}

// ---------- MessageCenter (admin) -------------------------------
function MessageCenter({ figures }) {
  const [messages, setMessages] = useState([]);
  const [filter, setFilter] = useState("open");
  const load = () => api.allMessages().then((j) => setMessages(j.messages || []));
  useEffect(() => { load(); }, []);
  const setStatus = async (id, status) => { await api.updateMessage(id, { status }); load(); };
  const reply = async (id) => {
    const r = prompt("Reply to message:");
    if (r == null) return;
    await api.updateMessage(id, { adminReply: r });
    load();
  };
  const remove = async (id) => {
    if (!confirm("Delete this message?")) return;
    await api.deleteMessage(id); load();
  };
  const list = filter === "all" ? messages : messages.filter((m) => m.status === filter);
  return (
    <div className="page">
      <h2>Message Center <span className="role-tag admin">admin</span></h2>
      <div className="sub-tabs">
        {["open","resolved","dismissed","all"].map((s) => (
          <button key={s} className={filter === s ? "active" : ""} onClick={() => setFilter(s)}>
            {s} ({s === "all" ? messages.length : messages.filter((m) => m.status === s).length})
          </button>
        ))}
      </div>
      {list.length === 0 && <p style={{ color: "var(--ink-muted)" }}>No messages.</p>}
      {list.map((m) => {
        const fig = figures.find((f) => f.id === m.figureId);
        return (
          <div key={m.id} style={{ padding: 12, background: "var(--panel-alt)", borderRadius: "var(--radius)", marginBottom: 10 }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 8 }}>
              <div>
                <strong>{m.subject}</strong>
                <div style={{ fontSize: 11, color: "var(--ink-muted)" }}>From <strong>{m.from}</strong> - {new Date(m.createdAt).toLocaleString()}{fig ? " - re: " + fig.name + " (id " + fig.id + ")" : ""}</div>
              </div>
              <span className="role-tag" style={{ background: m.status === "resolved" ? "var(--green)" : m.status === "dismissed" ? "var(--ink-muted)" : "var(--gold)", color: "#000" }}>{m.status}</span>
            </div>
            <div style={{ marginTop: 8, whiteSpace: "pre-wrap" }}>{m.body}</div>
            {m.photoUrl && <img src={m.photoUrl} style={{ maxWidth: 240, marginTop: 8, borderRadius: 8 }} />}
            {m.adminReply && <div style={{ marginTop: 8, padding: 8, background: "var(--panel)", borderRadius: 8, fontSize: 13 }}><strong>Reply:</strong> {m.adminReply}</div>}
            <div className="btn-row" style={{ marginTop: 10 }}>
              <button className="btn secondary" onClick={() => reply(m.id)}>Reply</button>
              {m.status !== "resolved" && <button className="btn" style={{ background: "var(--green)" }} onClick={() => setStatus(m.id, "resolved")}>Mark resolved</button>}
              {m.status !== "dismissed" && <button className="btn secondary" onClick={() => setStatus(m.id, "dismissed")}>Dismiss</button>}
              {m.status !== "open" && <button className="btn secondary" onClick={() => setStatus(m.id, "open")}>Reopen</button>}
              <button className="btn danger" onClick={() => remove(m.id)}>Delete</button>
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ---------- ManageRegions (admin) -------------------------------
function ManageRegions() {
  const [regions, setRegions] = useState([]);
  const [draft, setDraft] = useState("");
  const load = () => fetch("/api/catalog/regions").then((r) => r.json()).then((j) => setRegions(j.regions || []));
  useEffect(() => { load(); }, []);
  const add = async () => {
    const v = draft.trim().toUpperCase(); if (!v) return;
    const r = await api.addRegion(v); if (!r.ok) return alert((await r.json()).error || "failed");
    setDraft(""); load();
  };
  const remove = async (region) => {
    if (!confirm("Remove region '" + region + "'? Existing figures keep that region as text but it won't be in dropdowns anymore.")) return;
    await api.delRegion(region); load();
  };
  return (
    <div className="page">
      <h2>Manage Regions <span className="role-tag admin">admin</span></h2>
      <div className="series-list">
        {regions.length === 0 && <span style={{ color: "var(--ink-muted)", fontSize: 13 }}>No regions defined.</span>}
        {regions.map((r) => (
          <span key={r} className="series-tag">{r}<button onClick={() => remove(r)} title="Remove">x</button></span>
        ))}
      </div>
      <div style={{ display: "flex", gap: 6, marginTop: 8 }}>
        <input placeholder="New region (e.g. JP, UK, CA)" value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => e.key === "Enter" && add()} style={{ flex: 1 }} />
        <button className="btn" onClick={add} disabled={!draft.trim()}>+ Add region</button>
      </div>
    </div>
  );
}

// ---------- Trophy / Budget / Brand-Overview / Cover Mode ---------
function TrophyRoom({ figures, state, onOpen }) {
  const [mode, setMode] = useState("rare");
  const owned = figures.filter((f) => state.collection[f.id] && state.collection[f.id].owned);
  const sorted = mode === "rare" ? [...owned].sort((a,b) => b.rarity - a.rarity) : [...owned].sort((a,b) => b.complete_value - a.complete_value);
  return (
    <div className="page">
      <h2>Trophy Room</h2>
      <div className="sub-tabs">
        <button className={mode === "rare" ? "active" : ""} onClick={() => setMode("rare")}>Most rare</button>
        <button className={mode === "value" ? "active" : ""} onClick={() => setMode("value")}>Most valuable</button>
      </div>
      {sorted.length === 0 && <p style={{ color: "var(--ink-muted)" }}>Mark figures as owned to see them here.</p>}
      <div className="trophy-grid">
        {sorted.slice(0, 16).map((f) => (
          <div key={f.id} className="trophy-card" onClick={() => onOpen(f.id)}>
            <div className="cover"><img src={coverUrl(f, state.collection[f.id])} alt={f.name} /></div>
            <div className="name">{f.name}</div>
            <div className="val">{mode === "rare" ? rarityLabel(f.rarity) : fmtMoney(f.complete_value, state.settings.currency)}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function BudgetTracker({ figures, state, update }) {
  const currency = state.settings.currency;
  const totalSpent  = state.budget.filter((t) => t.kind === "purchase").reduce((a, t) => a + t.amount, 0);
  const totalEarned = state.budget.filter((t) => t.kind === "sale").reduce((a, t) => a + t.amount, 0);
  const net = totalEarned - totalSpent;
  const addQuick = (kind) => {
    const noteRaw = prompt("Description of " + kind, "");
    const note = noteRaw == null ? "" : noteRaw.trim();
    const amt = Number(prompt("Amount (" + currency + ")", "10")) || 0;
    if (!amt) return;
    update((s) => ({ ...s, budget: [{ id: Date.now(), figureId: null, kind: kind, amount: amt, note: note, date: new Date().toISOString().slice(0,10) }, ...s.budget] }));
  };
  const remove = (id) => update((s) => ({ ...s, budget: s.budget.filter((t) => t.id !== id) }));
  return (
    <div className="page">
      <h2>Budget Tracker</h2>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10, marginBottom: 16 }}>
        <div className="stat-line"><span>Spent</span><span className="val" style={{ color: "var(--red)" }}>{fmtMoney(totalSpent, currency)}</span></div>
        <div className="stat-line"><span>Earned</span><span className="val" style={{ color: "var(--green)" }}>{fmtMoney(totalEarned, currency)}</span></div>
        <div className="stat-line"><span>Net</span><span className="val">{fmtMoney(net, currency)}</span></div>
      </div>
      <div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
        <button className="btn danger" onClick={() => addQuick("purchase")}>+ Purchase</button>
        <button className="btn" style={{ background: "var(--green)" }} onClick={() => addQuick("sale")}>+ Sale</button>
      </div>
      {state.budget.length === 0 && <p style={{ color: "var(--ink-muted)" }}>No purchases or sales logged yet.</p>}
      {state.budget.map((t) => {
        const fig = figures.find((f) => f.id === t.figureId);
        return (
          <div key={t.id} className={"txn " + t.kind} style={{ alignItems: "center" }}>
            <span style={{ minWidth: 80 }}>{t.date}</span>
            <span style={{ minWidth: 80 }}>{t.kind === "purchase" ? "-" : "+"} {fmtMoney(t.amount, currency)}</span>
            <span style={{ flex: 1, textAlign: "left" }}>{fig ? fig.name : (t.note || "Manual entry")}</span>
            <button onClick={() => remove(t.id)} style={{ background: "transparent", border: 0, color: "#fff", cursor: "pointer", fontSize: 18 }}>x</button>
          </div>
        );
      })}
    </div>
  );
}

function BrandOverview({ figures, brands, state }) {
  const [mode, setMode] = useState("value");
  const palette = ["#007aff","#ff9500","#ff3b30","#34c759","#af52de","#ffcc00","#ff2d55","#5ac8fa","#a2845e","#8e8e93"];
  const names = Object.keys(brands);
  const slices = names.map((b, i) => {
    const brandFigs = figures.filter((f) => f.brand === b);
    const owned = brandFigs.filter((f) => state.collection[f.id] && state.collection[f.id].owned);
    const value = owned.reduce((a, f) => a + f.complete_value, 0);
    const val = mode === "count" ? owned.length : mode === "value" ? value : (brandFigs.length ? (owned.length / brandFigs.length) * 100 : 0);
    return { name: b, value: val, color: palette[i % palette.length], total: brandFigs.length, owned: owned.length, money: value };
  });
  const total = slices.reduce((a, s) => a + s.value, 0);
  const totalLabel = mode === "value" ? fmtMoney(total, state.settings.currency) : mode === "completion" ? (slices.length ? (total / slices.length).toFixed(1) + "%" : "0%") : total;
  return (
    <div className="page">
      <h2>Multi-Brand Overview</h2>
      <div className="sub-tabs">
        <button className={mode === "count" ? "active" : ""} onClick={() => setMode("count")}>Figure count</button>
        <button className={mode === "value" ? "active" : ""} onClick={() => setMode("value")}>Value</button>
        <button className={mode === "completion" ? "active" : ""} onClick={() => setMode("completion")}>Completion %</button>
      </div>
      <div className="pie-wrap">
        <PieChart slices={slices} total={total || 1} label={totalLabel} />
        <div>
          {slices.map((s) => (
            <div key={s.name} className="legend-row">
              <div><span className="legend-swatch" style={{ background: s.color }} /><strong>{s.name}</strong></div>
              <div style={{ color: "var(--ink-muted)", fontSize: 13 }}>{s.owned}/{s.total} - {fmtMoney(s.money, state.settings.currency)}</div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function CoverMode({ figures, state, onOpen }) {
  const [idx, setIdx] = useState(0);
  const owned = figures.filter((f) => state.collection[f.id] && state.collection[f.id].owned);
  const list = owned.length ? owned : figures;
  if (!list.length) return <div className="page"><h2>Cover Mode</h2><p>No figures to browse.</p></div>;
  const f = list[((idx % list.length) + list.length) % list.length];
  return (
    <div className="page" style={{ textAlign: "center" }}>
      <h2>Cover Mode</h2>
      <p style={{ color: "var(--ink-muted)", marginTop: -8 }}>Flip through your collection like album art.</p>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 20, margin: "20px 0" }}>
        <button className="btn secondary" onClick={() => setIdx(idx - 1)}>&larr;</button>
        <img src={coverUrl(f, state.collection[f.id])} alt={f.name} onClick={() => onOpen(f.id)} style={{ width: 280, cursor: "pointer", borderRadius: "var(--radius)", boxShadow: "var(--shadow-lg)" }} />
        <button className="btn secondary" onClick={() => setIdx(idx + 1)}>&rarr;</button>
      </div>
      <div style={{ fontWeight: 700 }}>{f.name}</div>
      <div style={{ color: "var(--ink-muted)", fontSize: 14 }}>{f.brand} - {f.series} - {f.year}</div>
    </div>
  );
}

// ---------- ShareLinkRow (toggle + copy URL) ---------------------
function ShareLinkRow({ kind, me }) {
  const initial = (me.shareTokens && me.shareTokens[kind]) || null;
  const [token, setToken] = useState(initial);
  const url = token ? (window.location.origin + "/share/" + token) : "";
  const enable = async () => {
    const r = await api.manageShare(me.username, kind, "enable");
    if (!r.ok) return alert((await r.json()).error || "failed");
    const j = await r.json(); setToken(j.token);
  };
  const disable = async () => {
    if (!confirm("Disable the public " + kind + " share link? Anyone using the old URL will see 'not found'.")) return;
    const r = await api.manageShare(me.username, kind, "disable");
    if (!r.ok) return alert((await r.json()).error || "failed");
    setToken(null);
  };
  const regenerate = async () => {
    if (!confirm("Regenerate the public " + kind + " share link? The old URL will stop working.")) return;
    const r = await api.manageShare(me.username, kind, "regenerate");
    if (!r.ok) return alert((await r.json()).error || "failed");
    const j = await r.json(); setToken(j.token);
  };
  const copy = () => { navigator.clipboard.writeText(url); alert("Link copied to clipboard."); };
  return (
    <div className="kv-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 6 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        <span style={{ textTransform: "capitalize" }}>{kind} share</span>
        <label className="switch">
          <input type="checkbox" checked={!!token} onChange={(e) => e.target.checked ? enable() : disable()} />
          <span className="track" />
        </label>
      </div>
      {token && (
        <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
          <input readOnly value={url} style={{ flex: 1, fontSize: 12 }} onFocus={(e) => e.target.select()} />
          <button className="btn secondary" onClick={copy}>Copy</button>
          <button className="btn secondary" onClick={regenerate}>Regenerate</button>
        </div>
      )}
    </div>
  );
}

// ---------- SerpAPIConfigSection (admin Settings) -------------
function SerpAPIConfigSection() {
  const [cfg, setCfg] = useState(null);
  const [draftKey, setDraftKey] = useState("");
  const [draftLimit, setDraftLimit] = useState(250);
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState("");

  const load = () => api.serpapiConfig().then((j) => { setCfg(j); setDraftLimit(j.monthlyLimit || 250); });
  useEffect(() => { load(); }, []);

  const save = async () => {
    setBusy(true); setMsg("");
    try {
      const body = { monthlyLimit: Number(draftLimit) || 250 };
      if (draftKey.trim()) body.apiKey = draftKey.trim();
      const r = await api.serpapiSetConfig(body);
      if (!r.ok) { setMsg("Save failed: " + ((await r.json()).error || "")); return; }
      setDraftKey(""); load(); setMsg("Saved.");
    } finally { setBusy(false); }
  };
  const clearKey = async () => {
    if (!confirm("Remove the saved SerpAPI key? Image search will stop working.")) return;
    await api.serpapiSetConfig({ clear: true }); load();
  };
  const resetCounter = async () => {
    if (!confirm("Reset the monthly call counter to 0? Use only if you know your plan was reset.")) return;
    await api.serpapiSetConfig({ resetCounter: true }); load();
  };
  const resetCache = async () => {
    if (!confirm("Drop all cached image search results? Subsequent searches will re-call SerpAPI.")) return;
    await api.serpapiSetConfig({ resetCache: true }); load();
  };

  if (!cfg) return null;
  const used = cfg.callsThisMonth || 0;
  const lim = cfg.monthlyLimit || 250;
  const pct = Math.min(100, (used / lim) * 100);

  return (
    <div style={{ marginTop: 18, padding: 12, border: "0.5px solid var(--bar-border)", borderRadius: "var(--radius)" }}>
      <h3 style={{ marginTop: 0, fontSize: 15 }}>SerpAPI image search <span className="role-tag admin">admin</span></h3>
      <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 8 }}>
        Used to fetch official-looking images for figures from Google Images. The key is stored on the server and never sent to clients.
      </div>
      <div className="kv-row"><span>Status</span><span>{cfg.configured ? "Configured (key " + cfg.keyPreview + ")" : "Not configured"}</span></div>
      <div className="kv-row">
        <span>This month ({cfg.monthYear})</span>
        <span>{used} / {lim} calls used</span>
      </div>
      <div style={{ height: 6, background: "var(--panel-alt)", borderRadius: 3, marginTop: -2, overflow: "hidden" }}>
        <div style={{ width: pct + "%", height: "100%", background: pct > 90 ? "var(--red)" : pct > 70 ? "var(--gold)" : "var(--green)" }} />
      </div>
      <div className="kv-row"><span>Cached figure searches</span><span>{cfg.cachedFigures || 0}</span></div>
      <div className="kv-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 6 }}>
        <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>SerpAPI key</span>
        <input type="password" placeholder="leave blank to keep existing" value={draftKey} onChange={(e) => setDraftKey(e.target.value)} autoComplete="off" />
      </div>
      <div className="kv-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 6 }}>
        <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Monthly call limit</span>
        <input type="number" min="1" value={draftLimit} onChange={(e) => setDraftLimit(e.target.value)} />
      </div>
      <div className="btn-row" style={{ marginTop: 8 }}>
        <button className="btn" disabled={busy} onClick={save}>Save</button>
        {cfg.configured && <button className="btn secondary" disabled={busy} onClick={resetCounter}>Reset counter</button>}
        {(cfg.cachedFigures || 0) > 0 && <button className="btn secondary" disabled={busy} onClick={resetCache}>Clear cache</button>}
        {cfg.configured && <button className="btn danger" disabled={busy} onClick={clearKey}>Remove key</button>}
      </div>
      {msg && <div style={{ marginTop: 8, fontSize: 13, color: "var(--ink-muted)" }}>{msg}</div>}
    </div>
  );
}

// ---------- SerpImageSearchModal (single figure) -----------------
function SerpImageSearchModal({ figure, onClose, onSaved }) {
  const [data, setData] = useState(null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  const load = async (force) => {
    setBusy(true); setErr("");
    try {
      const j = await api.serpapiSearch(figure.id, force);
      if (!j.ok) { setErr(j.error || "search failed"); return; }
      setData(j);
    } catch (e) { setErr(String(e.message || e)); }
    finally { setBusy(false); }
  };
  useEffect(() => { load(false); /* eslint-disable-line */ }, [figure.id]);

  const saveOne = async (url, slot) => {
    const r = await api.serpapiSave(figure.id, url, slot);
    if (!r.ok) return alert("Save failed: " + (r.error || ""));
    if (onSaved) onSaved();
  };
  const saveBest = async () => {
    if (!data || !data.results || !data.results[0]) return;
    await saveOne(data.results[0].original, "customCoverUrl");
    alert("Saved best image as catalog cover.");
  };
  const saveAll = async () => {
    if (!data || !data.results) return;
    const slots = ["customCoverUrl","front","back","loose","accessories"];
    for (let i = 0; i < Math.min(slots.length, data.results.length); i++) {
      await api.serpapiSave(figure.id, data.results[i].original, slots[i]);
    }
    alert("Saved up to 5 images: cover + front + back + loose + accessories.");
    if (onSaved) onSaved();
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ maxWidth: 920 }} onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <div>
            <div style={{ fontWeight: 700 }}>SerpAPI image search</div>
            <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>{figure.name}{figure.brand ? " - " + figure.brand : ""}</div>
          </div>
          <button className="close" onClick={onClose}>x</button>
        </div>
        <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
          {err && <div style={{ background: "var(--red)", color: "#fff", padding: 8, borderRadius: 8, marginBottom: 8 }}>{err}</div>}
          {busy && <div style={{ padding: 24, textAlign: "center", color: "var(--ink-muted)" }}>Searching...</div>}
          {data && (
            <>
              <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginBottom: 8, fontSize: 12, color: "var(--ink-muted)" }}>
                <span><strong>Query:</strong> {data.query}</span>
                {data.cached && <span className="role-tag" style={{ background: "var(--gold)", color: "#000" }}>cached</span>}
                {typeof data.calls === "number" && <span>{data.calls} / {data.limit} SerpAPI calls used this month</span>}
                <button className="btn secondary" onClick={() => load(true)} disabled={busy}>Re-run search</button>
              </div>
              <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
                <button className="btn" onClick={saveBest} disabled={!data.results || !data.results.length}>Save best as cover</button>
                <button className="btn secondary" onClick={saveAll} disabled={!data.results || !data.results.length}>Save first 4 (cover + front + back + loose)</button>
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 12 }}>
                {(data.results || []).map((r, i) => (
                  <div key={i} style={{ border: "0.5px solid var(--bar-border)", borderRadius: "var(--radius)", padding: 8, background: "var(--panel-alt)" }}>
                    <a href={r.original} target="_blank" rel="noopener">
                      <img src={r.thumbnail || r.original} alt={r.title} style={{ width: "100%", aspectRatio: "1 / 1", objectFit: "contain", background: "var(--panel)", borderRadius: 8 }} />
                    </a>
                    <div style={{ fontSize: 11, color: "var(--ink-muted)", marginTop: 6, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={r.title}>{r.title || "(no title)"}</div>
                    <div style={{ fontSize: 10, color: "var(--ink-muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={r.source}>{r.source}</div>
                    <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 }}>
                      <button className="btn secondary" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => saveOne(r.original, "customCoverUrl")}>Cover</button>
                      <button className="btn secondary" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => saveOne(r.original, "front")}>Front</button>
                      <button className="btn secondary" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => saveOne(r.original, "back")}>Back</button>
                      <button className="btn secondary" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => saveOne(r.original, "loose")}>Loose</button>
                      <button className="btn secondary" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => saveOne(r.original, "accessories")}>Accs.</button>
                    </div>
                  </div>
                ))}
                {data.results && data.results.length === 0 && <div style={{ padding: 16, color: "var(--ink-muted)" }}>No images found for that query. Try refining the figure name or description and re-run.</div>}
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

function figureHasImages(fig) {
  if (!fig) return false;
  if (fig.customCoverUrl) return true;
  const p = fig.photos || {};
  return !!(p.front || p.back || p.loose || p.accessories);
}

// ---------- BulkSerpAPISearch (Manage Catalog) -----------------
function BulkSerpAPIModal({ ids, figures, onClose, refresh }) {
  const queue = ids.filter((id) => !figureHasImages(figures.find((f) => f.id === id)));
  const skipped = ids.length - queue.length;
  const [done, setDone] = useState(0);
  const [running, setRunning] = useState(false);
  const [stopped, setStopped] = useState(false);
  const [log, setLog] = useState([]);
  const stopRef = useRef(false);

  const append = (line) => setLog((prev) => [line, ...prev].slice(0, 200));

  const run = async () => {
    setRunning(true); setStopped(false); stopRef.current = false;
    for (let i = done; i < queue.length; i++) {
      if (stopRef.current) { setStopped(true); break; }
      const id = queue[i];
      const fig = figures.find((f) => f.id === id);
      try {
        const j = await api.serpapiSearch(id, false);
        if (!j.ok) { append("FAIL " + (fig && fig.name) + ": " + (j.error || "unknown")); setStopped(true); break; }
        if (j.results && j.results[0]) {
          await api.serpapiSave(id, j.results[0].original, "customCoverUrl");
          append((j.cached ? "cached " : "") + (fig && fig.name) + " -> saved cover");
        } else {
          append("no results: " + (fig && fig.name));
        }
      } catch (e) { append("error: " + (fig && fig.name) + " - " + (e.message || e)); setStopped(true); break; }
      setDone(i + 1);
      // polite delay between calls
      await new Promise(r => setTimeout(r, 1200));
    }
    setRunning(false);
    refresh && refresh();
  };

  const stop = () => { stopRef.current = true; setRunning(false); };

  return (
    <div className="modal-backdrop" onClick={running ? null : onClose}>
      <div className="modal" style={{ maxWidth: 720 }} onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <div>
            <div style={{ fontWeight: 700 }}>SerpAPI bulk image search</div>
            <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>Selected: {ids.length} - already have images: {skipped} - remaining: {queue.length}</div>
          </div>
          {!running && <button className="close" onClick={onClose}>x</button>}
        </div>
        <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
          <div>
            <div style={{ marginBottom: 12 }}>
              <div style={{ height: 8, background: "var(--panel-alt)", borderRadius: 4, overflow: "hidden" }}>
                <div style={{ width: queue.length ? (done / queue.length * 100) + "%" : "0%", height: "100%", background: "var(--accent)" }} />
              </div>
              <div style={{ fontSize: 12, color: "var(--ink-muted)", marginTop: 4 }}>{done} / {queue.length} processed{stopped ? " (stopped)" : ""}</div>
            </div>
            <div className="btn-row">
              {!running && done < queue.length && <button className="btn" onClick={run}>{done > 0 ? "Continue" : "Start"}</button>}
              {running && <button className="btn danger" onClick={stop}>Stop</button>}
              <button className="btn secondary" onClick={onClose} disabled={running}>Close</button>
            </div>
            <div style={{ marginTop: 12, maxHeight: 240, overflowY: "auto", border: "0.5px solid var(--bar-border)", borderRadius: "var(--radius)", padding: 8, fontFamily: "monospace", fontSize: 11 }}>
              {log.length === 0 && <div style={{ color: "var(--ink-muted)" }}>(nothing logged yet)</div>}
              {log.map((line, i) => <div key={i}>{line}</div>)}
            </div>
            <div style={{ marginTop: 8, fontSize: 11, color: "var(--ink-muted)" }}>
              Figures that already have at least one image saved are skipped. Press Stop and Continue later to resume from where you left off.
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- AIConfigSection (admin Settings) -------------------
function AIConfigSection() {
  const [cfg, setCfg] = useState(null);
  const [draftKey, setDraftKey] = useState("");
  const [draftModel, setDraftModel] = useState("claude-sonnet-4-6");
  const [busy, setBusy] = useState(false);
  const [testResult, setTestResult] = useState("");

  const load = () => api.aiConfig().then((j) => { setCfg(j); setDraftModel(j.model || "claude-sonnet-4-6"); });
  useEffect(() => { load(); }, []);

  const save = async () => {
    setBusy(true); setTestResult("");
    try {
      const body = { model: draftModel };
      if (draftKey.trim()) body.apiKey = draftKey.trim();
      const r = await api.aiSetConfig(body);
      if (!r.ok) { alert((await r.json()).error || "failed"); return; }
      setDraftKey(""); load();
    } finally { setBusy(false); }
  };
  const clearKey = async () => {
    if (!confirm("Remove the saved API key? AI features will stop working.")) return;
    await api.aiSetConfig({ clear: true }); setDraftKey(""); load();
  };
  const test = async () => {
    setBusy(true); setTestResult("Testing...");
    try {
      const r = await api.aiTest();
      setTestResult(r.ok ? "OK - " + (r.sample || "") : "Error: " + (r.error || "unknown"));
    } finally { setBusy(false); }
  };

  if (!cfg) return null;
  return (
    <div style={{ marginTop: 18, padding: 12, border: "0.5px solid var(--bar-border)", borderRadius: "var(--radius)" }}>
      <h3 style={{ marginTop: 0, fontSize: 15 }}>AI assistant <span className="role-tag admin">admin</span></h3>
      <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 8 }}>
        Connect an Anthropic API key to enable AI suggestions on the New Figure form. The key is stored on the server and is never sent to clients.
      </div>
      <div className="kv-row">
        <span>Status</span>
        <span>{cfg.configured ? "Configured (key " + cfg.keyPreview + ")" : "Not configured"}</span>
      </div>
      <div className="kv-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 6 }}>
        <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Anthropic API key</span>
        <input type="password" placeholder="sk-ant-..." value={draftKey} onChange={(e) => setDraftKey(e.target.value)} autoComplete="off" />
      </div>
      <div className="kv-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 6 }}>
        <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Model</span>
        <input value={draftModel} onChange={(e) => setDraftModel(e.target.value)} placeholder="claude-sonnet-4-6" />
      </div>
      <div className="btn-row" style={{ marginTop: 8 }}>
        <button className="btn" disabled={busy} onClick={save}>Save</button>
        <button className="btn secondary" disabled={busy || !cfg.configured} onClick={test}>Test connection</button>
        {cfg.configured && <button className="btn danger" disabled={busy} onClick={clearKey}>Remove key</button>}
      </div>
      {testResult && <div style={{ marginTop: 8, fontSize: 13, color: testResult.startsWith("OK") ? "var(--green)" : "var(--red)" }}>{testResult}</div>}
    </div>
  );
}

// ---------- AISuggestRow (in admin catalog forms) --------------
function AISuggestRow({ name, getCurrent, applyPatch }) {
  const [busy, setBusy] = useState(false);
  const [aiAvailable, setAiAvailable] = useState(false);
  const photoInputRef = useRef(null);

  useEffect(() => { api.aiConfig().then((j) => setAiAvailable(!!(j && j.configured))).catch(() => {}); }, []);

  const fromName = async () => {
    if (!name || !name.trim()) return alert("Type a figure name first.");
    setBusy(true);
    try {
      const r = await api.aiSuggestFromName(name.trim());
      if (!r.ok) return alert("AI error: " + (r.error || "failed"));
      applyPatch(r.suggestion || {});
    } finally { setBusy(false); }
  };
  const fromPhoto = (file) => {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = async () => {
      setBusy(true);
      try {
        const r = await api.aiSuggestFromPhoto(reader.result, name || "");
        if (!r.ok) return alert("AI error: " + (r.error || "failed"));
        applyPatch(r.suggestion || {});
      } finally { setBusy(false); }
    };
    reader.readAsDataURL(file);
  };
  const description = async () => {
    const cur = getCurrent ? getCurrent() : {};
    if (!cur.name) return alert("Type a figure name first.");
    setBusy(true);
    try {
      const r = await api.aiSuggestDescription({ name: cur.name, brand: cur.brand, series: cur.series, year: cur.year });
      if (!r.ok) return alert("AI error: " + (r.error || "failed"));
      applyPatch({ description: r.description || "" });
    } finally { setBusy(false); }
  };

  if (!aiAvailable) {
    return <div style={{ fontSize: 12, color: "var(--ink-muted)", padding: "6px 0" }}>AI suggestions disabled - admin can configure an Anthropic API key in Settings.</div>;
  }
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", padding: 8, background: "var(--panel-alt)", borderRadius: "var(--radius)", marginBottom: 8 }}>
      <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>AI:</span>
      <button className="btn secondary" disabled={busy} onClick={fromName}>{busy ? "..." : "Suggest from name"}</button>
      <input ref={photoInputRef} type="file" accept="image/*" style={{ display: "none" }} onChange={(e) => { fromPhoto(e.target.files[0]); e.target.value = ""; }} />
      <button className="btn secondary" disabled={busy} onClick={() => photoInputRef.current && photoInputRef.current.click()}>{busy ? "..." : "Suggest from photo"}</button>
      <button className="btn secondary" disabled={busy} onClick={description}>{busy ? "..." : "Suggest description"}</button>
    </div>
  );
}

// ---------- Settings ----------------------------------------------
function Settings({ state, update, syncStatus, me, onLogout, figures }) {
  const s = state.settings;
  const patch = (p) => update((st) => ({ ...st, settings: { ...st.settings, ...p } }));
  const exportCsv = (kind) => {
    const list =
      kind === "collection" ? figures.filter((f) => state.collection[f.id] && state.collection[f.id].owned) :
      kind === "wanted"     ? figures.filter((f) => state.collection[f.id] && state.collection[f.id].wanted) :
      kind === "remaining"  ? figures.filter((f) => !(state.collection[f.id] && state.collection[f.id].owned)) :
      figures;
    const csv = buildCsv(list, state);
    downloadFile("toy_collector_" + kind + "_" + new Date().toISOString().slice(0,10) + ".csv", csv);
  };
  const emailList = (kind) => openExternal("mailto:?subject=" + encodeURIComponent("My toy collection: " + kind) + "&body=" + encodeURIComponent(kind));
  return (
    <div className="page">
      <h2>Settings</h2>
      <div className="kv-row"><span>Signed in as</span><span><strong>{me.username}</strong> - {me.role}</span></div>
      <div className="kv-row"><span>Dark mode</span><label className="switch"><input type="checkbox" checked={!!s.darkMode} onChange={(e) => patch({ darkMode: e.target.checked })} /><span className="track" /></label></div>
      <div className="kv-row"><span>Currency</span><select value={s.currency} onChange={(e) => patch({ currency: e.target.value })}>{Object.keys(CURRENCY_SYMBOL).map((c) => <option key={c}>{c}</option>)}</select></div>
      <div className="kv-row"><span>eBay region</span><select value={s.ebayRegion} onChange={(e) => patch({ ebayRegion: e.target.value })}>{["US","UK","DE","AU","CA","FR"].map((c) => <option key={c}>{c}</option>)}</select></div>
      <div className="kv-row"><span>Sync status</span><span>{syncStatus === "saved" ? "Synced to backend" : syncStatus}</span></div>
      <h3 style={{ marginTop: 18, fontSize: 15 }}>Export to CSV</h3>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
        <button className="btn" onClick={() => exportCsv("collection")}>Collection</button>
        <button className="btn" onClick={() => exportCsv("wanted")}>Wanted</button>
        <button className="btn" onClick={() => exportCsv("remaining")}>Remaining</button>
        {me.permissions && me.permissions.canManageCatalog && (
          <button className="btn secondary" onClick={() => exportCsv("all")}>Full catalog (admin)</button>
        )}
      </div>
      <h3 style={{ marginTop: 18, fontSize: 15 }}>Public share links</h3>
      <ShareLinkRow kind="collection" me={me} />
      <ShareLinkRow kind="wanted" me={me} />

      <h3 style={{ marginTop: 18, fontSize: 15 }}>Email a list</h3>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
        {["collection","wanted","remaining","forsale","duplicates"].map((k) => <button key={k} className="btn secondary" onClick={() => emailList(k)}>{k}</button>)}
      </div>
      {me.permissions && me.permissions.canManageCatalog && <AIConfigSection />}
      {me.permissions && me.permissions.canManageCatalog && <SerpAPIConfigSection />}
      <div style={{ marginTop: 18 }}><button className="btn danger" onClick={onLogout}>Sign out</button></div>
    </div>
  );
}

// ---------- Manage Brands -----------------------------------------
function ManageBrands({ brands, figures, refresh }) {
  const [adding, setAdding] = useState(false);
  const [draft, setDraft] = useState({ name: "", hue: 200, accent: "#FFD447", icon: "star" });
  const create = async () => {
    if (!draft.name) return alert("name required");
    const r = await api.createBrand(draft);
    if (!r.ok) return alert((await r.json()).error || "failed");
    setAdding(false); setDraft({ name: "", hue: 200, accent: "#FFD447", icon: "star" });
    refresh();
  };
  const update = async (name, patch) => { await api.updateBrand(name, patch); refresh(); };
  const remove = async (name) => {
    // First peek at the catalog to count figures using this brand.
    let figureCount = 0;
    try {
      const j = await api.catalog();
      figureCount = (j.figures || []).filter((f) => f.brand === name).length;
    } catch {}
    let cascade = false;
    if (figureCount > 0) {
      const ok = confirm(
        "Delete brand \"" + name + "\" AND all " + figureCount + " figure(s) using it?\n\n" +
        "This permanently removes those figures from the catalog and from every customer\'s collection / wanted / budget. This cannot be undone."
      );
      if (!ok) return;
      cascade = true;
    } else {
      if (!confirm("Delete brand \"" + name + "\"? (No figures use it.)")) return;
    }
    const r = await api.deleteBrand(name, cascade);
    const j = await r.json().catch(() => ({}));
    if (!r.ok) alert(j.error || "failed");
    else if (cascade) alert("Deleted brand \"" + name + "\" and " + (j.removedFigures || 0) + " figure(s).");
    refresh();
  };
  return (
    <div className="page">
      <h2>Manage Brands <span className="role-tag admin">admin</span></h2>
      <div style={{ display: "grid", gap: 8 }}>
        {Object.entries(brands).map(([name, b]) => (
          <BrandRow key={name} name={name} brand={b} figures={figures} onSave={(p) => update(name, p)} onDelete={() => remove(name)} refresh={refresh} />
        ))}
      </div>
      {adding ? (
        <div className="brand-row" style={{ marginTop: 12 }}>
          <input placeholder="Brand name" value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} />
          <input type="number" placeholder="hue" value={draft.hue} onChange={(e) => setDraft({ ...draft, hue: Number(e.target.value) })} style={{ width: 70 }} />
          <input type="color" value={draft.accent} onChange={(e) => setDraft({ ...draft, accent: e.target.value })} />
          <select value={draft.icon} onChange={(e) => setDraft({ ...draft, icon: e.target.value })}>{ICON_OPTIONS.map((i) => <option key={i}>{i}</option>)}</select>
          <button className="btn" onClick={create}>Save</button>
          <button className="btn secondary" onClick={() => setAdding(false)}>Cancel</button>
        </div>
      ) : (
        <button className="btn" style={{ marginTop: 12 }} onClick={() => setAdding(true)}>+ Add Brand</button>
      )}
    </div>
  );
}

function BrandRow({ name, brand, figures, onSave, onDelete, refresh }) {
  const [edit, setEdit] = useState(false);
  const [showSeries, setShowSeries] = useState(false);
  const [showGroups, setShowGroups] = useState(false);
  const [showManufacturers, setShowManufacturers] = useState(false);
  const [draft, setDraft] = useState({ name: name, hue: brand.hue, accent: brand.accent, icon: brand.icon });
  const [newSeries, setNewSeries] = useState("");
  const [newGroup, setNewGroup] = useState("");
  const [newManufacturer, setNewManufacturer] = useState("");

  const figuresInBrand = (figures || []).filter((f) => f.brand === name);
  const groupCount = (g) => figuresInBrand.filter((f) => f.group === g).length;
  useEffect(() => setDraft({ name: name, hue: brand.hue, accent: brand.accent, icon: brand.icon }), [name, brand.hue, brand.accent, brand.icon]);
  const addSeries = async () => {
    const s = newSeries.trim(); if (!s) return;
    await api.addSeries(name, s); setNewSeries(""); refresh();
  };
  const delSeries = async (s) => {
    if (!confirm("Remove series \"" + s + "\" from " + name + "? Figures using it will become unassigned.")) return;
    await api.delSeries(name, s); refresh();
  };
  const addGroup = async () => {
    const g = newGroup.trim(); if (!g) return;
    await api.addGroup(name, g); setNewGroup(""); refresh();
  };
  const delGroup = async (g) => {
    if (!confirm("Remove group \"" + g + "\" from " + name + "? Figures using it will be unassigned.")) return;
    await api.delGroup(name, g); refresh();
  };
  const addManufacturer = async () => {
    const m = newManufacturer.trim(); if (!m) return;
    await api.addManufacturer(name, m); setNewManufacturer(""); refresh();
  };
  const delManufacturer = async (m) => {
    if (!confirm("Remove manufacturer \"" + m + "\" from " + name + "? Figures referencing it will be unassigned.")) return;
    await api.delManufacturer(name, m); refresh();
  };
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
      {!edit ? (
        <div className="brand-row">
          <span className="legend-swatch" style={{ background: brand.accent }} />
          <strong style={{ flex: 1 }}>{name}</strong>
          <span style={{ color: "var(--ink-muted)", fontSize: 12 }}>hue {brand.hue} - {brand.icon} - {(brand.series || []).length} series - {(brand.groups || []).length} groups - {(brand.manufacturers || []).length} mfrs</span>
          <button className="btn secondary" onClick={() => setShowSeries(!showSeries)}>{showSeries ? "Hide series" : "Series"}</button>
          <button className="btn secondary" onClick={() => setShowGroups(!showGroups)}>{showGroups ? "Hide groups" : "Groups"}</button>
          <button className="btn secondary" onClick={() => setShowManufacturers(!showManufacturers)}>{showManufacturers ? "Hide mfrs" : "Mfrs"}</button>
          <button className="btn secondary" onClick={() => setEdit(true)}>Edit</button>
          <button className="btn danger" onClick={onDelete}>Delete</button>
        </div>
      ) : (
        <div className="brand-row">
          <input value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} />
          <input type="number" value={draft.hue} onChange={(e) => setDraft({ ...draft, hue: Number(e.target.value) })} style={{ width: 70 }} />
          <input type="color" value={draft.accent} onChange={(e) => setDraft({ ...draft, accent: e.target.value })} />
          <select value={draft.icon} onChange={(e) => setDraft({ ...draft, icon: e.target.value })}>{ICON_OPTIONS.map((i) => <option key={i}>{i}</option>)}</select>
          <button className="btn" onClick={() => { onSave(draft); setEdit(false); }}>Save</button>
          <button className="btn secondary" onClick={() => setEdit(false)}>Cancel</button>
        </div>
      )}
      {showSeries && (
        <div style={{ background: "var(--panel-alt)", padding: 10, borderRadius: "var(--radius)" }}>
          <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 6 }}>Series for {name} (hidden ones are dimmed and never shown to customers)</div>
          <div className="series-list">
            {(brand.series || []).length === 0 && <span style={{ color: "var(--ink-muted)", fontSize: 13 }}>No series yet.</span>}
            {(brand.series || []).map((s) => {
              const isHidden = (brand.hiddenSeries || []).includes(s);
              return (
                <span key={s} className="series-tag" style={isHidden ? { opacity: 0.4 } : null}>
                  {s}
                  <button onClick={async () => { await api.toggleHiddenSeries(name, s); refresh(); }} title={isHidden ? "Show to customers" : "Hide from customers"} style={{ fontSize: 12 }}>{isHidden ? "show" : "hide"}</button>
                  <button onClick={() => delSeries(s)} title="Remove">x</button>
                </span>
              );
            })}
          </div>
          <div style={{ display: "flex", gap: 6 }}>
            <input placeholder="New series name" value={newSeries} onChange={(e) => setNewSeries(e.target.value)} style={{ flex: 1 }} onKeyDown={(e) => e.key === "Enter" && addSeries()} />
            <button className="btn" onClick={addSeries} disabled={!newSeries.trim()}>+ Add</button>
          </div>
        </div>
      )}
      {showGroups && (
        <div style={{ background: "var(--panel-alt)", padding: 10, borderRadius: "var(--radius)" }}>
          <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 6 }}>Groups (waves) for {name} (hidden ones are dimmed and never shown to customers)</div>
          <div className="series-list">
            {(brand.groups || []).length === 0 && <span style={{ color: "var(--ink-muted)", fontSize: 13 }}>No groups yet.</span>}
            {(brand.groups || []).map((g) => {
              const isHidden = (brand.hiddenGroups || []).includes(g);
              return (
                <span key={g} className="series-tag" style={isHidden ? { opacity: 0.4 } : null}>
                  {g} <span style={{ color: "var(--ink-muted)", fontSize: 11 }}>({groupCount(g)})</span>
                  <button onClick={async () => { await api.toggleHiddenGroup(name, g); refresh(); }} title={isHidden ? "Show to customers" : "Hide from customers"} style={{ fontSize: 12 }}>{isHidden ? "show" : "hide"}</button>
                  <button onClick={() => delGroup(g)} title="Remove">x</button>
                </span>
              );
            })}
          </div>
          <div style={{ display: "flex", gap: 6 }}>
            <input placeholder="New group name (e.g. Wave 1)" value={newGroup} onChange={(e) => setNewGroup(e.target.value)} style={{ flex: 1 }} onKeyDown={(e) => e.key === "Enter" && addGroup()} />
            <button className="btn" onClick={addGroup} disabled={!newGroup.trim()}>+ Add</button>
          </div>
        </div>
      )}
      {showManufacturers && (
        <div style={{ background: "var(--panel-alt)", padding: 10, borderRadius: "var(--radius)" }}>
          <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 6 }}>Manufacturers for {name}</div>
          <div className="series-list">
            {(brand.manufacturers || []).length === 0 && <span style={{ color: "var(--ink-muted)", fontSize: 13 }}>No manufacturers yet.</span>}
            {(brand.manufacturers || []).map((m) => (
              <span key={m} className="series-tag">{m}<button onClick={() => delManufacturer(m)} title="Remove">x</button></span>
            ))}
          </div>
          <div style={{ display: "flex", gap: 6 }}>
            <input placeholder="New manufacturer (e.g. Hasbro, Mattel)" value={newManufacturer} onChange={(e) => setNewManufacturer(e.target.value)} style={{ flex: 1 }} onKeyDown={(e) => e.key === "Enter" && addManufacturer()} />
            <button className="btn" onClick={addManufacturer} disabled={!newManufacturer.trim()}>+ Add</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ---------- Manage Catalog ----------------------------------------
function ManageCatalog({ figures, brands, regions, refresh }) {
  const [filter, setFilter] = useState("");
  const [adding, setAdding] = useState(false);
  const [brandFilter, setBrandFilter] = useState("ALL");
  const [seriesFilter, setSeriesFilter] = useState("ALL");
  const [groupFilter, setGroupFilter] = useState("ALL");
  const [manufacturerFilter, setManufacturerFilter] = useState("ALL");
  const [alpha, setAlpha] = useState(null);
  const [selected, setSelected] = useState(new Set());
  const [bulkSerp, setBulkSerp] = useState(false);
  const [mcShowHidden, setMcShowHidden] = useState(true);
  const importRef = useRef(null);

  const toggleSelect = (id) => {
    const n = new Set(selected);
    if (n.has(id)) n.delete(id); else n.add(id);
    setSelected(n);
  };
  const bulkDelete = async () => {
    if (selected.size === 0) return;
    if (!confirm("Delete " + selected.size + " selected figure(s)? This cannot be undone.")) return;
    const r = await api.bulkDelete([...selected]);
    if (!r.ok) alert((await r.json()).error || "failed");
    else { setSelected(new Set()); refresh(); }
  };
  const importCsv = async (file) => {
    if (!file) return;
    const text = await file.text();
    const r = await api.importCsv(text);
    const j = await r.json();
    if (!r.ok) return alert(j.error || "import failed");
    let extras = [];
    if (j.addedBrands)  extras.push("+" + j.addedBrands  + " new brand(s)");
    if (j.addedSeries)  extras.push("+" + j.addedSeries  + " new series");
    if (j.addedGroups)  extras.push("+" + j.addedGroups  + " new group(s)");
    const extraTxt = extras.length ? " [" + extras.join(", ") + "]" : "";
    alert("Imported " + j.added + " figure(s)" + (j.skipped ? " (skipped " + j.skipped + " duplicate(s))" : "") + extraTxt + (j.errors && j.errors.length ? "\nDetails:\n" + j.errors.slice(0,10).join("\n") : ""));
    refresh();
  };

  // Series options narrow to the chosen brand (or everything if no brand picked).
  const seriesOptions = brandFilter === "ALL"
    ? [...new Set(Object.values(brands).flatMap((b) => b.series || []))].sort()
    : [...((brands[brandFilter] && brands[brandFilter].series) || [])].sort();
  const groupOptions = brandFilter === "ALL"
    ? [...new Set(Object.values(brands).flatMap((b) => b.groups || []))].sort()
    : [...((brands[brandFilter] && brands[brandFilter].groups) || [])].sort();
  const manufacturerOptions = brandFilter === "ALL"
    ? [...new Set(Object.values(brands).flatMap((b) => b.manufacturers || []))].sort()
    : [...((brands[brandFilter] && brands[brandFilter].manufacturers) || [])].sort();

  let list = figures.filter((f) => {
    if (!mcShowHidden && f.hidden) return false;
    if (brandFilter !== "ALL" && f.brand !== brandFilter) return false;
    if (seriesFilter !== "ALL" && f.series !== seriesFilter) return false;
    if (groupFilter !== "ALL" && f.group !== groupFilter) return false;
    if (manufacturerFilter !== "ALL" && (f.manufacturer || "") !== manufacturerFilter) return false;
    if (alpha) {
      if (firstChar(f.name) !== alpha) return false;
    }
    return f.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0 ||
           (f.upc || "").indexOf(filter) >= 0;
  });
  // Default: alphabetical by name, with brand as secondary key.
  list = [...list].sort((a, b) => a.brand.localeCompare(b.brand) || a.name.localeCompare(b.name));

  const remove = async (id, name) => {
    if (!confirm("Delete \"" + name + "\"?")) return;
    await api.deleteFigure(id); refresh();
  };
  return (
    <div className="page">
      <h2>Manage Catalog <span className="role-tag admin">admin</span></h2>

      {/* Add-figure / Import / Bulk-delete controls at the TOP */}
      {adding
        ? <CatalogNew brands={brands} regions={regions} onCancel={() => setAdding(false)} onSaved={() => { setAdding(false); refresh(); }} />
        : (
          <div style={{ marginBottom: 12, display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
            <button className="btn" onClick={() => setAdding(true)}>+ Add Figure</button>
            <input ref={importRef} type="file" accept=".csv,text/csv" style={{ display: "none" }} onChange={(e) => { importCsv(e.target.files[0]); e.target.value = ""; }} />
            <button className="btn secondary" onClick={() => importRef.current && importRef.current.click()}>Import CSV...</button>
            <a className="btn secondary" href="/api/catalog/figures/import-template.csv" download style={{ textDecoration: "none" }}>Download template CSV</a>
            {selected.size > 0 && <button className="btn danger" onClick={bulkDelete}>Delete {selected.size} selected</button>}
            {selected.size > 0 && <button className="btn secondary" onClick={() => setBulkSerp(true)}>Search images for {selected.size} selected (SerpAPI)</button>}
            {selected.size > 0 && <button className="btn secondary" onClick={() => setSelected(new Set())}>Clear selection</button>}
          </div>
        )
      }

      <div style={{ display: "flex", gap: 8, marginBottom: 10, flexWrap: "wrap", alignItems: "center" }}>
        <div className="search-bar" style={{ flex: 1, minWidth: 200, maxWidth: "none" }}>
          <input placeholder="Filter by name or UPC..." value={filter} onChange={(e) => setFilter(e.target.value)} />
          {filter && <button className="clear-btn" onClick={() => setFilter("")} aria-label="Clear filter">x</button>}
        </div>
        <select value={brandFilter} onChange={(e) => { setBrandFilter(e.target.value); setSeriesFilter("ALL"); setGroupFilter("ALL"); setManufacturerFilter("ALL"); }} style={{ minWidth: 180 }}>
          <option value="ALL">All brands</option>
          {Object.keys(brands).map((b) => <option key={b} value={b}>{b}</option>)}
        </select>
        <select value={seriesFilter} onChange={(e) => setSeriesFilter(e.target.value)} style={{ minWidth: 200 }}>
          <option value="ALL">All series</option>
          {seriesOptions.map((s) => <option key={s} value={s}>{s}</option>)}
        </select>
        <select value={groupFilter} onChange={(e) => setGroupFilter(e.target.value)} style={{ minWidth: 160 }}>
          <option value="ALL">All groups</option>
          {groupOptions.map((g) => <option key={g} value={g}>{g}</option>)}
        </select>
        <select value={manufacturerFilter} onChange={(e) => setManufacturerFilter(e.target.value)} style={{ minWidth: 180 }}>
          <option value="ALL">All manufacturers</option>
          {manufacturerOptions.map((m) => <option key={m} value={m}>{m}</option>)}
        </select>
        <label className="switch" title="Show hidden figures in this list" style={{ marginLeft: 4 }}>
          <input type="checkbox" checked={mcShowHidden} onChange={(e) => setMcShowHidden(e.target.checked)} />
          <span className="track" /> <span style={{ fontSize: 12 }}>Show hidden</span>
        </label>
        {alpha && (
          <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>
            Letter: <strong>{alpha}</strong>
            <button className="btn secondary" style={{ marginLeft: 6, padding: "4px 10px" }} onClick={() => setAlpha(null)}>Clear</button>
          </span>
        )}
        <span style={{ color: "var(--ink-muted)", fontSize: 12 }}>{list.length} of {figures.length}</span>
      </div>

      {list.length > 0 && (
        <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 12px", color: "var(--ink-muted)", fontSize: 12 }}>
          <input type="checkbox" checked={list.every((f) => selected.has(f.id)) && list.length > 0}
                 onChange={(e) => {
                   const n = new Set(selected);
                   if (e.target.checked) list.forEach((f) => n.add(f.id));
                   else list.forEach((f) => n.delete(f.id));
                   setSelected(n);
                 }} />
          <span>Select all visible ({list.length})</span>
        </div>
      )}

      {bulkSerp && <BulkSerpAPIModal ids={[...selected]} figures={figures} onClose={() => setBulkSerp(false)} refresh={refresh} />}
      {/* List + alpha bar side-by-side */}
      <div style={{ display: "flex", gap: 0, alignItems: "stretch", border: "0.5px solid var(--bar-border)", borderRadius: "var(--radius)", overflow: "hidden" }}>
        <div style={{ flex: 1, maxHeight: 460, overflowY: "auto" }}>
          {list.map((f) => <CatalogRow key={f.id} fig={f} brands={brands} regions={regions} refresh={refresh} onDelete={() => remove(f.id, f.name)} selected={selected.has(f.id)} onToggle={() => toggleSelect(f.id)} />)}
          {list.length === 0 && <div style={{ padding: 24, textAlign: "center", color: "var(--ink-muted)" }}>No figures match the current filter.</div>}
        </div>
        <AlphaBar figures={figures.filter((f) => (brandFilter === "ALL" || f.brand === brandFilter) && (seriesFilter === "ALL" || f.series === seriesFilter))} active={alpha} onPick={setAlpha} />
      </div>
    </div>
  );
}

// Multi-image lightbox for a catalog row. Toggles through cover + photos + variant.
function CatalogRowGallery({ fig, onClose }) {
  const items = [];
  const cover = coverUrl(fig);
  if (cover) items.push({ url: cover, label: "Cover" });
  if (fig.photos && fig.photos.front)       items.push({ url: fig.photos.front,       label: "Front" });
  if (fig.photos && fig.photos.back)        items.push({ url: fig.photos.back,        label: "Back" });
  if (fig.photos && fig.photos.loose)       items.push({ url: fig.photos.loose,       label: "Loose" });
  if (fig.photos && fig.photos.accessories) items.push({ url: fig.photos.accessories, label: "Accessories" });
  if (fig.variantImage) items.push({ url: fig.variantImage, label: "Variant" });
  const [idx, setIdx] = useState(0);
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowLeft")  setIdx((i) => (i - 1 + items.length) % items.length);
      if (e.key === "ArrowRight") setIdx((i) => (i + 1) % items.length);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [items.length, onClose]);
  if (items.length === 0) return null;
  const cur = items[((idx % items.length) + items.length) % items.length];
  return (
    <div className="lightbox-backdrop" onClick={onClose}>
      <button className="lightbox-close" onClick={onClose} aria-label="Close"
              style={{ display: "flex", alignItems: "center", gap: 5, fontSize: 14,
                       padding: "5px 14px", borderRadius: 20,
                       background: "rgba(0,0,0,0.6)", border: "none", color: "#fff", cursor: "pointer" }}>
        ✕&nbsp;Close
      </button>
      <div onClick={(e) => e.stopPropagation()} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }}>
        <div style={{ color: "#fff", fontSize: 14, opacity: 0.85 }}>
          {fig.name} - <strong>{cur.label}</strong> ({(idx % items.length) + 1} / {items.length})
        </div>
        <img className="lightbox-img" src={cur.url} alt={cur.label} />
        {items.length > 1 && (
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn secondary" onClick={() => setIdx((idx - 1 + items.length) % items.length)}>&larr; Prev</button>
            <button className="btn secondary" onClick={() => setIdx((idx + 1) % items.length)}>Next &rarr;</button>
          </div>
        )}
      </div>
    </div>
  );
}

function CatalogRow({ fig, brands, regions, refresh, onDelete, selected, onToggle }) {
  const [edit, setEdit] = useState(false);
  const [gallery, setGallery] = useState(false);
  const toggleHidden = async () => {
    await api.updateFigure(fig.id, { hidden: !fig.hidden });
    refresh();
  };
  if (!edit) {
    return (
      <div className="catalog-row" style={fig.hidden ? { opacity: 0.5 } : null}>
        {gallery && <CatalogRowGallery fig={fig} onClose={() => setGallery(false)} />}
        <input type="checkbox" checked={!!selected} onChange={onToggle} onClick={(e) => e.stopPropagation()} />
        <img src={coverUrl(fig)} alt="" onClick={() => setGallery(true)} title="Click to enlarge" style={{ width: 36, height: 50, borderRadius: 6, objectFit: "cover", cursor: "zoom-in" }} />
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 600 }}>{fig.name}{fig.hidden && <span className="role-tag" style={{ background: "var(--ink-muted)", color: "#fff" }}>hidden</span>}</div>
          <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>{fig.brand}{fig.series ? " - " + fig.series : ""}{fig.group ? " - " + fig.group : ""}{fig.figure_number ? " - #" + fig.figure_number : ""} - {fig.year} - rarity {fig.rarity}/10{fig.upc ? " - UPC " + fig.upc : ""}</div>
        </div>
        <button className="btn secondary" title={fig.hidden ? "Show this figure to customers" : "Hide this figure from customers"} onClick={toggleHidden}>{fig.hidden ? "Show" : "Hide"}</button>
        <button className="btn secondary" onClick={() => setEdit(true)}>Edit</button>
        <button className="btn danger" onClick={onDelete}>Delete</button>
      </div>
    );
  }
  return <CatalogEdit fig={fig} brands={brands} regions={regions} onCancel={() => setEdit(false)} onSaved={() => { setEdit(false); refresh(); }} />;
}

// ---------- AccessoriesEditor (admin: manage figure accessories list) ---
function AccessoriesEditor({ accessories, onChange, figure }) {
  const items = accessories || [];
  const [newName, setNewName] = useState("");
  const [busy, setBusy] = useState(false);
  const [aiAvailable, setAiAvailable] = useState(false);

  useEffect(() => {
    api.aiConfig().then((j) => setAiAvailable(!!(j && j.configured))).catch(() => {});
  }, []);

  const add = () => {
    const n = newName.trim();
    if (!n) return;
    if (items.some((a) => a.name.toLowerCase() === n.toLowerCase())) return;
    onChange([...items, { name: n, imageUrl: null }]);
    setNewName("");
  };
  const remove = (i) => {
    const next = [...items]; next.splice(i, 1); onChange(next);
  };
  const setImageUrl = (i, url) => {
    const next = [...items];
    next[i] = { ...next[i], imageUrl: url || null };
    onChange(next);
  };
  const suggest = async () => {
    if (!figure || !figure.name) return alert("Enter a figure name first.");
    setBusy(true);
    try {
      const r = await api.aiSuggestAccessories({ name: figure.name, brand: figure.brand, series: figure.series, year: figure.year });
      if (!r.ok) return alert("AI error: " + (r.error || "failed"));
      const existing = new Set(items.map((a) => a.name.toLowerCase()));
      const merged = [...items, ...(r.accessories || []).filter((a) => !existing.has(a.name.toLowerCase()))];
      onChange(merged);
    } finally { setBusy(false); }
  };

  return (
    <div style={{ padding: 8, background: "var(--panel-alt)", borderRadius: "var(--radius)" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }}>
        <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>Accessories list ({items.length})</span>
        {aiAvailable && (
          <button className="btn secondary" style={{ padding: "3px 10px", fontSize: 11 }} disabled={busy} onClick={suggest}>
            {busy ? "..." : "AI Suggest accessories"}
          </button>
        )}
      </div>
      {items.length > 0 && (
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 8 }}>
          {items.map((a, i) => (
            <div key={i} style={{ display: "flex", alignItems: "center", gap: 4, background: "var(--panel)", border: "0.5px solid var(--bar-border)", borderRadius: 20, padding: "3px 8px 3px 6px", fontSize: 12 }}>
              {a.imageUrl && (
                <img src={a.imageUrl} alt={a.name} style={{ width: 18, height: 18, borderRadius: 3, objectFit: "cover" }} />
              )}
              <span>{a.name}</span>
              <input
                type="text"
                value={a.imageUrl || ""}
                onChange={(e) => setImageUrl(i, e.target.value)}
                placeholder="img url"
                title="Optional image URL for this accessory"
                style={{ width: 90, fontSize: 10, padding: "2px 4px", background: "var(--panel-alt)", border: "0.5px solid var(--bar-border)", borderRadius: 4, color: "var(--ink-muted)" }}
              />
              <button onClick={() => remove(i)} title="Remove" style={{ background: "transparent", border: 0, cursor: "pointer", color: "var(--ink-muted)", fontSize: 14, lineHeight: 1, padding: "0 2px" }}>×</button>
            </div>
          ))}
        </div>
      )}
      {items.length === 0 && (
        <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 8 }}>
          No accessories yet. Add manually or use AI Suggest.
        </div>
      )}
      <div style={{ display: "flex", gap: 6 }}>
        <input
          value={newName}
          onChange={(e) => setNewName(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && add()}
          placeholder="e.g. Lightsaber, Blaster, Cape..."
          style={{ flex: 1, fontSize: 13 }}
        />
        <button className="btn" disabled={!newName.trim()} onClick={add}>+ Add</button>
      </div>
    </div>
  );
}

function CatalogEdit({ fig, brands, regions, onCancel, onSaved }) {
  const [d, setD] = useState({ ...fig });
  const set = (k, v) => setD({ ...d, [k]: v });
  const seriesOptions = (brands[d.brand] && brands[d.brand].series) || [];
  const groupOptions = (brands[d.brand] && brands[d.brand].groups) || [];
  const save = async () => { await api.updateFigure(fig.id, d); onSaved(); };
  const [serpOpen, setSerpOpen] = useState(false);
  return (
    <div className="catalog-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 8, padding: 12 }}>
      <AISuggestRow name={d.name} getCurrent={() => d} applyPatch={(p) => setD({ ...d, ...p })} />
      <div style={{ display: "flex", gap: 6, alignItems: "center", padding: 8, background: "var(--panel-alt)", borderRadius: "var(--radius)", marginBottom: 8 }}>
        <span style={{ fontSize: 12, color: "var(--ink-muted)" }}>SerpAPI:</span>
        <button className="btn secondary" onClick={() => setSerpOpen(true)} disabled={!d.name}>Search Images with SerpAPI</button>
      </div>
      {serpOpen && <SerpImageSearchModal figure={d} onClose={() => setSerpOpen(false)} onSaved={() => { onSaved && onSaved(); }} />}
      <div className="form-grid">
        <label>Name<input value={d.name} onChange={(e) => set("name", e.target.value)} /></label>
        <label>Brand<select value={d.brand} onChange={(e) => set("brand", e.target.value)}>{Object.keys(brands).map((b) => <option key={b}>{b}</option>)}</select></label>
        <label>Series<select value={d.series || ""} onChange={(e) => set("series", e.target.value)}><option value="">-- none --</option>{seriesOptions.map((s) => <option key={s}>{s}</option>)}</select></label>
        <label>Group / Wave<select value={d.group || ""} onChange={(e) => set("group", e.target.value)}><option value="">-- none --</option>{groupOptions.map((g) => <option key={g}>{g}</option>)}</select></label>
        <label>Year<input type="number" value={d.year} onChange={(e) => set("year", Number(e.target.value))} /></label>
        <label>Region<select value={d.region} onChange={(e) => set("region", e.target.value)}>{(regions || ["US","EU","AU"]).map((r) => <option key={r}>{r}</option>)}</select></label>
        <label>Rarity (1-10)<input type="number" min="1" max="10" value={d.rarity} onChange={(e) => set("rarity", Number(e.target.value))} /></label>
        <label>Original Retail Price<input type="number" step="0.01" value={d.original_retail_value || 0} onChange={(e) => set("original_retail_value", Number(e.target.value))} /></label>
        <label>Loose value<input type="number" value={d.loose_value} onChange={(e) => set("loose_value", Number(e.target.value))} /></label>
        <label>Complete value<input type="number" value={d.complete_value} onChange={(e) => set("complete_value", Number(e.target.value))} /></label>
        <label>MOC/MIB value<input type="number" value={d.moc_value} onChange={(e) => set("moc_value", Number(e.target.value))} /></label>
        <label>Graded avg eBay value<input type="number" value={d.graded_value || 0} onChange={(e) => set("graded_value", Number(e.target.value))} /></label>
        <label>UPC<input value={d.upc || ""} onChange={(e) => set("upc", e.target.value)} placeholder="UPC barcode" /></label>
        <label>Figure number<input value={d.figure_number || ""} onChange={(e) => set("figure_number", e.target.value)} placeholder="e.g. 39-100, TF-001 (leave blank if none)" /></label>
        <label>Variant of (figure id, optional)<input type="number" value={d.variantOf == null ? "" : d.variantOf} onChange={(e) => set("variantOf", e.target.value === "" ? null : Number(e.target.value))} placeholder="leave blank if not a variant" /></label>
        <label>Variant note<input value={d.variantNote || ""} onChange={(e) => set("variantNote", e.target.value)} placeholder="e.g. diamond foil, kicking variant" /></label>
      </div>
      <label style={{ fontSize: 12, color: "var(--ink-muted)" }}>Description<textarea className="notes" value={d.description || ""} onChange={(e) => set("description", e.target.value)} /></label>
      <AccessoriesEditor
        accessories={d.accessories || []}
        onChange={(list) => set("accessories", list)}
        figure={d}
      />
      <div style={{ display: "flex", alignItems: "center", gap: 12, padding: 8, background: "var(--panel-alt)", borderRadius: "var(--radius)" }}>
        <img src={coverUrl(d)} alt="" style={{ width: 56, height: 78, borderRadius: 6, objectFit: "contain", background: "var(--panel)" }} />
        <PhotoUploader
          currentUrl={d.customCoverUrl}
          onUploaded={async (url) => { await api.updateFigure(fig.id, { customCoverUrl: url }); set("customCoverUrl", url); }}
          onRemoved={async () => { await api.updateFigure(fig.id, { customCoverUrl: null }); set("customCoverUrl", null); }}
          label="catalog cover"
        />
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 12, padding: 8, background: "var(--panel-alt)", borderRadius: "var(--radius)" }}>
        <div style={{ fontSize: 12, color: "var(--ink-muted)", flex: 1 }}>Variant photo (used to highlight what's different from the base figure)</div>
        {d.variantImage && <img src={d.variantImage} style={{ width: 64, height: 64, objectFit: "cover", borderRadius: 6 }} />}
        <PhotoUploader currentUrl={d.variantImage} onUploaded={async (u) => { await api.updateFigure(fig.id, { variantImage: u }); set("variantImage", u); }} onRemoved={async () => { await api.updateFigure(fig.id, { variantImage: null }); set("variantImage", null); }} label="variant photo" />
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(170px, 1fr))", gap: 8 }}>
        {["front","back","loose","accessories"].map((slot) => {
          const photos = d.photos || { front: null, back: null, loose: null, accessories: null };
          const url = photos[slot];
          return (
            <div key={slot} style={{ background: "var(--panel-alt)", borderRadius: "var(--radius)", padding: 8, textAlign: "center" }}>
              <div style={{ fontSize: 12, color: "var(--ink-muted)", textTransform: "capitalize", marginBottom: 4 }}>{slot} photo</div>
              {url
                ? <img src={url} style={{ width: "100%", aspectRatio: "1 / 1", objectFit: "cover", borderRadius: 8, background: "var(--panel)" }} />
                : <div style={{ width: "100%", aspectRatio: "1 / 1", borderRadius: 8, background: "var(--panel)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-muted)", fontSize: 12 }}>(empty)</div>
              }
              <div style={{ marginTop: 6 }}>
                <PhotoUploader
                  currentUrl={url}
                  onUploaded={async (u) => { const nphotos = { ...photos, [slot]: u }; await api.updateFigure(fig.id, { photos: nphotos }); set("photos", nphotos); }}
                  onRemoved={async () => { const nphotos = { ...photos, [slot]: null }; await api.updateFigure(fig.id, { photos: nphotos }); set("photos", nphotos); }}
                  label={slot}
                />
              </div>
            </div>
          );
        })}
      </div>
      <div className="btn-row"><button className="btn" onClick={save}>Save</button><button className="btn secondary" onClick={onCancel}>Cancel</button></div>
    </div>
  );
}

function CatalogNew({ brands, regions, onCancel, onSaved }) {
  const firstBrand = Object.keys(brands)[0] || "";
  const [d, setD] = useState({ name: "", brand: firstBrand, series: "", group: "", year: 2024, region: "US", rarity: 3, loose_value: 0, complete_value: 0, moc_value: 0, graded_value: 0, original_retail_value: 0, variantOf: null, variantNote: "", figure_number: "", description: "", upc: "", accessories: [] });
  const set = (k, v) => setD({ ...d, [k]: v });
  const seriesOptions = (brands[d.brand] && brands[d.brand].series) || [];
  const groupOptions = (brands[d.brand] && brands[d.brand].groups) || [];
  const save = async () => {
    if (!d.name) return alert("name required");
    const r = await api.createFigure(d);
    if (!r.ok) return alert((await r.json()).error || "failed");
    onSaved();
  };
  return (
    <div className="catalog-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 8, padding: 12, marginTop: 10 }}>
      <div style={{ fontWeight: 700, fontSize: 16 }}>New Figure</div>
      <AISuggestRow name={d.name} getCurrent={() => d} applyPatch={(p) => setD({ ...d, ...p })} />
      <div className="form-grid">
        <label>Name<input value={d.name} onChange={(e) => set("name", e.target.value)} /></label>
        <label>Brand<select value={d.brand} onChange={(e) => set("brand", e.target.value)}>{Object.keys(brands).map((b) => <option key={b}>{b}</option>)}</select></label>
        <label>Series<select value={d.series} onChange={(e) => set("series", e.target.value)}><option value="">-- none --</option>{seriesOptions.map((s) => <option key={s}>{s}</option>)}</select></label>
        <label>Group / Wave<select value={d.group} onChange={(e) => set("group", e.target.value)}><option value="">-- none --</option>{groupOptions.map((g) => <option key={g}>{g}</option>)}</select></label>
        <label>Year<input type="number" value={d.year} onChange={(e) => set("year", Number(e.target.value))} /></label>
        <label>Region<select value={d.region} onChange={(e) => set("region", e.target.value)}>{(regions || ["US","EU","AU"]).map((r) => <option key={r}>{r}</option>)}</select></label>
        <label>Rarity (1-10)<input type="number" min="1" max="10" value={d.rarity} onChange={(e) => set("rarity", Number(e.target.value))} /></label>
        <label>Original Retail Price<input type="number" step="0.01" value={d.original_retail_value || 0} onChange={(e) => set("original_retail_value", Number(e.target.value))} /></label>
        <label>Loose value<input type="number" value={d.loose_value} onChange={(e) => set("loose_value", Number(e.target.value))} /></label>
        <label>Complete value<input type="number" value={d.complete_value} onChange={(e) => set("complete_value", Number(e.target.value))} /></label>
        <label>MOC/MIB value<input type="number" value={d.moc_value} onChange={(e) => set("moc_value", Number(e.target.value))} /></label>
        <label>Graded avg eBay value<input type="number" value={d.graded_value || 0} onChange={(e) => set("graded_value", Number(e.target.value))} /></label>
        <label>UPC<input value={d.upc} onChange={(e) => set("upc", e.target.value)} /></label>
        <label>Figure number<input value={d.figure_number || ""} onChange={(e) => set("figure_number", e.target.value)} placeholder="e.g. 39-100" /></label>
        <label>Variant of (figure id, optional)<input type="number" value={d.variantOf == null ? "" : d.variantOf} onChange={(e) => set("variantOf", e.target.value === "" ? null : Number(e.target.value))} placeholder="leave blank if not a variant" /></label>
        <label>Variant note<input value={d.variantNote || ""} onChange={(e) => set("variantNote", e.target.value)} placeholder="e.g. diamond foil" /></label>
      </div>
      <label style={{ fontSize: 12, color: "var(--ink-muted)" }}>Description<textarea className="notes" value={d.description} onChange={(e) => set("description", e.target.value)} /></label>
      <AccessoriesEditor
        accessories={d.accessories || []}
        onChange={(list) => set("accessories", list)}
        figure={d}
      />
      <div className="btn-row"><button className="btn" onClick={save}>Create</button><button className="btn secondary" onClick={onCancel}>Cancel</button></div>
    </div>
  );
}

// ---------- Manage Users (admin only) ------------------------------
function ManageUsers({ me }) {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [adding, setAdding] = useState(false);
  const [expanded, setExpanded] = useState(null);
  const [draft, setDraft] = useState({ username: "", password: "", role: "customer" });

  const load = async () => {
    setLoading(true);
    const j = await api.listUsers();
    setUsers(j.users || []);
    setLoading(false);
  };
  useEffect(() => { load(); }, []);

  const create = async () => {
    if (!draft.username || !draft.password) return alert("username and password required");
    const r = await api.createUser(draft);
    if (!r.ok) return alert((await r.json()).error || "failed");
    setDraft({ username: "", password: "", role: "customer" });
    setAdding(false);
    load();
  };

  const setRole = async (username, role) => {
    const r = await api.updateUser(username, { role });
    if (!r.ok) alert((await r.json()).error || "failed");
    load();
  };
  const setPermission = async (username, key, value) => {
    const r = await api.updateUser(username, { permissions: { [key]: value } });
    if (!r.ok) alert((await r.json()).error || "failed");
    load();
  };
  const setDisabled = async (username, disabled) => {
    const r = await api.updateUser(username, { disabled });
    if (!r.ok) alert((await r.json()).error || "failed");
    load();
  };
  const resetPassword = async (username) => {
    const pwd = prompt("New password for " + username + ":");
    if (!pwd) return;
    const r = await api.updateUser(username, { password: pwd });
    if (!r.ok) alert((await r.json()).error || "failed");
    else alert("Password updated.");
  };
  const remove = async (username) => {
    if (!confirm("Delete user \"" + username + "\" and all their collection data? This cannot be undone.")) return;
    const r = await api.deleteUser(username);
    if (!r.ok) alert((await r.json()).error || "failed");
    load();
  };

  return (
    <div className="page">
      <h2>Manage Users <span className="role-tag admin">admin</span></h2>
      {loading && <p style={{ color: "var(--ink-muted)" }}>Loading users...</p>}

      {!loading && (
        <div style={{ overflowX: "auto" }}>
          <table style={{ width: "100%", borderCollapse: "collapse" }}>
            <thead>
              <tr style={{ textAlign: "left", color: "var(--ink-muted)", fontSize: 12, textTransform: "uppercase", letterSpacing: 0.4 }}>
                <th style={{ padding: "8px 10px" }}>Username</th>
                <th style={{ padding: "8px 10px" }}>Role</th>
                <th style={{ padding: "8px 10px" }}>Status</th>
                <th style={{ padding: "8px 10px" }}>Owned</th>
                <th style={{ padding: "8px 10px" }}>Sessions</th>
                <th style={{ padding: "8px 10px" }}>Created</th>
                <th style={{ padding: "8px 10px" }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {users.flatMap((u) => {
                const isMe = u.username === me.username;
                const open = expanded === u.username;
                const rows = [
                  <tr key={u.username} style={{ borderTop: "0.5px solid var(--bar-border)" }}>
                    <td style={{ padding: "10px" }}>
                      <strong>{u.username}</strong>{isMe && <span style={{ marginLeft: 6, fontSize: 11, color: "var(--ink-muted)" }}>(you)</span>}
                    </td>
                    <td style={{ padding: "10px" }}>
                      <select value={u.role} disabled={isMe} onChange={(e) => setRole(u.username, e.target.value)}>
                        <option value="customer">customer</option>
                        <option value="admin">admin</option>
                      </select>
                    </td>
                    <td style={{ padding: "10px" }}>
                      {u.disabled
                        ? <span className="role-tag" style={{ background: "var(--red)", color: "#fff" }}>disabled</span>
                        : <span className="role-tag" style={{ background: "var(--green)", color: "#fff" }}>active</span>}
                    </td>
                    <td style={{ padding: "10px", color: "var(--ink-muted)" }}>{u.figureCount}</td>
                    <td style={{ padding: "10px", color: "var(--ink-muted)" }}>{u.sessionCount}</td>
                    <td style={{ padding: "10px", color: "var(--ink-muted)", fontSize: 12 }}>
                      {u.createdAt ? new Date(u.createdAt).toLocaleDateString() : "--"}
                    </td>
                    <td style={{ padding: "10px", display: "flex", gap: 6, flexWrap: "wrap" }}>
                      <button className="btn secondary" disabled={isMe} onClick={() => setDisabled(u.username, !u.disabled)}>
                        {u.disabled ? "Enable" : "Disable"}
                      </button>
                      <button className="btn secondary" onClick={() => resetPassword(u.username)}>Reset password</button>
                      <button className="btn secondary" onClick={() => setExpanded(open ? null : u.username)}>
                        {open ? "Hide perms" : "Permissions"}
                      </button>
                      <button className="btn danger" disabled={isMe} onClick={() => remove(u.username)}>Delete</button>
                    </td>
                  </tr>
                ];
                if (open) {
                  rows.push(
                    <tr key={u.username + "-perms"} style={{ background: "var(--panel-alt)" }}>
                      <td colSpan={7} style={{ padding: 14 }}>
                        <div style={{ fontSize: 12, color: "var(--ink-muted)", marginBottom: 8 }}>Fine-grained permissions for <strong>{u.username}</strong></div>
                        <div style={{ display: "flex", flexWrap: "wrap", gap: 16 }}>
                          {PERMISSION_KEYS.map((k) => (
                            <label key={k} className="switch">
                              <input type="checkbox"
                                     checked={!!(u.permissions && u.permissions[k])}
                                     disabled={isMe}
                                     onChange={(ev) => setPermission(u.username, k, ev.target.checked)} />
                              <span className="track" /> {PERMISSION_LABELS[k]}
                            </label>
                          ))}
                        </div>
                        <div style={{ marginTop: 8, fontSize: 12, color: "var(--ink-muted)" }}>
                          Tip: Setting role to <strong>admin</strong> turns all on; <strong>customer</strong> turns all off. You can flip individual toggles afterwards.
                        </div>
                      </td>
                    </tr>
                  );
                }
                return rows;
              })}
            </tbody>
          </table>
        </div>
      )}

      <div style={{ marginTop: 16 }}>
        {adding ? (
          <div className="brand-row">
            <input placeholder="Username" value={draft.username} onChange={(e) => setDraft({ ...draft, username: e.target.value })} />
            <input type="password" placeholder="Password" value={draft.password} onChange={(e) => setDraft({ ...draft, password: e.target.value })} />
            <select value={draft.role} onChange={(e) => setDraft({ ...draft, role: e.target.value })}>
              <option value="customer">customer</option>
              <option value="admin">admin</option>
            </select>
            <button className="btn" onClick={create}>Create user</button>
            <button className="btn secondary" onClick={() => setAdding(false)}>Cancel</button>
          </div>
        ) : (
          <button className="btn" onClick={() => setAdding(true)}>+ Add user</button>
        )}
      </div>

      <div style={{ marginTop: 18, padding: 12, background: "var(--panel-alt)", borderRadius: "var(--radius)", fontSize: 13, color: "var(--ink-muted)" }}>
        <strong style={{ color: "var(--ink)" }}>Permissions overview</strong>
        <ul style={{ margin: "8px 0 0 18px", padding: 0 }}>
          <li><strong>canManageCatalog</strong>: add / edit / delete figures, upload catalog photos.</li>
          <li><strong>canManageBrands</strong>: add / edit / delete brands and brand-scoped series.</li>
          <li><strong>canManageUsers</strong>: this page (add / disable / reset password / change role / delete users).</li>
        </ul>
        <div style={{ marginTop: 6 }}>The <em>role</em> is just a shortcut: setting role to <strong>admin</strong> flips all permissions on; <strong>customer</strong> flips them off. Use the per-row Permissions panel to grant individual capabilities.</div>
        <div style={{ marginTop: 6 }}>You can't change your own role, permissions, or disabled flag; have another admin do it.</div>
      </div>
    </div>
  );
}

// ---------- More menu ---------------------------------------------
function MoreMenu({ setPage, role }) {
  const tiles = [
    { label: "Trophy Room",       glyph: "T", page: "trophy",  primary: true },
    { label: "Budget tracker",    glyph: "$", page: "budget" },
    { label: "Brand overview",    glyph: "%", page: "brand_chart" },
    { label: "Cover Mode",        glyph: "C", page: "cover" },
    { label: "Popular figures",   glyph: "*", page: "popular" },
    { label: "Help & Info",       glyph: "?", page: "help" },
    { label: "Settings",          glyph: "=", page: "settings" },
  ];
  const perms = arguments[0].permissions || {};
  // My messages is for any signed-in user
  tiles.push({ label: "My messages", glyph: "M", page: "my_messages" });
  if (perms.canManageUsers)   tiles.push({ label: "Manage Users",   glyph: "U", page: "manage_users",   primary: true });
  if (perms.canManageUsers)   tiles.push({ label: "Message Center", glyph: "@", page: "message_center", primary: true });
  if (perms.canManageBrands)  tiles.push({ label: "Manage Brands",  glyph: "B", page: "manage_brands",  primary: true });
  if (perms.canManageBrands)  tiles.push({ label: "Manage Regions", glyph: "R", page: "manage_regions", primary: true });
  if (perms.canManageCatalog) tiles.push({ label: "Manage Catalog", glyph: "K", page: "manage_catalog", primary: true });
  return (
    <div className="page">
      <h2>More</h2>
      <div className="more-grid">
        {tiles.map((t) => (
          <button key={t.label} className={"more-tile" + (t.primary ? " primary" : "")} onClick={() => setPage(t.page)}>
            <div className="glyph">{t.glyph}</div><div>{t.label}</div>
          </button>
        ))}
      </div>
    </div>
  );
}

function SimplePage({ title, children }) { return <div className="page"><h2>{title}</h2>{children}</div>; }

// ---------- ShareView (public read-only) -------------------------
function ShareView({ token }) {
  const [data, setData] = useState(null);
  const [err, setErr] = useState("");
  const [openId, setOpenId] = useState(null);
  const [lightbox, setLightbox] = useState(null);

  useEffect(() => {
    api.share(token).then((d) => { if (!d) setErr("Share link not found or has been revoked."); else setData(d); }).catch(() => setErr("Could not load share link."));
  }, [token]);

  if (err) return <div className="auth-shell"><div className="auth-card">{err}</div></div>;
  if (!data) return <div style={{ padding: 40, textAlign: "center" }}>Loading...</div>;

  const fig = data.figures.find((f) => f.id === openId) || null;

  // Build ordered image list for the open figure (same logic as FigureDetail).
  const shareLightboxItems = fig ? (() => {
    const items = [];
    const cv = coverUrl(fig, fig.userEntry);
    if (cv) items.push({ url: cv, label: "Cover" });
    const photos = fig.photos || {};
    if (photos.front)       items.push({ url: photos.front,       label: "Front" });
    if (photos.back)        items.push({ url: photos.back,        label: "Back" });
    if (photos.loose)       items.push({ url: photos.loose,       label: "Loose" });
    if (photos.accessories) items.push({ url: photos.accessories, label: "Accessories photo" });
    if (fig.variantImage)   items.push({ url: fig.variantImage,   label: "Variant" });
    (fig.accessories || []).forEach((a) => { if (a.imageUrl) items.push({ url: a.imageUrl, label: a.name }); });
    return items;
  })() : [];
  const openShareLightbox = (url) => {
    const i = shareLightboxItems.findIndex((item) => item.url === url);
    setLightbox(i >= 0 ? i : 0);
  };

  const totalValue = data.figures.reduce((a, f) => {
    const e = f.userEntry || {};
    const isComplete = e.condition === "mint" || e.condition === "near_mint" || e.condition === "moc";
    const base = e.condition === "moc" ? f.moc_value : (isComplete ? f.complete_value : f.loose_value);
    return a + base * (e.quantity || 1);
  }, 0);

  return (
    <div className="app">
      <div className="top-bar" style={{ gridTemplateColumns: "1fr auto" }}>
        <div className="title-block">
          <span className="title">Toy Collector</span>
          <span className="role-tag">read-only</span>
          <span style={{ marginLeft: 12, color: "var(--ink-muted)", fontSize: 14 }}>
            {data.username}'s {data.kind === "wanted" ? "Wanted list" : "Collection"}
            {" · "} {data.figures.length} figures {" · "} {fmtMoney(totalValue, data.currency)}
          </span>
        </div>
        <a href="/" className="btn secondary">Sign in</a>
      </div>
      <div className="main" style={{ gridTemplateColumns: "1fr" }}>
        <div className="content" style={{ padding: 18 }}>
          <div className="grid">
            {data.figures.map((f) => {
              const e = f.userEntry || {};
              const cond = CONDITION_BY_ID[e.condition];
              return (
                <div key={f.id} className="card" onClick={() => setOpenId(f.id)}>
                  <div className="cover"><img src={coverUrl(f, e)} alt={f.name} /></div>
                  <div className="caption">{f.name}</div>
                  {e.quantity > 1 && <span className="pill">x{e.quantity}</span>}
                  {cond && <span className="pill" style={{ background: cond.color, color: "#000" }}>{cond.label}</span>}
                </div>
              );
            })}
            {data.figures.length === 0 && <div style={{ color: "var(--ink-muted)", padding: 40, gridColumn: "1 / -1", textAlign: "center" }}>No figures in this list.</div>}
          </div>
        </div>
      </div>

      {fig && (
        <div className="modal-backdrop" onClick={() => setOpenId(null)}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <div className="modal-head">
              <div>
                <div style={{ fontWeight: 700 }}>{fig.name}</div>
                <div style={{ fontSize: 13, color: "var(--ink-muted)" }}>{fig.brand}{fig.series ? " - " + fig.series : ""}{fig.group ? " - " + fig.group : ""} - {fig.year} - {fig.region}</div>
              </div>
              <button className="close" onClick={() => setOpenId(null)}>x</button>
            </div>
            <div className="modal-body">
              <div>
                <img className="detail-cover" src={coverUrl(fig, fig.userEntry)} alt={fig.name} style={{ cursor: "zoom-in" }} onClick={() => openShareLightbox(coverUrl(fig, fig.userEntry))} />
                <PhotoGallery photos={fig.photos} onOpen={openShareLightbox} />
              </div>
              <div>
                <table className="pricing-table">
                  <thead><tr><th></th><th>Value</th><th>Owner's listing</th></tr></thead>
                  <tbody>
                    <tr><td>Loose</td><td>{fmtMoney(fig.loose_value, data.currency)}</td><td>{fig.userEntry && fig.userEntry.condition ? CONDITION_BY_ID[fig.userEntry.condition].label : "--"}</td></tr>
                    <tr><td>Complete</td><td>{fmtMoney(fig.complete_value, data.currency)}</td><td>--</td></tr>
                    <tr><td>MOC/MIB</td><td>{fmtMoney(fig.moc_value, data.currency)}</td><td>--</td></tr>
                    <tr><td>Graded avg eBay</td><td>{fmtMoney(fig.graded_value || 0, data.currency)}</td><td>{fig.userEntry && fig.userEntry.grading ? (fig.userEntry.grading.company + " " + fig.userEntry.grading.grade) : "--"}</td></tr>
                  </tbody>
                </table>
                {fig.description && (
                  <div style={{ padding: 12, background: "var(--panel-alt)", borderRadius: "var(--radius)", fontSize: 14 }}>{fig.description}</div>
                )}
              </div>
            </div>
          </div>
        </div>
      )}

      {lightbox !== null && <FigureLightbox items={shareLightboxItems} startIdx={lightbox} onClose={() => setLightbox(null)} />}
    </div>
  );
}

// ---------- PhotoGallery (front / back / loose thumbnails) ---------
function PhotoGallery({ photos, onOpen }) {
  if (!photos) return null;
  const slots = [["front","Front"],["back","Back"],["loose","Loose"],["accessories","Accessories"]];
  const present = slots.filter(([k]) => photos[k]);
  if (present.length === 0) return null;
  return (
    <div style={{ display: "flex", gap: 6, marginTop: 8 }}>
      {present.map(([k, label]) => (
        <div key={k} style={{ flex: 1, textAlign: "center" }}>
          <img src={photos[k]} alt={label} title={label}
               style={{ width: "100%", aspectRatio: "1 / 1", objectFit: "cover", borderRadius: 8, cursor: "zoom-in", background: "var(--panel-alt)" }}
               onClick={() => onOpen(photos[k])} />
          <div style={{ fontSize: 11, color: "var(--ink-muted)", marginTop: 2 }}>{label}</div>
        </div>
      ))}
    </div>
  );
}

// ---------- App root ---------------------------------------------
// ---------- Root App ----------------------------------------------
function App() {
  const [me, setMe] = useState(null);
  const [authChecked, setAuthChecked] = useState(false);
  const [catalog, refreshCatalog] = useCatalog();
  const [state, update, syncStatus] = useSyncedState(!!me);
  const [tab, setTab] = useState("all");
  const [page, setPage] = useState(null);
  const [openId, setOpenId] = useState(null);
  const [brandFilter, setBrandFilter] = useState(() => new Set());
  const [seriesFilter, setSeriesFilter] = useState("ALL");
  const [groupFilter, setGroupFilter] = useState("ALL");
  const [sort, setSort] = useState("rarity");
  const [view, setView] = useState("grid");
  const [search, setSearch] = useState("");
  const [alpha, setAlpha] = useState(null);
  const [scanning, setScanning] = useState(false);
  const [showHidden, setShowHidden] = useState(false);

  const goHome = () => { setTab("all"); setPage(null); setBrandFilter(new Set()); setSeriesFilter("ALL"); setGroupFilter("ALL"); setSearch(""); setAlpha(null); setOpenId(null); };

  const darkMode = state && state.settings && state.settings.darkMode;
  useEffect(() => { if (darkMode != null) document.body.classList.toggle("dark", !!darkMode); }, [darkMode]);

  const allFiguresRaw = useMemo(() => catalog.figures || [], [catalog.figures]);
  // Auto-hide for everyone unless an admin has flipped the "Show hidden" switch.
  const canSeeHidden = !!(me && me.permissions && me.permissions.canManageCatalog && showHidden);
  const allFigures = useMemo(() => {
    if (canSeeHidden) return allFiguresRaw;
    return (allFiguresRaw || []).filter((f) => !f.hidden && !((catalog.brands[f.brand] && (catalog.brands[f.brand].hiddenSeries || []).includes(f.series))) && !((catalog.brands[f.brand] && (catalog.brands[f.brand].hiddenGroups || []).includes(f.group))));
  }, [allFiguresRaw, canSeeHidden, catalog.brands]);

  useEffect(() => { api.me().then((m) => { setMe(m); setAuthChecked(true); }); }, []);

  // Public share-link mode: no auth required.
  const shareMatch = window.location.pathname.match(/^\/share\/([a-f0-9]+)$/);
  if (shareMatch) return <ShareView token={shareMatch[1]} />;

  if (!authChecked) return <div style={{ padding: 40, textAlign: "center" }}>Loading...</div>;
  if (!me)         return <AuthScreen onAuthed={(m) => setMe(m)} />;
  if (!state)      return <div style={{ padding: 40, textAlign: "center" }}>Loading collection...</div>;

  const logout = async () => { await api.logout(); setMe(null); };

  let visible = allFigures;
  if (brandFilter.size > 0) visible = visible.filter((f) => brandFilter.has(f.brand));
  if (seriesFilter !== "ALL") visible = visible.filter((f) => f.series === seriesFilter);
  if (groupFilter  !== "ALL") visible = visible.filter((f) => f.group === groupFilter);
  visible = visible.filter((f) => state.excluded.indexOf(f.id) < 0);
  if (search.trim()) {
    // Split query into tokens. Every token must match somewhere.
    const tokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
    visible = visible.filter((f) => {
      const haystacks = [
        f.name.toLowerCase(),
        (f.brand || "").toLowerCase(),
        (f.series || "").toLowerCase(),
        (f.group || "").toLowerCase(),
        (f.upc || "").toLowerCase(),
        (f.figure_number || "").toLowerCase(),
        String(f.year || ""),
      ];
      return tokens.every((tok) =>
        // 4-digit number must match the year exactly; other tokens substring-match any field.
        /^\d{4}$/.test(tok) ? String(f.year) === tok : haystacks.some((h) => h.indexOf(tok) >= 0)
      );
    });
  }
  if (alpha) visible = visible.filter((f) => firstChar(f.name) === alpha);
  visible = [...visible].sort((a, b) =>
    sort === "rarity" ? b.rarity - a.rarity :
    sort === "value"  ? b.complete_value - a.complete_value :
    sort === "name"   ? a.name.localeCompare(b.name) :
                        b.user_rating - a.user_rating
  );

  const gridList =
    tab === "wanted"     ? visible.filter((f) => state.collection[f.id] && state.collection[f.id].wanted) :
    tab === "collection" ? visible.filter((f) => state.collection[f.id] && state.collection[f.id].owned) :
    tab === "remaining"  ? visible.filter((f) => !(state.collection[f.id] && state.collection[f.id].owned)) :
    visible;

  const openFigure = (id) => setOpenId(id);
  const openFigureObj = allFigures.find((f) => f.id === openId);

  const onScanned = (text) => {
    setScanning(false);
    const t = (text || "").trim();
    if (!t) return;
    const fig = allFigures.find((f) => (f.upc || "") === t);
    if (fig) setOpenId(fig.id);
    else { alert("No figure found with UPC " + t + "."); setSearch(t); setTab("all"); }
  };

  let overlay = null;
  if (tab === "more" && page) {
    if (page === "trophy")          overlay = <TrophyRoom figures={allFigures} state={state} onOpen={openFigure} />;
    else if (page === "budget")     overlay = <BudgetTracker figures={allFigures} state={state} update={update} />;
    else if (page === "brand_chart")overlay = <BrandOverview figures={allFigures} brands={catalog.brands} state={state} />;
    else if (page === "cover")      overlay = <CoverMode figures={allFigures} state={state} onOpen={openFigure} />;
    else if (page === "settings")   overlay = <Settings state={state} update={update} syncStatus={syncStatus} me={me} onLogout={logout} figures={allFigures} />;
    else if (page === "manage_users")   overlay = <ManageUsers me={me} />;
    else if (page === "manage_brands")  overlay = <ManageBrands brands={catalog.brands} figures={allFiguresRaw} refresh={refreshCatalog} />;
    else if (page === "manage_catalog") overlay = <ManageCatalog figures={allFigures} brands={catalog.brands} regions={catalog.regions} refresh={refreshCatalog} />;
    else if (page === "manage_regions") overlay = <ManageRegions />;
    else if (page === "message_center") overlay = <MessageCenter figures={allFigures} />;
    else if (page === "my_messages")    overlay = <MyMessages />;
    else if (page === "popular") overlay = (
      <SimplePage title="Popular Figures">
        <div className="trophy-grid" style={{ marginTop: 12 }}>
          {[...allFigures].sort((a,b) => b.user_rating - a.user_rating).slice(0,8).map((f) => (
            <div key={f.id} className="trophy-card" onClick={() => openFigure(f.id)}>
              <div className="cover"><img src={coverUrl(f, state.collection[f.id])} /></div>
              <div className="name">{f.name}</div>
              <div className="val">{f.user_rating} *</div>
            </div>
          ))}
        </div>
      </SimplePage>
    );
    else if (page === "help") overlay = (
      <SimplePage title="Help & Info">
        <p>Tap a figure to view details, log purchases/sales, set the condition, attach a personal photo, or add notes.</p>
        <p>Use the search bar to find figures by <strong>name, brand, series, or UPC</strong>. Tap the scan button to use your camera to read a UPC.</p>
        <p>Toggle between grid and list view from the buttons in the top bar. Use the alphabet bar on the right to jump to a letter.</p>
        <p>Export Collection / Wanted / Remaining as CSV from the Settings page.</p>
        {me.role === "admin"
          ? <p>You're an <strong>admin</strong> -- use Manage Brands (with the Series editor) and Manage Catalog to edit the shared catalog.</p>
          : <p>Catalog edits (adding figures, brands, series, prices) are admin-only.</p>}
      </SimplePage>
    );
  }

  return (
    <div className="app">
      <div className="top-bar">
        <div className="title-block">
          <button className="title-link" onClick={goHome} title="Home">Toy Collector</button>
          <span className={"sync-badge " + syncStatus}>{syncStatus}</span>
          <span className={"role-tag " + me.role}>{me.username} - {me.role}</span>
        </div>
        <div className="search-bar">
          <input placeholder="Search by name, brand, series, group, year, figure number, or UPC..." value={search} onChange={(e) => setSearch(e.target.value)} />
          {search && <button className="clear-btn" onClick={() => setSearch("")} aria-label="Clear search" title="Clear">x</button>}
        </div>
        <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
          <button className="scan-btn" onClick={() => setScanning(true)}>Scan UPC</button>
          <div className="view-toggle">
            <button className={view === "grid" ? "active" : ""} onClick={() => setView("grid")} title="Grid">Grid</button>
            <button className={view === "list" ? "active" : ""} onClick={() => setView("list")} title="List">List</button>
          </div>
          {me.permissions && me.permissions.canManageCatalog && (
            <label className="switch" title="Show hidden figures, series, and groups (admin only)" style={{ marginLeft: 4 }}>
              <input type="checkbox" checked={showHidden} onChange={(e) => setShowHidden(e.target.checked)} />
              <span className="track" /> <span style={{ fontSize: 12 }}>Hidden</span>
            </label>
          )}
          <div className="sort-toggle">
            <button className={sort === "rarity" ? "active" : ""} onClick={() => setSort("rarity")}>Rarity</button>
            <button className={sort === "value" ? "active" : ""} onClick={() => setSort("value")}>Value</button>
            <button className={sort === "name" ? "active" : ""} onClick={() => setSort("name")}>Name</button>
            <button className={sort === "user" ? "active" : ""} onClick={() => setSort("user")}>*</button>
          </div>
        </div>
      </div>

      <div className="main">
        <Sidebar figures={allFigures} state={state} onOpenFigure={openFigure} />
        <div className="content-wrap">
          <div className="content">
            {tab === "more" ? (
              page ? (
                <>
                  <button className="btn secondary" style={{ marginBottom: 12 }} onClick={() => setPage(null)}>&larr; Back</button>
                  {overlay}
                </>
              ) : <MoreMenu setPage={setPage} role={me.role} permissions={me.permissions || {}} />
            ) : (
              <>
                <div className="brand-bar">
                  <button className={"brand-chip " + (brandFilter.size === 0 ? "active" : "")} onClick={() => { setBrandFilter(new Set()); setSeriesFilter("ALL"); setGroupFilter("ALL"); }}>All brands</button>
                  {Object.keys(catalog.brands).map((b) => (
                    <button key={b} className={"brand-chip " + (brandFilter.has(b) ? "active" : "")} onClick={() => {
                      const next = new Set(brandFilter);
                      if (next.has(b)) next.delete(b); else next.add(b);
                      setBrandFilter(next);
                      // If the surviving brands no longer offer the selected series/group, clear them.
                      const survivingSeries = new Set();
                      const survivingGroups = new Set();
                      for (const bn of next) {
                        const br = catalog.brands[bn] || {};
                        for (const s of br.series || []) survivingSeries.add(s);
                        for (const g of br.groups || []) survivingGroups.add(g);
                      }
                      setSeriesFilter(prev => (prev !== "ALL" && survivingSeries.has(prev)) ? prev : "ALL");
                      setGroupFilter(prev => (prev !== "ALL" && survivingGroups.has(prev)) ? prev : "ALL");
                    }}>{b}</button>
                  ))}
                  {brandFilter.size > 1 && (
                    <span style={{ alignSelf: "center", fontSize: 12, color: "var(--ink-muted)" }}>
                      {brandFilter.size} selected
                      <button className="btn secondary" style={{ marginLeft: 8, padding: "4px 10px" }} onClick={() => { setBrandFilter(new Set()); setSeriesFilter("ALL"); setGroupFilter("ALL"); }}>Clear</button>
                    </span>
                  )}
                </div>
                {(() => {
                  // Series + Group dropdown row (only when a brand is selected).
                  if (brandFilter.size === 0) return null;
                  const seriesOpts = new Set();
                  const groupOpts = new Set();
                  for (const bn of brandFilter) {
                    const br = catalog.brands[bn] || {};
                    const hideS = canSeeHidden ? new Set() : new Set(br.hiddenSeries || []);
                    const hideG = canSeeHidden ? new Set() : new Set(br.hiddenGroups || []);
                    for (const s of (br.series || [])) if (!hideS.has(s)) seriesOpts.add(s);
                    for (const g of (br.groups || [])) if (!hideG.has(g)) groupOpts.add(g);
                  }
                  if (seriesOpts.size === 0 && groupOpts.size === 0) return null;
                  return (
                    <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "center", marginBottom: 14 }}>
                      {seriesOpts.size > 0 && (
                        <label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--ink-muted)" }}>
                          Series:
                          <select value={seriesFilter} onChange={(e) => setSeriesFilter(e.target.value)} style={{ minWidth: 200 }}>
                            <option value="ALL">All series</option>
                            {[...seriesOpts].sort().map((s) => <option key={s} value={s}>{s}</option>)}
                          </select>
                        </label>
                      )}
                      {groupOpts.size > 0 && (
                        <label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--ink-muted)" }}>
                          Group:
                          <select value={groupFilter} onChange={(e) => setGroupFilter(e.target.value)} style={{ minWidth: 180 }}>
                            <option value="ALL">All groups</option>
                            {[...groupOpts].sort().map((g) => <option key={g} value={g}>{g}</option>)}
                          </select>
                        </label>
                      )}
                    </div>
                  );
                })()}
                {alpha && (
                  <div style={{ marginBottom: 8, fontSize: 13, color: "var(--ink-muted)" }}>
                    Filtering by <strong>{alpha}</strong>
                    <button className="btn secondary" style={{ marginLeft: 8, padding: "4px 10px" }} onClick={() => setAlpha(null)}>Clear</button>
                  </div>
                )}
                {view === "grid"
                  ? <Grid figures={gridList} state={state} onOpen={openFigure} />
                  : <ListView figures={gridList} state={state} onOpen={openFigure} />}
              </>
            )}
          </div>
          {tab !== "more" && <AlphaBar figures={visible} active={alpha} onPick={setAlpha} />}
        </div>
      </div>

      <div className="tab-bar">
        {[
          ["all",        "All figures", "All"],
          ["wanted",     "Wanted",      "*"],
          ["collection", "Collection",  "[]"],
          ["remaining",  "Remaining",   "..."],
          ["more",       "More",        "="],
        ].map(([k, label, icon]) => (
          <button key={k} className={tab === k ? "active" : ""} onClick={() => { setTab(k); setPage(null); }}>
            <span className="icon">{icon}</span>{label}
          </button>
        ))}
      </div>

      {openFigureObj && <FigureDetail figure={openFigureObj} state={state} update={update} role={me.role} brands={catalog.brands} onClose={() => setOpenId(null)} onCatalogChange={refreshCatalog} />}
      {scanning && <ScannerModal onClose={() => setScanning(false)} onScan={onScanned} />}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
