/* global React, Icon */
const { useState, useEffect, useRef } = React;

/* ============== Gacha tiers — 10-step osu!-style difficulty spread ==============
   Color ramp evokes rhythm-game difficulty without copying any specific UI:
   cyan → green → yellow → orange → pink → hot-pink → purple → deep-purple → magenta → gold/black. */
const GACHA_TIERS = [
  { stars: 1,  name: "Beginner",     weight: 32,   color: "#66ccff", glow: "#3aa8e8", pps: 30,    spReward: 0,  flair: "Warm-up" },
  { stars: 2,  name: "Easy",         weight: 22,   color: "#7be4a3", glow: "#46c878", pps: 100,   spReward: 0,  flair: "Smooth" },
  { stars: 3,  name: "Normal",       weight: 16,   color: "#ffe066", glow: "#e4bd2c", pps: 280,   spReward: 1,  flair: "Locked in" },
  { stars: 4,  name: "Hard",         weight: 12,   color: "#ffa86b", glow: "#e87e2c", pps: 700,   spReward: 1,  flair: "Sweaty" },
  { stars: 5,  name: "Light Insane", weight: 8,    color: "#ff7aa8", glow: "#e64a83", pps: 1600,  spReward: 2,  flair: "Heating up" },
  { stars: 6,  name: "Insane",       weight: 5,    color: "#ff4f8a", glow: "#d62674", pps: 3500,  spReward: 3,  flair: "Inhuman" },
  { stars: 7,  name: "Extra",        weight: 3,    color: "#c074ff", glow: "#8a40d4", pps: 7500,  spReward: 4,  flair: "Top 5%" },
  { stars: 8,  name: "Expert",       weight: 1.5,  color: "#8e3fd4", glow: "#5a1da0", pps: 15000, spReward: 6,  flair: "Free buff" },
  { stars: 9,  name: "Expert+",      weight: 0.5,  color: "#ff2ec0", glow: "#c40098", pps: 35000, spReward: 8,  flair: "Top 0.5%" },
  { stars: 10, name: "Ultra",        weight: 0.2,  color: "#ffd24a", glow: "#ff8a00", pps: 90000, spReward: 12, flair: "JACKPOT" },
];

/* ============== Beatmap pool ==============
   Loaded at runtime from ./beatmaps.json (generated by scripts/fetch-beatmaps.mjs).
   The hardcoded array below is a tiny offline fallback used until the fetch resolves
   or if it fails. Real pool replaces it on load. */
let BEATMAPS = [
  // 1 — Beginner
  { artist: "Camellia",          title: "Nacreous Snowmelt",          mapper: "Mismagius",     bpm: 170, lenSec: 200, tier: 1 },
  { artist: "ginkiha",           title: "EOS",                        mapper: "Akitoshi",      bpm: 174, lenSec: 220, tier: 1 },
  { artist: "MARETU",            title: "Knife",                      mapper: "ProfiZocker7",  bpm: 156, lenSec: 195, tier: 1 },
  { artist: "ClariS",            title: "Connect",                    mapper: "Sotarks",       bpm: 174, lenSec: 240, tier: 1 },

  // 2 — Easy
  { artist: "yanaginagi",        title: "Vidro Moyou",                mapper: "Yales",         bpm: 162, lenSec: 250, tier: 2 },
  { artist: "Hatsune Miku",      title: "Senbonzakura",               mapper: "Skystar",       bpm: 154, lenSec: 240, tier: 2 },
  { artist: "LiSA",              title: "Crossing Field",             mapper: "Monstrata",     bpm: 160, lenSec: 244, tier: 2 },
  { artist: "Eve",               title: "Kaikai Kitan",               mapper: "Mafumafu",      bpm: 175, lenSec: 218, tier: 2 },

  // 3 — Normal
  { artist: "Yorushika",         title: "Hitchcock",                  mapper: "Pho",           bpm: 132, lenSec: 252, tier: 3 },
  { artist: "TUYU",              title: "Iyaiya yo",                  mapper: "Lasse",         bpm: 138, lenSec: 230, tier: 3 },
  { artist: "Reol",              title: "No title",                   mapper: "Skystar",       bpm: 196, lenSec: 220, tier: 3 },
  { artist: "ZUTOMAYO",          title: "Kan Saete Kuyashiiwa",       mapper: "handsome",      bpm: 138, lenSec: 250, tier: 3 },

  // 4 — Hard
  { artist: "Yousei Teikoku",    title: "Wahrheit",                   mapper: "Garven",        bpm: 200, lenSec: 232, tier: 4 },
  { artist: "Eve",               title: "Bouken",                     mapper: "Frey",          bpm: 155, lenSec: 226, tier: 4 },
  { artist: "BABYMETAL",         title: "Gimme Chocolate!!",          mapper: "Vell",          bpm: 174, lenSec: 252, tier: 4 },
  { artist: "Halozy",            title: "Kanshou no Matenrou",        mapper: "Doomsday",      bpm: 168, lenSec: 210, tier: 4 },

  // 5 — Light Insane
  { artist: "Lapix",             title: "Routing!!",                  mapper: "Lasse",         bpm: 200, lenSec: 218, tier: 5 },
  { artist: "Camellia",          title: "Looking For Edge of Ground", mapper: "Mir",           bpm: 174, lenSec: 240, tier: 5 },
  { artist: "Cranky",            title: "Necro Fantasia",             mapper: "Karen",         bpm: 162, lenSec: 260, tier: 5 },
  { artist: "kradness",          title: "Remind",                     mapper: "Sotarks",       bpm: 200, lenSec: 220, tier: 5 },

  // 6 — Insane
  { artist: "xi",                title: "Parousia",                   mapper: "Asphyxia",      bpm: 180, lenSec: 248, tier: 6 },
  { artist: "Yuyoyuppe",         title: "AiAe",                       mapper: "Fort",          bpm: 200, lenSec: 244, tier: 6 },
  { artist: "t+pazolite",        title: "Sweet Sweet Sweet Magic",    mapper: "Sotarks",       bpm: 200, lenSec: 218, tier: 6 },
  { artist: "Helblinde",         title: "When the Seasons Change",    mapper: "Lasse",         bpm: 200, lenSec: 268, tier: 6 },

  // 7 — Extra
  { artist: "DJ SHARPNEL",       title: "StrangeProgram",             mapper: "Mismagius",     bpm: 200, lenSec: 220, tier: 7 },
  { artist: "Halozy",            title: "Genryuu Kaiko",              mapper: "handsome",      bpm: 245, lenSec: 256, tier: 7 },
  { artist: "Demetori",          title: "Yuuga ni Sake, Sumizome",    mapper: "Doomsday",      bpm: 165, lenSec: 320, tier: 7 },
  { artist: "HyuN",              title: "Disorder",                   mapper: "Karen",         bpm: 175, lenSec: 230, tier: 7 },

  // 8 — Expert
  { artist: "xi",                title: "FREEDOM DiVE",               mapper: "Nakagawa-Kanon",bpm: 222, lenSec: 254, tier: 8 },
  { artist: "Camellia",          title: "Exit This Earth's Atmosphere", mapper: "Mir",         bpm: 240, lenSec: 232, tier: 8 },
  { artist: "Helblinde",         title: "What Is Love?",              mapper: "Lasse",         bpm: 200, lenSec: 248, tier: 8 },
  { artist: "LeaF",              title: "Aleph-0",                    mapper: "Akitoshi",      bpm: 180, lenSec: 232, tier: 8 },

  // 9 — Expert+
  { artist: "Big Black",         title: "Blue Zenith",                mapper: "Asphyxia",      bpm: 200, lenSec: 280, tier: 9 },
  { artist: "Imperial Circus",   title: "Yomi yori Kikoyu",           mapper: "Doomsday",      bpm: 200, lenSec: 540, tier: 9 },
  { artist: "Camellia",          title: "Ascension to Heaven",        mapper: "Mir",           bpm: 280, lenSec: 260, tier: 9 },
  { artist: "USAO",              title: "Night of Knights",           mapper: "Frey",          bpm: 220, lenSec: 268, tier: 9 },

  // 10 — Ultra
  { artist: "USAO",              title: "Cthugha",                    mapper: "Frey",          bpm: 200, lenSec: 248, tier: 10 },
  { artist: "Frums",             title: "We're Gonna Need a Bigger Map", mapper: "Sotarks",    bpm: 230, lenSec: 240, tier: 10 },
  { artist: "Camellia",          title: "GHOST",                      mapper: "Mir",           bpm: 256, lenSec: 222, tier: 10 },
  { artist: "Quree",             title: "ouroboros -twin stroke-",    mapper: "Karen",         bpm: 222, lenSec: 290, tier: 10 },
];

const MODS = [
  { tag: "NM", label: "NoMod",      desc: "No modifiers" },
  { tag: "HD", label: "Hidden",     desc: "Notes fade out" },
  { tag: "HR", label: "HardRock",   desc: "Flipped, faster" },
  { tag: "DT", label: "DoubleTime", desc: "150% speed" },
  { tag: "FL", label: "Flashlight", desc: "Limited vision" },
  { tag: "EZ", label: "Easy",       desc: "Larger circles" },
  { tag: "NF", label: "NoFail",     desc: "Can't fail" },
];

/* ============== Date helpers ============== */
function todayKey() {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}

function nextResetMs() {
  const d = new Date();
  const next = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1, 0, 0, 0, 0);
  return next.getTime() - d.getTime();
}

function fmtCountdown(ms) {
  if (ms <= 0) return "now";
  const s = Math.floor(ms / 1000);
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const sec = s % 60;
  if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
  if (m > 0) return `${m}m ${String(sec).padStart(2, "0")}s`;
  return `${sec}s`;
}

/* ============== Rolling ============== */
function rollTier(streakBonus = 0) {
  const lift = Math.min(streakBonus * 0.08, 0.6); // cap +60% to rare
  // tiers 5+ count as "rare"; lower tiers get slight haircut
  const tiers = GACHA_TIERS.map((t, i) => ({
    ...t,
    weight: i >= 4 ? t.weight * (1 + lift) : t.weight * (1 - lift * 0.25),
  }));
  const total = tiers.reduce((s, t) => s + t.weight, 0);
  let r = Math.random() * total;
  for (const t of tiers) {
    r -= t.weight;
    if (r <= 0) return GACHA_TIERS[tiers.indexOf(t)];
  }
  return GACHA_TIERS[0];
}

function rollMap(tier) {
  let pool = BEATMAPS.filter((b) => b.tier === tier.stars);
  if (pool.length === 0) pool = BEATMAPS.filter((b) => Math.abs(b.tier - tier.stars) <= 1);
  if (pool.length === 0) pool = BEATMAPS;
  const pick = pool[Math.floor(Math.random() * pool.length)] || {
    artist: "Unknown", title: "No map loaded", mapper: "—", version: "",
    bpm: 120, lenSec: 180, stars: tier.stars,
  };
  const mod = MODS[Math.floor(Math.random() * MODS.length)];
  let bpm = pick.bpm;
  let lenSec = pick.lenSec;
  if (mod.tag === "DT") { bpm = Math.round(bpm * 1.5); lenSec = Math.round(lenSec / 1.5); }
  if (mod.tag === "HR") { bpm = Math.round(bpm * 1.05); }
  if (mod.tag === "EZ") { lenSec = Math.round(lenSec * 1.1); }
  const stars = (pick.stars != null ? pick.stars : tier.stars).toFixed(2);
  return {
    title: pick.title,
    artist: pick.artist,
    mapper: pick.mapper,
    version: pick.version || "",
    url: pick.url || null,
    legendary: !!pick.legendary,
    playcount: pick.playcount || 0,
    bpm, lenSec, mod, stars, tier,
  };
}

function fmtLen(sec) {
  const m = Math.floor(sec / 60);
  const s = sec % 60;
  return `${m}:${String(s).padStart(2, "0")}`;
}

/* ============== Sidebar card — opens the modal ============== */
function GachaCard({ available, streak, lastTier, onOpen }) {
  const remaining = nextResetMs();
  return (
    <button
      className={`gacha-card ${available ? "ready" : "done"}`}
      onClick={onOpen}
    >
      <div className="gacha-card-head">
        <div className="gacha-card-eyebrow">
          {Icon.music(12)} <span>Daily Map</span>
        </div>
        {streak > 0 && (
          <div className="gacha-card-streak" title="Daily streak">
            <span aria-hidden="true">🔥</span>
            <span className="sr-only">Streak:</span>
            {" "}{streak}
          </div>
        )}
      </div>
      <div className="gacha-card-title">
        {available ? "Queue today's beatmap" : "Played for today"}
      </div>
      <div className="gacha-card-sub">
        {available
          ? "Pull a random map · scale by stars"
          : lastTier
            ? <>Last: <span style={{ color: lastTier.color }}>★ {lastTier.name}</span></>
            : "Come back tomorrow"}
      </div>
      <div className="gacha-card-foot">
        {available ? (
          <span className="gacha-card-cta">
            <span className="gacha-card-dot" /> READY
          </span>
        ) : (
          <span className="gacha-card-timer">
            next: {fmtCountdown(remaining)}
          </span>
        )}
      </div>
    </button>
  );
}

/* ============== The big reveal modal ============== */
function GachaModal({ open, onClose, onClaim, streak, forcedTier, onConsumeForced, pullRequest }) {
  const [phase, setPhase] = useState("idle");
  const [subPhase, setSubPhase] = useState("charge"); // charge | peak | burst
  const [pendingTier, setPendingTier] = useState(GACHA_TIERS[0]);
  const [result, setResult] = useState(null);
  const timers = useRef([]);

  useEffect(() => {
    if (!open) {
      setPhase("idle");
      setSubPhase("charge");
      setResult(null);
      timers.current.forEach(clearTimeout);
      timers.current = [];
    }
  }, [open]);

  useEffect(() => {
    if (!open) return;
    const fn = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", fn);
    return () => document.removeEventListener("keydown", fn);
  }, [open, onClose]);

  if (!open) return null;

  async function startPull() {
    setSubPhase("charge");
    setPhase("spin");

    let finalTier;
    let finalMap;
    if (forcedTier != null) {
      finalTier = GACHA_TIERS.find((t) => t.stars === forcedTier) || GACHA_TIERS[0];
      onConsumeForced && onConsumeForced();
      finalMap = rollMap(finalTier);
    } else if (pullRequest) {
      const r = await pullRequest();
      if (r?.error || !r?.result) {
        setPhase("idle");
        if (typeof window !== "undefined") {
          window.alert(`Gacha failed: ${r?.error || "no result"}`);
        }
        onClose();
        return;
      }
      finalTier = r.result.tier;
      finalMap = { ...r.result.map, tier: r.result.tier };
    } else {
      finalTier = rollTier(streak);
      finalMap = rollMap(finalTier);
    }
    setPendingTier(finalTier);

    timers.current.push(setTimeout(() => setSubPhase("peak"), 3000));
    timers.current.push(setTimeout(() => setSubPhase("burst"), 3900));
    timers.current.push(setTimeout(() => {
      setResult(finalMap);
      setPhase("reveal");
    }, 4300));
  }

  function doClaim() {
    if (!result) return;
    onClaim(result);
  }

  const gachaModalLabel = phase === "reveal" && result
    ? `Daily map result: ${result.tier.stars} star ${result.tier.name} — ${result.artist} - ${result.title}`
    : "Daily Map Roulette";

  return (
    <div className="gacha-veil" onClick={phase === "reveal" ? doClaim : onClose} aria-hidden="true">
      <div
        className={`gacha-modal phase-${phase}`}
        onClick={(e) => e.stopPropagation()}
        role="dialog"
        aria-modal="true"
        aria-label={gachaModalLabel}
        style={phase === "reveal" ? {
          "--tier-color": result.tier.color,
          "--tier-glow": result.tier.glow,
        } : {
          "--tier-color": pendingTier.color,
          "--tier-glow": pendingTier.glow,
        }}
      >
        <button className="gacha-close" onClick={onClose} aria-label="close">×</button>

        {phase === "idle" && (
          <>
            <div className="gacha-eyebrow">{Icon.music(13)} <span>Daily Beatmap</span></div>
            <div className="gacha-title">Map roulette</div>
            <div className="gacha-body">
              WhiteCat queues one random ranked map per day. Star rating decides the reward —
              a <strong>1★ warm-up</strong> hands you pocket change, a{" "}
              <strong style={{ color: "#ffd24a" }}>10★ Ultra</strong> hands you a jackpot.
            </div>

            <div className="gacha-odds">
              {GACHA_TIERS.map((t) => {
                const total = GACHA_TIERS.reduce((s, x) => s + x.weight, 0);
                const pct = (t.weight / total) * 100;
                return (
                  <div key={t.stars} className="gacha-odd-row">
                    <span className="gacha-odd-stars" style={{ color: t.color }}>
                      {t.stars}★
                    </span>
                    <span className="gacha-odd-name">{t.name}</span>
                    <span className="gacha-odd-weight">
                      {pct < 1 ? pct.toFixed(2) : pct.toFixed(1)}%
                    </span>
                  </div>
                );
              })}
            </div>

            {streak > 0 && (
              <div className="gacha-streak-banner">
                🔥 Daily streak: <strong>{streak}</strong> · rare odds +{Math.min(streak * 8, 60)}%
              </div>
            )}

            <button className="gacha-pull-btn" onClick={startPull}>
              {Icon.sparkle(16)} <span>Queue today's map</span>
            </button>
          </>
        )}

        {phase === "spin" && (
          <>
            <div className="gacha-eyebrow">{Icon.music(13)} <span>Drawing from ranked pool</span></div>
            <div
              className={`gacha-orb-stage sub-${subPhase}`}
              style={{
                "--final-color": pendingTier.color,
                "--final-glow": pendingTier.glow,
              }}
            >
              <div className="gacha-orb-veil" />
              <div className="gacha-orb-halo" />
              <div className="gacha-orb-core" />
              <div className="gacha-orb-ring" />
              <div className="gacha-orb-particles">
                {[0, 1, 2, 3, 4, 5, 6, 7].map((i) => (
                  <span key={i} className={`gacha-orb-particle p-${i}`} />
                ))}
              </div>
              <div className="gacha-orb-flash" />
            </div>
            <div className="gacha-spin-hint">
              {subPhase === "charge" && "gathering energy…"}
              {subPhase === "peak" && "tier locking in…"}
              {subPhase === "burst" && ""}
            </div>
          </>
        )}

        {phase === "reveal" && result && (
          <>
            <div className="gacha-burst" />
            <div className="gacha-eyebrow">{Icon.sparkle(13)} <span>Today's map</span></div>

            <div className={`gacha-reveal-card ${result.legendary ? "is-legendary" : ""}`} style={{ borderColor: result.tier.color }}>
              <div className="gacha-reveal-bg" />
              {result.legendary && (
                <div className="gacha-legendary-badge">✦ LEGENDARY</div>
              )}
              <div className="gacha-reveal-tier" style={{ color: result.tier.color }}>
                <span className="gacha-reveal-stars">{result.tier.stars}★</span>
                <span className="gacha-reveal-tname">{result.tier.name}</span>
              </div>
              <div className="gacha-reveal-artist">{result.artist}</div>
              <div className="gacha-reveal-title">
                {result.url ? (
                  <a
                    href={result.url}
                    target="_blank"
                    rel="noreferrer noopener"
                    onClick={(e) => e.stopPropagation()}
                    style={{ color: "inherit", textDecoration: "none" }}
                  >
                    {result.title}
                  </a>
                ) : result.title}
              </div>
              {result.version && (
                <div className="gacha-reveal-version" style={{ color: result.tier.color }}>
                  [{result.version}]
                </div>
              )}
              <div className="gacha-reveal-mapper">mapped by {result.mapper}</div>

              <div className="gacha-reveal-meta">
                <div className="grm-cell">
                  <div className="grm-lbl">Stars</div>
                  <div className="grm-val" style={{ color: result.tier.color }}>{result.stars}★</div>
                </div>
                <div className="grm-cell">
                  <div className="grm-lbl">BPM</div>
                  <div className="grm-val">{result.bpm}</div>
                </div>
                <div className="grm-cell">
                  <div className="grm-lbl">Length</div>
                  <div className="grm-val">{fmtLen(result.lenSec)}</div>
                </div>
                <div className="grm-cell">
                  <div className="grm-lbl">Mod</div>
                  <div className="grm-val grm-mod" title={result.mod.desc}>{result.mod.tag}</div>
                </div>
              </div>

              <div className="gacha-reveal-flair" style={{ color: result.tier.glow }}>
                {result.legendary
                  ? `played ${result.playcount.toLocaleString()} times`
                  : `"${result.tier.flair}"`}
              </div>
            </div>

            <div className="gacha-rewards">
              <div className="gacha-reward">
                <div className="gr-ico">{Icon.paw(16)}</div>
                <div className="gr-text">
                  <div className="gr-val">+{result.tier.pps}s of PP</div>
                  <div className="gr-sub">based on your current PP/s</div>
                </div>
              </div>
              {result.tier.spReward > 0 && (
                <div className="gacha-reward">
                  <div className="gr-ico">{Icon.sparkle(16)}</div>
                  <div className="gr-text">
                    <div className="gr-val">+{result.tier.spReward} skill point{result.tier.spReward > 1 ? "s" : ""}</div>
                    <div className="gr-sub">spendable in the skill tree</div>
                  </div>
                </div>
              )}
              {result.tier.stars >= 8 && (
                <div className="gacha-reward gacha-reward-hot">
                  <div className="gr-ico">{Icon.trophy(16)}</div>
                  <div className="gr-text">
                    <div className="gr-val">
                      {result.tier.stars === 10 ? "Ultra buff (×3 helpers, 60s)"
                        : result.tier.stars === 9 ? "Encore buff (×1.5, 40s)"
                        : "Lucky buff (×2, 30s)"}
                    </div>
                    <div className="gr-sub">activates on claim</div>
                  </div>
                </div>
              )}
            </div>

            <button className="gacha-pull-btn gacha-claim-btn" onClick={doClaim}>
              {Icon.heart(15, "#fff")} <span>Claim rewards</span>
            </button>
          </>
        )}
      </div>
    </div>
  );
}

/* ============== Collection tab — gallery of past pulls ============== */
function CollectionTab({ pulls, tiers }) {
  const [filter, setFilter] = useState("all"); // all | star number | "legendary"
  const [sort, setSort] = useState("newest"); // newest | rarity

  const counts = {};
  pulls.forEach((p) => {
    const k = p.tier.stars;
    counts[k] = (counts[k] || 0) + 1;
  });

  const legendaryCount = pulls.filter((p) => p.legendary).length;
  const highestStars = pulls.reduce((m, p) => Math.max(m, p.tier.stars), 0);

  let visible = pulls;
  if (filter === "legendary") visible = visible.filter((p) => p.legendary);
  else if (filter !== "all") visible = visible.filter((p) => p.tier.stars === Number(filter));

  if (sort === "rarity") {
    visible = [...visible].sort((a, b) => b.tier.stars - a.tier.stars || b.ts - a.ts);
  }

  if (pulls.length === 0) {
    return (
      <div className="collection-tab">
        <div className="collection-empty">
          <div className="collection-empty-ico">{Icon.music(48)}</div>
          <div className="collection-empty-title">No maps yet</div>
          <div className="collection-empty-sub">Pull your daily map to start building the collection.</div>
        </div>
      </div>
    );
  }

  return (
    <div className="collection-tab">
      <div className="collection-head">
        <div>
          <div className="collection-eyebrow">{Icon.music(13)} <span>Beatmap Collection</span></div>
          <div className="collection-title">Your queued maps</div>
        </div>
        <div className="collection-stats">
          <div className="cs-cell">
            <div className="cs-val">{pulls.length}</div>
            <div className="cs-lbl">Total</div>
          </div>
          <div className="cs-cell">
            <div className="cs-val" style={{ color: tiers[highestStars - 1]?.color }}>{highestStars}★</div>
            <div className="cs-lbl">Best</div>
          </div>
          {legendaryCount > 0 && (
            <div className="cs-cell">
              <div className="cs-val" style={{ color: "#ffd24a" }}>{legendaryCount}</div>
              <div className="cs-lbl">Legendary</div>
            </div>
          )}
        </div>
      </div>

      <div className="collection-filters">
        <button
          className={`coll-filter ${filter === "all" ? "active" : ""}`}
          onClick={() => setFilter("all")}
        >All ({pulls.length})</button>
        {tiers.map((t) => {
          const c = counts[t.stars] || 0;
          if (c === 0) return null;
          return (
            <button
              key={t.stars}
              className={`coll-filter ${filter === String(t.stars) ? "active" : ""}`}
              onClick={() => setFilter(String(t.stars))}
              style={{
                borderColor: t.color,
                color: filter === String(t.stars) ? "#fff" : t.color,
                background: filter === String(t.stars)
                  ? `linear-gradient(135deg, ${t.color}, ${t.glow})`
                  : undefined,
              }}
            >
              {t.stars}★ ({c})
            </button>
          );
        })}
        {legendaryCount > 0 && (
          <button
            className={`coll-filter ${filter === "legendary" ? "active" : ""}`}
            onClick={() => setFilter("legendary")}
            style={{
              borderColor: "#ffd24a",
              color: filter === "legendary" ? "#2a1a00" : "#d68a00",
              background: filter === "legendary"
                ? "linear-gradient(135deg, #ffd24a, #ff8a00)"
                : undefined,
            }}
          >
            ✦ Legendary ({legendaryCount})
          </button>
        )}
        <div className="coll-sort">
          <button
            className={`coll-sort-btn ${sort === "newest" ? "active" : ""}`}
            onClick={() => setSort("newest")}
          >Newest</button>
          <button
            className={`coll-sort-btn ${sort === "rarity" ? "active" : ""}`}
            onClick={() => setSort("rarity")}
          >Rarity</button>
        </div>
      </div>

      <div className="collection-grid">
        {visible.map((p) => (
          <div
            key={p.id}
            className={`coll-card ${p.legendary ? "is-legendary" : ""}`}
            style={{
              borderColor: p.tier.color,
              "--tier-color": p.tier.color,
              "--tier-glow": p.tier.glow,
            }}
          >
            <div className="coll-card-bg" />
            {p.legendary && <div className="coll-card-legendary">✦</div>}
            <div className="coll-card-tier" style={{ color: p.tier.color }}>
              <span className="coll-card-stars">{p.tier.stars}★</span>
              <span className="coll-card-tname">{p.tier.name}</span>
            </div>
            <div className="coll-card-artist">{p.artist}</div>
            <div className="coll-card-title">
              {p.url ? (
                <a href={p.url} target="_blank" rel="noreferrer noopener">{p.title}</a>
              ) : p.title}
            </div>
            <div className="coll-card-mapper">by {p.mapper}{p.version && ` [${p.version}]`}</div>
            <div className="coll-card-meta">
              <span>{p.bpm} BPM</span>
              <span>·</span>
              <span>{fmtLen(p.lenSec)}</span>
              <span>·</span>
              <span className="coll-card-mod" title={p.mod.desc}>{p.mod.tag}</span>
            </div>
            <div className="coll-card-date">{p.date}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* Pull live snapshot — generated by scripts/fetch-beatmaps.mjs.
   Fetched from the page-relative path so it works under /pages/*.html. */
fetch("./beatmaps.json")
  .then((r) => (r.ok ? r.json() : null))
  .then((j) => {
    if (j && Array.isArray(j.beatmaps) && j.beatmaps.length) {
      BEATMAPS = j.beatmaps;
      window.GACHA.BEATMAPS = BEATMAPS;
      window.GACHA.generatedAt = j.generatedAt || null;
    }
  })
  .catch(() => {});

window.GachaCard = GachaCard;
window.GachaModal = GachaModal;
window.CollectionTab = CollectionTab;
window.GACHA = {
  todayKey,
  nextResetMs,
  fmtCountdown,
  TIERS: GACHA_TIERS,
  BEATMAPS,
};
