//! High-entropy secrets extractor.
//!
//! Detects high-entropy strings that are likely leaked secrets by combining:
//! - Known secret prefixes (AKIA, sk_live_, ghp_, etc.) - high confidence
//! - Shannon entropy analysis for generic secrets in context
//!
//! This extractor catches real leaked keys that pattern-only detection misses,
//! while filtering out false positives like UUIDs and git SHAs.

mod entropy;
mod patterns;

use stemedb_core::types::ObjectValue;

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

use entropy::{charset_variety, shannon_entropy};
use patterns::{classify_known_secret, is_likely_not_secret, SecretPatterns};

/// Extractor for high-entropy secrets that pattern matching might miss.
///
/// Uses Shannon entropy combined with charset variety to detect secrets
/// with configurable thresholds to balance precision and recall.
pub struct HighEntropySecretsExtractor {
    /// Configuration for entropy thresholds.
    config: EntropyConfig,

    /// Compiled regex patterns for secret detection.
    patterns: SecretPatterns,
}

impl Default for HighEntropySecretsExtractor {
    fn default() -> Self {
        Self::new(&EntropyConfig::default())
    }
}

impl HighEntropySecretsExtractor {
    /// Create a new high-entropy secrets extractor with the given config.
    pub fn new(config: &EntropyConfig) -> Self {
        Self { config: config.clone(), patterns: SecretPatterns::new() }
    }

    /// Check if the string passes entropy thresholds.
    fn passes_entropy_check(&self, s: &str) -> bool {
        if s.len() < self.config.min_length || s.len() > self.config.max_length {
            return false;
        }

        if is_likely_not_secret(s) {
            return false;
        }

        let entropy = shannon_entropy(s);
        let variety = charset_variety(s);

        entropy >= self.config.min_entropy && variety >= self.config.min_charset_variety
    }

    fn make_claim(
        path_segments: &[String],
        file: &str,
        line: usize,
        matched_text: &str,
        secret_type: &str,
        description: &str,
        base_confidence: f32,
    ) -> ExtractedClaim {
        build_claim(
            path_segments,
            &["secrets", secret_type],
            "leaked_secret",
            ObjectValue::Text("high_entropy".to_string()),
            file,
            line,
            matched_text,
            base_confidence,
            description,
        )
    }
}

impl Extractor for HighEntropySecretsExtractor {
    fn name(&self) -> &str {
        "high_entropy_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;

            // Check known prefixes first (high confidence)
            if let Some(matched) = self.patterns.known_prefixes.find(line) {
                let matched_str = matched.as_str();

                let (secret_type, description) = classify_known_secret(matched_str);

                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    matched_str,
                    secret_type,
                    description,
                    1.0, // High confidence for known prefixes
                ));
            }

            // Check context patterns with entropy analysis
            for caps in self.patterns.secret_context.captures_iter(line) {
                if let Some(secret_match) = caps.get(1) {
                    let secret = secret_match.as_str();
                    if self.passes_entropy_check(secret) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            caps.get(0).map(|m| m.as_str()).unwrap_or(secret),
                            "high_entropy_secret",
                            "High-entropy string in secret context detected",
                            0.85, // Slightly lower confidence for entropy-based detection
                        ));
                    }
                }
            }

            // Check env var patterns
            for caps in self.patterns.env_var_secret.captures_iter(line) {
                if let Some(secret_match) = caps.get(2) {
                    let secret = secret_match.as_str();
                    if self.passes_entropy_check(secret) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            caps.get(0).map(|m| m.as_str()).unwrap_or(secret),
                            "env_var_secret",
                            "High-entropy secret in environment variable",
                            0.85,
                        ));
                    }
                }
            }
        }

        claims
    }
}

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

    fn extractor() -> HighEntropySecretsExtractor {
        HighEntropySecretsExtractor::new(&EntropyConfig::default())
    }

    #[test]
    fn test_aws_access_key_detected() {
        let ext = extractor();
        let content = r#"aws_access_key_id = "AKIAIOSFODNN7EXAMPLE""#;

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

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

    #[test]
    fn test_stripe_live_key_detected() {
        let ext = extractor();
        let content = r#"stripe_key: "sk_live_51H7xyzABCDEF1234567890abc""#;

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

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

    #[test]
    fn test_stripe_test_key_detected() {
        let ext = extractor();
        let content = r#"stripe_key = "sk_test_51H7xyzABCDEF1234567890abc""#;

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

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

    #[test]
    fn test_github_pat_detected() {
        let ext = extractor();
        let content = r#"GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"#;

        let claims = ext.extract(&["env".to_string()], content, Language::Dotenv, ".env");

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

    #[test]
    fn test_github_oauth_detected() {
        let ext = extractor();
        let content = r#"token: gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"#;

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

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

    #[test]
    fn test_gitlab_pat_detected() {
        let ext = extractor();
        let content = r#"gitlab_token = "glpat-xxxxxxxxxxxxxxxxxxxx""#;

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

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

    #[test]
    fn test_slack_token_detected() {
        let ext = extractor();
        let content = r#"SLACK_TOKEN=xoxb-123456789012-1234567890123-abcdefghij"#;

        let claims = ext.extract(&["env".to_string()], content, Language::Dotenv, ".env");

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

    #[test]
    fn test_high_entropy_in_context() {
        let ext = extractor();
        // Random base64-like string with high entropy
        let content = r#"api_key = "xK9mN2pQ7rS4tU8vW3xY6zA5bC1dE0fG""#;

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

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

    #[test]
    fn test_uuid_not_flagged() {
        let ext = extractor();
        let content = r#"session_id = "550e8400-e29b-41d4-a716-446655440000""#;

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

        // UUID should be excluded even in secret context
        assert!(claims.is_empty());
    }

    #[test]
    fn test_git_sha_not_flagged() {
        let ext = extractor();
        let content = r#"commit = "da39a3ee5e6b4b0d3255bfef95601890afd80709""#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_file_hash_not_flagged() {
        let ext = extractor();
        // SHA256 hash (64 hex chars)
        let content =
            r#"checksum = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855""#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_md5_hash_not_flagged() {
        let ext = extractor();
        // MD5 hash (32-char hex)
        let content = r#"checksum = "d41d8cd98f00b204e9800998ecf8427e""#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_low_entropy_not_flagged() {
        let ext = extractor();
        let content = r#"api_key = "password123456789012""#;

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

        // Low entropy string should not be flagged
        assert!(claims.is_empty());
    }

    #[test]
    fn test_test_file_lower_confidence() {
        let ext = extractor();
        let content = r#"stripe_key: "sk_live_51H7xyzABCDEF1234567890abc""#;

        let claims = ext.extract(
            &["test".to_string()],
            content,
            Language::Yaml,
            "tests/fixtures/config.yaml",
        );

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].confidence, 0.5); // 1.0 * 0.5 for test file
    }

    #[test]
    fn test_placeholder_not_flagged() {
        let ext = extractor();
        let content = r#"
            api_key = "your_api_key_here_example"
            secret = "placeholder_secret_changeme"
        "#;

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

        assert!(claims.is_empty());
    }
}
