//! Mutual TLS (mTLS) — client certificate verification.
//!
//! Configures rustls to verify client certificates against a trusted CA.
//! Supports both mandatory and optional (permissive) mTLS modes.

use anyhow::{Context, Result};
use rustls::server::danger::ClientCertVerifier;
use rustls::server::WebPkiClientVerifier;
use rustls::RootCertStore;
use rustls_pki_types::CertificateDer;
use std::sync::Arc;

/// mTLS configuration.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct MtlsConfig {
    /// Enable mTLS (require or accept client certificates).
    pub enabled: bool,
    /// Path to CA certificate(s) PEM file for verifying client certs.
    pub ca_cert: String,
    /// Whether to allow clients without certificates (optional mTLS).
    /// If false, all clients MUST present a valid certificate.
    #[serde(default)]
    pub optional: bool,
}

/// Build a rustls client cert verifier from the mTLS configuration.
///
/// Reads the CA certificate(s) from the PEM file, builds a root store,
/// and returns a `ClientCertVerifier` that validates client certs
/// against those roots.
///
/// If `config.optional` is true, clients without certificates are
/// allowed through (optional mTLS). If false, a valid client cert
/// is required.
pub fn build_client_verifier(config: &MtlsConfig) -> Result<Arc<dyn ClientCertVerifier>> {
    let ca_pem = std::fs::read(&config.ca_cert)
        .with_context(|| format!("read CA cert file: {}", config.ca_cert))?;

    build_client_verifier_from_pem(&ca_pem, config.optional)
}

/// Build a client cert verifier from PEM bytes (useful for testing).
pub fn build_client_verifier_from_pem(
    ca_pem: &[u8],
    optional: bool,
) -> Result<Arc<dyn ClientCertVerifier>> {
    let ca_certs = parse_pem_certs(ca_pem).context("parse CA PEM certificates")?;

    if ca_certs.is_empty() {
        anyhow::bail!("no certificates found in CA PEM data");
    }

    let mut root_store = RootCertStore::empty();
    for cert in &ca_certs {
        root_store
            .add(cert.clone())
            .context("add CA cert to root store")?;
    }

    let verifier = if optional {
        WebPkiClientVerifier::builder(Arc::new(root_store))
            .allow_unauthenticated()
            .build()
            .context("build optional client verifier")?
    } else {
        WebPkiClientVerifier::builder(Arc::new(root_store))
            .build()
            .context("build required client verifier")?
    };

    Ok(verifier)
}

/// Information extracted from a client certificate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientCertInfo {
    /// Certificate subject (distinguished name string).
    pub subject: String,
    /// Certificate issuer (distinguished name string).
    pub issuer: String,
    /// Serial number (hex-encoded).
    pub serial: String,
    /// Expiry date (not_after) in RFC 3339 format.
    pub not_after: String,
}

/// Parse client certificate info from DER-encoded bytes.
///
/// Extracts the subject, issuer, serial number, and expiry date
/// from the certificate.
pub fn extract_client_info(cert_der: &[u8]) -> Result<ClientCertInfo> {
    let (_, cert) = x509_parser::parse_x509_certificate(cert_der)
        .map_err(|e| anyhow::anyhow!("parse client X.509 certificate: {e}"))?;

    let subject = cert.subject().to_string();
    let issuer = cert.issuer().to_string();

    // Serial number as hex.
    let serial = cert
        .raw_serial()
        .iter()
        .map(|b| format!("{b:02X}"))
        .collect::<Vec<_>>()
        .join(":");

    // not_after as RFC 3339.
    let not_after = chrono::DateTime::from_timestamp(cert.validity().not_after.timestamp(), 0)
        .map(|dt| dt.to_rfc3339())
        .unwrap_or_else(|| "unknown".to_string());

    Ok(ClientCertInfo {
        subject,
        issuer,
        serial,
        not_after,
    })
}

/// Parse PEM-encoded certificates into DER blobs.
fn parse_pem_certs(pem: &[u8]) -> Result<Vec<CertificateDer<'static>>> {
    let mut reader = std::io::BufReader::new(pem);
    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
        .collect::<std::result::Result<Vec<_>, _>>()
        .context("read PEM certificates")?;
    Ok(certs)
}

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

    /// Generate a self-signed CA certificate for testing.
    fn gen_ca_cert() -> (Vec<u8>, Vec<u8>) {
        use rcgen::{
            BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair,
            PKCS_ECDSA_P256_SHA256,
        };
        use time::{Duration, OffsetDateTime};

        let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
        let mut params = CertificateParams::default();
        let mut dn = DistinguishedName::new();
        dn.push(DnType::CommonName, "bext test CA");
        dn.push(DnType::OrganizationName, "bext test");
        params.distinguished_name = dn;
        params.not_before = OffsetDateTime::now_utc();
        params.not_after = OffsetDateTime::now_utc() + Duration::days(365);
        params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);

        let cert = params.self_signed(&key_pair).unwrap();
        (
            cert.pem().into_bytes(),
            key_pair.serialize_pem().into_bytes(),
        )
    }

    fn init_crypto() {
        let _ = rustls::crypto::ring::default_provider().install_default();
    }

    #[test]
    fn build_verifier_from_ca_pem_required() {
        init_crypto();
        let (ca_pem, _) = gen_ca_cert();
        let verifier = build_client_verifier_from_pem(&ca_pem, false);
        assert!(verifier.is_ok(), "should build required verifier");
    }

    #[test]
    fn build_verifier_from_ca_pem_optional() {
        init_crypto();
        let (ca_pem, _) = gen_ca_cert();
        let verifier = build_client_verifier_from_pem(&ca_pem, true);
        assert!(verifier.is_ok(), "should build optional verifier");
    }

    #[test]
    fn build_verifier_empty_pem_fails() {
        init_crypto();
        let result = build_client_verifier_from_pem(b"", false);
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("no certificates"), "got: {msg}");
    }

    #[test]
    fn build_verifier_invalid_pem_fails() {
        init_crypto();
        let result = build_client_verifier_from_pem(b"not a certificate", false);
        assert!(result.is_err());
    }

    #[test]
    fn build_verifier_from_file() {
        init_crypto();
        let (ca_pem, _) = gen_ca_cert();
        let dir = tempfile::tempdir().unwrap();
        let ca_path = dir.path().join("ca.pem");
        std::fs::write(&ca_path, &ca_pem).unwrap();

        let config = MtlsConfig {
            enabled: true,
            ca_cert: ca_path.to_str().unwrap().to_string(),
            optional: false,
        };

        let verifier = build_client_verifier(&config);
        assert!(verifier.is_ok());
    }

    #[test]
    fn build_verifier_missing_file_fails() {
        init_crypto();
        let config = MtlsConfig {
            enabled: true,
            ca_cert: "/nonexistent/ca.pem".to_string(),
            optional: false,
        };

        let result = build_client_verifier(&config);
        assert!(result.is_err());
    }

    #[test]
    fn extract_client_info_from_self_signed() {
        let out = generate_self_signed(&["client.test".into()]).unwrap();

        // Convert PEM to DER for extract_client_info.
        let (_, pem) = x509_parser::pem::parse_x509_pem(&out.cert_pem).unwrap();

        let info = extract_client_info(&pem.contents).unwrap();

        assert!(
            info.subject.contains("bext self-signed"),
            "subject: {}",
            info.subject
        );
        assert!(!info.serial.is_empty());
        assert!(!info.not_after.is_empty());
        assert_ne!(info.not_after, "unknown");
    }

    #[test]
    fn extract_client_info_invalid_der_fails() {
        let result = extract_client_info(b"not a certificate");
        assert!(result.is_err());
    }

    #[test]
    fn mtls_config_deserialize() {
        let toml_str = r#"
enabled = true
ca_cert = "/etc/ssl/ca.pem"
optional = true
"#;
        let cfg: MtlsConfig = toml::from_str(toml_str).unwrap();
        assert!(cfg.enabled);
        assert_eq!(cfg.ca_cert, "/etc/ssl/ca.pem");
        assert!(cfg.optional);
    }

    #[test]
    fn mtls_config_deserialize_defaults() {
        let toml_str = r#"
enabled = false
ca_cert = "/path/to/ca.pem"
"#;
        let cfg: MtlsConfig = toml::from_str(toml_str).unwrap();
        assert!(!cfg.enabled);
        // optional defaults to false.
        assert!(!cfg.optional);
    }

    #[test]
    fn extract_client_info_from_ca_cert() {
        let (ca_pem, _) = gen_ca_cert();
        let (_, pem) = x509_parser::pem::parse_x509_pem(&ca_pem).unwrap();
        let info = extract_client_info(&pem.contents).unwrap();

        assert!(
            info.subject.contains("bext test CA"),
            "subject: {}",
            info.subject
        );
        assert!(
            info.issuer.contains("bext test CA"),
            "issuer: {}",
            info.issuer
        );
    }

    #[test]
    fn client_cert_info_equality() {
        let info1 = ClientCertInfo {
            subject: "CN=test".into(),
            issuer: "CN=ca".into(),
            serial: "01:02".into(),
            not_after: "2026-01-01T00:00:00+00:00".into(),
        };
        let info2 = info1.clone();
        assert_eq!(info1, info2);
    }
}
