// ============ APP ROOT — WishTrak with Supabase backend ============

const { useState: useStateA, useEffect: useEffectA, useMemo: useMemoA, useRef: useRefA } = React;

const STORAGE_KEY   = "wishtrak.v1";
const TROPHY_KEY    = "wishtrak.trophies.v1";

// ─────────────────────────────────────────────────────────────
// SUPABASE CLIENT
// ─────────────────────────────────────────────────────────────
// Reads credentials from config.js (window.WISHTRAK_CONFIG).
// Falls back to localStorage-only mode when not configured.

const _cfg = window.WISHTRAK_CONFIG || {};
const _sbClient = (_cfg.supabaseUrl && _cfg.supabaseAnonKey)
  ? supabase.createClient(_cfg.supabaseUrl, _cfg.supabaseAnonKey)
  : null;

// Map Supabase row → app item
function _rowToItem(row) {
  return {
    id:           row.id,
    category:     row.category,
    title:        row.title,
    desc:         row.description || "",
    notes:        row.notes       || "",
    cover:        row.cover       || "",
    target:       Number(row.target)       || 0,
    saved:        Number(row.saved)        || 0,
    targetHours:  Number(row.target_hours) || 0,
    hours:        Number(row.hours)        || 0,
    pct:          Number(row.pct)          || 0,
    checklist:    row.checklist            || [],
    completed:    !!row.completed,
    completedAt:  row.completed_at ? new Date(row.completed_at).getTime() : null,
    createdAt:    row.created_at   ? new Date(row.created_at).getTime()   : Date.now(),
    sourceUrl:    row.source_url   || "",
    donors: (row.donors || []).map(d => ({
      txnId:   d.id,
      name:    d.name    || "Anonymous",
      amount:  Number(d.amount),
      message: d.message || "",
      ts:      d.ts      ? new Date(d.ts).getTime() : Date.now(),
    })),
  };
}

// Map app item → Supabase row (no donors col — separate table)
function _itemToRow(item) {
  return {
    id:           item.id,
    category:     item.category,
    title:        item.title,
    description:  item.desc        || "",
    notes:        item.notes       || "",
    cover:        item.cover       || "",
    target:       Number(item.target)       || 0,
    saved:        Number(item.saved)        || 0,
    target_hours: Number(item.targetHours)  || 0,
    hours:        Number(item.hours)        || 0,
    pct:          Number(item.pct)          || 0,
    checklist:    item.checklist            || [],
    completed:    !!item.completed,
    // Schema stores timestamps as BIGINT (ms since epoch) — send raw numbers, not ISO strings
    completed_at: item.completedAt || null,
    created_at:   item.createdAt   || Date.now(),
    source_url:   item.sourceUrl   || "",
  };
}

const WishDB = {
  get ready() { return !!_sbClient; },

  // ── READ ──────────────────────────────────────────────────
  async loadWishes() {
    const { data, error } = await _sbClient
      .from("wishes")
      .select("*, donors(*)")
      .order("created_at", { ascending: false });
    if (error) throw error;
    return (data || []).map(_rowToItem);
  },

  async loadTrophies() {
    const { data, error } = await _sbClient.from("trophies").select("*");
    if (error) throw error;
    const out = {};
    (data || []).forEach(r => { out[r.id] = Number(r.unlocked_at); });
    return out;
  },

  // ── WRITE ─────────────────────────────────────────────────
  async upsertWish(item) {
    const { error } = await _sbClient
      .from("wishes")
      .upsert(_itemToRow(item), { onConflict: "id" });
    if (error) console.error("upsertWish:", error);
  },

  async deleteWish(id) {
    const { error } = await _sbClient.from("wishes").delete().eq("id", id);
    if (error) console.error("deleteWish:", error);
  },

  async insertDonor(wishId, txn) {
    const { error } = await _sbClient.from("donors").insert({
      id:      txn.txnId,
      wish_id: wishId,
      name:    txn.name    || "Anonymous",
      amount:  txn.amount,
      message: txn.message || "",
      ts:      txn.ts,     // BIGINT (ms since epoch) — send raw number, not ISO string
    });
    if (error) console.error("insertDonor:", error);
  },

  async upsertTrophy(id, ts) {
    const { error } = await _sbClient
      .from("trophies")
      .upsert({ id, unlocked_at: ts }, { onConflict: "id" });
    if (error) console.error("upsertTrophy:", error);
  },

  // Seed with demo data on first run (only called when table is empty)
  async seedWishes(seedItems) {
    for (const item of seedItems) {
      await _sbClient.from("wishes").upsert(_itemToRow(item), { onConflict: "id" });
    }
  },
};

// ─────────────────────────────────────────────────────────────
// SEED DATA
// ─────────────────────────────────────────────────────────────
const SEED = [
  { id:"w_seed1", category:"product",    title:"Vintage Synthesizer",    desc:"A used Juno-60 to anchor the studio.",                                      target:1800, saved:740,  completed:false, createdAt:Date.now()-86400000*40,  pct:0, hours:0 },
  { id:"w_seed2", category:"experience", title:"Trip to Tokyo",           desc:"Two weeks. Cherry blossoms, ramen, record stores.",                         target:4200, saved:1900, completed:false, createdAt:Date.now()-86400000*60,  pct:0, hours:0 },
  { id:"w_seed3", category:"goal",       title:"Run a Half Marathon",     desc:"21.1km. By autumn.",
    checklist:[
      {text:"Run 5k without stopping",    done:true},
      {text:"Run 10k under 60min",        done:true},
      {text:"Long run: 15k",              done:false},
      {text:"Long run: 18k",              done:false},
      {text:"Race day",                   done:false},
    ], completed:false, createdAt:Date.now()-86400000*20,  saved:0, hours:0, pct:0 },
  { id:"w_seed4", category:"media",      title:"Finish Persona 5 Royal",  desc:"100+ hours. Worth it.",                                                     pct:60,  completed:false, createdAt:Date.now()-86400000*15,  saved:0, hours:0 },
  { id:"w_seed5", category:"skill",      title:"Learn Piano",             desc:"Sight-read at intermediate level.",            targetHours:200, hours:42,    completed:false, createdAt:Date.now()-86400000*90,  saved:0, pct:0 },
  { id:"w_seed6", category:"product",    title:"Mechanical Keyboard",     desc:"Built from a kit.",                                                         target:300, saved:300, completed:true, completedAt:Date.now()-86400000*5, createdAt:Date.now()-86400000*30, pct:0, hours:0 },
  { id:"w_seed7", category:"media",      title:"Read Dune (Books 1–3)",   desc:"Slow & careful.",                                                           pct:33,  completed:false, createdAt:Date.now()-86400000*70,  saved:0, hours:0 },
  { id:"w_seed8", category:"experience", title:"See the Northern Lights", desc:"Iceland or Norway.",                                                        target:3500, saved:200, completed:false, createdAt:Date.now()-86400000*8,   pct:0, hours:0 },
];

// ─────────────────────────────────────────────────────────────
// TROPHIES
// ─────────────────────────────────────────────────────────────
const TROPHY_DEFS = [
  { id:"first_wish",    icon:"★", name:"First Wish",      desc:"Add your first wish",                hint:"Add any wish",            test:(items)=>items.length>=1 },
  { id:"five_wishes",   icon:"V", name:"Wishful Five",    desc:"Track 5 wishes at once",             hint:"Track 5 wishes",          test:(items)=>items.length>=5 },
  { id:"first_grant",   icon:"✦", name:"First Granted",   desc:"Complete your first wish",           hint:"Grant a wish",            test:(items)=>items.some(i=>i.completed) },
  { id:"three_grants",  icon:"3", name:"Wish Trinity",    desc:"Grant three wishes",                 hint:"Grant 3 wishes",          test:(items)=>items.filter(i=>i.completed).length>=3 },
  { id:"all_cats",      icon:"◇", name:"Renaissance",     desc:"Have a wish in every category",      hint:"5 categories used",       test:(items)=>CAT_LIST.every(c=>items.some(i=>i.category===c.id)) },
  { id:"saver_500",     icon:"$", name:"Penny Saver",     desc:"Save $500 toward a wish",            hint:"Save $500 cumulative",    test:(items)=>items.reduce((s,i)=>s+Number(i.saved||0),0)>=500 },
  { id:"saver_5k",      icon:"§", name:"Treasury",        desc:"Save $5,000 cumulative",             hint:"Save $5,000 cumulative",  test:(items)=>items.reduce((s,i)=>s+Number(i.saved||0),0)>=5000 },
  { id:"hours_50",      icon:"♪", name:"Apprentice",      desc:"Log 50 hours of practice",           hint:"50 hours logged",         test:(items)=>items.reduce((s,i)=>s+Number(i.hours||0),0)>=50 },
  { id:"halfway",       icon:"½", name:"Halfway There",   desc:"Hit 50% on any wish",                hint:"50% on a wish",           test:(items)=>items.some(i=>pctOf(i)>=50&&!i.completed) },
  { id:"first_backer",  icon:"♥", name:"First Backer",    desc:"Receive your first donation",        hint:"Get one donation",        test:(items)=>items.some(i=>(i.donors||[]).length>=1) },
  { id:"crowd_funded",  icon:"※", name:"Crowd Funded",    desc:"5+ contributions to a single wish",  hint:"5 backers on one wish",   test:(items)=>items.some(i=>(i.donors||[]).length>=5) },
  { id:"benefactor",    icon:"₿", name:"Benefactor",      desc:"$500 in cumulative donations",       hint:"$500 from backers",       test:(items)=>items.reduce((s,i)=>s+(i.donors||[]).reduce((a,d)=>a+d.amount,0),0)>=500 },
];

function evalTrophies(items, prevUnlocked) {
  const unlocked = { ...prevUnlocked };
  const newly = [];
  for (const def of TROPHY_DEFS) {
    if (!unlocked[def.id] && def.test(items)) {
      unlocked[def.id] = Date.now();
      newly.push(def);
    }
  }
  return { unlocked, newly };
}

// ─────────────────────────────────────────────────────────────
// APP ROOT
// ─────────────────────────────────────────────────────────────
// PIN is verified server-side (/api/verify-pin). Only a random token
// returned by the server is stored client-side — never the PIN itself.
const OWNER_SESSION_KEY = "wishtrak.ownersession.v2";

function App() {
  // ── Tweaks ───────────────────────────────────────────────
  const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
    "scanlines":  true,
    "sound":      true,
    "background": "stars"
  }/*EDITMODE-END*/;
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // ── Owner auth ───────────────────────────────────────────
  // PIN is verified server-side. The server returns a random 48-char hex
  // token on success; we store it here. Session ends when browser closes.
  const [ownerMode, setOwnerMode] = useStateA(() => {
    const t = sessionStorage.getItem(OWNER_SESSION_KEY);
    return !!(t && t.length >= 48); // expect token from /api/verify-pin
  });
  const [signInOpen, setSignInOpen] = useStateA(false);
  const [donateFor,  setDonateFor]  = useStateA(null);

  // ── Data ─────────────────────────────────────────────────
  const [dbLoading, setDbLoading] = useStateA(true);   // true while fetching from Supabase
  const [items,    setItems]    = useStateA([]);
  const [trophies, setTrophies] = useStateA({});

  // localStorage helpers (cache + offline fallback)
  const lsGetItems  = () => { try { const r = localStorage.getItem(STORAGE_KEY);  return r ? JSON.parse(r) : null; } catch(e){ return null; } };
  const lsGetTrophy = () => { try { const r = localStorage.getItem(TROPHY_KEY);   return r ? JSON.parse(r) : {};   } catch(e){ return {};   } };
  const lsSaveItems  = (arr) => { try { localStorage.setItem(STORAGE_KEY,  JSON.stringify(arr));  } catch(e){} };
  const lsSaveTrophy = (obj) => { try { localStorage.setItem(TROPHY_KEY,   JSON.stringify(obj));  } catch(e){} };

  // Load data on mount
  useEffectA(() => {
    let cancelled = false;
    async function load() {
      if (WishDB.ready) {
        try {
          const [wishData, trophyData] = await Promise.all([
            WishDB.loadWishes(),
            WishDB.loadTrophies(),
          ]);
          if (cancelled) return;
          if (wishData.length === 0) {
            // First run — seed demo data into Supabase
            await WishDB.seedWishes(SEED);
            if (!cancelled) setItems(SEED);
          } else {
            if (!cancelled) setItems(wishData);
          }
          if (!cancelled) setTrophies(trophyData);
        } catch(err) {
          console.error("Supabase load error:", err);
          // Fall back to localStorage
          if (!cancelled) {
            const fallback = lsGetItems();
            setItems(fallback || SEED);
            setTrophies(lsGetTrophy());
          }
        }
      } else {
        // No Supabase configured — use localStorage only
        if (!cancelled) {
          const fallback = lsGetItems();
          setItems(fallback || SEED);
          setTrophies(lsGetTrophy());
        }
      }
      if (!cancelled) setDbLoading(false);
    }
    load();
    return () => { cancelled = true; };
  }, []);

  // Sync items to localStorage whenever they change
  useEffectA(() => {
    if (!dbLoading) lsSaveItems(items);
  }, [items, dbLoading]);

  // Sync trophies to localStorage whenever they change
  useEffectA(() => {
    if (!dbLoading) lsSaveTrophy(trophies);
  }, [trophies, dbLoading]);

  // ── Trophy evaluation ─────────────────────────────────────
  useEffectA(() => {
    if (dbLoading) return;
    const { unlocked, newly } = evalTrophies(items, trophies);
    if (!newly.length) return;
    setTrophies(unlocked);
    // Persist new trophies to Supabase
    if (WishDB.ready) {
      newly.forEach(def => WishDB.upsertTrophy(def.id, unlocked[def.id]).catch(console.error));
    }
    newly.forEach((def, i) => {
      setTimeout(() => {
        if (tweaks.sound) SFX.trophy();
        setToast({ ...def, t: Date.now() + i });
      }, i * 1200);
    });
  }, [items, dbLoading]);

  // ── Routing ───────────────────────────────────────────────
  const [route,  setRoute]  = useStateA({ name: "hub" });
  const [loading, setLoading] = useStateA(null);
  const [filter,  setFilter]  = useStateA("all");
  const [search,  setSearch]  = useStateA("");
  const [sort,    setSort]    = useStateA("newest");
  const [toast,   setToast]   = useStateA(null);
  const fileInputRef = useRefA(null);

  const navigate = (next, label = "Loading") => {
    SFX_safePlay("select", tweaks.sound);
    setLoading(label);
    setTimeout(() => {
      setRoute(next);
      setTimeout(() => setLoading(null), 450);
    }, 50);
  };

  // ── CRUD helpers ──────────────────────────────────────────

  const addItem = (_, item) => {
    setItems(arr => [item, ...arr]);
    if (WishDB.ready) WishDB.upsertWish(item).catch(console.error);
    navigate({ name: "detail", id: item.id }, "Saving");
  };

  const editItem = (id, patch) => {
    setItems(arr => arr.map(it => {
      if (it.id !== id) return it;
      const next = { ...it, ...patch };
      if (WishDB.ready) WishDB.upsertWish(next).catch(console.error);
      return next;
    }));
    navigate({ name: "detail", id }, "Saving");
  };

  const deleteItem = (id) => {
    setItems(arr => arr.filter(it => it.id !== id));
    if (WishDB.ready) WishDB.deleteWish(id).catch(console.error);
    navigate({ name: "grid" }, "Ejecting");
  };

  // Progress update (owner controls + auto-complete)
  const updateItem = (id, patch) => {
    setItems(arr => arr.map(it => {
      if (it.id !== id) return it;
      let next = { ...it, ...patch };
      if (!next.completed && pctOf(next) >= 100) {
        next.completed = true;
        next.completedAt = Date.now();
        if (tweaks.sound) SFX.trophy();
      }
      if (WishDB.ready) WishDB.upsertWish(next).catch(console.error);
      return next;
    }));
  };

  // Donation: record in Supabase donors table + update saved amount
  const donateToItem = (id, txn) => {
    if (WishDB.ready) WishDB.insertDonor(id, txn).catch(console.error);
    setItems(arr => arr.map(it => {
      if (it.id !== id) return it;
      const donors = [...(it.donors || []), txn];
      const next   = { ...it, donors, saved: Number(it.saved || 0) + Number(txn.amount) };
      if (!next.completed && pctOf(next) >= 100) {
        next.completed  = true;
        next.completedAt = Date.now();
      }
      // Keep the wishes row saved amount in sync too
      if (WishDB.ready) WishDB.upsertWish({ ...next, donors: undefined }).catch(console.error);
      return next;
    }));
  };

  // Import multiple wishes from web scrape results
  const importItems = async (newItems) => {
    setItems(arr => [...newItems, ...arr]);
    if (WishDB.ready) {
      for (const item of newItems) {
        await WishDB.upsertWish(item).catch(console.error);
      }
    }
    navigate({ name: "grid" }, "Importing");
  };

  // ── Navigation helpers ────────────────────────────────────
  const onHome       = () => navigate({ name: "hub"    }, "Returning to Menu");
  const onGrid       = () => navigate({ name: "grid"   }, "Loading Library");
  const onStats      = () => navigate({ name: "stats"  }, "Loading Stats");
  const onAdd        = () => { if (!ownerMode) { setSignInOpen(true); return; } navigate({ name: "add" }, "Insert Disc"); };
  const onEdit       = (id) => { if (!ownerMode) { setSignInOpen(true); return; } navigate({ name: "edit", id }, "Loading Editor"); };
  const onImportWeb  = () => { if (!ownerMode) { setSignInOpen(true); return; } navigate({ name: "import" }, "Loading Scanner"); };

  const requireOwner = (fn) => (...args) => { if (!ownerMode) { setSignInOpen(true); return; } fn(...args); };
  const onSignInSuccess = (token) => {
    sessionStorage.setItem(OWNER_SESSION_KEY, token); // token from /api/verify-pin
    setOwnerMode(true);
    setSignInOpen(false);
  };
  const toggleOwner = () => {
    if (ownerMode) {
      sessionStorage.removeItem(OWNER_SESSION_KEY);
      setOwnerMode(false);
    } else {
      setSignInOpen(true);
    }
  };

  const hubNav = (id) => {
    if (id === "grid")                            onGrid();
    else if (id === "add")                        onAdd();
    else if (id === "import")                     onImportWeb();
    else if (id === "stats" || id === "trophy")   onStats();
  };

  const onOpenDonate  = (item) => setDonateFor(item);
  const onDonateSuccess = (txn) => { if (donateFor) donateToItem(donateFor.id, txn); };

  // ── Export / Import / Reset ───────────────────────────────
  const onExport = () => {
    const blob = new Blob([JSON.stringify({ items, trophies, version: 1, exportedAt: Date.now() }, null, 2)], { type: "application/json" });
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement("a");
    a.href     = url;
    a.download = `wishtrak-${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    URL.revokeObjectURL(url);
  };

  const onImport = () => fileInputRef.current?.click();

  const handleFile = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = async (ev) => {
      try {
        const data = JSON.parse(ev.target.result);
        if (!Array.isArray(data.items)) { alert("Invalid file format."); return; }
        setItems(data.items);
        if (data.trophies) setTrophies(data.trophies);
        // Sync imported data to Supabase
        if (WishDB.ready) {
          for (const item of data.items) {
            await WishDB.upsertWish(item).catch(console.error);
          }
          if (data.trophies) {
            for (const [id, ts] of Object.entries(data.trophies)) {
              await WishDB.upsertTrophy(id, ts).catch(console.error);
            }
          }
        }
        if (tweaks.sound) SFX.ok();
        alert(`Imported ${data.items.length} wishes.`);
      } catch (err) {
        alert("Could not parse file: " + err.message);
      }
    };
    reader.readAsText(file);
    e.target.value = "";
  };

  const onReset = async () => {
    if (!confirm("Reset all wishes and trophies to defaults? This cannot be undone.")) return;
    // Clear Supabase if configured
    if (WishDB.ready) {
      // Delete all then re-seed
      for (const item of items) {
        await WishDB.deleteWish(item.id).catch(console.error);
      }
      await WishDB.seedWishes(SEED).catch(console.error);
      // Clear trophies
      const { error } = await _sbClient.from("trophies").delete().neq("id", "__never__");
      if (error) console.error("reset trophies:", error);
    }
    localStorage.removeItem(STORAGE_KEY);
    localStorage.removeItem(TROPHY_KEY);
    setItems(SEED);
    setTrophies({});
    navigate({ name: "hub" }, "Resetting");
  };

  // ── Keyboard shortcuts ────────────────────────────────────
  useEffectA(() => {
    const onKey = (e) => {
      if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
      if (e.key === "Escape") onHome();
      if (e.key === "+" && route.name !== "add" && route.name !== "edit") onAdd();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [route, ownerMode]);

  // Power-on chime
  useEffectA(() => {
    if (tweaks.sound) setTimeout(() => SFX.power(), 200);
  }, []);

  // ── Render ────────────────────────────────────────────────
  const trophyList = TROPHY_DEFS.map(d => ({ ...d, unlocked: !!trophies[d.id] }));

  let view = null;

  if (dbLoading) {
    // Show loading disc while Supabase data is fetching
    view = null; // handled by the persistent loading overlay below
  } else if (route.name === "hub") {
    view = <HubScreen items={items} onNav={hubNav} tweaks={tweaks} setTweak={setTweak} ownerMode={ownerMode} onToggleOwner={toggleOwner} />;
  } else if (route.name === "grid") {
    view = <GridScreen
      items={items}
      onOpen={(id) => navigate({ name: "detail", id }, "Loading Wish")}
      onAdd={onAdd} onHome={onHome} onStats={onStats}
      filter={filter} setFilter={setFilter}
      search={search} setSearch={setSearch}
      sort={sort}     setSort={setSort}
      ownerMode={ownerMode}
    />;
  } else if (route.name === "detail") {
    const item = items.find(i => i.id === route.id);
    view = <DetailScreen
      item={item}
      onBack={onGrid} onAdd={onAdd} onStats={onStats} onHome={onHome}
      onUpdate={ownerMode ? updateItem : () => setSignInOpen(true)}
      onDelete={requireOwner((id) => deleteItem(id))}
      onEdit={onEdit}
      ownerMode={ownerMode}
      onOpenDonate={onOpenDonate}
    />;
  } else if (route.name === "add") {
    if (!ownerMode) { setTimeout(() => { setRoute({ name: "hub" }); setSignInOpen(true); }, 0); view = null; }
    else view = <AddScreen onSave={addItem} onCancel={onGrid} onHome={onHome} onStats={onStats} />;
  } else if (route.name === "edit") {
    if (!ownerMode) { setTimeout(() => { setRoute({ name: "hub" }); setSignInOpen(true); }, 0); view = null; }
    else {
      const existing = items.find(i => i.id === route.id);
      view = <AddScreen
        existing={existing}
        onSave={editItem}
        onCancel={() => navigate({ name: "detail", id: route.id }, "Cancelling")}
        onHome={onHome} onStats={onStats}
      />;
    }
  } else if (route.name === "stats") {
    view = <StatsScreen
      items={items} trophies={trophyList}
      onHome={onHome} onAdd={onAdd}
      onExport={ownerMode ? onExport : () => setSignInOpen(true)}
      onImport={ownerMode ? onImport : () => setSignInOpen(true)}
      onReset={ownerMode  ? onReset  : () => setSignInOpen(true)}
      ownerMode={ownerMode}
    />;
  } else if (route.name === "import") {
    if (!ownerMode) { setTimeout(() => { setRoute({ name: "hub" }); setSignInOpen(true); }, 0); view = null; }
    else view = <ImportScreen
      onImport={importItems}
      onCancel={onHome}
      onHome={onHome}
      onStats={onStats}
    />;
  }

  return (
    <SoundProvider on={tweaks.sound}>
      <div className="dvd-frame" data-screen-label={route.name}>
        <Background kind={tweaks.background} />
        {view}
        <Scanlines on={tweaks.scanlines} />

        {/* Route-change loading interstitial */}
        {loading && <LoadingInterstitial label={loading} />}

        {/* Initial DB loading overlay */}
        {dbLoading && <LoadingInterstitial label="Loading" />}

        {toast && <AchievementToast trophy={toast} onDone={() => setToast(null)} />}

        {signInOpen && (
          <SignInModal
            onClose={() => setSignInOpen(false)}
            onSuccess={onSignInSuccess}
          />
        )}

        {donateFor && (
          <DonateModal
            item={items.find(i => i.id === donateFor.id) || donateFor}
            onClose={() => setDonateFor(null)}
            onSuccess={onDonateSuccess}
          />
        )}

        <input ref={fileInputRef} type="file" accept="application/json"
          style={{ display: "none" }} onChange={handleFile} />

        <TweaksPanel title="Tweaks">
          <TweakSection title="Display">
            <TweakToggle label="Scanlines" value={tweaks.scanlines} onChange={(v) => setTweak("scanlines", v)} />
            <TweakRadio  label="Background" value={tweaks.background}
              options={[{label:"Stars",value:"stars"},{label:"DVD",value:"dvd"},{label:"None",value:"none"}]}
              onChange={(v) => setTweak("background", v)} />
          </TweakSection>
          <TweakSection title="Audio">
            <TweakToggle label="Sound effects" value={tweaks.sound} onChange={(v) => setTweak("sound", v)} />
          </TweakSection>
          <TweakSection title="Owner">
            <TweakToggle label="Owner mode" value={ownerMode}
              onChange={(v) => { if (v) setSignInOpen(true); else toggleOwner(); }} />
          </TweakSection>
          {ownerMode && (
            <TweakSection title="Data">
              <TweakButton onClick={onExport}>Export to JSON</TweakButton>
              <TweakButton onClick={onImport}>Import from JSON</TweakButton>
              <TweakButton onClick={onReset}>Reset to demo data</TweakButton>
            </TweakSection>
          )}
        </TweaksPanel>
      </div>
    </SoundProvider>
  );
}

function SFX_safePlay(name, on) {
  if (on && SFX[name]) SFX[name]();
}

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