use compliance_core::error::CoreError;
use compliance_core::models::dast::{DastEvidence, DastFinding, DastTarget, DastVulnType};
use compliance_core::models::Severity;
use compliance_core::traits::dast_agent::{DastAgent, DastContext};
use tracing::{info, warn};

/// SQL Injection testing agent
pub struct SqlInjectionAgent {
    http: reqwest::Client,
}

impl SqlInjectionAgent {
    pub fn new(http: reqwest::Client) -> Self {
        Self { http }
    }

    /// Test payloads for SQL injection detection
    fn payloads(&self) -> Vec<(&str, &str)> {
        vec![
            ("' OR '1'='1", "boolean-based blind"),
            ("1' AND SLEEP(2)-- -", "time-based blind"),
            ("' UNION SELECT NULL--", "union-based"),
            ("1; DROP TABLE test--", "stacked queries"),
            ("' OR 1=1#", "mysql boolean"),
            ("1' ORDER BY 1--", "order by probe"),
            ("') OR ('1'='1", "parenthesis bypass"),
        ]
    }

    /// Error patterns that indicate SQL injection
    fn error_patterns(&self) -> Vec<&str> {
        vec![
            "sql syntax",
            "mysql_fetch",
            "ORA-01756",
            "SQLite3::query",
            "pg_query",
            "unclosed quotation mark",
            "quoted string not properly terminated",
            "you have an error in your sql",
            "warning: mysql",
            "microsoft sql native client error",
            "postgresql query failed",
            "unterminated string",
            "syntax error at or near",
        ]
    }
}

impl DastAgent for SqlInjectionAgent {
    fn name(&self) -> &str {
        "sql_injection"
    }

    async fn run(
        &self,
        target: &DastTarget,
        context: &DastContext,
    ) -> Result<Vec<DastFinding>, CoreError> {
        let mut findings = Vec::new();
        let target_id = target
            .id
            .map(|oid| oid.to_hex())
            .unwrap_or_else(|| "unknown".to_string());

        for endpoint in &context.endpoints {
            // Only test endpoints with parameters
            if endpoint.parameters.is_empty() {
                continue;
            }

            for param in &endpoint.parameters {
                for (payload, technique) in self.payloads() {
                    // Build the request with the injection payload
                    let test_url = if endpoint.method == "GET" {
                        format!(
                            "{}?{}={}",
                            endpoint.url,
                            param.name,
                            urlencoding::encode(payload)
                        )
                    } else {
                        endpoint.url.clone()
                    };

                    let request = if endpoint.method == "POST" {
                        self.http
                            .post(&endpoint.url)
                            .form(&[(param.name.as_str(), payload)])
                    } else {
                        self.http.get(&test_url)
                    };

                    let response = match request.send().await {
                        Ok(r) => r,
                        Err(_) => continue,
                    };

                    let status = response.status().as_u16();
                    let headers: std::collections::HashMap<String, String> = response
                        .headers()
                        .iter()
                        .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
                        .collect();
                    let body = response.text().await.unwrap_or_default();

                    // Check for SQL error patterns in response
                    let body_lower = body.to_lowercase();
                    let is_vulnerable = self
                        .error_patterns()
                        .iter()
                        .any(|pattern| body_lower.contains(pattern));

                    if is_vulnerable {
                        let snippet = body.chars().take(500).collect::<String>();

                        let evidence = DastEvidence {
                            request_method: endpoint.method.clone(),
                            request_url: test_url.clone(),
                            request_headers: None,
                            request_body: if endpoint.method == "POST" {
                                Some(format!("{}={}", param.name, payload))
                            } else {
                                None
                            },
                            response_status: status,
                            response_headers: Some(headers),
                            response_snippet: Some(snippet),
                            screenshot_path: None,
                            payload: Some(payload.to_string()),
                            response_time_ms: None,
                        };

                        let mut finding = DastFinding::new(
                            String::new(), // scan_run_id set by orchestrator
                            target_id.clone(),
                            DastVulnType::SqlInjection,
                            format!("SQL Injection ({technique}) in parameter '{}'", param.name),
                            format!(
                                "SQL injection vulnerability detected in parameter '{}' at {} using {} technique. \
                                 The server returned SQL error messages in response to the injected payload.",
                                param.name, endpoint.url, technique
                            ),
                            Severity::Critical,
                            endpoint.url.clone(),
                            endpoint.method.clone(),
                        );
                        finding.parameter = Some(param.name.clone());
                        finding.exploitable = true;
                        finding.evidence = vec![evidence];
                        finding.cwe = Some("CWE-89".to_string());
                        finding.remediation = Some(
                            "Use parameterized queries or prepared statements. \
                             Never concatenate user input into SQL queries."
                                .to_string(),
                        );

                        findings.push(finding);

                        warn!(
                            endpoint = %endpoint.url,
                            param = %param.name,
                            technique,
                            "SQL injection found"
                        );

                        // Don't test more payloads for same param once confirmed
                        break;
                    }
                }
            }
        }

        info!(findings = findings.len(), "SQL injection scan complete");
        Ok(findings)
    }
}

/// URL-encode a string for query parameters
mod urlencoding {
    pub fn encode(input: &str) -> String {
        let mut encoded = String::new();
        for byte in input.bytes() {
            match byte {
                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                    encoded.push(byte as char);
                }
                _ => {
                    encoded.push_str(&format!("%{:02X}", byte));
                }
            }
        }
        encoded
    }
}
