//! Ignore rules module for filtering files during indexing.
//!
//! This module provides functionality to filter files based on:
//! - .gitignore patterns
//! - .ignore files
//! - Custom patterns
//! - File extensions
//! - Directory names
//! - File size limits

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

use ignore::gitignore::{Gitignore, GitignoreBuilder};
use ignore::WalkBuilder;

use crate::config::IgnoreConfig;
use crate::error::Result;

/// File filter for determining which files to index.
pub struct FileFilter {
    config: IgnoreConfig,
    gitignore: Option<Gitignore>,
    custom_patterns: Vec<glob::Pattern>,
    /// Root path of the workspace (for computing relative paths).
    root_path: Option<PathBuf>,
    /// Normalized excluded file paths (relative to workspace root).
    excluded_files: Vec<PathBuf>,
    /// Normalized excluded directory paths (relative to workspace root).
    excluded_directory_paths: Vec<PathBuf>,
}

impl FileFilter {
    /// Create a new file filter with the given configuration.
    pub fn new(config: IgnoreConfig) -> Self {
        // Normalize excluded paths for cross-platform matching
        let excluded_files: Vec<PathBuf> = config.excluded_files.iter()
            .map(|p| Self::normalize_path(p))
            .collect();
        let excluded_directory_paths: Vec<PathBuf> = config.excluded_directories.iter()
            .map(|p| Self::normalize_path(p))
            .collect();
        
        Self {
            config,
            gitignore: None,
            custom_patterns: Vec::new(),
            root_path: None,
            excluded_files,
            excluded_directory_paths,
        }
    }

    /// Create a file filter for a specific workspace.
    pub fn for_workspace(root: &Path, config: IgnoreConfig) -> Result<Self> {
        let mut filter = Self::new(config);
        filter.root_path = Some(root.to_path_buf());
        filter.load_gitignore(root)?;
        filter.compile_patterns()?;
        Ok(filter)
    }

    /// Normalize a path for cross-platform comparison.
    /// Converts backslashes to forward slashes and removes trailing slashes.
    fn normalize_path(path: &Path) -> PathBuf {
        let path_str = path.to_string_lossy().replace('\\', "/");
        let trimmed = path_str.trim_end_matches('/');
        PathBuf::from(trimmed)
    }

    /// Get the relative path from the workspace root, normalized for comparison.
    fn get_relative_path(&self, path: &Path) -> PathBuf {
        let relative = if let Some(ref root) = self.root_path {
            path.strip_prefix(root).unwrap_or(path)
        } else {
            path
        };
        Self::normalize_path(relative)
    }

    /// Load .gitignore file from workspace root.
    fn load_gitignore(&mut self, root: &Path) -> Result<()> {
        if !self.config.use_gitignore {
            return Ok(());
        }

        let gitignore_path = root.join(".gitignore");
        if gitignore_path.exists() {
            let mut builder = GitignoreBuilder::new(root);
            builder.add(&gitignore_path);

            // Also check for global gitignore
            if let Some(home) = dirs::home_dir() {
                let global_gitignore = home.join(".gitignore_global");
                if global_gitignore.exists() {
                    builder.add(&global_gitignore);
                }
            }

            self.gitignore = builder.build().ok();
        }

        Ok(())
    }

    /// Compile custom glob patterns.
    fn compile_patterns(&mut self) -> Result<()> {
        for pattern in &self.config.patterns {
            if let Ok(compiled) = glob::Pattern::new(pattern) {
                self.custom_patterns.push(compiled);
            }
        }
        Ok(())
    }

    /// Check if a file should be indexed.
    pub fn should_index(&self, path: &Path, file_size: u64) -> bool {
        // Check file size limit
        if file_size > self.config.max_file_size {
            return false;
        }

        // Get filename for pattern matching
        let filename = path.file_name()
            .and_then(|n| n.to_str())
            .map(|s| s.to_lowercase())
            .unwrap_or_default();

        // Check for specific ignored filenames (lock files, etc.)
        // These are critical files that should NEVER be indexed
        const IGNORED_FILENAMES: &[&str] = &[
            "pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb",
            "cargo.lock", "gemfile.lock", "composer.lock", "poetry.lock",
            "pipfile.lock", "pubspec.lock", "packages.lock.json",
            "shrinkwrap.yaml", "npm-shrinkwrap.json",
            ".ds_store", "thumbs.db", "desktop.ini",
            ".gitignore", ".gitattributes", ".gitmodules",
            ".npmrc", ".yarnrc", ".nvmrc", ".node-version",
            ".env", ".env.local", ".env.development", ".env.production",
        ];
        
        if IGNORED_FILENAMES.iter().any(|&f| filename == f) {
            return false;
        }

        // Check extension
        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
            let ext_lower = ext.to_lowercase();
            if self.config.ignored_extensions.iter().any(|e| e == &ext_lower) {
                return false;
            }
        }

        // Check if in ignored directory
        for component in path.components() {
            if let Some(name) = component.as_os_str().to_str() {
                if self.config.ignored_directories.iter().any(|d| d == name) {
                    return false;
                }
            }
        }

        // Check gitignore
        if let Some(ref gitignore) = self.gitignore {
            if gitignore.matched(path, path.is_dir()).is_ignore() {
                return false;
            }
        }

        // Check custom patterns
        let path_str = path.to_string_lossy();
        for pattern in &self.custom_patterns {
            if pattern.matches(&path_str) {
                return false;
            }
        }

        // Check explicitly excluded file paths
        let relative_path = self.get_relative_path(path);
        if self.excluded_files.iter().any(|excluded| {
            *excluded == relative_path
        }) {
            return false;
        }

        // Check if file is inside an explicitly excluded directory
        for excluded_dir in &self.excluded_directory_paths {
            if relative_path.starts_with(excluded_dir) {
                return false;
            }
        }

        true
    }

    /// Check if a directory should be traversed.
    pub fn should_traverse(&self, path: &Path) -> bool {
        // Check if directory is in ignored list
        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
            if self.config.ignored_directories.iter().any(|d| d == name) {
                return false;
            }
        }

        // Check gitignore
        if let Some(ref gitignore) = self.gitignore {
            if gitignore.matched(path, true).is_ignore() {
                return false;
            }
        }

        // Check explicitly excluded directory paths
        let relative_path = self.get_relative_path(path);
        for excluded_dir in &self.excluded_directory_paths {
            if relative_path == *excluded_dir || relative_path.starts_with(excluded_dir) {
                return false;
            }
        }

        true
    }
}

/// Walk a directory and yield files that should be indexed.
pub struct FileWalker {
    filter: FileFilter,
    root: PathBuf,
}

impl FileWalker {
    /// Create a new file walker.
    pub fn new(root: PathBuf, config: IgnoreConfig) -> Result<Self> {
        let filter = FileFilter::for_workspace(&root, config)?;
        Ok(Self { filter, root })
    }

    /// Walk the directory and collect files to index.
    pub fn walk(&self) -> Result<Vec<PathBuf>> {
        let mut files = Vec::new();

        let walker = WalkBuilder::new(&self.root)
            .hidden(false) // Show hidden files
            .git_ignore(self.filter.config.use_gitignore)
            .git_global(self.filter.config.use_gitignore)
            .git_exclude(self.filter.config.use_gitignore)
            .ignore(self.filter.config.use_ignore_files)
            .build();

        for entry in walker.filter_map(|e| e.ok()) {
            let path = entry.path();

            // Skip directories
            if path.is_dir() {
                continue;
            }

            // Get file size
            let metadata = match std::fs::metadata(path) {
                Ok(m) => m,
                Err(_) => continue,
            };

            // Apply filter
            if self.filter.should_index(path, metadata.len()) {
                files.push(path.to_path_buf());
            }
        }

        Ok(files)
    }

    /// Walk the directory with a callback for each file.
    pub fn walk_with_callback<F>(&self, mut callback: F) -> Result<()>
    where
        F: FnMut(&Path, u64) -> bool, // Return false to stop
    {
        let walker = WalkBuilder::new(&self.root)
            .hidden(false)
            .git_ignore(self.filter.config.use_gitignore)
            .git_global(self.filter.config.use_gitignore)
            .git_exclude(self.filter.config.use_gitignore)
            .ignore(self.filter.config.use_ignore_files)
            .build();

        for entry in walker.filter_map(|e| e.ok()) {
            let path = entry.path();

            if path.is_dir() {
                continue;
            }

            let metadata = match std::fs::metadata(path) {
                Ok(m) => m,
                Err(_) => continue,
            };

            if self.filter.should_index(path, metadata.len()) {
                if !callback(path, metadata.len()) {
                    break;
                }
            }
        }

        Ok(())
    }
}

/// Scan a workspace and count files without loading them.
#[allow(dead_code)]
pub fn scan_workspace(root: &Path, config: &IgnoreConfig) -> Result<WorkspaceScan> {
    let filter = FileFilter::for_workspace(root, config.clone())?;

    let walker = WalkBuilder::new(root)
        .hidden(false)
        .git_ignore(config.use_gitignore)
        .git_global(config.use_gitignore)
        .git_exclude(config.use_gitignore)
        .ignore(config.use_ignore_files)
        .build();

    let mut scan = WorkspaceScan {
        file_count: 0,
        total_bytes: 0,
        by_extension: std::collections::HashMap::new(),
    };

    for entry in walker.filter_map(|e| e.ok()) {
        let path = entry.path();

        if path.is_dir() {
            continue;
        }

        let metadata = match std::fs::metadata(path) {
            Ok(m) => m,
            Err(_) => continue,
        };

        if filter.should_index(path, metadata.len()) {
            scan.file_count += 1;
            scan.total_bytes += metadata.len();

            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
                *scan.by_extension.entry(ext.to_lowercase()).or_insert(0) += 1;
            }
        }
    }

    Ok(scan)
}

/// Result of scanning a workspace.
#[derive(Debug)]
#[allow(dead_code)]
pub struct WorkspaceScan {
    /// Number of files to index.
    pub file_count: usize,
    /// Total size of files in bytes.
    pub total_bytes: u64,
    /// File count by extension.
    pub by_extension: std::collections::HashMap<String, usize>,
}

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

    #[test]
    fn test_file_filter_extension() {
        let config = IgnoreConfig::default();
        let filter = FileFilter::new(config);

        // Should filter out binary files
        assert!(!filter.should_index(Path::new("test.exe"), 100));
        assert!(!filter.should_index(Path::new("test.dll"), 100));

        // Should allow source files
        assert!(filter.should_index(Path::new("test.rs"), 100));
        assert!(filter.should_index(Path::new("test.py"), 100));
    }

    #[test]
    fn test_file_filter_size() {
        let config = IgnoreConfig {
            max_file_size: 1000,
            ..Default::default()
        };
        let filter = FileFilter::new(config);

        assert!(filter.should_index(Path::new("small.rs"), 500));
        assert!(!filter.should_index(Path::new("large.rs"), 2000));
    }

    #[test]
    fn test_file_filter_directory() {
        let config = IgnoreConfig::default();
        let filter = FileFilter::new(config);

        assert!(!filter.should_index(Path::new("node_modules/package/index.js"), 100));
        assert!(!filter.should_index(Path::new("target/debug/main.rs"), 100));
        assert!(filter.should_index(Path::new("src/main.rs"), 100));
    }

    #[test]
    fn test_file_filter_excluded_paths() {
        let config = IgnoreConfig {
            excluded_files: vec![PathBuf::from("src/generated.rs")],
            excluded_directories: vec![PathBuf::from("vendor/libs")],
            ..Default::default()
        };
        let filter = FileFilter::for_workspace(Path::new("/project"), config).unwrap();
        
        // Should exclude explicitly specified file
        assert!(!filter.should_index(Path::new("/project/src/generated.rs"), 100));
        
        // Should allow other files in same directory
        assert!(filter.should_index(Path::new("/project/src/main.rs"), 100));
        
        // Should exclude files inside excluded directory
        assert!(!filter.should_index(Path::new("/project/vendor/libs/helper.rs"), 100));
        
        // Should not traverse excluded directory
        assert!(!filter.should_traverse(Path::new("/project/vendor/libs")));
        
        // Should allow traversing non-excluded directories
        assert!(filter.should_traverse(Path::new("/project/src")));
    }

    #[test]
    fn test_file_filter_aurora_excluded_by_default() {
        let config = IgnoreConfig::default();
        let filter = FileFilter::for_workspace(Path::new("/project"), config).unwrap();
        
        // Aurora index directory should be excluded by default
        assert!(!filter.should_traverse(Path::new("/project/.aurora")));
        assert!(!filter.should_index(Path::new("/project/.aurora/index.bin"), 100));
    }
}
