//! Unreal Engine INI Config extractor.
//!
//! Detects configuration issues in Unreal Engine .ini files, such as:
//! - High network bandwidth limits
//! - Security settings disabled in DefaultEngine.ini
//! - Hardcoded API keys or insecure URLs

#![allow(clippy::too_many_arguments)]

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

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

/// Extractor for Unreal Engine INI patterns.
pub struct UnrealConfigExtractor {
    /// MaxClientRate setting
    max_client_rate: Regex,
    /// MaxInternetClientRate setting
    max_internet_client_rate: Regex,
    /// API key detection (non-empty)
    api_key: Regex,
    /// Insecure API URL detection
    insecure_url: Regex,
}

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

impl UnrealConfigExtractor {
    /// Create a new Unreal Config extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if regex patterns are invalid.
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            max_client_rate: Regex::new(r"MaxClientRate=(\d+)").expect("valid regex"),
            max_internet_client_rate: Regex::new(r"MaxInternetClientRate=(\d+)")
                .expect("valid regex"),
            // Matches ApiKey= followed by at least 1 non-whitespace character (actual credentials)
            api_key: Regex::new(r"(?i)ApiKey=\s*(\S+)").expect("valid regex"),
            // Matches URL starting with http:// (insecure)
            insecure_url: Regex::new(r#"(?i)BaseUrl=\s*['"](http://[^'"]+)['"]"#)
                .expect("valid regex"),
        }
    }

    fn check_numeric(
        &self,
        content: &str,
        pattern: &Regex,
        path_segments: &[String],
        file: &str,
        category: &str,
        leaf: &str,
        desc_template: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(caps) = pattern.captures(line) {
                if let Some(val_match) = caps.get(1) {
                    if let Ok(val) = val_match.as_str().parse::<i64>() {
                        let mut concept_path = path_segments.to_vec();
                        concept_path.push("unreal".to_string());
                        concept_path.push(category.to_string());
                        concept_path.push(leaf.to_string());

                        claims.push(ExtractedClaim {
                            concept_path: format!("code://{}", concept_path.join("/")),
                            predicate: "value".to_string(),
                            value: ObjectValue::Number(val as f64),
                            file: file.to_string(),
                            line: line_idx + 1,
                            matched_text: line.trim().to_string(),
                            confidence: 1.0,
                            description: format!("{} set to {}", desc_template, val),
                        });
                    }
                }
            }
        }

        claims
    }

    fn check_string(
        &self,
        content: &str,
        pattern: &Regex,
        path_segments: &[String],
        file: &str,
        category: &str,
        leaf: &str,
        predicate: &str,
        desc_template: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(caps) = pattern.captures(line) {
                if let Some(val_match) = caps.get(1) {
                    let mut concept_path = path_segments.to_vec();
                    concept_path.push("unreal".to_string());
                    concept_path.push(category.to_string());
                    concept_path.push(leaf.to_string());

                    claims.push(ExtractedClaim {
                        concept_path: format!("code://{}", concept_path.join("/")),
                        predicate: predicate.to_string(),
                        value: ObjectValue::Text(val_match.as_str().to_string()),
                        file: file.to_string(),
                        line: line_idx + 1,
                        matched_text: line.trim().to_string(),
                        confidence: 1.0,
                        description: desc_template.to_string(),
                    });
                }
            }
        }

        claims
    }
}

impl Extractor for UnrealConfigExtractor {
    fn name(&self) -> &str {
        "unreal_config"
    }

    fn languages(&self) -> &[Language] {
        &[Language::Ini]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        if language != Language::Ini {
            return vec![];
        }

        let mut claims = Vec::new();

        claims.extend(self.check_numeric(
            content,
            &self.max_client_rate,
            path_segments,
            file,
            "network",
            "max_client_rate",
            "MaxClientRate",
        ));

        claims.extend(self.check_numeric(
            content,
            &self.max_internet_client_rate,
            path_segments,
            file,
            "network",
            "max_internet_client_rate",
            "MaxInternetClientRate",
        ));

        claims.extend(self.check_string(
            content,
            &self.api_key,
            path_segments,
            file,
            "security",
            "api_key",
            "storage_method",
            "API key found in INI config. Use secure storage instead.",
        ));

        claims.extend(self.check_string(
            content,
            &self.insecure_url,
            path_segments,
            file,
            "network",
            "https_enforcement",
            "protocol",
            "Insecure HTTP URL found in config. Use HTTPS instead.",
        ));

        claims
    }
}

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

    #[test]
    fn test_config_extraction() {
        let extractor = UnrealConfigExtractor::new();
        let content = r#"
            [/Script/Engine.Player]
            ConfiguredInternetSpeed=10000
            MaxClientRate=15000

            [/Script/LivelyVideoStreamer.MasqueradeClient]
            ApiBaseUrl="http://api.masq.live/v1"
            ApiKey=sk_live_12345
        "#;

        let claims = extractor.extract(
            &["config".to_string()],
            content,
            Language::Ini,
            "Config/DefaultMasq.ini",
        );

        assert_eq!(claims.len(), 3);
        assert!(claims.iter().any(|c| c.concept_path.contains("max_client_rate")));
        assert!(claims.iter().any(|c| c.concept_path.contains("https_enforcement")));
        assert!(claims.iter().any(|c| c.concept_path.contains("api_key")));
    }

    #[test]
    fn test_empty_api_key_not_flagged() {
        // Empty API keys are NOT a security issue - they're standard Unreal practice.
        // Only non-empty keys (actual credentials) should be flagged.
        let extractor = UnrealConfigExtractor::new();
        let content = r#"
[/Script/LivelyVideoStreamer.MasqueradeClient]
ApiKey=
SomeOtherSetting=value
"#;

        let claims = extractor.extract(
            &["config".to_string()],
            content,
            Language::Ini,
            "Config/DefaultMasq.ini",
        );

        // Empty ApiKey should NOT be detected
        assert!(claims.is_empty());
    }

    #[test]
    fn test_real_api_key_flagged() {
        let extractor = UnrealConfigExtractor::new();
        let content = r#"
[/Script/LivelyVideoStreamer.MasqueradeClient]
ApiKey=sk_live_12345_real_credential
"#;

        let claims = extractor.extract(
            &["config".to_string()],
            content,
            Language::Ini,
            "Config/DefaultMasq.ini",
        );

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