//! Config-aware security extractor.
//!
//! Parses YAML/JSON/TOML config files into structured form and applies
//! security rules based on path context. This catches issues that
//! line-by-line regex scanning misses, such as deeply nested structures.
//!
//! # Detected Patterns
//!
//! - TLS verification disabled (`*.tls.verify: false`, `*.ssl_verify: false`)
//! - Security features disabled (`*.security.enabled: false`)
//! - Debug mode enabled (`debug: true` in production files)
//! - CSRF protection disabled (`*.csrf.enabled: false`)
//! - Weak password policies (`*.password.min_length < 8`)
//!
//! # Example
//!
//! ```yaml
//! # This deeply nested config is now detected:
//! server:
//!   internal:
//!     api:
//!       tls:
//!         verify: false  # BLOCK: TLS verification disabled
//! ```

use regex::Regex;
use stemedb_core::types::ObjectValue;

use super::config_parser::{parse_config, walk_config, ConfigValue};
use super::traits::is_test_file;
use super::Extractor;
use crate::types::{ExtractedClaim, Language};

/// A security rule that matches config paths and values.
struct SecurityRule {
    /// Name of the rule (for debugging)
    name: &'static str,
    /// Regex pattern to match against the config path
    path_pattern: Regex,
    /// Function to check if the value violates the rule
    value_check: fn(&ConfigValue) -> bool,
    /// Description for the claim
    description: &'static str,
    /// Concept path segments to append
    concept_segments: &'static [&'static str],
    /// Predicate for the claim
    predicate: &'static str,
    /// Value to emit for the claim
    claim_value: ObjectValue,
    /// Base confidence (reduced for test files)
    confidence: f32,
}

/// Extractor that parses config files and applies security rules.
pub struct ConfigSecurityExtractor {
    rules: Vec<SecurityRule>,
}

impl Default for ConfigSecurityExtractor {
    fn default() -> Self {
        Self::new()
    }
}

impl ConfigSecurityExtractor {
    /// Create a new config security extractor with built-in rules.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        let rules = vec![
            // TLS verification disabled
            SecurityRule {
                name: "tls_verify_disabled",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(tls|ssl)[._]?(verify|verification|cert_verify|verify_cert|check_cert)$"
                ).expect("valid regex"),
                value_check: |v| v.is_false(),
                description: "TLS certificate verification is disabled in config",
                concept_segments: &["tls", "cert_verification"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.95,
            },
            // Alternative: insecure_skip_verify = true (skip verification IS insecure)
            SecurityRule {
                name: "insecure_skip_verify",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(insecure_skip_verify|skip_verify|skip_tls_verify|skip_ssl_verify)$"
                ).expect("valid regex"),
                value_check: |v| v.is_true(),
                description: "TLS verification is explicitly skipped in config",
                concept_segments: &["tls", "cert_verification"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.95,
            },
            // SSL/TLS verify with string values
            SecurityRule {
                name: "tls_verify_string_disabled",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(tls|ssl)[._]?(verify|verification)$"
                ).expect("valid regex"),
                value_check: |v| {
                    matches!(v.as_str(), Some(s) if s.eq_ignore_ascii_case("false")
                        || s.eq_ignore_ascii_case("no")
                        || s.eq_ignore_ascii_case("off")
                        || s == "0")
                },
                description: "TLS certificate verification is disabled (string value)",
                concept_segments: &["tls", "cert_verification"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.95,
            },
            // Security feature disabled
            SecurityRule {
                name: "security_disabled",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(security|auth|authentication)[._]?(enabled|active|on)$"
                ).expect("valid regex"),
                value_check: |v| v.is_false(),
                description: "Security/authentication is disabled in config",
                concept_segments: &["security", "enabled"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.90,
            },
            // CSRF disabled
            SecurityRule {
                name: "csrf_disabled",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(csrf|xsrf)[._]?(enabled|protection|check)$"
                ).expect("valid regex"),
                value_check: |v| v.is_false(),
                description: "CSRF protection is disabled in config",
                concept_segments: &["csrf", "enabled"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.90,
            },
            // Debug mode enabled (only flag if not in dev file)
            SecurityRule {
                name: "debug_enabled",
                path_pattern: Regex::new(
                    r"(?i)^debug$"
                ).expect("valid regex"),
                value_check: |v| v.is_true(),
                description: "Debug mode is enabled in config",
                concept_segments: &["debug", "enabled"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(true),
                confidence: 0.85,
            },
            // Weak password minimum length
            SecurityRule {
                name: "weak_password_length",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(password|pwd)[._]?(min[._]?length|minimum[._]?length|min[._]?len)$"
                ).expect("valid regex"),
                value_check: |v| {
                    v.as_integer().map(|i| i < 8).unwrap_or(false)
                },
                description: "Password minimum length is less than 8 characters",
                concept_segments: &["password", "min_length"],
                predicate: "min_length",
                claim_value: ObjectValue::Text("weak".to_string()),
                confidence: 0.90,
            },
            // Cookie secure flag disabled
            SecurityRule {
                name: "cookie_secure_disabled",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(cookie|session)[._]?(secure)$"
                ).expect("valid regex"),
                value_check: |v| v.is_false(),
                description: "Cookie secure flag is disabled",
                concept_segments: &["cookie", "secure"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.90,
            },
            // Cookie httpOnly disabled
            SecurityRule {
                name: "cookie_httponly_disabled",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(cookie|session)[._]?(http[._]?only|httponly)$"
                ).expect("valid regex"),
                value_check: |v| v.is_false(),
                description: "Cookie httpOnly flag is disabled",
                concept_segments: &["cookie", "httponly"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.90,
            },
            // CORS allow all origins with credentials
            SecurityRule {
                name: "cors_allow_all",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(cors|access[._]?control)[._]?(allow[._]?origin|origins?)$"
                ).expect("valid regex"),
                value_check: |v| {
                    matches!(v.as_str(), Some("*"))
                },
                description: "CORS allows all origins",
                concept_segments: &["cors", "allow_origin"],
                predicate: "policy",
                claim_value: ObjectValue::Text("*".to_string()),
                confidence: 0.85,
            },
            // Rate limiting disabled
            SecurityRule {
                name: "rate_limit_disabled",
                path_pattern: Regex::new(
                    r"(?i)(^|\.)(rate[._]?limit|throttle)[._]?(enabled|active)$"
                ).expect("valid regex"),
                value_check: |v| v.is_false(),
                description: "Rate limiting is disabled",
                concept_segments: &["rate_limit", "enabled"],
                predicate: "enabled",
                claim_value: ObjectValue::Boolean(false),
                confidence: 0.85,
            },
        ];

        Self { rules }
    }

    /// Check if file is a development/test config (lower severity).
    fn is_dev_config(file: &str) -> bool {
        let lower = file.to_lowercase();
        lower.contains("dev")
            || lower.contains("development")
            || lower.contains("local")
            || lower.contains("test")
            || lower.contains("example")
            || lower.contains("sample")
    }

    /// Extract security claims from parsed config.
    fn extract_from_config(
        &self,
        config: &ConfigValue,
        path_segments: &[String],
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();
        let is_dev = Self::is_dev_config(file);
        let is_test = is_test_file(file);

        walk_config(config, &mut |path, value| {
            for rule in &self.rules {
                // Skip debug rule for dev configs
                if rule.name == "debug_enabled" && is_dev {
                    continue;
                }

                if rule.path_pattern.is_match(path) && (rule.value_check)(value) {
                    let mut concept_path = path_segments.to_vec();
                    for segment in rule.concept_segments {
                        concept_path.push((*segment).to_string());
                    }

                    // Reduce confidence for test files
                    let confidence = if is_test { rule.confidence * 0.5 } else { rule.confidence };

                    claims.push(ExtractedClaim {
                        concept_path: format!("code://{}", concept_path.join("/")),
                        predicate: rule.predicate.to_string(),
                        value: rule.claim_value.clone(),
                        file: file.to_string(),
                        line: 0, // Structured parsing doesn't give line numbers
                        matched_text: format!("{}: {:?}", path, value),
                        confidence,
                        description: rule.description.to_string(),
                    });
                }
            }
        });

        claims
    }
}

impl Extractor for ConfigSecurityExtractor {
    fn name(&self) -> &str {
        "config_security"
    }

    fn languages(&self) -> &[Language] {
        &[Language::Yaml, Language::Json, Language::Toml]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        // Skip empty or very small files
        if content.trim().is_empty() || content.len() < 5 {
            return Vec::new();
        }

        // Try to parse the config file
        let config = match parse_config(content, language) {
            Ok(c) => c,
            Err(_) => {
                // If parsing fails, fall back to regex extractors
                // (handled by other extractors)
                return Vec::new();
            }
        };

        self.extract_from_config(&config, path_segments, file)
    }
}

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

    #[test]
    fn test_deeply_nested_tls_verify() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
server:
  internal:
    api:
      tls:
        verify: false
"#;
        let claims = extractor.extract(
            &["config".to_string(), "myapp".to_string()],
            yaml,
            Language::Yaml,
            "config/production.yaml",
        );

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("tls/cert_verification"));
        assert_eq!(claims[0].predicate, "enabled");
        assert_eq!(claims[0].value, ObjectValue::Boolean(false));
    }

    #[test]
    fn test_insecure_skip_verify_true() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
http:
  client:
    insecure_skip_verify: true
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].description.contains("skipped"));
    }

    #[test]
    fn test_security_disabled() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
app:
  security:
    enabled: false
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("security/enabled"));
    }

    #[test]
    fn test_csrf_disabled() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
server:
  csrf:
    enabled: false
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("csrf/enabled"));
    }

    #[test]
    fn test_debug_enabled_production() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
debug: true
"#;
        let claims = extractor.extract(
            &["config".to_string()],
            yaml,
            Language::Yaml,
            "config/production.yaml",
        );

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("debug/enabled"));
    }

    #[test]
    fn test_debug_enabled_dev_file_skipped() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
debug: true
"#;
        let claims = extractor.extract(
            &["config".to_string()],
            yaml,
            Language::Yaml,
            "config/development.yaml",
        );

        // Debug in dev file should NOT be flagged
        assert!(claims.is_empty());
    }

    #[test]
    fn test_weak_password_length() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
auth:
  password:
    min_length: 4
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("password/min_length"));
    }

    #[test]
    fn test_no_false_positive_secure_config() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
server:
  tls:
    verify: true
  security:
    enabled: true
  debug: false
  password:
    min_length: 12
"#;
        let claims = extractor.extract(
            &["config".to_string()],
            yaml,
            Language::Yaml,
            "config/production.yaml",
        );

        // All settings are secure, no claims
        assert!(claims.is_empty());
    }

    #[test]
    fn test_json_parsing() {
        let extractor = ConfigSecurityExtractor::new();
        let json = r#"{"server": {"tls": {"verify": false}}}"#;

        let claims =
            extractor.extract(&["config".to_string()], json, Language::Json, "config.json");

        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_toml_parsing() {
        let extractor = ConfigSecurityExtractor::new();
        let toml_content = r#"
[server.tls]
verify = false
"#;
        let claims =
            extractor.extract(&["config".to_string()], toml_content, Language::Toml, "config.toml");

        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_cookie_flags() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
session:
  cookie:
    secure: false
    httpOnly: false
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 2);
    }

    #[test]
    fn test_cors_allow_all() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
cors:
  allow_origin: "*"
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("cors/allow_origin"));
    }

    #[test]
    fn test_rate_limit_disabled() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
api:
  rate_limit:
    enabled: false
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_multiple_issues() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
server:
  tls:
    verify: false
  csrf:
    enabled: false
debug: true
"#;
        let claims = extractor.extract(
            &["config".to_string()],
            yaml,
            Language::Yaml,
            "config/production.yaml",
        );

        // Should find: TLS verify, CSRF, debug
        assert_eq!(claims.len(), 3);
    }

    #[test]
    fn test_test_file_reduced_confidence() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
tls:
  verify: false
"#;
        let prod_claims = extractor.extract(
            &["config".to_string()],
            yaml,
            Language::Yaml,
            "config/production.yaml",
        );
        let test_claims = extractor.extract(
            &["config".to_string()],
            yaml,
            Language::Yaml,
            "test/fixtures/config.yaml",
        );

        assert!(test_claims[0].confidence < prod_claims[0].confidence);
    }

    #[test]
    fn test_invalid_yaml_graceful() {
        let extractor = ConfigSecurityExtractor::new();
        let invalid = r#"
server:
  - this: is
  invalid: yaml: content
"#;
        let claims =
            extractor.extract(&["config".to_string()], invalid, Language::Yaml, "config.yaml");

        // Should not panic, just return empty
        assert!(claims.is_empty());
    }

    #[test]
    fn test_string_value_false() {
        let extractor = ConfigSecurityExtractor::new();
        let yaml = r#"
tls:
  verify: "false"
"#;
        let claims =
            extractor.extract(&["config".to_string()], yaml, Language::Yaml, "config.yaml");

        assert_eq!(claims.len(), 1);
    }
}
