import { apiKeyEnvName, resolveApiKey, type TranscriptionResult, type VoiceConfig, type VoiceProvider } from "./voice-types"

export type SttProvider = {
  readonly provider: VoiceProvider
  start(onTranscription: (result: TranscriptionResult) => void, onError: (error: Error) => void): Promise<void>
  send(chunk: Buffer): void
  stop(): Promise<string | undefined>
}

export function createSttProvider(config: VoiceConfig): SttProvider {
  if (config.provider === "deepgram") return createDeepgramProvider(config)
  if (config.provider === "groq") return createHttpChunkProvider(config, "groq")
  return createHttpChunkProvider(config, "openai-whisper")
}

function requireKey(provider: VoiceProvider) {
  const key = resolveApiKey(provider)
  if (key) return key
  throw new Error(`Missing ${apiKeyEnvName(provider)}. Set it in the environment before using voice input.`)
}

function createDeepgramProvider(config: VoiceConfig): SttProvider {
  let socket: WebSocket | undefined
  let onTranscription: ((result: TranscriptionResult) => void) | undefined
  let onError: ((error: Error) => void) | undefined
  let finalText = ""

  return {
    provider: "deepgram",
    async start(nextTranscription, nextError) {
      onTranscription = nextTranscription
      onError = nextError
      finalText = ""
      const key = requireKey("deepgram")
      const params = new URLSearchParams({
        model: "nova-2",
        encoding: "linear16",
        sample_rate: "16000",
        channels: "1",
        punctuate: "true",
        interim_results: "true",
        smart_format: "true",
      })
      if (config.language && config.language !== "auto") params.set("language", config.language)

      const ws = new WebSocket(`wss://api.deepgram.com/v1/listen?${params}`, ["token", key])
      socket = ws

      await new Promise<void>((resolve, reject) => {
        const timer = setTimeout(() => reject(new Error("Deepgram connection timed out")), 10_000)
        ws.addEventListener("open", () => {
          clearTimeout(timer)
          resolve()
        })
        ws.addEventListener("error", () => {
          clearTimeout(timer)
          reject(new Error("Deepgram WebSocket failed to connect"))
        })
      })

      ws.addEventListener("message", (event) => {
        const payload = typeof event.data === "string" ? event.data : undefined
        if (!payload) return
        const parsed = JSON.parse(payload) as {
          type?: string
          channel?: { alternatives?: Array<{ transcript?: string; confidence?: number }> }
          is_final?: boolean
        }
        if (parsed.type && parsed.type !== "Results") return
        const alternative = parsed.channel?.alternatives?.[0]
        const text = alternative?.transcript?.trim()
        if (!text) return
        if (parsed.is_final) {
          finalText = joinText(finalText, text)
          onTranscription?.({ text: finalText, isFinal: true, confidence: alternative?.confidence })
          return
        }
        onTranscription?.({ text: joinText(finalText, text), isFinal: false, confidence: alternative?.confidence })
      })

      ws.addEventListener("error", () => {
        onError?.(new Error("Deepgram WebSocket error"))
      })
    },
    send(chunk) {
      if (socket?.readyState !== WebSocket.OPEN) return
      socket.send(chunk)
    },
    async stop() {
      const ws = socket
      socket = undefined
      if (!ws) return finalText || undefined
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: "CloseStream" }))
        await new Promise<void>((resolve) => {
          const timer = setTimeout(resolve, 1000)
          ws.addEventListener("close", () => {
            clearTimeout(timer)
            resolve()
          })
        })
      }
      if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close()
      return finalText || undefined
    },
  }
}

function createHttpChunkProvider(config: VoiceConfig, provider: "groq" | "openai-whisper"): SttProvider {
  const chunkDurationMs = provider === "groq" ? (config as VoiceConfig & { chunkDurationMs?: number }).chunkDurationMs ?? 5000 : 10_000
  const bytesPerMs = (16000 * 2) / 1000
  const chunkBytes = Math.floor(chunkDurationMs * bytesPerMs)

  let buffer = Buffer.alloc(0)
  let parts: string[] = []
  let onTranscription: ((result: TranscriptionResult) => void) | undefined
  let onError: ((error: Error) => void) | undefined
  let busy = Promise.resolve()
  let active = false

  const flush = (force = false) => {
    if (!active) return
    if (!force && buffer.length < chunkBytes) return
    if (buffer.length === 0) return
    const payload = buffer
    buffer = Buffer.alloc(0)
    busy = busy
      .then(async () => {
        const text = await transcribeHttp(provider, payload, config.language)
        if (!text) return
        parts.push(text)
        onTranscription?.({ text: parts.join(" "), isFinal: false })
      })
      .catch((error) => {
        onError?.(error instanceof Error ? error : new Error(String(error)))
      })
  }

  return {
    provider,
    async start(nextTranscription, nextError) {
      requireKey(provider)
      onTranscription = nextTranscription
      onError = nextError
      buffer = Buffer.alloc(0)
      parts = []
      busy = Promise.resolve()
      active = true
    },
    send(chunk) {
      if (!active) return
      buffer = Buffer.concat([buffer, chunk])
      flush(false)
    },
    async stop() {
      active = false
      flush(true)
      await busy
      const text = parts.join(" ").trim()
      if (text) onTranscription?.({ text, isFinal: true })
      return text || undefined
    },
  }
}

async function transcribeHttp(provider: "groq" | "openai-whisper", pcm: Buffer, language: string) {
  const key = requireKey(provider)
  const wav = pcmToWav(pcm, 16000, 1)
  const form = new FormData()
  form.append("file", new Blob([new Uint8Array(wav)], { type: "audio/wav" }), "audio.wav")
  form.append("model", provider === "groq" ? "whisper-large-v3-turbo" : "whisper-1")
  form.append("response_format", "json")
  if (language && language !== "auto") form.append("language", language)

  const url =
    provider === "groq"
      ? "https://api.groq.com/openai/v1/audio/transcriptions"
      : "https://api.openai.com/v1/audio/transcriptions"

  const response = await fetch(url, {
    method: "POST",
    headers: { Authorization: `Bearer ${key}` },
    body: form,
  })

  if (!response.ok) {
    const body = await response.text().catch(() => "")
    throw new Error(`${provider} transcription failed (${response.status}): ${body || response.statusText}`)
  }

  const json = (await response.json()) as { text?: string }
  return json.text?.trim() ?? ""
}

function pcmToWav(pcm: Buffer, sampleRate: number, channels: number) {
  const header = Buffer.alloc(44)
  const dataSize = pcm.length
  header.write("RIFF", 0)
  header.writeUInt32LE(36 + dataSize, 4)
  header.write("WAVE", 8)
  header.write("fmt ", 12)
  header.writeUInt32LE(16, 16)
  header.writeUInt16LE(1, 20)
  header.writeUInt16LE(channels, 22)
  header.writeUInt32LE(sampleRate, 24)
  header.writeUInt32LE(sampleRate * channels * 2, 28)
  header.writeUInt16LE(channels * 2, 32)
  header.writeUInt16LE(16, 34)
  header.write("data", 36)
  header.writeUInt32LE(dataSize, 40)
  return Buffer.concat([header, pcm])
}

function joinText(left: string, right: string) {
  if (!left) return right
  if (!right) return left
  return `${left} ${right}`
}
