//! Hardcoded secrets detector
//!
//! Identifies potential security vulnerabilities from hardcoded secrets:
//!
//! - Private keys (RSA, EC, PGP)
//! - AWS credentials
//! - API keys and tokens
//! - Passwords
//! - Database connection strings
//! - JWT tokens
//! - OAuth tokens (GitHub, GitLab, Slack, etc.)
//!
//! CWE-798: Use of Hard-coded Credentials

use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphClient;
use crate::models::{Finding, Severity};
use anyhow::Result;
use regex::Regex;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
use uuid::Uuid;

/// Secret pattern definition
struct SecretPattern {
    name: &'static str,
    pattern: Regex,
    secret_type: &'static str,
    description: &'static str,
    severity: Severity,
}

/// Default file patterns to exclude
const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[
    "tests/",
    "test_",
    "_test.py",
    "spec/",
    "mock",
    "fixture",
    "example",
    "doc/",
    "README",
    "CHANGELOG",
    ".md",
    ".rst",
    ".txt",
    ".lock",
    "node_modules/",
    "vendor/",
    ".git/",
];

/// Line patterns to skip (false positives)
const SKIP_LINE_PATTERNS: &[&str] = &[
    "example",
    "placeholder",
    "your_api_key",
    "process.env",
    "os.environ",
    "getenv",
    "TODO",
    "FIXME",
    "test",
    "dummy",
    "fake",
    "mock",
    "sample",
];

/// Value patterns that are obvious false positives
const FALSE_POSITIVE_PATTERNS: &[&str] = &[
    r"^[x\*]+$",
    r"^password$",
    r"^secret$",
    r"^changeme$",
    r"^replace_?me$",
    r"^your_",
    r"^\$\{",
    r"^ENV\[",
    r"^\{\{",
    r"^<[^>]+>$",
    r"^None$",
    r"^null$",
    r"^undefined$",
    r"^true$",
    r"^false$",
    r"^0{8,}$",
    r"^1{8,}$",
    r"^test",
    r"^demo",
    r"^abc",
];

/// Detects hardcoded secrets, API keys, passwords, and credentials
pub struct HardcodedSecretsDetector {
    config: DetectorConfig,
    repository_path: PathBuf,
    max_findings: usize,
    include_base64: bool,
    patterns: Vec<SecretPattern>,
    skip_file_patterns: Vec<Regex>,
    false_positive_patterns: Vec<Regex>,
}

impl HardcodedSecretsDetector {
    /// Create a new detector with default settings
    pub fn new() -> Self {
        Self::with_config(DetectorConfig::new(), PathBuf::from("."))
    }

    /// Create with custom repository path
    pub fn with_repository_path(repository_path: PathBuf) -> Self {
        Self::with_config(DetectorConfig::new(), repository_path)
    }

    /// Create with custom config and repository path
    pub fn with_config(config: DetectorConfig, repository_path: PathBuf) -> Self {
        let max_findings = config.get_option_or("max_findings", 100);
        let include_base64 = config.get_option_or("include_base64", false);

        // Compile secret patterns
        let patterns = Self::compile_patterns();

        // Compile skip patterns
        let skip_file_patterns: Vec<Regex> = DEFAULT_EXCLUDE_PATTERNS
            .iter()
            .filter_map(|p| Regex::new(&format!("(?i){}", regex::escape(p))).ok())
            .collect();

        // Compile false positive patterns
        let false_positive_patterns: Vec<Regex> = FALSE_POSITIVE_PATTERNS
            .iter()
            .filter_map(|p| Regex::new(&format!("(?i){}", p)).ok())
            .collect();

        Self {
            config,
            repository_path,
            max_findings,
            include_base64,
            patterns,
            skip_file_patterns,
            false_positive_patterns,
        }
    }

    /// Compile all secret detection patterns
    fn compile_patterns() -> Vec<SecretPattern> {
        vec![
            // Private keys (very high confidence)
            SecretPattern {
                name: "rsa_private_key",
                pattern: Regex::new(r"(?i)-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----").unwrap(),
                secret_type: "private_key",
                description: "RSA/EC/DSA private key detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "pgp_private_key",
                pattern: Regex::new(r"(?i)-----BEGIN PGP PRIVATE KEY BLOCK-----").unwrap(),
                secret_type: "private_key",
                description: "PGP private key detected",
                severity: Severity::Critical,
            },
            // AWS credentials
            SecretPattern {
                name: "aws_access_key_id",
                pattern: Regex::new(r"(AKIA[0-9A-Z]{16})").unwrap(),
                secret_type: "aws_credentials",
                description: "AWS Access Key ID detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "aws_secret_key",
                pattern: Regex::new(r#"(?i)(?:aws_secret(?:_access)?_key|secret_key)\s*[=:]+\s*['"]?([A-Za-z0-9/+=]{40})['"]?"#).unwrap(),
                secret_type: "aws_credentials",
                description: "AWS Secret Access Key detected",
                severity: Severity::Critical,
            },
            // Generic API keys
            SecretPattern {
                name: "api_key_assignment",
                pattern: Regex::new(r#"(?i)(?:api[_-]?key|apikey|api_token|auth_token|access_token)\s*[=:]+\s*['"]([a-zA-Z0-9_\-]{20,})['"]"#).unwrap(),
                secret_type: "api_key",
                description: "API key assignment detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "bearer_token",
                pattern: Regex::new(r#"(?i)['"]Bearer\s+([a-zA-Z0-9_\-\.]{20,})['"]"#).unwrap(),
                secret_type: "api_key",
                description: "Bearer token detected",
                severity: Severity::Critical,
            },
            // Passwords
            SecretPattern {
                name: "password_assignment",
                pattern: Regex::new(r#"(?i)(?:password|passwd|pwd|pass|secret)\s*[=:]+\s*['"]([^'"]{8,})['"]"#).unwrap(),
                secret_type: "password",
                description: "Password assignment detected",
                severity: Severity::High,
            },
            SecretPattern {
                name: "db_password",
                pattern: Regex::new(r#"(?i)(?:db_password|database_password|mysql_pwd|postgres_password)\s*[=:]+\s*['"]([^'"]+)['"]"#).unwrap(),
                secret_type: "password",
                description: "Database password detected",
                severity: Severity::High,
            },
            // Connection strings
            SecretPattern {
                name: "mongodb_uri",
                pattern: Regex::new(r"(?i)mongodb(?:\+srv)?://[^:]+:([^@]+)@[^\s'\"]+").unwrap(),
                secret_type: "connection_string",
                description: "MongoDB connection string with password detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "postgres_uri",
                pattern: Regex::new(r"(?i)postgres(?:ql)?://[^:]+:([^@]+)@[^\s'\"]+").unwrap(),
                secret_type: "connection_string",
                description: "PostgreSQL connection string with password detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "mysql_uri",
                pattern: Regex::new(r"(?i)mysql://[^:]+:([^@]+)@[^\s'\"]+").unwrap(),
                secret_type: "connection_string",
                description: "MySQL connection string with password detected",
                severity: Severity::Critical,
            },
            // JWT tokens
            SecretPattern {
                name: "jwt_token",
                pattern: Regex::new(r#"['"]?(eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,})['"]?"#).unwrap(),
                secret_type: "jwt_token",
                description: "JWT token detected",
                severity: Severity::Critical,
            },
            // OAuth tokens
            SecretPattern {
                name: "github_token",
                pattern: Regex::new(r"(ghp_[a-zA-Z0-9]{36})").unwrap(),
                secret_type: "oauth_token",
                description: "GitHub personal access token detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "github_oauth",
                pattern: Regex::new(r"(gho_[a-zA-Z0-9]{36})").unwrap(),
                secret_type: "oauth_token",
                description: "GitHub OAuth token detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "gitlab_token",
                pattern: Regex::new(r#"(?i)(?:gitlab[_-]?token|gl[_-]?token)\s*[=:]+\s*['""]?(glpat-[a-zA-Z0-9_\-]{20,})['""]?"#).unwrap(),
                secret_type: "oauth_token",
                description: "GitLab personal access token detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "slack_token",
                pattern: Regex::new(r"xox[baprs]-[0-9]{10,}-[a-zA-Z0-9]{10,}").unwrap(),
                secret_type: "oauth_token",
                description: "Slack token detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "stripe_key",
                pattern: Regex::new(r"(?:sk|pk|rk)_(?:live|test)_[a-zA-Z0-9]{20,}").unwrap(),
                secret_type: "api_key",
                description: "Stripe API key detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "sendgrid_key",
                pattern: Regex::new(r"(SG\.[a-zA-Z0-9_\-]{20,}\.[a-zA-Z0-9_\-]{20,})").unwrap(),
                secret_type: "api_key",
                description: "SendGrid API key detected",
                severity: Severity::Critical,
            },
            SecretPattern {
                name: "twilio_key",
                pattern: Regex::new(r"SK[a-f0-9]{32}").unwrap(),
                secret_type: "api_key",
                description: "Twilio API key detected",
                severity: Severity::Critical,
            },
            // Generic secrets
            SecretPattern {
                name: "generic_secret",
                pattern: Regex::new(r#"(?i)(?:secret|private|credential|cred)\s*[=:]+\s*['"]([a-zA-Z0-9_\-]{16,})['"]"#).unwrap(),
                secret_type: "generic_secret",
                description: "Generic secret assignment detected",
                severity: Severity::High,
            },
        ]
    }

    /// Check if file should be skipped
    fn should_skip_file(&self, path: &str) -> bool {
        self.skip_file_patterns.iter().any(|p| p.is_match(path))
    }

    /// Check if line should be skipped
    fn should_skip_line(&self, line: &str) -> bool {
        let line_lower = line.to_lowercase();
        SKIP_LINE_PATTERNS.iter().any(|p| line_lower.contains(p))
    }

    /// Check if matched value is likely a false positive
    fn is_false_positive(&self, matched_value: &str, full_context: &str) -> bool {
        if matched_value.is_empty() || matched_value.len() < 8 {
            return true;
        }

        // Check value against false positive patterns
        for pattern in &self.false_positive_patterns {
            if pattern.is_match(matched_value) {
                return true;
            }
        }

        // Check for low entropy (repeated characters)
        let unique_chars: HashSet<char> = matched_value.chars().collect();
        if unique_chars.len() < 4 {
            return true;
        }

        // Check for obvious test patterns in context
        let context_lower = full_context.to_lowercase();
        if ["example", "sample", "test", "demo", "placeholder", "mock"]
            .iter()
            .any(|w| context_lower.contains(w))
        {
            return true;
        }

        false
    }

    /// Scan source files for secrets
    fn scan_source_files(&self) -> Vec<Finding> {
        let mut findings = Vec::new();
        let mut seen_secrets: HashSet<String> = HashSet::new();

        if !self.repository_path.exists() {
            return findings;
        }

        // File extensions to scan
        let extensions: HashSet<&str> = [
            "py", "js", "ts", "jsx", "tsx", "java", "go", "rb", "php",
            "yml", "yaml", "json", "env", "sh", "bash"
        ].into_iter().collect();

        // Walk through files
        for entry in walkdir::WalkDir::new(&self.repository_path)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().is_file())
        {
            let path = entry.path();
            
            // Check extension
            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
            if !extensions.contains(ext) {
                continue;
            }

            let rel_path = path
                .strip_prefix(&self.repository_path)
                .unwrap_or(path)
                .to_string_lossy()
                .to_string();

            if self.should_skip_file(&rel_path) {
                continue;
            }

            let content = match std::fs::read_to_string(path) {
                Ok(c) => c,
                Err(_) => continue,
            };

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

                if self.should_skip_line(line) {
                    continue;
                }

                for pattern in &self.patterns {
                    // Skip base64 unless configured
                    if pattern.secret_type == "base64_secret" && !self.include_base64 {
                        continue;
                    }

                    if let Some(caps) = pattern.pattern.captures(line) {
                        let matched_value = caps.get(1)
                            .or_else(|| caps.get(0))
                            .map(|m| m.as_str())
                            .unwrap_or("");

                        if self.is_false_positive(matched_value, line) {
                            continue;
                        }

                        // Dedupe by secret value hash (first 20 chars)
                        let secret_hash = matched_value[..matched_value.len().min(20)].to_string();
                        if seen_secrets.contains(&secret_hash) {
                            continue;
                        }
                        seen_secrets.insert(secret_hash);

                        findings.push(self.create_finding(
                            &rel_path,
                            line_num,
                            pattern,
                            matched_value,
                        ));

                        if findings.len() >= self.max_findings {
                            return findings;
                        }
                        break; // One finding per line
                    }
                }
            }
        }

        // Sort by severity
        findings.sort_by(|a, b| b.severity.cmp(&a.severity));

        findings
    }

    /// Create a finding for a detected secret
    fn create_finding(
        &self,
        file_path: &str,
        line_start: u32,
        pattern: &SecretPattern,
        matched_value: &str,
    ) -> Finding {
        // Mask the secret value
        let masked = if matched_value.len() > 10 {
            format!(
                "{}{}{}",
                &matched_value[..4],
                "*".repeat(matched_value.len() - 8),
                &matched_value[matched_value.len()-4..]
            )
        } else {
            "*".repeat(matched_value.len())
        };

        let title = format!(
            "Hardcoded {}: {}:{}",
            pattern.secret_type.replace('_', " "),
            file_path,
            line_start
        );

        let description = format!(
            "{}.\n\n\
             **Location:** `{}` (line {})\n\
             **Pattern:** `{}`\n\
             **Masked value:** `{}`\n\n\
             Hardcoded secrets in source code pose severe security risks:\n\
             - Secrets may be exposed in version control history\n\
             - Credentials can be extracted from compiled binaries\n\
             - Rotation requires code changes and redeployment",
            pattern.description, file_path, line_start, pattern.name, masked
        );

        let suggested_fix = "1. **Remove the secret immediately** from the source code\n\
             2. **Rotate the credential** - assume it has been compromised\n\
             3. **Use environment variables** or a secrets manager:\n\
                - `os.environ['API_KEY']` (Python)\n\
                - `process.env.API_KEY` (Node.js)\n\
                - AWS Secrets Manager, HashiCorp Vault, etc.\n\
             4. **Scan git history** for previous commits containing secrets\n\
             5. Consider using tools like `git-secrets` or `trufflehog` in CI/CD";

        Finding {
            id: Uuid::new_v4().to_string(),
            detector: "HardcodedSecretsDetector".to_string(),
            severity: pattern.severity,
            title,
            description,
            affected_files: vec![PathBuf::from(file_path)],
            line_start: Some(line_start),
            line_end: Some(line_start),
            suggested_fix: Some(suggested_fix.to_string()),
            estimated_effort: Some("Immediate (security critical)".to_string()),
            category: Some("security".to_string()),
            cwe_id: Some("CWE-798".to_string()),
            why_it_matters: Some(
                "Hardcoded credentials can be easily extracted from source code, leading to \
                 unauthorized access, data breaches, and compliance violations."
                    .to_string(),
            ),
        }
    }
}

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

impl Detector for HardcodedSecretsDetector {
    fn name(&self) -> &'static str {
        "HardcodedSecretsDetector"
    }

    fn description(&self) -> &'static str {
        "Detects hardcoded secrets, API keys, passwords, and credentials"
    }

    fn category(&self) -> &'static str {
        "security"
    }

    fn config(&self) -> Option<&DetectorConfig> {
        Some(&self.config)
    }

    fn detect(&self, _graph: &GraphClient) -> Result<Vec<Finding>> {
        debug!("Starting hardcoded secrets detection");

        let findings = self.scan_source_files();

        info!("HardcodedSecretsDetector found {} potential secrets", findings.len());

        Ok(findings)
    }
}

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

    #[test]
    fn test_aws_key_detection() {
        let detector = HardcodedSecretsDetector::new();
        let patterns = &detector.patterns;
        
        let aws_pattern = patterns.iter().find(|p| p.name == "aws_access_key_id").unwrap();
        assert!(aws_pattern.pattern.is_match("AKIAIOSFODNN7EXAMPLE"));
    }

    #[test]
    fn test_github_token_detection() {
        let detector = HardcodedSecretsDetector::new();
        let patterns = &detector.patterns;
        
        let gh_pattern = patterns.iter().find(|p| p.name == "github_token").unwrap();
        assert!(gh_pattern.pattern.is_match("ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
    }

    #[test]
    fn test_false_positive_detection() {
        let detector = HardcodedSecretsDetector::new();
        
        assert!(detector.is_false_positive("password", "password = 'password'"));
        assert!(detector.is_false_positive("changeme", "secret = 'changeme'"));
        assert!(detector.is_false_positive("xxxxxxxx", "api_key = 'xxxxxxxx'"));
        assert!(detector.is_false_positive("test123", "# This is a test key"));
    }

    #[test]
    fn test_skip_patterns() {
        let detector = HardcodedSecretsDetector::new();
        
        assert!(detector.should_skip_file("tests/test_auth.py"));
        assert!(detector.should_skip_file("node_modules/package/index.js"));
        assert!(!detector.should_skip_file("src/auth.py"));
    }
}
