//! Build `rustls::ServerConfig` from bext TLS configuration.

use crate::config::{MinTlsVersion, TlsConfig, TlsMode};
use crate::mtls;
use crate::resolver::SniCertResolver;
use crate::self_signed::generate_self_signed;
use anyhow::{bail, Context, Result};
use rustls::server::ResolvesServerCert;
use rustls::ServerConfig;
use std::fs;
use std::sync::Arc;

/// Build a `rustls::ServerConfig` from the bext TLS configuration.
///
/// - In `Auto` or `SelfSigned` mode, the resolver must already contain
///   the relevant certificates.
/// - In `Manual` mode, the cert/key are loaded from disk and added to
///   the resolver.
/// - In `Off` mode, this returns an error (the caller should not be
///   building a TLS config).
pub fn build_server_config(
    tls_config: &TlsConfig,
    resolver: Arc<SniCertResolver>,
) -> Result<ServerConfig> {
    match &tls_config.mode {
        TlsMode::Off => bail!("TLS is disabled — cannot build ServerConfig"),
        TlsMode::Manual {
            cert_path,
            key_path,
        } => {
            // Load from disk and add to resolver.
            let cert_pem =
                fs::read(cert_path).with_context(|| format!("read cert file: {cert_path}"))?;
            let key_pem =
                fs::read(key_path).with_context(|| format!("read key file: {key_path}"))?;

            // Determine domain from the certificate.
            let domain = extract_common_name(&cert_pem).unwrap_or_else(|_| "default".to_string());

            resolver.add_cert(&domain, &cert_pem, &key_pem)?;
            resolver.set_default(&cert_pem, &key_pem)?;
        }
        TlsMode::Auto | TlsMode::SelfSigned => {
            // Certificates should already be in the resolver.
            // (provisioned by the ACME client or self_signed generator
            //  before calling this function.)
        }
    }

    let resolver_dyn: Arc<dyn ResolvesServerCert> = resolver;

    // Build the ServerConfig, optionally with mTLS client cert verification.
    let use_mtls = tls_config.mtls.as_ref().map(|m| m.enabled).unwrap_or(false);

    let mut config = match (tls_config.min_version, use_mtls) {
        (MinTlsVersion::Tls12, false) => ServerConfig::builder()
            .with_no_client_auth()
            .with_cert_resolver(resolver_dyn),
        (MinTlsVersion::Tls13, false) => {
            ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
                .with_no_client_auth()
                .with_cert_resolver(resolver_dyn)
        }
        (MinTlsVersion::Tls12, true) => {
            let verifier = mtls::build_client_verifier(tls_config.mtls.as_ref().unwrap())
                .context("build mTLS client verifier")?;
            ServerConfig::builder()
                .with_client_cert_verifier(verifier)
                .with_cert_resolver(resolver_dyn)
        }
        (MinTlsVersion::Tls13, true) => {
            let verifier = mtls::build_client_verifier(tls_config.mtls.as_ref().unwrap())
                .context("build mTLS client verifier")?;
            ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
                .with_client_cert_verifier(verifier)
                .with_cert_resolver(resolver_dyn)
        }
    };

    // Set ALPN protocols: h2, http/1.1.
    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

    Ok(config)
}

/// Convenience: build a self-signed `ServerConfig` for dev mode.
///
/// Generates a self-signed certificate for the given domains and
/// returns a fully configured `ServerConfig` ready for binding.
pub fn build_self_signed_config(
    domains: &[String],
) -> Result<(ServerConfig, Arc<SniCertResolver>)> {
    let output = generate_self_signed(domains)?;

    let resolver = Arc::new(SniCertResolver::new());

    // Add the cert for each domain.
    for domain in domains {
        resolver.add_cert(domain, &output.cert_pem, &output.key_pem)?;
    }
    // Also add for localhost (always present in the self-signed cert).
    resolver.add_cert("localhost", &output.cert_pem, &output.key_pem)?;

    // Set as default fallback.
    resolver.set_default(&output.cert_pem, &output.key_pem)?;

    let resolver_dyn: Arc<dyn ResolvesServerCert> =
        Arc::clone(&resolver) as Arc<dyn ResolvesServerCert>;

    let mut config = ServerConfig::builder()
        .with_no_client_auth()
        .with_cert_resolver(resolver_dyn);

    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

    tracing::info!(
        fingerprint = %output.fingerprint,
        "self-signed TLS configuration built"
    );

    Ok((config, resolver))
}

/// Extract the Common Name from a PEM-encoded certificate.
fn extract_common_name(cert_pem: &[u8]) -> Result<String> {
    let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem)
        .map_err(|e| anyhow::anyhow!("parse PEM: {e}"))?;

    let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)
        .map_err(|e| anyhow::anyhow!("parse X.509: {e}"))?;

    for attr in cert.subject().iter_common_name() {
        if let Ok(cn) = attr.as_str() {
            return Ok(cn.to_string());
        }
    }

    bail!("no Common Name found in certificate subject");
}

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

    /// Install the ring crypto provider for rustls tests.
    /// Safe to call multiple times (only the first call takes effect).
    fn init_crypto() {
        let _ = rustls::crypto::ring::default_provider().install_default();
    }

    #[test]
    fn build_self_signed_config_basic() {
        init_crypto();
        let domains = vec!["dev.local".to_string()];
        let (config, resolver) = build_self_signed_config(&domains).unwrap();

        // Check ALPN.
        assert_eq!(
            config.alpn_protocols,
            vec![b"h2".to_vec(), b"http/1.1".to_vec()]
        );

        // Resolver should have the domain + localhost.
        assert!(resolver.has_cert("dev.local"));
        assert!(resolver.has_cert("localhost"));
    }

    #[test]
    fn build_self_signed_config_multiple_domains() {
        init_crypto();
        let domains = vec![
            "app1.local".to_string(),
            "app2.local".to_string(),
            "app3.local".to_string(),
        ];
        let (_, resolver) = build_self_signed_config(&domains).unwrap();

        assert!(resolver.has_cert("app1.local"));
        assert!(resolver.has_cert("app2.local"));
        assert!(resolver.has_cert("app3.local"));
        assert!(resolver.has_cert("localhost"));
    }

    #[test]
    fn build_self_signed_config_empty_domains() {
        init_crypto();
        // Empty domains should still work (just localhost).
        let (_, resolver) = build_self_signed_config(&[]).unwrap();
        assert!(resolver.has_cert("localhost"));
    }

    #[test]
    fn build_server_config_off_mode_errors() {
        let config = TlsConfig {
            mode: TlsMode::Off,
            ..Default::default()
        };
        let resolver = Arc::new(SniCertResolver::new());

        let result = build_server_config(&config, resolver);
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("TLS is disabled"), "got: {msg}");
    }

    #[test]
    fn build_server_config_manual_missing_file_errors() {
        let config = TlsConfig {
            mode: TlsMode::Manual {
                cert_path: "/nonexistent/cert.pem".to_string(),
                key_path: "/nonexistent/key.pem".to_string(),
            },
            ..Default::default()
        };
        let resolver = Arc::new(SniCertResolver::new());

        let result = build_server_config(&config, resolver);
        assert!(result.is_err());
    }

    #[test]
    fn build_server_config_manual_with_temp_files() {
        init_crypto();
        let out = generate_self_signed(&["manual.test".into()]).unwrap();

        let dir = tempfile::tempdir().unwrap();
        let cert_path = dir.path().join("cert.pem");
        let key_path = dir.path().join("key.pem");

        fs::write(&cert_path, &out.cert_pem).unwrap();
        fs::write(&key_path, &out.key_pem).unwrap();

        let config = TlsConfig {
            mode: TlsMode::Manual {
                cert_path: cert_path.to_str().unwrap().to_string(),
                key_path: key_path.to_str().unwrap().to_string(),
            },
            ..Default::default()
        };
        let resolver = Arc::new(SniCertResolver::new());

        let server_config = build_server_config(&config, Arc::clone(&resolver)).unwrap();
        assert_eq!(
            server_config.alpn_protocols,
            vec![b"h2".to_vec(), b"http/1.1".to_vec()]
        );
    }

    #[test]
    fn build_server_config_auto_mode_with_preloaded_certs() {
        init_crypto();
        let out = generate_self_signed(&["auto.test".into()]).unwrap();
        let resolver = Arc::new(SniCertResolver::new());
        resolver
            .add_cert("auto.test", &out.cert_pem, &out.key_pem)
            .unwrap();

        let config = TlsConfig {
            mode: TlsMode::Auto,
            ..Default::default()
        };

        let server_config = build_server_config(&config, resolver).unwrap();
        assert_eq!(
            server_config.alpn_protocols,
            vec![b"h2".to_vec(), b"http/1.1".to_vec()]
        );
    }

    #[test]
    fn build_server_config_tls13_only() {
        init_crypto();
        let out = generate_self_signed(&["tls13.test".into()]).unwrap();
        let resolver = Arc::new(SniCertResolver::new());
        resolver
            .add_cert("tls13.test", &out.cert_pem, &out.key_pem)
            .unwrap();

        let config = TlsConfig {
            mode: TlsMode::SelfSigned,
            min_version: MinTlsVersion::Tls13,
            ..Default::default()
        };

        let server_config = build_server_config(&config, resolver).unwrap();
        assert!(!server_config.alpn_protocols.is_empty());
    }

    #[test]
    fn extract_common_name_works() {
        let out = generate_self_signed(&["cn.test".into()]).unwrap();
        let cn = extract_common_name(&out.cert_pem).unwrap();
        assert_eq!(cn, "bext self-signed");
    }

    #[test]
    fn extract_common_name_invalid_pem() {
        let result = extract_common_name(b"not a cert");
        assert!(result.is_err());
    }

    #[test]
    fn alpn_protocols_are_h2_and_http11() {
        init_crypto();
        let (config, _) = build_self_signed_config(&[]).unwrap();
        assert_eq!(config.alpn_protocols.len(), 2);
        assert_eq!(config.alpn_protocols[0], b"h2");
        assert_eq!(config.alpn_protocols[1], b"http/1.1");
    }
}
