<script>
  import { untrack } from 'svelte';
  import { pushGameState } from '$lib/gamesync.js';
  import { trackGame, gameShareUrl } from '$lib/analytics.js';
  import { COLOR_BY_KEY } from '$lib/games/match/palette.js';
  import { buildBoard, freeSeed } from '$lib/games/match/board.js';
  import MatchIcon from './MatchIcon.svelte';

  // mode: 'daily' (shared, synced, feeds the ritual) | 'free' (local-only, replayable)
  // format: 'icons' | 'colors'   ·   tier: 'gentle' | 'standard' | 'expert'
  let { mode = 'daily', format = 'icons', tier = 'standard', date = '', onstatus } = $props();

  let isFree = $derived(mode === 'free');
  let variant = $derived(`${tier}-${format}`);
  // Storage: daily keyed by the server date (cross-device synced); free keeps one
  // resumable scratch board per variant.
  let stateKey = $derived(isFree ? `goodnews:match:free:${variant}` : `goodnews:match:${variant}:${date}`);
  const ASSIST_KEY = 'goodnews:match:assist';

  let board = $state(null);
  let seed = $state('');
  let flipped = $state([]);          // card ids face-up, not yet resolved
  let matchedKeys = $state(new Set());
  let moves = $state(0);
  let locked = $state(false);        // brief hold while a mismatch is shown
  let ready = $state(false);
  let assist = $state(false);        // colorblind assist: show a tiny letter on swatches
  let celebrated = $state(false);

  let matchedIds = $derived(board ? new Set(board.cards.filter((c) => matchedKeys.has(c.key)).map((c) => c.id)) : new Set());
  let done = $derived(!!board && matchedKeys.size >= board.faces.length);

  function read(key) { try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; } }
  function write(key, v) { try { localStorage.setItem(key, JSON.stringify(v)); } catch { /* ignore */ } }

  function emitStatus() {
    onstatus?.({ count: matchedKeys.size, total: board?.faces.length ?? 0, moves, done });
  }

  function persist() {
    const snap = { matched: [...matchedKeys], moves, done, seed: isFree ? seed : undefined };
    write(stateKey, snap);
    emitStatus();
    // Daily resumes across devices; free play stays local.
    if (!isFree && date) pushGameState('match', variant, date, { matched: snap.matched, moves, done });
  }

  let loadSeq = 0;

  async function load(forceNew = false) {
    const myLoad = ++loadSeq;
    ready = false;
    // A pending mismatch flip-back must not fire onto a freshly switched board.
    if (flipTimer) { clearTimeout(flipTimer); flipTimer = null; }
    try { assist = localStorage.getItem(ASSIST_KEY) === '1'; } catch { /* ignore */ }

    const useFree = isFree;
    let saved, theSeed;
    if (useFree) {
      saved = forceNew ? null : read(stateKey);
      // a free board derives a stable seed so a reload resumes the same layout
      theSeed = saved?.seed || freeSeed((Date.now() ^ Math.floor(Math.random() * 1e9)) >>> 0);
    } else {
      theSeed = date;
      saved = read(stateKey);
      // pull/merge the server's copy first (cross-device resume)
      if (date) {
        const merged = await pushGameState('match', variant, date, {
          matched: saved?.matched || [], moves: saved?.moves || 0, done: !!saved?.done,
        });
        if (myLoad !== loadSeq) return;   // a newer board load started — drop this stale one
        if (merged) saved = merged;
      }
    }
    if (myLoad !== loadSeq) return;

    // Commit the new board + restored progress atomically AFTER any await, so a rapid
    // daily/free, icon/color, or tier switch can't hydrate an old response onto the
    // newer board.
    const b = buildBoard({ format, tier, seed: theSeed });
    const valid = new Set(b.faces);
    board = b;
    seed = theSeed;
    flipped = []; locked = false;
    matchedKeys = new Set((saved?.matched || []).filter((k) => valid.has(k)));
    moves = Number.isFinite(saved?.moves) ? saved.moves : 0;
    celebrated = matchedKeys.size >= b.faces.length;   // don't replay the celebration on resume
    // Persist immediately so a freshly-generated free board's seed survives a reload
    // even before the first move.
    write(stateKey, { matched: [...matchedKeys], moves, done: matchedKeys.size >= b.faces.length,
                      seed: useFree ? theSeed : undefined });
    ready = true;
    emitStatus();
  }

  function reset() { if (isFree) load(true); }

  function toggleAssist() {
    assist = !assist;
    try { localStorage.setItem(ASSIST_KEY, assist ? '1' : '0'); } catch { /* ignore */ }
  }

  let flipTimer = null;

  function flip(card) {
    if (!ready || locked || done) return;
    if (matchedKeys.has(card.key) || flipped.includes(card.id)) return;
    if (moves === 0 && flipped.length === 0 && matchedKeys.size === 0) trackGame('match', 'started');

    // Match off the LOCAL `next`, not the just-assigned reactive `flipped`, so the
    // evaluation never runs a click behind.
    const next = [...flipped, card.id];
    flipped = next;
    if (next.length < board.matchN) return;    // Expert needs all 3 before judging

    moves += 1;
    const keys = next.map((id) => board.cards[id].key);
    const allSame = keys.every((k) => k === keys[0]);
    if (allSame) {
      matchedKeys = new Set([...matchedKeys, keys[0]]);
      flipped = [];
      persist();
      if (matchedKeys.size >= board.faces.length && !celebrated) { celebrated = true; trackGame('match', 'completed'); }
    } else {
      locked = true;             // show the mismatch briefly, then flip back
      persist();
      flipTimer = setTimeout(() => { flipped = []; locked = false; flipTimer = null; }, 850);
    }
  }

  const TIER_LABEL = { gentle: 'Gentle', standard: 'Standard', expert: 'Expert' };
  let copied = $state(false);
  function share() {
    const label = `${TIER_LABEL[tier] || tier} · ${format === 'colors' ? 'colors' : 'icons'}`;
    const when = isFree ? 'Free play' : date;
    const text = `Upbeat Bytes · Memory Match (${label}) ${when}\nCleared in ${moves} moves\n${gameShareUrl('match', `${mode}-${format}-${tier}`)}`;
    if (navigator.share) navigator.share({ text }).then(() => trackGame('match', 'shared')).catch(() => {});
    else navigator.clipboard?.writeText(text).then(() => { trackGame('match', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); });
  }

  function faceLabel(card) {
    if (format === 'colors') return COLOR_BY_KEY[card.key]?.name ?? 'color';
    return card.key;
  }

  // Reload whenever the board identity (mode/format/tier/date, all encoded in
  // stateKey) changes. untrack() keeps the effect from tracking load()'s internal
  // state reads (board/matchedKeys/moves/done via emitStatus) — which load() also
  // writes — so a synchronous free-play load can't self-retrigger into a loop.
  $effect(() => {
    stateKey;
    untrack(() => load());
    return () => { if (flipTimer) clearTimeout(flipTimer); };
  });
</script>

<div class="match">
  <div class="topbar">
    <div class="meta">
      {#if board}
        <span class="tier">{board.label}{format === 'colors' ? ' · Color' : ''}</span>
        <span class="prog">{matchedKeys.size}/{board.faces.length}{moves ? ` · ${moves} moves` : ''}</span>
        <span class="rule" class:tri={board.matchN === 3}>{#if board.matchN === 3 && flipped.length > 0 && flipped.length < 3}Pick 3 · {flipped.length}/3{:else}{board.matchN === 3 ? 'Match 3 of a kind' : 'Match pairs'}{/if}</span>
      {/if}
    </div>
    <div class="controls">
      {#if format === 'colors'}
        <button class="ctl" aria-pressed={assist} onclick={toggleAssist}>Assist {assist ? 'on' : 'off'}</button>
      {/if}
      {#if isFree}
        <button class="ctl" onclick={reset}>New board</button>
      {/if}
    </div>
  </div>

  {#if board}
    <div class="grid" class:locked
         style="grid-template-columns: repeat({board.cols}, 1fr); max-width: {board.cols * 76}px;">
      {#each board.cards as card (card.id)}
        {@const up = matchedIds.has(card.id) || flipped.includes(card.id)}
        {@const matched = matchedIds.has(card.id)}
        <button class="card" class:up class:matched
                aria-label={up ? faceLabel(card) : 'card, face down'}
                disabled={matched}
                onclick={() => flip(card)}>
          <span class="inner">
            <span class="back" aria-hidden="true"></span>
            <span class="front" class:colored={format === 'colors'} aria-hidden="true">
              {#if format === 'colors'}
                <span class="colorfill" style="--c: {COLOR_BY_KEY[card.key]?.hex}">
                  {#if assist}<span class="assist">{COLOR_BY_KEY[card.key]?.assist}</span>{/if}
                </span>
              {:else}
                <MatchIcon name={card.key} />
              {/if}
              {#if matched}<span class="check" aria-hidden="true">✓</span>{/if}
            </span>
          </span>
        </button>
      {/each}
    </div>

    {#if done && celebrated}
      <p class="done">Lovely — you cleared {isFree ? 'the board' : "today's set"}.{isFree ? '' : ' Fresh one tomorrow.'}</p>
      <button class="share" onclick={share}>{copied ? 'Copied!' : 'Share result'}</button>
    {/if}
  {/if}
</div>

<style>
  .match { display: flex; flex-direction: column; align-items: center; gap: 14px; }
  .topbar { width: 100%; max-width: 460px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  .meta { display: flex; flex-direction: column; gap: 1px; }
  .tier { font-family: var(--label); font-weight: 600; }
  .prog { color: var(--muted); font-size: 0.85rem; font-variant-numeric: tabular-nums; }
  .rule { font-size: 0.8rem; color: var(--muted); margin-top: 2px; }
  /* Expert's 3-of-a-kind rule is the easy-to-forget one — give it a clear chip. */
  .rule.tri { align-self: flex-start; color: var(--accent-deep); background: var(--accent-soft);
              padding: 1px 9px; border-radius: 999px; font-weight: 600; }
  .controls { display: flex; gap: 8px; }
  .ctl { font-size: 0.8rem; padding: 5px 11px; border: 1px solid var(--line); border-radius: 9px;
         background: var(--surface); color: var(--accent-deep); cursor: pointer; }

  .grid { display: grid; gap: 8px; width: 100%; }
  .card { aspect-ratio: 1; min-width: 0; padding: 0; border: none; background: none; cursor: pointer;
          perspective: 600px; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
  .card[disabled] { cursor: default; }
  .inner { position: relative; display: block; width: 100%; height: 100%;
           transition: transform 0.32s ease; transform-style: preserve-3d; }
  .card.up .inner { transform: rotateY(180deg); }
  .back, .front { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
                  border-radius: 13px; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
  /* Face-down: a branded card back — azure gradient under a faint dot motif. */
  .back { background:
            radial-gradient(circle at 50% 50%, rgba(255,255,255,0.18) 1.2px, transparent 1.6px) 0 0 / 11px 11px,
            linear-gradient(150deg, var(--accent) 0%, var(--accent-deep) 100%);
          box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18); }
  .front { background: var(--surface); border: 1px solid var(--line); color: var(--accent-deep);
           transform: rotateY(180deg); }
  .front.colored { background: none; border: none; }   /* the colorfill is the whole face */
  /* Icon front-and-center: ~70% of the card (≈50% area) so the symbol is memorable. */
  .front :global(svg) { width: 70%; height: 70%; }
  .colorfill { position: absolute; inset: 0; border-radius: 13px; background: var(--c);
               border: 2px solid #3a3a3a; }
  .assist { position: absolute; top: 5px; right: 6px; font-family: var(--label); font-weight: 700;
            font-size: 0.72rem; color: #fff; background: rgba(0, 0, 0, 0.45); padding: 1px 5px; border-radius: 6px; }

  /* State hierarchy ----------------------------------------------------------
     Inspecting (flipped, unmatched): vivid + lifted, so the unknown cards pop. */
  .card.up:not(.matched) .front:not(.colored) { box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
  .card.up:not(.matched) .colorfill { box-shadow: 0 2px 8px rgba(0,0,0,0.20); }
  /* Matched: a settled "paint-chip" tile — receded, desaturated, with a quiet check,
     so the last few unknown cards stay obvious instead of blending in. */
  .card.matched .inner { transform: rotateY(180deg) scale(0.9); }
  .card.matched .front { box-shadow: none; }
  .card.matched .front.colored { background: var(--surface); border: 1px solid var(--line); }
  .card.matched .colorfill { inset: 5px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.22);
                             filter: saturate(0.6) brightness(1.06); box-shadow: none; }
  .card.matched :global(svg) { opacity: 0.42; }
  .card.matched .assist { display: none; }
  .check { position: absolute; bottom: 4px; right: 6px; font-size: 0.82rem; line-height: 1;
           color: var(--accent-deep); font-weight: 700; }

  .done { margin: 4px 0 0; color: var(--accent-deep); font-family: var(--label); text-align: center; }
  .share { margin-top: 10px; background: var(--accent); color: #fff; border: none; border-radius: 999px;
           padding: 9px 22px; font: inherit; font-weight: 600; cursor: pointer; }
  .share:hover { background: var(--accent-deep); }

  @media (prefers-reduced-motion: reduce) {
    .inner { transition: none; }
  }
</style>
