import { ref, watch, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
import { synthesiseSpeech } from '@/api/client'
import { useVoiceAudio } from '@/composables/useVoiceAudio'

/** Minimum stripped character count to bother synthesizing. */
const MIN_CHARS = 3

/** Matches sentence-terminal punctuation followed by whitespace or end-of-string. */
const SENTENCE_BOUNDARY = /[.!?]+(?=\s|$)/

function stripMarkdown(text: string): string {
  return text
    .replace(/```[\s\S]*?```/g, '')
    .replace(/`[^`]+`/g, (m) => m.slice(1, -1))
    .replace(/#{1,6}\s+/g, '')
    .replace(/\*\*([^*]+)\*\*/g, '$1')
    .replace(/\*([^*]+)\*/g, '$1')
    .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
    .replace(/^\s*[-*+]\s+/gm, '')
    .replace(/\n{2,}/g, ' ')
    .trim()
}

/**
 * Extract completed sentences from `text` using SENTENCE_BOUNDARY.
 * Returns the sentences found and the unconsumed remainder.
 */
function extractSentences(text: string): { sentences: string[]; remainder: string } {
  const sentences: string[] = []
  let remaining = text
  let match: RegExpExecArray | null

  while ((match = SENTENCE_BOUNDARY.exec(remaining)) !== null) {
    const boundary = match.index + match[0].length
    const sentence = remaining.slice(0, boundary).trim()
    if (sentence) sentences.push(sentence)
    remaining = remaining.slice(boundary)
  }

  return { sentences, remainder: remaining }
}

export interface UseStreamingTtsOptions {
  streamingContent: Ref<string> | ComputedRef<string>
  streaming: Ref<boolean> | ComputedRef<boolean>
  enabled: Ref<boolean> | ComputedRef<boolean>
}

export interface UseStreamingTtsReturn {
  /** True while any synthesis request is in-flight or audio is playing. */
  speaking: ComputedRef<boolean>
  /** Cancel all in-flight synthesis/playback and clear the queue. */
  stop: () => void
  /** Speak a complete text through the same sentence-chunking pipeline. */
  speak: (text: string) => void
}

export function useStreamingTts(options: UseStreamingTtsOptions): UseStreamingTtsReturn {
  const { streamingContent, streaming, enabled } = options
  const audio = useVoiceAudio()

  let sentenceBuffer = ''
  let lastSeenLength = 0
  let abortId = 0
  let playQueue: Promise<void> = Promise.resolve()
  const pendingCount = ref(0)

  const speaking = computed(() => pendingCount.value > 0 || audio.playing.value)

  function stop(): void {
    abortId++
    sentenceBuffer = ''
    lastSeenLength = 0
    playQueue = Promise.resolve()
    audio.stop()
    pendingCount.value = 0
  }

  async function enqueueSentence(sentence: string, myAbortId: number): Promise<void> {
    const stripped = stripMarkdown(sentence)
    if (stripped.length < MIN_CHARS) return

    pendingCount.value++
    let blob: Blob | null = null
    try {
      blob = await synthesiseSpeech(stripped)
    } catch (e) {
      const errMsg = e instanceof Error ? e.message : String(e)
      console.warn('[StreamingTTS] Synthesis failed, retrying', {
        chars: stripped.length,
        preview: stripped.slice(0, 80),
        error: errMsg,
      })
      try {
        blob = await synthesiseSpeech(stripped)
      } catch (e2) {
        const errMsg2 = e2 instanceof Error ? e2.message : String(e2)
        console.warn('[StreamingTTS] Retry failed, sentence dropped', {
          chars: stripped.length,
          preview: stripped.slice(0, 80),
          error: errMsg2,
        })
      }
    } finally {
      pendingCount.value--
    }

    if (!blob) return

    // Capture blob for the closure — TS can't narrow after async gap
    const resolvedBlob = blob
    playQueue = playQueue.then(async () => {
      if (abortId !== myAbortId) return
      await audio.play(resolvedBlob)
    })
  }

  function dispatchBuffer(flush: boolean): void {
    if (!enabled.value) return
    const myAbortId = abortId
    const { sentences, remainder } = extractSentences(sentenceBuffer)
    sentenceBuffer = flush ? '' : remainder
    for (const sentence of sentences) {
      enqueueSentence(sentence, myAbortId)
    }
    if (flush && remainder.trim().length >= MIN_CHARS) {
      enqueueSentence(remainder.trim(), myAbortId)
    }
  }

  // Watch accumulating content — extract new characters since last check
  watch(streamingContent, (newContent) => {
    if (!enabled.value) return
    const delta = newContent.slice(lastSeenLength)
    lastSeenLength = newContent.length
    sentenceBuffer += delta
    dispatchBuffer(false)
  })

  // Watch streaming flag — stop on new message start, flush on end
  watch(streaming, (isStreaming) => {
    if (!enabled.value) return
    if (isStreaming) {
      // New message starting — cancel previous response's audio
      stop()
    } else {
      // Stream ended — flush any remaining fragment
      dispatchBuffer(true)
      lastSeenLength = 0
    }
  })

  function speak(text: string): void {
    stop()
    if (!text.trim()) return
    const myAbortId = abortId
    const { sentences, remainder } = extractSentences(text)
    for (const sentence of sentences) {
      enqueueSentence(sentence, myAbortId)
    }
    if (remainder.trim().length >= MIN_CHARS) {
      enqueueSentence(remainder.trim(), myAbortId)
    }
  }

  return { speaking, stop, speak }
}
