// Gitignore-aware file discovery using the `ignore` crate.

use std::path::{Path, PathBuf};

use ignore::overrides::OverrideBuilder;
use ignore::WalkBuilder;
use tracing::warn;

use astmap_core::error::ScanError;
use astmap_core::port::FileDiscovery;

/// Directories to always skip, even without a .gitignore.
const FALLBACK_SKIP_DIRS: &[&str] = &[
    "target",
    "node_modules",
    "dist",
    "build",
    "out",
    "__pycache__",
    "venv",
    "vendor",
];

/// File discovery that respects .gitignore, .git/info/exclude, and global gitignore.
/// Falls back to hardcoded skip list for non-git projects.
pub struct IgnoreWalkDiscovery;

impl FileDiscovery for IgnoreWalkDiscovery {
    fn discover_files(
        &self,
        project_dir: &Path,
        supported_ext: &[&str],
    ) -> Result<Vec<PathBuf>, ScanError> {
        let overrides = build_overrides(project_dir)?;

        let walker = WalkBuilder::new(project_dir)
            .hidden(true)
            .git_ignore(true)
            .git_global(true)
            .git_exclude(true)
            .overrides(overrides)
            .build();

        let mut files = Vec::new();
        for result in walker {
            let entry = match result {
                Ok(e) => e,
                Err(e) => {
                    warn!("walk error: {}", e);
                    continue;
                }
            };
            if !entry.file_type().is_some_and(|ft| ft.is_file()) {
                continue;
            }
            let path = entry.into_path();
            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
                if supported_ext.contains(&ext) {
                    files.push(path);
                }
            }
        }
        files.sort();
        Ok(files)
    }
}

fn build_overrides(project_dir: &Path) -> Result<ignore::overrides::Override, ScanError> {
    let mut builder = OverrideBuilder::new(project_dir);
    for dir in FALLBACK_SKIP_DIRS {
        builder
            .add(&format!("!{}/", dir))
            .map_err(|e| ScanError::Io(std::io::Error::other(e.to_string())))?;
    }
    builder
        .add("!*.egg-info/")
        .map_err(|e| ScanError::Io(std::io::Error::other(e.to_string())))?;
    builder
        .build()
        .map_err(|e| ScanError::Io(std::io::Error::other(e.to_string())))
}

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

    const TEST_EXT: &[&str] = &["rs", "ts", "tsx", "py"];

    #[test]
    fn test_discover_files() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("main.rs"), "fn main() {}").unwrap();
        std::fs::write(src.join("lib.rs"), "pub mod foo;").unwrap();
        std::fs::write(src.join("notes.txt"), "not rust").unwrap();
        // hidden dir and target should be skipped
        std::fs::create_dir_all(tmp.path().join(".git")).unwrap();
        std::fs::write(tmp.path().join(".git").join("config.rs"), "").unwrap();
        std::fs::create_dir_all(tmp.path().join("target")).unwrap();
        std::fs::write(tmp.path().join("target").join("build.rs"), "").unwrap();
        std::fs::create_dir_all(tmp.path().join("node_modules")).unwrap();
        std::fs::write(tmp.path().join("node_modules").join("lib.rs"), "").unwrap();
        std::fs::create_dir_all(tmp.path().join("dist")).unwrap();
        std::fs::write(tmp.path().join("dist").join("index.ts"), "").unwrap();
        std::fs::create_dir_all(tmp.path().join("build")).unwrap();
        std::fs::write(tmp.path().join("build").join("output.ts"), "").unwrap();

        let discovery = IgnoreWalkDiscovery;
        let files = discovery.discover_files(tmp.path(), TEST_EXT).unwrap();
        assert_eq!(files.len(), 2);
        assert!(files.iter().any(|p| p.ends_with("main.rs")));
        assert!(files.iter().any(|p| p.ends_with("lib.rs")));
    }

    #[test]
    fn test_discover_ts_files() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("app.ts"), "").unwrap();
        std::fs::write(src.join("Button.tsx"), "").unwrap();
        std::fs::write(src.join("style.css"), "").unwrap();

        let discovery = IgnoreWalkDiscovery;
        let files = discovery.discover_files(tmp.path(), TEST_EXT).unwrap();
        assert_eq!(files.len(), 2);
        assert!(files.iter().any(|p| p.ends_with("app.ts")));
        assert!(files.iter().any(|p| p.ends_with("Button.tsx")));
    }

    #[test]
    fn test_discover_nested_files() {
        let tmp = tempfile::tempdir().unwrap();
        let nested = tmp.path().join("src").join("db");
        std::fs::create_dir_all(&nested).unwrap();
        std::fs::write(nested.join("mod.rs"), "").unwrap();
        std::fs::write(nested.join("models.rs"), "").unwrap();

        let discovery = IgnoreWalkDiscovery;
        let files = discovery.discover_files(tmp.path(), TEST_EXT).unwrap();
        assert_eq!(files.len(), 2);
    }

    #[test]
    fn test_respects_gitignore() {
        let tmp = tempfile::tempdir().unwrap();
        // Initialize a git repo so .gitignore is respected
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(tmp.path())
            .output()
            .unwrap();
        std::fs::write(tmp.path().join(".gitignore"), "generated/\n").unwrap();
        let src = tmp.path().join("src");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("main.rs"), "").unwrap();
        let gen = tmp.path().join("generated");
        std::fs::create_dir_all(&gen).unwrap();
        std::fs::write(gen.join("output.rs"), "").unwrap();

        let discovery = IgnoreWalkDiscovery;
        let files = discovery.discover_files(tmp.path(), TEST_EXT).unwrap();
        assert_eq!(files.len(), 1);
        assert!(files[0].ends_with("main.rs"));
    }

    #[test]
    fn test_egg_info_skipped() {
        let tmp = tempfile::tempdir().unwrap();
        std::fs::write(tmp.path().join("app.py"), "").unwrap();
        let egg = tmp.path().join("mylib.egg-info");
        std::fs::create_dir_all(&egg).unwrap();
        std::fs::write(egg.join("setup.py"), "").unwrap();

        let discovery = IgnoreWalkDiscovery;
        let files = discovery.discover_files(tmp.path(), TEST_EXT).unwrap();
        assert_eq!(files.len(), 1);
        assert!(files[0].ends_with("app.py"));
    }
}
