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

  let { size = 'med', onstatus } = $props();

  // A calm, distinct palette — each found word gets its own colour.
  const PALETTE = ['#a7d3c2', '#f4cf88', '#a9c8ec', '#eab0c0', '#cdb8e6',
                   '#f3a98a', '#c2d99a', '#e3b0d0', '#9fd0d8', '#d9c79a'];

  let theme = $state('');
  let words = $state([]);
  let grid = $state([]);            // array of row strings
  let date = $state('');
  let foundWords = $state([]);      // {word, cells:[[r,c]], ci}
  let sel = $state([]);             // current selection cells
  let selecting = false;
  let playedMs = 0;                 // accumulated ACTIVE play time (closed segments)
  let segStart = 0;                 // wall-clock start of the open segment (0 = paused)
  let resultMs = $state(0);
  let best = $state(0);
  let loading = $state(true);
  let ready = $state(false);
  let okFlash = $state(false);
  let copied = $state(false);
  let gridEl = $state(null);
  let loadSeq = 0;

  const n = $derived(grid.length);
  const stateKey = $derived(`goodnews:wordsearch:${size}:${date}`);
  const bestKey = $derived(`goodnews:wordsearch:best:${size}`);
  const found = $derived(foundWords.map((w) => w.word));
  const status = $derived(words.length && foundWords.length === words.length ? 'done' : 'playing');
  const selSet = $derived(new Set(sel.map(([r, c]) => r + ',' + c)));
  const cellColor = $derived.by(() => {
    const m = new Map();                 // "r,c" -> [colour index, ...] (all words covering it)
    for (const w of foundWords) for (const [r, c] of w.cells) {
      const k = r + ',' + c;
      if (!m.has(k)) m.set(k, []);
      m.get(k).push(w.ci);
    }
    return m;
  });

  // Multiply-blend the palette colours of every word covering a cell, so where
  // words cross the cell DEEPENS into a darker shade instead of one colour
  // stomping the others. Single-word cells keep their plain pastel.
  function cellStyle(indices) {
    let r = 255, g = 255, b = 255;
    for (const ci of indices) {
      const h = PALETTE[ci];
      r = (r * parseInt(h.slice(1, 3), 16)) / 255;
      g = (g * parseInt(h.slice(3, 5), 16)) / 255;
      b = (b * parseInt(h.slice(5, 7), 16)) / 255;
    }
    r = Math.round(r); g = Math.round(g); b = Math.round(b);
    const lum = 0.299 * r + 0.587 * g + 0.114 * b;   // keep text readable as cells darken
    return `background:rgb(${r},${g},${b});color:${lum < 140 ? '#fff' : '#2a2f36'}`;
  }
  const wordColor = $derived.by(() => {
    const m = new Map();
    for (const w of foundWords) m.set(w.word, w.ci);
    return m;
  });

  // --- the clock counts ACTIVE play only -----------------------------------
  // Wall-clock timing made "finish in one sitting" feel mandatory — the
  // opposite of calm. The clock runs only while the puzzle is on screen
  // (tab visible, window focused, game unfinished); stepping away pauses it,
  // coming back resumes it, and several sittings simply add up.
  function playedNow() { return playedMs + (segStart ? Date.now() - segStart : 0); }
  function pauseClock(save = true) {
    if (!segStart) return;
    playedMs += Date.now() - segStart; segStart = 0;
    if (save) persist();                  // don't lose the segment if the tab dies
  }
  function resumeClock() {
    if (!segStart && !loading && status === 'playing' && !document.hidden) segStart = Date.now();
  }
  $effect(() => {
    const onVis = () => (document.hidden ? pauseClock() : resumeClock());
    const onAway = () => pauseClock();
    document.addEventListener('visibilitychange', onVis);
    window.addEventListener('pagehide', onAway);
    window.addEventListener('blur', onAway);
    window.addEventListener('focus', onVis);
    return () => {
      document.removeEventListener('visibilitychange', onVis);
      window.removeEventListener('pagehide', onAway);
      window.removeEventListener('blur', onAway);
      window.removeEventListener('focus', onVis);
    };
  });

  async function load() {
    const seq = ++loadSeq;                 // stale-load guard for rapid size switches
    loading = true; ready = false;
    foundWords = []; sel = []; resultMs = 0; playedMs = 0; segStart = 0;
    try {
      const p = await getJSON('/api/puzzle/wordsearch?variant=' + size);
      if (seq !== loadSeq) return;         // a newer size was selected — abandon
      theme = p.theme; words = p.words; grid = p.grid; date = p.date;
      restore();
      try { best = JSON.parse(localStorage.getItem(bestKey) || '0'); } catch { best = 0; }
    } catch {
      if (seq !== loadSeq) return;
      theme = ''; words = [];
    }
    if (seq !== loadSeq) return;
    loading = false;
    resumeClock();
    requestAnimationFrame(() => (ready = true));
    // Reconcile with the server in the background (signed-in only): pull any
    // progress from another device, and pull the cross-device best time.
    syncNow();
    fetchGameStats('wordsearch', size).then((st) => {
      if (st && st.best && (!best || st.best < best)) best = st.best;
    });
  }

  // Keep only finds whose cells still spell their word in the CURRENT grid —
  // guards stale highlights if the puzzle/layout changed (and validates finds
  // arriving from another device, which is the same date+size grid).
  function validFinds(list) {
    return (list || []).filter((fw) =>
      fw && Array.isArray(fw.cells) && words.includes(fw.word) &&
      fw.cells.map(([r, c]) => (grid[r] && grid[r][c]) || '').join('') === fw.word);
  }

  function restore() {
    try {
      const s = JSON.parse(localStorage.getItem(stateKey) || 'null');
      if (s && Array.isArray(s.foundWords)) {
        foundWords = validFinds(s.foundWords);
        playedMs = s.played || 0;          // pre-"active clock" saves restart at 0:00 — the kind direction
        resultMs = foundWords.length === words.length ? (s.ms || 0) : 0;
      }
    } catch { /* ignore */ }
    onstatus?.(summary());
  }
  function persist() {
    try { localStorage.setItem(stateKey, JSON.stringify({ foundWords, played: playedNow(), ms: resultMs, status })); }
    catch { /* ignore */ }
    onstatus?.(summary());
  }

  // --- cross-device sync (signed-in only; merged server-side) ---
  let syncTimer;
  function adopt(merged) {
    if (!merged) return;
    const have = new Set(foundWords.map((w) => w.word));
    for (const fw of validFinds(merged.foundWords)) {
      if (!have.has(fw.word)) { foundWords = [...foundWords, fw]; have.add(fw.word); }
    }
    // renumber colours by find order so overlap blends stay consistent
    foundWords = foundWords.map((fw, i) => ({ ...fw, ci: i % PALETTE.length }));
    // another device may have accumulated more active time — credit the larger
    if ((merged.played || 0) > playedNow()) {
      playedMs = merged.played;
      if (segStart) segStart = Date.now();
    }
    if (foundWords.length === words.length && merged.ms) resultMs = Math.min(resultMs || merged.ms, merged.ms);
    persist();
  }
  async function syncNow() {
    const d = date, sz = size;                      // pin against a size switch mid-flight
    const merged = await pushGameState('wordsearch', sz, d, { foundWords, played: playedNow(), ms: resultMs });
    if (d === date && sz === size) adopt(merged);   // ignore if the user switched away
  }
  function syncSoon() { clearTimeout(syncTimer); syncTimer = setTimeout(syncNow, 1200); }
  function summary() { return { game: 'wordsearch', size, date, status, found: foundWords.length, total: words.length, ms: resultMs }; }

  function cellAt(e) {
    return cellFromPoint(gridEl.getBoundingClientRect(), e.clientX, e.clientY, n);
  }

  function down(e) {
    if (status === 'done') return;
    selecting = true;
    resumeClock();                         // safety net if a focus event was missed
    sel = [cellAt(e)];
    gridEl.setPointerCapture?.(e.pointerId);
    e.preventDefault();
  }
  function move(e) {
    if (!selecting) return;
    sel = lineFrom(sel[0], cellAt(e), n);
  }
  function up() {
    if (!selecting) return;
    selecting = false;
    evaluate(sel);
    sel = [];
  }

  function evaluate(cells) {
    const hit = matchWord(cells, grid, words, found);
    if (!hit) return;
    if (foundWords.length === 0) trackGame('wordsearch', 'started');   // first word found = started
    foundWords = [...foundWords, { word: hit, cells: cells.map((c) => [...c]), ci: foundWords.length % PALETTE.length }];
    okFlash = true; setTimeout(() => (okFlash = false), 500);
    if (foundWords.length === words.length) finish();
    persist();
    syncSoon();   // push this find (and completion) to the server, debounced
  }

  function finish() {
    pauseClock(false);                     // close the open segment; persist follows in evaluate()
    trackGame('wordsearch', 'completed');
    resultMs = playedMs;
    if (resultMs && (!best || resultMs < best)) {
      best = resultMs;
      try { localStorage.setItem(bestKey, JSON.stringify(best)); } catch { /* ignore */ }
    }
  }

  function fmt(ms) {
    const s = Math.round(ms / 1000);
    return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
  }

  function share() {
    const label = { small: 'Small', med: 'Medium', large: 'Large' }[size] || '';
    const text = `Upbeat Bytes · Word Search (${label}) ${date}\n${theme} — cleared in ${fmt(resultMs)}\n${gameShareUrl('wordsearch', size)}`;
    if (navigator.share) navigator.share({ text }).then(() => trackGame('wordsearch', 'shared')).catch(() => {});
    else navigator.clipboard?.writeText(text).then(() => { trackGame('wordsearch', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); });
  }

  // Load on mount and whenever the size changes. Tracks ONLY `size`.
  $effect(() => { size; load(); });
</script>

<div class="wordsearch" class:ready>
  {#if loading}
    <p class="muted">Loading today’s word search…</p>
  {:else if !words.length}
    <p class="muted">Could not load today’s word search.</p>
  {:else}
    <p class="theme">{theme}</p>

    <div class="gridwrap">
      <div class="grid" class:ok={okFlash} class:done={status === 'done'} bind:this={gridEl} style="--n:{n}"
           role="application" aria-label="Word search grid — drag across letters to find words"
           onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
        {#each grid as rowStr, r (r)}
          {#each rowStr.split('') as ch, c (c)}
            {@const key = r + ',' + c}
            <div class="cell" class:sel={selSet.has(key)}
                 style={cellColor.has(key) && !selSet.has(key) ? cellStyle(cellColor.get(key)) : ''}>{ch}</div>
          {/each}
        {/each}
      </div>
    </div>

    <div class="palette">
      <p class="plabel">Find these · {foundWords.length}/{words.length}</p>
      <ul class="words">
        {#each words as w (w)}
          <li class:got={found.includes(w)}
              style={wordColor.has(w) ? `background:${PALETTE[wordColor.get(w)]};color:#2a2f36` : ''}>{w}</li>
        {/each}
      </ul>
    </div>

    {#if status === 'done'}
      <div class="result rise">
        <p class="rmark">✦ all found ✦</p>
        <div class="times">
          <div><span class="n">{fmt(resultMs)}</span><span class="l">your time</span></div>
          {#if best}<div><span class="n">{fmt(best)}</span><span class="l">best</span></div>{/if}
        </div>
        <button class="share" onclick={share}>{copied ? 'Copied!' : 'Share result'}</button>
      </div>
    {:else}
      <p class="hint">{foundWords.length} / {words.length} found · drag across the letters</p>
    {/if}
  {/if}
</div>

<style>
  .wordsearch { max-width: 520px; margin: 0 auto; opacity: 0; transform: translateY(6px); }
  .wordsearch.ready { opacity: 1; transform: none; transition: opacity 0.3s ease, transform 0.3s ease; }
  .theme { text-align: center; font-family: var(--serif); font-weight: 600; font-size: 1.6rem;
           color: var(--accent-deep); margin: 0 0 14px; }
  .gridwrap { display: flex; justify-content: center; }
  .grid {
    /* Cells are ~32px on a wide screen (board grows with the grid) and shrink via
       1fr to fit a narrow phone — capped by max-width so it can never overflow. */
    display: grid; grid-template-columns: repeat(var(--n), 1fr); gap: 2px;
    width: 100%; max-width: calc(var(--n) * 32px + 16px); margin: 0 auto 16px;
    touch-action: none; user-select: none; -webkit-user-select: none;
    border-radius: 10px; padding: 6px; background: var(--surface); border: 1px solid var(--line);
  }
  .grid.done { opacity: 0.9; }
  .cell {
    aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
    font-family: var(--label); font-weight: 600; font-size: clamp(0.7rem, 2.6vw, 1.05rem);
    color: var(--ink); border-radius: 5px; background: transparent; text-transform: uppercase;
  }
  .cell.sel { background: var(--accent) !important; color: #fff !important; }
  /* Scale each letter to ~42% of its cell so the letter-to-spacing ratio is uniform
     across Small/Med/Large (Large is the reference). Guarded so older browsers keep
     the clamp() fallback above. */
  @supports (width: 1cqw) {
    .grid { container-type: inline-size; }
    .cell { font-size: calc(100cqw / var(--n) * 0.42); }
  }
  .palette { background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
             padding: 12px 14px 14px; margin: 0 auto 16px; max-width: 440px; box-shadow: var(--shadow); }
  .plabel { text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.62rem; font-family: var(--label);
            color: var(--muted); text-align: center; margin: 0 0 10px; }
  .words { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; padding: 0; margin: 0; }
  /* Unfound words are plain (transparent + outline) so it's easy to see what's
     left as the board fills; found words get their grid colour via inline style. */
  .words li { font-family: var(--label); font-size: 0.82rem; letter-spacing: 0.04em; color: var(--ink);
              padding: 4px 11px; border-radius: 999px; background: transparent;
              border: 1px solid var(--line); transition: background 0.2s ease, border-color 0.2s ease; }
  .words li.got { border-color: transparent; }
  .hint { text-align: center; color: var(--muted); font-size: 0.84rem; margin: 0; }
  .result { text-align: center; }
  .rmark { font-family: var(--serif); font-style: italic; color: var(--accent-deep); font-size: 1.2rem; margin: 0 0 12px; }
  .times { display: flex; justify-content: center; gap: 26px; margin: 0 0 16px; }
  .times .n { display: block; font-size: 1.5rem; font-weight: 700; font-family: var(--label); }
  .times .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); }
  .muted { color: var(--muted); text-align: center; }

  /* Mobile: a focused game screen (matches Daily Word). The grid sizes to the
     largest square that fits the space left between the theme and the palette,
     so the whole puzzle is on screen with no page scroll. */
  @media (max-width: 720px) {
    .wordsearch { display: flex; flex-direction: column; height: 100%; max-width: 100%; }
    .theme { flex-shrink: 0; font-size: 1.5rem; margin: 4px 0 8px; }
    .gridwrap { flex: 1; min-height: 0; container-type: size; align-items: center; padding: 2px 0; }
    .grid { width: min(100%, 100cqh, calc(var(--n) * 40px)); max-width: none; margin: 0; }
    .palette { flex-shrink: 0; margin: 10px auto 0; padding: 9px 12px 10px; max-width: 100%;
               width: 100%; box-sizing: border-box; }
    .plabel { margin-bottom: 7px; }
    .words { gap: 6px; }
    .words li { font-size: 0.78rem; padding: 3px 9px; }
    .hint { flex-shrink: 0; padding: 9px 0 calc(env(safe-area-inset-bottom) + 6px); }
    .result { flex-shrink: 0; padding-bottom: calc(env(safe-area-inset-bottom) + 6px); }
  }
</style>
