//! Server-Side Request Forgery (SSRF) vulnerability extractor.
//!
//! Detects patterns where HTTP requests are made with user-controlled URLs,
//! which can allow attackers to access internal services or exfiltrate data.

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

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

/// Extractor for SSRF vulnerabilities.
///
/// Detects patterns indicating unsafe URL handling in HTTP requests:
/// - HTTP clients (requests, fetch, axios, reqwest) with user-controlled URLs
/// - Webhook/callback URLs from user input
/// - Image/proxy URLs from request parameters
pub struct SsrfExtractor {
    // Python patterns
    python_requests: Regex,
    python_urllib: Regex,
    python_httpx: Regex,

    // JavaScript/TypeScript patterns
    js_fetch: Regex,
    js_axios: Regex,
    js_got: Regex,

    // Go patterns
    go_http: Regex,

    // Rust patterns
    rust_reqwest: Regex,

    // Common sink patterns (all languages)
    ssrf_sink: Regex,
}

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

impl SsrfExtractor {
    /// Create a new SSRF extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Python: requests with user URL
            python_requests: Regex::new(
                r#"requests\.(?:get|post|put|delete|head|patch|request)\s*\(\s*(?:url|uri|target|endpoint|request\.)"#,
            )
            .expect("valid regex"),
            python_urllib: Regex::new(
                r#"urllib\.request\.(?:urlopen|Request)\s*\(\s*(?:url|uri|request\.)"#,
            )
            .expect("valid regex"),
            python_httpx: Regex::new(
                r#"httpx\.(?:get|post|put|delete|AsyncClient)\s*\([^)]*(?:url|uri|request\.)"#,
            )
            .expect("valid regex"),

            // JavaScript: fetch with user URL
            js_fetch: Regex::new(
                r#"fetch\s*\(\s*(?:url|uri|target|endpoint|req\.(?:query|params|body)\.)"#,
            )
            .expect("valid regex"),
            js_axios: Regex::new(
                r#"axios\.(?:get|post|put|delete|request)\s*\(\s*(?:url|uri|target|endpoint)"#,
            )
            .expect("valid regex"),
            js_got: Regex::new(
                r#"got\s*\(\s*(?:url|uri|target|endpoint)"#,
            )
            .expect("valid regex"),

            // Go: http.Get with user URL
            go_http: Regex::new(
                r#"http\.(?:Get|Post|Head|PostForm|NewRequest)\s*\(\s*(?:url|uri|target|endpoint|r\.)"#,
            )
            .expect("valid regex"),

            // Rust: reqwest with user URL
            rust_reqwest: Regex::new(
                r#"reqwest::(?:get|Client).*\(\s*(?:url|&url|format!|user)"#,
            )
            .expect("valid regex"),

            // Common sink patterns - URLs that look user-controlled
            ssrf_sink: Regex::new(
                r#"(?i)(?:proxy_url|image_url|webhook_url|callback_url|redirect_url|target_url|remote_url|external_url)\s*[:=]\s*(?:request\.|params\.|req\.Query)"#,
            )
            .expect("valid regex"),
        }
    }

    fn make_claim(
        path_segments: &[String],
        file: &str,
        line: usize,
        matched: &str,
        category: &str,
        description: &str,
    ) -> ExtractedClaim {
        build_claim(
            path_segments,
            &["network", "ssrf", category],
            "request_url_source",
            ObjectValue::Text("user_controlled".to_string()),
            file,
            line,
            matched,
            0.85,
            description,
        )
    }
}

impl Extractor for SsrfExtractor {
    fn name(&self) -> &str {
        "ssrf"
    }

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

    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 for common sink patterns (all languages)
            if let Some(m) = self.ssrf_sink.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "url_sink",
                    "URL variable populated from user input (SSRF risk)",
                ));
            }

            // Language-specific patterns
            match language {
                Language::Python => {
                    if let Some(m) = self.python_requests.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "requests library with user-controlled URL (SSRF risk)",
                        ));
                    }
                    if let Some(m) = self.python_urllib.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "urllib with user-controlled URL (SSRF risk)",
                        ));
                    }
                    if let Some(m) = self.python_httpx.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "httpx with user-controlled URL (SSRF risk)",
                        ));
                    }
                }
                Language::JavaScript | Language::TypeScript => {
                    if let Some(m) = self.js_fetch.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "fetch with user-controlled URL (SSRF risk)",
                        ));
                    }
                    if let Some(m) = self.js_axios.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "axios with user-controlled URL (SSRF risk)",
                        ));
                    }
                    if let Some(m) = self.js_got.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "got with user-controlled URL (SSRF risk)",
                        ));
                    }
                }
                Language::Go => {
                    if let Some(m) = self.go_http.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "http.Get/Post with user-controlled URL (SSRF risk)",
                        ));
                    }
                }
                Language::Rust => {
                    if let Some(m) = self.rust_reqwest.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "http_client",
                            "reqwest with user-controlled URL (SSRF risk)",
                        ));
                    }
                }
                _ => {}
            }
        }

        claims
    }
}

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

    #[test]
    fn test_python_requests() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            response = requests.get(url)
        "#;

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

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

    #[test]
    fn test_python_requests_user_input() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            response = requests.post(request.json['webhook_url'], data=payload)
        "#;

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

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

    #[test]
    fn test_python_urllib() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            data = urllib.request.urlopen(url).read()
        "#;

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

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

    #[test]
    fn test_js_fetch() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            const data = await fetch(url);
        "#;

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

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

    #[test]
    fn test_js_axios() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            const response = await axios.get(url);
        "#;

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

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

    #[test]
    fn test_go_http() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            resp, err := http.Get(url)
        "#;

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

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

    #[test]
    fn test_rust_reqwest() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            let body = reqwest::get(url).await?.text().await?;
        "#;

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

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

    #[test]
    fn test_ssrf_sink_pattern() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            proxy_url = req.Query("proxy")
        "#;

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

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

    #[test]
    fn test_webhook_url_sink() {
        let extractor = SsrfExtractor::new();
        let content = r#"
            webhook_url = request.json.get('callback')
        "#;

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

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

    #[test]
    fn test_no_false_positives_hardcoded() {
        let extractor = SsrfExtractor::new();
        // Safe: hardcoded URL
        let content = r#"
            response = requests.get("https://api.example.com/data")
        "#;

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

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