// TapTheBeat — rhythm minigame for filling dead time in lobby/locked screens.
// The mini-leaderboard now reflects real lobby members: every player taps on
// their own phone, the score is debounced to the server, and the room state
// streams back so all phones see the same live ranking. PB stays per-device.
//
// Props:
//   playerName  display name for the "YOU" row
//   players     room.players (each has { id, name, color, tapScore })
//   myId        window.PLAYER_ID (we mark this row "self")
//   onScoreUpdate(score)  — called with the player's current local score
//                           on a ~400ms debounce (parent sends to server)
//   endSoon     when true, fire onFinal once with the sorted leaderboard
//   onFinal(sorted, myScore, pb)

(() => {
const R = window.R;

const TAP_PB_KEY = 'hitster_tap_pb_v1';
function loadTapPB() {
  try { return parseInt(localStorage.getItem(TAP_PB_KEY) || '0') || 0; } catch (_) { return 0; }
}
function saveTapPB(v) {
  try { localStorage.setItem(TAP_PB_KEY, String(v)); } catch (_) {}
}

function TapTheBeat({ playerName = 'You', players = [], myId, endSoon = false, onScoreUpdate, onFinal }) {
  const BPM = 96;
  const BEAT_MS = 60000 / BPM;
  const TRACK_W = 280;
  const HIT_X = 38;
  // Three hit grades, widening rings outward from the centre.
  //  PERFECT (≤6 px)  — +1 combo, biggest points
  //  GREAT   (≤14 px) — keeps the combo, mid points
  //  GOOD    (≤22 px) — resets the combo (still scores a little)
  //  MISS    (>22 px) — 0 score, 0 combo
  const HIT_TOL_PERFECT = 6;
  const HIT_TOL_GREAT   = 14;
  const HIT_TOL_GOOD    = 22;
  const TRAVEL_MS = 2400;

  const [score, setScore] = React.useState(0);
  const [combo, setCombo] = React.useState(0);
  const [feedback, setFeedback] = React.useState(null);
  const [pb, setPB] = React.useState(() => loadTapPB());
  const beatsRef = React.useRef([]);
  const judgedRef = React.useRef(new Set());
  const finalizedRef = React.useRef(false);
  const comboRef = React.useRef(0); // mirrored for the spawner's setTimeout chain
  const [, forceTick] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => { comboRef.current = combo; }, [combo]);

  // Personal best persists across sessions.
  React.useEffect(() => {
    if (score > pb) { setPB(score); saveTapPB(score); }
  }, [score]);

  // Debounce-push our score to the parent (which forwards to the server).
  // 400ms feels live without flooding the socket.
  React.useEffect(() => {
    if (!onScoreUpdate) return;
    const t = setTimeout(() => onScoreUpdate(score), 400);
    return () => clearTimeout(t);
  }, [score, onScoreUpdate]);

  // Finalize when the parent flips endSoon — emit sorted leaderboard once.
  React.useEffect(() => {
    if (!endSoon || finalizedRef.current) return;
    finalizedRef.current = true;
    const merged = (players || []).map((p) => ({
      id: p.id,
      name: p.name,
      color: p.color,
      // Use our own live local score for self — server-side may be a tick stale.
      score: p.id === myId ? score : (p.tapScore || 0),
      self: p.id === myId,
    }));
    // Make sure self is in the list even if players prop is empty/stale.
    if (!merged.some((p) => p.self)) {
      merged.push({ id: myId, name: playerName, color: R.yellow, score, self: true });
    }
    const sorted = merged.sort((a, b) => b.score - a.score);
    onFinal?.(sorted, score, score > pb ? score : pb);
  }, [endSoon, score, players, myId]);

  // Spawn beats on a steady tempo, ramping difficulty by current combo (not
  // cumulative score) — so a miss / lost streak drops you back to chill.
  //  combo <3   : every 2 beats   (warm-up)
  //  combo 3-6  : every 1.5 beats (↑ FAST)
  //  combo 7-14 : every beat + 25% double-dot (🔥 HOT)
  //  combo 15+  : every beat + 45% double-dot (🔥 INSANE)
  React.useEffect(() => {
    let id = 0;
    let timer;
    let mounted = true;
    const spawnOne = () => {
      id += 1;
      beatsRef.current.push({ id, spawnT: performance.now() });
      beatsRef.current = beatsRef.current.filter((b) => performance.now() - b.spawnT < TRAVEL_MS + 600);
    };
    const tier = () => {
      const c = comboRef.current;
      // Slightly looser cadence at the top tiers — gives just enough room to
      // think between dots so INSANE isn't a button-mash.
      if (c >= 15) return { step: 1.3, doubleChance: 0.45 };
      if (c >= 7)  return { step: 1.4, doubleChance: 0.25 };
      if (c >= 3)  return { step: 1.7, doubleChance: 0 };
      return { step: 2.0, doubleChance: 0 };
    };
    const loop = () => {
      if (!mounted) return;
      spawnOne();
      const t = tier();
      // Occasional double-tap dot — second dot half a step behind.
      if (t.doubleChance > 0 && Math.random() < t.doubleChance) {
        setTimeout(() => mounted && spawnOne(), BEAT_MS * 0.5);
      }
      timer = setTimeout(loop, BEAT_MS * t.step);
    };
    loop();
    return () => { mounted = false; clearTimeout(timer); };
  }, []);

  // Animation tick + auto-judge missed dots
  React.useEffect(() => {
    let raf;
    const loop = () => {
      forceTick();
      const now = performance.now();
      beatsRef.current.forEach((b) => {
        if (judgedRef.current.has(b.id)) return;
        const age = now - b.spawnT;
        const x = TRACK_W - (age / TRAVEL_MS) * TRACK_W;
        if (x < HIT_X - HIT_TOL_GOOD - 6) {
          judgedRef.current.add(b.id);
          setCombo(0);
          setFeedback({ kind: 'miss', id: b.id, t: now });
        }
      });
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);

  const tap = React.useCallback(() => {
    const now = performance.now();
    let bestB = null, bestDist = Infinity;
    beatsRef.current.forEach((b) => {
      if (judgedRef.current.has(b.id)) return;
      const age = now - b.spawnT;
      const x = TRACK_W - (age / TRAVEL_MS) * TRACK_W;
      const dist = Math.abs(x - HIT_X);
      if (dist < bestDist) { bestDist = dist; bestB = b; }
    });
    if (!bestB) return;
    if (bestDist <= HIT_TOL_PERFECT) {
      judgedRef.current.add(bestB.id);
      setScore((s) => s + 100 + combo * 5);
      setCombo((c) => c + 1);
      setFeedback({ kind: 'perfect', id: bestB.id, t: now });
      try { navigator.vibrate?.(8); } catch (_) {}
    } else if (bestDist <= HIT_TOL_GREAT) {
      judgedRef.current.add(bestB.id);
      setScore((s) => s + 70);
      // Combo stays — great keeps the streak, doesn't extend it.
      setFeedback({ kind: 'great', id: bestB.id, t: now });
    } else if (bestDist <= HIT_TOL_GOOD) {
      judgedRef.current.add(bestB.id);
      setScore((s) => s + 40);
      setCombo(0); // good still scores but breaks the streak
      setFeedback({ kind: 'good', id: bestB.id, t: now });
    } else {
      setCombo(0);
      setFeedback({ kind: 'early', id: 'x' + now, t: now });
    }
  }, [combo]);

  React.useEffect(() => {
    const k = (e) => { if (e.code === 'Space' || e.code === 'Enter') { e.preventDefault(); tap(); } };
    window.addEventListener('keydown', k);
    return () => window.removeEventListener('keydown', k);
  }, [tap]);

  const fbColor = feedback?.kind === 'perfect' ? R.yellow
                : feedback?.kind === 'great'   ? R.green
                : feedback?.kind === 'good'    ? R.blue
                : feedback?.kind === 'miss'    ? R.pink
                : R.muted;
  const fbLabel = feedback?.kind === 'perfect' ? 'PERFECT'
                : feedback?.kind === 'great'   ? 'GREAT'
                : feedback?.kind === 'good'    ? 'GOOD'
                : feedback?.kind === 'miss'    ? 'MISS'
                : 'EARLY';

  const now = performance.now();
  const visible = beatsRef.current.map((b) => {
    const age = now - b.spawnT;
    const x = TRACK_W - (age / TRAVEL_MS) * TRACK_W;
    if (x < -10 || x > TRACK_W + 10) return null;
    return { ...b, x, judged: judgedRef.current.has(b.id) };
  }).filter(Boolean);
  const pulseScale = 1 + ((Date.now() % BEAT_MS) / BEAT_MS) * 0.18;

  // Build the live leaderboard from real players. Self uses the local score
  // (the server echo is up to 400ms stale).
  const leaderboard = (players || []).map((p) => ({
    id: p.id,
    name: p.name,
    color: p.color,
    score: p.id === myId ? score : (p.tapScore || 0),
    self: p.id === myId,
  })).sort((a, b) => b.score - a.score);
  // Defensive fallback: if players is empty (race on first render), still show "YOU".
  if (leaderboard.length === 0) {
    leaderboard.push({ id: myId, name: playerName, color: R.yellow, score, self: true });
  }
  const maxScore = Math.max(score, ...leaderboard.map((p) => p.score), 1);

  return (
    <div style={{
      padding: '12px 14px', background: R.ink, color: R.paper,
      border: `2px solid ${R.ink}`, boxShadow: `4px 4px 0 ${R.ink}`,
      backgroundImage: window.risoTex('rgba(245,239,226,.06)', 3),
      position: 'relative',
    }}>
      {(() => {
        // Combo-driven difficulty label. Reserve fixed-width slots for the
        // tier and combo badges so the header never reflows when they appear.
        const tierLabel = combo >= 15 ? { text: '🔥 INSANE', bg: R.pink, fg: R.paper }
                        : combo >= 7  ? { text: '🔥 HOT',    bg: null,   fg: R.pink }
                        : combo >= 3  ? { text: '↑ FAST',    bg: null,   fg: R.yellow }
                        : null;
        return (
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
            <div style={{ fontSize: 10, fontWeight: 900, letterSpacing: '.24em', color: R.yellow }}>WHILE YOU WAIT · LIVE</div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <div style={{
                minWidth: 78, textAlign: 'right',
                fontSize: 10, fontWeight: 900, letterSpacing: '.18em',
                visibility: tierLabel ? 'visible' : 'hidden',
                color: tierLabel?.fg || R.paper,
                background: tierLabel?.bg || 'transparent',
                padding: tierLabel?.bg ? '2px 6px' : '0',
              }}>{tierLabel?.text || '—'}</div>
              <div style={{
                minWidth: 58, textAlign: 'right',
                fontSize: 10, fontWeight: 900, letterSpacing: '.18em', color: R.pink,
                visibility: combo >= 3 ? 'visible' : 'hidden',
              }}>×{Math.max(combo, 1)} COMBO</div>
              <div style={{
                fontSize: 14, fontWeight: 900, fontFamily: 'ui-monospace, monospace',
                color: R.paper, padding: '2px 8px', background: 'rgba(255,255,255,.08)',
                minWidth: 48, textAlign: 'right',
              }}>{score}</div>
            </div>
          </div>
        );
      })()}

      <div style={{ fontSize: 14, fontWeight: 900, letterSpacing: '-.01em', marginBottom: 8 }}>
        Tap the beat <span style={{ fontSize: 9, fontWeight: 700, color: 'rgba(245,239,226,.5)', letterSpacing: '.14em' }}>· TAP OR SPACE</span>
      </div>

      <div
        onPointerDown={(e) => { e.preventDefault(); tap(); }}
        style={{
          position: 'relative', width: '100%', height: 56,
          background: 'rgba(245,239,226,.06)',
          border: `1px solid rgba(245,239,226,.2)`,
          overflow: 'hidden', cursor: 'pointer',
          touchAction: 'manipulation', userSelect: 'none',
        }}>
        {/* Hit zone — circle. The visible ring intentionally sits outside
            HIT_TOL_PERFECT (6 px) so PERFECT means "really on the dot". */}
        <div style={{
          position: 'absolute', left: HIT_X - 20, top: 8, width: 40, height: 40,
          border: `2px solid ${R.yellow}`, background: `rgba(255,210,63,.1)`,
          borderRadius: '50%',
          transform: `scale(${pulseScale})`, transformOrigin: 'center',
          transition: 'transform .12s',
        }} />
        {/* Inner target dot — visually marks the PERFECT centre. */}
        <div style={{
          position: 'absolute', left: HIT_X - 3, top: 25, width: 6, height: 6,
          background: R.yellow, borderRadius: '50%', opacity: .9,
          pointerEvents: 'none',
        }} />
        {/* Beat dots */}
        {visible.map((b) => (
          <div key={b.id} style={{
            position: 'absolute',
            left: b.x - 12, top: 16,
            width: 24, height: 24,
            background: b.judged ? 'transparent' : R.pink,
            border: `2px solid ${b.judged ? 'rgba(255,90,122,.3)' : R.paper}`,
            borderRadius: '50%',
            transition: 'background .1s, border-color .1s',
            boxShadow: b.judged ? 'none' : `0 0 0 4px rgba(255,90,122,.2)`,
          }} />
        ))}
        {feedback && (
          <div key={feedback.id} style={{
            position: 'absolute', left: HIT_X - 30, top: 4, width: 60,
            textAlign: 'center', color: fbColor,
            fontSize: 11, fontWeight: 900, letterSpacing: '.16em',
            animation: 'fbpop .5s forwards',
            pointerEvents: 'none',
          }}>{fbLabel}</div>
        )}
      </div>
      <style>{`@keyframes fbpop {
        0% { opacity: 0; transform: translateY(0) scale(.8); }
        20% { opacity: 1; transform: translateY(-6px) scale(1.1); }
        100% { opacity: 0; transform: translateY(-22px) scale(1); }
      }`}</style>

      {/* Live leaderboard. Up to 4 players → wide card columns; 5+ → compact
          single-line rows so 10-player lobbies still fit on one phone screen. */}
      {leaderboard.length <= 4 ? (
        <div style={{ marginTop: 10, display: 'flex', alignItems: 'stretch', gap: 6 }}>
          {leaderboard.map((p, i) => {
            const w = Math.round((p.score / maxScore) * 100);
            return (
              <div key={p.id || p.name} style={{
                flex: 1, minWidth: 0,
                padding: '4px 6px 5px',
                background: p.self ? 'rgba(255,210,63,.12)' : 'rgba(245,239,226,.04)',
                border: `1px solid ${p.self ? R.yellow : 'rgba(245,239,226,.2)'}`,
                position: 'relative', overflow: 'hidden',
              }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                  <span style={{
                    fontSize: 8, fontWeight: 900, letterSpacing: '.1em',
                    color: i === 0 ? R.yellow : 'rgba(245,239,226,.5)', width: 8,
                  }}>{i + 1}</span>
                  <span style={{
                    fontSize: 9, fontWeight: 800, letterSpacing: '.06em',
                    color: R.paper, flex: 1, minWidth: 0,
                    whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                  }}>{p.self ? 'YOU' : (p.name || '').toUpperCase()}</span>
                </div>
                <div style={{
                  fontSize: 11, fontWeight: 900, fontFamily: 'ui-monospace, monospace',
                  color: p.self ? R.yellow : R.paper, marginTop: 1, lineHeight: 1,
                }}>{p.score}</div>
                <div style={{
                  position: 'absolute', left: 0, bottom: 0, height: 2,
                  width: `${w}%`, background: p.self ? R.yellow : R.pink,
                  transition: 'width .4s',
                }} />
              </div>
            );
          })}
        </div>
      ) : (
        <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 2 }}>
          {leaderboard.map((p, i) => {
            const w = Math.round((p.score / maxScore) * 100);
            return (
              <div key={p.id || p.name} style={{
                display: 'flex', alignItems: 'center', gap: 6,
                padding: '2px 6px',
                background: p.self ? 'rgba(255,210,63,.12)' : 'rgba(245,239,226,.04)',
                border: `1px solid ${p.self ? R.yellow : 'rgba(245,239,226,.18)'}`,
                position: 'relative', overflow: 'hidden',
                minHeight: 18,
              }}>
                <span style={{
                  fontSize: 9, fontWeight: 900, fontFamily: 'ui-monospace, monospace',
                  color: i === 0 ? R.yellow : 'rgba(245,239,226,.5)', width: 14,
                }}>{i + 1}</span>
                <span style={{
                  fontSize: 10, fontWeight: 800, letterSpacing: '.04em',
                  color: R.paper, flex: 1, minWidth: 0,
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>{p.self ? 'YOU' : (p.name || '').toUpperCase()}</span>
                <span style={{
                  fontSize: 10, fontWeight: 900, fontFamily: 'ui-monospace, monospace',
                  color: p.self ? R.yellow : R.paper, minWidth: 28, textAlign: 'right',
                }}>{p.score}</span>
                <div style={{
                  position: 'absolute', left: 0, bottom: 0, height: 1.5,
                  width: `${w}%`, background: p.self ? R.yellow : R.pink,
                  transition: 'width .4s',
                }} />
              </div>
            );
          })}
        </div>
      )}
      <div style={{
        marginTop: 6, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
        fontSize: 9, fontWeight: 800, letterSpacing: '.16em', color: 'rgba(245,239,226,.45)',
      }}>
        <span>PERSONAL BEST · <span style={{ color: R.yellow, fontFamily: 'ui-monospace, monospace' }}>{Math.max(pb, score)}</span></span>
        <span>{score > pb && pb > 0 ? <span style={{ color: R.green }}>↑ NEW PB</span> : `WAS ${pb || 0}`}</span>
      </div>
    </div>
  );
}

window.TapTheBeat = TapTheBeat;
})();
