/* ============================================================
   StackedGloss — the signature interaction.

   For any Russian letter / word / phrase / sentence it stacks:
     ┌ Cyrillic                (Golos Text, large)
     ├ ⏟ curly brace           (maps the token to its English gloss)
     ├ phonetic respelling     (mono)
     └ English gloss           (the literal per-word meaning)
   …with the real translation (the *meaning*) shown beneath, plus
   tap-to-hear audio on every token and a karaoke play for the whole.
   ============================================================ */
(function () {
  const { useState, useRef, useEffect, useCallback } = React;

  /* ---- speech ---------------------------------------------------- */
  let RU_VOICE = null;
  function pickVoice() {
    if (!('speechSynthesis' in window)) return null;
    const vs = speechSynthesis.getVoices();
    RU_VOICE = vs.find(v => /ru[-_]RU/i.test(v.lang)) || vs.find(v => /^ru/i.test(v.lang)) || null;
    return RU_VOICE;
  }
  if ('speechSynthesis' in window) {
    pickVoice();
    speechSynthesis.onvoiceschanged = pickVoice;
  }
  function synthSpeak(text, { rate = 0.82, onend } = {}) {
    if (!('speechSynthesis' in window)) { onend && setTimeout(onend, 600); return; }
    try {
      speechSynthesis.cancel();
      const u = new SpeechSynthesisUtterance(text);
      u.lang = 'ru-RU';
      u.rate = rate;
      if (RU_VOICE) u.voice = RU_VOICE;
      if (onend) u.onend = onend;
      speechSynthesis.speak(u);
    } catch (e) { onend && setTimeout(onend, 600); }
  }

  /* 2026-06-05 7:47PM — TTS repoint (doc 03 §A1.5 / PLAN §4).
     Try the cached/generated audio at POST /api/tts first; on {url}
     play it via <Audio> (client-cached by text). On {fallback:true},
     fetch error, or audio error, fall back to speechSynthesis with
     the SAME signature/onend contract so every caller works unchanged.
     Today the server returns {fallback:true} (no OpenAI key), so this
     codes the happy path but exercises the fallback in practice.
     Status: ⚠️ PENDING USER TESTING */
  const _audioCache = new Map();    // text -> object URL
  let _currentAudio = null;
  function stopAudio() {
    if (_currentAudio) { try { _currentAudio.pause(); } catch (e) {} _currentAudio = null; }
  }
  function playUrl(url, text, { onend } = {}) {
    try {
      stopAudio();
      if ('speechSynthesis' in window) speechSynthesis.cancel();
      const a = new Audio(url);
      _currentAudio = a;
      a.onended = () => { if (_currentAudio === a) _currentAudio = null; onend && onend(); };
      a.onerror = () => { if (_currentAudio === a) _currentAudio = null; synthSpeak(text, { onend }); };
      a.play().catch(() => { if (_currentAudio === a) _currentAudio = null; synthSpeak(text, { onend }); });
      return true;
    } catch (e) { synthSpeak(text, { onend }); return false; }
  }
  function speak(text, opts = {}) {
    const { rate = 0.82, onend } = opts;
    if (!text) { onend && setTimeout(onend, 200); return; }
    // cancel any in-flight synth before kicking off async fetch
    if ('speechSynthesis' in window) speechSynthesis.cancel();
    const cached = _audioCache.get(text);
    if (cached) { playUrl(cached, text, { onend }); return; }
    let settled = false;
    fetch('/api/tts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text }),
    })
      .then(r => (r.ok ? r.json() : Promise.reject(new Error('tts ' + r.status))))
      .then(d => {
        if (settled) return;
        settled = true;
        if (d && d.url) { _audioCache.set(text, d.url); playUrl(d.url, text, { onend }); }
        else { synthSpeak(text, { rate, onend }); }   // {fallback:true}
      })
      .catch(() => { if (!settled) { settled = true; synthSpeak(text, { rate, onend }); } });
  }
  window.speakRU = speak;

  /* ---- curly under-brace (stretches to any width) ---------------- */
  function Brace({ color = 'var(--brace)', active }) {
    return (
      <svg viewBox="0 0 100 14" preserveAspectRatio="none"
           style={{ width:'100%', height:13, display:'block', overflow:'visible',
                    transition:'opacity .2s' }}>
        <path d="M2,2 C2,8 5,8 9,8 L44,8 Q50,8 50,13 Q50,8 56,8 L91,8 C95,8 98,8 98,2"
              fill="none"
              stroke={active ? 'var(--primary)' : color}
              strokeWidth={active ? 2.4 : 1.8}
              strokeLinecap="round"
              vectorEffect="non-scaling-stroke"
              style={{ transition:'stroke .18s, stroke-width .18s' }} />
      </svg>
    );
  }

  /* ---- little speaker glyph -------------------------------------- */
  function Speaker({ size = 18, color = 'currentColor', playing }) {
    return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
        <path d="M4 9v6h4l5 4V5L8 9H4z" fill={color} />
        {playing
          ? <>
              <path d="M16 8.5a5 5 0 010 7" stroke={color} strokeWidth="2" strokeLinecap="round">
                <animate attributeName="opacity" values="0.3;1;0.3" dur="1s" repeatCount="indefinite"/>
              </path>
              <path d="M18.5 6a8 8 0 010 12" stroke={color} strokeWidth="2" strokeLinecap="round">
                <animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite"/>
              </path>
            </>
          : <>
              <path d="M16 8.5a5 5 0 010 7" stroke={color} strokeWidth="2" strokeLinecap="round"/>
              <path d="M18.5 6a8 8 0 010 12" stroke={color} strokeWidth="2" strokeLinecap="round" opacity="0.55"/>
            </>}
      </svg>
    );
  }
  window.Speaker = Speaker;

  /* ---- a single stacked token ----------------------------------- */
  function Token({ w, idx, ruSize, active, onTap }) {
    return (
      <button
        onClick={() => onTap(idx)}
        style={{
          appearance:'none', border:'none', background:'transparent',
          padding:'8px 6px 6px', borderRadius:16, cursor:'pointer',
          display:'flex', flexDirection:'column', alignItems:'center',
          gap:3, minWidth:46, position:'relative',
          transition:'background .18s, transform .12s',
          ...(active ? { background:'var(--primary-wash)', transform:'translateY(-1px)' } : null),
        }}>
        {/* Cyrillic */}
        <span style={{
          fontFamily:'"Golos Text", system-ui', fontWeight:600,
          fontSize:ruSize, lineHeight:1.04, letterSpacing:'-0.01em',
          color: active ? 'var(--primary-deep)' : 'var(--ink)',
          transition:'color .18s', whiteSpace:'nowrap',
        }}>{w.ru}</span>
        {/* brace */}
        <div style={{ width:'100%', minWidth:34 }}><Brace active={active} /></div>
        {/* phonetic */}
        <span style={{
          fontFamily:'"JetBrains Mono", monospace', fontSize:11.5, fontWeight:500,
          letterSpacing:'-0.02em', color: active ? 'var(--primary-deep)' : 'var(--muted)',
          whiteSpace:'nowrap', transition:'color .18s',
        }}>{w.phon}</span>
        {/* gloss */}
        <span style={{
          fontFamily:'"Hanken Grotesk", system-ui', fontSize:13.5, fontWeight:600,
          color:'var(--ink)', whiteSpace:'nowrap',
        }}>{w.en}</span>
      </button>
    );
  }

  /* ---- the component -------------------------------------------- */
  function StackedGloss({ item, autoplay = false, compact = false, surface = 'lesson', saveKind }) {
    const [active, setActive] = useState(-1);
    const [playingAll, setPlayingAll] = useState(false);
    const timers = useRef([]);
    const isLetter = item.type === 'letter';
    const isWord = item.type === 'word';
    const ruSize = isLetter ? 60 : isWord ? (compact ? 34 : 42) : (compact ? 26 : 30);

    const clearTimers = () => { timers.current.forEach(clearTimeout); timers.current = []; };
    useEffect(() => () => { clearTimers(); if ('speechSynthesis' in window) speechSynthesis.cancel(); }, []);

    const tapToken = useCallback((i) => {
      clearTimers(); setPlayingAll(false);
      setActive(i);
      const w = item.words[i];
      speak(w.say || w.ru, { onend: () => setActive(a => (a === i ? -1 : a)) });
    }, [item]);

    const playAll = useCallback(() => {
      clearTimers();
      if (playingAll) { speechSynthesis && speechSynthesis.cancel(); setPlayingAll(false); setActive(-1); return; }
      setPlayingAll(true);
      // estimate a per-word timeline from token length (karaoke highlight)
      const lens = item.words.map(w => Math.max(2, w.ru.length));
      const total = lens.reduce((a, b) => a + b, 0);
      const dur = Math.min(4200, 700 + total * 95); // ms
      let t = 250;
      item.words.forEach((w, i) => {
        const slice = (lens[i] / total) * (dur - 250);
        timers.current.push(setTimeout(() => setActive(i), t));
        t += slice;
      });
      timers.current.push(setTimeout(() => { setActive(-1); setPlayingAll(false); }, t + 120));
      speak(item.say || item.ru, { rate: 0.8 });
    }, [item, playingAll]);

    useEffect(() => { if (autoplay) { const id = setTimeout(playAll, 450); return () => clearTimeout(id); } }, []); // eslint-disable-line

    const typeLabel = isLetter ? 'Letter' : isWord ? 'Word' : 'Sentence';

    return (
      <div style={{
        background:'var(--surface)', borderRadius:24, padding:'16px 14px 18px',
        boxShadow:'0 1px 0 var(--line), 0 10px 30px -18px rgba(40,20,10,0.25)',
        border:'1px solid var(--line)',
      }}>
        {/* header row */}
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between',
                      padding:'0 4px 10px' }}>
          <span style={{
            fontFamily:'"JetBrains Mono", monospace', fontSize:10.5, fontWeight:600,
            letterSpacing:'0.14em', textTransform:'uppercase', color:'var(--muted)',
          }}>{typeLabel}{item.note ? ` · ${item.note}` : ''}</span>
          <div style={{ display:'flex', alignItems:'center', gap:4 }}>
          {window.SaveHeart && (
            <window.SaveHeart item={item}
              lemmaId={item.lemma_id != null ? item.lemma_id : null}
              kind={saveKind || (item.lemma_id != null ? 'lemma'
                : (item.type === 'sentence' || item.type === 'phrase') ? 'translation'
                : (item.type === 'letter') ? 'letter' : 'phrase')}
              surface={surface} size={20} />
          )}
          <button onClick={playAll} aria-label="Play"
            style={{
              appearance:'none', border:'none', cursor:'pointer',
              display:'flex', alignItems:'center', gap:7, borderRadius:999,
              padding:'7px 13px 7px 11px',
              background: playingAll ? 'var(--primary)' : 'var(--primary-wash)',
              color: playingAll ? '#fff' : 'var(--primary-deep)',
              fontFamily:'"Hanken Grotesk", system-ui', fontWeight:700, fontSize:12.5,
              transition:'background .18s, color .18s',
            }}>
            <Speaker size={16} color={playingAll ? '#fff' : 'var(--primary-deep)'} playing={playingAll} />
            {playingAll ? 'Playing' : 'Listen'}
          </button>
          </div>
        </div>

        {/* stacked tokens */}
        <div style={{
          display:'flex', flexWrap:'wrap', alignItems:'flex-start',
          justifyContent: item.words.length > 3 ? 'flex-start' : 'center',
          gap: isLetter ? 0 : 4, rowGap:10, padding:'2px 0 4px',
        }}>
          {item.words.map((w, i) => (
            <Token key={i} w={w} idx={i} ruSize={ruSize}
                   active={active === i} onTap={tapToken} />
          ))}
        </div>

        {/* meaning */}
        <div style={{
          marginTop:14, paddingTop:13, borderTop:'1px dashed var(--line)',
          display:'flex', alignItems:'baseline', gap:10,
        }}>
          <span style={{
            fontFamily:'"JetBrains Mono", monospace', fontSize:10, fontWeight:600,
            letterSpacing:'0.12em', textTransform:'uppercase', color:'var(--muted)',
            flexShrink:0, transform:'translateY(-1px)',
          }}>meaning</span>
          <span style={{
            fontFamily:'"Hanken Grotesk", system-ui', fontWeight:600, fontSize:16.5,
            color:'var(--ink)', lineHeight:1.25, textWrap:'pretty', flex:1, minWidth:0,
          }}>{item.translation}</span>
        </div>
      </div>
    );
  }

  window.StackedGloss = StackedGloss;
  window.Brace = Brace;

  /* ---- StackRow — the universal Russian display -----------------
     Cyrillic on top, phonetic respelling, then the English meaning,
     so the learner is never unsure what they're reading. Purely
     presentational; callers add play/check controls around it. */
  function rowPhon(item) {
    return item.phon || (item.words ? item.words.map(x => x.phon).join(' ') : '');
  }
  function StackRow({ item, size = 'md', active, note }) {
    const ru = item.ru, phon = rowPhon(item), en = item.translation || item.en;
    const nt = note !== undefined ? note : item.note;
    const ruSize = size === 'lg' ? 30 : size === 'sm' ? 19 : 24;
    return (
      <div style={{ display:'flex', flexDirection:'column', gap:2, minWidth:0 }}>
        <span style={{ fontFamily:'"Golos Text", system-ui', fontWeight:600, fontSize:ruSize,
          color: active ? 'var(--primary-deep)' : 'var(--ink)', lineHeight:1.12,
          letterSpacing:'-0.01em', textWrap:'pretty' }}>
          {ru}
          {nt && <span style={{ fontSize:13, color:'var(--muted)', fontWeight:600,
            fontFamily:'"Hanken Grotesk", system-ui' }}>  · {nt}</span>}
        </span>
        <span style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: size === 'lg' ? 12.5 : 11,
          fontWeight:600, color: active ? 'var(--primary-deep)' : 'oklch(0.55 0.1 65)', lineHeight:1.2 }}>{phon}</span>
        <span style={{ fontFamily:'"Hanken Grotesk", system-ui', fontSize: size === 'lg' ? 15.5 : 14,
          fontWeight:600, color:'var(--ink)', lineHeight:1.25, textWrap:'pretty' }}>{en}</span>
      </div>
    );
  }
  window.StackRow = StackRow;
  window.rowPhon = rowPhon;
})();
