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;

/// API fuzzing agent that tests for misconfigurations and information disclosure
pub struct ApiFuzzerAgent {
    http: reqwest::Client,
}

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

    /// Common API paths to probe
    fn discovery_paths(&self) -> Vec<(&str, &str)> {
        vec![
            ("/.env", "Environment file exposure"),
            ("/.git/config", "Git config exposure"),
            ("/api/swagger.json", "Swagger spec exposure"),
            ("/api/openapi.json", "OpenAPI spec exposure"),
            ("/api-docs", "API documentation exposure"),
            ("/graphql", "GraphQL endpoint"),
            ("/debug", "Debug endpoint"),
            ("/actuator/health", "Spring actuator"),
            ("/wp-config.php.bak", "WordPress config backup"),
            ("/.well-known/openid-configuration", "OIDC config"),
            ("/server-status", "Apache server status"),
            ("/phpinfo.php", "PHP info exposure"),
            ("/robots.txt", "Robots.txt"),
            ("/sitemap.xml", "Sitemap"),
            ("/.htaccess", "htaccess exposure"),
            ("/backup.sql", "SQL backup exposure"),
            ("/api/v1/users", "User enumeration endpoint"),
        ]
    }

    /// Patterns indicating sensitive information disclosure
    fn sensitive_patterns(&self) -> Vec<&str> {
        vec![
            "password",
            "api_key",
            "apikey",
            "secret",
            "token",
            "private_key",
            "aws_access_key",
            "jdbc:",
            "mongodb://",
            "redis://",
            "postgresql://",
        ]
    }
}

impl DastAgent for ApiFuzzerAgent {
    fn name(&self) -> &str {
        "api_fuzzer"
    }

    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());
        let base = target.base_url.trim_end_matches('/');

        // Phase 1: Path discovery
        for (path, description) in self.discovery_paths() {
            let url = format!("{base}{path}");
            let response = match self.http.get(&url).send().await {
                Ok(r) => r,
                Err(_) => continue,
            };

            let status = response.status().as_u16();
            if status == 200 {
                let body = response.text().await.unwrap_or_default();

                // Check if it's actually sensitive content (not just a 200 catch-all)
                let is_sensitive = !body.is_empty()
                    && body.len() > 10
                    && !body.contains("404")
                    && !body.contains("not found");

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

                    // Check for information disclosure
                    let body_lower = body.to_lowercase();
                    let has_secrets = self
                        .sensitive_patterns()
                        .iter()
                        .any(|p| body_lower.contains(p));

                    let severity = if has_secrets {
                        Severity::Critical
                    } else if path.contains(".env")
                        || path.contains(".git")
                        || path.contains("backup")
                    {
                        Severity::High
                    } else {
                        Severity::Medium
                    };

                    let evidence = DastEvidence {
                        request_method: "GET".to_string(),
                        request_url: url.clone(),
                        request_headers: None,
                        request_body: None,
                        response_status: status,
                        response_headers: None,
                        response_snippet: Some(snippet),
                        screenshot_path: None,
                        payload: None,
                        response_time_ms: None,
                    };

                    let vuln_type = if has_secrets {
                        DastVulnType::InformationDisclosure
                    } else {
                        DastVulnType::SecurityMisconfiguration
                    };

                    let mut finding = DastFinding::new(
                        String::new(),
                        target_id.clone(),
                        vuln_type,
                        format!("{description}: {path}"),
                        format!(
                            "Sensitive resource accessible at {url}. {}",
                            if has_secrets {
                                "Response contains potentially sensitive information."
                            } else {
                                "This resource should not be publicly accessible."
                            }
                        ),
                        severity,
                        url,
                        "GET".to_string(),
                    );
                    finding.exploitable = has_secrets;
                    finding.evidence = vec![evidence];
                    finding.cwe = Some(if has_secrets {
                        "CWE-200".to_string()
                    } else {
                        "CWE-16".to_string()
                    });

                    findings.push(finding);
                }
            }
        }

        // Phase 2: CORS misconfiguration check
        let cors_finding = self.check_cors(base, &target_id).await;
        if let Some(f) = cors_finding {
            findings.push(f);
        }

        // Phase 3: Check for verbose error responses
        let error_url = format!("{base}/nonexistent-path-{}", uuid::Uuid::new_v4());
        if let Ok(response) = self.http.get(&error_url).send().await {
            let body = response.text().await.unwrap_or_default();
            let body_lower = body.to_lowercase();

            let has_stack_trace = body_lower.contains("traceback")
                || body_lower.contains("stack trace")
                || body_lower.contains("at line")
                || body_lower.contains("exception in")
                || body_lower.contains("error in")
                || (body_lower.contains(".py") && body_lower.contains("line"));

            if has_stack_trace {
                let snippet = body.chars().take(500).collect::<String>();
                let evidence = DastEvidence {
                    request_method: "GET".to_string(),
                    request_url: error_url.clone(),
                    request_headers: None,
                    request_body: None,
                    response_status: 404,
                    response_headers: None,
                    response_snippet: Some(snippet),
                    screenshot_path: None,
                    payload: None,
                    response_time_ms: None,
                };

                let mut finding = DastFinding::new(
                    String::new(),
                    target_id.clone(),
                    DastVulnType::InformationDisclosure,
                    "Verbose error messages expose stack traces".to_string(),
                    "The application exposes detailed error information including stack traces. \
                     This can reveal internal paths, framework versions, and code structure."
                        .to_string(),
                    Severity::Low,
                    error_url,
                    "GET".to_string(),
                );
                finding.evidence = vec![evidence];
                finding.cwe = Some("CWE-209".to_string());
                finding.remediation = Some(
                    "Configure the application to use generic error pages in production. \
                     Do not expose stack traces or internal error details to end users."
                        .to_string(),
                );

                findings.push(finding);
            }
        }

        info!(findings = findings.len(), "API fuzzing scan complete");
        Ok(findings)
    }
}

impl ApiFuzzerAgent {
    async fn check_cors(&self, base_url: &str, target_id: &str) -> Option<DastFinding> {
        let response = self
            .http
            .get(base_url)
            .header("Origin", "https://evil.com")
            .send()
            .await
            .ok()?;

        let headers = response.headers();
        let acao = headers.get("access-control-allow-origin")?.to_str().ok()?;

        if acao == "*" || acao == "https://evil.com" {
            let acac = headers
                .get("access-control-allow-credentials")
                .and_then(|v| v.to_str().ok())
                .unwrap_or("false");

            // Wildcard CORS with credentials is the worst case
            let severity = if acac == "true" {
                Severity::High
            } else if acao == "*" {
                Severity::Medium
            } else {
                Severity::Low
            };

            let evidence = DastEvidence {
                request_method: "GET".to_string(),
                request_url: base_url.to_string(),
                request_headers: Some(
                    [("Origin".to_string(), "https://evil.com".to_string())]
                        .into_iter()
                        .collect(),
                ),
                request_body: None,
                response_status: response.status().as_u16(),
                response_headers: Some(
                    [("Access-Control-Allow-Origin".to_string(), acao.to_string())]
                        .into_iter()
                        .collect(),
                ),
                response_snippet: None,
                screenshot_path: None,
                payload: None,
                response_time_ms: None,
            };

            let mut finding = DastFinding::new(
                String::new(),
                target_id.to_string(),
                DastVulnType::SecurityMisconfiguration,
                "CORS misconfiguration allows arbitrary origins".to_string(),
                format!(
                    "The server responds with Access-Control-Allow-Origin: {acao} \
                     which may allow cross-origin attacks."
                ),
                severity,
                base_url.to_string(),
                "GET".to_string(),
            );
            finding.evidence = vec![evidence];
            finding.cwe = Some("CWE-942".to_string());
            finding.remediation = Some(
                "Configure CORS to only allow trusted origins. \
                 Never use wildcard (*) with credentials."
                    .to_string(),
            );

            Some(finding)
        } else {
            None
        }
    }
}
