// Read-aloud for an ESL writer: hear how an English word or passage sounds.
//
// Primary path is server-side neural TTS (Piper, proxied at /api/tts) — natural,
// consistent across devices, and far better than the browser's built-in voices
// (which fall back to robotic espeak on Chromium). If the server route is absent
// (TTS disabled) or unreachable, we fall back to the browser's Web Speech API so
// the buttons still do something. No model or network is strictly required.

// speechSupported reports whether read-aloud can do anything at all. Audio
// playback is universal, so as long as we can construct an Audio element OR the
// Web Speech API exists, the buttons should show. The server path is tried at
// call time and degrades on its own.
export function speechSupported(): boolean {
  if (typeof window === 'undefined') return false
  return typeof window.Audio === 'function' || 'speechSynthesis' in window
}

// webSpeechSupported gates the fallback path specifically.
function webSpeechSupported(): boolean {
  return typeof window !== 'undefined' && 'speechSynthesis' in window && 'SpeechSynthesisUtterance' in window
}

// current holds the in-flight server-audio element and its object URL so a rapid
// re-tap can stop and replace it (mirroring speechSynthesis.cancel()).
let current: { audio: HTMLAudioElement; url: string } | null = null

// requestSeq increments on every speak() call so a slow fetch that resolves after
// a newer tap can detect it's stale and bow out instead of double-playing.
let requestSeq = 0

function stopCurrent(): void {
  if (current) {
    current.audio.pause()
    URL.revokeObjectURL(current.url)
    current = null
  }
  if (webSpeechSupported()) window.speechSynthesis.cancel()
}

// pickVoice prefers an exact locale match (e.g. en-US), then any voice in the
// same language family (en-*). Returns undefined to let the engine default.
function pickVoice(lang: string): SpeechSynthesisVoice | undefined {
  const voices = window.speechSynthesis.getVoices()
  const base = lang.split('-')[0]
  return voices.find((v) => v.lang === lang) || voices.find((v) => v.lang?.startsWith(base))
}

// speakWebSpeech is the fallback: the browser's built-in synthesizer. A touch
// slower than default so learners can follow along.
function speakWebSpeech(text: string, lang: string): void {
  if (!webSpeechSupported()) return
  const synth = window.speechSynthesis
  synth.cancel()
  const utterance = new SpeechSynthesisUtterance(text)
  utterance.lang = lang
  const voice = pickVoice(lang)
  if (voice) utterance.voice = voice
  utterance.rate = 0.95
  synth.speak(utterance)
}

// detectLang guesses a BCP-47 locale from the text so the right Piper voice is
// chosen. Any Han/kana character routes to Chinese (zh-CN) — a selection is
// essentially never mixed at the level that matters, and reading a Chinese
// passage with the English voice spells each character out ("Chinese letter…").
// Everything else defaults to US English, the language she's learning.
const CJK = /[぀-ヿ㐀-䶿一-鿿豈-﫿ｦ-ﾟ]/
export function detectLang(text: string): string {
  return CJK.test(text) ? 'zh-CN' : 'en-US'
}

// speak reads `text` aloud, cancelling anything already in flight so rapid taps
// don't queue up. `lang` defaults to a guess from the text (Chinese vs English)
// so callers can just pass the selection; pass an explicit locale to override.
// It tries the server's neural voice first and silently falls back to the browser
// voice if that's unavailable (route off, network error, or a 404 for a language
// with no configured voice).
export function speak(text: string, lang = detectLang(text)): void {
  if (!text.trim()) return
  stopCurrent()
  const seq = ++requestSeq

  fetch('/api/tts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text, lang }),
  })
    .then((res) => {
      if (!res.ok) throw new Error(`tts ${res.status}`)
      return res.blob()
    })
    .then((blob) => {
      // A newer tap superseded this one while the fetch was in flight — drop it.
      if (seq !== requestSeq) return
      const url = URL.createObjectURL(blob)
      const audio = new Audio(url)
      current = { audio, url }
      // Release the blob once playback finishes (or errors) to avoid leaking.
      const cleanup = () => {
        if (current?.audio === audio) {
          URL.revokeObjectURL(url)
          current = null
        }
      }
      audio.addEventListener('ended', cleanup)
      audio.addEventListener('error', cleanup)
      return audio.play()
    })
    .catch(() => {
      // Server TTS unavailable for this request — use the browser voice instead,
      // unless a newer tap has already superseded this one.
      if (seq !== requestSeq) return
      speakWebSpeech(text, lang)
    })
}
