//! 项目采样(供 LLM 分析基础信息) — 纯 IO,控 token 不读源码。
//!
//! README(剥噪音后) + 目录树(2层) + 清单文件片段 + 内容图引用。

use std::path::Path;

use anyhow::Result;

use super::readme::read_readme_raw;
use super::scan_helpers::{collect_images, strip_readme_noise, ImageRef};
use super::{truncate_chars, SAMPLE_IGNORED_DIRS};

const SAMPLE_README_MAX: usize = 8000;
const SAMPLE_MANIFEST_MAX: usize = 1500;
const SAMPLE_TREE_MAX: usize = 80;

/// 项目采样结果 — README(剥噪音后) + 目录树(2层) + 清单文件片段 + 内容图引用
#[derive(Debug, Clone)]
pub struct ProjectSample {
    pub readme: Option<String>,
    pub tree: Vec<String>,
    /// (文件名, 截断内容)
    pub manifests: Vec<(String, String)>,
    /// README 内的内容图引用(架构图/截图等,跳徽章)
    pub images: Vec<ImageRef>,
}

/// 采集项目采样(README + 目录树 + 清单 + 内容图),供 LLM 分析填基础信息
pub fn collect_sample(root: &Path) -> Result<ProjectSample> {
    if !root.is_dir() {
        anyhow::bail!("路径不是目录: {}", root.display());
    }
    let raw = read_readme_raw(root);
    let (readme, images) = match raw {
        Some(text) => {
            let images = collect_images(&text);
            let cleaned = strip_readme_noise(&text);
            if cleaned.trim().is_empty() {
                (None, images)
            } else {
                (Some(truncate_chars(&cleaned, SAMPLE_README_MAX)), images)
            }
        }
        None => (None, Vec::new()),
    };
    Ok(ProjectSample {
        readme,
        tree: collect_tree(root),
        manifests: collect_manifests(root),
        images,
    })
}

/// 目录树(根 + 一层子目录),过滤噪音目录,控条目数
fn collect_tree(root: &Path) -> Vec<String> {
    let mut lines = Vec::new();
    let mut count = 0usize;
    collect_tree_level(root, "", &mut lines, &mut count, false);
    lines
}

fn collect_tree_level(dir: &Path, prefix: &str, lines: &mut Vec<String>, count: &mut usize, is_sub: bool) {
    if *count >= SAMPLE_TREE_MAX {
        return;
    }
    let Ok(entries) = std::fs::read_dir(dir) else { return };
    let mut items: Vec<_> = entries.flatten().collect();
    items.sort_by_key(|e| e.file_name());
    for e in items {
        if *count >= SAMPLE_TREE_MAX {
            return;
        }
        let name = e.file_name().to_string_lossy().to_string();
        let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
        if is_dir {
            if SAMPLE_IGNORED_DIRS.contains(&name.as_str()) {
                continue;
            }
            lines.push(format!("{}{}/", prefix, name));
            *count += 1;
            // 仅根目录的子目录展开一层(is_sub=true 不再递归)
            if !is_sub {
                collect_tree_level(&e.path(), &format!("{}  ", prefix), lines, count, true);
            }
        } else {
            // 跳过隐藏文件(保留 .gitignore 作 git 标识)
            if name.starts_with('.') && name != ".gitignore" {
                continue;
            }
            lines.push(format!("{}{}", prefix, name));
            *count += 1;
        }
    }
}

fn collect_manifests(root: &Path) -> Vec<(String, String)> {
    let mut out = Vec::new();
    for name in &["package.json", "Cargo.toml", "go.mod", "pyproject.toml", "pom.xml", "build.gradle", "build.gradle.kts"] {
        let p = root.join(name);
        if let Ok(content) = std::fs::read_to_string(&p) {
            out.push(((*name).to_string(), truncate_chars(&content, SAMPLE_MANIFEST_MAX)));
        }
    }
    out
}

#[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 collects_sample() {
        let d = scratch("sample");
        fs::write(
            d.join("README.md"),
            "# Test\n\n![badge](https://img.shields.io/badge/x-y-green)\n\n![arch](./docs/arch.png)\n\nA test project.\nMore.",
        )
        .unwrap();
        fs::write(d.join("package.json"), r#"{"name":"x","dependencies":{"vue":"3"}}"#).unwrap();
        fs::create_dir(d.join("src")).unwrap();
        fs::write(d.join("src/main.ts"), "x").unwrap();
        fs::create_dir(d.join("node_modules")).unwrap();
        fs::write(d.join("node_modules/junk.json"), "x").unwrap();
        let s = collect_sample(&d).unwrap();
        // 徽章行剥,内容图 + 正文保留
        let readme = s.readme.as_deref().unwrap_or("");
        assert!(readme.contains("test project"), "readme={readme}");
        assert!(!readme.contains("shields.io"), "徽章未剥: {readme}");
        assert!(readme.contains("docs/arch.png"), "内容图丢失: {readme}");
        // 内容图收集(badge 跳,arch 留)
        assert_eq!(s.images.len(), 1);
        assert_eq!(s.images[0].src, "./docs/arch.png");
        assert!(s.manifests.iter().any(|(n, _)| n == "package.json"));
        assert!(s.tree.iter().any(|t| t.contains("src")));
        // node_modules 应被过滤
        assert!(s.tree.iter().all(|t| !t.contains("node_modules")));
        fs::remove_dir_all(&d).ok();
    }

    #[test]
    fn strips_frontmatter_and_toc_and_badges() {
        let d = scratch("noise");
        let readme = "---\ntitle: Foo\n---\n\n# Foo\n\n<!-- hidden comment -->\n\n[Install](#install)\n\n- [Usage](#usage)\n\n![build](https://img.shields.io/badge/build-passing)\n\nThis is the real intro.\n";
        fs::write(d.join("README.md"), readme).unwrap();
        let s = collect_sample(&d).unwrap();
        let r = s.readme.as_deref().unwrap_or("");
        assert!(r.contains("# Foo"), "标题应保留: {r}");
        assert!(r.contains("real intro"), "正文应保留: {r}");
        assert!(!r.contains("hidden comment"), "HTML 注释未剥: {r}");
        assert!(!r.contains("shields.io"), "徽章未剥: {r}");
        assert!(!r.contains("[Install](#install)"), "TOC 锚点未剥: {r}");
        assert!(!r.contains("[Usage](#usage)"), "TOC 列表项未剥: {r}");
        assert!(!r.contains("title: Foo"), "frontmatter 未剥: {r}");
        fs::remove_dir_all(&d).ok();
    }
}
