//! 环境感知系统 — 启动时一次性探测 OS/shell/工具版本/编码,全局缓存,注入 system_prompt。
//!
//! 设计目标:让 LLM 准确生成跨平台命令。LLM 训练数据 Unix 多,易生成 macOS/Linux 语法
//! 命令(PowerShell 5 不支持 `&&`、Windows 路径分隔符 `\`、GBK 终端中文乱码等),通过
//! 把当前平台的真实环境(操作系统/Shell/工具版本)拼进 system_prompt,LLM 即可生成与
//! 当前平台兼容的命令,从根上治「LLM 跨平台命令幻觉」。
//!
//! 缓存策略:`OnceLock` 全局单例,首次 `detect()` 探测,后续调用零开销。探测本身在
//! `spawn_blocking` 中执行(执行 `tool --version` 等阻塞 IO),避免阻塞 tokio runtime。
//! 探测失败的字段设 None/默认值,不向上传播错误(环境感知是 best-effort 增强,不应
//! 阻断主流程)。

use std::sync::OnceLock;

/// 环境快照 — 系统环境的不可变视图。
#[derive(Debug, Clone, serde::Serialize)]
pub struct EnvSnapshot {
    /// 操作系统族:"windows" / "macos" / "linux"
    pub os: String,
    /// OS 版本:"11" / "Ubuntu 22.04"(探测失败为空串)
    pub os_version: String,
    /// Shell 名:"powershell" / "pwsh" / "bash" / "zsh" / "sh"
    pub shell: String,
    /// 路径分隔符:"\\"(Windows) / "/"(Unix)
    pub path_sep: String,
    /// 终端编码:"utf-8" / "gbk"(Windows 中文常见 GBK,致 LLM 输出乱码)
    pub encoding: String,
    /// 工具版本(逐个探测,缺失为 None)
    pub tools: ToolVersions,
}

/// 各开发工具的版本信息。
#[derive(Debug, Clone, serde::Serialize, Default)]
pub struct ToolVersions {
    pub python: Option<String>,
    pub node: Option<String>,
    pub rust: Option<String>,
    pub go: Option<String>,
    pub docker: Option<String>,
    pub git: Option<String>,
}

impl EnvSnapshot {
    /// 探测环境(惰性,OnceLock 全局缓存,整个进程生命周期只探一次)。
    ///
    /// 返回 `&'static EnvSnapshot` —— 引用静态存储,可安全地长存于 loop 不变量中。
    /// 探测在 `spawn_blocking` 中同步执行(`tool --version` 是阻塞 IO),不卡 runtime。
    pub async fn detect() -> &'static EnvSnapshot {
        static SNAPSHOT: OnceLock<EnvSnapshot> = OnceLock::new();
        if let Some(snap) = SNAPSHOT.get() {
            return snap;
        }
        // 首次探测:同步逻辑包到 spawn_blocking,避免阻塞 async runtime。
        // probe_version 内 std::process::Command::output() 无 timeout,
        // Windows 上 python/node 若是 Microsoft Store App Execution Alias(用户未装但开了
        // "应用执行别名"),`python --version` 触发 Store 重定向、process 不退出 → output()
        // 永久阻塞 → spawn_blocking 线程永不返回 → detect().await 永久挂 → run_agentic_loop
        // 卡在 EnvSnapshot::detect() 调用(agentic/mod.rs),后端日志断在"意图收敛工具"后,
        // 前端表现为「发消息完全无回应然后卡死」(trace.log 实测 3 次发送全卡于此)。
        // 修复: spawn_blocking 外包 5s timeout,超时返回 fallback 并 set 进 OnceLock(后续命中
        // 缓存不再 probe),run_agentic_loop 最多等 5s 后继续,不再永久卡。
        // 注: 超时后 spawn_blocking 线程仍在跑(卡在 output),但已脱离 await,不阻塞调用方;
        // 线程最终随 process 退出或进程结束回收,无泄漏累积(OnceLock 已 set,不会重复 probe)。
        let snap = match tokio::time::timeout(
            std::time::Duration::from_secs(5),
            tokio::task::spawn_blocking(|| EnvSnapshot::do_detect()),
        ).await {
            Ok(Ok(s)) => s,
            Ok(Err(join_err)) => {
                eprintln!("[env_snapshot] 探测任务异常,回退默认环境: {}", join_err);
                EnvSnapshot::fallback()
            }
            Err(_elapsed) => {
                eprintln!(
                    "[env_snapshot] 探测超时(5s),某 tool(python/node/rustc/go/docker/git)命令卡住 \
                     (疑似 Windows Store App Execution Alias 触发 Store 重定向),回退默认环境"
                );
                EnvSnapshot::fallback()
            }
        };
        // 多任务竞态:均等价,以先到者为准。
        let _ = SNAPSHOT.set(snap);
        SNAPSHOT.get().expect("EnvSnapshot 已初始化")
    }

    /// 同步探测(可能短暂阻塞,仅在 spawn_blocking 中调用)。
    fn do_detect() -> EnvSnapshot {
        EnvSnapshot {
            os: std::env::consts::OS.to_string(),
            os_version: detect_os_version(),
            shell: detect_shell(),
            path_sep: std::path::MAIN_SEPARATOR.to_string(),
            encoding: detect_encoding(),
            tools: ToolVersions {
                python: probe_version("python", &["--version"]),
                node: probe_version("node", &["--version"]),
                rust: probe_version("rustc", &["--version"]),
                go: probe_version("go", &["version"]),
                docker: probe_version("docker", &["--version"]),
                git: probe_version("git", &["--version"]),
            },
        }
    }

    /// 兜底:spawn_blocking panic/join 失败时返回最小可用快照(全 None,字段不空)。
    fn fallback() -> EnvSnapshot {
        EnvSnapshot {
            os: std::env::consts::OS.to_string(),
            os_version: String::new(),
            shell: if cfg!(windows) { "powershell".into() } else { "sh".into() },
            path_sep: std::path::MAIN_SEPARATOR.to_string(),
            encoding: "utf-8".into(),
            tools: ToolVersions::default(),
        }
    }

    /// 生成 system_prompt 注入文本(拼到 system_prompt 尾部)。
    ///
    /// 末尾加「请生成本平台兼容的命令」软提示,锚定 LLM 输出平台一致性。
    pub fn to_prompt(&self) -> String {
        let mut lines: Vec<String> = vec![
            "## 系统环境".to_string(),
            format!("- 操作系统: {} {}", self.os, self.os_version).trim_end().to_string(),
            format!("- Shell: {}", self.shell),
            format!("- 路径分隔符: {}", self.path_sep),
            format!("- 终端编码: {}", self.encoding),
        ];
        if let Some(v) = &self.tools.python {
            lines.push(format!("- Python: {}", v));
        }
        if let Some(v) = &self.tools.node {
            lines.push(format!("- Node: {}", v));
        }
        if let Some(v) = &self.tools.rust {
            lines.push(format!("- Rust: {}", v));
        }
        if let Some(v) = &self.tools.go {
            lines.push(format!("- Go: {}", v));
        }
        if let Some(v) = &self.tools.docker {
            lines.push(format!("- Docker: {}", v));
        }
        if let Some(v) = &self.tools.git {
            lines.push(format!("- Git: {}", v));
        }
        lines.push(String::new());
        lines.push("注意: 请生成本平台兼容的命令。".to_string());
        lines.join("\n")
    }
}

/// 探测 OS 版本(各平台路径不一,失败返回空串而非 None,简化 prompt 拼接)。
fn detect_os_version() -> String {
    // Windows:读注册表 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion (ProductName/DisplayVersion)。
    // 不依赖 winver GUI / reg.exe 输出格式,直接读注册表最稳。
    #[cfg(windows)]
    {
        if let Some(v) = read_windows_version() {
            return v;
        }
        return String::new();
    }
    // macOS:sw_vers -productVersion 输出如 "12.5"
    #[cfg(target_os = "macos")]
    {
        if let Ok(out) = std::process::Command::new("sw_vers").arg("-productVersion").output() {
            if out.status.success() {
                return String::from_utf8_lossy(&out.stdout).trim().to_string();
            }
        }
        return String::new();
    }
    // Linux:读 /etc/os-release 的 PRETTY_NAME 字段(系统标准位置)
    #[cfg(target_os = "linux")]
    {
        if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
            for line in content.lines() {
                if let Some(rest) = line.strip_prefix("PRETTY_NAME=") {
                    return rest.trim_matches('"').to_string();
                }
            }
        }
        return String::new();
    }
    #[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
    {
        String::new()
    }
}

#[cfg(windows)]
fn read_windows_version() -> Option<String> {
    // 用 reg.exe query 读注册表(DisplayVersion 优先,如 "22H2";回退 ProductName,如 "Windows 10 Pro")。
    // 避开 winreg crate 依赖(增加构建复杂度,且 reg.exe 在所有 Win 版本均自带)。
    let out = std::process::Command::new("reg")
        .args([
            "query",
            r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion",
            "/v",
            "DisplayVersion",
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .creation_flags(0x0800_0000) // CREATE_NO_WINDOW
        .output()
        .ok()?;
    if !out.status.success() {
        return None;
    }
    let text = String::from_utf8_lossy(&out.stdout);
    // 输出形如: "    DisplayVersion    REG_SZ    22H2"
    for line in text.lines() {
        let trimmed = line.trim();
        if let Some(idx) = trimmed.find("REG_SZ") {
            let val = trimmed[idx + "REG_SZ".len()..].trim();
            if !val.is_empty() {
                return Some(format!("Windows {}", val));
            }
        }
    }
    None
}

/// 探测默认 shell(Windows 走 shell.rs 单源,Unix 读 SHELL 环境变量)。
///
/// 单源语义(2026-08 修复):Windows 分支不再独立 probe pwsh/powershell,而是:
///   1) 先调 `shell::probe_pwsh_blocking()` 填充模块级 `PWSH_CACHE`(与 `shell::probe_pwsh()` 异步路径
///      共用同一 OnceLock + 同一阻塞探测实现,杜绝两套逻辑漂移);
///   2) 再调 `shell::current_shell()`(同步读 `PWSH_CACHE`),映射 ShellType → prompt 字符串。
/// 后续 `execute()` → `probe_pwsh().await` 直接命中缓存,跳过重复探测。
/// Unix 分支保留原 SHELL 环境变量读取(与执行侧 ShellType::Sh 默认一致,无漂移风险)。
fn detect_shell() -> String {
    #[cfg(windows)]
    {
        // 单源填充 PWSH_CACHE(本函数运行在 EnvSnapshot::detect 的 spawn_blocking 内,
        // 通常先于首次 execute(),故常是缓存的首次写入者)。
        crate::shell::probe_pwsh_blocking();
        // 单源读取并映射为 prompt 字符串。
        match crate::shell::current_shell() {
            crate::shell::ShellType::Pwsh => "pwsh".to_string(),
            crate::shell::ShellType::PowerShell => "powershell".to_string(),
            crate::shell::ShellType::Cmd => "cmd".to_string(),
            crate::shell::ShellType::Sh => "sh".to_string(),
        }
    }
    #[cfg(not(windows))]
    {
        // Unix:SHELL 环境变量优先,常见值 /bin/bash /bin/zsh /bin/sh。
        if let Ok(sh) = std::env::var("SHELL") {
            // 取 basename(/bin/zsh → zsh)
            let name = sh.rsplit('/').next().unwrap_or(&sh);
            if !name.is_empty() {
                return name.to_string();
            }
        }
        "sh".to_string()
    }
}

/// 探测终端编码(Windows 中文常见 GBK,导致 LLM 输出 UTF-8 在终端乱码)。
fn detect_encoding() -> String {
    #[cfg(windows)]
    {
        // chcp 输出形如 "活动代码页: 936"(GBK)。936 → gbk,65001 → utf-8,其余按数字降级。
        if let Ok(out) = std::process::Command::new("chcp")
            .stdout(Stdio::piped())
            .stderr(Stdio::null())
            .creation_flags(0x0800_0000)
            .output()
        {
            let text = String::from_utf8_lossy(&out.stdout);
            if let Some(code) = extract_codepage(&text) {
                return match code.as_str() {
                    "65001" => "utf-8".to_string(),
                    "936" => "gbk".to_string(),
                    "950" => "big5".to_string(),
                    other => format!("cp{}", other),
                };
            }
        }
        return "utf-8".to_string();
    }
    #[cfg(not(windows))]
    {
        // Unix 默认 UTF-8(LANG/LC_ALL 通常含 UTF-8)。
        if let Ok(lang) = std::env::var("LANG") {
            if lang.to_ascii_uppercase().contains("UTF-8") {
                return "utf-8".to_string();
            }
        }
        "utf-8".to_string()
    }
}

#[cfg(windows)]
fn extract_codepage(text: &str) -> Option<String> {
    // 提取末尾数字("...936" / "...: 65001")。
    let mut num = String::new();
    for c in text.chars().rev() {
        if c.is_ascii_digit() {
            num.insert(0, c);
        } else if !num.is_empty() {
            break;
        }
    }
    if num.is_empty() {
        None
    } else {
        Some(num)
    }
}

/// 执行 `tool --version`,解析首行返回版本串。失败/超时返回 None,不阻塞调用方。
///
/// 例:python --version 输出 "Python 3.11.5" → 返回 "3.11.5";git --version 输出
/// "git version 2.41.0" → 返回 "2.41.0"。统一抽掉工具名前缀,只保留版本号本身。
fn probe_version(tool: &str, args: &[&str]) -> Option<String> {
    let mut cmd = std::process::Command::new(tool);
    cmd.args(args);
    cmd.stdout(Stdio::piped()).stderr(Stdio::null());
    #[cfg(windows)]
    cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
    let out = cmd.output().ok()?;
    if !out.status.success() {
        return None;
    }
    let text = String::from_utf8_lossy(&out.stdout);
    let first_line = text.lines().next()?;
    Some(extract_version_token(first_line))
}

/// 从版本命令首行抽取版本号:取首个形如 N(.N)+ 的 token(至少一个点号)。
/// 兼容 "Python 3.11.5" / "v18.17.0" / "git version 2.41.0.windows.1" / "go version go1.21.0 ..."。
///
/// 策略:把行切成空白 token,逐个匹配「数字开头 + 至少一个 `.`」的模式,取首个命中。
/// 比 char 状态机更鲁棒(状态机遇 `2.41.0.windows.1` 这种多层嵌套点会误判)。
fn extract_version_token(line: &str) -> String {
    for token in line.split_whitespace() {
        // 找 token 内首个数字位置,从这里开始扫 "数字段(.数字段)*" 序列。
        // 遇点要求下一字符为数字,否则在该点处截断(避免 "2.41.0.windows.1" 被吞成
        // "2.41.0.windows.1",实际应止于 "2.41.0")。
        let bytes = token.as_bytes();
        let mut i = match bytes.iter().position(|b| b.is_ascii_digit()) {
            Some(i) => i,
            None => continue,
        };
        let mut head = String::new();
        loop {
            // 收数字段
            let seg_start = i;
            while i < bytes.len() && bytes[i].is_ascii_digit() {
                i += 1;
            }
            head.push_str(&token[seg_start..i]);
            // 点号:仅当下一字符为数字时才续,否则收尾
            if i < bytes.len() && bytes[i] == b'.' && i + 1 < bytes.len()
                && bytes[i + 1].is_ascii_digit()
            {
                head.push('.');
                i += 1;
                continue;
            }
            break;
        }
        if head.contains('.') {
            return head;
        }
    }
    // 无版本号 token 时退回整行(避免返回空串让 prompt 出现 "None")。
    line.trim().to_string()
}

// Windows 下统一在文件顶部引入 CommandExt,使各 #[cfg(windows)] 块可直接调用 creation_flags。
#[cfg(windows)]
use std::os::windows::process::CommandExt;

use std::process::Stdio;

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

    #[test]
    fn extract_version_python_style() {
        assert_eq!(extract_version_token("Python 3.11.5"), "3.11.5");
    }

    #[test]
    fn extract_version_node_style() {
        assert_eq!(extract_version_token("v18.17.0"), "18.17.0");
    }

    #[test]
    fn extract_version_git_style() {
        assert_eq!(extract_version_token("git version 2.41.0.windows.1"), "2.41.0");
    }

    #[test]
    fn extract_version_go_style() {
        assert_eq!(
            extract_version_token("go version go1.21.0 windows/amd64"),
            "1.21.0"
        );
    }

    #[test]
    fn extract_version_no_match_returns_line() {
        assert_eq!(extract_version_token("no version here"), "no version here");
    }

    #[cfg(windows)]
    #[test]
    fn extract_codepage_parsing() {
        assert_eq!(extract_codepage("活动代码页: 936"), Some("936".into()));
        assert_eq!(
            extract_codepage("Active code page: 65001"),
            Some("65001".into())
        );
        assert_eq!(extract_codepage("no digits here"), None);
    }

    #[tokio::test]
    async fn detect_returns_cached_static_ref() {
        // 两次 detect 返回同一引用(OnceLock 全局缓存)。
        let a = EnvSnapshot::detect().await as *const _;
        let b = EnvSnapshot::detect().await as *const _;
        assert_eq!(a, b, "detect() 应返回同一静态引用");
    }

    /// 单源不变量回归:prompt 期 shell(env_snapshot.shell)与执行期 shell(current_shell)必须一致。
    ///
    /// 历史 bug:detect_shell 持独立 probe_command_success 探测,与 shell.rs::probe_pwsh 各填各的
    /// OnceLock → prompt 告诉 LLM 用 pwsh,执行却走 powershell(或反之)。根本修:二者共用
    /// shell.rs 的 probe_pwsh_blocking + PWSH_CACHE 单源。本测试锁定「无漂移」不变量。
    #[cfg(windows)]
    #[tokio::test]
    async fn detect_shell_matches_shell_rs_current_shell() {
        // detect() 内 detect_shell → probe_pwsh_blocking 填 PWSH_CACHE,再读 current_shell 映射。
        let snap = EnvSnapshot::detect().await;
        let current = crate::shell::current_shell();
        // 二者必须一致(同一 PWSH_CACHE 单源读出)。
        match snap.shell.as_str() {
            "pwsh" => assert_eq!(current, crate::shell::ShellType::Pwsh),
            "powershell" => assert_eq!(current, crate::shell::ShellType::PowerShell),
            "cmd" => assert_eq!(current, crate::shell::ShellType::Cmd),
            "sh" => assert_eq!(current, crate::shell::ShellType::Sh),
            other => panic!("未知 shell 字符串: {}", other),
        }
    }

    #[test]
    fn to_prompt_contains_os_and_shell() {
        let snap = EnvSnapshot {
            os: "test_os".into(),
            os_version: "v1".into(),
            shell: "test_shell".into(),
            path_sep: "/".into(),
            encoding: "utf-8".into(),
            tools: ToolVersions {
                python: Some("3.11".into()),
                node: None,
                rust: None,
                go: None,
                docker: None,
                git: None,
            },
        };
        let prompt = snap.to_prompt();
        assert!(prompt.contains("test_os"));
        assert!(prompt.contains("test_shell"));
        assert!(prompt.contains("Python: 3.11"));
        assert!(!prompt.contains("Node"));
        assert!(prompt.contains("请生成本平台兼容的命令"));
    }
}
