<script>
  import { getJSON, postJSON } from '$lib/api.js';
  import { pushGameState, fetchGameStats } from '$lib/gamesync.js';
  import { trackGame, gameShareUrl } from '$lib/analytics.js';

  let { variant = '5', onstatus } = $props();

  let length = $state(5);
  let maxGuesses = $state(6);
  let answer = $state(null);          // revealed by the server only at win/loss
  let why = $state(null);
  let date = $state('');
  let dict = $state(null);            // Set of valid guesses
  let guesses = $state([]);           // submitted rows (strings)
  let cols = $state([]);              // server-returned colours, parallel to guesses
  let current = $state('');           // the row being typed
  let status = $state('playing');     // 'playing' | 'won' | 'lost'
  let loading = $state(true);
  let submitting = $state(false);
  let shake = $state(false);
  let message = $state('');
  let ready = $state(false);          // animate-in once loaded

  const ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'].map((r) => r.split(''));

  const stateKey = $derived(`goodnews:word:${variant}:${date}`);
  const statsKey = $derived(`goodnews:word:${variant}:stats`);

  async function load() {
    loading = true; ready = false;
    guesses = []; cols = []; current = ''; status = 'playing'; answer = null; why = null; message = '';
    try {
      const p = await getJSON('/api/puzzle/word?variant=' + variant);  // holds NO answer
      length = p.length; maxGuesses = p.guesses; date = p.date;
      if (!dict || dict._len !== length) {
        const words = await getJSON('/words-' + length + '.json');
        dict = new Set(words); dict._len = length;
      }
      restore();
    } catch {
      message = 'Could not load today’s puzzle.';
    }
    loading = false;
    requestAnimationFrame(() => (ready = true));
    syncNow();   // reconcile with the server in the background (signed-in only)
  }

  function restore() {
    try {
      const saved = JSON.parse(localStorage.getItem(stateKey) || 'null');
      if (saved && Array.isArray(saved.guesses)) {
        guesses = saved.guesses;
        cols = saved.cols || [];
        status = saved.status || 'playing';
        answer = saved.answer ?? null;
        why = saved.why ?? null;
      }
    } catch { /* ignore */ }
    onstatus?.(summary());
  }
  function persist() {
    try { localStorage.setItem(stateKey, JSON.stringify({ guesses, cols, status, answer, why })); } catch { /* ignore */ }
    onstatus?.(summary());
  }

  // --- cross-device sync (signed-in only; "furthest progress" merged server-side) ---
  let serverStats = $state(null);
  let syncTimer;
  function adopt(merged) {
    if (!merged || !Array.isArray(merged.guesses)) return;
    const lr = [status !== 'playing' ? 1 : 0, guesses.length];
    const mr = [merged.status && merged.status !== 'playing' ? 1 : 0, merged.guesses.length];
    if (mr[0] > lr[0] || (mr[0] === lr[0] && mr[1] > lr[1])) {  // server is further along
      guesses = merged.guesses;
      cols = Array.isArray(merged.cols) ? merged.cols : cols;
      status = merged.status || 'playing';
      answer = merged.answer ?? answer;
      why = merged.why ?? why;
      persist();
    }
  }
  async function syncNow() {
    const d = date, v = variant;
    const merged = await pushGameState('word', v, d, { guesses, cols, status, answer, why });
    if (d === date && v === variant) adopt(merged);
    serverStats = await fetchGameStats('word', variant);   // streak/dist follow you across devices
  }
  function syncSoon() { clearTimeout(syncTimer); syncTimer = setTimeout(syncNow, 1000); }
  function summary() {
    return { variant, date, status, tries: guesses.length, max: maxGuesses };
  }

  // Best-known status per key for the on-screen keyboard, from server colours.
  let keyState = $derived.by(() => {
    const ks = {};
    const rank = { absent: 0, present: 1, correct: 2 };
    guesses.forEach((g, gi) => {
      const cs = cols[gi] || [];
      for (let i = 0; i < g.length; i++) {
        const k = g[i];
        if (cs[i] && (!(k in ks) || rank[cs[i]] > rank[ks[k]])) ks[k] = cs[i];
      }
    });
    return ks;
  });

  function flash(m) { message = m; setTimeout(() => (message = ''), 1400); }

  function key(k) {
    if (status !== 'playing' || loading || submitting) return;
    if (k === 'enter') return submit();
    if (k === 'back') { current = current.slice(0, -1); return; }
    if (/^[a-z]$/.test(k) && current.length < length) current += k;
  }

  function triggerShake() { shake = true; setTimeout(() => (shake = false), 420); }

  async function submit() {
    if (submitting) return;
    if (current.length < length) { flash('Not enough letters'); triggerShake(); return; }
    if (dict && !dict.has(current)) { flash('Not in word list'); triggerShake(); return; }
    submitting = true;
    try {
      const res = await postJSON('/api/puzzle/word/guess', { variant, guess: current, n: guesses.length + 1 });
      if (guesses.length === 0) trackGame('word', 'started');   // first real guess = started
      guesses = [...guesses, current];
      cols = [...cols, res.colors];
      current = '';
      if (res.solved) { status = 'won'; answer = res.answer; why = res.why; recordStat(true); trackGame('word', 'completed'); }
      else if (guesses.length >= maxGuesses) { status = 'lost'; answer = res.answer; why = res.why; recordStat(false); trackGame('word', 'completed'); }
      persist();
      syncSoon();   // push this guess (and any completion) to the server, debounced
    } catch {
      flash('Hmm — couldn’t check that. Try again.');
    } finally {
      submitting = false;
    }
  }

  function recordStat(won) {
    try {
      const s = JSON.parse(localStorage.getItem(statsKey) || 'null') || { played: 0, won: 0, streak: 0, dist: {} };
      s.played += 1;
      if (won) { s.won += 1; s.streak += 1; s.dist[guesses.length] = (s.dist[guesses.length] || 0) + 1; }
      else { s.streak = 0; }
      localStorage.setItem(statsKey, JSON.stringify(s));
    } catch { /* ignore */ }
  }
  let stats = $derived.by(() => {
    if (status === 'playing') return null;
    if (serverStats) return serverStats;   // signed-in: consistent across devices
    try { return JSON.parse(localStorage.getItem(statsKey) || 'null'); } catch { return null; }
  });

  function onKeydown(e) {
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    const k = e.key.toLowerCase();
    if (k === 'enter' || k === 'backspace' || /^[a-z]$/.test(k)) {
      e.preventDefault();
      key(k === 'backspace' ? 'back' : k);
    }
  }

  const EMOJI = { correct: '🟩', present: '🟨', absent: '⬛' };
  let copied = $state(false);
  function share() {
    const label = variant === '6' ? 'Long Word' : 'Daily Word';
    const score = status === 'won' ? guesses.length : 'X';
    const grid = cols.map((cs) => cs.map((c) => EMOJI[c]).join('')).join('\n');
    const text = `Upbeat Bytes · ${label} ${date}\n${score}/${maxGuesses}\n${grid}\n${gameShareUrl('word', variant)}`;
    // Count a share only once it actually happens (sheet completed / clipboard wrote),
    // never on a cancelled share sheet or denied clipboard.
    if (navigator.share) navigator.share({ text }).then(() => trackGame('word', 'shared')).catch(() => {});
    else navigator.clipboard?.writeText(text).then(() => { trackGame('word', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); });
  }

  // Load on mount and whenever the variant toggles. Tracks ONLY `variant` — it
  // must not depend on `loading`, or load()'s own loading flip would re-fire it.
  $effect(() => {
    variant;
    load();
  });
</script>

<svelte:window onkeydown={onKeydown} />

<div class="wordgame" class:ready>
  {#if loading}
    <p class="muted">Loading today’s puzzle…</p>
  {:else}
    <div class="play-area">
      <div class="board" style="--cols:{length}; --rows:{maxGuesses}">
        {#each Array(maxGuesses) as _, r (r)}
          {@const g = guesses[r]}
          {@const cs = cols[r] || null}
          <div class="row" class:shake={shake && r === guesses.length}>
            {#each Array(length) as _, c (c)}
              {@const ch = g ? g[c] : (r === guesses.length ? current[c] : '')}
              <div class="tile {cs ? cs[c] : ''}" class:filled={!!ch}
                   style={cs ? `--d:${c * 0.08}s` : ''}>{(ch || '').toUpperCase()}</div>
            {/each}
          </div>
        {/each}
      </div>

      {#if message}<p class="flash">{message}</p>{/if}

      {#if status !== 'playing'}
        <div class="result rise">
          <p class="rmark">{status === 'won' ? '✦ nicely done ✦' : 'today’s word:'}
            {#if status === 'lost' && answer}<strong>{answer.toUpperCase()}</strong>{/if}</p>
          {#if why}<p class="why"><span class="lbl">Why this word</span>{why}</p>{/if}
          {#if stats}
            <div class="stats">
              <div><span class="n">{stats.played}</span><span class="l">played</span></div>
              <div><span class="n">{stats.played ? Math.round(100 * stats.won / stats.played) : 0}%</span><span class="l">solved</span></div>
              <div><span class="n">{stats.streak}</span><span class="l">streak</span></div>
            </div>
          {/if}
          <button class="share" onclick={share}>{copied ? 'Copied!' : 'Share result'}</button>
        </div>
      {/if}
    </div>

    {#if status === 'playing'}
      <div class="kbzone">
        <div class="keyboard">
          <div class="letters">
            {#each ROWS as row, ri (ri)}
              <div class="krow">
                {#each row as k (k)}
                  <button class="key {keyState[k] || ''}" onclick={() => key(k)}>{k.toUpperCase()}</button>
                {/each}
              </div>
            {/each}
          </div>
          <div class="controls">
            <button class="key ctl back" onclick={() => key('back')} aria-label="Backspace">
              <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M20 5H8.4L2.5 12l5.9 7H20a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1z"/><path d="M11 9.5l5 5M16 9.5l-5 5"/></svg>
            </button>
            <button class="key ctl enter" onclick={() => key('enter')} aria-label="Enter">
              <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6v3.5a3 3 0 0 1-3 3H5"/><path d="M9 8.5l-4 4 4 4"/></svg>
            </button>
          </div>
        </div>
      </div>
    {/if}
  {/if}
</div>

<style>
  .wordgame { max-width: 480px; margin: 0 auto; opacity: 0; transform: translateY(6px); }
  .wordgame.ready { opacity: 1; transform: none; transition: opacity 0.3s ease, transform 0.3s ease; }
  .board { display: grid; gap: 6px; margin: 0 auto 18px; width: min(100%, 340px); }
  .row { display: grid; grid-template-columns: repeat(var(--cols), 1fr); gap: 6px; }
  .tile {
    aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
    border: 2px solid var(--line); border-radius: 8px; font-family: var(--label);
    font-weight: 700; font-size: 1.5rem; color: var(--ink); text-transform: uppercase;
    background: var(--surface); position: relative; overflow: hidden;
  }
  .tile.filled { border-color: #b7c0cb; }
  /* Judged tiles set like glazed enamel: a soft top-light gradient over the
     colour, an inner bevel, and a little lift off the board. Pending tiles stay
     flat on purpose — depth marks the moment a letter is settled. */
  .tile.correct, .tile.present, .tile.absent { color: #fff; text-shadow: 0 1px 2px rgba(20, 30, 25, 0.22); }
  .tile.correct {
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0) 45%),
                linear-gradient(165deg, #56ac7c, #4a9d6e 55%, #3e8a5e);
    border-color: #3e8a5e;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.32), inset 0 -3px 5px rgba(22, 64, 42, 0.2),
                0 2px 5px rgba(58, 125, 86, 0.28);
  }
  .tile.present {
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0) 45%),
                linear-gradient(165deg, #e2c163, #d8b24a 55%, #c29c38);
    border-color: #c29c38;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35), inset 0 -3px 5px rgba(122, 92, 22, 0.2),
                0 2px 5px rgba(184, 148, 58, 0.28);
  }
  .tile.absent {
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0) 45%),
                linear-gradient(165deg, #a7b2bd, #9aa6b2 55%, #87939f);
    border-color: #87939f;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.26), inset 0 -3px 5px rgba(50, 60, 70, 0.16),
                0 2px 4px rgba(110, 122, 134, 0.24);
  }
  /* One quiet glint sweeps each tile just after its flip lands — once, not a loop. */
  .tile.correct::after, .tile.present::after, .tile.absent::after {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, 0.38) 50%, transparent 62%);
    transform: translateX(-130%);
    animation: sheen 0.65s ease-out both;
    animation-delay: calc(var(--d, 0s) + 0.32s);
  }

  /* Juice: a tile pops as you type; the row reveals with a staggered bounce when
     you submit; the row shakes on an invalid word. */
  .tile.filled:not(.correct):not(.present):not(.absent) { animation: pop 0.13s ease; }
  .tile.correct, .tile.present, .tile.absent { animation: reveal 0.34s ease both; animation-delay: var(--d, 0s); }
  .row.shake { animation: shake 0.4s ease; }
  @keyframes pop { 0% { transform: scale(1); } 45% { transform: scale(1.09); } 100% { transform: scale(1); } }
  @keyframes reveal { 0% { transform: scale(0.5); opacity: 0.3; } 55% { transform: scale(1.12); } 100% { transform: scale(1); opacity: 1; } }
  @keyframes sheen { to { transform: translateX(130%); } }
  @keyframes shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }
  @media (prefers-reduced-motion: reduce) { .tile, .tile::after, .row { animation: none !important; } }
  .flash {
    text-align: center; background: var(--ink); color: #fff; border-radius: 8px;
    padding: 7px 14px; width: fit-content; margin: 0 auto 12px; font-size: 0.86rem;
  }
  /* Keyboard fills its area like a real phone game: a left block of square,
     alphabetical letter keys + a right column with Backspace (top) / Enter
     (bottom) tucked in. Flat, warm, on-brand keys — off-white, hairline border. */
  .kbzone { display: block; }
  .keyboard {
    --kw: min(44px, calc((100vw - 126px) / 10));   /* uniform key width; row 1 (10) fills */
    display: flex; align-items: stretch; gap: 6px; width: 100%; max-width: 470px; margin: 12px auto 0;
  }
  .letters { flex: 1; display: flex; flex-direction: column; gap: 6px; }
  .krow { display: flex; gap: 4px; justify-content: center; }       /* shorter rows auto-centre */
  .controls { display: grid; grid-template-rows: 1fr 1fr; gap: 6px; width: 44px; flex-shrink: 0; }
  .key {
    border: 1px solid var(--line); border-radius: 11px; background: var(--surface); color: var(--ink);
    font-family: var(--label); font-weight: 700; font-size: 1.05rem; cursor: pointer; text-transform: uppercase;
    padding: 0; box-shadow: 0 2px 0 rgba(120, 108, 84, 0.22), 0 3px 6px rgba(60, 50, 30, 0.07);
    transition: transform 0.06s ease, box-shadow 0.06s ease, background 0.12s ease, filter 0.12s ease;
  }
  .key:active { transform: translateY(2px); box-shadow: 0 0 0 rgba(0, 0, 0, 0); background: var(--bg); }
  .krow .key { width: var(--kw); height: 50px; }
  .controls .key { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
  .controls .key svg { width: 25px; height: 25px; display: block; }
  .controls .enter { background: var(--accent); border-color: var(--accent); color: #fff;
                     box-shadow: 0 2px 0 var(--accent-deep), 0 3px 6px rgba(0, 131, 173, 0.18); }
  .key.correct { background: #4a9d6e; border-color: #4a9d6e; color: #fff; box-shadow: 0 2px 0 #3a7d56; }
  .key.present { background: #d8b24a; border-color: #d8b24a; color: #fff; box-shadow: 0 2px 0 #b8943a; }
  .key.absent { background: #9aa6b2; border-color: #9aa6b2; color: #fff; box-shadow: 0 2px 0 #7e8a96; }
  .key:hover { filter: brightness(0.98); }
  .muted { color: var(--muted); text-align: center; }

  /* Mobile: a focused app screen — board sits up top and sizes to fit BOTH width
     (never overflows → no horizontal scroll) and the height left above the
     keyboard, so the active row + keyboard are always visible. */
  @media (max-width: 720px) {
    .wordgame { display: flex; flex-direction: column; height: 100%; max-width: 100%; }
    .play-area { flex: 0 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column;
                 justify-content: flex-start; padding: 8px 0 0; scrollbar-width: none; }
    .play-area::-webkit-scrollbar { display: none; }  /* no stray scrollbar "divider" */
    .board {
      /* budget covers page padding + inter-tile gaps so 6 letters never overflow */
      --tile: min(64px, calc((100vw - 64px) / var(--cols)), calc((100dvh - 300px) / var(--rows)));
      gap: 5px; width: fit-content; margin: 0 auto;
    }
    .row { grid-template-columns: repeat(var(--cols), var(--tile)); gap: 5px; }
    .tile { width: var(--tile); height: var(--tile); aspect-ratio: auto; font-size: calc(var(--tile) * 0.46); }
    /* Float the keyboard up, centered in the space below the board. */
    .kbzone { flex: 1; display: flex; align-items: center; justify-content: center; min-height: 0;
              padding-bottom: calc(env(safe-area-inset-bottom) + 4px); }
    .keyboard { --kw: min(44px, calc((100vw - 120px) / 10)); width: 100%; max-width: none; margin: 0; }
    .krow .key { height: 47px; }
  }
  .result { text-align: center; }
  .rmark { font-family: var(--serif); font-style: italic; color: var(--accent-deep); font-size: 1.2rem; margin: 0 0 10px; }
  .rmark strong { font-style: normal; letter-spacing: 0.06em; }
  .result .why { color: #3b4754; font-size: 0.95rem; margin: 0 auto 16px; max-width: 380px;
                 border-left: 2px solid var(--accent); padding-left: 12px; text-align: left; }
  .result .why .lbl { display: block; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.68rem;
                      color: var(--accent-deep); font-weight: 600; margin-bottom: 2px; }
  .stats { display: flex; justify-content: center; gap: 26px; margin: 0 0 18px; }
  .stats .n { display: block; font-size: 1.6rem; font-weight: 700; font-family: var(--label); }
  .stats .l { color: var(--muted); font-size: 0.78rem; }
  .share { background: var(--accent); color: #fff; border: none; border-radius: 999px;
           padding: 11px 26px; font: inherit; font-weight: 600; cursor: pointer; }
  .share:hover { background: var(--accent-deep); }
</style>
