//! Stage 1 — Input: source file discovery.
//!
//! Uses the `ignore` crate to walk a directory tree while respecting
//! `.gitignore`, `.ignore`, and other standard ignore files.

use anyhow::Result;
use ignore::WalkBuilder;
use std::path::PathBuf;
use tracing::{debug, info};

/// Supported source file extensions.
/// This is the single source of truth for all supported languages across the indexing pipeline.
pub const SUPPORTED_EXTENSIONS: &[&str] = &[
    "java",
    "ts",
    "tsx",
    "cts",
    "js",
    "mjs",
    "cjs",
    "jsx",
    "kt",
    "kts",
    "py",
    "pyi",
    "pyw",
    "html",
    "htm",
    "css",
    "scss",
    "sass",
    "rs",
    "groovy",
    "gradle",
    "c",
    "h",
    "cpp",
    "hpp",
    "cc",
    "cxx",
    "hh",
    "hxx",
    "yml",
    "yaml",
    "json",
    "properties",
    "tpl",
];

/// Lock/config file names to exclude from indexing (prevent indexing large generated files).
const EXCLUDED_NAMES: &[&str] = &[
    "package-lock.json",
    "yarn.lock",
    "pnpm-lock.yaml",
    "Cargo.lock",
    "composer.lock",
    "Gemfile.lock",
    "poetry.lock",
    "Pipfile.lock",
];

/// Maximum file size in bytes for indexing (500 KB).
/// Files larger than this are skipped to prevent indexing data dumps or generated files.
const MAX_FILE_SIZE: u64 = 500 * 1024;

/// Recursively discover all supported source files under `repo_path`.
///
/// Respects `.gitignore` and other ignore files found during traversal.
/// Returns absolute [`PathBuf`]s sorted for deterministic processing order.
pub fn discover_files(repo_path: &str) -> Result<Vec<PathBuf>> {
    use std::collections::HashSet;

    let mut files: Vec<PathBuf> = WalkBuilder::new(repo_path)
        .hidden(false)
        .git_ignore(true)
        .git_global(true)
        .git_exclude(true)
        .build()
        .filter_map(|entry| {
            let entry = entry.ok()?;
            let path = entry.path().to_path_buf();

            if !path.is_file() {
                return None;
            }

            // Exclude lock files and generated files by name
            if let Some(name) = path.file_name().and_then(|n| n.to_str())
                && EXCLUDED_NAMES.contains(&name)
            {
                debug!("Skipping excluded file: {}", name);
                return None;
            }

            // File size check — skip files > 500KB
            if let Ok(metadata) = std::fs::metadata(&path)
                && metadata.len() > MAX_FILE_SIZE
            {
                debug!(
                    "Skipping file over size limit ({} bytes): {}",
                    metadata.len(),
                    path.display()
                );
                return None;
            }

            // Match by extension first
            if let Some(ext) = path.extension().and_then(|e| e.to_str())
                && SUPPORTED_EXTENSIONS.contains(&ext)
            {
                return Some(path);
            }

            // Match by filename (e.g. Jenkinsfile has no extension)
            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                let known_names = [
                    "Jenkinsfile",
                    "pom.xml",
                    "Cargo.toml",
                    "package.json",
                    "tsconfig.json",
                ];
                if known_names.contains(&name) {
                    return Some(path);
                }
            }

            None
        })
        .collect();

    // Deduplicate (some files match both extension and filename)
    let mut seen = HashSet::new();
    files.retain(|p| seen.insert(p.clone()));

    files.sort();

    files.sort();

    info!(
        "Discovered {} source files under '{}'",
        files.len(),
        repo_path
    );

    Ok(files)
}

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

    #[test]
    fn test_discover_files_basic() {
        let dir = tempdir().unwrap();
        let repo_path = dir.path().to_str().unwrap();

        // Create supported files
        fs::write(dir.path().join("test.java"), "public class Test {}").unwrap();
        fs::write(dir.path().join("app.ts"), "export class App {}").unwrap();
        fs::write(
            dir.path().join("component.tsx"),
            "export const Comp = () => {}",
        )
        .unwrap();
        fs::write(dir.path().join("legacy.cts"), "module.exports = {}").unwrap();
        fs::write(dir.path().join("vanilla.js"), "console.log('test')").unwrap();
        fs::write(dir.path().join("module.mjs"), "export {}").unwrap();
        fs::write(dir.path().join("service.kt"), "class Service {}").unwrap();
        fs::write(dir.path().join("main.py"), "def main(): pass").unwrap();
        fs::write(dir.path().join("stub.pyi"), "def foo() -> None: ...").unwrap();
        fs::write(dir.path().join("gui.pyw"), "import tkinter").unwrap();

        // Create unsupported files
        fs::write(dir.path().join("readme.md"), "# Readme").unwrap();
        fs::write(dir.path().join("data.xml"), "<root/>").unwrap();

        // Create nested supported file
        let src_dir = dir.path().join("src");
        fs::create_dir(&src_dir).unwrap();
        fs::write(src_dir.join("utils.ts"), "export {}").unwrap();

        let files = discover_files(repo_path).unwrap();

        assert_eq!(files.len(), 11);

        // Verify extensions
        for path in files {
            let ext = path.extension().unwrap().to_str().unwrap();
            assert!(SUPPORTED_EXTENSIONS.contains(&ext));
        }
    }

    #[test]
    fn test_discover_files_with_gitignore() {
        let dir = tempdir().unwrap();
        let repo_path = dir.path().to_str().unwrap();

        // Create .git directory to make WalkBuilder treat it as a repo
        fs::create_dir(dir.path().join(".git")).unwrap();

        // Create supported files
        fs::write(dir.path().join("tracked.java"), "public class Tracked {}").unwrap();
        fs::write(dir.path().join("ignored.java"), "public class Ignored {}").unwrap();

        // Create .gitignore
        fs::write(dir.path().join(".gitignore"), "ignored.java").unwrap();

        let files = discover_files(repo_path).unwrap();

        // Should only find tracked.java
        assert_eq!(files.len(), 1);
        assert!(files[0].to_str().unwrap().contains("tracked.java"));
    }

    #[test]
    fn test_discover_files_empty() {
        let dir = tempdir().unwrap();
        let repo_path = dir.path().to_str().unwrap();

        let files = discover_files(repo_path).unwrap();
        assert!(files.is_empty());
    }
}
