//! Weak cryptography extractor.
//!
//! Detects usage of cryptographically weak algorithms (MD5, SHA1, DES, RC4, etc.)
//! that violate OWASP Cryptographic Failures guidelines.

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

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

/// Extractor for weak cryptographic algorithm usage.
///
/// Detects patterns indicating use of:
/// - MD5 hashing
/// - SHA1 hashing (deprecated for security use)
/// - DES encryption
/// - RC4 encryption
/// - Blowfish with small keys
pub struct WeakCryptoExtractor {
    // Rust patterns
    rust_md5: Regex,
    rust_sha1: Regex,
    rust_des: Regex,

    // Go patterns
    go_md5: Regex,
    go_sha1: Regex,
    go_des: Regex,
    go_rc4: Regex,

    // Python patterns
    python_md5: Regex,
    python_sha1: Regex,
    python_des: Regex,

    // JavaScript/Node patterns
    js_md5: Regex,
    js_sha1: Regex,
    js_des: Regex,
}

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

impl WeakCryptoExtractor {
    /// Create a new weak crypto extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Rust: md5 crate usage
            rust_md5: Regex::new(r"(?:Md5|md5)::(?:new|digest|hash)").expect("valid regex"),
            rust_sha1: Regex::new(r"(?:Sha1|sha1)::(?:new|digest|hash)").expect("valid regex"),
            rust_des: Regex::new(r"(?:Des|des|DES)::(?:new|encrypt|decrypt)").expect("valid regex"),

            // Go: crypto imports and usage
            go_md5: Regex::new(r"(?:md5\.New|md5\.Sum)").expect("valid regex"),
            go_sha1: Regex::new(r"(?:sha1\.New|sha1\.Sum)").expect("valid regex"),
            go_des: Regex::new(r"des\.NewCipher").expect("valid regex"),
            go_rc4: Regex::new(r"rc4\.NewCipher").expect("valid regex"),

            // Python: hashlib and Crypto usage
            python_md5: Regex::new(r"(?:hashlib\.md5|MD5\.new)").expect("valid regex"),
            python_sha1: Regex::new(r"(?:hashlib\.sha1|SHA\.new|SHA1\.new)").expect("valid regex"),
            python_des: Regex::new(r"(?:DES\.new|DES3\.new)").expect("valid regex"),

            // JavaScript/Node: crypto module usage
            js_md5: Regex::new(r#"createHash\s*\(\s*['"]md5['"]\s*\)"#).expect("valid regex"),
            js_sha1: Regex::new(r#"createHash\s*\(\s*['"]sha1['"]\s*\)"#).expect("valid regex"),
            js_des: Regex::new(r#"createCipher(?:iv)?\s*\(\s*['"]des[^'"]*['"]\s*"#)
                .expect("valid regex"),
        }
    }

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

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(matched) = pattern.find(line) {
                let mut concept_path = path_segments.to_vec();
                concept_path.push("crypto".to_string());
                concept_path.push("hashing".to_string());
                concept_path.push("algorithm".to_string());

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

        claims
    }

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

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(matched) = pattern.find(line) {
                let mut concept_path = path_segments.to_vec();
                concept_path.push("crypto".to_string());
                concept_path.push("encryption".to_string());
                concept_path.push("algorithm".to_string());

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

        claims
    }
}

impl Extractor for WeakCryptoExtractor {
    fn name(&self) -> &str {
        "weak_crypto"
    }

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

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        match language {
            Language::Rust => {
                // Hashing algorithms
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.rust_md5,
                    path_segments,
                    file,
                    "md5",
                    "MD5 hash algorithm is cryptographically broken",
                ));
                // Note: SHA1 detection is intentionally broad. It will trigger for
                // non-security uses (e.g., git commit hashes) which is expected behavior.
                // Users can filter these via concept path or suppress specific files.
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.rust_sha1,
                    path_segments,
                    file,
                    "sha1",
                    "SHA-1 is deprecated for cryptographic use",
                ));
                // Encryption algorithms use crypto/encryption path
                claims.extend(self.check_encryption_pattern(
                    content,
                    &self.rust_des,
                    path_segments,
                    file,
                    "des",
                    "DES encryption is cryptographically weak",
                ));
            }
            Language::Go => {
                // Hashing algorithms
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.go_md5,
                    path_segments,
                    file,
                    "md5",
                    "MD5 hash algorithm is cryptographically broken",
                ));
                // Note: SHA1 detection is intentionally broad. See Rust comment above.
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.go_sha1,
                    path_segments,
                    file,
                    "sha1",
                    "SHA-1 is deprecated for cryptographic use",
                ));
                // Encryption algorithms use crypto/encryption path
                claims.extend(self.check_encryption_pattern(
                    content,
                    &self.go_des,
                    path_segments,
                    file,
                    "des",
                    "DES encryption is cryptographically weak",
                ));
                claims.extend(self.check_encryption_pattern(
                    content,
                    &self.go_rc4,
                    path_segments,
                    file,
                    "rc4",
                    "RC4 stream cipher is cryptographically broken",
                ));
            }
            Language::Python => {
                // Hashing algorithms
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.python_md5,
                    path_segments,
                    file,
                    "md5",
                    "MD5 hash algorithm is cryptographically broken",
                ));
                // Note: SHA1 detection is intentionally broad. See Rust comment above.
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.python_sha1,
                    path_segments,
                    file,
                    "sha1",
                    "SHA-1 is deprecated for cryptographic use",
                ));
                // Encryption algorithms use crypto/encryption path
                claims.extend(self.check_encryption_pattern(
                    content,
                    &self.python_des,
                    path_segments,
                    file,
                    "des",
                    "DES encryption is cryptographically weak",
                ));
            }
            Language::TypeScript | Language::JavaScript => {
                // Hashing algorithms
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.js_md5,
                    path_segments,
                    file,
                    "md5",
                    "MD5 hash algorithm is cryptographically broken",
                ));
                // Note: SHA1 detection is intentionally broad. See Rust comment above.
                claims.extend(self.check_hash_pattern(
                    content,
                    &self.js_sha1,
                    path_segments,
                    file,
                    "sha1",
                    "SHA-1 is deprecated for cryptographic use",
                ));
                // Encryption algorithms use crypto/encryption path
                claims.extend(self.check_encryption_pattern(
                    content,
                    &self.js_des,
                    path_segments,
                    file,
                    "des",
                    "DES encryption is cryptographically weak",
                ));
            }
            _ => {}
        }

        claims
    }
}

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

    #[test]
    fn test_rust_md5_detection() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            use md5::Md5;
            let digest = Md5::new();
        "#;

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

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Text("md5".to_string()));
    }

    #[test]
    fn test_go_md5_detection() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            import "crypto/md5"

            func hash(data []byte) {
                h := md5.New()
                h.Write(data)
            }
        "#;

        let claims = extractor.extract(&["go".to_string()], content, Language::Go, "hash.go");

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Text("md5".to_string()));
    }

    #[test]
    fn test_python_sha1_detection() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            import hashlib

            def hash_password(password):
                return hashlib.sha1(password.encode()).hexdigest()
        "#;

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

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Text("sha1".to_string()));
    }

    #[test]
    fn test_js_md5_detection() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            const crypto = require('crypto');

            function hash(data) {
                return crypto.createHash('md5').update(data).digest('hex');
            }
        "#;

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

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Text("md5".to_string()));
    }

    #[test]
    fn test_no_false_positives_sha256() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            use sha2::Sha256;
            let digest = Sha256::new();
        "#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_go_rc4_detection() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            import "crypto/rc4"

            cipher, _ := rc4.NewCipher(key)
        "#;

        let claims = extractor.extract(&["go".to_string()], content, Language::Go, "crypto.go");

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Text("rc4".to_string()));
    }

    #[test]
    fn test_encryption_uses_correct_concept_path() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            import "crypto/des"
            import "crypto/rc4"

            cipher, _ := des.NewCipher(key)
            stream, _ := rc4.NewCipher(key)
        "#;

        let claims = extractor.extract(&["go".to_string()], content, Language::Go, "crypto.go");

        assert_eq!(claims.len(), 2);

        // DES and RC4 are encryption algorithms, should use crypto/encryption path
        for claim in &claims {
            assert!(
                claim.concept_path.contains("crypto/encryption/algorithm"),
                "Encryption algorithms should use crypto/encryption/algorithm path, got: {}",
                claim.concept_path
            );
        }
    }

    #[test]
    fn test_hashing_uses_correct_concept_path() {
        let extractor = WeakCryptoExtractor::new();
        let content = r#"
            import "crypto/md5"

            h := md5.New()
        "#;

        let claims = extractor.extract(&["go".to_string()], content, Language::Go, "hash.go");

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

        // MD5 is a hashing algorithm, should use crypto/hashing path
        assert!(
            claims[0].concept_path.contains("crypto/hashing/algorithm"),
            "Hashing algorithms should use crypto/hashing/algorithm path, got: {}",
            claims[0].concept_path
        );
    }

    /// SHA1 detection is intentionally broad and will trigger for non-security uses
    /// like git commit hashes. This is expected behavior - users can filter via
    /// concept path or file suppression if needed.
    #[test]
    fn test_sha1_triggers_broadly_including_non_security_contexts() {
        let extractor = WeakCryptoExtractor::new();
        // This might be used for git hashes, but we still flag it
        let content = r#"
            import "crypto/sha1"

            h := sha1.New()
            h.Write([]byte(data))
        "#;

        let claims = extractor.extract(&["go".to_string()], content, Language::Go, "git.go");

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Text("sha1".to_string()));
        assert!(claims[0].concept_path.contains("crypto/hashing/algorithm"));
    }
}
