// Player screen — the breathing session. Owns engine, voice cues, and
// session completion. Used inside mobile + web shells.
//
// `onClose` returns to prior screen; `onComplete` fires once `done`.

// Voice cues route through the BF_VOICE_PACKS manifest in src/voice-packs.js.
// For each cue (inhale/exhale/hold/finishing/progression) we:
//   1. Resolve the pack's mp3 URL via BFVoicePacks.cueUrl(packId, cueId).
//      If the file exists on disk/SW-cache, it plays directly.
//   2. If the pack has no URL for this cue (system pack, or the mp3 hasn't
//      been generated yet for a new locale), we fall through to
//      BFAudio.speak(text, { lang }) using the pack's locale. That's how
//      Spanish / Russian / Ukrainian work today without any mp3 assets —
//      the OS TTS voice in the right language handles it.
//
// Preload step: when the pack has mp3 URLs, we preload all of them on mount
// so the first phase cue is zero-latency. Switching packs mid-session
// unloads the prior pack's clips.

function _bfPhaseClip(phase) {
  if (phase === 'inhale') return 'inhale';
  if (phase === 'exhale') return 'exhale';
  if (phase && phase.startsWith('hold')) return 'hold';
  return null;
}

function BFPlayer({ pattern, totalSeconds, vizStyle: initialViz, setVizStyle, accentH, theme, voice, voicePack, sound, ambientId, ambientVol, haptics, onClose, onComplete }) {
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  // Long-lived during a session — re-render on locale change so HUD labels
  // (Elapsed/Cycles/Remaining/…) pick up the new language mid-session.
  const [, forceI18nRender] = React.useState(0);
  React.useEffect(() => {
    if (!window.BFI18n) return;
    return window.BFI18n.onChange(() => forceI18nRender((n) => n + 1));
  }, []);
  const [playing, setPlaying] = React.useState(true);
  const [viz, setViz] = React.useState(() => bfDefaultViz(pattern, initialViz));
  const [focus, setFocus] = React.useState(false);
  const [wakeActive, setWakeActive] = React.useState(false);
  const [muted, setMuted] = React.useState(false);
  // Progression is now configured at build time on the pattern itself (custom
  // patterns in the preset editor). Built-ins have no progression; they stay
  // at their designer-chosen rhythm. The player reads whatever the pattern
  // carries — no mid-session toggle.
  const progression = (pattern && pattern.progression && pattern.progression.enabled) ? pattern.progression : null;
  const progressionOn = !!progression;
  const s = useBreathEngine({ pattern, totalSeconds, playing, progression });
  const lastPhase = React.useRef(null);
  // Tracks the last announced progression bonus (seconds added). Starts at 0;
  // every time the live bonus steps up (e.g. 0→1, 1→2…) we fire the cue once.
  // Reset when the pattern itself changes below.
  const lastProgressionBonusRef = React.useRef(0);

  // Session-start haptic: fires once on mount if haptics enabled. Gives the
  // user tactile confirmation that the session actually began (useful when
  // screen dims or they look away mid-inhale).
  React.useEffect(() => {
    if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.medium();
  }, []);

  // Track "last round" so we announce it once per session.
  const lastRoundAnnouncedRef = React.useRef(false);
  const cycleSec = bfCycleSeconds(pattern);
  const onLastCycle = BFLastRound.isLastCycle(s.remaining, cycleSec);

  // Background-audio survival. When the user presses the lock button mid-
  // session, we want voice cues + haptics + phase timing to keep going. Two
  // pieces are needed for that:
  //   1. An active audio source to keep the OS audio thread alive. We start
  //      a near-silent looping buffer on a private AudioContext (gain 0.0001).
  //      As long as a source is playing, Android Chrome / WebView don't kill
  //      setTimeout in the JS thread → engine.jsx keeps ticking.
  //   2. MediaSession metadata + 'playing' state. Tells the OS this page is
  //      doing media playback, which extends background lifetime + surfaces
  //      lock-screen pause/play (we wire those handlers to setPlaying).
  // Both clean up on session end so a paused/stopped player isn't holding
  // audio focus indefinitely.
  React.useEffect(() => {
    if (!playing) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    let ctx = null;
    let src = null;
    let gain = null;
    try {
      ctx = new AC();
      // 1-sec silent buffer, looped — Web Audio considers this an active source
      // so the page is treated as "playing media" for backgrounding purposes.
      const buf = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
      src = ctx.createBufferSource();
      src.buffer = buf; src.loop = true;
      gain = ctx.createGain();
      gain.gain.value = 0.0001; // effectively silent, but non-zero so the source counts as audible
      src.connect(gain).connect(ctx.destination);
      src.start();
      if (ctx.state === 'suspended') { ctx.resume().catch(() => {}); }
    } catch (e) { /* no AudioContext support — skip; rAF-only mode still works while screen on */ }

    // MediaSession — sets the page as active media playback.
    let prevMetadata = null;
    let prevPlaybackState = null;
    let registeredHandlers = false;
    try {
      if ('mediaSession' in navigator) {
        prevMetadata = navigator.mediaSession.metadata;
        prevPlaybackState = navigator.mediaSession.playbackState;
        if (typeof window.MediaMetadata === 'function') {
          navigator.mediaSession.metadata = new window.MediaMetadata({
            title: bfPatternName(pattern),
            artist: 'BreatheFlow',
            album: t('player.session') || 'Breathing session',
            artwork: [
              { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
              { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
            ],
          });
        }
        navigator.mediaSession.playbackState = 'playing';
        navigator.mediaSession.setActionHandler('pause', () => setPlaying(false));
        navigator.mediaSession.setActionHandler('play', () => setPlaying(true));
        navigator.mediaSession.setActionHandler('stop', () => { setPlaying(false); onClose?.(); });
        registeredHandlers = true;
      }
    } catch (e) {}

    return () => {
      try { if (src) src.stop(); } catch (e) {}
      try { if (gain) gain.disconnect(); } catch (e) {}
      try { if (ctx) ctx.close(); } catch (e) {}
      try {
        if (registeredHandlers && 'mediaSession' in navigator) {
          navigator.mediaSession.playbackState = prevPlaybackState || 'none';
          navigator.mediaSession.metadata = prevMetadata || null;
          ['pause', 'play', 'stop'].forEach((a) => {
            try { navigator.mediaSession.setActionHandler(a, null); } catch (e) {}
          });
        }
      } catch (e) {}
    };
  }, [playing, pattern.id]);

  // Screen Wake Lock — keep the display awake during an active session so
  // the page doesn't freeze when the phone's idle timer fires mid-breath.
  // Released on pause, unmount, or when the OS evicts it (tab hidden). On
  // visibility return we re-request since the lock auto-releases.
  React.useEffect(() => {
    if (!playing) return;
    if (typeof navigator === 'undefined' || !navigator.wakeLock) return;
    let sentinel = null;
    let cancelled = false;
    const acquire = async () => {
      try {
        const s = await navigator.wakeLock.request('screen');
        if (cancelled) { s.release().catch(() => {}); return; }
        sentinel = s;
        setWakeActive(true);
        s.addEventListener?.('release', () => setWakeActive(false));
      } catch (_) { setWakeActive(false); /* user gesture missing, OS denial — ignore */ }
    };
    acquire();
    const onVis = () => { if (document.visibilityState === 'visible') acquire(); };
    document.addEventListener('visibilitychange', onVis);
    return () => {
      cancelled = true;
      setWakeActive(false);
      document.removeEventListener('visibilitychange', onVis);
      if (sentinel) sentinel.release().catch(() => {});
    };
  }, [playing]);

  // Ambient: scoped to this session only. Starts when sound/ambient are on
  // and the session is playing. Pauses with the session, stops on unmount,
  // and the mute toggle shortcircuits without touching persisted settings.
  React.useEffect(() => {
    if (!sound || muted || !playing || !ambientId || ambientId === 'silence') {
      try { bfAmbient.stop(); } catch (e) {}
      return;
    }
    try { bfAmbient.setVolume(ambientVol ?? 0.35); bfAmbient.play(ambientId); } catch (e) {}
    return () => { try { bfAmbient.stop(); } catch (e) {} };
  }, [sound, muted, playing, ambientId, ambientVol]);

  // Preload the active pack's mp3s so the first phase cue is zero-latency.
  // Pack switching unloads every cue id first, then reloads whatever the
  // new pack provides. Packs without mp3s (system, or new locales before
  // they've been generated) simply leave the asset map empty, and cue
  // playback falls through to BFAudio.speak() with the right lang.
  React.useEffect(() => {
    if (!voice) return;
    const packId = voicePack || 'en-drew';
    // Unload everything first — covers the pack-switch case cleanly.
    (window.BF_VOICE_CUES || []).forEach((cueId) => {
      BFAudio.unload('voice.' + cueId).catch(() => {});
    });
    (window.BF_VOICE_CUES || []).forEach((cueId) => {
      const url = window.BFVoicePacks && BFVoicePacks.cueUrl(packId, cueId);
      if (!url) return;
      BFAudio.preload('voice.' + cueId, url, { volume: 0.9 }).catch(() => {});
    });
  }, [voice, voicePack]);

  // Play a voice cue for the active pack. Uses the pre-loaded mp3 when it
  // exists; falls back to BFAudio.speak() with the pack's locale so
  // non-English packs work before their mp3s are generated.
  const cueRef = React.useRef({ voicePack, voice });
  cueRef.current = { voicePack, voice };
  const playCue = React.useCallback((cueId) => {
    const { voicePack: pk, voice: voiceOn } = cueRef.current;
    if (!voiceOn) return;
    const packId = pk || 'en-drew';
    const pack = (window.BF_VOICE_PACKS || {})[packId];
    const ttsText = window.BFVoicePacks
      ? BFVoicePacks.cueText(cueId, pack ? pack.locale : 'en')
      : cueId;
    const ttsLang = pack ? pack.ttsLang : null;
    const speak = () => BFAudio.speak(ttsText, ttsLang ? { lang: ttsLang } : undefined).catch(() => {});
    if (BFAudio.isLoaded('voice.' + cueId)) {
      BFAudio.play('voice.' + cueId, { restart: true }).catch(speak);
    } else {
      speak();
    }
  }, []);

  // Announce "Increasing duration" on each actual step-up. The engine grows
  // phase durations in discrete +stepBy bumps at elapsed = startAfterSec +
  // k*stepEvery (k = 1, 2, …, maxAdd/stepBy). We watch the live bonus via
  // BFProgression.currentBonus and fire the cue every time it crosses up.
  // First fire happens at the FIRST bump (not at t=0).
  React.useEffect(() => {
    if (!voice || !progressionOn) return;
    if (typeof window.BFProgression === 'undefined') return;
    const bonus = window.BFProgression.currentBonus(s.elapsed, progression);
    if (bonus > lastProgressionBonusRef.current) {
      lastProgressionBonusRef.current = bonus;
      playCue('progression');
    }
  }, [s.elapsed, voice, progressionOn, progression, playCue]);

  // Reset the progression-bump tracker whenever the underlying pattern
  // changes (engine also resets elapsed → 0 on pattern.id change).
  React.useEffect(() => {
    lastProgressionBonusRef.current = 0;
  }, [pattern.id]);

  // Voice cue on phase change. Final cycle's inhale plays "Finishing cycle"
  // instead of the normal inhale cue — once per session.
  //
  // `onLastCycle` can flip to true *after* the phase already transitioned to
  // inhale (remaining drops below cycleSec + epsilon mid-inhale). If we gate
  // this effect only on phase change, the finishing cue is missed. So we
  // check the "announce finishing" condition independently of the phase-diff
  // guard.
  React.useEffect(() => {
    if (!playing) return;
    // Engine wraps phaseIdx → 0 (next cycle's inhale) at the same tick it
    // sets s.done = true. Without this guard the wrapped-inhale would
    // double up with BFComplete's "complete" cue — two sounds at session
    // end. Suppress phase cues once done.
    if (s.done) return;

    // Haptics fire on every phase transition independent of the voice flag —
    // a user can have haptics on and voice off (quiet session on the bus).
    if (haptics && s.phase !== lastPhase.current && typeof bfHaptic !== 'undefined') {
      bfHaptic.breathe(s.phase);
    }

    if (!voice) { lastPhase.current = s.phase; return; }

    if (onLastCycle && s.phase === 'inhale' && !lastRoundAnnouncedRef.current) {
      lastRoundAnnouncedRef.current = true;
      lastPhase.current = s.phase;
      playCue('finishing');
      return;
    }

    if (s.phase === lastPhase.current) return;
    lastPhase.current = s.phase;

    const clipId = _bfPhaseClip(s.phase);
    if (clipId) playCue(clipId);
  }, [s.phase, s.done, voice, haptics, playing, onLastCycle, playCue]);

  React.useEffect(() => { if (s.done) onComplete?.(); }, [s.done]);

  const progress = Math.min(1, s.elapsed / totalSeconds);

  return (
    <div style={{
      position: 'absolute', inset: 0,
      background: `radial-gradient(ellipse at 50% 40%, ${bfIsLightTheme(theme) ? `oklch(0.92 0.05 ${accentH} / 0.5)` : `oklch(0.32 0.04 ${accentH} / 0.5)`} 0%, ${theme.bg} 60%)`,
      color: theme.fg, fontFamily: BF_FONTS.sans,
      display: 'flex', flexDirection: 'column',
      overflow: 'hidden',
    }}>
      {/* top bar */}
      {!focus && (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: 'calc(20px + env(safe-area-inset-top, 40px)) 20px 0' }}>
          <button onClick={onClose} style={{ background: theme.cardSoft, border: 'none', width: 40, height: 40, borderRadius: 999, color: theme.fg, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <BFIcon name="close" size={18} />
          </button>
          <div style={{ textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
            <div style={{ fontSize: 11, letterSpacing: 2, textTransform: 'uppercase', color: theme.fgFaint }}>{bfPatternTag(pattern)}</div>
            <div style={{ fontFamily: BF_FONTS.serif, fontSize: 20, fontStyle: 'italic' }}>{bfPatternName(pattern)}</div>
            {wakeActive && (
              <div title="Screen stays on during the session" style={{
                display: 'inline-flex', alignItems: 'center', gap: 5,
                padding: '2px 8px', borderRadius: 999,
                background: bfAccentTint(theme, accentH, 0.14),
                color: bfAccentText(theme, accentH),
                fontFamily: BF_FONTS.sans, fontSize: 9, fontWeight: 600,
                letterSpacing: 0.8, textTransform: 'uppercase',
              }}>
                <span style={{
                  width: 5, height: 5, borderRadius: 999,
                  background: bfAccentText(theme, accentH),
                }}/>
                {t('player.screen_on')}
              </div>
            )}
          </div>
          <button onClick={() => setFocus(true)} title={t('player.focus_mode_title')} aria-label={t('player.enter_focus')} style={{
            display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 14px 8px 8px',
            borderRadius: 999, cursor: 'pointer',
            background: bfAccentTint(theme, accentH, 0.14),
            border: `1px solid ${bfAccentBorder(theme, accentH, 0.4)}`,
            color: bfAccentText(theme, accentH),
            fontFamily: BF_FONTS.sans, fontSize: 11, fontWeight: 600, letterSpacing: 0.8,
            textTransform: 'uppercase',
          }}>
            <span style={{
              width: 28, height: 28, borderRadius: 999,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center', position: 'relative',
            }}>
              <BFProgressRing progress={progress} size={28} color={bfAccentText(theme, accentH)} track={bfIsLightTheme(theme) ? 'rgba(0,0,0,0.12)' : 'rgba(255,255,255,0.15)'} />
            </span>
            {t('player.focus')}
          </button>
        </div>
      )}
      {focus && (
        <button onClick={() => setFocus(false)} title={t('player.exit_focus')} style={{
          position: 'absolute', top: 'calc(30px + env(safe-area-inset-top, 0px))', right: 20,
          width: 36, height: 36, borderRadius: 999, border: 'none', cursor: 'pointer',
          background: theme.cardSoft, color: theme.fgMuted,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          zIndex: 5,
        }}>
          <BFIcon name="close" size={14} />
        </button>
      )}

      {/* visualizer — in focus mode we render only the sphere, no phase
          label / countdown / final-breath pill. Everything text- or number-
          based is stripped so the user can simply gaze at the image. */}
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
        <BreathingVisualizer style={viz} amplitude={s.amplitude} phase={s.phase} phaseT={s.phaseT} accentH={accentH} size={280} theme={theme} />
        {!focus && (
          <div style={{ textAlign: 'center' }}>
            {onLastCycle && (
              <div style={{
                display: 'inline-flex', alignItems: 'center', gap: 6,
                padding: '3px 10px', borderRadius: 999,
                background: bfAccentTint(theme, accentH, 0.18),
                color: bfAccentText(theme, accentH),
                fontFamily: BF_FONTS.sans, fontSize: 10, fontWeight: 600,
                letterSpacing: 1, textTransform: 'uppercase',
                marginBottom: 10,
              }}>
                {t('player.final_breath')}
              </div>
            )}
            <div style={{
              fontFamily: BF_FONTS.serif, fontSize: 32, fontStyle: 'italic',
              color: theme.fg, letterSpacing: -0.5,
              transition: 'opacity .3s',
            }}>{s.phaseLabel}</div>
            <div style={{
              marginTop: 4, fontFamily: BF_FONTS.mono, fontSize: 13,
              color: theme.fgFaint, letterSpacing: 0.5,
            }}>
              {Math.max(0, Math.ceil(s.phaseDuration - s.phaseDuration * s.phaseT))}s
            </div>
          </div>
        )}
      </div>

      {/* bottom stats + controls — hidden in focus mode */}
      {!focus && (
      <div style={{ padding: '0 24px calc(40px + env(safe-area-inset-bottom, 0px))' }}>
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
          padding: '14px 4px', borderTop: `1px solid ${theme.line}`,
          fontFamily: BF_FONTS.mono, fontSize: 12, color: theme.fgMuted,
        }}>
          <div>
            <div style={{ fontSize: 10, letterSpacing: 1, color: theme.fgFaint, textTransform: 'uppercase' }}>{t('player.elapsed')}</div>
            <div style={{ fontSize: 18, color: theme.fg, marginTop: 2 }}>{bfFmt(s.elapsed)}</div>
          </div>
          <div style={{ textAlign: 'center' }}>
            <div style={{ fontSize: 10, letterSpacing: 1, color: theme.fgFaint, textTransform: 'uppercase' }}>{t('player.cycles')}</div>
            <div style={{ fontSize: 18, color: theme.fg, marginTop: 2 }}>{s.cycles}</div>
          </div>
          <div style={{ textAlign: 'right' }}>
            <div style={{ fontSize: 10, letterSpacing: 1, color: theme.fgFaint, textTransform: 'uppercase' }}>{t('player.remaining')}</div>
            <div style={{ fontSize: 18, color: theme.fg, marginTop: 2 }}>{bfFmt(s.remaining)}</div>
          </div>
        </div>

        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 20, marginTop: 22 }}>
          {sound && ambientId && ambientId !== 'silence' && (
            <button onClick={() => setMuted(m => !m)}
              aria-label={muted ? t('player.ambient_unmute') : t('player.ambient_mute')}
              title={muted ? t('player.ambient_muted') : t('player.ambient_mute')}
              style={{
                width: 44, height: 44, borderRadius: 999, border: 'none', cursor: 'pointer',
                background: theme.cardSoft, color: muted ? theme.fgFaint : theme.fgMuted,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>
              <BFIcon name={muted ? 'volumeOff' : 'volume'} size={18} />
            </button>
          )}
          <button onClick={() => setPlaying(p => !p)} style={{
            width: 68, height: 68, borderRadius: 999, border: 'none', cursor: 'pointer',
            background: bfAccentSolid(accentH), color: bfAccentSolidFg(),
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: `0 8px 24px oklch(0.78 0.1 ${accentH} / 0.35)`,
          }}>
            <BFIcon name={playing ? 'pause' : 'play'} size={26} color={bfAccentSolidFg()} strokeWidth={0} />
          </button>
        </div>

        {/* Visualizer switcher — hidden for kids patterns (Balloon, Dragon)
            because they're opinionated about their visual skin. Picking a
            generic shape mid-session would also kill the kid skin's running
            animation, which is jarring for the child. */}
        {pattern && pattern.audience !== 'kids' && (
          <div style={{ display: 'flex', justifyContent: 'center', gap: 4, marginTop: 18, padding: 3, background: theme.cardSoft, borderRadius: 999, width: 'fit-content', margin: '18px auto 0' }}>
            {[
              { id: 'circle',    label: t('player.viz_orb') },
              { id: 'geometric', label: t('player.viz_square') },
              { id: 'flower',    label: t('player.viz_flower') },
            ].map(o => (
              <button key={o.id} onClick={() => { setViz(o.id); setVizStyle?.(o.id); }} style={{
                padding: '7px 14px', borderRadius: 999, border: 'none', cursor: 'pointer',
                background: viz === o.id ? bfAccentSolid(accentH) : 'transparent',
                color: viz === o.id ? bfAccentSolidFg() : theme.fgMuted,
                fontFamily: BF_FONTS.sans, fontSize: 11, fontWeight: 500, letterSpacing: 0.3,
              }}>{o.label}</button>
            ))}
          </div>
        )}

        {progressionOn && (
          <div style={{
            margin: '14px auto 0', display: 'inline-flex', alignItems: 'center', gap: 8,
            padding: '6px 12px', borderRadius: 999,
            background: bfAccentTint(theme, accentH, 0.14),
            color: bfAccentText(theme, accentH),
            fontFamily: BF_FONTS.sans, fontSize: 10, fontWeight: 500, letterSpacing: 0.8,
            textTransform: 'uppercase',
          }}>
            <span style={{ width: 5, height: 5, borderRadius: 999, background: bfAccentText(theme, accentH) }}/>
            {t('player.progression_active')}
          </div>
        )}
      </div>
      )}

      {/* Focus-mode floating pause/play — always reachable during a session */}
      {focus && (
        <button onClick={() => setPlaying(p => !p)}
          aria-label={playing ? t('player.pause') : t('player.play')}
          title={playing ? t('player.pause') : t('player.play')}
          style={{
            position: 'absolute', bottom: 'calc(40px + env(safe-area-inset-bottom, 0px))',
            left: '50%', transform: 'translateX(-50%)',
            width: 56, height: 56, borderRadius: 999, border: 'none', cursor: 'pointer',
            background: bfAccentSolid(accentH), color: bfAccentSolidFg(),
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: `0 8px 24px oklch(0.78 0.1 ${accentH} / 0.35)`,
            zIndex: 5,
          }}>
          <BFIcon name={playing ? 'pause' : 'play'} size={22} color={bfAccentSolidFg()} strokeWidth={0} />
        </button>
      )}
    </div>
  );
}

Object.assign(window, { BFPlayer });
