//! Whitelist pattern matching for auto-approval of safe operations.
//!
//! **Status**: Implemented (TOOLS-PERM-04)
//!
//! This module provides pattern matching against configured whitelists to
//! automatically approve safe operations without prompting the user.
//!
//! ## Whitelist Strategy
//!
//! - **Read operations**: Always approved in whitelist mode (reads are safe)
//! - **Write operations**: Match path against `write_paths` glob patterns
//! - **Execute operations**: Match command against `execute_commands` glob patterns
//!
//! ## Pattern Matching
//!
//! Uses `globset` for efficient compiled glob patterns:
//! - `**` matches any number of path segments
//! - `*` matches any characters within a segment
//! - `?` matches a single character
//! - Character classes like `[abc]` are supported
//!
//! ## Performance
//!
//! Globs are pre-compiled at configuration load time and stored in `CompiledWhitelist`
//! for fast matching during permission checks.

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

/// Compiled whitelist with pre-built glob sets for efficient matching.
///
/// This should be constructed once at configuration load time and reused
/// for all permission checks.
#[derive(Debug, Clone)]
pub struct CompiledWhitelist {
    /// Compiled glob patterns for write path matching
    write_paths: GlobSet,
    /// Compiled glob patterns for execute command matching
    execute_commands: GlobSet,
}

impl CompiledWhitelist {
    /// Compile a whitelist configuration into an efficient matcher.
    ///
    /// ## Errors
    ///
    /// Returns an error if any glob pattern is invalid.
    pub fn compile(config: &WhitelistConfig) -> ToolResult<Self> {
        let write_paths = Self::compile_patterns(&config.write_paths)?;
        let execute_commands = Self::compile_patterns(&config.execute_commands)?;

        Ok(Self {
            write_paths,
            execute_commands,
        })
    }

    /// Compile a list of glob patterns into a GlobSet.
    fn compile_patterns(patterns: &[String]) -> ToolResult<GlobSet> {
        let mut builder = GlobSetBuilder::new();

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

        builder.build().map_err(|e| {
            ToolError::InvalidConfig(format!("Failed to build whitelist glob set: {}", e))
        })
    }

    /// Check if a read operation matches the whitelist.
    ///
    /// In whitelist mode, all read operations are considered safe and return true.
    pub fn matches_read(&self, _path: &Path) -> bool {
        // Reads are always safe in whitelist mode
        true
    }

    /// Check if a write operation matches the whitelist.
    ///
    /// Returns true if the path matches any of the configured write_paths patterns.
    pub fn matches_write(&self, path: &Path) -> bool {
        self.write_paths.is_match(path)
    }

    /// Check if an execute operation matches the whitelist.
    ///
    /// Returns true if the command matches any of the configured execute_commands patterns.
    ///
    /// The command is compared as a string (not a path) to support both:
    /// - Simple command names (e.g., "cargo", "npm")
    /// - Command patterns (e.g., "cargo*", "npm*")
    pub fn matches_execute(&self, command: &str) -> bool {
        // For command matching, we treat the command as a simple string path
        // This allows patterns like "cargo", "npm*", etc.
        self.execute_commands.is_match(command)
    }

    /// Check if the whitelist has any write patterns configured.
    pub fn has_write_patterns(&self) -> bool {
        !self.write_paths.is_empty()
    }

    /// Check if the whitelist has any execute patterns configured.
    pub fn has_execute_patterns(&self) -> bool {
        !self.execute_commands.is_empty()
    }
}

/// Operation type for whitelist matching.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PermissionOperation {
    Read { path: String },
    Write { path: String },
    Execute { command: String, cwd: String },
}

impl PermissionOperation {
    /// Get a display string for this operation (for logging/debugging).
    pub fn display(&self) -> String {
        match self {
            Self::Read { path } => format!("read {}", path),
            Self::Write { path } => format!("write {}", path),
            Self::Execute { command, cwd } => format!("execute '{}' in {}", command, cwd),
        }
    }

    /// Get the operation kind as a string.
    pub fn kind(&self) -> &'static str {
        match self {
            Self::Read { .. } => "read",
            Self::Write { .. } => "write",
            Self::Execute { .. } => "execute",
        }
    }
}

/// Check if an operation matches the configured whitelist.
///
/// ## Returns
///
/// - `true` if the operation should be auto-approved
/// - `false` if the operation requires a permission prompt
///
/// ## Examples
///
/// ```rust
/// use dirigent_tools::config::WhitelistConfig;
/// use dirigent_tools::permission::whitelist::{CompiledWhitelist, PermissionOperation, matches_whitelist};
///
/// let config = WhitelistConfig {
///     write_paths: vec!["C:/work/project/**".to_string()],
///     execute_commands: vec!["cargo".to_string(), "npm".to_string()],
/// };
///
/// let whitelist = CompiledWhitelist::compile(&config).unwrap();
///
/// // Read operations always match
/// let read_op = PermissionOperation::Read { path: "C:/anywhere/file.txt".to_string() };
/// assert!(matches_whitelist(&read_op, &whitelist));
///
/// // Write within allowed path
/// let write_op = PermissionOperation::Write { path: "C:/work/project/src/main.rs".to_string() };
/// assert!(matches_whitelist(&write_op, &whitelist));
///
/// // Execute whitelisted command
/// let exec_op = PermissionOperation::Execute {
///     command: "cargo".to_string(),
///     cwd: "C:/work/project".to_string(),
/// };
/// assert!(matches_whitelist(&exec_op, &whitelist));
/// ```
pub fn matches_whitelist(operation: &PermissionOperation, whitelist: &CompiledWhitelist) -> bool {
    match operation {
        PermissionOperation::Read { path } => {
            whitelist.matches_read(Path::new(path))
        }
        PermissionOperation::Write { path } => {
            whitelist.matches_write(Path::new(path))
        }
        PermissionOperation::Execute { command, .. } => {
            whitelist.matches_execute(command)
        }
    }
}

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

    fn create_test_whitelist() -> CompiledWhitelist {
        let config = WhitelistConfig {
            write_paths: vec![
                "C:/work/project/**".to_string(),
                "/home/user/project/**".to_string(),
                "**/safe_dir/**".to_string(),
            ],
            execute_commands: vec![
                "cargo".to_string(),
                "npm".to_string(),
                "git".to_string(),
                "python*".to_string(),
            ],
        };
        CompiledWhitelist::compile(&config).unwrap()
    }

    #[test]
    fn test_read_always_matches() {
        let whitelist = create_test_whitelist();

        // Any read operation should match
        assert!(whitelist.matches_read(Path::new("C:/anywhere/file.txt")));
        assert!(whitelist.matches_read(Path::new("/random/path")));
        assert!(whitelist.matches_read(Path::new("../../../etc/passwd")));
    }

    #[test]
    fn test_write_matches_patterns() {
        let whitelist = create_test_whitelist();

        // Should match configured patterns
        assert!(whitelist.matches_write(Path::new("C:/work/project/src/main.rs")));
        assert!(whitelist.matches_write(Path::new("C:/work/project/Cargo.toml")));
        assert!(whitelist.matches_write(Path::new("/home/user/project/README.md")));

        // Should not match paths outside patterns
        assert!(!whitelist.matches_write(Path::new("C:/other/project/file.txt")));
        assert!(!whitelist.matches_write(Path::new("/tmp/file.txt")));
    }

    #[test]
    fn test_write_matches_relative_patterns() {
        let whitelist = create_test_whitelist();

        // Pattern **/safe_dir/** should match anywhere
        assert!(whitelist.matches_write(Path::new("some/path/safe_dir/file.txt")));
        assert!(whitelist.matches_write(Path::new("safe_dir/file.txt")));
    }

    #[test]
    fn test_execute_matches_commands() {
        let whitelist = create_test_whitelist();

        // Should match exact command names
        assert!(whitelist.matches_execute("cargo"));
        assert!(whitelist.matches_execute("npm"));
        assert!(whitelist.matches_execute("git"));

        // Should match patterns
        assert!(whitelist.matches_execute("python"));
        assert!(whitelist.matches_execute("python3"));

        // Should not match unlisted commands
        assert!(!whitelist.matches_execute("rm"));
        assert!(!whitelist.matches_execute("format"));
        assert!(!whitelist.matches_execute("del"));
    }

    #[test]
    fn test_empty_whitelist() {
        let config = WhitelistConfig {
            write_paths: vec![],
            execute_commands: vec![],
        };
        let whitelist = CompiledWhitelist::compile(&config).unwrap();

        // Reads should still match
        assert!(whitelist.matches_read(Path::new("any/path")));

        // Writes and executes should not match empty whitelist
        assert!(!whitelist.matches_write(Path::new("any/path")));
        assert!(!whitelist.matches_execute("any_command"));
    }

    #[test]
    fn test_invalid_glob_pattern() {
        let config = WhitelistConfig {
            write_paths: vec!["[invalid".to_string()], // Unclosed bracket
            execute_commands: vec![],
        };

        let result = CompiledWhitelist::compile(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_operation_display() {
        let read_op = PermissionOperation::Read {
            path: "/path/to/file".to_string(),
        };
        assert_eq!(read_op.display(), "read /path/to/file");

        let write_op = PermissionOperation::Write {
            path: "/path/to/file".to_string(),
        };
        assert_eq!(write_op.display(), "write /path/to/file");

        let exec_op = PermissionOperation::Execute {
            command: "cargo test".to_string(),
            cwd: "/project".to_string(),
        };
        assert_eq!(exec_op.display(), "execute 'cargo test' in /project");
    }

    #[test]
    fn test_operation_kind() {
        let read_op = PermissionOperation::Read {
            path: "/path".to_string(),
        };
        assert_eq!(read_op.kind(), "read");

        let write_op = PermissionOperation::Write {
            path: "/path".to_string(),
        };
        assert_eq!(write_op.kind(), "write");

        let exec_op = PermissionOperation::Execute {
            command: "cmd".to_string(),
            cwd: "/".to_string(),
        };
        assert_eq!(exec_op.kind(), "execute");
    }

    #[test]
    fn test_matches_whitelist_function() {
        let whitelist = create_test_whitelist();

        // Test read
        let read_op = PermissionOperation::Read {
            path: "C:/any/file.txt".to_string(),
        };
        assert!(matches_whitelist(&read_op, &whitelist));

        // Test write (match)
        let write_ok = PermissionOperation::Write {
            path: "C:/work/project/file.txt".to_string(),
        };
        assert!(matches_whitelist(&write_ok, &whitelist));

        // Test write (no match)
        let write_fail = PermissionOperation::Write {
            path: "C:/other/file.txt".to_string(),
        };
        assert!(!matches_whitelist(&write_fail, &whitelist));

        // Test execute (match)
        let exec_ok = PermissionOperation::Execute {
            command: "cargo".to_string(),
            cwd: "/project".to_string(),
        };
        assert!(matches_whitelist(&exec_ok, &whitelist));

        // Test execute (no match)
        let exec_fail = PermissionOperation::Execute {
            command: "rm".to_string(),
            cwd: "/".to_string(),
        };
        assert!(!matches_whitelist(&exec_fail, &whitelist));
    }

    #[test]
    fn test_has_patterns() {
        let config = WhitelistConfig {
            write_paths: vec!["some/path/**".to_string()],
            execute_commands: vec![],
        };
        let whitelist = CompiledWhitelist::compile(&config).unwrap();

        assert!(whitelist.has_write_patterns());
        assert!(!whitelist.has_execute_patterns());
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn test_windows_paths() {
        let config = WhitelistConfig {
            write_paths: vec![
                "C:\\work\\project\\**".to_string(),
                "\\\\server\\share\\**".to_string(), // UNC path
            ],
            execute_commands: vec![],
        };
        let whitelist = CompiledWhitelist::compile(&config).unwrap();

        // Test Windows backslash paths
        assert!(whitelist.matches_write(Path::new("C:\\work\\project\\file.txt")));

        // Test UNC paths
        assert!(whitelist.matches_write(Path::new("\\\\server\\share\\file.txt")));
    }
}
