//! Hardcoded secrets extractor.
//!
//! Detects credentials, API keys, and tokens embedded in source code,
//! which violates security best practices.

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

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

/// Extractor for hardcoded secrets in source code.
pub struct HardcodedSecretsExtractor {
    /// API keys (generic)
    api_key: Regex,
    /// Passwords
    password: Regex,
    /// AWS access key IDs (AKIA prefix)
    aws_key: Regex,
    /// Private keys (PEM format)
    private_key: Regex,
    /// Generic secrets/tokens
    secret_token: Regex,
    /// Placeholder values to exclude
    placeholder: Regex,
}

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

impl HardcodedSecretsExtractor {
    /// Create a new secrets extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // API key patterns - support both quoted and unquoted YAML values
            // Expanded to include common service-specific key names
            // Unquoted pattern uses [A-Za-z0-9_\-]+ (greedy) then checks context
            api_key: Regex::new(
                r#"(?i)(api[_-]?key|apikey|stripe[_-]?(?:secret[_-]?)?key|sendgrid[_-]?api[_-]?key|twilio[_-]?(?:api[_-]?)?key|slack[_-]?(?:api[_-]?)?token|aws[_-]?(?:access[_-]?)?key[_-]?id)\s*[:=]\s*(?:["'][A-Za-z0-9_\-]{20,}["']|[A-Za-z0-9_\-]{20,})"#
            ).expect("valid regex"),
            // Password patterns - support unquoted YAML values
            // Expanded to include database_password, db_password, redis_password
            // Unquoted: match 8+ non-whitespace chars (YAML values typically end at line/comment)
            password: Regex::new(
                r#"(?i)(password|passwd|pwd|database[_-]?password|db[_-]?password|redis[_-]?password)\s*[:=]\s*(?:["'][^"']{4,}["']|[^\s"'#]{8,})"#
            ).expect("valid regex"),
            aws_key: Regex::new(r"AKIA[0-9A-Z]{16}").expect("valid regex"),
            private_key: Regex::new(r"-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----")
                .expect("valid regex"),
            // Secret/token patterns - support unquoted YAML values
            // Expanded to include aws_secret_access_key
            secret_token: Regex::new(
                r#"(?i)(secret|token|auth[_-]?key|aws[_-]?secret[_-]?access[_-]?key|encryption[_-]?key|client[_-]?secret)\s*[:=]\s*(?:["'][A-Za-z0-9_\-/.+=]{16,}["']|[A-Za-z0-9_\-/.+=]{16,})"#,
            )
            .expect("valid regex"),
            // Placeholder pattern - matches values that ARE placeholders, not values containing these words
            // Uses word boundaries and specific patterns to avoid false positives
            placeholder: Regex::new(
                r#"(?i)^(password|changeme|placeholder|CHANGE_ME|xxx+|your[_-]?(?:api[_-]?key|password|secret|token)|example|dummy|fake|sample|todo|fixme)$|^<.*>$|^\$\{.*\}$|^\{\{.*\}\}$"#,
            )
            .expect("valid regex"),
        }
    }

    fn is_placeholder(&self, value: &str) -> bool {
        // Strip quotes and whitespace before checking
        let clean = value.trim().trim_matches(|c| c == '"' || c == '\'');
        self.placeholder.is_match(clean)
    }

    fn is_test_file(&self, file: &str) -> bool {
        let lower = file.to_lowercase();
        lower.contains("test")
            || lower.contains("spec")
            || lower.contains("example")
            || lower.contains("fixture")
            || lower.contains("mock")
    }

    fn extract_secret(
        &self,
        path_segments: &[String],
        file: &str,
        line: usize,
        matched_text: &str,
        leaf: &str,
        description: &str,
    ) -> ExtractedClaim {
        let mut concept_path = path_segments.to_vec();
        concept_path.push("secrets".to_string());
        concept_path.push(leaf.to_string());

        // Lower confidence for test files
        let confidence = if self.is_test_file(file) { 0.5 } else { 1.0 };

        ExtractedClaim {
            concept_path: format!("code://{}", concept_path.join("/")),
            predicate: "storage_method".to_string(),
            value: ObjectValue::Text("hardcoded".to_string()),
            file: file.to_string(),
            line,
            matched_text: matched_text.to_string(),
            confidence,
            description: description.to_string(),
        }
    }
}

impl Extractor for HardcodedSecretsExtractor {
    fn name(&self) -> &str {
        "hardcoded_secrets"
    }

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

    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;

            // API key detection
            if let Some(matched) = self.api_key.find(line) {
                let matched_str = matched.as_str();
                if !self.is_placeholder(matched_str) {
                    claims.push(self.extract_secret(
                        path_segments,
                        file,
                        line_num,
                        matched_str,
                        "api_key",
                        "API key is hardcoded in source",
                    ));
                }
            }

            // Password detection
            if let Some(matched) = self.password.find(line) {
                let matched_str = matched.as_str();
                // Extract just the value (after : or =) for placeholder check
                // The property name "password" shouldn't trigger placeholder detection
                let value_only = matched_str
                    .split(&[':', '='][..])
                    .nth(1)
                    .map(|s| s.trim())
                    .unwrap_or(matched_str);
                if !self.is_placeholder(value_only) {
                    claims.push(self.extract_secret(
                        path_segments,
                        file,
                        line_num,
                        matched_str,
                        "password",
                        "Password is hardcoded in source",
                    ));
                }
            }

            // AWS key detection
            if let Some(matched) = self.aws_key.find(line) {
                claims.push(self.extract_secret(
                    path_segments,
                    file,
                    line_num,
                    matched.as_str(),
                    "aws_credentials",
                    "AWS access key ID is hardcoded in source",
                ));
            }

            // Private key detection
            if let Some(matched) = self.private_key.find(line) {
                claims.push(self.extract_secret(
                    path_segments,
                    file,
                    line_num,
                    matched.as_str(),
                    "private_key",
                    "Private key is embedded in source",
                ));
            }

            // Generic secret/token detection
            if let Some(matched) = self.secret_token.find(line) {
                let matched_str = matched.as_str();
                if !self.is_placeholder(matched_str) {
                    claims.push(self.extract_secret(
                        path_segments,
                        file,
                        line_num,
                        matched_str,
                        "secret_token",
                        "Secret or token is hardcoded in source",
                    ));
                }
            }
        }

        claims
    }
}

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

    #[test]
    fn test_api_key_detection() {
        let extractor = HardcodedSecretsExtractor::new();
        let content = r#"
            const API_KEY = "sk_live_1234567890abcdefghij";
        "#;

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

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

    #[test]
    fn test_aws_key_detection() {
        let extractor = HardcodedSecretsExtractor::new();
        let content = r#"
            aws_access_key = "AKIAIOSFODNN7EXAMPLE"
        "#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "config.py");

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

    #[test]
    fn test_private_key_detection() {
        let extractor = HardcodedSecretsExtractor::new();
        let content = r#"
            -----BEGIN RSA PRIVATE KEY-----
            MIIEowIBAAKCAQEA...
            -----END RSA PRIVATE KEY-----
        "#;

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

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

    #[test]
    fn test_excludes_placeholders() {
        let extractor = HardcodedSecretsExtractor::new();
        let content = r#"
            password = "changeme"
            api_key = "your_api_key_here"
            secret = "CHANGE_ME"
        "#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_lower_confidence_for_test_files() {
        let extractor = HardcodedSecretsExtractor::new();
        let content = r#"
            const API_KEY = "sk_live_1234567890abcdefghij";
        "#;

        let claims = extractor.extract(
            &["js".to_string()],
            content,
            Language::JavaScript,
            "src/__tests__/api.spec.js",
        );

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].confidence, 0.5);
    }

    #[test]
    fn test_yaml_unquoted_password() {
        let extractor = HardcodedSecretsExtractor::new();
        // Test with flat YAML (no nesting) - unquoted value
        let content = r#"password: super_secret_prod_password_123"#;
        let claims = extractor.extract(
            &["config".to_string()],
            content,
            Language::Yaml,
            "config/production.yaml",
        );

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

    #[test]
    fn test_yaml_stripe_secret_key() {
        let extractor = HardcodedSecretsExtractor::new();
        let content = r#"
api:
  stripe_secret_key: sk_live_51H7xyz123456789abcdef
"#;
        let claims = extractor.extract(
            &["config".to_string()],
            content,
            Language::Yaml,
            "config/production.yaml",
        );

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

    #[test]
    fn test_yaml_database_password() {
        let extractor = HardcodedSecretsExtractor::new();
        // Test database_password property name
        let content = r#"database_password: my_prod_db_password_2024"#;
        let claims =
            extractor.extract(&["config".to_string()], content, Language::Yaml, "config.yaml");

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