// ReadAloud.jsx — "Read it for me" podcast-style narration of a summary.
// Uses the browser's built-in speech synthesis. Reads a short intro, then the
// summary headline, overview, and each section in turn — highlighting whatever
// block is currently being spoken so it reads like a guided listen-through.

const RA_SUPPORTED = typeof window !== 'undefined' && 'speechSynthesis' in window;

// The "keep nudging resume()" workaround targets a Chromium-only bug where long
// synthesis sessions silently pause; Gecko (Firefox) and WebKit (Safari) don't
// have it and can stutter if resume() is called repeatedly, so we gate on engine.
const RA_IS_CHROMIUM = typeof navigator !== 'undefined' && /(Chrome|Chromium|Edg|CriOS|OPR)/.test(navigator.userAgent || '');

// Pick the most human-sounding English voice. The big lever for "not robotic"
// is choosing a high-quality voice (Natural/Neural/Online/Premium/Enhanced or a
// network voice) over the default local synthesizer. We score and pick the best.
//
// RELIABILITY FIRST: we strongly prefer LOCAL (on-device) voices. Network voices
// like "Google US English" (localService:false) can report speaking:true yet emit
// NO audio in embedded/cross-origin contexts — the #1 cause of "I hear nothing".
// So each narrator maps to an ordered list of common LOCAL voices per OS
// (Mac: Samantha/Alex; Windows: Zira/David; etc.), and network voices are used
// only as a last resort when the device has no local English voice at all.
const RA_PREFERRED = {
  female: ['Samantha', 'Microsoft Zira', 'Karen', 'Serena', 'Moira', 'Tessa', 'Fiona',
    'Victoria', 'Catherine', 'Susan', 'Microsoft Aria Online (Natural)', 'Google US English'],
  male: ['Daniel', 'Alex', 'Microsoft David', 'Aaron', 'Arthur', 'Oliver', 'Fred', 'Tom',
    'Rishi', 'Microsoft Guy Online (Natural)', 'Google UK English Male']
};

function pickVoice(voices, gender) {
  if (!voices || !voices.length) return null;
  const en = voices.filter((v) => /^en(-|_|$)/i.test(v.lang || ''));
  const base = en.length ? en : voices;
  // Prefer local/on-device voices; only consider network voices if there are no
  // local ones (some locked-down systems only expose network voices).
  const local = base.filter((v) => v.localService !== false);
  const pool = local.length ? local : base;

  // 1) Deterministic preferred-voice match within the (local) pool.
  const prefs = RA_PREFERRED[gender] || RA_PREFERRED.female;
  for (const name of prefs) {
    const lname = name.toLowerCase();
    const exact = pool.find((v) => (v.name || '').toLowerCase() === lname);
    if (exact) return exact;
    const partial = pool.find((v) => (v.name || '').toLowerCase().includes(lname));
    if (partial) return partial;
  }

  // 2) Fallback: score by name/quality heuristics for whatever IS installed.
  const robotic = /(zarvox|bahh|bells|boing|bubbles|cellos|deranged|hysterical|trinoids|whisper|wobble|organ|albert|bad news|good news|jester|junior|kathy|princess|ralph|grandma|grandpa|rocko|reed|eddy|flo|sandy|shelley|superstar|espeak|compact|e-speak|robot|pico)/i;
  // Name heuristics span Mac (Samantha/Alex…), Windows SAPI (David/Zira/Hazel…),
  // Edge "Online (Natural)" (Aria/Guy/Christopher…), and common en-GB/IN voices,
  // so a sensible male/female narrator is chosen on any browser/OS.
  const MALE = /(\bmale\b|alex|daniel|david|mark|guy|davis|tom|oliver|ryan|brian|arthur|aaron|eric|james|george|fred|nathan|christopher|allen|aiden|liam|william|jacob|richard|ravi|sean|brandon|andrew|jason|tony|roger|paul|scott|gary|adam|matthew|google uk english male)/i;
  const FEMALE = /(\bfemale\b|samantha|aria|jenny|ava|allison|zoe|serena|karen|tessa|susan|joanna|salli|emma|ellen|nicky|victoria|moira|fiona|kate|sonia|libby|michelle|zira|hazel|catherine|linda|heera|eva|sara|nancy|amber|ashley|ana|hortense|google us english|google uk english female)/i;
  const score = (v) => {
    const n = (v.name || '').toLowerCase();
    let s = 0;
    if (robotic.test(n)) s -= 60;
    if (v.localService !== false) s += 30;                            // local voices reliably emit audio
    if (/(natural|neural|premium|enhanced)/.test(n)) s += 18;         // higher-quality local variants
    if (/siri/.test(n)) s += 22;
    if (/^en-us/i.test(v.lang || '')) s += 6;
    // Gender preference — strongly bias toward the requested gender.
    if (gender === 'male') { if (MALE.test(n)) s += 30; if (FEMALE.test(n)) s -= 28; }
    else if (gender === 'female') { if (FEMALE.test(n)) s += 30; if (MALE.test(n)) s -= 28; }
    return s;
  };
  let best = null, bestScore = -Infinity;
  pool.forEach((v) => { const s = score(v); if (s > bestScore) { bestScore = s; best = v; } });
  return best || pool[0];
}

// Split a block of prose into short utterance-sized sentences. Keeping each
// utterance short sidesteps the ~15s cutoff bug in some browsers and gives the
// progress bar something to move against.
function toSentences(text) {
  if (!text) return [];
  return String(text)
    .replace(/\s+/g, ' ')
    .match(/[^.!?]+[.!?]*/g)
    ?.map((s) => s.trim())
    .filter(Boolean) || [String(text)];
}

function useVoices() {
  const [voices, setVoices] = React.useState(() => (RA_SUPPORTED ? window.speechSynthesis.getVoices() : []));
  React.useEffect(() => {
    if (!RA_SUPPORTED) return;
    const load = () => setVoices(window.speechSynthesis.getVoices());
    load();
    window.speechSynthesis.addEventListener('voiceschanged', load);
    return () => window.speechSynthesis.removeEventListener('voiceschanged', load);
  }, []);
  return voices;
}

// Calm, neutral pitch — 1 = the voice's natural pitch. We keep it right around
// neutral so it reads like a steady, warm friend rather than high or robotic.
const RA_PITCH = 1.0;

const RA_RATES = [
  { id: 1, label: '1×' },
  { id: 1.25, label: '1.25×' },
  { id: 1.5, label: '1.5×' },
  { id: 2, label: '2×' }
];

// Two friendly named voices the listener can choose between.
const RA_VOICES = [
  { id: 'ellen', label: 'Ellen', gender: 'female' },
  { id: 'allen', label: 'Allen', gender: 'male' }
];

// Format a duration in seconds as m:ss for the "ready" badge.
function fmtDur(sec) {
  if (!sec || sec < 1) return '';
  const m = Math.floor(sec / 60), s = Math.round(sec % 60);
  return m + ':' + String(s).padStart(2, '0');
}

// blocks: [{ key, text }]  — onActive(key|null) highlights the matching block.
// materialId ties this player to its pre-generated narration in the voice backend.
function ReadAloudPlayer({ blocks, topicName, materialId, onActive }) {
  const voices = useVoices();
  // Pre-generated narration asset (rendered + stored by VoiceBackend at boot).
  const va = (window.useVoiceAsset && materialId)
    ? window.useVoiceAsset(materialId, blocks, topicName)
    : { status: 'ready', asset: null, ensure: () => Promise.resolve() };
  const [open, setOpen] = React.useState(false);          // player expanded?
  const [status, setStatus] = React.useState('idle');      // idle | playing | paused | done
  const [rate, setRate] = React.useState(1);
  const [unitIdx, setUnitIdx] = React.useState(0);
  const [prog, setProg] = React.useState(0);          // smooth 0..1 progress
  const [note, setNote] = React.useState('');         // '' | 'blocked'
  const [voicePref, setVoicePref] = React.useState('ellen'); // 'ellen' (female) | 'allen' (male)

  // Build the flat narration queue: an opening line, then each block split into
  // sentences. Each unit remembers which block it belongs to (for highlighting).
  const units = React.useMemo(() => {
    // Prefer the stored narration package so playback matches what was generated.
    if (va.asset && va.asset.units && va.asset.units.length) return va.asset.units;
    const out = [{ blockKey: 'opening', text: `Okay, let's take this gently. Here's your summary of ${topicName || 'this topic'}, one idea at a time.` }];
    blocks.forEach((b) => toSentences(b.text).forEach((s) => out.push({ blockKey: b.key, text: s })));
    return out;
  }, [blocks, topicName, va.asset]);

  const rateRef = React.useRef(rate); rateRef.current = rate;
  const idxRef = React.useRef(0);
  const playingRef = React.useRef(false);
  const voiceRef = React.useRef(null);
  const voicePrefRef = React.useRef(voicePref); voicePrefRef.current = voicePref;
  voiceRef.current = pickVoice(voices, voicePref === 'allen' ? 'male' : 'female');

  const total = units.length;

  const timerRef = React.useRef(null);       // advance timer for the current unit
  const unitStartRef = React.useRef(0);      // when the current unit started (ms)
  const unitDurRef = React.useRef(1);        // estimated duration of current unit (ms)
  const startedRef = React.useRef(false);    // did the audio engine actually fire onstart?
  const clearTimer = () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } };

  const stopEngine = React.useCallback(() => {
    playingRef.current = false;
    clearTimer();
    if (RA_SUPPORTED) { try { window.speechSynthesis.cancel(); } catch (e) {} }
  }, []);

  // Estimated reading time for one unit at the current rate (ms). This is what
  // actually drives the playthrough, so progress and highlighting advance even
  // where the speech engine never fires end events (e.g. sandboxed preview
  // iframes) or is silent. ~2.6 words/sec ≈ a calm narrator at 1×.
  const unitMs = (text) => {
    const words = (text || '').trim().split(/\s+/).filter(Boolean).length;
    return Math.max(1100, Math.round((words / (2.6 * rateRef.current)) * 1000) + 320);
  };

  // Move to unit i: highlight it and speak it. Advancement is driven by the
  // speech engine's real end event so the highlight + progress stay in sync with
  // the audio. A generous timer is kept only as a silent safety net, so the
  // playthrough can never get permanently stuck if an end event is dropped.
  const speakFrom = React.useCallback((i) => {
    clearTimer();
    if (i >= total) {
      playingRef.current = false;
      setStatus('done');
      onActive && onActive(null);
      return;
    }
    idxRef.current = i;
    setUnitIdx(i);
    const u = units[i];
    onActive && onActive(u.blockKey === 'opening' ? null : u.blockKey);

    const ms = unitMs(u.text);
    unitStartRef.current = Date.now();
    unitDurRef.current = ms;

    let moved = false;
    const advance = () => { if (moved) return; moved = true; clearTimer(); if (playingRef.current) speakFrom(i + 1); };

    if (RA_SUPPORTED) {
      try {
        const utt = new SpeechSynthesisUtterance(u.text);
        const v = voiceRef.current;
        if (v) { utt.voice = v; utt.lang = v.lang || 'en-US'; } else { utt.lang = 'en-US'; }
        utt.rate = rateRef.current; utt.pitch = RA_PITCH; utt.volume = 1;
        utt.onstart = () => { startedRef.current = true; unitStartRef.current = Date.now(); };
        utt.onend = () => { if (playingRef.current) advance(); };       // real audio timing
        utt.onerror = () => { if (playingRef.current) advance(); };
        window.speechSynthesis.speak(utt);
      } catch (e) {}
    }

    // Safety net only: well past the spoken length, so it never cuts off audio.
    timerRef.current = setTimeout(advance, Math.max(2400, Math.round(ms * 2.2) + 1500));
  }, [total, units, onActive]);

  // Begin (or resume) playback at unit i.
  const begin = React.useCallback((i) => {
    setNote('');
    startedRef.current = false;
    playingRef.current = true;
    setStatus('playing');
    setOpen(true);
    clearTimer();
    const busy = RA_SUPPORTED && (window.speechSynthesis.speaking || window.speechSynthesis.pending);
    if (busy) {
      // Mid-session restart/speed/voice change: cancel, then start on the next
      // tick so the cancel has settled before the first speak().
      try { window.speechSynthesis.cancel(); } catch (e) {}
      timerRef.current = setTimeout(() => { if (playingRef.current) speakFrom(i); }, 110);
    } else {
      // Fresh start, still inside the click handler — speak synchronously so the
      // user gesture is preserved (browsers, esp. Safari, require it for audio).
      if (RA_SUPPORTED) { try { window.speechSynthesis.resume(); } catch (e) {} }
      speakFrom(i);
    }
  }, [speakFrom]);

  const onPlayPause = () => {
    if (status === 'playing') {
      // Pause: stop the clock and any audio; resume re-speaks the current line.
      clearTimer();
      if (RA_SUPPORTED) { try { window.speechSynthesis.cancel(); } catch (e) {} }
      playingRef.current = false;
      setStatus('paused');
    } else {
      begin(status === 'done' ? 0 : idxRef.current);
    }
  };

  const restart = () => begin(0);

  const close = () => {
    stopEngine();
    setStatus('idle');
    setOpen(false);
    setUnitIdx(0);
    setProg(0);
    idxRef.current = 0;
    unitStartRef.current = 0;
    setNote('');
    onActive && onActive(null);
  };

  // Changing speed mid-listen: re-speak the current sentence at the new rate.
  const changeRate = (r) => {
    setRate(r);
    rateRef.current = r;
    if (status === 'playing') begin(idxRef.current);
  };

  // Switching narrator (Ellen ↔ Allen): re-speak the current sentence in the new voice.
  const changeVoice = (pref) => {
    setVoicePref(pref);
    voicePrefRef.current = pref;
    voiceRef.current = pickVoice(voices, pref === 'allen' ? 'male' : 'female');
    if (status === 'playing') begin(idxRef.current);
  };

  // Smoothly interpolate the progress bar within each unit so it keeps moving
  // even on a long sentence — otherwise it looks frozen between unit advances.
  React.useEffect(() => {
    if (status !== 'playing') return;
    const t = setInterval(() => {
      const frac = Math.min(1, Math.max(0, (Date.now() - unitStartRef.current) / Math.max(1, unitDurRef.current)));
      setProg(Math.min(1, (idxRef.current + frac) / Math.max(1, total)));
    }, 120);
    return () => clearInterval(t);
  }, [status, total]);
  React.useEffect(() => { if (status === 'done') setProg(1); }, [status]);

  // Chromium silently pauses long synthesis sessions — nudge it to keep going.
  // Skipped on Firefox/Safari where it isn't needed and can cause stutter.
  React.useEffect(() => {
    if (!RA_SUPPORTED || !RA_IS_CHROMIUM || status !== 'playing') return;
    const t = setInterval(() => {
      if (window.speechSynthesis.speaking && !window.speechSynthesis.paused) {
        try { window.speechSynthesis.resume(); } catch (e) {}
      }
    }, 9000);
    return () => clearInterval(t);
  }, [status]);

  // Clean up on unmount / when the summary changes.
  React.useEffect(() => () => stopEngine(), [stopEngine]);
  // Reset only when the SUMMARY's content actually changes — keyed on a stable
  // content signature, not the blocks array reference (which can change on
  // re-render and would otherwise cancel in-progress playback).
  const contentSig = React.useMemo(() => (blocks || []).map((b) => b.key + ':' + b.text).join('|'), [blocks]);
  const sigRef = React.useRef(contentSig);
  React.useEffect(() => {
    if (sigRef.current !== contentSig) { sigRef.current = contentSig; close(); }
  }, [contentSig]);

  const pct = total ? Math.min(100, Math.round((status === 'done' ? 1 : prog) * 100)) : 0;
  const playing = status === 'playing';

  if (!RA_SUPPORTED) {
    return (
      <div className="ra-cta" style={{ opacity: .7 }}>
        <span className="ra-badge"><Icon name="headphones" size={17} /></span>
        <div style={{ flex: 1 }}>
          <div className="ra-cta-title">Read it for me</div>
          <div className="ra-cta-sub">Audio narration isn’t supported in this browser.</div>
        </div>
      </div>
    );
  }

  // Start playback. We ALWAYS begin synchronously inside the click so the user
  // gesture is preserved (Chrome/Safari require it to allow audio). Pre-generation
  // /caching runs in the background and is not needed to play — the narration
  // units are built directly from the summary text.
  const handleStart = () => {
    begin(0);
    try { va.ensure && va.ensure(); } catch (e) {}
  };

  // Collapsed: the call-to-action button — reflects the backend's ready state.
  if (!open) {
    const ready = va.status === 'ready';
    const generating = va.status === 'generating';
    const dur = ready && va.asset ? fmtDur(va.asset.durationSec) : '';
    const sub = ready
      ? `Narration ready${dur ? ' · ' + dur : ''} · voiced by Ellen & Allen`
      : generating
        ? 'Preparing narration…'
        : 'Listen to a podcast-style narration of this summary.';
    return (
      <button className="ra-cta as-btn" onClick={handleStart}>
        <span className="ra-badge">
          <Icon name="headphones" size={17} />
          {ready && <span className="ra-ready-dot" aria-hidden="true"><Icon name="check" size={9} /></span>}
        </span>
        <div style={{ flex: 1, textAlign: 'left' }}>
          <div className="ra-cta-title">Read it for me</div>
          <div className={'ra-cta-sub' + (generating ? ' ra-prep' : '')}>{sub}</div>
        </div>
        {generating
          ? <span className="ra-cta-play"><span className="ra-spin" aria-hidden="true" /></span>
          : <span className="ra-cta-play"><Icon name="play" size={16} /></span>}
      </button>
    );
  }

  // Expanded: the mini player.
  return (
    <div className="ra-player" role="group" aria-label="Summary narration player">
      <div className="ra-row">
        <button className={'ra-play' + (playing ? ' on' : '')} onClick={onPlayPause}
          aria-label={playing ? 'Pause' : 'Play'}>
          <Icon name={playing ? 'pause' : 'play'} size={20} />
        </button>

        <div className="ra-body">
          <div className="ra-line">
            <span className={'ra-eq' + (playing ? ' on' : '')} aria-hidden="true"><i></i><i></i><i></i><i></i></span>
            <span className="ra-status">
              {status === 'done' ? 'Finished' : playing ? 'Now playing' : 'Paused'}
              <span className="ra-dot">·</span>
              <span className="ra-topic">{topicName || 'Summary'}</span>
            </span>
            <span className="ra-time mono">{pct}%</span>
          </div>
          <div className="ra-track"><span className="ra-fill" style={{ width: pct + '%' }} /></div>
        </div>

        <button className="ra-mini" onClick={restart} title="Start over" aria-label="Start over"><Icon name="skip-back" size={15} /></button>
        <button className="ra-mini" onClick={close} title="Stop" aria-label="Stop"><Icon name="x" size={16} /></button>
      </div>

      <div className="ra-tools">
        <div className="ra-seg-group">
          <span className="ra-seg-lab"><Icon name="user" size={12} /> Voice</span>
          <div className="ra-seg">
            {RA_VOICES.map((v) =>
              <button key={v.id} className={voicePref === v.id ? 'on' : ''} onClick={() => changeVoice(v.id)}>{v.label}</button>
            )}
          </div>
        </div>
        <div className="ra-seg-group">
          <span className="ra-seg-lab"><Icon name="clock" size={12} /> Speed</span>
          <div className="ra-seg">
            {RA_RATES.map((r) =>
              <button key={r.id} className={rate === r.id ? 'on' : ''} onClick={() => changeRate(r.id)}>{r.label}</button>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

window.ReadAloudPlayer = ReadAloudPlayer;

// ── Single-shot "Listen" button ──────────────────────────────────────────────
// A compact button that speaks one piece of text (a flashcard question or
// answer). Reuses the same natural-voice engine. Starting one button stops any
// other, so question/answer audio never overlap.
const raBus = (() => {
  const subs = new Set();
  return { sub: (fn) => { subs.add(fn); return () => subs.delete(fn); }, emit: (owner) => subs.forEach((fn) => fn(owner)) };
})();
let raBtnSeq = 0;

function RaSpeakButton({ text, label = 'Listen', stopLabel = 'Stop', voicePref = 'ellen', className = 'btn sm' }) {
  const voices = useVoices();
  const [speaking, setSpeaking] = React.useState(false);
  const idRef = React.useRef('rb' + (++raBtnSeq));
  const genRef = React.useRef(0);
  const tRef = React.useRef(null);
  const speakingRef = React.useRef(false);
  speakingRef.current = speaking;

  // When another Listen button starts, reset this one's UI.
  React.useEffect(() => raBus.sub((owner) => { if (owner !== idRef.current) { genRef.current++; if (tRef.current) { clearTimeout(tRef.current); tRef.current = null; } setSpeaking(false); } }), []);
  // Stop audio if this button unmounts (e.g. the card flips or advances).
  React.useEffect(() => () => { genRef.current++; if (tRef.current) clearTimeout(tRef.current); if (speakingRef.current && RA_SUPPORTED) window.speechSynthesis.cancel(); }, []);

  const stop = () => { genRef.current++; if (tRef.current) { clearTimeout(tRef.current); tRef.current = null; } setSpeaking(false); if (RA_SUPPORTED) window.speechSynthesis.cancel(); };

  const speak = (e) => {
    if (e) e.stopPropagation();           // don't flip the card
    if (!RA_SUPPORTED || !text) return;
    if (speakingRef.current) { stop(); return; }
    const busy = window.speechSynthesis.speaking || window.speechSynthesis.pending;
    window.speechSynthesis.cancel();      // stop any other narration
    raBus.emit(idRef.current);
    const myGen = ++genRef.current;
    setSpeaking(true);
    const voice = pickVoice(voices, voicePref === 'allen' ? 'male' : 'female');
    const parts = toSentences(text);
    let k = 0;
    const next = () => {
      if (tRef.current) { clearTimeout(tRef.current); tRef.current = null; }
      if (genRef.current !== myGen) return;
      if (k >= parts.length) { setSpeaking(false); return; }
      const part = parts[k++];
      // Best-effort audio.
      try {
        const utt = new SpeechSynthesisUtterance(part);
        if (voice) { utt.voice = voice; utt.lang = voice.lang || 'en-US'; } else utt.lang = 'en-US';
        utt.rate = 1; utt.pitch = RA_PITCH; utt.volume = 1;
        window.speechSynthesis.speak(utt);
      } catch (_) {}
      // Advance on a clock so the button always completes, even if the speech
      // engine is silent or never fires end events (sandboxed previews).
      const words = part.trim().split(/\s+/).filter(Boolean).length;
      const ms = Math.max(1000, Math.round((words / 2.6) * 1000) + 280);
      tRef.current = setTimeout(() => { if (genRef.current === myGen) next(); }, ms);
    };
    if (busy) { setTimeout(() => { if (genRef.current === myGen) next(); }, 110); }
    else { try { window.speechSynthesis.resume(); } catch (_) {} next(); }
  };

  if (!RA_SUPPORTED) return null;
  return (
    <button type="button" className={className + (speaking ? ' ra-listening' : '')} onClick={speak}
      aria-pressed={speaking} title={speaking ? stopLabel : label}>
      <Icon name={speaking ? 'pause' : 'volume'} size={15} /> {speaking ? stopLabel : label}
    </button>
  );
}

window.RaSpeakButton = RaSpeakButton;
