//! Configuration types for Phase 03 features.
//!
//! This module defines configuration structures for:
//! - `SandboxConfig` - Filesystem sandboxing configuration
//! - `PermissionConfig` - Permission prompt and decision caching
//! - `TerminalConfig` - Terminal/command execution limits
//! - `SearchConfig` - Search operation limits and defaults
//! - `EmbeddingConfig` - File embedding thresholds and policies

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

// =============================================================================
// Sandbox Configuration
// =============================================================================

/// Filesystem sandboxing configuration.
///
/// Determines which paths are accessible and how symlinks are handled.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SandboxConfig {
    /// Absolute paths that are allowed for file operations.
    ///
    /// Operations outside these roots will be rejected.
    pub allowed_roots: Vec<PathBuf>,

    /// Path patterns that are explicitly blocked even if within allowed roots.
    ///
    /// Supports glob patterns like "**/.env", "**/secrets/**".
    pub blocked_paths: Vec<String>,

    /// Whether to allow symlinks to escape allowed roots.
    ///
    /// If false (recommended), symlinks pointing outside allowed roots are rejected.
    pub allow_symlink_escape: bool,

    /// Whether to follow symlinks within allowed roots.
    ///
    /// If true, symlinks within allowed roots are followed.
    pub follow_symlinks_within_roots: bool,

    /// Enable read operations.
    pub read_enabled: bool,

    /// Enable write operations.
    pub write_enabled: bool,

    /// Maximum bytes to read in a single request.
    ///
    /// Soft cap for previews. Default: 1 MB.
    pub max_read_bytes: u64,

    /// Maximum bytes to write in a single request.
    ///
    /// Default: 1 MB.
    pub max_write_bytes: u64,

    /// End-of-line policy for file operations.
    pub eol_policy: EolPolicy,

    /// Text encoding support.
    ///
    /// Currently only UTF-8 is supported.
    pub encoding: String,
}

impl Default for SandboxConfig {
    fn default() -> Self {
        Self {
            allowed_roots: vec![],
            blocked_paths: vec!["**/.env".to_string(), "**/secrets/**".to_string()],
            allow_symlink_escape: false,
            follow_symlinks_within_roots: true,
            read_enabled: true,
            write_enabled: false,
            max_read_bytes: 1_048_576, // 1 MB
            max_write_bytes: 1_048_576, // 1 MB
            eol_policy: EolPolicy::Preserve,
            encoding: "utf-8".to_string(),
        }
    }
}

/// End-of-line handling policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EolPolicy {
    /// Preserve original line endings.
    Preserve,
    /// Normalize to LF (\n).
    Lf,
    /// Normalize to CRLF (\r\n).
    Crlf,
}

// =============================================================================
// Permission Configuration
// =============================================================================

/// Permission prompt and decision caching configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PermissionConfig {
    /// Permission mode strategy.
    pub mode: PermissionMode,

    /// Whether to remember permission decisions.
    pub remember_decisions: bool,

    /// Time-to-live for cached decisions in seconds.
    ///
    /// Default: 86400 (24 hours).
    pub remember_ttl_secs: u64,

    /// Scope of cached decisions.
    pub scope: DecisionScope,

    /// Whitelist configuration for whitelist mode.
    pub whitelist: WhitelistConfig,
}

impl Default for PermissionConfig {
    fn default() -> Self {
        Self {
            mode: PermissionMode::Whitelist,
            remember_decisions: true,
            remember_ttl_secs: 86_400, // 24 hours
            scope: DecisionScope::PerConnector,
            whitelist: WhitelistConfig::default(),
        }
    }
}

/// Permission mode strategy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionMode {
    /// Prompt for every sensitive operation.
    Ask,
    /// Auto-approve whitelisted operations, prompt for others.
    Whitelist,
    /// Auto-approve all operations (with audit logging).
    Yolo,
}

/// Scope for cached permission decisions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionScope {
    /// Decisions persist per connector.
    PerConnector,
    /// Decisions persist only within a session.
    PerSession,
}

/// Whitelist configuration for auto-approved operations.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitelistConfig {
    /// Path patterns that are safe for write operations.
    ///
    /// Glob patterns like "C:/work/project/**".
    pub write_paths: Vec<String>,

    /// Commands that are safe to execute.
    ///
    /// Glob patterns like "cargo", "npm", "git".
    pub execute_commands: Vec<String>,
}

impl Default for WhitelistConfig {
    fn default() -> Self {
        Self {
            write_paths: vec![],
            execute_commands: vec![],
        }
    }
}

// =============================================================================
// Terminal Configuration
// =============================================================================

/// Terminal/command execution configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TerminalConfig {
    /// Enable terminal operations.
    pub enabled: bool,

    /// Default current working directory for terminal commands.
    ///
    /// Must be within an allowed sandbox root.
    pub default_cwd: Option<PathBuf>,

    /// Environment variable names that are allowed.
    ///
    /// Only these variables can be set in spawned processes.
    pub env_allowlist: Vec<String>,

    /// Command patterns that are blocked (best-effort).
    ///
    /// Glob patterns for dangerous commands like "rm", "format".
    pub command_blocklist: Vec<String>,

    /// Maximum bytes to capture from terminal output (ring buffer).
    ///
    /// Default: 200,000 bytes.
    pub output_byte_limit: u64,

    /// Maximum runtime for a terminal command in seconds.
    ///
    /// Commands exceeding this will be killed. Default: 3600 (1 hour).
    pub max_runtime_secs: u64,
}

impl Default for TerminalConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            default_cwd: None,
            env_allowlist: vec![
                "RUST_LOG".to_string(),
                "NODE_ENV".to_string(),
                "PATH".to_string(),
            ],
            command_blocklist: vec![
                "rm".to_string(),
                "rd".to_string(),
                "format".to_string(),
                "mkfs*".to_string(),
            ],
            output_byte_limit: 200_000,
            max_runtime_secs: 3_600, // 1 hour
        }
    }
}

// =============================================================================
// Search Configuration
// =============================================================================

/// Search operation configuration (glob, grep, ls).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SearchConfig {
    /// Maximum number of results to return.
    ///
    /// Default: 5,000.
    pub max_results: u32,

    /// Maximum total bytes in search results.
    ///
    /// Default: 1,000,000 (1 MB).
    pub max_bytes: u64,

    /// Default include patterns for searches.
    pub default_include_globs: Vec<String>,

    /// Default exclude patterns for searches.
    ///
    /// Common directories to skip like target/, .git/, node_modules/.
    pub default_exclude_globs: Vec<String>,
}

impl Default for SearchConfig {
    fn default() -> Self {
        Self {
            max_results: 5_000,
            max_bytes: 1_000_000, // 1 MB
            default_include_globs: vec![],
            default_exclude_globs: vec![
                "**/target/**".to_string(),
                "**/.git/**".to_string(),
                "**/node_modules/**".to_string(),
                "**/__pycache__/**".to_string(),
                "**/.venv/**".to_string(),
            ],
        }
    }
}

// =============================================================================
// Embedding Configuration
// =============================================================================

/// File embedding configuration for ACP prompt context.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EmbeddingConfig {
    /// Maximum bytes to embed as ContentBlock::resource per file.
    ///
    /// Larger files will use resource_link instead. Default: 256,000.
    pub max_embed_bytes: u64,

    /// Whether to allow resource_link for large files.
    ///
    /// If false, large files are rejected instead of linked.
    pub allow_resource_link: bool,

    /// Regex patterns for redacting secrets in embedded content.
    ///
    /// Best-effort redaction (does not modify files on disk).
    pub redact_patterns: Vec<String>,

    /// Snippet strategy when file is too large to embed fully.
    pub snippet_strategy: SnippetStrategy,

    /// Maximum number of files to embed in a single prompt.
    pub max_files_per_prompt: u32,
}

impl Default for EmbeddingConfig {
    fn default() -> Self {
        Self {
            max_embed_bytes: 256_000,
            allow_resource_link: true,
            redact_patterns: vec![
                // Common secret patterns
                r"(?i)(api[_-]?key|password|secret|token)[:]\s*[']?([a-zA-Z0-9_\-\.]+)[']?".to_string(),
            ],
            snippet_strategy: SnippetStrategy::HeadTail,
            max_files_per_prompt: 10,
        }
    }
}

/// Strategy for creating snippets from large files.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SnippetStrategy {
    /// Include beginning and end of file.
    HeadTail,
    /// Include only the beginning.
    HeadOnly,
    /// Include only the end.
    TailOnly,
}

// =============================================================================
// Validation
// =============================================================================

impl SandboxConfig {
    /// Validate the sandbox configuration.
    ///
    /// Returns an error message if the configuration is invalid.
    pub fn validate(&self) -> Result<(), String> {
        if self.allowed_roots.is_empty() && (self.read_enabled || self.write_enabled) {
            return Err("allowed_roots cannot be empty when read or write is enabled".to_string());
        }

        if self.encoding != "utf-8" {
            return Err(format!("unsupported encoding: {}", self.encoding));
        }

        Ok(())
    }

    /// Normalize allowed roots to canonical paths.
    ///
    /// This should be called after loading configuration to ensure all roots are canonical.
    /// Panics if any root cannot be canonicalized (configuration error).
    pub fn normalize_roots(&mut self) {
        self.allowed_roots = self.allowed_roots
            .iter()
            .map(|root| {
                dunce::canonicalize(root)
                    .unwrap_or_else(|_| panic!("Failed to canonicalize allowed root: {:?}", root))
            })
            .collect();
    }
}

impl TerminalConfig {
    /// Validate the terminal configuration.
    ///
    /// Returns an error message if the configuration is invalid.
    pub fn validate(&self) -> Result<(), String> {
        if self.output_byte_limit == 0 {
            return Err("output_byte_limit must be greater than 0".to_string());
        }

        if self.max_runtime_secs == 0 {
            return Err("max_runtime_secs must be greater than 0".to_string());
        }

        Ok(())
    }
}

impl SearchConfig {
    /// Validate the search configuration.
    ///
    /// Returns an error message if the configuration is invalid.
    pub fn validate(&self) -> Result<(), String> {
        if self.max_results == 0 {
            return Err("max_results must be greater than 0".to_string());
        }

        if self.max_bytes == 0 {
            return Err("max_bytes must be greater than 0".to_string());
        }

        Ok(())
    }
}

impl EmbeddingConfig {
    /// Validate the embedding configuration.
    ///
    /// Returns an error message if the configuration is invalid.
    pub fn validate(&self) -> Result<(), String> {
        if self.max_embed_bytes == 0 {
            return Err("max_embed_bytes must be greater than 0".to_string());
        }

        if self.max_files_per_prompt == 0 {
            return Err("max_files_per_prompt must be greater than 0".to_string());
        }

        Ok(())
    }
}
