import { VoiceRecorder } from "./voice-recorder"
import { createSttProvider, type SttProvider } from "./voice-stt"
import {
  DEFAULT_VOICE_CONFIG,
  type RecordingState,
  type TranscriptionResult,
  type VoiceConfig,
  type VoiceError,
} from "./voice-types"

export type VoiceManagerCallbacks = {
  onState: (state: RecordingState) => void
  onTranscription: (result: TranscriptionResult) => void
  onError: (error: VoiceError) => void
  /** Request renderer MediaDevices capture when SoX/FFmpeg is unavailable. */
  onCaptureMode: (mode: "native" | "renderer") => void
}

export class VoiceManager {
  private config: VoiceConfig = { ...DEFAULT_VOICE_CONFIG }
  private state: RecordingState = "idle"
  private recorder: VoiceRecorder | undefined
  private stt: SttProvider | undefined
  private captureMode: "native" | "renderer" = "native"
  private toggleArmed = false
  private lastPressAt = 0
  private callbacks: VoiceManagerCallbacks

  constructor(callbacks: VoiceManagerCallbacks) {
    this.callbacks = callbacks
  }

  getConfig() {
    return { ...this.config }
  }

  setConfig(patch: Partial<VoiceConfig>) {
    this.config = {
      ...this.config,
      ...patch,
      provider: patch.provider ?? this.config.provider,
      language: patch.language ?? this.config.language,
      hotkey: patch.hotkey ?? this.config.hotkey,
      enabled: patch.enabled ?? this.config.enabled,
      commands: patch.commands ?? this.config.commands,
      autoSend: patch.autoSend ?? this.config.autoSend,
    }
    return this.getConfig()
  }

  getState() {
    return this.state
  }

  async start() {
    if (!this.config.enabled) return
    if (this.state === "recording" || this.state === "processing") return

    this.setState("recording")
    const hasNative = !!VoiceRecorder.detectTool()
    this.captureMode = hasNative ? "native" : "renderer"
    this.callbacks.onCaptureMode(this.captureMode)

    try {
      this.stt = createSttProvider(this.config)
      await this.stt.start(
        (result) => this.callbacks.onTranscription(result),
        (error) => this.fail("stt-failed", error.message),
      )
    } catch (error) {
      this.fail("no-api-key", error instanceof Error ? error.message : String(error))
      return
    }

    if (this.captureMode === "native") {
      this.recorder = new VoiceRecorder()
      this.recorder.on("data", (chunk) => this.stt?.send(chunk))
      this.recorder.on("error", (error) => {
        // Fall back to renderer capture if native recorder fails at runtime
        this.recorder?.stop()
        this.recorder = undefined
        this.captureMode = "renderer"
        this.callbacks.onCaptureMode("renderer")
        this.callbacks.onError({ code: "no-recorder", message: error.message })
      })
      this.recorder.start()
    }
  }

  async stop() {
    if (this.state !== "recording") return
    this.setState("processing")
    this.recorder?.stop()
    this.recorder = undefined

    const finalText = await this.stt?.stop().catch((error) => {
      this.fail("stt-failed", error instanceof Error ? error.message : String(error))
      return undefined
    })
    this.stt = undefined

    if (finalText) {
      this.callbacks.onTranscription({ text: finalText, isFinal: true })
    }
    this.setState("idle")
    this.callbacks.onCaptureMode("native")
  }

  async toggle() {
    if (this.state === "recording") {
      await this.stop()
      return
    }
    await this.start()
  }

  /** Push-to-talk press. Double-press within 400ms arms toggle mode. */
  async press() {
    const now = Date.now()
    if (now - this.lastPressAt < 400) {
      this.toggleArmed = true
      this.lastPressAt = now
      await this.toggle()
      return
    }
    this.lastPressAt = now
    this.toggleArmed = false
    await this.start()
  }

  /** Push-to-talk release. Ignored when toggle mode is armed. */
  async release() {
    if (this.toggleArmed) return
    await this.stop()
  }

  ingestAudio(chunk: ArrayBuffer | Buffer) {
    if (this.state !== "recording") return
    if (this.captureMode !== "renderer") return
    const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
    this.stt?.send(buffer)
  }

  dispose() {
    this.recorder?.stop()
    this.recorder = undefined
    void this.stt?.stop()
    this.stt = undefined
    this.setState("idle")
  }

  private setState(state: RecordingState) {
    this.state = state
    this.callbacks.onState(state)
  }

  private fail(code: VoiceError["code"], message: string) {
    this.recorder?.stop()
    this.recorder = undefined
    void this.stt?.stop()
    this.stt = undefined
    this.setState("idle")
    this.callbacks.onError({ code, message })
  }
}
