import { spawn, type ChildProcessByStdio } from "node:child_process"
import { EventEmitter } from "node:events"
import { existsSync } from "node:fs"
import { delimiter, join } from "node:path"
import type { Readable } from "node:stream"

export type VoiceRecorderEvents = {
  data: [Buffer]
  error: [Error]
  end: []
}

/**
 * Captures 16 kHz mono signed 16-bit little-endian PCM via SoX or FFmpeg.
 * When neither tool is available, callers should fall back to renderer MediaDevices capture.
 */
export class VoiceRecorder extends EventEmitter<VoiceRecorderEvents> {
  private process: ChildProcessByStdio<null, Readable, Readable> | undefined
  private running = false

  get active() {
    return this.running
  }

  static detectTool(): { tool: "sox" | "ffmpeg"; command: string } | undefined {
    const sox = which("sox")
    if (sox) return { tool: "sox", command: sox }
    const ffmpeg = which("ffmpeg")
    if (ffmpeg) return { tool: "ffmpeg", command: ffmpeg }
    return
  }

  start() {
    if (this.running) return
    const detected = VoiceRecorder.detectTool()
    if (!detected) {
      this.emit("error", new Error("No recording tool found. Install SoX (winget install sox) or FFmpeg."))
      return
    }

    const child =
      detected.tool === "sox"
        ? spawn(
            detected.command,
            ["-d", "-q", "-r", "16000", "-c", "1", "-e", "signed-integer", "-b", "16", "-t", "raw", "-"],
            { stdio: ["ignore", "pipe", "pipe"], windowsHide: true },
          )
        : spawn(
            detected.command,
            ffmpegArgs(),
            { stdio: ["ignore", "pipe", "pipe"], windowsHide: true },
          )

    this.process = child
    this.running = true

    child.stdout.on("data", (chunk: Buffer) => {
      this.emit("data", chunk)
    })

    child.stderr.on("data", (chunk: Buffer) => {
      const message = chunk.toString("utf8").trim()
      if (!message) return
      if (/error|failed|cannot|unable/i.test(message) && !/Input #|Output #|Stream #|size=/i.test(message)) {
        this.emit("error", new Error(message))
      }
    })

    child.on("error", (error) => {
      this.running = false
      this.emit("error", error instanceof Error ? error : new Error(String(error)))
    })

    child.on("close", () => {
      this.running = false
      this.process = undefined
      this.emit("end")
    })
  }

  stop() {
    if (!this.process) {
      this.running = false
      return
    }
    const child = this.process
    this.process = undefined
    this.running = false
    child.kill("SIGTERM")
    setTimeout(() => {
      if (!child.killed) child.kill("SIGKILL")
    }, 1500).unref?.()
  }
}

function ffmpegArgs() {
  if (process.platform === "win32") {
    return ["-f", "dshow", "-i", "audio=default", "-ac", "1", "-ar", "16000", "-f", "s16le", "-acodec", "pcm_s16le", "pipe:1"]
  }
  if (process.platform === "darwin") {
    return ["-f", "avfoundation", "-i", ":0", "-ac", "1", "-ar", "16000", "-f", "s16le", "-acodec", "pcm_s16le", "pipe:1"]
  }
  return ["-f", "alsa", "-i", "default", "-ac", "1", "-ar", "16000", "-f", "s16le", "-acodec", "pcm_s16le", "pipe:1"]
}

function which(binary: string) {
  const extensions = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""]
  const dirs = (process.env.PATH ?? "").split(delimiter)
  for (const dir of dirs) {
    for (const ext of extensions) {
      const candidate = join(dir, binary + ext)
      if (existsSync(candidate)) return candidate
    }
  }
}
