import { ref, onUnmounted, type Ref } from 'vue'
import { synthesize } from '../services/speechService'

export type PlaySource =
  | { kind: 'tts'; text: string }
  | { kind: 'blob'; blob: Blob }

export interface AudioPlayer {
  /** Id of the message currently playing (audio element fired `playing`). */
  playingId: Readonly<Ref<string | null>>
  /** Id of the message whose synthesis or play() is in flight. */
  loadingId: Readonly<Ref<string | null>>
  /** Id of the message whose last playback attempt failed. Sticky until next play(). */
  errorId: Readonly<Ref<string | null>>

  play(id: string, source: PlaySource): Promise<void>
  stop(): void
}

/**
 * Single audio playback owner for the dialogue view.
 *
 * Structural guarantees:
 *  - At most one HTMLAudioElement / one in-flight synthesis at a time.
 *    Every play() begins by aborting & pausing the prior session.
 *  - Cached MP3 blobs (one per AI messageId) live in-memory; URLs are
 *    revoked on view unmount.
 *  - Errors collapse to a single per-id state. The view renders a retry
 *    affordance; clicking the same play button re-enters play().
 */
export function useAudioPlayer(): AudioPlayer {
  const playingId = ref<string | null>(null)
  const loadingId = ref<string | null>(null)
  const errorId = ref<string | null>(null)

  // Closure-private state. Not reactive on purpose.
  let currentAudio: HTMLAudioElement | null = null
  let synthAbort: AbortController | null = null
  const ttsCache = new Map<string, Blob>()
  const cachedUrls: string[] = []

  function clearCurrentAudio() {
    if (currentAudio) {
      currentAudio.onplaying = null
      currentAudio.onended = null
      currentAudio.onerror = null
      try { currentAudio.pause() } catch { /* ignore */ }
      currentAudio = null
    }
  }

  function failPlayback(id: string, reason: unknown) {
    if (loadingId.value === id) loadingId.value = null
    if (playingId.value === id) playingId.value = null
    errorId.value = id
    console.warn('[audio-player] playback failed:', id, reason)
  }

  async function play(id: string, source: PlaySource): Promise<void> {
    // A fresh attempt — drop stale error.
    errorId.value = null

    // Abort any in-flight synthesis and pause any current audio.
    stop()

    loadingId.value = id

    try {
      let blob: Blob
      if (source.kind === 'blob') {
        blob = source.blob
      }
      else {
        const cached = ttsCache.get(id)
        if (cached) {
          blob = cached
        }
        else {
          synthAbort = new AbortController()
          blob = await synthesize(source.text, synthAbort.signal)
          synthAbort = null
          // We may have been interrupted while awaiting (loadingId changed).
          if (loadingId.value !== id) return
          ttsCache.set(id, blob)
        }
      }

      const url = URL.createObjectURL(blob)
      cachedUrls.push(url)

      const audio = new Audio(url)
      currentAudio = audio

      audio.onplaying = () => {
        if (loadingId.value === id) loadingId.value = null
        playingId.value = id
      }
      audio.onended = () => {
        if (currentAudio === audio) currentAudio = null
        if (playingId.value === id) playingId.value = null
      }
      audio.onerror = () => {
        if (currentAudio === audio) currentAudio = null
        failPlayback(id, audio.error)
      }

      try {
        await audio.play()
      }
      catch (err) {
        if (currentAudio === audio) currentAudio = null
        // pause() during a pending play() rejects with AbortError —
        // that is an intentional interrupt (from stop()), not a failure.
        if (err instanceof DOMException && err.name === 'AbortError') return
        // Most often: autoplay policy blocked the call.
        failPlayback(id, err)
      }
    }
    catch (err) {
      // Synthesis path errors. AbortError fires when stop() was called
      // mid-synthesis — that is a normal interrupt, not a failure.
      synthAbort = null
      if (err instanceof Error && err.name === 'AbortError') {
        // loadingId was already cleared synchronously by stop(); if a newer
        // play() has since set it again, that value belongs to that call,
        // not this aborted one. Do nothing here.
        return
      }
      failPlayback(id, err)
    }
  }

  function stop(): void {
    if (synthAbort) {
      synthAbort.abort()
      synthAbort = null
    }
    clearCurrentAudio()
    loadingId.value = null
    playingId.value = null
    // Note: errorId is intentionally left alone here. It is only
    // cleared at the start of a new play() attempt.
  }

  onUnmounted(() => {
    stop()
    for (const url of cachedUrls) {
      try { URL.revokeObjectURL(url) } catch { /* ignore */ }
    }
    cachedUrls.length = 0
    ttsCache.clear()
  })

  return {
    playingId,
    loadingId,
    errorId,
    play,
    stop,
  }
}
