// App shell — daily state, storage, overlays, share, stats, free play.
const { useState: useS, useEffect: useE, useRef: useR, useMemo: useM, useCallback: useC } = React;

// ============ STORAGE ============
const LS_DAILY = "bbp_daily_v1";
const LS_STATS = "bbp_stats_v1";
const LS_PREFS = "bbp_prefs_v1";
const LS_HOWTO = "bbp_howto_v1";

function lsGet(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : fallback;
  } catch (e) { return fallback; }
}
function lsSet(key, val) {
  try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {}
}

const EMPTY_STATS = { played: 0, streak: 0, maxStreak: 0, lastDayNum: null, dist: {} };
const DIST_ROWS = [
  { key: "ace", label: "ACE", color: "#7ecfff" },
  { key: "eagle", label: "EAGLE", color: "#8be88b" },
  { key: "birdie", label: "BIRDIE", color: "#8be88b" },
  { key: "par", label: "PAR", color: "#eaeadd" },
  { key: "bogey", label: "BOGEY", color: "#e8c84d" },
  { key: "double", label: "DOUBLE", color: "#e8884d" },
  { key: "worse", label: "+3 & UP", color: "#e84d3c" },
];

function bucketFor(strokes, par) {
  const d = strokes - par;
  if (strokes === 1) return "ace";
  if (d <= -2) return "eagle";
  if (d === -1) return "birdie";
  if (d === 0) return "par";
  if (d === 1) return "bogey";
  if (d === 2) return "double";
  return "worse";
}

function resultLabel(strokes, par, pickedUp) {
  if (pickedUp) return "PICKED UP";
  const d = strokes - par;
  if (strokes === 1) return "HOLE IN ONE!";
  if (d <= -2) return "EAGLE!";
  if (d === -1) return "BIRDIE";
  if (d === 0) return "PAR";
  if (d === 1) return "BOGEY";
  if (d === 2) return "DOUBLE BOGEY";
  return "+" + d;
}

function seqEmoji(seq, pickedUp) {
  // 🔵 survey · ⚪ putt · last putt becomes ⛳ when holed, ❌ when picked up
  const out = [];
  let lastPutt = -1;
  seq.forEach((s, i) => { if (s === "P") lastPutt = i; });
  seq.forEach((s, i) => {
    if (s === "S") out.push("🔵");
    else if (i === lastPutt && !pickedUp) out.push("⛳");
    else out.push("⚪");
  });
  if (pickedUp) out.push("❌");
  return out.join("");
}

function buildShareText(info, result, stats) {
  const lines = [];
  lines.push("⛳ BLIND BREAK PUTT #" + info.dayNum);
  let line2 = "PAR " + result.par + " · " + resultLabel(result.strokes, result.par, result.pickedUp) +
    " (" + result.strokes + ")";
  if (result.redo) line2 += "*";
  if (result.hard) line2 += " · HARD";
  lines.push(line2);
  lines.push(seqEmoji(result.seq, result.pickedUp));
  if (stats.streak > 1) lines.push("🔥 " + stats.streak + " DAY STREAK");
  return lines.join("\n");
}

function fmtCountdown(ms) {
  const s = Math.max(0, Math.floor(ms / 1000));
  const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;
  return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(ss).padStart(2, "0");
}

// ============ APP ============
function App() {
  const B = window.BBP;
  const info = useM(() => B.dailyInfo(), []);
  const dailyDef = useM(() => B.dailyHole(info), [info]);

  const prefs0 = useM(() => lsGet(LS_PREFS, { sound: true, hint: false, hard: false }), []);
  const saved0 = useM(() => {
    const rec = lsGet(LS_DAILY, null);
    return rec && rec.dateStr === info.dateStr ? rec : null;
  }, [info]);

  const soundRef = useR(null);
  if (!soundRef.current) soundRef.current = new B.SoundEngine();

  // --- mode & hole ---
  const [mode, setMode] = useS("daily"); // daily | free
  const [freeHole, setFreeHole] = useS(null); // {def, seed}
  const [attempt, setAttempt] = useS(0);

  // --- daily record ---
  const [dailyRec, setDailyRec] = useS(() => saved0 || { dateStr: info.dateStr, dayNum: info.dayNum, done: false, result: null, progress: null });
  const [stats, setStats] = useS(() => lsGet(LS_STATS, EMPTY_STATS));

  // --- play state ---
  const resume = saved0 && !saved0.done ? saved0.progress : null;
  const [strokes, setStrokes] = useS(resume ? resume.strokes : 0);
  const [surveysLeft, setSurveysLeft] = useS(
    resume ? Math.max(0, info.surveys - resume.seq.filter((s) => s === "S").length) : info.surveys
  );
  const [surveyArmed, setSurveyArmed] = useS(false);
  const [phase, setPhase] = useS("transition");
  const [soundOn, setSoundOn] = useS(prefs0.sound);
  const [hintOn, setHintOn] = useS(prefs0.hint);
  const [hardMode, setHardMode] = useS(prefs0.hard);
  const redoUsedRef = useR(resume ? !!resume.redo : false);

  // --- overlay ---
  const [overlay, setOverlay] = useS(() =>
    !lsGet(LS_HOWTO, false) ? "howto" : (saved0 && saved0.done ? "result" : null)
  );
  const [practiceResult, setPracticeResult] = useS(null);
  const [freeResult, setFreeResult] = useS(null);
  const [copied, setCopied] = useS(false);
  const [countdown, setCountdown] = useS(B.msUntilTomorrow());

  useE(() => { soundRef.current.enabled = soundOn; }, [soundOn]);
  useE(() => { lsSet(LS_PREFS, { sound: soundOn, hint: hintOn, hard: hardMode }); }, [soundOn, hintOn, hardMode]);

  // countdown tick while a result overlay is up
  useE(() => {
    if (overlay !== "result") return;
    const t = setInterval(() => {
      const ms = B.msUntilTomorrow();
      setCountdown(ms);
      if (ms < 1000) window.location.reload();
    }, 1000);
    return () => clearInterval(t);
  }, [overlay]);

  // --- current hole ---
  const isDaily = mode === "daily";
  const def = isDaily ? dailyDef : (freeHole ? freeHole.def : dailyDef);
  const surveysAllotted = isDaily ? info.surveys : 3;
  const palette = useM(
    () => B.greenPalette(isDaily ? info.rampIdx : (freeHole ? freeHole.rampIdx : 6), def.night),
    [isDaily, freeHole, def]
  );
  const defKey = isDaily ? "daily-" + info.dateStr + "-a" + attempt : "free-" + (freeHole ? freeHole.seed : 0);

  const intro = useM(() => {
    if (!isDaily) return { tag: "FREE PLAY", title: "PRACTICE GREEN", sub: "DOESN'T COUNT", color: "#8be88b" };
    if (dailyRec.done && attempt > 0) return { tag: "PRACTICE RUN", title: "DAILY #" + info.dayNum, sub: "FIRST RUN ALREADY CARDED", color: "#8be88b" };
    if (attempt > 0) return { tag: "REDO — CARD MARKED *", title: "DAILY #" + info.dayNum, sub: info.label, color: "#e8c84d" };
    return { tag: "DAILY HOLE", title: "#" + info.dayNum, sub: info.label, color: "#7ecfff" };
  }, [isDaily, attempt, dailyRec.done, info]);

  // --- persistence helpers ---
  const saveRec = useC((rec) => { setDailyRec(rec); lsSet(LS_DAILY, rec); }, []);

  const persistProgress = useC((snap) => {
    if (!isDaily || dailyRec.done) return;
    saveRec({ ...dailyRec, progress: { ...snap, redo: redoUsedRef.current } });
  }, [isDaily, dailyRec, saveRec]);

  // --- canvas events ---
  const onUpdate = useC(({ strokes: n }) => setStrokes(n), []);

  const onSurveyFired = useC(() => {
    setSurveysLeft((v) => Math.max(0, v - 1));
    setSurveyArmed(false);
  }, []);

  const onBallStopped = useC((snap) => { persistProgress(snap); }, [persistProgress]);

  const onFinish = useC(({ strokes: finalStrokes, pickedUp, seq }) => {
    if (!isDaily) {
      setFreeResult({ strokes: finalStrokes, par: def.par, pickedUp });
      setTimeout(() => setOverlay("freeResult"), 900);
      return;
    }
    if (!dailyRec.done) {
      const result = {
        strokes: finalStrokes, par: def.par, pickedUp, seq,
        redo: redoUsedRef.current, hard: hardMode,
      };
      const newStats = { ...stats, dist: { ...stats.dist } };
      newStats.played += 1;
      newStats.streak = stats.lastDayNum === info.dayNum - 1 ? stats.streak + 1 : 1;
      newStats.maxStreak = Math.max(newStats.streak, stats.maxStreak);
      newStats.lastDayNum = info.dayNum;
      const bk = bucketFor(finalStrokes, def.par);
      newStats.dist[bk] = (newStats.dist[bk] || 0) + 1;
      setStats(newStats);
      lsSet(LS_STATS, newStats);
      saveRec({ dateStr: info.dateStr, dayNum: info.dayNum, done: true, result, progress: null });
      if (!pickedUp && finalStrokes <= def.par) setTimeout(() => soundRef.current.fanfare(), 500);
      setTimeout(() => setOverlay("result"), 900);
    } else {
      setPracticeResult({ strokes: finalStrokes, par: def.par, pickedUp });
      setTimeout(() => setOverlay("practiceResult"), 900);
    }
  }, [isDaily, dailyRec.done, def, hardMode, stats, info, saveRec]);

  const onPhase = useC((p) => setPhase(p), []);

  // --- actions ---
  const doRedo = () => {
    if (phase === "rolling" || phase === "transition") return;
    if (isDaily && !dailyRec.done) {
      redoUsedRef.current = true;
      saveRec({ ...dailyRec, progress: null });
    }
    if (!isDaily) {
      setFreeHole(B.randomFreeHole());
    }
    setOverlay(null);
    setStrokes(0);
    setSurveysLeft(surveysAllotted);
    setSurveyArmed(false);
    setPracticeResult(null);
    setAttempt((a) => a + 1);
  };

  const startFreePlay = () => {
    setMode("free");
    setFreeHole(B.randomFreeHole());
    setOverlay(null);
    setStrokes(0);
    setSurveysLeft(3);
    setSurveyArmed(false);
    setFreeResult(null);
    setAttempt((a) => a + 1);
  };

  const backToDaily = () => {
    setMode("daily");
    setOverlay(dailyRec.done ? "result" : null);
    setStrokes(0);
    setSurveysLeft(surveysAllotted);
    setSurveyArmed(false);
    setFreeResult(null);
    setAttempt((a) => a + 1);
  };

  const doShare = async () => {
    const text = buildShareText(info, dailyRec.result, stats);
    let shared = false;
    if (navigator.share) {
      try { await navigator.share({ text }); shared = true; } catch (e) {}
    }
    if (!shared) {
      try {
        await navigator.clipboard.writeText(text);
        setCopied(true);
        setTimeout(() => setCopied(false), 1800);
      } catch (e) {}
    }
  };

  const dismissHowto = () => {
    lsSet(LS_HOWTO, true);
    setOverlay(dailyRec.done ? "result" : null);
  };

  // --- derived ---
  const pips = B.featureCount(def);
  const res = dailyRec.result;

  const instruction = (() => {
    if (overlay) return " ";
    if (phase === "transition") return " ";
    if (phase === "rolling") return "...";
    if (phase === "holed") return " ";
    if (surveyArmed) return "Survey armed — this putt is free intel.";
    if (isDaily && dailyRec.done) return "practice run — today is already carded";
    return "Drag anywhere → release to putt.";
  })();

  // ============ RENDER ============
  return (
    <div className="bb-app" data-screen-label={isDaily ? "daily-game" : "free-play"}>
      <header className="bb-header">
        <div className="bb-title">★ BLIND BREAK PUTT ★</div>
        <div className="bb-header-btns">
          <button className="bb-icon-btn" onClick={() => setOverlay("howto")} aria-label="How to play">?</button>
          <button className="bb-icon-btn" onClick={() => setOverlay("stats")} aria-label="Stats">▦</button>
        </div>
      </header>

      <div className="bb-chips">
        {isDaily ? (
          <React.Fragment>
            <span className="chip chip-cyan">DAILY #{info.dayNum}</span>
            <span className="chip">{info.label}</span>
          </React.Fragment>
        ) : (
          <span className="chip chip-green">FREE PLAY · NO STAKES</span>
        )}
        <span className="chip chip-gold">{"◆".repeat(pips)}{"◇".repeat(Math.max(0, 4 - pips))}</span>
        {def.dusk && <span className="chip chip-dusk">DUSK</span>}
        {def.night && <span className="chip chip-green">NIGHT</span>}
      </div>

      <div className="bb-hud">
        <div className="bb-hud-cell">
          <div className="bb-hud-label">Par</div>
          <div className="bb-hud-val">{def.par}</div>
        </div>
        <div className="bb-hud-cell">
          <div className="bb-hud-label">Strokes</div>
          <div className="bb-hud-val">{strokes}</div>
        </div>
        <div className="bb-hud-cell">
          <div className="bb-hud-label">Surveys</div>
          <div className="bb-hud-val bb-hud-cyan">
            {"●".repeat(surveysLeft)}{"○".repeat(Math.max(0, surveysAllotted - surveysLeft))}
          </div>
        </div>
      </div>

      <div className="bb-stage">
        <div className="bb-canvas-wrap">
          <GameCanvas
            defKey={defKey}
            def={def}
            palette={palette}
            intro={intro}
            sound={soundRef.current}
            hintOn={hintOn}
            hardMode={hardMode}
            surveyArmed={surveyArmed}
            surveysLeft={surveysLeft}
            initState={attempt === 0 ? resume : null}
            onUpdate={onUpdate}
            onSurveyFired={onSurveyFired}
            onBallStopped={onBallStopped}
            onFinish={onFinish}
            onPhase={onPhase}
          ></GameCanvas>

          {overlay === "howto" && (
            <div className="bb-overlay" data-screen-label="how-to-play">
              <div className="bb-ov-tag">ONE GREEN A DAY</div>
              <div className="bb-ov-title">HOW TO PLAY</div>
              <div className="bb-howto">
                <p><b>Drag &amp; release</b> anywhere to putt. Longer drag = more power.</p>
                <p>The green looks flat. <b>It isn't.</b> Slopes, bumps, walls and worse are invisible — your ball's tracer is your only map.</p>
                <p><b>SURVEY</b> rolls a free scout ball. Limited supply — spend it wisely.</p>
                <p>Everyone gets the <b>same hole</b> each day. Your first run is the one that counts.</p>
              </div>
              <button className="bb-btn" onClick={dismissHowto}>PLAY ▶</button>
            </div>
          )}

          {overlay === "result" && res && (
            <div className="bb-overlay" data-screen-label="daily-result">
              <div className="bb-ov-tag">DAILY #{info.dayNum} · {info.label}</div>
              <div className={"bb-ov-title" + (res.pickedUp ? " bb-red" : "")}>
                {resultLabel(res.strokes, res.par, res.pickedUp)}{res.redo ? "*" : ""}
              </div>
              <div className="bb-ov-sub">
                {res.pickedUp
                  ? "Max strokes — carded " + res.strokes
                  : <span>Sunk in <b>{res.strokes}</b> · Par {res.par}</span>}
                {res.hard && <span className="bb-hard-chip">HARD</span>}
              </div>
              <div className="bb-emoji-row">{seqEmoji(res.seq, res.pickedUp)}</div>
              {res.redo && <div className="bb-fineprint">* used redo</div>}
              <div className="bb-streak-row">
                STREAK {stats.streak} · MAX {stats.maxStreak} · PLAYED {stats.played}
              </div>
              <div className="bb-countdown">
                <span className="bb-countdown-label">NEXT GREEN IN</span>
                <span className="bb-countdown-time">{fmtCountdown(countdown)}</span>
              </div>
              <div className="bb-btn-row">
                <button className="bb-btn" onClick={doShare}>{copied ? "COPIED!" : "SHARE"}</button>
                <button className="bb-btn secondary" onClick={startFreePlay}>FREE PLAY</button>
              </div>
              <div className="bb-link-row">
                <button className="bb-link" onClick={doRedo}>replay (practice)</button>
                <button className="bb-link" onClick={() => setOverlay("stats")}>stats</button>
              </div>
            </div>
          )}

          {overlay === "practiceResult" && practiceResult && (
            <div className="bb-overlay" data-screen-label="practice-result">
              <div className="bb-ov-tag">PRACTICE RUN · DOESN'T COUNT</div>
              <div className="bb-ov-title bb-green">
                {resultLabel(practiceResult.strokes, practiceResult.par, practiceResult.pickedUp)}
              </div>
              <div className="bb-ov-sub">
                {practiceResult.pickedUp
                  ? "Max strokes — " + practiceResult.strokes
                  : <span>Sunk in <b>{practiceResult.strokes}</b> · Par {practiceResult.par}</span>}
              </div>
              <div className="bb-fineprint">
                carded today: {res ? resultLabel(res.strokes, res.par, res.pickedUp) + " (" + res.strokes + ")" + (res.redo ? "*" : "") : ""}
              </div>
              <div className="bb-btn-row">
                <button className="bb-btn secondary" onClick={doRedo}>AGAIN</button>
                <button className="bb-btn" onClick={() => setOverlay("result")}>RESULTS ▶</button>
              </div>
            </div>
          )}

          {overlay === "freeResult" && freeResult && (
            <div className="bb-overlay" data-screen-label="free-result">
              <div className="bb-ov-tag">FREE PLAY · NO STAKES</div>
              <div className="bb-ov-title bb-green">
                {resultLabel(freeResult.strokes, freeResult.par, freeResult.pickedUp)}
              </div>
              <div className="bb-ov-sub">
                {freeResult.pickedUp
                  ? "Max strokes — " + freeResult.strokes
                  : <span>Sunk in <b>{freeResult.strokes}</b> · Par {freeResult.par}</span>}
              </div>
              <div className="bb-btn-row">
                <button className="bb-btn" onClick={startFreePlay}>NEXT HOLE ▶</button>
                <button className="bb-btn secondary" onClick={backToDaily}>DAILY</button>
              </div>
            </div>
          )}

          {overlay === "stats" && (
            <div className="bb-overlay" data-screen-label="stats">
              <div className="bb-ov-title">STATS</div>
              <div className="bb-stats-grid">
                <div className="bb-stat"><div className="bb-stat-val">{stats.played}</div><div className="bb-stat-label">Played</div></div>
                <div className="bb-stat"><div className="bb-stat-val">{stats.streak}</div><div className="bb-stat-label">Streak</div></div>
                <div className="bb-stat"><div className="bb-stat-val">{stats.maxStreak}</div><div className="bb-stat-label">Max</div></div>
              </div>
              <div className="bb-dist">
                {DIST_ROWS.map((row) => {
                  const n = stats.dist[row.key] || 0;
                  const max = Math.max(1, ...DIST_ROWS.map((r) => stats.dist[r.key] || 0));
                  return (
                    <div className="bb-dist-row" key={row.key}>
                      <span className="bb-dist-label">{row.label}</span>
                      <span className="bb-dist-bar-track">
                        <span className="bb-dist-bar" style={{ width: Math.max(n > 0 ? 12 : 4, (n / max) * 100) + "%", background: n > 0 ? row.color : "#2a2a3d" }}></span>
                      </span>
                      <span className="bb-dist-n">{n}</span>
                    </div>
                  );
                })}
              </div>
              <button className="bb-btn secondary" onClick={() => setOverlay(dailyRec.done && isDaily && !practiceResult ? "result" : null)}>CLOSE</button>
            </div>
          )}
        </div>
      </div>

      <div className="bb-instr">
        {isDaily && dailyRec.done && !overlay && phase === "aim" ? (
          <button className="bb-link" onClick={() => setOverlay("result")}>view today's result ▸</button>
        ) : instruction}
      </div>

      <div className="bb-controls">
        <button
          className={"bb-toggle bb-toggle-survey" + (surveyArmed ? " on" : "") + (surveysLeft === 0 ? " disabled" : "")}
          onClick={() => surveysLeft > 0 && phase === "aim" && setSurveyArmed(!surveyArmed)}
          disabled={surveysLeft === 0}
        >
          <span className="bb-toggle-glyph">◌</span>SURVEY {surveysLeft}
        </button>
        <button className={"bb-toggle" + (hintOn ? " on" : "")} onClick={() => setHintOn(!hintOn)}>
          <span className="bb-toggle-glyph">○</span>HINT
        </button>
        <button className={"bb-toggle" + (hardMode ? " on" : "")} onClick={() => setHardMode(!hardMode)}>
          <span className="bb-toggle-glyph">★</span>HARD
        </button>
        <button className={"bb-toggle" + (soundOn ? " on" : "")} onClick={() => setSoundOn(!soundOn)}>
          <span className="bb-toggle-glyph">{soundOn ? "♪" : "×"}</span>SND
        </button>
        <button className="bb-toggle" onClick={doRedo}>
          <span className="bb-toggle-glyph">↺</span>REDO
        </button>
      </div>
    </div>
  );
}

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