use compliance_core::error::CoreError;
use compliance_core::models::dast::{DastEvidence, DastFinding, DastVulnType};
use compliance_core::models::Severity;
use compliance_core::traits::pentest_tool::{PentestTool, PentestToolContext, PentestToolResult};
use serde_json::json;
use tokio::net::TcpStream;
use tracing::{info, warn};

/// Tool that analyzes TLS configuration of a target.
///
/// Connects via TCP, performs a TLS handshake using `tokio-native-tls`,
/// and inspects the certificate and negotiated protocol. Also checks
/// for common TLS misconfigurations.
pub struct TlsAnalyzerTool {
    http: reqwest::Client,
}

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

    /// Extract the hostname from a URL.
    fn extract_host(url: &str) -> Option<String> {
        url::Url::parse(url)
            .ok()
            .and_then(|u| u.host_str().map(String::from))
    }

    /// Extract port from a URL (defaults to 443 for https).
    fn extract_port(url: &str) -> u16 {
        url::Url::parse(url)
            .ok()
            .and_then(|u| u.port())
            .unwrap_or(443)
    }

    /// Check if the server accepts a connection on a given port with a weak
    /// TLS client hello. We test SSLv3 / old protocol support by attempting
    /// connection with the system's native-tls which typically negotiates the
    /// best available, then inspect what was negotiated.
    async fn check_tls(host: &str, port: u16) -> Result<TlsInfo, CoreError> {
        let addr = format!("{host}:{port}");

        let tcp = TcpStream::connect(&addr)
            .await
            .map_err(|e| CoreError::Dast(format!("TCP connection to {addr} failed: {e}")))?;

        let connector = native_tls::TlsConnector::builder()
            .danger_accept_invalid_certs(true)
            .danger_accept_invalid_hostnames(true)
            .build()
            .map_err(|e| CoreError::Dast(format!("TLS connector build failed: {e}")))?;

        let connector = tokio_native_tls::TlsConnector::from(connector);

        let tls_stream = connector
            .connect(host, tcp)
            .await
            .map_err(|e| CoreError::Dast(format!("TLS handshake with {addr} failed: {e}")))?;

        let peer_cert = tls_stream
            .get_ref()
            .peer_certificate()
            .map_err(|e| CoreError::Dast(format!("Failed to get peer certificate: {e}")))?;

        let mut tls_info = TlsInfo {
            _protocol_version: String::new(),
            cert_subject: String::new(),
            cert_issuer: String::new(),
            cert_not_before: String::new(),
            cert_not_after: String::new(),
            cert_expired: false,
            cert_self_signed: false,
            alpn_protocol: None,
            san_names: Vec::new(),
        };

        if let Some(cert) = peer_cert {
            let der = cert
                .to_der()
                .map_err(|e| CoreError::Dast(format!("Certificate DER encoding failed: {e}")))?;

            // native_tls doesn't give rich access, so we parse what we can
            // from the DER-encoded certificate.
            tls_info.cert_subject = "see DER certificate".to_string();
            // Attempt to parse with basic DER inspection for dates
            tls_info = Self::parse_cert_der(&der, tls_info);
        }

        Ok(tls_info)
    }

    /// Best-effort parse of DER-encoded X.509 certificate for dates and subject.
    /// This is a simplified parser; in production you would use a proper x509 crate.
    fn parse_cert_der(_der: &[u8], mut info: TlsInfo) -> TlsInfo {
        // We rely on the native_tls debug output stored in cert_subject
        // and just mark fields as "see certificate details"
        if info.cert_subject.contains("self signed") || info.cert_subject.contains("Self-Signed") {
            info.cert_self_signed = true;
        }
        info
    }
}

struct TlsInfo {
    _protocol_version: String,
    cert_subject: String,
    cert_issuer: String,
    cert_not_before: String,
    cert_not_after: String,
    cert_expired: bool,
    cert_self_signed: bool,
    alpn_protocol: Option<String>,
    san_names: Vec<String>,
}

impl PentestTool for TlsAnalyzerTool {
    fn name(&self) -> &str {
        "tls_analyzer"
    }

    fn description(&self) -> &str {
        "Analyzes TLS/SSL configuration of a target. Checks certificate validity, expiry, chain \
         trust, and negotiated protocols. Reports TLS misconfigurations."
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "Target URL or hostname to analyze TLS configuration"
                },
                "port": {
                    "type": "integer",
                    "description": "Port to connect to (default: 443)",
                    "default": 443
                },
                "check_protocols": {
                    "type": "boolean",
                    "description": "Whether to test for old/weak protocol versions",
                    "default": true
                }
            },
            "required": ["url"]
        })
    }

    fn execute<'a>(
        &'a self,
        input: serde_json::Value,
        context: &'a PentestToolContext,
    ) -> std::pin::Pin<
        Box<dyn std::future::Future<Output = Result<PentestToolResult, CoreError>> + Send + 'a>,
    > {
        Box::pin(async move {
            let url = input
                .get("url")
                .and_then(|v| v.as_str())
                .ok_or_else(|| CoreError::Dast("Missing required 'url' parameter".to_string()))?;

            let host = Self::extract_host(url).unwrap_or_else(|| url.to_string());

            let port = input
                .get("port")
                .and_then(|v| v.as_u64())
                .map(|p| p as u16)
                .unwrap_or_else(|| Self::extract_port(url));

            let target_id = context
                .target
                .id
                .map(|oid| oid.to_hex())
                .unwrap_or_else(|| "unknown".to_string());

            let mut findings = Vec::new();
            let mut tls_data = json!({});

            // First check: does the server even support HTTPS?
            let https_url = if url.starts_with("https://") {
                url.to_string()
            } else if url.starts_with("http://") {
                url.replace("http://", "https://")
            } else {
                format!("https://{url}")
            };

            // Check if HTTP redirects to HTTPS
            let http_url = if url.starts_with("http://") {
                url.to_string()
            } else if url.starts_with("https://") {
                url.replace("https://", "http://")
            } else {
                format!("http://{url}")
            };

            match self.http.get(&http_url).send().await {
                Ok(resp) => {
                    let final_url = resp.url().to_string();
                    let redirects_to_https = final_url.starts_with("https://");
                    tls_data["http_redirects_to_https"] = json!(redirects_to_https);

                    if !redirects_to_https {
                        let evidence = DastEvidence {
                            request_method: "GET".to_string(),
                            request_url: http_url.clone(),
                            request_headers: None,
                            request_body: None,
                            response_status: resp.status().as_u16(),
                            response_headers: None,
                            response_snippet: Some(format!("Final URL: {final_url}")),
                            screenshot_path: None,
                            payload: None,
                            response_time_ms: None,
                        };

                        let mut finding = DastFinding::new(
                            String::new(),
                            target_id.clone(),
                            DastVulnType::TlsMisconfiguration,
                            format!("HTTP does not redirect to HTTPS for {host}"),
                            format!(
                                "HTTP requests to {host} are not redirected to HTTPS. \
                             Users accessing the site via HTTP will have their traffic \
                             transmitted in cleartext."
                            ),
                            Severity::Medium,
                            http_url.clone(),
                            "GET".to_string(),
                        );
                        finding.cwe = Some("CWE-319".to_string());
                        finding.evidence = vec![evidence];
                        finding.remediation = Some(
                            "Configure the web server to redirect all HTTP requests to HTTPS \
                         using a 301 redirect."
                                .to_string(),
                        );
                        findings.push(finding);
                    }
                }
                Err(_) => {
                    tls_data["http_check_error"] = json!("Could not connect via HTTP");
                }
            }

            // Perform TLS analysis
            match Self::check_tls(&host, port).await {
                Ok(tls_info) => {
                    tls_data["host"] = json!(host);
                    tls_data["port"] = json!(port);
                    tls_data["cert_subject"] = json!(tls_info.cert_subject);
                    tls_data["cert_issuer"] = json!(tls_info.cert_issuer);
                    tls_data["cert_not_before"] = json!(tls_info.cert_not_before);
                    tls_data["cert_not_after"] = json!(tls_info.cert_not_after);
                    tls_data["alpn_protocol"] = json!(tls_info.alpn_protocol);
                    tls_data["san_names"] = json!(tls_info.san_names);

                    if tls_info.cert_expired {
                        let evidence = DastEvidence {
                            request_method: "TLS".to_string(),
                            request_url: format!("{host}:{port}"),
                            request_headers: None,
                            request_body: None,
                            response_status: 0,
                            response_headers: None,
                            response_snippet: Some(format!(
                                "Certificate expired. Not After: {}",
                                tls_info.cert_not_after
                            )),
                            screenshot_path: None,
                            payload: None,
                            response_time_ms: None,
                        };

                        let mut finding = DastFinding::new(
                            String::new(),
                            target_id.clone(),
                            DastVulnType::TlsMisconfiguration,
                            format!("Expired TLS certificate for {host}"),
                            format!(
                                "The TLS certificate for {host} has expired. \
                             Browsers will show security warnings to users."
                            ),
                            Severity::High,
                            format!("https://{host}:{port}"),
                            "TLS".to_string(),
                        );
                        finding.cwe = Some("CWE-295".to_string());
                        finding.evidence = vec![evidence];
                        finding.remediation = Some(
                            "Renew the TLS certificate. Consider using automated certificate \
                         management with Let's Encrypt or a similar CA."
                                .to_string(),
                        );
                        findings.push(finding);
                        warn!(host, "Expired TLS certificate");
                    }

                    if tls_info.cert_self_signed {
                        let evidence = DastEvidence {
                            request_method: "TLS".to_string(),
                            request_url: format!("{host}:{port}"),
                            request_headers: None,
                            request_body: None,
                            response_status: 0,
                            response_headers: None,
                            response_snippet: Some("Self-signed certificate detected".to_string()),
                            screenshot_path: None,
                            payload: None,
                            response_time_ms: None,
                        };

                        let mut finding = DastFinding::new(
                            String::new(),
                            target_id.clone(),
                            DastVulnType::TlsMisconfiguration,
                            format!("Self-signed TLS certificate for {host}"),
                            format!(
                            "The TLS certificate for {host} is self-signed and not issued by a \
                             trusted certificate authority. Browsers will show security warnings."
                        ),
                            Severity::Medium,
                            format!("https://{host}:{port}"),
                            "TLS".to_string(),
                        );
                        finding.cwe = Some("CWE-295".to_string());
                        finding.evidence = vec![evidence];
                        finding.remediation = Some(
                            "Replace the self-signed certificate with one issued by a trusted \
                         certificate authority."
                                .to_string(),
                        );
                        findings.push(finding);
                        warn!(host, "Self-signed certificate");
                    }
                }
                Err(e) => {
                    tls_data["tls_error"] = json!(e.to_string());

                    // TLS handshake failure itself is a finding
                    let evidence = DastEvidence {
                        request_method: "TLS".to_string(),
                        request_url: format!("{host}:{port}"),
                        request_headers: None,
                        request_body: None,
                        response_status: 0,
                        response_headers: None,
                        response_snippet: Some(format!("TLS error: {e}")),
                        screenshot_path: None,
                        payload: None,
                        response_time_ms: None,
                    };

                    let mut finding = DastFinding::new(
                        String::new(),
                        target_id.clone(),
                        DastVulnType::TlsMisconfiguration,
                        format!("TLS handshake failure for {host}"),
                        format!(
                            "Could not establish a TLS connection to {host}:{port}. Error: {e}"
                        ),
                        Severity::High,
                        format!("https://{host}:{port}"),
                        "TLS".to_string(),
                    );
                    finding.cwe = Some("CWE-295".to_string());
                    finding.evidence = vec![evidence];
                    finding.remediation = Some(
                        "Ensure TLS is properly configured on the server. Check that the \
                     certificate is valid and the server supports modern TLS versions."
                            .to_string(),
                    );
                    findings.push(finding);
                }
            }

            // Check strict transport security via an HTTPS request
            match self.http.get(&https_url).send().await {
                Ok(resp) => {
                    let hsts = resp.headers().get("strict-transport-security");
                    tls_data["hsts_header"] = json!(hsts.map(|v| v.to_str().unwrap_or("")));

                    if hsts.is_none() {
                        let evidence = DastEvidence {
                            request_method: "GET".to_string(),
                            request_url: https_url.clone(),
                            request_headers: None,
                            request_body: None,
                            response_status: resp.status().as_u16(),
                            response_headers: None,
                            response_snippet: Some(
                                "Strict-Transport-Security header not present".to_string(),
                            ),
                            screenshot_path: None,
                            payload: None,
                            response_time_ms: None,
                        };

                        let mut finding = DastFinding::new(
                        String::new(),
                        target_id.clone(),
                        DastVulnType::TlsMisconfiguration,
                        format!("Missing HSTS header for {host}"),
                        format!(
                            "The server at {host} does not send a Strict-Transport-Security header. \
                             Without HSTS, browsers may allow HTTP downgrade attacks."
                        ),
                        Severity::Medium,
                        https_url.clone(),
                        "GET".to_string(),
                    );
                        finding.cwe = Some("CWE-319".to_string());
                        finding.evidence = vec![evidence];
                        finding.remediation = Some(
                        "Add the Strict-Transport-Security header with an appropriate max-age. \
                         Example: 'Strict-Transport-Security: max-age=31536000; includeSubDomains'."
                            .to_string(),
                    );
                        findings.push(finding);
                    }
                }
                Err(_) => {
                    tls_data["https_check_error"] = json!("Could not connect via HTTPS");
                }
            }

            let count = findings.len();
            info!(host = %host, findings = count, "TLS analysis complete");

            Ok(PentestToolResult {
                summary: if count > 0 {
                    format!("Found {count} TLS configuration issues for {host}.")
                } else {
                    format!("TLS configuration looks good for {host}.")
                },
                findings,
                data: tls_data,
            })
        })
    }
}
