//! 项目技术栈探测 — 浅读根目录标志文件识别技术栈
//!
//! 纯函数、零状态、零外部依赖(仅 std + serde_json + anyhow)。
//! 只读根目录标志文件,不递归遍历(控制性能与安全)。
//!
//! 供 commands 层薄封装暴露为 IPC 命令,新建/导入项目时自动填充 `ProjectRecord.stack`;
//! 未来 df-ai/df-workflow 亦可复用以感知「项目是什么技术栈」。
//!
//! ## 模块拆分(2026-06-22,REFACTOR-260619-10)
//!
//! 原 `scan.rs`(741 行)按职责拆为 4 子模块,纯文件重组、零行为变化:
//! - [`stack`]:技术栈探测(`detect_stack` + package.json 解析)
//! - [`discover`]:历史项目发现/monorepo 展开(`discover_projects`/`is_monorepo`)
//! - [`readme`]:README 读取与首段抽取(`read_readme_raw`/`extract_description`)
//! - [`sample`]:项目采样(`collect_sample` + 目录树/清单/噪音目录)
//!
//! 跨模块共享的常量/helper 留在本 `mod.rs`(`SAMPLE_IGNORED_DIRS`/`truncate_chars`),
//! 子模块经 `use super::*` 引用。`pub use` 重导出保持外部 `df_project::scan::*` 路径不变。
//!
//! README 噪音处理与图片收集已抽离至 `scan_helpers`(2026-06-19 自底向上拆分),
//! 此处通过 `pub use` 重导出 `ImageRef` 等保持外部路径 `df_project::scan::*` 不变。

use std::path::Path;

#[path = "../scan_helpers.rs"]
mod scan_helpers;
pub use scan_helpers::ImageRef;

pub mod discover;
pub mod readme;
pub mod sample;
pub mod stack;

// ── 对外稳定 API 重导出(外部 `df_project::scan::*` 路径不变)──
pub use discover::{discover_projects, is_monorepo, DiscoveredProject};
pub use readme::extract_description;
pub use sample::{collect_sample, ProjectSample};
pub use stack::detect_stack;

// ============================================================
// 跨模块共享常量/helper
// ============================================================

/// 目录树/项目发现过滤的噪音目录(依赖产物/构建/缓存/IDE)。
///
/// 同时被 [`discover`] 与 [`sample`] 引用,故置于本 `mod.rs`,`pub(super)` 暴露给子模块。
pub(super) const SAMPLE_IGNORED_DIRS: &[&str] = &[
    "node_modules", "target", ".git", "dist", "build", ".next", "venv", ".venv",
    "__pycache__", ".idea", ".vscode", ".cache", "out", "coverage", ".svelte-kit",
    ".turbo", ".angular", ".gradle", "vendor",
];

/// 按字符数截断(避免截断 UTF-8 多字节边界)。
///
/// 被 [`readme`](crate::scan::readme) 与 [`sample`](crate::scan::sample) 引用,故置于本 `mod.rs`。
pub(super) fn truncate_chars(s: &str, max: usize) -> String {
    if s.chars().count() <= max {
        return s.to_string();
    }
    let truncated: String = s.chars().take(max).collect();
    format!("{}…(已截断)", truncated)
}

/// 规范化路径用于比较:canonicalize 解析绝对规范路径(失败降级),
/// 统一正斜杠 + 小写。防 `C:\a\b` vs `C:/a/b/` 绕过重复检查。
/// 注:仅用于比较,存库保留用户输入的原始可读路径。
pub fn normalize_path(p: &str) -> String {
    match Path::new(p).canonicalize() {
        Ok(abs) => abs.to_string_lossy().replace('\\', "/").to_lowercase(),
        Err(_) => p
            .trim_end_matches(['\\', '/'])
            .replace('\\', "/")
            .to_lowercase(),
    }
}

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

    // ── normalize_path 跨平台归一化 ──
    // 锁定:① 反斜杠→正斜杠 ② 小写 ③ 尾部分隔符裁剪 ④ 相同逻辑路径相等(防重复录入绕过)。
    // 注:canonicalize 成功路径在不同 OS 返回不同形态(Windows 带前缀),故只断言跨平台不变性,
    // 不断言精确串,避免与平台耦合。
    #[test]
    fn normalize_path_uses_forward_slash_and_lowercase() {
        // 不存在的路径走降级分支(trim + replace + lowercase)
        let n = normalize_path(r"C:\Foo\Bar\");
        assert!(
            !n.contains('\\'),
            "反斜杠未归一: {n}"
        );
        assert_eq!(n, n.to_lowercase(), "未小写: {n}");
        assert!(
            !n.ends_with('/') && !n.ends_with('\\'),
            "尾部分隔符未裁剪: {n}"
        );
    }

    #[test]
    fn normalize_path_equivalent_inputs_equal() {
        // 两种写法应归一为同一串(去重场景)
        let a = normalize_path(r"C:\Foo\Bar");
        let b = normalize_path("C:/Foo/Bar/");
        assert_eq!(a, b, "等价路径归一不等: {a} vs {b}");
    }

    #[test]
    fn normalize_path_relative_is_lowercase_and_forward_slash() {
        let n = normalize_path(r"..\Some\Path");
        assert!(!n.contains('\\'), "反斜杠未归一: {n}");
        assert_eq!(n, n.to_lowercase(), "未小写: {n}");
    }
}
