//! 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>,
    /// piper 依赖的库路径（libespeak-ng.so / libpiper_phonemize.so 所在）
    piper_lib_dir: Option<PathBuf>,
    /// piper 模型 config json 路径（.onnx.json）
    piper_config: Option<PathBuf>,
    /// piper espeak-ng-data 目录路径
    espeak_data_dir: Option<PathBuf>,
}

impl LocalCliBackend {
    pub fn new(tools_dir: PathBuf) -> Self {
        Self {
            tools_dir,
            whisper_lib_dir: None,
            piper_lib_dir: None,
            piper_config: None,
            espeak_data_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
    }

    /// 设置 piper 依赖（库路径、config、espeak-ng-data 目录）
    pub fn with_piper_deps(mut self, lib_dir: PathBuf, config: PathBuf, espeak_data: PathBuf) -> Self {
        self.piper_lib_dir = Some(lib_dir);
        self.piper_config = Some(config);
        self.espeak_data_dir = Some(espeak_data);
        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（合并所有库路径）
    fn with_lib_path(&self, cmd: &mut Command, lib_dir: &Path) {
        let existing = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
        let new_path = if existing.is_empty() {
            lib_dir.to_string_lossy().into_owned()
        } else {
            format!("{}:{}", lib_dir.to_string_lossy(), existing)
        };
        cmd.env("LD_LIBRARY_PATH", new_path);
    }
}

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>
        let tmp_out = audio_path.with_extension("asr.txt");
        let mut cmd = Command::new(self.whisper_cli());
        if let Some(lib_dir) = &self.whisper_lib_dir {
            self.with_lib_path(&mut cmd, lib_dir);
        }
        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             用户当前消息（V1 不带历史，避免模型回显历史内容）
        //
        // V1 边界：会话内上下文由 ChatPipeline 的 sessions 维护，
        // 但 llama-cli --single-turn 模式每次是独立调用，
        // 历史拼接会导致模型把历史当输入回显。V1 简化为无历史单轮对话。
        let _ = history; // V1 不使用历史，参数保留供未来扩展
        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(user_message)
            .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 --single-turn 输出格式：
        //   [banner/logo]
        //   > <user_input>      ← 用户输入回显
        //   [进度动画 |/-\]
        //   <生成的回复>        ← 我们要提取的部分
        //   [空行]
        //   [ Prompt: ... | Generation: ... ]  ← 性能统计
        //
        // 解析策略：找到 "> " 开头的行，跳过进度动画行，取实际回复行
        let raw = String::from_utf8_lossy(&output.stdout);
        let reply = parse_llama_output(&raw);
        Ok(reply)
    }

    fn tts(&self, text: &str, model_path: &Path, output_path: &Path) -> Result<()> {
        // echo <text> | piper -m <model> [-c <config>] -f <output> --output_format wav
        // piper 依赖 libespeak-ng.so / libpiper_phonemize.so，需设 LD_LIBRARY_PATH
        // piper 需要 ESPEAK_DATA_PATH 指向 espeak-ng-data 目录
        let mut cmd = Command::new(self.piper());
        if let Some(lib_dir) = &self.piper_lib_dir {
            self.with_lib_path(&mut cmd, lib_dir);
        }
        if let Some(espeak_data) = &self.espeak_data_dir {
            cmd.env("ESPEAK_DATA_PATH", espeak_data);
        }
        cmd.arg("-m")
            .arg(model_path)
            .arg("-f")
            .arg(output_path)
            .arg("--output_format")
            .arg("wav");

        // piper 需要 -c 指定 .onnx.json config 文件
        if let Some(config) = &self.piper_config {
            cmd.arg("-c").arg(config);
        }

        cmd.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 失败 (exit={}): {}",
                output.status,
                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"
    }
}

/// 解析 llama-cli --single-turn 的 stdout 输出，提取生成的回复文本
///
/// 输出格式：
/// ```text
/// [banner/logo ASCII art]
/// build      : ...
/// model      : ...
/// ...
/// > <user_input>
///
/// [进度动画 |/-\]
/// <生成的回复>
///
/// [ Prompt: ... | Generation: ... ]
///
/// Exiting...
/// ```
fn parse_llama_output(raw: &str) -> String {
    let lines: Vec<&str> = raw.lines().collect();
    let mut reply_lines = Vec::new();
    let mut after_user_input = false;

    for line in &lines {
        let trimmed = line.trim();

        // 跳过空行（但在回复区内保留）
        if trimmed.is_empty() && !after_user_input {
            continue;
        }

        // 找到用户输入回显行 "> xxx"
        if trimmed.starts_with('>') {
            after_user_input = true;
            continue;
        }

        if !after_user_input {
            continue;
        }

        // 遇到性能统计行 [ Prompt: ... ] 停止
        if trimmed.starts_with('[') && trimmed.contains("Generation") {
            break;
        }
        // 遇到 "Exiting..." 停止
        if trimmed == "Exiting..." {
            break;
        }

        // 清理进度动画字符 |/-\ 和退格符
        // llama-cli 的进度动画用退格符 \u{8} 刷新，stdout 捕获后保留为字面退格或 \b 转义
        let cleaned: String = trimmed
            .replace("\\b", "")      // 字面 \b 转义序列
            .replace("\u{8}", "")    // 真正的退格符
            .chars()
            .filter(|c| !"|\\-/".contains(*c))
            .collect::<String>();
        let cleaned_str = cleaned.trim();
        if cleaned_str.is_empty() {
            continue;
        }

        reply_lines.push(cleaned_str.to_string());
    }

    reply_lines.join("\n").trim().to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_llama_output() {
        // 进度动画和回复在同一行，用真正的退格符 \u{8} 分隔
        let bs = "\u{8}"; // backspace
        let raw = format!(
            "  build      : b1-fdb1db8\n\
model      : model_store/llm/qwen2.5-0.5b-q4_k_m.gguf\n\
\n\
> hello\n\
\n\
{bs}-{bs}|{bs}/{bs}-{bs}|{bs}/{bs} 汪！\n\
\n\
[ Prompt: 31.1 t/s | Generation: 26.3 t/s ]\n\
\n\
Exiting...\n"
        );

        let reply = parse_llama_output(&raw);
        assert_eq!(reply, "汪！");
    }
}
