//! Configuration management for nginx-lint.
//!
//! This module handles loading and validating the `.nginx-lint.toml`
//! configuration file. The main entry point is [`LintConfig`], which can be
//! loaded from a file with [`LintConfig::from_file`] or discovered
//! automatically with [`LintConfig::find_and_load`].

use schemars::JsonSchema;
use serde::{Deserialize, Deserializer};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs;
use std::path::Path;

/// Indent size configuration: either a fixed number or "auto" for auto-detection
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndentSize {
    /// Auto-detect indent size from the first indented line
    #[default]
    Auto,
    /// Fixed indent size (number of spaces)
    Fixed(usize),
}

impl fmt::Display for IndentSize {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            IndentSize::Auto => write!(f, "auto"),
            IndentSize::Fixed(n) => write!(f, "{}", n),
        }
    }
}

impl<'de> Deserialize<'de> for IndentSize {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        use serde::de::{self, Visitor};

        struct IndentSizeVisitor;

        impl<'de> Visitor<'de> for IndentSizeVisitor {
            type Value = IndentSize;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a positive integer or \"auto\"")
            }

            fn visit_u64<E>(self, value: u64) -> Result<IndentSize, E>
            where
                E: de::Error,
            {
                Ok(IndentSize::Fixed(value as usize))
            }

            fn visit_i64<E>(self, value: i64) -> Result<IndentSize, E>
            where
                E: de::Error,
            {
                if value > 0 {
                    Ok(IndentSize::Fixed(value as usize))
                } else {
                    Err(de::Error::custom("indent_size must be positive"))
                }
            }

            fn visit_str<E>(self, value: &str) -> Result<IndentSize, E>
            where
                E: de::Error,
            {
                if value.eq_ignore_ascii_case("auto") {
                    Ok(IndentSize::Auto)
                } else {
                    Err(de::Error::custom(
                        "expected \"auto\" or a positive integer for indent_size",
                    ))
                }
            }
        }

        deserializer.deserialize_any(IndentSizeVisitor)
    }
}

impl JsonSchema for IndentSize {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "IndentSize".into()
    }

    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        serde_json::from_value(serde_json::json!({
            "description": "Indentation size: a positive integer or \"auto\" for auto-detection",
            "default": "auto",
            "oneOf": [
                { "type": "integer", "minimum": 1 },
                { "type": "string", "enum": ["auto"] }
            ]
        }))
        .unwrap()
    }
}

/// Default configuration template for nginx-lint
pub const DEFAULT_CONFIG_TEMPLATE: &str = r#"# nginx-lint configuration file
# This file was generated by `nginx-lint config init`
# See https://github.com/walf443/nginx-lint for more documentation

# Color output settings
[color]
# Color mode: "auto", "always", or "never"
ui = "auto"
# Severity colors (available: black, red, green, yellow, blue, magenta, cyan, white,
#                  bright_black, bright_red, bright_green, bright_yellow, bright_blue,
#                  bright_magenta, bright_cyan, bright_white)
error = "red"
warning = "yellow"

# =============================================================================
# Include Resolution Settings
# =============================================================================
[include]

# Base directory for resolving relative include paths (similar to nginx -p prefix).
# When set, all relative include paths are resolved from this directory
# instead of the directory containing the config file with the include directive.
# prefix = "/etc/nginx"

# Path mappings applied to include patterns before resolving them.
# Mappings are applied in declaration order, each receiving the output of the
# previous one (chained).  Useful when the config references a directory that
# differs from where the actual files live (e.g. sites-enabled → sites-available).
#
# Example (for Debian nginx package):

# [[include.path_map]]
# from = "/etc/nginx/"
# to   = ""
#
# [[include.path_map]]
# from = "sites-enabled"
# to   = "sites-available"
#
# [[include.path_map]]
# from = "modules-enabled"
# to   = "modules-available"

# =============================================================================
# Style Rules
# =============================================================================

[rules.indent]
enabled = true
# Indentation size: number or "auto" for auto-detection (default: "auto")
# indent_size = 4
indent_size = "auto"

[rules.trailing-whitespace]
enabled = true

[rules.space-before-semicolon]
enabled = true

[rules.block-lines]
enabled = true
# Maximum number of lines allowed in a block (default: 100)
# max_block_lines = 100

# =============================================================================
# Syntax Rules
# =============================================================================

[rules.duplicate-directive]
enabled = true

[rules.unmatched-braces]
enabled = true

[rules.unclosed-quote]
enabled = true

[rules.missing-semicolon]
enabled = true

[rules.invalid-directive-context]
enabled = true
# Additional valid parent contexts for directives (for extension modules like nginx-rtmp-module)
# Example for nginx-rtmp-module:
# additional_contexts = { server = ["rtmp"], upstream = ["rtmp"] }

[rules.include-path-exists]
enabled = true

# =============================================================================
# Security Rules
# =============================================================================

[rules.deprecated-ssl-protocol]
enabled = true
# Allowed protocols for auto-fix (default: ["TLSv1.2", "TLSv1.3"])
allowed_protocols = ["TLSv1.2", "TLSv1.3"]

[rules.server-tokens-enabled]
enabled = true

[rules.autoindex-enabled]
enabled = true

[rules.weak-ssl-ciphers]
enabled = true
# Weak cipher patterns to detect
weak_ciphers = [
    "NULL",
    "EXPORT",
    "DES",
    "RC4",
    "MD5",
    "aNULL",
    "eNULL",
    "ADH",
    "AECDH",
    "PSK",
    "SRP",
    "CAMELLIA",
]
# Required exclusion patterns
required_exclusions = ["!aNULL", "!eNULL", "!EXPORT", "!DES", "!RC4", "!MD5"]

# =============================================================================
# Best Practices
# =============================================================================

[rules.gzip-not-enabled]
# Disabled by default: gzip is not always appropriate (CDN, CPU constraints, BREACH attack)
enabled = false

[rules.missing-error-log]
# Disabled by default: error_log is typically set at top level in main config
enabled = false

[rules.proxy-pass-domain]
enabled = true

[rules.upstream-server-no-resolve]
enabled = true

[rules.directive-inheritance]
enabled = true
# Exclude specific directives from checking
# excluded_directives = ["grpc_set_header", "uwsgi_param"]
# Add custom directives to check (name is required, case_insensitive and multi_key default to false)
# additional_directives = [
#   { name = "proxy_set_cookie", case_insensitive = true },
# ]

[rules.root-in-location]
enabled = true

[rules.alias-location-slash-mismatch]
enabled = true

[rules.proxy-pass-with-uri]
enabled = true

[rules.proxy-keepalive]
enabled = true

[rules.try-files-with-proxy]
enabled = true

[rules.if-is-evil-in-location]
enabled = true

# =============================================================================
# Parser Settings
# =============================================================================

[parser]
# Additional block directives for extension modules
# These are added to the built-in list (http, server, location, etc.)
# Example for nginx-rtmp-module:
# block_directives = ["rtmp", "application"]
"#;

/// Configuration for nginx-lint loaded from `.nginx-lint.toml`.
///
/// Use [`from_file`](Self::from_file) to load from a specific path, or
/// [`find_and_load`](Self::find_and_load) to search the directory tree upward.
/// A default `LintConfig` enables all rules except those listed in
/// [`DISABLED_BY_DEFAULT`](Self::DISABLED_BY_DEFAULT).
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct LintConfig {
    /// Per-rule configuration keyed by rule name (e.g. `"indent"`, `"server-tokens-enabled"`).
    #[serde(default)]
    pub rules: HashMap<String, RuleConfig>,
    /// Color output settings.
    #[serde(default)]
    pub color: ColorConfig,
    /// Parser-level settings (e.g. additional block directives for extension modules).
    #[serde(default)]
    pub parser: ParserConfig,
    /// Include resolution settings (e.g. path mappings for include directives).
    #[serde(default)]
    pub include: IncludeConfig,
}

/// Parser configuration
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
pub struct ParserConfig {
    /// Additional block directives (extension modules, etc.)
    /// These are added to the built-in list of block directives
    #[serde(default)]
    pub block_directives: Vec<String>,
}

/// A single path mapping rule for include directive resolution.
///
/// When an `include` directive's path contains path segment(s) that exactly
/// match `from`, they are replaced with `to` before the path is resolved on
/// disk.  Matching is performed at the path-component level (split by `/`),
/// so `from = "sites-enabled"` matches `.../sites-enabled/...` but does NOT
/// match `.../asites-enabled/...`.  Multi-segment values like
/// `from = "nginx/sites-enabled"` match consecutive components.
///
/// This is useful when the production config references a directory
/// (e.g. `sites-enabled`) that is only populated at runtime via symlinks,
/// and you want nginx-lint to evaluate the actual source files
/// (e.g. `sites-available`) instead.
///
/// Mappings are applied in declaration order and chained, so the output of one
/// mapping is fed into the next.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct PathMapping {
    /// Path segment(s) to match in the include path (compared component-wise)
    pub from: String,
    /// Replacement path segment(s)
    pub to: String,
}

/// Include resolution configuration.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
pub struct IncludeConfig {
    /// Path mappings applied to include patterns before resolving them.
    /// Applied in declaration order; each mapping receives the output of the previous one.
    #[serde(default)]
    pub path_map: Vec<PathMapping>,
    /// Base directory for resolving relative include paths (similar to nginx `-p` prefix).
    /// When set, all relative include paths are resolved from this directory
    /// instead of the directory containing the config file with the include directive.
    pub prefix: Option<String>,
}

/// Color output configuration
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct ColorConfig {
    /// Color mode: "auto" (default), "always", or "never"
    #[serde(default)]
    pub ui: ColorMode,
    /// Color for error messages (default: "red")
    #[serde(default = "default_error_color")]
    pub error: Color,
    /// Color for warning messages (default: "yellow")
    #[serde(default = "default_warning_color")]
    pub warning: Color,
}

impl Default for ColorConfig {
    fn default() -> Self {
        Self {
            ui: ColorMode::Auto,
            error: Color::Red,
            warning: Color::Yellow,
        }
    }
}

fn default_error_color() -> Color {
    Color::Red
}

fn default_warning_color() -> Color {
    Color::Yellow
}

/// Available colors for output
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Color {
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    #[default]
    White,
    BrightBlack,
    BrightRed,
    BrightGreen,
    BrightYellow,
    BrightBlue,
    BrightMagenta,
    BrightCyan,
    BrightWhite,
}

impl JsonSchema for Color {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "Color".into()
    }

    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        serde_json::from_value(serde_json::json!({
            "type": "string",
            "enum": [
                "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
                "bright_black", "bright_red", "bright_green", "bright_yellow",
                "bright_blue", "bright_magenta", "bright_cyan", "bright_white"
            ]
        }))
        .unwrap()
    }
}

impl<'de> Deserialize<'de> for Color {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::Error;

        let s = String::deserialize(deserializer)?;
        match s.to_lowercase().as_str() {
            "black" => Ok(Color::Black),
            "red" => Ok(Color::Red),
            "green" => Ok(Color::Green),
            "yellow" => Ok(Color::Yellow),
            "blue" => Ok(Color::Blue),
            "magenta" => Ok(Color::Magenta),
            "cyan" => Ok(Color::Cyan),
            "white" => Ok(Color::White),
            "bright_black" | "brightblack" => Ok(Color::BrightBlack),
            "bright_red" | "brightred" => Ok(Color::BrightRed),
            "bright_green" | "brightgreen" => Ok(Color::BrightGreen),
            "bright_yellow" | "brightyellow" => Ok(Color::BrightYellow),
            "bright_blue" | "brightblue" => Ok(Color::BrightBlue),
            "bright_magenta" | "brightmagenta" => Ok(Color::BrightMagenta),
            "bright_cyan" | "brightcyan" => Ok(Color::BrightCyan),
            "bright_white" | "brightwhite" => Ok(Color::BrightWhite),
            _ => Err(D::Error::custom(format!(
                "invalid color '{}', expected one of: black, red, green, yellow, blue, magenta, cyan, white, \
                 bright_black, bright_red, bright_green, bright_yellow, bright_blue, bright_magenta, bright_cyan, bright_white",
                s
            ))),
        }
    }
}

/// Color mode for output
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorMode {
    /// Automatically detect (default) - respects NO_COLOR env and terminal detection
    #[default]
    Auto,
    /// Always use colors
    Always,
    /// Never use colors
    Never,
}

impl JsonSchema for ColorMode {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "ColorMode".into()
    }

    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        serde_json::from_value(serde_json::json!({
            "type": "string",
            "description": "Color mode: \"auto\" respects NO_COLOR env and terminal detection, \"always\" forces colors, \"never\" disables colors",
            "default": "auto",
            "enum": ["auto", "always", "never"]
        }))
        .unwrap()
    }
}

impl<'de> Deserialize<'de> for ColorMode {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::Error;

        let s = String::deserialize(deserializer)?;
        match s.as_str() {
            "auto" => Ok(ColorMode::Auto),
            "always" => Ok(ColorMode::Always),
            "never" => Ok(ColorMode::Never),
            _ => Err(D::Error::custom(format!(
                "invalid color mode '{}', expected 'auto', 'always', or 'never'",
                s
            ))),
        }
    }
}

/// An additional directive to check for inheritance issues.
///
/// Used in `[rules.directive-inheritance]` configuration.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct AdditionalDirective {
    /// The directive name (e.g., "proxy_set_cookie")
    pub name: String,
    /// Whether the first argument key comparison is case-insensitive (default: false)
    #[serde(default)]
    pub case_insensitive: bool,
    /// If true, all numeric arguments are separate keys like error_page (default: false)
    #[serde(default)]
    pub multi_key: bool,
}

/// Configuration for a specific lint rule.
///
/// Every `[rules.<name>]` section in `.nginx-lint.toml` is deserialized into
/// a `RuleConfig`. The only universal field is [`enabled`](Self::enabled);
/// the remaining fields are rule-specific options.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
pub struct RuleConfig {
    /// Whether this rule is active (`true` by default for most rules).
    #[serde(default = "default_true")]
    pub enabled: bool,
    /// For indent rule: number or "auto" for auto-detection
    pub indent_size: Option<IndentSize>,
    /// For deprecated-ssl-protocol rule: allowed protocols (default: ["TLSv1.2", "TLSv1.3"])
    pub allowed_protocols: Option<Vec<String>>,
    /// For weak-ssl-ciphers rule: weak cipher patterns to detect
    pub weak_ciphers: Option<Vec<String>>,
    /// For weak-ssl-ciphers rule: required exclusion patterns
    pub required_exclusions: Option<Vec<String>>,
    /// For invalid-directive-context rule: additional valid parent contexts
    /// Format: { "server" = ["rtmp"], "upstream" = ["rtmp"] }
    pub additional_contexts: Option<HashMap<String, Vec<String>>>,
    /// For block-lines rule: maximum number of lines allowed in a block
    pub max_block_lines: Option<usize>,
    /// For directive-inheritance rule: directives to exclude from checking
    pub excluded_directives: Option<Vec<String>>,
    /// For directive-inheritance rule: additional directives to check
    pub additional_directives: Option<Vec<AdditionalDirective>>,
}

fn default_true() -> bool {
    true
}

impl LintConfig {
    /// Load configuration from a file
    pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
        let content = fs::read_to_string(path).map_err(|e| ConfigError::IoError {
            path: path.to_path_buf(),
            source: e,
        })?;

        toml::from_str(&content).map_err(|e| ConfigError::ParseError {
            path: path.to_path_buf(),
            source: e,
        })
    }

    /// Parse configuration from a TOML string
    pub fn parse(content: &str) -> Result<Self, String> {
        toml::from_str(content).map_err(|e| e.to_string())
    }

    /// Find and load .nginx-lint.toml from the given directory or its parents.
    ///
    /// Returns the loaded config along with the path of the config file found.
    pub fn find_and_load(dir: &Path) -> Option<(Self, std::path::PathBuf)> {
        let mut current = dir.to_path_buf();

        loop {
            let config_path = current.join(".nginx-lint.toml");
            if config_path.exists() {
                return Self::from_file(&config_path)
                    .ok()
                    .map(|cfg| (cfg, config_path));
            }

            if !current.pop() {
                break;
            }
        }

        None
    }

    /// Rules that are disabled by default
    pub const DISABLED_BY_DEFAULT: &'static [&'static str] = &[
        "gzip-not-enabled", // gzip is not always appropriate (CDN, CPU constraints, security)
        "missing-error-log", // error_log is typically set at top level in main config
    ];

    /// Check if a rule is enabled
    pub fn is_rule_enabled(&self, name: &str) -> bool {
        self.rules
            .get(name)
            .map(|r| r.enabled)
            .unwrap_or_else(|| !Self::DISABLED_BY_DEFAULT.contains(&name))
    }

    /// Get the configuration for a specific rule
    pub fn get_rule_config(&self, name: &str) -> Option<&RuleConfig> {
        self.rules.get(name)
    }

    /// Get the color mode setting
    pub fn color_mode(&self) -> ColorMode {
        self.color.ui
    }

    /// Get additional block directives from config
    pub fn additional_block_directives(&self) -> &[String] {
        &self.parser.block_directives
    }

    /// Get include path mappings (applied in order to include patterns before resolving)
    pub fn include_path_mappings(&self) -> &[PathMapping] {
        &self.include.path_map
    }

    /// Generate the JSON Schema for the configuration file.
    ///
    /// The schema is derived from the Rust type definitions, so it automatically
    /// stays in sync with the actual configuration structure.
    pub fn json_schema() -> serde_json::Value {
        let generator = schemars::SchemaGenerator::default();
        let schema = generator.into_root_schema_for::<LintConfig>();
        serde_json::to_value(schema).unwrap()
    }

    /// Get include prefix (base directory for resolving relative include paths)
    pub fn include_prefix(&self) -> Option<&str> {
        self.include.prefix.as_deref()
    }

    /// Get additional contexts for invalid-directive-context rule
    pub fn additional_contexts(&self) -> Option<&HashMap<String, Vec<String>>> {
        self.rules
            .get("invalid-directive-context")
            .and_then(|r| r.additional_contexts.as_ref())
    }

    /// Get excluded directives for directive-inheritance rule
    pub fn directive_inheritance_excluded(&self) -> Option<&[String]> {
        self.rules
            .get("directive-inheritance")
            .and_then(|r| r.excluded_directives.as_deref())
    }

    /// Get additional directives for directive-inheritance rule
    pub fn directive_inheritance_additional(&self) -> Option<&[AdditionalDirective]> {
        self.rules
            .get("directive-inheritance")
            .and_then(|r| r.additional_directives.as_deref())
    }

    /// Validate a configuration file and return any errors
    pub fn validate_file(path: &Path) -> Result<Vec<ValidationError>, ConfigError> {
        let content = fs::read_to_string(path).map_err(|e| ConfigError::IoError {
            path: path.to_path_buf(),
            source: e,
        })?;

        Self::validate_content(&content, path)
    }

    /// Validate configuration content and return any errors
    fn validate_content(content: &str, path: &Path) -> Result<Vec<ValidationError>, ConfigError> {
        let value: toml::Value = toml::from_str(content).map_err(|e| ConfigError::ParseError {
            path: path.to_path_buf(),
            source: e,
        })?;

        let mut errors = Vec::new();

        if let toml::Value::Table(root) = value {
            // Known top-level keys
            let known_top_level: HashSet<&str> = ["rules", "color", "parser", "include"]
                .into_iter()
                .collect();

            for key in root.keys() {
                if !known_top_level.contains(key.as_str()) {
                    let line = find_key_line(content, None, key);
                    errors.push(ValidationError::UnknownField {
                        path: key.clone(),
                        line,
                        suggestion: suggest_field(key, &known_top_level),
                    });
                }
            }

            // Validate [color] section
            if let Some(toml::Value::Table(color)) = root.get("color") {
                let known_color_keys: HashSet<&str> =
                    ["ui", "error", "warning"].into_iter().collect();

                for key in color.keys() {
                    if !known_color_keys.contains(key.as_str()) {
                        let line = find_key_line(content, Some("color"), key);
                        errors.push(ValidationError::UnknownField {
                            path: format!("color.{}", key),
                            line,
                            suggestion: suggest_field(key, &known_color_keys),
                        });
                    }
                }
            }

            // Validate [parser] section
            if let Some(toml::Value::Table(parser)) = root.get("parser") {
                let known_parser_keys: HashSet<&str> = ["block_directives"].into_iter().collect();

                for key in parser.keys() {
                    if !known_parser_keys.contains(key.as_str()) {
                        let line = find_key_line(content, Some("parser"), key);
                        errors.push(ValidationError::UnknownField {
                            path: format!("parser.{}", key),
                            line,
                            suggestion: suggest_field(key, &known_parser_keys),
                        });
                    }
                }
            }

            // Validate [include] section
            if let Some(toml::Value::Table(include)) = root.get("include") {
                let known_include_keys: HashSet<&str> =
                    ["path_map", "prefix"].into_iter().collect();

                for key in include.keys() {
                    if !known_include_keys.contains(key.as_str()) {
                        let line = find_key_line(content, Some("include"), key);
                        errors.push(ValidationError::UnknownField {
                            path: format!("include.{}", key),
                            line,
                            suggestion: suggest_field(key, &known_include_keys),
                        });
                    }
                }
            }

            // Validate [rules.*] sections
            if let Some(toml::Value::Table(rules)) = root.get("rules") {
                let known_rules: HashSet<&str> = [
                    "duplicate-directive",
                    "unmatched-braces",
                    "unclosed-quote",
                    "missing-semicolon",
                    "invalid-directive-context",
                    "deprecated-ssl-protocol",
                    "server-tokens-enabled",
                    "autoindex-enabled",
                    "weak-ssl-ciphers",
                    "indent",
                    "trailing-whitespace",
                    "space-before-semicolon",
                    "block-lines",
                    "gzip-not-enabled",
                    "missing-error-log",
                    "proxy-pass-domain",
                    "upstream-server-no-resolve",
                    "root-in-location",
                    "alias-location-slash-mismatch",
                    "proxy-pass-with-uri",
                    "proxy-keepalive",
                    "try-files-with-proxy",
                    "if-is-evil-in-location",
                    "directive-inheritance",
                    "include-path-exists",
                ]
                .into_iter()
                .collect();

                for (rule_name, rule_value) in rules {
                    if !known_rules.contains(rule_name.as_str()) {
                        let line = find_key_line(content, Some("rules"), rule_name);
                        errors.push(ValidationError::UnknownRule {
                            name: rule_name.clone(),
                            line,
                            suggestion: suggest_field(rule_name, &known_rules),
                        });
                        continue;
                    }

                    // Validate rule options
                    if let toml::Value::Table(rule_config) = rule_value {
                        let known_rule_options = get_known_rule_options(rule_name);
                        let section = format!("rules.{}", rule_name);

                        for key in rule_config.keys() {
                            if !known_rule_options.contains(key.as_str()) {
                                let line = find_key_line(content, Some(&section), key);
                                errors.push(ValidationError::UnknownRuleOption {
                                    rule: rule_name.clone(),
                                    option: key.clone(),
                                    line,
                                    suggestion: suggest_field(key, &known_rule_options),
                                });
                            }
                        }
                    }
                }
            }
        }

        Ok(errors)
    }
}

/// Find the line number where a key is defined in the TOML content
fn find_key_line(content: &str, section: Option<&str>, key: &str) -> Option<usize> {
    let lines: Vec<&str> = content.lines().collect();

    // For top-level sections (section is None), look for [key]
    if section.is_none() {
        let section_header = format!("[{}]", key);
        for (i, line) in lines.iter().enumerate() {
            if line.trim() == section_header {
                return Some(i + 1);
            }
        }
        return None;
    }

    let target_section = section.unwrap();
    let mut in_section = false;

    for (i, line) in lines.iter().enumerate() {
        let trimmed = line.trim();

        // Check for section header [section] or [section.subsection]
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            let section_name = &trimmed[1..trimmed.len() - 1];

            // Check if this is a [rules.rule-name] style section
            let full_section = format!("{}.{}", target_section, key);
            if section_name == full_section {
                return Some(i + 1);
            }

            in_section = section_name == target_section
                || section_name.starts_with(&format!("{}.", target_section));
            continue;
        }

        // Check for key = value within the section
        if in_section && let Some((k, _)) = trimmed.split_once('=') {
            let k = k.trim();
            if k == key {
                return Some(i + 1);
            }
        }
    }

    None
}

/// Get known options for a specific rule
fn get_known_rule_options(rule_name: &str) -> HashSet<&'static str> {
    let mut options: HashSet<&str> = ["enabled"].into_iter().collect();

    match rule_name {
        "indent" => {
            options.insert("indent_size");
        }
        "deprecated-ssl-protocol" => {
            options.insert("allowed_protocols");
        }
        "weak-ssl-ciphers" => {
            options.insert("weak_ciphers");
            options.insert("required_exclusions");
        }
        "block-lines" => {
            options.insert("max_block_lines");
        }
        "directive-inheritance" => {
            options.insert("excluded_directives");
            options.insert("additional_directives");
        }
        _ => {}
    }

    options
}

/// Suggest a similar field name if one exists
fn suggest_field(input: &str, known: &HashSet<&str>) -> Option<String> {
    let input_lower = input.to_lowercase();

    // Find the closest match using simple edit distance
    known
        .iter()
        .filter(|&&k| {
            let k_lower = k.to_lowercase();
            // Simple heuristic: check if strings are similar
            k_lower.contains(&input_lower)
                || input_lower.contains(&k_lower)
                || levenshtein_distance(&input_lower, &k_lower) <= 2
        })
        .min_by_key(|&&k| levenshtein_distance(&input.to_lowercase(), &k.to_lowercase()))
        .map(|&s| s.to_string())
}

/// Simple Levenshtein distance implementation
fn levenshtein_distance(a: &str, b: &str) -> usize {
    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();
    let a_len = a_chars.len();
    let b_len = b_chars.len();

    if a_len == 0 {
        return b_len;
    }
    if b_len == 0 {
        return a_len;
    }

    let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];

    for (i, row) in matrix.iter_mut().enumerate().take(a_len + 1) {
        row[0] = i;
    }
    for (j, cell) in matrix[0].iter_mut().enumerate().take(b_len + 1) {
        *cell = j;
    }

    for i in 1..=a_len {
        for j in 1..=b_len {
            let cost = usize::from(a_chars[i - 1] != b_chars[j - 1]);
            matrix[i][j] = (matrix[i - 1][j] + 1)
                .min(matrix[i][j - 1] + 1)
                .min(matrix[i - 1][j - 1] + cost);
        }
    }

    matrix[a_len][b_len]
}

/// Validation error for configuration files.
///
/// Returned by [`LintConfig::validate_file`] when the TOML file contains
/// unrecognised keys or rule names. Each variant includes an optional
/// `suggestion` produced by fuzzy matching.
#[derive(Debug, Clone)]
pub enum ValidationError {
    /// An unrecognised top-level or section-level field (e.g. `[colour]` instead of `[color]`).
    UnknownField {
        /// Dotted path to the field (e.g. `"color.ui_mode"`).
        path: String,
        /// 1-indexed line number where the field appears, if found.
        line: Option<usize>,
        /// A similar known field name, if one is close enough.
        suggestion: Option<String>,
    },
    /// A `[rules.<name>]` section where `<name>` is not a known rule.
    UnknownRule {
        /// The unrecognised rule name.
        name: String,
        /// 1-indexed line number where the rule section appears, if found.
        line: Option<usize>,
        /// A similar known rule name, if one is close enough.
        suggestion: Option<String>,
    },
    /// An unrecognised option inside a rule section (e.g. `indent_sizee` in `[rules.indent]`).
    UnknownRuleOption {
        /// The rule name containing the unknown option.
        rule: String,
        /// The unrecognised option key.
        option: String,
        /// 1-indexed line number where the option appears, if found.
        line: Option<usize>,
        /// A similar known option name, if one is close enough.
        suggestion: Option<String>,
    },
}

impl std::fmt::Display for ValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ValidationError::UnknownField {
                path,
                line,
                suggestion,
            } => {
                if let Some(l) = line {
                    write!(f, "line {}: ", l)?;
                }
                write!(f, "unknown field '{}'", path)?;
                if let Some(s) = suggestion {
                    write!(f, ", did you mean '{}'?", s)?;
                }
                Ok(())
            }
            ValidationError::UnknownRule {
                name,
                line,
                suggestion,
            } => {
                if let Some(l) = line {
                    write!(f, "line {}: ", l)?;
                }
                write!(f, "unknown rule '{}'", name)?;
                if let Some(s) = suggestion {
                    write!(f, ", did you mean '{}'?", s)?;
                }
                Ok(())
            }
            ValidationError::UnknownRuleOption {
                rule,
                option,
                line,
                suggestion,
            } => {
                if let Some(l) = line {
                    write!(f, "line {}: ", l)?;
                }
                write!(f, "unknown option '{}' for rule '{}'", option, rule)?;
                if let Some(s) = suggestion {
                    write!(f, ", did you mean '{}'?", s)?;
                }
                Ok(())
            }
        }
    }
}

/// Error returned when loading or parsing a configuration file fails.
#[derive(Debug)]
pub enum ConfigError {
    /// The file could not be read (missing, permission denied, etc.).
    IoError {
        /// Path that was attempted.
        path: std::path::PathBuf,
        /// Underlying I/O error.
        source: std::io::Error,
    },
    /// The file was read but contains invalid TOML.
    ParseError {
        /// Path that was parsed.
        path: std::path::PathBuf,
        /// Underlying TOML parse error.
        source: toml::de::Error,
    },
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConfigError::IoError { path, source } => {
                write!(
                    f,
                    "Failed to read config file '{}': {}",
                    path.display(),
                    source
                )
            }
            ConfigError::ParseError { path, source } => {
                write!(
                    f,
                    "Failed to parse config file '{}': {}",
                    path.display(),
                    source
                )
            }
        }
    }
}

impl std::error::Error for ConfigError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ConfigError::IoError { source, .. } => Some(source),
            ConfigError::ParseError { source, .. } => Some(source),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_default_config() {
        let config = LintConfig::default();
        assert!(config.is_rule_enabled("any-rule"));
    }

    #[test]
    fn test_disabled_by_default_rules() {
        let config = LintConfig::default();
        // These rules should be disabled by default
        assert!(!config.is_rule_enabled("gzip-not-enabled"));
        assert!(!config.is_rule_enabled("missing-error-log"));
        // Other rules should still be enabled by default
        assert!(config.is_rule_enabled("server-tokens-enabled"));
    }

    #[test]
    fn test_parse_config() {
        let toml_content = r#"
[rules.indent]
enabled = true
indent_size = 2

[rules.server-tokens-enabled]
enabled = false
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();

        assert!(config.is_rule_enabled("indent"));
        assert!(!config.is_rule_enabled("server-tokens-enabled"));
        assert!(config.is_rule_enabled("unknown-rule"));

        let indent_config = config.get_rule_config("indent").unwrap();
        assert_eq!(indent_config.indent_size, Some(IndentSize::Fixed(2)));
    }

    #[test]
    fn test_empty_config() {
        let toml_content = "";
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        assert!(config.is_rule_enabled("any-rule"));
    }

    #[test]
    fn test_indent_size_auto() {
        let toml_content = r#"
[rules.indent]
enabled = true
indent_size = "auto"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        let indent_config = config.get_rule_config("indent").unwrap();
        assert_eq!(indent_config.indent_size, Some(IndentSize::Auto));
    }

    #[test]
    fn test_color_config_default() {
        let config = LintConfig::default();
        assert_eq!(config.color_mode(), ColorMode::Auto);
    }

    #[test]
    fn test_color_config_auto() {
        let toml_content = r#"
[color]
ui = "auto"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        assert_eq!(config.color_mode(), ColorMode::Auto);
    }

    #[test]
    fn test_color_config_never() {
        let toml_content = r#"
[color]
ui = "never"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        assert_eq!(config.color_mode(), ColorMode::Never);
    }

    #[test]
    fn test_color_config_always() {
        let toml_content = r#"
[color]
ui = "always"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        assert_eq!(config.color_mode(), ColorMode::Always);
    }

    #[test]
    fn test_color_config_default_colors() {
        let config = LintConfig::default();
        assert_eq!(config.color.error, Color::Red);
        assert_eq!(config.color.warning, Color::Yellow);
    }

    #[test]
    fn test_color_config_custom_colors() {
        let toml_content = r#"
[color]
error = "magenta"
warning = "cyan"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        assert_eq!(config.color.error, Color::Magenta);
        assert_eq!(config.color.warning, Color::Cyan);
    }

    #[test]
    fn test_color_config_bright_colors() {
        let toml_content = r#"
[color]
error = "bright_red"
warning = "bright_yellow"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        assert_eq!(config.color.error, Color::BrightRed);
        assert_eq!(config.color.warning, Color::BrightYellow);
    }

    #[test]
    fn test_block_lines_max_block_lines_parsing() {
        let toml_content = r#"
[rules.block-lines]
enabled = true
max_block_lines = 50
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        assert!(config.is_rule_enabled("block-lines"));
        let rule_config = config.get_rule_config("block-lines").unwrap();
        assert_eq!(rule_config.max_block_lines, Some(50));
    }

    #[test]
    fn test_block_lines_default_no_max() {
        let toml_content = r#"
[rules.block-lines]
enabled = true
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = LintConfig::from_file(file.path()).unwrap();
        let rule_config = config.get_rule_config("block-lines").unwrap();
        assert_eq!(rule_config.max_block_lines, None);
    }

    #[test]
    fn test_block_lines_validation_rejects_unknown_option() {
        let toml_content = r#"
[rules.block-lines]
enabled = true
unknown_option = 42
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let errors = LintConfig::validate_file(file.path()).unwrap();
        assert_eq!(errors.len(), 1);
        match &errors[0] {
            ValidationError::UnknownRuleOption { rule, option, .. } => {
                assert_eq!(rule, "block-lines");
                assert_eq!(option, "unknown_option");
            }
            other => panic!("expected UnknownRuleOption, got: {:?}", other),
        }
    }

    #[test]
    fn test_include_path_map_empty_by_default() {
        let config = LintConfig::default();
        assert!(config.include_path_mappings().is_empty());
    }

    #[test]
    fn test_include_path_map_single_entry() {
        let toml_content = r#"
[[include.path_map]]
from = "sites-enabled"
to   = "sites-available"
"#;
        let config = LintConfig::parse(toml_content).unwrap();
        let mappings = config.include_path_mappings();
        assert_eq!(mappings.len(), 1);
        assert_eq!(mappings[0].from, "sites-enabled");
        assert_eq!(mappings[0].to, "sites-available");
    }

    #[test]
    fn test_include_path_map_multiple_entries_preserve_order() {
        let toml_content = r#"
[[include.path_map]]
from = "sites-enabled"
to   = "sites-available"

[[include.path_map]]
from = "/etc/nginx"
to   = "/usr/local/nginx"
"#;
        let config = LintConfig::parse(toml_content).unwrap();
        let mappings = config.include_path_mappings();
        assert_eq!(mappings.len(), 2);
        assert_eq!(mappings[0].from, "sites-enabled");
        assert_eq!(mappings[0].to, "sites-available");
        assert_eq!(mappings[1].from, "/etc/nginx");
        assert_eq!(mappings[1].to, "/usr/local/nginx");
    }

    #[test]
    fn test_include_validation_rejects_unknown_field() {
        let toml_content = r#"
[include]
unknown_key = "value"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let errors = LintConfig::validate_file(file.path()).unwrap();
        assert_eq!(errors.len(), 1);
        match &errors[0] {
            ValidationError::UnknownField { path, .. } => {
                assert_eq!(path, "include.unknown_key");
            }
            other => panic!("expected UnknownField, got: {:?}", other),
        }
    }

    #[test]
    fn test_include_prefix_none_by_default() {
        let config = LintConfig::default();
        assert!(config.include_prefix().is_none());
    }

    #[test]
    fn test_include_prefix_parsed() {
        let toml_content = r#"
[include]
prefix = "/etc/nginx"
"#;
        let config = LintConfig::parse(toml_content).unwrap();
        assert_eq!(config.include_prefix(), Some("/etc/nginx"));
    }

    #[test]
    fn test_include_prefix_with_path_map() {
        let toml_content = r#"
[include]
prefix = "."

[[include.path_map]]
from = "sites-enabled"
to   = "sites-available"
"#;
        let config = LintConfig::parse(toml_content).unwrap();
        assert_eq!(config.include_prefix(), Some("."));
        assert_eq!(config.include_path_mappings().len(), 1);
    }

    #[test]
    fn test_include_prefix_validation_accepted() {
        let toml_content = r#"
[include]
prefix = "/etc/nginx"
"#;
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let errors = LintConfig::validate_file(file.path()).unwrap();
        assert!(
            errors.is_empty(),
            "prefix should be a valid include field, got errors: {:?}",
            errors
        );
    }

    #[test]
    fn test_json_schema_is_valid() {
        let schema = LintConfig::json_schema();

        // Should have $schema key
        assert_eq!(
            schema.get("$schema").and_then(|v| v.as_str()),
            Some("https://json-schema.org/draft/2020-12/schema")
        );

        // Should have properties for all top-level config sections
        let props = schema.get("properties").unwrap().as_object().unwrap();
        assert!(props.contains_key("rules"), "missing 'rules' property");
        assert!(props.contains_key("color"), "missing 'color' property");
        assert!(props.contains_key("parser"), "missing 'parser' property");
        assert!(props.contains_key("include"), "missing 'include' property");
    }

    #[test]
    fn test_json_schema_rule_config_has_all_fields() {
        let schema = LintConfig::json_schema();

        // RuleConfig definition should contain all fields from the struct
        // schemars v1 uses "$defs" instead of "definitions"
        let rule_config_def = schema
            .pointer("/$defs/RuleConfig")
            .expect("RuleConfig definition missing from schema");

        let props = rule_config_def
            .get("properties")
            .unwrap()
            .as_object()
            .unwrap();

        let expected_fields = [
            "enabled",
            "indent_size",
            "allowed_protocols",
            "weak_ciphers",
            "required_exclusions",
            "additional_contexts",
            "max_block_lines",
            "excluded_directives",
            "additional_directives",
        ];

        for field in &expected_fields {
            assert!(
                props.contains_key(*field),
                "RuleConfig schema missing field '{field}'"
            );
        }
    }
}
