//! Insecure deserialization vulnerability extractor.
//!
//! Detects patterns where untrusted data is deserialized using unsafe methods,
//! which can lead to remote code execution vulnerabilities.

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

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

/// Extractor for insecure deserialization vulnerabilities.
///
/// Detects patterns indicating unsafe deserialization:
/// - Python pickle (critical - RCE)
/// - Python yaml.load without SafeLoader
/// - Python marshal
/// - Python eval/exec with user input
/// - JavaScript node-serialize
/// - Go gob without validation
/// - Java ObjectInputStream patterns
pub struct InsecureDeserializationExtractor {
    // Python patterns (critical)
    python_pickle: Regex,
    python_yaml_unsafe: Regex,
    python_marshal: Regex,
    python_eval: Regex,

    // JavaScript patterns
    js_serialize: Regex,

    // Go patterns
    go_gob: Regex,

    // Java-style patterns (polyglot detection)
    java_ois: Regex,
}

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

impl InsecureDeserializationExtractor {
    /// Create a new insecure deserialization extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Python: pickle (critical - allows arbitrary code execution)
            python_pickle: Regex::new(r#"pickle\.(?:loads?|Unpickler)\s*\("#).expect("valid regex"),

            // Python: yaml.load without SafeLoader (yaml.safe_load is OK)
            // Matches yaml.load( but not yaml.safe_load(
            python_yaml_unsafe: Regex::new(r#"yaml\.(?:load|unsafe_load)\s*\([^)]*(?:\)|,[^S])"#)
                .expect("valid regex"),

            // Python: marshal (unsafe, similar to pickle)
            python_marshal: Regex::new(r#"marshal\.loads?\s*\("#).expect("valid regex"),

            // Python: eval/exec with user input
            python_eval: Regex::new(
                r#"(?:eval|exec)\s*\(\s*(?:request\.|params\.|input|user|data)"#,
            )
            .expect("valid regex"),

            // JavaScript: node-serialize (known vulnerable)
            js_serialize: Regex::new(
                r#"(?:require\s*\(\s*["']node-serialize["']\)|\.unserialize\s*\()"#,
            )
            .expect("valid regex"),

            // Go: gob decoder (can be unsafe with untrusted input)
            go_gob: Regex::new(r#"gob\.(?:NewDecoder|Decode)\s*\("#).expect("valid regex"),

            // Java-style patterns (for polyglot detection in config files, etc.)
            java_ois: Regex::new(r#"ObjectInputStream|readObject\s*\(\)"#).expect("valid regex"),
        }
    }

    fn make_claim(
        path_segments: &[String],
        file: &str,
        line: usize,
        matched: &str,
        method: &str,
        confidence: f32,
        description: &str,
    ) -> ExtractedClaim {
        build_claim(
            path_segments,
            &["serialization", "deserialization"],
            "deserialize_method",
            ObjectValue::Text(method.to_string()),
            file,
            line,
            matched,
            confidence,
            description,
        )
    }
}

impl Extractor for InsecureDeserializationExtractor {
    fn name(&self) -> &str {
        "insecure_deserialization"
    }

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

    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;

            match language {
                Language::Python => {
                    // Pickle (critical - RCE)
                    if let Some(m) = self.python_pickle.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "pickle",
                            0.95,
                            "pickle deserialization allows arbitrary code execution (CRITICAL)",
                        ));
                    }

                    // yaml.load without SafeLoader
                    if let Some(m) = self.python_yaml_unsafe.find(line) {
                        // Check if SafeLoader is used on the same line
                        if !line.contains("SafeLoader") && !line.contains("safe_load") {
                            claims.push(Self::make_claim(
                                path_segments,
                                file,
                                line_num,
                                m.as_str(),
                                "yaml_unsafe",
                                0.85,
                                "yaml.load without SafeLoader allows code execution",
                            ));
                        }
                    }

                    // marshal
                    if let Some(m) = self.python_marshal.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "marshal",
                            0.9,
                            "marshal deserialization is unsafe with untrusted data",
                        ));
                    }

                    // eval/exec with user input
                    if let Some(m) = self.python_eval.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "eval",
                            0.95,
                            "eval/exec with user input allows arbitrary code execution (CRITICAL)",
                        ));
                    }
                }
                Language::JavaScript | Language::TypeScript => {
                    // node-serialize (known vulnerable)
                    if let Some(m) = self.js_serialize.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "node_serialize",
                            0.95,
                            "node-serialize is vulnerable to remote code execution (CVE-2017-5941)",
                        ));
                    }
                }
                Language::Go => {
                    // gob decoder
                    if let Some(m) = self.go_gob.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "gob",
                            0.75, // Lower confidence - gob is safer but can still be problematic
                            "gob deserialization with untrusted input may be unsafe",
                        ));
                    }
                }
                _ => {}
            }

            // Check for Java patterns in any language (polyglot detection)
            if let Some(m) = self.java_ois.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "java_ois",
                    0.85,
                    "Java ObjectInputStream deserialization is vulnerable to RCE",
                ));
            }
        }

        claims
    }
}

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

    #[test]
    fn test_python_pickle() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            data = pickle.loads(request.data)
        "#;

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

        assert_eq!(claims.len(), 1);
        assert!(claims[0].description.contains("CRITICAL"));
        assert!(claims[0].confidence >= 0.9);
    }

    #[test]
    fn test_python_pickle_load() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            with open('data.pkl', 'rb') as f:
                obj = pickle.load(f)
        "#;

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

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

    #[test]
    fn test_python_yaml_unsafe() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            data = yaml.load(file_content)
        "#;

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

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

    #[test]
    fn test_python_yaml_safe_no_detection() {
        let extractor = InsecureDeserializationExtractor::new();
        // Safe: using SafeLoader
        let content = r#"
            data = yaml.load(file_content, Loader=yaml.SafeLoader)
        "#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_python_marshal() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            obj = marshal.loads(data)
        "#;

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

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

    #[test]
    fn test_python_eval() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            result = eval(request.data)
        "#;

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

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

    #[test]
    fn test_js_node_serialize_require() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            const s = require('node-serialize');
        "#;

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

        assert_eq!(claims.len(), 1); // require pattern
    }

    #[test]
    fn test_js_unserialize() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            const obj = s.unserialize(data);
        "#;

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

        assert_eq!(claims.len(), 1); // unserialize pattern
    }

    #[test]
    fn test_go_gob() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            dec := gob.NewDecoder(reader)
        "#;

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

        assert_eq!(claims.len(), 1); // NewDecoder
    }

    #[test]
    fn test_go_gob_decode() {
        let extractor = InsecureDeserializationExtractor::new();
        let content = r#"
            err := gob.Decode(&data)
        "#;

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

        assert_eq!(claims.len(), 1); // Decode
    }

    #[test]
    fn test_java_ois_polyglot() {
        let extractor = InsecureDeserializationExtractor::new();
        // Java pattern detected in any language
        let content = r#"
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            Object obj = ois.readObject();
        "#;

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

        assert_eq!(claims.len(), 2); // ObjectInputStream and readObject
    }
}
