/*
 * game.jsx — Game 1: see a photo, pick the right Hebrew letter.
 *
 * Three letter buttons in an RTL row. correctPosition: 0 = rightmost.
 *
 * Two on-screen affordances, deliberately SEPARATE visual systems so the
 * 4-year-old reading them can't confuse one for the other:
 *
 *   1. INVITATION (always on) — "the letters are tappable".
 *      Right after the round renders, the three buttons play a short
 *      piano-ripple: each lifts ~8px in sequence right→left (so it reads
 *      naturally in RTL), one after the other. The ripple replays every
 *      15s as a gentle re-invitation. It is NOT a press simulation — the
 *      lift is small, no glyph leaves the button, no background changes,
 *      and the buttons never go through the wrong/correct visual state.
 *      Cancels when the kid first interacts (no need to keep nudging).
 *
 *   2. HINT (escalates if the kid stalls) — "if you're stuck, here's a
 *      narrowing down".
 *      After 10s of no input: ONE wrong button is marked with two thin
 *      red diagonal crossing lines. After 20s: the OTHER wrong button is
 *      marked the same way. After 30s: the correct button gets a soft
 *      green glow + faint sparkles. These marks PERSIST until the kid
 *      taps (they're decoration on the button, not transient feedback).
 *      They do not trigger the press / wrong / correct animations —
 *      those fire only on real interaction.
 *
 * REAL presses commit on pointerdown (no need to release), wrong shows
 * an X + red shake on the button, correct flies the letter from its
 * button into the slot in 1.2s and fills the slot green. Those are the
 * locked-in real-interaction behaviors; the two systems above are
 * pre-interaction help only.
 */

const TWEAK_DEFAULTS = window.TWEAK_DEFAULTS;

// Real-press timings. `fly-letter` CSS keyframe duration must equal FLIGHT_MS.
const FLIGHT_MS         = 1200;
const WRONG_REVEAL_MS   = 900;
const CORRECT_HOLD_MS   = 600;

// Invitation + hint timings. Defaults are tuned for a real kid; tests
// override them with URL params (e.g. ?hint1=200&hint2=400&hint3=600
// &invite=800) so a 30s wait collapses to under a second of real time.
// Production never sets these params, so the defaults always apply live.
const _qs = new URLSearchParams(window.location.search);
const _ms = (key, def) => {
  const v = _qs.get(key);
  return v === null || v === "" ? def : Math.max(0, +v) || def;
};
const INVITE_INTERVAL_MS   = _ms("invite", 15000);
const INVITE_STAGGER_MS    = 130;
const INVITE_LIFT_PX       = 9;
const INVITE_LIFT_MS       = 360;
// Idle-cue timing (HINT_STEPS / HINT_CYCLE_MS) + the cue mapping
// (hintCueFor) are shared from game-shared.jsx — the one pattern, ADR 0028.

function buildRow(round, pool) {
  const row = new Array(3).fill(null);
  row[round.correctPosition] = round.letter;
  const distractors = pickDistractors(pool, round.letter, 2);
  let di = 0;
  for (let i = 0; i < row.length; i++) if (row[i] === null) row[i] = distractors[di++];
  return row;
}

function wrongIndices(round) {
  const c = round.correctPosition;
  return [0, 1, 2].filter((i) => i !== c);
}

// ── Word display: slot + remaining letters ───────────────────────────────
const SUCCESS_SPARK_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315];

const WordSlots = React.forwardRef(function WordSlots({ word, status, filledLetter }, slotRef) {
  const tail = word.slice(1);
  return (
    <div className="word-slots" aria-hidden="true">
      <span
        ref={slotRef}
        className="slot-target"
        data-state={status === "correct" ? "correct" : "idle"}
      >
        {filledLetter && (
          <span key={filledLetter} className="slot-filled-letter">{filledLetter}</span>
        )}
        {status === "correct" && (
          <span className="success-burst" aria-hidden="true">
            {SUCCESS_SPARK_ANGLES.map((a) => (
              <span key={a} className="success-spark" style={{ "--angle": `${a}deg` }} />
            ))}
          </span>
        )}
      </span>
      {tail && <span className="slot-tail">{tail}</span>}
    </div>
  );
});

// ── Letter row ───────────────────────────────────────────────────────────
// `hintFor(i)` returns "wrong" | "correct" | null based on the current
// hintLevel + which positions are wrong/correct for this round.
function Buttons({ row, letterNames, status, wrongIdx, hiddenIdx, hintFor, onPick, buttonRefs }) {
  return (
    <div className="buttons" role="group" aria-label="בחר את האות">
      {row.map((letter, i) => {
        const isWrong  = status === "wrong" && wrongIdx === i;
        const isHidden = hiddenIdx === i;
        const disabled = status === "correct";
        // Hints only visible in idle state — once the kid presses, the
        // wrong/correct feedback owns the button.
        const hint = status === "idle" ? hintFor(i) : null;
        return (
          <button
            key={i + ":" + letter}
            ref={(el) => { buttonRefs.current[i] = el; }}
            type="button"
            className="btn"
            data-wrong={isWrong ? "1" : "0"}
            data-disabled={disabled ? "1" : "0"}
            data-hint={hint || "none"}
            aria-label={letterNames[letter] || letter}
            onPointerDown={(e) => { e.preventDefault(); onPick(i, letter); }}
          >
            <span className="glyph" style={{ opacity: isHidden ? 0 : undefined }}>{letter}</span>
            <span className="x-mark" aria-hidden="true">
              <svg viewBox="0 0 60 60" width="56" height="56">
                <path d="M16 16 L44 44 M44 16 L16 44" stroke="#fff" strokeWidth="7" strokeLinecap="round" fill="none" />
              </svg>
            </span>
            <span className="hint-cross" aria-hidden="true">
              <svg viewBox="0 0 60 60" preserveAspectRatio="none">
                <path d="M10 10 L50 50" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
                <path d="M50 10 L10 50" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
              </svg>
            </span>
            <span className="hint-sparkles" aria-hidden="true">
              <span className="hint-sparkle" style={{ left: "10%", top: "14%" }} />
              <span className="hint-sparkle" style={{ left: "82%", top: "18%" }} />
              <span className="hint-sparkle" style={{ left: "18%", top: "80%" }} />
              <span className="hint-sparkle" style={{ left: "78%", top: "82%" }} />
            </span>
          </button>
        );
      })}
    </div>
  );
}

// FlyingLetter (button → slot) now lives in shared/game-shared.jsx so other
// games can reuse the baseline COMMIT flight (docs/game-baseline.md).

// ── App ──────────────────────────────────────────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  useApplyTweaks(t);

  const { rounds, letterPool, letterNames } = window.GAME_CONFIG;

  // Resume from where the kid stopped last time, scoped by (theme, n).
  // window.PROGRESS.load returns null if nothing's saved or if the saved
  // round is out of range for the current deck.
  //
  // v1.36: bump lastBreak so the resume lands DIRECTLY in the saved
  // round's letter rather than re-prompting on the break screen. If the
  // kid stopped at a multiple-of-5 round (e.g. 5, 10), the natural break
  // would otherwise re-fire on mount and require another "continue" tap.
  // We accept the trade-off that an intentionally-stopped-on-the-break
  // session won't see its recap again; the next batch will still break.
  const saved = (typeof window !== "undefined" && window.PROGRESS) ? window.PROGRESS.load("letter") : null;
  const initialRound = saved ? saved.roundIdx : 0;
  const initialFirstTries = saved ? saved.firstTries : [];
  const initialLastBreak = saved ? Math.max(saved.lastBreak, Math.floor(saved.roundIdx / 5)) : 0;

  const [roundIdx, setRoundIdx] = React.useState(initialRound);
  const [row, setRow]           = React.useState(() => buildRow(rounds[initialRound], letterPool));
  const [status, setStatus]     = React.useState("idle");
  const [wrongIdx, setWrongIdx] = React.useState(-1);
  const [flying, setFlying]     = React.useState(null);
  const [hiddenIdx, setHidden]  = React.useState(-1);
  const [done, setDone]         = React.useState(false);
  const [lastBreak, setLastBreak] = React.useState(initialLastBreak);

  // Invitation + hint state. `engaged` flips on the first real press of a
  // round; both systems halt as soon as it flips. It resets when the round
  // (or the game) restarts.
  const [engaged, setEngaged]       = React.useState(false);
  const [inviteCycle, setInviteCycle] = React.useState(0);

  // Per-round "first try clean" flag — true when the kid solved without
  // any wrong tap and without any hint having fired. Pushed on correct
  // tap; rendered as gold/outline stars on the break screen. Restored
  // from progress so the star count survives a stop-and-resume.
  const [firstTries, setFirstTries] = React.useState(initialFirstTries);
  const roundCleanRef = React.useRef(true);

  const buttonRefs = React.useRef({});
  const slotRef    = React.useRef(null);

  const breakAt = (roundIdx > 0 && roundIdx % 5 === 0) ? Math.floor(roundIdx / 5) : 0;
  const showBreak = breakAt > 0 && lastBreak < breakAt && !done && status === "idle";
  const batchRounds = showBreak ? rounds.slice(roundIdx - 5, roundIdx) : [];

  // Accelerated, repeating hint cycle (docs/game-baseline.md): level 1/2 fade
  // the two wrong buttons (@5s/@10s), level 3 glitters the correct (@15s),
  // repeating every 20s while the kid is idle.
  const hintLevel = useHintCycle(
    !done && !showBreak && !engaged && status === "idle",
    HINT_STEPS, HINT_CYCLE_MS
  );

  const round = rounds[roundIdx];
  const filledLetter = status === "correct" ? round.letter : null;

  const advance = React.useCallback(() => {
    if (roundIdx + 1 >= rounds.length) { setDone(true); return; }
    const next = roundIdx + 1;
    setRoundIdx(next);
    setRow(buildRow(rounds[next], letterPool));
    setStatus("idle");
    setWrongIdx(-1);
    setHidden(-1);
    setFlying(null);
    setEngaged(false);
    setInviteCycle(0);
    roundCleanRef.current = true;
  }, [roundIdx, rounds, letterPool]);

  const restart = () => {
    setDone(false);
    setRoundIdx(0);
    setRow(buildRow(rounds[0], letterPool));
    setStatus("idle");
    setWrongIdx(-1);
    setHidden(-1);
    setFlying(null);
    setLastBreak(0);
    setEngaged(false);
    setInviteCycle(0);
    setFirstTries([]);
    roundCleanRef.current = true;
  };

  // ── System 1: INVITATION. Plays a piano ripple at round mount, then
  //    every 15s until the kid interacts. Triggers on inviteCycle bumps;
  //    Web Animations API plays the lift without fighting React's render.
  //    Suppressed once hintLevel > 0 — the hint visuals already pull the
  //    eye, so layering invitation on top would compound motion noise.
  React.useEffect(() => {
    if (done || showBreak || engaged || hintLevel > 0) return;
    // Initial cue fires shortly after mount (300 ms) so the buttons are
    // laid out and React has settled.
    const t0 = window.setTimeout(() => setInviteCycle((c) => c + 1), 300);
    const tid = window.setInterval(() => setInviteCycle((c) => c + 1), INVITE_INTERVAL_MS);
    return () => { clearTimeout(t0); clearInterval(tid); };
  }, [roundIdx, done, showBreak, engaged, hintLevel]);

  React.useEffect(() => {
    if (inviteCycle === 0) return;
    // Respect prefers-reduced-motion — CSS @media rules don't reach
    // Element.animate() so we have to opt out manually here.
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    // Play the ripple right→left = button index 0 → 1 → 2 (button[0] is
    // rightmost in RTL). Each button lifts INVITE_LIFT_PX and returns.
    for (let i = 0; i < 3; i++) {
      const btn = buttonRefs.current[i];
      if (!btn) continue;
      btn.animate(
        [
          { transform: "translateY(0)" },
          { transform: `translateY(-${INVITE_LIFT_PX}px)` },
          { transform: "translateY(0)" },
        ],
        {
          duration: INVITE_LIFT_MS,
          delay: i * INVITE_STAGGER_MS,
          easing: "cubic-bezier(.2,.9,.3,1.1)",
        }
      );
    }
  }, [inviteCycle]);

  // System 2: HINTS now come from the shared useHintCycle (above) — the
  // accelerated, repeating 5/10/15s escalation defined in game-baseline.md.

  // Debug hook — lets tests verify the state machine without inspecting
  // the React tree. Cheap; remove or guard once the system stabilizes.
  React.useEffect(() => {
    document.documentElement.setAttribute("data-hint-level", String(hintLevel));
    document.documentElement.setAttribute("data-engaged", String(engaged));
  }, [hintLevel, engaged]);

  // Map button-index → which hint (if any) applies right now.
  // Unified idle cue (ADR 0028): the shared helper owns the mapping. 3
  // buttons → fewOptions, so t+5 fades only the first wrong.
  const hintFor = React.useCallback((i) => {
    const [w1] = wrongIndices(round);
    return hintCueFor(hintLevel, {
      correct: i === round.correctPosition,
      firstWrong: i === w1,
      fewOptions: true,
    });
  }, [hintLevel, round]);

  // Any hint having fired counts against first-try cleanliness too.
  React.useEffect(() => {
    if (hintLevel > 0) roundCleanRef.current = false;
  }, [hintLevel]);

  // Fire one-shot break/end sounds when those overlays open.
  React.useEffect(() => { if (showBreak && window.SOUNDS) window.SOUNDS.play("break"); }, [showBreak]);
  React.useEffect(() => { if (done && window.SOUNDS) window.SOUNDS.play("end"); }, [done]);

  // Persist resume-state on every advance. Clears when done (round
  // overflow → load() returns null next time → fresh deck).
  React.useEffect(() => {
    if (!window.PROGRESS) return;
    if (done) { window.PROGRESS.clear("letter"); return; }
    window.PROGRESS.save("letter", { roundIdx, firstTries, lastBreak });
  }, [roundIdx, firstTries, lastBreak, done]);

  // ── Real user press — pointerdown commits. Cancels both invitation
  //    (engaged flips) and hint (cleared via engaged flag).
  const onPick = (i, letter) => {
    if (!engaged) setEngaged(true);
    // Mid-feedback debounce so a double-tap can't restart the wrong-shake.
    if (engaged && status !== "idle") return;
    if (window.SOUNDS) window.SOUNDS.play("tap");

    if (letter === round.letter) {
      if (window.SOUNDS) window.SOUNDS.play("correct");
      const wasClean = roundCleanRef.current;
      setFirstTries((prev) => [...prev, wasClean]);
      if (window.MASTERY) window.MASTERY.recordRound(round.letter, wasClean);
      const btn  = buttonRefs.current[i];
      const slot = slotRef.current;
      if (btn && slot && t.wordHint === "slot") {
        const fromRect = btn.getBoundingClientRect();
        const toRect   = slot.getBoundingClientRect();
        setFlying({ letter, fromRect, toRect });
        setHidden(i);
        window.setTimeout(() => {
          setFlying(null);
          setHidden(-1);
          setStatus("correct");
          window.setTimeout(advance, CORRECT_HOLD_MS);
        }, FLIGHT_MS);
      } else {
        setStatus("correct");
        window.setTimeout(advance, CORRECT_HOLD_MS);
      }
    } else {
      if (window.SOUNDS) window.SOUNDS.play("wrong");
      roundCleanRef.current = false;
      setStatus("wrong");
      setWrongIdx(i);
      window.setTimeout(() => { setStatus("idle"); setWrongIdx(-1); }, WRONG_REVEAL_MS);
    }
  };

  return (
    <div className="app">
      {/* progress: 1 dot = 1 letter (default 22; truncated by ?n=) */}
      <Header index={roundIdx} total={rounds.length} />
      <div style={{ display: "grid", gridTemplateRows: "auto 1fr", gap: 12, minHeight: 0 }}>
        {t.wordHint === "slot" && (
          <WordSlots
            key={`slot-${roundIdx}`}
            ref={slotRef}
            word={round.word}
            status={status}
            filledLetter={filledLetter}
          />
        )}
        {t.wordHint === "full" && (
          <div className="word-row" aria-hidden="true">{round.word}</div>
        )}
        {t.wordHint === "none" && <div style={{ height: 8 }} />}
        <PhotoCard key={`card-${roundIdx}`} round={round} shake={status === "wrong"} />
      </div>
      <Buttons
        key={`row-${roundIdx}`}
        row={row}
        letterNames={letterNames}
        status={status}
        wrongIdx={wrongIdx}
        hiddenIdx={hiddenIdx}
        hintFor={hintFor}
        buttonRefs={buttonRefs}
        onPick={onPick}
      />

      <CorrectOverlay on={status === "correct"} />
      {flying && <FlyingLetter {...flying} />}

      {showBreak && (
        <BreakScreen
          batchRounds={batchRounds}
          batchFirstTries={firstTries.slice(roundIdx - 5, roundIdx)}
          completed={roundIdx}
          total={rounds.length}
          onContinue={() => { setEngaged(false); setLastBreak(breakAt); }}
          // "stop" → landing. `document.baseURI` resolves the page's
          // <base href> to the app root regardless of how deep the game
          // is nested (root /initial/ or /games/initial/, ADR 0029), so it
          // needs no manual segment-stripping — matching EndScreen's home
          // nav. (The old per-game segment-strip went one level short once
          // the games moved under games/.)
          onStop={() => {
            window.location.href = document.baseURI;
          }}
        />
      )}
      {done && (
        <EndScreen
          onRestart={restart}
          // v1.37 fix: roundIdx stays at the last-played index when
          // advance() flips done=true (it doesn't advance past the deck
          // end). firstTries is pushed on every correct tap, so its
          // length is the authoritative completed-rounds count.
          playedRounds={rounds.slice(0, firstTries.length)}
          playedFirstTries={firstTries}
        />
      )}

      <TweaksPanel title="Tweaks">
        <SharedTweakControls t={t} setTweak={setTweak}>
          <TweakSection label="כפתורים" />
          <TweakRadio
            label="צורה"
            value={t.buttonShape}
            options={[
              { value: "squircle", label: "Squircle" },
              { value: "circle",   label: "עיגול" },
              { value: "cushion",  label: "כרית" },
            ]}
            onChange={(v) => setTweak("buttonShape", v)}
          />

          <TweakSection label="מילה" />
          <TweakRadio
            label="רמז"
            value={t.wordHint}
            options={[
              { value: "none",  label: "ללא" },
              { value: "slot",  label: "ריבוע" },
              { value: "full",  label: "מלאה" },
            ]}
            onChange={(v) => setTweak("wordHint", v)}
          />

          <TweakSection label="פעולות" />
          <TweakButton label="שחק מההתחלה" onClick={restart} />
        </SharedTweakControls>
      </TweaksPanel>
    </div>
  );
}

// #102 themes-under-one-face: themes are a sub-step INSIDE the game, not
// sibling landing tiles. The landing's single "אותיות" tile links to
// ?pick=1, which shows this picker; choosing a collection navigates to
// ?theme=<id> and the game plays. Bare /games/initial/ and existing
// ?theme= deep-links are unchanged (so every gameplay spec still passes).
function ThemePicker() {
  const themes = [
    { id: "alex",     label: "אלכס",         img: "assets/images/alex/yalda.svg" },
    { id: "giborim",  label: "גיבורים",      img: "assets/images/alex/giborim.svg" },
    { id: "tales",    label: "אגדות",        img: "assets/images/tales/had-keren.svg" },
    { id: "stage",    label: "אופרה ובלט",   img: "assets/images/stage/swan-lake.svg" },
    { id: "default",  label: "הכל",          img: "assets/images/ohel.svg" },
    { id: "animals",  label: "חיות",         img: "assets/images/animals/arieh.svg" },
    { id: "first",    label: "מילים ראשונות", img: "assets/images/first/glida.svg" },
    { id: "david",    label: "דוד",          img: "assets/images/david.svg" },
  ];
  return (
    <div className="app theme-picker">
      <Crown />
      <h2 className="theme-picker__title">בחרו אוסף</h2>
      {/* href is base-relative to the GAME dir (like the landing tiles), NOT a
          bare "?theme=" — a query-only href resolves against <base href="../">
          and would land on /aleph-bet/?theme=… (the landing): the same <base>
          gotcha that caused the EndScreen 404. */}
      <div className="theme-picker__grid" role="group" aria-label="בחרו אוסף">
        {themes.map((th) => (
          <a key={th.id} className="theme-pick" href={`games/initial/?theme=${th.id}`} aria-label={`אוסף ${th.label}`}>
            <span className="theme-pick__art"><img src={th.img} alt="" /></span>
            <span className="theme-pick__label">{th.label}</span>
          </a>
        ))}
      </div>
    </div>
  );
}

function Root() {
  const showPicker =
    typeof window !== "undefined" &&
    new URLSearchParams(window.location.search).get("pick") === "1";
  return showPicker ? <ThemePicker /> : <App />;
}

ReactDOM.createRoot(document.getElementById("root")).render(<Root />);
