//! 技术栈探测 — 浅读根目录标志文件识别技术栈。
//!
//! 纯函数、零状态。读 `Cargo.toml`/`go.mod`/`package.json` 等标志,
//! 不递归遍历(控制性能与安全)。

use std::path::Path;

use anyhow::{Context, Result};

/// 探测目录的技术栈
///
/// 返回去重后的技术栈标签数组(如 `["rust","vue","tauri","typescript"]`)。
/// 空数组表示未识别出任何已知标志(空目录或非常规项目)。非目录返回 Err。
pub fn detect_stack(root: &Path) -> Result<Vec<String>> {
    if !root.is_dir() {
        anyhow::bail!("路径不是目录: {}", root.display());
    }
    let mut stack: Vec<String> = Vec::new();

    // ── 后端/系统语言 ──
    if root.join("Cargo.toml").exists() {
        push_unique(&mut stack, "rust");
    }
    if root.join("go.mod").exists() {
        push_unique(&mut stack, "go");
    }
    if root.join("pom.xml").exists()
        || root.join("build.gradle").exists()
        || root.join("build.gradle.kts").exists()
    {
        push_unique(&mut stack, "java");
    }
    if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
        push_unique(&mut stack, "python");
    }
    // C#: 根目录存在 .csproj 文件(仅一层)
    if has_file_with_ext(root, "csproj") {
        push_unique(&mut stack, "csharp");
    }

    // ── Tauri 桌面应用(通常含 src-tauri 目录) ──
    if root.join("src-tauri").is_dir() {
        push_unique(&mut stack, "tauri");
    }

    // ── 前端/Node:解析 package.json 的依赖推断框架 ──
    if root.join("package.json").exists() {
        if let Ok(deps) = read_package_deps(root.join("package.json")) {
            if deps.iter().any(|d| d == "vue") {
                push_unique(&mut stack, "vue");
            }
            if deps.iter().any(|d| d == "react" || d == "react-dom") {
                push_unique(&mut stack, "react");
            }
            if deps.iter().any(|d| d == "@angular/core") {
                push_unique(&mut stack, "angular");
            }
            if deps.iter().any(|d| d == "svelte") {
                push_unique(&mut stack, "svelte");
            }
            if deps.iter().any(|d| d == "next") {
                push_unique(&mut stack, "next");
            }
            if deps.iter().any(|d| d == "vite") {
                push_unique(&mut stack, "vite");
            }
            if deps.iter().any(|d| d == "typescript") {
                push_unique(&mut stack, "typescript");
            }
            if deps.iter().any(|d| d == "express" || d == "koa" || d == "fastify") {
                push_unique(&mut stack, "node");
            }
        }
    }

    Ok(stack)
}

/// 解析 package.json,合并 dependencies + devDependencies 的包名
fn read_package_deps(path: impl AsRef<Path>) -> Result<Vec<String>> {
    let content = std::fs::read_to_string(path.as_ref())
        .with_context(|| format!("读取 package.json 失败: {}", path.as_ref().display()))?;
    let pkg: serde_json::Value = serde_json::from_str(&content).context("解析 package.json 失败")?;
    let mut names = Vec::new();
    for key in &["dependencies", "devDependencies"] {
        if let Some(obj) = pkg.get(key).and_then(|v| v.as_object()) {
            for k in obj.keys() {
                names.push(k.clone());
            }
        }
    }
    Ok(names)
}

/// 目录下是否存在指定扩展名的文件(仅一层)。
///
/// 被 `detect_stack`(C# csproj)与 `super::discover`(项目清单判定)共用,
/// 故 `pub(super)`。
pub(super) fn has_file_with_ext(dir: &Path, ext: &str) -> bool {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return false;
    };
    entries.flatten().any(|e| {
        e.path()
            .extension()
            .and_then(|x| x.to_str())
            .map(|x| x == ext)
            .unwrap_or(false)
    })
}

/// 去重 push
fn push_unique(stack: &mut Vec<String>, s: &str) {
    if !stack.iter().any(|x| x == s) {
        stack.push(s.to_string());
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::path::PathBuf;

    /// 在系统临时目录建唯一子目录(以进程号隔离并发),返回路径
    fn scratch(name: &str) -> PathBuf {
        let mut p = std::env::temp_dir();
        p.push(format!("df-project-scan-{}-{}", name, std::process::id()));
        let _ = fs::remove_dir_all(&p);
        fs::create_dir_all(&p).unwrap();
        p
    }

    #[test]
    fn non_dir_errors() {
        let r = detect_stack(Path::new("definitely-not-exist-xyz-123"));
        assert!(r.is_err());
    }

    #[test]
    fn detects_rust() {
        let d = scratch("rust");
        fs::write(d.join("Cargo.toml"), "").unwrap();
        let s = detect_stack(&d).unwrap();
        assert!(s.contains(&"rust".to_string()));
        fs::remove_dir_all(&d).ok();
    }

    #[test]
    fn detects_full_stack() {
        // 模拟 DevFlow 自身:rust + tauri + vue + vite + typescript
        let d = scratch("full");
        fs::write(d.join("Cargo.toml"), "").unwrap();
        fs::create_dir(d.join("src-tauri")).unwrap();
        fs::write(
            d.join("package.json"),
            r#"{"dependencies":{"vue":"^3.5.0"},"devDependencies":{"vite":"^6.0.0","typescript":"~5.6.0"}}"#,
        )
        .unwrap();
        let s = detect_stack(&d).unwrap();
        assert!(s.contains(&"rust".to_string()));
        assert!(s.contains(&"tauri".to_string()));
        assert!(s.contains(&"vue".to_string()));
        assert!(s.contains(&"vite".to_string()));
        assert!(s.contains(&"typescript".to_string()));
        fs::remove_dir_all(&d).ok();
    }

    #[test]
    fn empty_dir_returns_empty() {
        let d = scratch("empty");
        let s = detect_stack(&d).unwrap();
        assert!(s.is_empty());
        fs::remove_dir_all(&d).ok();
    }
}
