/**
 * Voice backend - manages the Python voice server process.
 * Communicates via JSON-line protocol over stdin/stdout.
 */
import { spawn } from "child_process";
import { EventEmitter } from "events";
import { existsSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { createInterface } from "readline";
const VOICE_DIR = join(homedir(), ".avis", "agent", "voice");
const VENV_DIR = join(VOICE_DIR, ".venv");
const PYTHON_PATH = join(VENV_DIR, "bin", "python3");
export class VoiceBackend extends EventEmitter {
    constructor() {
        super(...arguments);
        this.process = null;
        this.readline = null;
        this._ready = false;
        this._hasProfile = false;
        this._listening = false;
        this._speaking = false;
        this._installing = false;
    }
    get ready() {
        return this._ready;
    }
    get hasProfile() {
        return this._hasProfile;
    }
    get listening() {
        return this._listening;
    }
    get speaking() {
        return this._speaking;
    }
    get installing() {
        return this._installing;
    }
    /**
     * Ensure the Python virtual environment and dependencies are installed.
     */
    async ensureInstalled() {
        if (existsSync(PYTHON_PATH)) {
            return true;
        }
        this._installing = true;
        try {
            // Create venv using uv
            await this.runCommand("uv", ["venv", VENV_DIR, "--python", "3.13"]);
            // Install dependencies from pyproject.toml
            await this.runCommand("uv", ["pip", "install", "--python", PYTHON_PATH, "-e", VOICE_DIR]);
            return true;
        }
        catch (error) {
            this.emit("error", `Failed to install voice dependencies: ${error}`);
            return false;
        }
        finally {
            this._installing = false;
        }
    }
    runCommand(cmd, args) {
        return new Promise((resolve, reject) => {
            const proc = spawn(cmd, args, { stdio: "pipe" });
            let stderr = "";
            proc.stderr?.on("data", (data) => {
                stderr += data.toString();
            });
            proc.on("close", (code) => {
                if (code === 0) {
                    resolve();
                }
                else {
                    reject(new Error(`${cmd} ${args[0]} failed (code ${code}): ${stderr.slice(-200)}`));
                }
            });
            proc.on("error", reject);
        });
    }
    /**
     * Start the voice backend process.
     */
    async start() {
        if (this.process) {
            return true;
        }
        if (!existsSync(PYTHON_PATH)) {
            const installed = await this.ensureInstalled();
            if (!installed) {
                return false;
            }
        }
        return new Promise((resolve) => {
            this.process = spawn(PYTHON_PATH, ["-m", "avis_voice.server"], {
                stdio: ["pipe", "pipe", "pipe"],
                cwd: VOICE_DIR,
                env: { ...process.env },
            });
            this.readline = createInterface({ input: this.process.stdout });
            this.readline.on("line", (line) => {
                try {
                    const event = JSON.parse(line);
                    this.handleEvent(event);
                    // Resolve start() once we get the ready event
                    if (event.event === "ready") {
                        resolve(true);
                    }
                }
                catch {
                    // Ignore malformed lines
                }
            });
            this.process.stderr?.on("data", (data) => {
                // Log stderr but don't crash
                const msg = data.toString().trim();
                if (msg) {
                    this.emit("error", msg);
                }
            });
            this.process.on("close", (code) => {
                this._ready = false;
                this._listening = false;
                this.process = null;
                this.readline = null;
                this.emit("exit", code);
            });
            this.process.on("error", (err) => {
                this.emit("error", `Voice process error: ${err.message}`);
                resolve(false);
            });
            // Timeout if no ready event
            setTimeout(() => {
                if (!this._ready) {
                    this.emit("error", "Voice backend startup timeout");
                    resolve(false);
                }
            }, 30000);
        });
    }
    handleEvent(event) {
        switch (event.event) {
            case "ready":
                this._ready = true;
                this._hasProfile = event.has_profile;
                this.emit("ready", event.has_profile);
                break;
            case "recording":
                this.emit("recording", event.duration);
                break;
            case "enrolled":
                this._hasProfile = true;
                this.emit("enrolled", event.profile);
                break;
            case "listening":
                this._listening = true;
                this.emit("listening");
                break;
            case "speech_start":
                this.emit("speech_start");
                break;
            case "speech_end":
                this.emit("speech_end");
                break;
            case "transcription":
                this.emit("transcription", {
                    text: event.text,
                    verified: event.verified,
                    confidence: event.confidence,
                    sttLatencyUs: event.stt_latency_us,
                });
                break;
            case "error":
                this.emit("error", event.message);
                break;
            case "rejected":
                this.emit("rejected", event.reason, event.confidence ?? 0);
                break;
            case "speaking":
                this._speaking = true;
                this.emit("speaking", event.text);
                break;
            case "tts_timing":
                this.emit("tts_timing", event.stage, event.latency_us);
                break;
            case "speak_done":
                this._speaking = false;
                this.emit("speak_done", event.tts_total_us);
                break;
            case "speak_interrupted":
                this._speaking = false;
                this.emit("speak_interrupted", event.tts_total_us);
                break;
            case "stopped":
                this._listening = false;
                this.emit("stopped");
                break;
        }
    }
    send(cmd) {
        if (!this.process?.stdin?.writable) {
            this.emit("error", "Voice backend not running");
            return;
        }
        this.process.stdin.write(`${JSON.stringify(cmd)}\n`);
    }
    /**
     * Enroll voice profile by recording a sample.
     */
    enroll(duration = 10) {
        this.send({ cmd: "enroll", duration });
    }
    /**
     * Start listening for voice input.
     */
    listen() {
        this.send({ cmd: "listen" });
    }
    /**
     * Stop listening.
     */
    stopListening() {
        this.send({ cmd: "stop" });
    }
    /**
     * Speak text via Hume AI TTS.
     */
    speak(text) {
        this.send({ cmd: "speak", text });
    }
    /**
     * Interrupt TTS playback.
     */
    stopSpeaking() {
        this.send({ cmd: "stop_speaking" });
    }
    /**
     * Shut down the voice backend.
     */
    async shutdown() {
        if (!this.process)
            return;
        const proc = this.process;
        this.process = null;
        this._ready = false;
        this._listening = false;
        this._speaking = false;
        // Try graceful quit first
        try {
            if (proc.stdin?.writable) {
                proc.stdin.write(`${JSON.stringify({ cmd: "quit" })}\n`);
            }
        }
        catch {
            // stdin may already be closed
        }
        await new Promise((resolve) => {
            // Hard kill after 2s if graceful quit didn't work
            const timeout = setTimeout(() => {
                try {
                    proc.kill("SIGKILL");
                }
                catch {
                    // already dead
                }
                resolve();
            }, 2000);
            proc.on("close", () => {
                clearTimeout(timeout);
                resolve();
            });
        });
        if (this.readline) {
            this.readline.close();
            this.readline = null;
        }
    }
}
//# sourceMappingURL=voice-backend.js.map