//! AI 后端 trait + 本地命令行实现
//!
//! 抽象 ASR/LLM/TTS 三层，支持本地命令行（LocalCliBackend）和未来云端后端。
//! V1 只实现 local；cloud 配置时返回"暂未支持"错误。

use anyhow::{bail, Result};
use std::path::{Path, PathBuf};
use std::process::Command;

/// AI 后端抽象 trait
pub trait AiBackend: Send {
    /// 语音转文字 (ASR)
    /// - audio_path: 16kHz mono wav 临时文件
    /// - model_path: whisper.cpp 模型 (ggml-tiny.bin 等)
    fn asr(&self, audio_path: &Path, model_path: &Path) -> Result<String>;

    /// LLM 对话
    /// - model_path: llama.cpp GGUF 模型
    /// - persona: 角色人设 system prompt
    /// - user_message: 本轮用户输入
    /// - history: 历史轮次 (role, content)
    /// - max_tokens: 最大回复 token (0=默认 128)
    fn llm_chat(
        &self,
        model_path: &Path,
        persona: &str,
        user_message: &str,
        history: &[(String, String)],
        max_tokens: u16,
    ) -> Result<String>;

    /// 文字转语音 (TTS)
    /// - text: 要合成的文本
    /// - model_path: piper 模型目录/文件
    /// - output_path: 输出 wav 文件路径
    fn tts(&self, text: &str, model_path: &Path, output_path: &Path) -> Result<()>;

    /// 后端标识（local / cloud）
    fn backend_name(&self) -> &str;
}

/// 本地命令行后端
///
/// 通过子进程调用 whisper-cli / llama-cli / piper 二进制。
/// Spike (2026-07-03) 验证的命令行参数固化于此。
pub struct LocalCliBackend {
    /// 工具目录（含 whisper-cli, llama-cli, piper 二进制）
    tools_dir: PathBuf,
    /// whisper-cli 依赖的库路径（libggml*.so 所在）
    whisper_lib_dir: Option<PathBuf>,
}

impl LocalCliBackend {
    pub fn new(tools_dir: PathBuf) -> Self {
        Self {
            tools_dir,
            whisper_lib_dir: None,
        }
    }

    /// 设置 whisper-cli 依赖库路径（部署时由 deploy_ai.sh 标定）
    pub fn with_whisper_lib_dir(mut self, lib_dir: PathBuf) -> Self {
        self.whisper_lib_dir = Some(lib_dir);
        self
    }

    fn whisper_cli(&self) -> PathBuf {
        self.tools_dir.join("whisper-cli")
    }

    fn llama_cli(&self) -> PathBuf {
        self.tools_dir.join("llama-cli")
    }

    fn piper(&self) -> PathBuf {
        self.tools_dir.join("piper")
    }

    /// 给命令设置 LD_LIBRARY_PATH（whisper-cli 依赖 libggml*.so）
    fn with_env(&self, cmd: &mut Command) {
        if let Some(lib_dir) = &self.whisper_lib_dir {
            cmd.env("LD_LIBRARY_PATH", lib_dir);
        }
    }
}

impl AiBackend for LocalCliBackend {
    fn asr(&self, audio_path: &Path, model_path: &Path) -> Result<String> {
        // whisper-cli -m <model> -f <audio> -l zh --no-timestamps -otxt -of <tmp>
        // 输出到 <tmp>.txt，读取后返回
        let tmp_out = audio_path.with_extension("asr.txt");
        let mut cmd = Command::new(self.whisper_cli());
        self.with_env(&mut cmd);
        let output = cmd
            .arg("-m")
            .arg(model_path)
            .arg("-f")
            .arg(audio_path)
            .arg("-l")
            .arg("zh")
            .arg("--no-timestamps")
            .arg("-otxt")
            .arg("-of")
            .arg(&tmp_out)
            .output()?;

        if !output.status.success() {
            bail!(
                "whisper-cli 失败: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }

        let txt_path = tmp_out.with_extension("txt");
        let text = std::fs::read_to_string(&txt_path)?;
        let _ = std::fs::remove_file(&txt_path);
        Ok(text.trim().to_string())
    }

    fn llm_chat(
        &self,
        model_path: &Path,
        persona: &str,
        user_message: &str,
        history: &[(String, String)],
        max_tokens: u16,
    ) -> Result<String> {
        // 对齐 spike (2026-07-03) 验证的调用方式：
        // llama-cli --single-turn --no-warmup -sys <persona> -p <user_msg> -n <tokens> -t 2 -c 1024 --temp 0.7
        //
        // 关键参数：
        //   --single-turn  单轮模式，生成完即退出（否则进交互模式等待 stdin，永远不退出）
        //   --no-warmup    跳过预热（减少延迟）
        //   -sys           system prompt（角色人设）
        //   -p             用户输入（只传当前消息，历史通过 context 另传，V1 简化）
        //
        // 历史上下文 V1 简化处理：拼到 -p 里（小模型 ctx 1024 有限，只保留最近 2 轮）
        let mut prompt = String::new();
        for (role, content) in history.iter().rev().take(4) {
            // 逆序取最近 2 轮（4 条消息），正序拼接
            prompt.insert_str(0, &format!("{content}\n"));
        }
        prompt.push_str(user_message);

        let tokens = if max_tokens == 0 { 128 } else { max_tokens as u32 };

        let output = Command::new(self.llama_cli())
            .arg("--single-turn")
            .arg("--no-warmup")
            .arg("-m")
            .arg(model_path)
            .arg("-sys")
            .arg(persona)
            .arg("-p")
            .arg(&prompt)
            .arg("-n")
            .arg(tokens.to_string())
            .arg("-t")
            .arg("2") // 锁大核 (spike 结论: t=2 最优)
            .arg("-c")
            .arg("1024") // 限死上下文 (spike 警告: 默认 ctx 吃 1-3.3G)
            .arg("--temp")
            .arg("0.7")
            .arg("--no-display-prompt")
            .output()?;

        if !output.status.success() {
            bail!(
                "llama-cli 失败 (exit={}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }

        // llama-cli 输出到 stdout，取生成部分
        let reply = String::from_utf8_lossy(&output.stdout);
        Ok(reply.trim().to_string())
    }

    fn tts(&self, text: &str, model_path: &Path, output_path: &Path) -> Result<()> {
        // echo <text> | piper -m <model> -f <output> --output_format wav
        let mut cmd = Command::new(self.piper());
        cmd.arg("-m")
            .arg(model_path)
            .arg("-f")
            .arg(output_path)
            .arg("--output_format")
            .arg("wav")
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::piped());

        let mut child = cmd.spawn()?;
        if let Some(mut stdin) = child.stdin.take() {
            use std::io::Write;
            stdin.write_all(text.as_bytes())?;
        }
        let output = child.wait_with_output()?;

        if !output.status.success() {
            bail!(
                "piper 失败: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    fn backend_name(&self) -> &str {
        "local"
    }
}

/// 云端后端（V1 仅占位，返回"暂未支持"）
pub struct CloudBackend;

impl AiBackend for CloudBackend {
    fn asr(&self, _audio_path: &Path, _model_path: &Path) -> Result<String> {
        bail!("云端 ASR 暂未支持 (V1 仅实现 local)")
    }

    fn llm_chat(
        &self,
        _model_path: &Path,
        _persona: &str,
        _user_message: &str,
        _history: &[(String, String)],
        _max_tokens: u16,
    ) -> Result<String> {
        bail!("云端 LLM 暂未支持 (V1 仅实现 local)")
    }

    fn tts(&self, _text: &str, _model_path: &Path, _output_path: &Path) -> Result<()> {
        bail!("云端 TTS 暂未支持 (V1 仅实现 local)")
    }

    fn backend_name(&self) -> &str {
        "cloud"
    }
}
