//! Pre-ingest classification + skip helpers for the local-filesystem
//! SourceConnector. Moved from `kebab-parse-code` (refactor 2026-05-26)
//! to drop the 9 tree-sitter grammar drag from this crate's dep tree.
//!
//! `BUILTIN_BLACKLIST` is `pub` because it implements the **frozen contract
//! in design §5.2** (the 6-pattern safety-net list). External integration
//! tests (`tests/code_meta.rs`) verify the contract from outside the module
//! to prevent silent breakage. The 3 helper fns are `pub(crate)` — no
//! external consumer today.

use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;

use anyhow::Result;

/// 6 built-in gitignore-style patterns. Applied in addition to `.gitignore`
/// + `.kebabignore`. User can override via `.kebabignore` negation
///   (`!pattern`).
///
/// Source of truth: design §5.2 (frozen).
pub const BUILTIN_BLACKLIST: &[&str] = &[
    "**/node_modules/**",
    "**/target/**",
    "**/__pycache__/**",
    "**/.venv/**",
    "**/venv/**",
    "**/env/**",
];

/// Returns the canonical language identifier for a given file path, or
/// `None` if the extension / filename is not recognized.
///
/// Matching priority:
///   1. Tier 1 basename exact match (e.g. `Dockerfile`, `Makefile`)
///   2. Tier 2 basename match (e.g. `Cargo.toml`, `package.json`, `build.gradle`)
///   3. Tier 2 `Dockerfile.*` prefix variant
///   4. Tier 1 + Tier 2 extension fallback (lowercase)
pub(crate) fn code_lang_for_path(path: &Path) -> Option<&'static str> {
    if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
        // Tier 1 basename exact match
        match name {
            "Dockerfile" => return Some("dockerfile"),
            "Makefile" | "GNUmakefile" => return Some("make"),
            _ => {}
        }

        // Tier 2 basename match (configuration / manifest files)
        match name {
            "Cargo.toml" | "pyproject.toml" => return Some("toml"),
            "package.json" | "tsconfig.json" => return Some("json"),
            "go.mod" => return Some("go-mod"),
            "pom.xml" => return Some("xml"),
            "build.gradle" => return Some("groovy"),
            _ => {}
        }

        // Tier 2: `Dockerfile.*` prefix variant (e.g. `Dockerfile.dev`, `Dockerfile.prod`)
        if name.starts_with("Dockerfile.") && name.len() > "Dockerfile.".len() {
            return Some("dockerfile");
        }
    }

    // Extension fallback (Tier 1 + Tier 2)
    let ext = path.extension()?.to_str()?.to_ascii_lowercase();
    match ext.as_str() {
        // Tier 1 extensions
        "rs" => Some("rust"),
        "py" | "pyi" => Some("python"),
        "ts" | "tsx" | "mts" | "cts" => Some("typescript"),
        "js" | "mjs" | "cjs" | "jsx" => Some("javascript"),
        "go" => Some("go"),
        "java" => Some("java"),
        "kt" | "kts" => Some("kotlin"),
        "c" | "h" => Some("c"),
        "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => Some("cpp"),
        "sh" | "bash" | "zsh" => Some("shell"),
        "mk" => Some("make"),
        // Tier 2 extensions
        "yaml" | "yml" => Some("yaml"),
        "toml" => Some("toml"),
        "json" => Some("json"),
        "xml" => Some("xml"),
        "dockerfile" => Some("dockerfile"),
        "gradle" => Some("groovy"),
        _ => None,
    }
}

/// Read first 512 bytes, check for any of 7 case-insensitive generated-file
/// markers. Returns Ok(true) on match, Ok(false) otherwise.
pub(crate) fn is_generated_file(path: &Path) -> Result<bool> {
    let mut buf = [0u8; 512];
    let mut f = File::open(path)?;
    let n = f.read(&mut buf)?;
    if n == 0 {
        return Ok(false);
    }
    let head = std::str::from_utf8(&buf[..n]).unwrap_or("");
    let lower: String = head
        .lines()
        .take(10)
        .collect::<Vec<_>>()
        .join("\n")
        .to_ascii_lowercase();
    Ok(lower.contains("@generated")
        || lower.contains("code generated by")
        || lower.contains("do not edit")
        || lower.contains("do not modify")
        || lower.contains("automatically generated")
        || lower.contains("auto-generated")
        || lower.contains("autogenerated"))
}

/// Returns true when `path`'s filename/extension is recognised as a code
/// file (per `code_lang_for_path`). Used by the walker to apply
/// `[ingest.code].max_file_bytes` / `max_file_lines` only to code files,
/// not to PDF/image/markdown (which have their own size controls in
/// their respective parsers).
pub(crate) fn is_code_file(path: &Path) -> bool {
    code_lang_for_path(path).is_some()
}

/// Check if `path` exceeds `max_bytes` or `max_lines`. Byte cap first
/// (cheap), then line cap (streaming with early exit).
pub(crate) fn is_oversized(path: &Path, max_bytes: u64, max_lines: u32) -> Result<bool> {
    let meta = std::fs::metadata(path)?;
    if meta.len() > max_bytes {
        return Ok(true);
    }
    let reader = BufReader::new(File::open(path)?);
    let mut count: u32 = 0;
    for line in reader.lines() {
        let _ = line?;
        count = count.saturating_add(1);
        if count > max_lines {
            return Ok(true);
        }
    }
    Ok(false)
}

#[cfg(test)]
mod tests {
    use super::{code_lang_for_path, is_generated_file, is_oversized};
    use std::fs;
    use std::path::Path;
    use tempfile::NamedTempFile;

    // ── code_lang_for_path tests (ex-kebab-parse-code/tests/lang.rs) ─────────

    #[test]
    fn known_extensions_map_to_canonical_identifiers() {
        let cases = [
            ("foo.rs", Some("rust")),
            ("foo.py", Some("python")),
            ("foo.pyi", Some("python")),
            ("foo.ts", Some("typescript")),
            ("foo.tsx", Some("typescript")),
            ("foo.mts", Some("typescript")), // ESM TS — same grammar
            ("foo.cts", Some("typescript")), // CommonJS TS — same grammar
            ("foo.js", Some("javascript")),
            ("foo.mjs", Some("javascript")),
            ("foo.cjs", Some("javascript")),
            ("foo.jsx", Some("javascript")),
            ("foo.go", Some("go")),
            ("foo.java", Some("java")),
            ("foo.kt", Some("kotlin")),
            ("foo.kts", Some("kotlin")),
            ("foo.c", Some("c")),
            ("foo.h", Some("c")),
            ("foo.cpp", Some("cpp")),
            ("foo.cc", Some("cpp")),
            ("foo.cxx", Some("cpp")),
            ("foo.hpp", Some("cpp")),
            ("foo.hh", Some("cpp")),
            ("foo.hxx", Some("cpp")),
            ("foo.yaml", Some("yaml")),
            ("foo.yml", Some("yaml")),
            ("foo.toml", Some("toml")),
            ("foo.json", Some("json")),
            ("foo.sh", Some("shell")),
            ("foo.bash", Some("shell")),
            ("foo.zsh", Some("shell")),
            ("foo.mk", Some("make")),
        ];
        for (path, expected) in cases {
            assert_eq!(
                code_lang_for_path(Path::new(path)),
                expected,
                "path = {path}"
            );
        }
    }

    #[test]
    fn special_filenames_map_to_identifiers() {
        assert_eq!(
            code_lang_for_path(Path::new("Dockerfile")),
            Some("dockerfile")
        );
        assert_eq!(
            code_lang_for_path(Path::new("foo.dockerfile")),
            Some("dockerfile")
        );
        assert_eq!(code_lang_for_path(Path::new("Makefile")), Some("make"));
        assert_eq!(code_lang_for_path(Path::new("GNUmakefile")), Some("make"));
    }

    #[test]
    fn unknown_extension_returns_none() {
        assert_eq!(code_lang_for_path(Path::new("foo.docx")), None);
        assert_eq!(code_lang_for_path(Path::new("foo")), None);
        assert_eq!(code_lang_for_path(Path::new("foo.unknown")), None);
    }

    #[test]
    fn case_insensitive() {
        assert_eq!(code_lang_for_path(Path::new("Foo.RS")), Some("rust"));
        assert_eq!(code_lang_for_path(Path::new("FOO.YAML")), Some("yaml"));
    }

    #[test]
    fn tier2_basename_takes_precedence_over_extension() {
        assert_eq!(
            code_lang_for_path(Path::new("Dockerfile")),
            Some("dockerfile")
        );
        assert_eq!(
            code_lang_for_path(Path::new("foo/Dockerfile.dev")),
            Some("dockerfile")
        );
        assert_eq!(
            code_lang_for_path(Path::new("myapp.dockerfile")),
            Some("dockerfile")
        );
        assert_eq!(
            code_lang_for_path(Path::new("repo/Cargo.toml")),
            Some("toml")
        );
        assert_eq!(
            code_lang_for_path(Path::new("pyproject.toml")),
            Some("toml")
        );
        assert_eq!(
            code_lang_for_path(Path::new("repo/package.json")),
            Some("json")
        );
        assert_eq!(code_lang_for_path(Path::new("tsconfig.json")), Some("json"));
        assert_eq!(code_lang_for_path(Path::new("go.mod")), Some("go-mod"));
        assert_eq!(code_lang_for_path(Path::new("pom.xml")), Some("xml"));
        assert_eq!(
            code_lang_for_path(Path::new("build.gradle")),
            Some("groovy")
        );
    }

    #[test]
    fn tier2_extension_fallback() {
        assert_eq!(
            code_lang_for_path(Path::new("k8s/deploy.yaml")),
            Some("yaml")
        );
        assert_eq!(
            code_lang_for_path(Path::new("k8s/deploy.yml")),
            Some("yaml")
        );
        assert_eq!(code_lang_for_path(Path::new("foo/bar.toml")), Some("toml"));
        assert_eq!(code_lang_for_path(Path::new("foo/bar.json")), Some("json"));
        assert_eq!(code_lang_for_path(Path::new("foo/bar.xml")), Some("xml"));
        assert_eq!(
            code_lang_for_path(Path::new("foo/bar.gradle")),
            Some("groovy")
        );
    }

    // ── is_generated_file + is_oversized tests (ex-kebab-parse-code/tests/skip.rs) ──

    #[test]
    fn generated_header_markers_trigger_skip() {
        let cases = [
            "// @generated\nfn foo() {}\n",
            "// Code generated by tonic-build. DO NOT EDIT.\nfn x() {}\n",
            "/* DO NOT EDIT */\nfn x() {}\n",
            "/* do not modify */\nfn x() {}\n",
            "// AUTOMATICALLY GENERATED\nfn x() {}\n",
            "# auto-generated\ndef x(): pass\n",
            "// autogenerated\nfn x() {}\n",
        ];
        for content in cases {
            let f = NamedTempFile::new().unwrap();
            fs::write(f.path(), content).unwrap();
            assert!(is_generated_file(f.path()).unwrap(), "content: {content:?}");
        }
    }

    #[test]
    fn normal_code_is_not_flagged_generated() {
        let f = NamedTempFile::new().unwrap();
        fs::write(f.path(), "fn main() {\n    println!(\"hi\");\n}\n").unwrap();
        assert!(!is_generated_file(f.path()).unwrap());
    }

    #[test]
    fn is_generated_returns_false_for_empty_file() {
        let f = NamedTempFile::new().unwrap();
        fs::write(f.path(), "").unwrap();
        assert!(!is_generated_file(f.path()).unwrap());
    }

    #[test]
    fn oversized_by_bytes_returns_true() {
        let f = NamedTempFile::new().unwrap();
        let body: String = "x".repeat(300_000);
        fs::write(f.path(), &body).unwrap();
        assert!(is_oversized(f.path(), 262_144, 5_000).unwrap());
    }

    #[test]
    fn oversized_by_lines_returns_true() {
        let f = NamedTempFile::new().unwrap();
        let body: String = "x\n".repeat(6_000);
        fs::write(f.path(), &body).unwrap();
        assert!(is_oversized(f.path(), 262_144, 5_000).unwrap());
    }

    #[test]
    fn small_file_returns_false_for_oversize() {
        let f = NamedTempFile::new().unwrap();
        fs::write(f.path(), "fn foo() {}\n").unwrap();
        assert!(!is_oversized(f.path(), 262_144, 5_000).unwrap());
    }
}
