//! API key security configuration extractor.
//!
//! Detects potential API authentication misconfigurations:
//! - `require_for_all: false` - API key not required for all endpoints
//! - Excessive public paths (> 5 paths) - overly permissive access
//! - Using DEFAULT_API_KEY_RATE_LIMIT without customization

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

use super::Extractor;
use crate::types::{ExtractedClaim, Language};

/// Extractor for API key security configuration.
///
/// Focuses on authentication and rate limiting misconfigurations.
pub struct ApiKeySecurityExtractor {
    /// Pattern: require_for_all: false
    require_for_all_false: Regex,
    /// Pattern: public_paths: vec![...] with more than 5 entries
    public_paths_array: Regex,
    /// Pattern: DEFAULT_API_KEY_RATE_LIMIT usage
    default_rate_limit: Regex,
}

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

impl ApiKeySecurityExtractor {
    /// Create a new API key security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Rust: require_for_all: false
            // Go: RequireForAll: false
            // YAML: require_for_all: false
            require_for_all_false: Regex::new(r#"(?i)require_?for_?all\s*[:=]\s*false"#)
                .expect("valid regex"),

            // Look for public_paths arrays - we'll count entries manually
            // Handles Rust vec![...], Go []string{...}, YAML lists
            public_paths_array: Regex::new(r#"(?i)public_?paths\s*[:=]\s*(?:vec!|[\[\{])"#)
                .expect("valid regex"),

            // Using default rate limit constant
            default_rate_limit: Regex::new(r"DEFAULT_API_KEY_RATE_LIMIT").expect("valid regex"),
        }
    }

    /// Determine confidence based on context.
    fn confidence_for_file(&self, file: &str) -> f32 {
        if file.contains("test") || file.contains("example") {
            0.5
        } else {
            1.0
        }
    }

    /// Count public paths in a potential array definition.
    fn count_public_paths(&self, content: &str, start_line: usize) -> usize {
        let lines: Vec<&str> = content.lines().collect();
        let mut count = 0;
        let mut depth = 0;
        let mut in_array = false;

        for (idx, line) in lines.iter().enumerate().skip(start_line) {
            if idx >= start_line + 20 {
                // Don't search more than 20 lines ahead
                break;
            }

            for ch in line.chars() {
                match ch {
                    '[' => {
                        depth += 1;
                        in_array = true;
                    }
                    ']' => {
                        depth -= 1;
                        if depth == 0 {
                            return count;
                        }
                    }
                    '"' | '\'' if in_array && depth > 0 => {
                        count += 1;
                        // Skip to end of string to avoid double-counting
                        break;
                    }
                    _ => {}
                }
            }

            if depth == 0 && in_array {
                break;
            }
        }

        count
    }
}

impl Extractor for ApiKeySecurityExtractor {
    fn name(&self) -> &str {
        "api_key_security"
    }

    fn languages(&self) -> &[Language] {
        &[
            Language::Rust,
            Language::Go,
            Language::Python,
            Language::TypeScript,
            Language::JavaScript,
            Language::Yaml,
            Language::Toml,
            Language::Json,
        ]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        _language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();
        let confidence = self.confidence_for_file(file);

        for (line_idx, line) in content.lines().enumerate() {
            let line_num = line_idx + 1;

            // Check for require_for_all: false
            if self.require_for_all_false.is_match(line) {
                let mut concept_path = path_segments.to_vec();
                concept_path.push("api".to_string());
                concept_path.push("auth".to_string());

                claims.push(ExtractedClaim {
                    concept_path: format!("code://{}", concept_path.join("/")),
                    predicate: "require_api_key".to_string(),
                    value: ObjectValue::Boolean(false),
                    file: file.to_string(),
                    line: line_num,
                    matched_text: line.trim().to_string(),
                    confidence,
                    description: "API key not required for all endpoints (require_for_all: false)"
                        .to_string(),
                });
            }

            // Check for public_paths arrays
            if self.public_paths_array.is_match(line) {
                let count = self.count_public_paths(content, line_idx);

                if count > 5 {
                    let mut concept_path = path_segments.to_vec();
                    concept_path.push("api".to_string());
                    concept_path.push("auth".to_string());

                    claims.push(ExtractedClaim {
                        concept_path: format!("code://{}", concept_path.join("/")),
                        predicate: "public_paths_count".to_string(),
                        value: ObjectValue::Number(count as f64),
                        file: file.to_string(),
                        line: line_num,
                        matched_text: line.trim().to_string(),
                        confidence: confidence * 0.9, // Slight reduction since we're inferring
                        description: format!("Overly permissive public paths ({} paths)", count),
                    });
                }
            }

            // Check for DEFAULT_API_KEY_RATE_LIMIT usage
            if self.default_rate_limit.is_match(line) {
                // Only flag if it looks like it's being used directly without customization
                if !line.contains("const") && !line.contains("pub const") && !line.contains("//") {
                    let mut concept_path = path_segments.to_vec();
                    concept_path.push("api".to_string());
                    concept_path.push("rate_limit".to_string());

                    claims.push(ExtractedClaim {
                        concept_path: format!("code://{}", concept_path.join("/")),
                        predicate: "using_default".to_string(),
                        value: ObjectValue::Boolean(true),
                        file: file.to_string(),
                        line: line_num,
                        matched_text: line.trim().to_string(),
                        confidence: confidence * 0.7, // Lower confidence - might be intentional
                        description: "Using default API key rate limit without customization"
                            .to_string(),
                    });
                }
            }
        }

        claims
    }
}

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

    #[test]
    fn test_require_for_all_false_rust() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
            ApiKeyAuthConfig {
                require_for_all: false,
                public_paths: vec!["/health".to_string()],
            }
        "#;

        let claims = extractor.extract(
            &["rust".to_string(), "myapi".to_string()],
            content,
            Language::Rust,
            "src/config.rs",
        );

        assert!(!claims.is_empty());
        let require_claim = claims.iter().find(|c| c.predicate == "require_api_key");
        assert!(require_claim.is_some());
        if let Some(claim) = require_claim {
            assert_eq!(claim.value, ObjectValue::Boolean(false));
            assert!(claim.concept_path.contains("api/auth"));
        }
    }

    #[test]
    fn test_require_for_all_false_yaml() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
api:
  auth:
    require_for_all: false
    public_paths:
      - /health
      - /metrics
"#;

        let claims =
            extractor.extract(&["config".to_string()], content, Language::Yaml, "config/api.yaml");

        assert!(!claims.is_empty());
        let require_claim = claims.iter().find(|c| c.predicate == "require_api_key");
        assert!(require_claim.is_some());
    }

    #[test]
    fn test_excessive_public_paths() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
            public_paths: vec![
                "/health".to_string(),
                "/metrics".to_string(),
                "/swagger-ui".to_string(),
                "/docs".to_string(),
                "/status".to_string(),
                "/ping".to_string(),
                "/info".to_string(),
            ]
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/middleware.rs");

        assert!(!claims.is_empty());
        let paths_claim = claims.iter().find(|c| c.predicate == "public_paths_count");
        assert!(paths_claim.is_some());
        if let Some(claim) = paths_claim {
            if let ObjectValue::Number(count) = claim.value {
                assert!(count > 5.0);
            } else {
                panic!("Expected Number value");
            }
        }
    }

    #[test]
    fn test_reasonable_public_paths_not_flagged() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
            public_paths: vec![
                "/health".to_string(),
                "/v1/health".to_string(),
                "/swagger-ui".to_string(),
            ]
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/middleware.rs");

        // Should not flag this - only 3 paths
        let paths_claim = claims.iter().find(|c| c.predicate == "public_paths_count");
        assert!(paths_claim.is_none());
    }

    #[test]
    fn test_default_rate_limit_usage() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
            let rate_limit = record.rate_limit.unwrap_or(DEFAULT_API_KEY_RATE_LIMIT);
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/handlers.rs");

        assert!(!claims.is_empty());
        let rate_claim = claims.iter().find(|c| c.predicate == "using_default");
        assert!(rate_claim.is_some());
    }

    #[test]
    fn test_default_rate_limit_const_definition_not_flagged() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
            pub const DEFAULT_API_KEY_RATE_LIMIT: u64 = 10_000;
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/config.rs");

        // Should not flag constant definition
        let rate_claim = claims.iter().find(|c| c.predicate == "using_default");
        assert!(rate_claim.is_none());
    }

    #[test]
    fn test_confidence_in_test_file() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
            ApiKeyAuthConfig {
                require_for_all: false,
            }
        "#;

        let claims = extractor.extract(
            &["rust".to_string()],
            content,
            Language::Rust,
            "src/middleware_test.rs",
        );

        assert!(!claims.is_empty());
        assert_eq!(claims[0].confidence, 0.5);
    }

    #[test]
    fn test_go_api_config() {
        let extractor = ApiKeySecurityExtractor::new();
        let content = r#"
config := &AuthConfig{
    RequireForAll: false,
    PublicPaths: []string{"/health", "/metrics"},
}
"#;

        let claims = extractor.extract(&["go".to_string()], content, Language::Go, "config.go");

        assert!(!claims.is_empty());
        let require_claim = claims.iter().find(|c| c.predicate == "require_api_key");
        assert!(require_claim.is_some());
    }
}
