//! Rate limiting configuration extractor.
//!
//! Detects rate limiting that is disabled or set unreasonably high,
//! which can lead to availability and security issues.

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

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

/// Configuration for rate limit thresholds.
#[derive(Debug, Clone)]
pub struct RateLimitThresholds {
    /// Maximum reasonable requests per minute.
    pub max_requests_per_minute: u64,
}

impl Default for RateLimitThresholds {
    fn default() -> Self {
        Self { max_requests_per_minute: 10_000 }
    }
}

/// Extractor for rate limiting configuration.
pub struct RateLimitExtractor {
    /// Rate limiting disabled patterns
    disabled: Regex,
    /// Numeric rate limit patterns
    numeric_limit: Regex,
    /// Thresholds for flagging
    thresholds: RateLimitThresholds,
}

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

impl RateLimitExtractor {
    /// Create a new rate limit extractor with the given thresholds.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new(thresholds: RateLimitThresholds) -> Self {
        Self {
            // Match rate limiting disabled patterns in config contexts:
            // Pattern 1: Direct value assignment
            //   - "rate_limit: disabled", "ratelimit = off"
            //   - "rate_limit: 0" or "rate_limit = 0" (explicitly set to zero)
            // Pattern 2: Property-style with suffix
            //   - "ratelimit_enabled = false", "rate_limit_enabled: false"
            // NOTE: The \b0\b pattern ensures we match standalone zero, not zeros
            // embedded in other numbers like "100" or "10".
            // NOTE: We require [:=] to ensure assignment context, avoiding matches like
            // "RateLimiter::new(10)" which would previously match due to "0" at the end.
            // Expanded to include rate_limiting, throttle, throttling
            // Handles flat YAML/TOML patterns like:
            //   rate_limiting: false
            //   rate_limiting_enabled: false
            //   throttle: disabled
            // NOTE: Nested YAML (rate_limiting:\n  enabled: false) requires multi-line parsing
            disabled: Regex::new(
                r"(?i)(rate_?limit(?:ing)?|ratelimit|throttl(?:e|ing))(?:[_.]?enabled)?\s*[:=]\s*(?:disabled|off|false|\b0\b|none|skip)",
            )
            .expect("valid regex"),
            // Match numeric rate limits. Supports:
            // - "rate_limit = 100000"
            // - "rateLimitPerTenant: 100_000" (YAML with underscores)
            // - "max_requests = 1000"
            // - "requests_per_second = 500"
            // The [\d_]+ pattern matches numbers with optional underscores for readability.
            numeric_limit: Regex::new(
                r"(?i)(rate_?limit\w*|max_?requests|requests_?per_?(?:second|minute|hour))\s*[:=]\s*([\d_]+)",
            )
            .expect("valid regex"),
            thresholds,
        }
    }

    fn normalize_to_per_minute(&self, value: u64, line: &str) -> u64 {
        let lower = line.to_lowercase();

        if lower.contains("per_second") || lower.contains("persecond") || lower.contains("/s") {
            value * 60
        } else if lower.contains("per_hour") || lower.contains("perhour") || lower.contains("/h") {
            value / 60
        } else {
            // Default: assume per minute
            value
        }
    }
}

impl Extractor for RateLimitExtractor {
    fn name(&self) -> &str {
        "rate_limit"
    }

    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();

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

            // Rate limiting disabled
            if let Some(matched) = self.disabled.find(line) {
                let mut concept_path = path_segments.to_vec();
                concept_path.push("rate_limit".to_string());
                concept_path.push("enabled".to_string());

                claims.push(ExtractedClaim {
                    concept_path: format!("code://{}", concept_path.join("/")),
                    predicate: "enabled".to_string(),
                    value: ObjectValue::Boolean(false),
                    file: file.to_string(),
                    line: line_num,
                    matched_text: matched.as_str().to_string(),
                    confidence: 1.0,
                    description: "Rate limiting is disabled".to_string(),
                });
                continue;
            }

            // Numeric rate limit check
            if let Some(captures) = self.numeric_limit.captures(line) {
                if let Some(value_match) = captures.get(2) {
                    // Strip underscores (used for readability in YAML/Rust literals)
                    let value_str: String =
                        value_match.as_str().chars().filter(|c| *c != '_').collect();
                    if let Ok(value) = value_str.parse::<u64>() {
                        let per_minute = self.normalize_to_per_minute(value, line);

                        if per_minute > self.thresholds.max_requests_per_minute {
                            let mut concept_path = path_segments.to_vec();
                            concept_path.push("rate_limit".to_string());
                            concept_path.push("max_requests".to_string());

                            claims.push(ExtractedClaim {
                                concept_path: format!("code://{}", concept_path.join("/")),
                                predicate: "config_value".to_string(),
                                value: ObjectValue::Number(per_minute as f64),
                                file: file.to_string(),
                                line: line_num,
                                matched_text: captures
                                    .get(0)
                                    .map(|m| m.as_str())
                                    .unwrap_or("")
                                    .to_string(),
                                confidence: 1.0,
                                description: format!(
                                    "Rate limit {} req/min exceeds recommended maximum {} req/min",
                                    per_minute, self.thresholds.max_requests_per_minute
                                ),
                            });
                        }
                    }
                }
            }
        }

        claims
    }
}

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

    #[test]
    fn test_rate_limit_disabled() {
        let extractor = RateLimitExtractor::default();
        let content = r#"
            rate_limit: disabled
        "#;

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

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Boolean(false));
    }

    #[test]
    fn test_rate_limit_false() {
        let extractor = RateLimitExtractor::default();
        let content = r#"
            ratelimit_enabled = false
        "#;

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

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

    #[test]
    fn test_unreasonably_high_limit() {
        let extractor = RateLimitExtractor::default();
        let content = r#"
            max_requests = 100000  // 100k per minute
        "#;

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

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

    #[test]
    fn test_reasonable_limit_no_claims() {
        let extractor = RateLimitExtractor::default();
        let content = r#"
            max_requests = 1000
        "#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_per_second_normalization() {
        let extractor = RateLimitExtractor::default();
        let content = r#"
            requests_per_second = 500  // 30k per minute
        "#;

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

        assert_eq!(claims.len(), 1);
        // 500 * 60 = 30000 > 10000
    }

    // Regression tests for false positives

    #[test]
    fn test_rate_limiter_constructor_not_flagged() {
        // RateLimiter::new(10) should NOT match - it's creating a limiter, not disabling one
        let extractor = RateLimitExtractor::default();
        let content = r#"
            let limiter = RateLimiter::new(10); // 10 req/min
        "#;

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

        assert!(claims.is_empty(), "RateLimiter::new(10) should not be flagged as disabled");
    }

    #[test]
    fn test_rate_limit_constant_not_flagged() {
        // RATE_LIMIT: u64 = 100 is defining a limit, not disabling one
        let extractor = RateLimitExtractor::default();
        let content = r#"
            const RATE_LIMIT: u64 = 100;
        "#;

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

        // Should only flag if 100 > 10000 (it's not), so no claims
        assert!(claims.is_empty(), "RATE_LIMIT constant should not be flagged as disabled");
    }

    #[test]
    fn test_rate_limit_per_tenant_not_flagged() {
        // rateLimitPerTenant: 100_000 is setting a high limit, not disabling
        let extractor = RateLimitExtractor::default();
        let content = r#"
            rateLimitPerTenant: 100_000
        "#;

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

        // This should trigger the "high limit" check if 100000 > 10000
        // but NOT the "disabled" check
        assert_eq!(claims.len(), 1, "High limit should be flagged");
        assert!(
            claims[0].description.contains("exceeds"),
            "Should flag as high limit, not disabled"
        );
    }

    #[test]
    fn test_rate_limit_explicitly_zero() {
        // rate_limit = 0 should be flagged as disabled
        let extractor = RateLimitExtractor::default();
        let content = r#"
            rate_limit = 0
        "#;

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

        assert_eq!(claims.len(), 1, "rate_limit = 0 should be flagged as disabled");
        assert_eq!(claims[0].value, ObjectValue::Boolean(false));
    }

    #[test]
    fn test_rate_limit_documentation_not_flagged() {
        // Documentation mentioning rate limits should not be flagged
        let extractor = RateLimitExtractor::default();
        let content = r#"
            // The rate limit is set to 10 requests per second
            // Error: rate_limit_exceeded - you've hit the limit
        "#;

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

        assert!(claims.is_empty(), "Documentation should not be flagged");
    }

    #[test]
    fn test_rate_limiting_false() {
        // Test flat YAML pattern (nested YAML requires multi-line parser)
        let extractor = RateLimitExtractor::default();
        let content = r#"
rate_limiting: false
"#;

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

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Boolean(false));
    }

    #[test]
    fn test_rate_limiting_enabled_false_flat() {
        // Test flat property pattern
        let extractor = RateLimitExtractor::default();
        let content = r#"
rate_limiting_enabled: false
"#;

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

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Boolean(false));
    }

    #[test]
    fn test_throttling_disabled() {
        let extractor = RateLimitExtractor::default();
        let content = r#"
throttling: disabled
"#;

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

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