//! Blocklist evaluation using glob patterns.
//!
//! This module checks if paths match configured blocklist patterns,
//! allowing fine-grained denial of sensitive paths even within allowed roots.

use crate::error::{ToolError, ToolResult};
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::path::Path;

/// Check if a path matches any blocklist patterns.
///
/// Blocklist patterns can be:
/// - Absolute paths (exact match)
/// - Glob patterns (e.g., `**/.env`, `**/secrets/**`)
///
/// # Arguments
///
/// * `canonical_path` - The canonical path to check
/// * `blocklist_patterns` - List of path patterns or globs to deny
///
/// # Returns
///
/// Ok(()) if the path is not blocked, or an error if it matches a blocklist pattern.
///
/// # Performance
///
/// For best performance, pre-compile patterns into a `CompiledBlocklist` and use
/// `check_blocklist_compiled` instead.
pub fn check_blocklist(
    canonical_path: &Path,
    blocklist_patterns: &[String],
) -> ToolResult<()> {
    if blocklist_patterns.is_empty() {
        return Ok(());
    }

    // Compile patterns on the fly (slower)
    let compiled = compile_blocklist(blocklist_patterns)?;
    check_blocklist_compiled(canonical_path, &compiled)
}

/// Pre-compiled blocklist for efficient matching.
pub struct CompiledBlocklist {
    glob_set: GlobSet,
}

impl CompiledBlocklist {
    /// Create a new compiled blocklist.
    pub fn new(glob_set: GlobSet) -> Self {
        Self { glob_set }
    }

    /// Get the underlying GlobSet.
    pub fn glob_set(&self) -> &GlobSet {
        &self.glob_set
    }
}

/// Compile blocklist patterns into a GlobSet for efficient matching.
///
/// This should be done once at configuration load time.
///
/// # Arguments
///
/// * `patterns` - List of glob patterns or absolute paths
///
/// # Returns
///
/// A compiled blocklist ready for fast matching.
pub fn compile_blocklist(patterns: &[String]) -> ToolResult<CompiledBlocklist> {
    let mut builder = GlobSetBuilder::new();

    for pattern in patterns {
        let glob = Glob::new(pattern).map_err(|e| {
            ToolError::InvalidConfig(format!("Invalid blocklist pattern '{}': {}", pattern, e))
        })?;
        builder.add(glob);
    }

    let glob_set = builder.build().map_err(|e| {
        ToolError::InvalidConfig(format!("Failed to compile blocklist patterns: {}", e))
    })?;

    Ok(CompiledBlocklist::new(glob_set))
}

/// Check if a path matches a pre-compiled blocklist.
///
/// This is the fast path for blocklist checking.
///
/// # Arguments
///
/// * `canonical_path` - The canonical path to check
/// * `compiled` - Pre-compiled blocklist
///
/// # Returns
///
/// Ok(()) if the path is not blocked, or an error if it matches a pattern.
pub fn check_blocklist_compiled(
    canonical_path: &Path,
    compiled: &CompiledBlocklist,
) -> ToolResult<()> {
    if compiled.glob_set.is_match(canonical_path) {
        return Err(ToolError::blocked_path(format!(
            "Path matches blocklist: {}",
            super::basename(canonical_path)
        )));
    }

    Ok(())
}

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

    #[test]
    fn test_empty_blocklist() {
        let result = check_blocklist(Path::new("/any/path"), &[]);
        assert!(result.is_ok());
    }

    #[test]
    fn test_exact_path_match() {
        let patterns = vec![
            "/etc/passwd".to_string(),
            "/home/user/.ssh/id_rsa".to_string(),
        ];

        // Blocked
        assert!(check_blocklist(Path::new("/etc/passwd"), &patterns).is_err());
        assert!(check_blocklist(Path::new("/home/user/.ssh/id_rsa"), &patterns).is_err());

        // Not blocked
        assert!(check_blocklist(Path::new("/etc/hosts"), &patterns).is_ok());
    }

    #[test]
    fn test_glob_patterns() {
        let patterns = vec![
            "**/.env".to_string(),
            "**/secrets/**".to_string(),
            "**/.git/**".to_string(),
        ];

        // Blocked by .env pattern
        assert!(check_blocklist(Path::new("/project/.env"), &patterns).is_err());
        assert!(check_blocklist(Path::new("/project/subdir/.env"), &patterns).is_err());

        // Blocked by secrets pattern
        assert!(check_blocklist(Path::new("/project/secrets/key.txt"), &patterns).is_err());
        assert!(check_blocklist(Path::new("/app/secrets/api_key"), &patterns).is_err());

        // Blocked by .git pattern
        assert!(check_blocklist(Path::new("/project/.git/config"), &patterns).is_err());

        // Not blocked
        assert!(check_blocklist(Path::new("/project/src/main.rs"), &patterns).is_ok());
        assert!(check_blocklist(Path::new("/project/README.md"), &patterns).is_ok());
    }

    #[test]
    fn test_compile_blocklist() {
        let patterns = vec![
            "**/.env".to_string(),
            "**/secrets/**".to_string(),
        ];

        let compiled = compile_blocklist(&patterns).unwrap();

        // Blocked
        assert!(check_blocklist_compiled(
            Path::new("/project/.env"),
            &compiled
        ).is_err());

        // Not blocked
        assert!(check_blocklist_compiled(
            Path::new("/project/src/main.rs"),
            &compiled
        ).is_ok());
    }

    #[test]
    fn test_invalid_pattern() {
        let patterns = vec![
            "[invalid".to_string(), // Invalid glob syntax
        ];

        let result = compile_blocklist(&patterns);
        assert!(result.is_err());
    }

    #[cfg(windows)]
    #[test]
    fn test_windows_paths() {
        let patterns = vec![
            "**/.env".to_string(),
            "C:/secrets/**".to_string(),
        ];

        let compiled = compile_blocklist(&patterns).unwrap();

        // Blocked
        assert!(check_blocklist_compiled(
            Path::new("C:\\project\\.env"),
            &compiled
        ).is_err());
        assert!(check_blocklist_compiled(
            Path::new("C:\\secrets\\key.txt"),
            &compiled
        ).is_err());

        // Not blocked
        assert!(check_blocklist_compiled(
            Path::new("C:\\project\\src\\main.rs"),
            &compiled
        ).is_ok());
    }
}
