// Demo data so the board's design is previewable BEFORE the Worker is
// redeployed with the new ?action=xptop endpoint. Replaced by real data the
// moment the live endpoint responds. Flagged so the UI can label it.
const XP_DEMO_PLAYERS = [
  { name: 'Aydin Adam',   xp: 6788, plays: 31, games: 4 },
  { name: 'Aaliyah',      xp: 4150, plays: 24, games: 5 },
  { name: 'Leo',          xp: 2400, plays: 18, games: 3 },
  { name: 'Maya',         xp: 1650, plays: 15, games: 4 },
  { name: 'Noah',         xp: 950,  plays: 11, games: 2 },
  { name: 'Zara',         xp: 600,  plays: 8,  games: 3 },
  { name: 'Ethan',        xp: 350,  plays: 6,  games: 2 },
  { name: 'Lily',         xp: 150,  plays: 3,  games: 1 },
];

// Fetch the global XP leaderboard (top players by total experience points).
// Optionally pass the current player's email to also get their own standing
// (so we can pin their row when they're outside the top N).
async function fetchXpTop(limit = 20, email = '') {
  try {
    let u = `${window.LEADERBOARD_API}?action=xptop&limit=${limit}`;
    if (email) u += `&email=${encodeURIComponent(email)}`;
    const r = await fetch(u);
    if (!r.ok) throw new Error('unavailable');
    const data = await r.json();
    if (data && data.error) throw new Error(data.error);
    return {
      ok: true,
      demo: false,
      players: Array.isArray(data.players) ? data.players : [],
      self: data.self || null,
    };
  } catch {
    // Endpoint not deployed yet (or offline) — show labelled demo data so the
    // design is visible. Goes away as soon as the live endpoint works.
    return { ok: true, demo: true, players: XP_DEMO_PLAYERS.slice(0, limit), self: null };
  }
}

// ===== XP levels — kid-friendly tiers derived from total experience points =====
// ===== XP titles — rank progression derived purely from Total XP =====
// Each tier's `min` is the lower bound of its XP band; xpLevel() picks the
// highest tier whose min the player's Total XP has reached. Bands:
//   Rookie 0–999 · Explorer 1k–4,999 · Adventurer 5k–14,999 · Champion 15k–39,999
//   Hero 40k–74,999 · Legend 75k–99,999 · Mythic Legend 100k–149,999
//   Immortal 150k–199,999 · Overlord 200k–299,999 · Vanguard 300k–499,999
//   Ascendant 500k–999,999 · Elite 1,000,000+
const XP_LEVELS = [
  { min: 0,       name: 'Rookie',        icon: '🌱',  color: 'oklch(0.80 0.15 150)' },
  { min: 1000,    name: 'Explorer',      icon: '🔍',  color: 'oklch(0.82 0.11 225)' },
  { min: 5000,    name: 'Adventurer',    icon: '⚔️',  color: 'oklch(0.86 0.15 90)' },
  { min: 15000,   name: 'Champion',      icon: '🏆',  color: 'oklch(0.74 0.16 50)' },
  { min: 40000,   name: 'Hero',          icon: '🦸',  color: 'oklch(0.56 0.18 305)', text: '#fff' },
  { min: 75000,   name: 'Legend',        icon: '👑',  color: 'oklch(0.58 0.21 25)',  text: '#fff' },
  { min: 100000,  name: 'Mythic Legend', icon: '💎',  color: 'oklch(0.52 0.17 260)', text: '#fff' },
  { min: 150000,  name: 'Immortal',      icon: '🔥',  color: 'oklch(0.70 0.19 350)' },
  { min: 200000,  name: 'Overlord',      icon: '🔱',  color: 'oklch(0.62 0.13 195)', text: '#fff' },
  { min: 300000,  name: 'Vanguard',      icon: '⚡',  color: 'oklch(0.84 0.17 125)' },
  { min: 500000,  name: 'Ascendant',     icon: '🌟',  color: 'oklch(0.52 0.14 160)', text: '#fff' },
  { min: 1000000, name: 'Elite',         icon: '👑✨', color: 'var(--ink)',           text: '#fff' },
];
function xpLevel(xp) {
  let lvl = XP_LEVELS[0];
  for (const l of XP_LEVELS) { if (xp >= l.min) lvl = l; }
  return lvl;
}

// ——— Leaderboard helpers + UI ———

// Set this to your live api.php URL once deployed.
// Default is relative ('api.php') which works when site + api.php are at the same path.
window.LEADERBOARD_API = window.LEADERBOARD_API || 'api.php';

// ===== Stored player credentials (name + email) =====
const GL_NAME_KEY  = 'gl_lastname';
const GL_EMAIL_KEY = 'gl_email';

function safeGet(k) { try { return localStorage.getItem(k) || ''; } catch { return ''; } }
function safeSet(k, v) { try { localStorage.setItem(k, v); } catch {} }
function safeDel(k) { try { localStorage.removeItem(k); } catch {} }

function getStoredName()  { return safeGet(GL_NAME_KEY); }
function getStoredEmail() { return safeGet(GL_EMAIL_KEY); }
function isValidEmail(e)  { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e || ''); }
function hasStoredCredentials() {
  return getStoredName().length >= 2 && isValidEmail(getStoredEmail());
}
function saveStoredCredentials(name, email) {
  // Normalize on save: trim + collapse spaces in name, lowercase email
  const n = (name || '').trim().replace(/\s+/g, ' ');
  const e = (email || '').trim().toLowerCase();
  safeSet(GL_NAME_KEY, n);
  safeSet(GL_EMAIL_KEY, e);
  window.dispatchEvent(new Event('gl-credentials-changed'));
}
function clearStoredCredentials() {
  safeDel(GL_NAME_KEY);
  safeDel(GL_EMAIL_KEY);
  // Also wipe the cached XP / plays totals — they're tied to the player
  // who just logged out. Leaving them behind makes the star badge keep
  // showing the previous player's points on the empty/profile screen.
  safeDel('gl_total_xp');
  safeDel('gl_total_plays');
  window.dispatchEvent(new Event('gl-xp-changed'));
  window.dispatchEvent(new Event('gl-credentials-changed'));
}

async function submitScore(gameId, name, email, score, lowerIsBetter) {
  try {
    const r = await fetch(window.LEADERBOARD_API + '?action=submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        game: gameId, name, email,
        score: Math.floor(score),
        lowerIsBetter: !!lowerIsBetter,
      }),
    });
    const data = await r.json();
    if (!r.ok) throw new Error(data.error || 'Failed to submit');
    return data;
  } catch (e) {
    return { error: e.message };
  }
}

async function fetchTop(gameId, limit = 20, order = 'desc') {
  try {
    const r = await fetch(`${window.LEADERBOARD_API}?action=top&game=${encodeURIComponent(gameId)}&limit=${limit}&order=${order}`);
    if (!r.ok) return { ok: false, scores: [] };
    const data = await r.json();
    if (data && data.error) return { ok: false, scores: [] };
    // ok:true even when the array is empty — distinguishes "server reachable,
    // no scores yet" from "server unreachable" so we don't wrongly fall back
    // to this-device-only local storage.
    return { ok: true, scores: Array.isArray(data.scores) ? data.scores : [] };
  } catch {
    return { ok: false, scores: [] };
  }
}

// Local fallback (when api.php isn't reachable — dev preview)
function localKey(g) { return 'gl_local_' + g; }
function getLocal(g) {
  try { return JSON.parse(localStorage.getItem(localKey(g)) || '[]'); } catch { return []; }
}
function pushLocal(g, name, email, score, lowerIsBetter) {
  const arr = getLocal(g);
  // Dedupe by (lower(name) + lower(email))
  const lname = (name || '').toLowerCase();
  const lemail = (email || '').toLowerCase();
  const idx = arr.findIndex(r =>
    (r.email || '').toLowerCase() === lemail &&
    (r.name || '').toLowerCase() === lname
  );
  if (idx >= 0) {
    const better = lowerIsBetter ? (score < arr[idx].score) : (score > arr[idx].score);
    if (better) arr[idx] = { name, email, score, ts: Math.floor(Date.now()/1000) };
  } else {
    arr.push({ name, email, score, ts: Math.floor(Date.now()/1000) });
  }
  arr.sort((a,b) => lowerIsBetter ? a.score - b.score : b.score - a.score);
  safeSet(localKey(g), JSON.stringify(arr.slice(0, 50)));
}

// Submit + fall back to local if API fails
async function submitScoreSmart(gameId, name, email, score, opts) {
  const o = opts || {};
  const lowerIsBetter = !!o.lowerIsBetter;
  const result = await submitScore(gameId, name, email, score, lowerIsBetter);
  if (result.error) {
    pushLocal(gameId, name, email, score, lowerIsBetter);
    // Local fallback: assume not top, just +50 XP
    const prevPlays = parseInt(safeGet('gl_total_plays') || '0', 10);
    const prevXp = parseInt(safeGet('gl_total_xp') || '0', 10);
    safeSet('gl_total_plays', String(prevPlays + 1));
    safeSet('gl_total_xp', String(prevXp + 50));
    window.dispatchEvent(new Event('gl-xp-changed'));
    return { ok: true, rank: null, local: true, improved: true, currentBest: score, xpEarned: 50 };
  }
  // Server returned a canonical name? Adopt it locally so the badge always
  // shows the first-ever name registered with this email — even if the
  // player just submitted with a typo / variant ("Aydinn" → "Aydin").
  if (result.canonicalName && typeof result.canonicalName === 'string') {
    const stored = getStoredName();
    if (stored && stored !== result.canonicalName) {
      safeSet(GL_NAME_KEY, result.canonicalName);
      window.dispatchEvent(new Event('gl-credentials-changed'));
    }
  }
  if (typeof result.totalPlays === 'number') {
    safeSet('gl_total_plays', String(result.totalPlays));
  }
  if (typeof result.totalXp === 'number') {
    // Server total is authoritative after a successful save, but guard
    // against a transient SUM that's lower than what we already cached
    // (e.g. partial DB read, missing row lookup) — never erase progress.
    const prevXp = parseInt(safeGet('gl_total_xp') || '0', 10);
    safeSet('gl_total_xp', String(Math.max(prevXp, result.totalXp)));
  }
  window.dispatchEvent(new Event('gl-xp-changed'));
  return result;
}

// Fetch player stats by email
async function fetchPlayerStats(name, email) {
  if (!isValidEmail(email) || !name) return null;
  try {
    const r = await fetch(
      `${window.LEADERBOARD_API}?action=player`
      + `&email=${encodeURIComponent(email)}`
      + `&name=${encodeURIComponent(name)}`
    );
    const data = await r.json();
    if (!r.ok) return null;
    return data; // { canonicalName, totalPlays, totalXp }
  } catch { return null; }
}

async function fetchTopSmart(gameId, limit = 20, lowerIsBetter = false) {
  const order = lowerIsBetter ? 'asc' : 'desc';
  const remote = await fetchTop(gameId, limit, order);
  // If the server responded (even with an empty array), show that — the
  // leaderboard is authoritative. Only fall back to per-device local cache
  // when the network/API actually failed, otherwise we'd hide everyone
  // else's scores any time a single request returned no rows.
  if (remote.ok) return { scores: remote.scores, local: false };
  let local = getLocal(gameId).slice();
  local.sort((a,b) => lowerIsBetter ? a.score - b.score : b.score - a.score);
  return { scores: local.slice(0, limit), local: local.length > 0 };
}

// ——— Score Submit Modal ———
function ScoreSubmitModal({ gameId, score, metric, lowerIsBetter, onClose, onSubmitted }) {
  const [name, setName]   = React.useState(() => getStoredName());
  const [email, setEmail] = React.useState(() => getStoredEmail());
  const [busy, setBusy]   = React.useState(false);
  const [err, setErr]     = React.useState(null);
  const [result, setResult] = React.useState(null);
  const unit = metric || 'points';

  // Run a submit with the provided credentials
  const doSubmit = async (n, e) => {
    setBusy(true); setErr(null);
    const res = await submitScoreSmart(gameId, n, e, score, { lowerIsBetter });
    setBusy(false);
    if (res.error) { setErr(res.error); return; }
    saveStoredCredentials(n, e);
    setResult(res);
  };

  // If we already have stored credentials, auto-submit on open (no form shown)
  const autoSubmittedRef = React.useRef(false);
  React.useEffect(() => {
    if (autoSubmittedRef.current) return;
    if (hasStoredCredentials() && score > 0) {
      autoSubmittedRef.current = true;
      doSubmit(getStoredName(), getStoredEmail());
    }
  }, []);

  const submitForm = () => {
    const n = name.trim();
    const e = email.trim().toLowerCase();
    if (n.length < 2) { setErr('Please enter your full name (at least 2 letters)'); return; }
    if (!isValidEmail(e)) { setErr('Please enter a valid email address'); return; }
    doSubmit(n, e);
  };

  const closeWithResult = () => onSubmitted(result || {});

  // ——— Result screen ———
  if (result) {
    const { improved, previousBest, rank, local } = result;
    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(27,24,64,0.65)',
        display: 'grid', placeItems: 'center', zIndex: 200, padding: 16,
      }}>
        <div style={{
          background: 'var(--paper)', border: 'var(--border-thick)',
          borderRadius: 24, boxShadow: 'var(--shadow-lg)',
          padding: 28, maxWidth: 420, width: '100%', textAlign: 'center',
        }}>
          <div style={{ fontSize: 64, marginBottom: 6 }}>{improved ? '🎉' : '👍'}</div>
          <h2 style={{ marginBottom: 6 }}>
            {improved
              ? (previousBest !== null ? 'New best score!' : 'Score saved!')
              : 'Keep practising!'}
          </h2>
          <p style={{ color: 'var(--ink-soft)', marginBottom: 14, fontSize: 16 }}>
            {improved && previousBest !== null && (
              <>You beat your previous best of <strong>{previousBest}</strong> {unit}.</>
            )}
            {improved && previousBest === null && (
              <>First time playing — welcome to the leaderboard!</>
            )}
            {!improved && lowerIsBetter && (
              <>You finished in <strong>{score}</strong> {unit}, but your best is fewer ({previousBest} {unit}).</>
            )}
            {!improved && !lowerIsBetter && (
              <>You scored <strong>{score}</strong> {unit}, but your best is still <strong>{previousBest}</strong> {unit}.</>
            )}
          </p>
          {rank && (
            <div style={{ marginTop: 6 }}>
              <span className="pill-stat" style={{
                background: rank <= 3 ? 'var(--c-sun)' : 'var(--bg-warm)',
                fontSize: 16, padding: '8px 18px',
              }}>
                You're ranked #{rank}{rank === 1 ? ' 🥇' : rank === 2 ? ' 🥈' : rank === 3 ? ' 🥉' : ''}
              </span>
            </div>
          )}
          {local && (
            <p style={{ fontSize: 12, color: 'var(--ink-soft)', marginTop: 12 }}>
              (Saved on this device — server unreachable)
            </p>
          )}
          <div style={{ marginTop: 22 }}>
            <button className="btn btn-primary btn-lg" onClick={closeWithResult}>OK</button>
          </div>
        </div>
      </div>
    );
  }

  // While auto-submitting with stored creds, show a minimal spinner instead of the form
  if (busy && hasStoredCredentials()) {
    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(27,24,64,0.65)',
        display: 'grid', placeItems: 'center', zIndex: 200, padding: 16,
      }}>
        <div style={{
          background: 'var(--paper)', border: 'var(--border-thick)',
          borderRadius: 24, boxShadow: 'var(--shadow-lg)',
          padding: 36, maxWidth: 360, width: '100%', textAlign: 'center',
        }}>
          <div style={{ fontSize: 40, marginBottom: 10 }}>⏳</div>
          <p style={{ fontWeight: 700 }}>Saving your score…</p>
        </div>
      </div>
    );
  }

  // ——— Form screen ———
  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(27,24,64,0.65)',
      display: 'grid', placeItems: 'center', zIndex: 200, padding: 16,
    }}>
      <div style={{
        background: 'var(--paper)', border: 'var(--border-thick)',
        borderRadius: 24, boxShadow: 'var(--shadow-lg)',
        padding: 28, maxWidth: 460, width: '100%', textAlign: 'center',
      }}>
        <div style={{ fontSize: 56, marginBottom: 8 }}>🏆</div>
        <h2 style={{ marginBottom: 6 }}>You finished with {score} {unit}!</h2>
        <p style={{ color: 'var(--ink-soft)', marginBottom: 18, fontSize: 14 }}>
          One-time setup on this device. We'll remember you next time. <br/>
          <span style={{ fontSize: 12 }}>Use a parent's email — it's just used to keep your scores separate from other players with the same name. No emails will be sent.</span>
        </p>
        <input
          autoFocus
          value={name}
          onChange={e => setName(e.target.value)}
          maxLength={30}
          placeholder="Full name (e.g. Aydin Adam)"
          style={{
            width: '100%', padding: '14px 16px', marginBottom: 10,
            border: 'var(--border)', borderRadius: 14,
            fontFamily: 'var(--font-body)', fontSize: 16, fontWeight: 700,
            textAlign: 'center', background: 'var(--bg)',
          }}
        />
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') submitForm(); }}
          maxLength={120}
          placeholder="Email (parent's is fine)"
          style={{
            width: '100%', padding: '14px 16px',
            border: 'var(--border)', borderRadius: 14,
            fontFamily: 'var(--font-body)', fontSize: 16, fontWeight: 700,
            textAlign: 'center', background: 'var(--bg)',
          }}
        />
        {err && <p style={{ color: 'oklch(0.55 0.18 25)', marginTop: 10, fontWeight: 700 }}>{err}</p>}
        <div style={{ display: 'flex', gap: 10, marginTop: 18, justifyContent: 'center' }}>
          <button className="btn" onClick={onClose} disabled={busy}>Skip</button>
          <button className="btn btn-primary" onClick={submitForm} disabled={busy}>
            {busy ? 'Saving…' : 'Save my score'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ——— Leaderboard List ———
function LeaderboardList({ gameId, highlightName, lowerIsBetter, metric, limit }) {
  const [data, setData] = React.useState({ scores: [], loading: true, local: false });
  const unit = metric ? ` ${metric}` : '';
  const max = limit || 20;

  React.useEffect(() => {
    let alive = true;
    setData(d => ({ ...d, loading: true }));
    fetchTopSmart(gameId, max, !!lowerIsBetter).then(r => {
      if (alive) setData({ scores: r.scores, loading: false, local: r.local });
    });
    return () => { alive = false; };
  }, [gameId, lowerIsBetter, max]);

  if (data.loading) return <p style={{ textAlign: 'center', color: 'var(--ink-soft)' }}>Loading…</p>;
  if (!data.scores.length) return (
    <p style={{ textAlign: 'center', color: 'var(--ink-soft)', padding: 20 }}>
      No scores yet — be the first!
    </p>
  );

  const medals = ['🥇','🥈','🥉'];

  return (
    <div>
      {data.local && (
        <p style={{ fontSize: 12, textAlign: 'center', color: 'var(--ink-soft)', marginBottom: 10 }}>
          Showing scores saved on this device (server unreachable)
        </p>
      )}
      {/* Column headers */}
      <div className="lb-row lb-row-head" style={{
        display: 'grid',
        gridTemplateColumns: '1fr 48px 88px 84px',
        gap: 8, padding: '4px 12px', marginBottom: 4,
        fontSize: 11, fontWeight: 800, color: 'var(--ink-soft)',
        textTransform: 'uppercase', letterSpacing: '.06em',
        whiteSpace: 'nowrap',
      }}>
        <div>Player</div>
        <div style={{ textAlign: 'center' }}>Rank</div>
        <div style={{ textAlign: 'right' }}>
          {metric ? 'Best ' + metric.charAt(0).toUpperCase() + metric.slice(1) : 'Best Score'}
        </div>
        <div style={{ textAlign: 'right' }}>Times Played</div>
      </div>
      <ol style={{ listStyle: 'none', padding: 0, margin: 0 }}>
        {data.scores.map((s, i) => {
          const isMe = highlightName && s.name
            && s.name.trim().toLowerCase() === highlightName.trim().toLowerCase();
          const totalPlays = (typeof s.total_plays === 'number') ? s.total_plays
                            : (typeof s.totalPlays === 'number') ? s.totalPlays
                            : 1;
          return (
            <li key={i} className="lb-row" style={{
              display: 'grid',
              gridTemplateColumns: '1fr 48px 88px 84px',
              alignItems: 'center', gap: 8, padding: '10px 12px', marginBottom: 6,
              background: isMe ? 'var(--c-sun)' : i < 3 ? 'var(--bg-warm)' : 'var(--paper)',
              border: 'var(--border)', borderRadius: 14,
              fontWeight: 700,
            }}>
              <div style={{ fontSize: 15, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {s.name}{isMe ? ' (you)' : ''}
              </div>
              <div style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 18, textAlign: 'center' }}>
                {medals[i] || (i + 1)}
              </div>
              <div style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 18, textAlign: 'right' }}>
                {s.score}
              </div>
              <div style={{ fontSize: 13, color: 'var(--ink-soft)', textAlign: 'right', fontWeight: 700 }}>
                {totalPlays}
              </div>
            </li>
          );
        })}
      </ol>
    </div>
  );
}

// ——— Post-game leaderboard panel (shown after a game ends) ———
function GameOverLeaderboard({ gameId, score, onPlayAgain }) {
  const [phase, setPhase]       = React.useState('submit'); // submit | view
  const [submission, setSubmit] = React.useState(null);

  if (phase === 'submit' && score > 0) {
    return (
      <ScoreSubmitModal
        gameId={gameId}
        score={score}
        onClose={() => setPhase('view')}
        onSubmitted={(r) => { setSubmit(r); setPhase('view'); }}
      />
    );
  }

  return (
    <div style={{
      marginTop: 24, padding: 20, background: 'var(--bg)',
      border: 'var(--border)', borderRadius: 18,
    }}>
      <h3 style={{ textAlign: 'center', marginBottom: 6 }}>🏆 Top Players</h3>
      {submission?.rank && (
        <p style={{ textAlign: 'center', color: 'var(--ink-soft)', marginBottom: 12 }}>
          Your rank: <strong style={{ color: 'var(--ink)' }}>#{submission.rank}</strong>
        </p>
      )}
      <LeaderboardList gameId={gameId} highlightName={getStoredName()} />
      <div style={{ textAlign: 'center', marginTop: 16 }}>
        <button className="btn btn-primary" onClick={onPlayAgain}>Play again</button>
      </div>
    </div>
  );
}

// Hook each game uses to publish its current score
function useReportScore(score) {
  React.useEffect(() => {
    window.__glCurrentScore = score;
    window.dispatchEvent(new CustomEvent('gl-score', { detail: score }));
    return () => {
      window.__glCurrentScore = 0;
      window.dispatchEvent(new CustomEvent('gl-score', { detail: 0 }));
    };
  }, [score]);
}

function PointsBadge() {
  // Experience Points — synced with server when player is logged in.
  // 50 XP per save, +1000 bonus if rank #1 (or tied with world top) after the save.
  const [xp, setXp] = React.useState(
    () => parseInt(safeGet('gl_total_xp') || '0', 10)
  );

  // Pull fresh totals from the server. Important rule: never DROP the
  // visible XP below what we already trust locally. The server can only
  // ever push the number up — if it returns less than our local cache
  // (xp column missing, transient lookup miss, name/email casing quirk),
  // we keep the local value rather than wiping the player's progress.
  const refreshFromServer = React.useCallback(async () => {
    const email = getStoredEmail();
    const name  = getStoredName();
    // No credentials = no player = no XP. Reset the badge so a logout
    // doesn't keep displaying the previous player's points.
    if (!isValidEmail(email) || !name) {
      safeSet('gl_total_xp', '0');
      setXp(0);
      return;
    }
    const data = await fetchPlayerStats(name, email);
    if (!data) return;
    // Server tells us the canonical name for this email — adopt it locally
    // so the badge stays consistent across the site.
    if (data.canonicalName && data.canonicalName !== getStoredName()) {
      safeSet(GL_NAME_KEY, data.canonicalName);
      window.dispatchEvent(new Event('gl-credentials-changed'));
    }
    if (typeof data.totalXp !== 'number') return;
    const local = parseInt(safeGet('gl_total_xp') || '0', 10);
    const next  = Math.max(local, data.totalXp);
    safeSet('gl_total_xp', String(next));
    setXp(next);
  }, []);

  React.useEffect(() => {
    const onLocal = () => {
      // 1) Reflect the latest localStorage value immediately so the badge
      //    updates the moment a save completes (snappy UX).
      setXp(parseInt(safeGet('gl_total_xp') || '0', 10));
      // 2) Then double-check against the server in the background so the
      //    totals stay authoritative across devices / multiple games.
      refreshFromServer();
    };
    window.addEventListener('gl-xp-changed', onLocal);
    window.addEventListener('storage', onLocal);
    window.addEventListener('gl-credentials-changed', refreshFromServer);
    refreshFromServer();
    return () => {
      window.removeEventListener('gl-xp-changed', onLocal);
      window.removeEventListener('storage', onLocal);
      window.removeEventListener('gl-credentials-changed', refreshFromServer);
    };
  }, [refreshFromServer]);

  return (
    <div className="points-badge" title={`Experience points: ${xp.toLocaleString()}`} style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '8px 14px', borderRadius: 999,
      background: 'var(--c-sun)', border: '2.5px solid var(--ink)',
      boxShadow: '3px 3px 0 var(--ink)',
      fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 14,
      color: 'var(--ink)', whiteSpace: 'nowrap',
    }}>
      <span style={{ fontSize: 16, lineHeight: 1 }}>⭐</span>
      <span>{xp.toLocaleString()}</span>
    </div>
  );
}

// ——— Profile modal (set / edit name + email) ———
function ProfileModal({ onClose }) {
  const initialName  = getStoredName();
  const initialEmail = getStoredEmail();
  const isUpdate = !!initialName && !!initialEmail;

  const [name, setName]   = React.useState(initialName);
  const [email, setEmail] = React.useState(initialEmail);
  const [err, setErr]     = React.useState(null);

  const save = () => {
    const n = name.trim();
    const e = email.trim().toLowerCase();
    if (n.length < 2) { setErr('Please enter your full name (at least 2 letters)'); return; }
    if (!isValidEmail(e)) { setErr('Please enter a valid email address'); return; }
    saveStoredCredentials(n, e);
    onClose();
  };

  const signOut = () => {
    if (confirm('Sign out? Next time you save a score, you\'ll be asked for name + email again.')) {
      clearStoredCredentials();
      onClose();
    }
  };

  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(27,24,64,0.65)',
      display: 'grid', placeItems: 'center', zIndex: 200, padding: 16,
    }}>
      <div style={{
        background: 'var(--paper)', border: 'var(--border-thick)',
        borderRadius: 24, boxShadow: 'var(--shadow-lg)',
        padding: 28, maxWidth: 460, width: '100%', textAlign: 'center',
      }}>
        <div style={{ fontSize: 56, marginBottom: 8 }}>👋</div>
        <h2 style={{ marginBottom: 6 }}>{isUpdate ? 'Edit your profile' : 'Who\'s playing?'}</h2>
        <p style={{ color: 'var(--ink-soft)', marginBottom: 18, fontSize: 14 }}>
          {isUpdate
            ? 'Change your name or email here. Saved on this device.'
            : 'One-time setup so your scores are saved across devices.'}
          <br/>
          <span style={{ fontSize: 12 }}>Use a parent's email — it's just used to keep scores separate from other players with the same name. No emails will be sent.</span>
        </p>
        <input
          autoFocus
          value={name}
          onChange={e => setName(e.target.value)}
          maxLength={30}
          placeholder="Full name (e.g. Aydin Adam)"
          style={{
            width: '100%', padding: '14px 16px', marginBottom: 10,
            border: 'var(--border)', borderRadius: 14,
            fontFamily: 'var(--font-body)', fontSize: 16, fontWeight: 700,
            textAlign: 'center', background: 'var(--bg)',
          }}
        />
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') save(); }}
          maxLength={120}
          placeholder="Email (parent's is fine)"
          style={{
            width: '100%', padding: '14px 16px',
            border: 'var(--border)', borderRadius: 14,
            fontFamily: 'var(--font-body)', fontSize: 16, fontWeight: 700,
            textAlign: 'center', background: 'var(--bg)',
          }}
        />
        {err && <p style={{ color: 'oklch(0.55 0.18 25)', marginTop: 10, fontWeight: 700 }}>{err}</p>}
        <div style={{ display: 'flex', gap: 10, marginTop: 18, justifyContent: 'center', flexWrap: 'wrap' }}>
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" onClick={save}>
            {isUpdate ? 'Save changes' : 'Start playing'}
          </button>
        </div>
        {isUpdate && (
          <p style={{ marginTop: 14, fontSize: 13 }}>
            <a onClick={signOut}
              style={{ textDecoration: 'underline', cursor: 'pointer', color: 'var(--ink-soft)' }}>
              Sign out (clear name &amp; email)
            </a>
          </p>
        )}
      </div>
    </div>
  );
}

// ——— Player badge (shown in top nav: "Playing as Aydin") ———
function PlayerBadge() {
  const [showModal, setShowModal] = React.useState(false);
  const [, bump] = React.useReducer(x => x + 1, 0);

  React.useEffect(() => {
    const onCreds = () => bump();
    const onOpen = () => setShowModal(true); // e.g. "Set up my profile" on the Stickers page
    window.addEventListener('gl-credentials-changed', onCreds);
    window.addEventListener('gl-open-profile', onOpen);
    return () => {
      window.removeEventListener('gl-credentials-changed', onCreds);
      window.removeEventListener('gl-open-profile', onOpen);
    };
  }, []);

  const hasCreds = hasStoredCredentials();
  const name = getStoredName();
  const firstName = (name.split(' ')[0] || name) || '';

  return (
    <React.Fragment>
      <button
        className="player-badge"
        title={hasCreds ? `Playing as ${name}` : 'Set up your profile'}
        onClick={() => setShowModal(true)}
        style={{
          display: 'inline-flex', alignItems: 'center', gap: 6,
          padding: '8px 14px', borderRadius: 999,
          background: hasCreds ? 'var(--paper)' : 'var(--c-coral)',
          color: hasCreds ? 'var(--ink)' : 'white',
          border: '2.5px solid var(--ink)',
          boxShadow: '3px 3px 0 var(--ink)',
          fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 14,
          cursor: 'pointer', whiteSpace: 'nowrap',
        }}
      >
        <span style={{ fontSize: 16, lineHeight: 1 }}>👋</span>
        <span style={{ maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis' }}>
          {hasCreds ? firstName : 'Set profile'}
        </span>
      </button>
      {showModal && <ProfileModal onClose={() => setShowModal(false)} />}
    </React.Fragment>
  );
}

Object.assign(window, {
  submitScoreSmart, fetchTopSmart, fetchXpTop, xpLevel, XP_LEVELS,
  useReportScore, PointsBadge, PlayerBadge,
  ScoreSubmitModal, ProfileModal, LeaderboardList, GameOverLeaderboard,
  hasStoredCredentials, getStoredName, getStoredEmail, clearStoredCredentials,
});
