//! Self-signed certificate generation for development / testing.

use anyhow::{Context, Result};
use rcgen::{
    CertificateParams, DistinguishedName, DnType, Ia5String, IsCa, KeyPair, SanType,
    PKCS_ECDSA_P256_SHA256,
};
use ring::digest;
use std::net::IpAddr;
use time::{Duration, OffsetDateTime};
use x509_parser::pem::parse_x509_pem;

/// Result of self-signed certificate generation.
pub struct SelfSignedOutput {
    /// PEM-encoded certificate chain.
    pub cert_pem: Vec<u8>,
    /// PEM-encoded private key (PKCS#8).
    pub key_pem: Vec<u8>,
    /// SHA-256 fingerprint of the DER-encoded certificate (hex-encoded).
    pub fingerprint: String,
}

/// Generate a self-signed ECDSA P-256 certificate valid for 365 days.
///
/// `domains` are added as Subject Alternative Names. `localhost` and
/// `127.0.0.1` are always included.
///
/// Returns PEM-encoded certificate and private key plus the SHA-256
/// fingerprint for trust/pinning messages.
pub fn generate_self_signed(domains: &[String]) -> Result<SelfSignedOutput> {
    // Build the key pair.
    let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)
        .context("failed to generate ECDSA P-256 key pair")?;

    let mut params = CertificateParams::default();

    // Distinguished name.
    let mut dn = DistinguishedName::new();
    dn.push(DnType::CommonName, "bext self-signed");
    dn.push(DnType::OrganizationName, "bext dev");
    params.distinguished_name = dn;

    // Validity: now .. +365 days.
    params.not_before = OffsetDateTime::now_utc();
    params.not_after = OffsetDateTime::now_utc() + Duration::days(365);

    // Not a CA.
    params.is_ca = IsCa::NoCa;

    // Collect SANs — deduplicated.
    let mut dns_sans: Vec<String> = Vec::new();
    let mut ip_sans: Vec<IpAddr> = Vec::new();

    // Always include localhost + 127.0.0.1.
    dns_sans.push("localhost".to_string());
    ip_sans.push("127.0.0.1".parse().unwrap());

    for d in domains {
        // If it parses as an IP, add it as an IP SAN.
        if let Ok(ip) = d.parse::<IpAddr>() {
            if !ip_sans.contains(&ip) {
                ip_sans.push(ip);
            }
        } else if !dns_sans.iter().any(|existing| existing == d) {
            dns_sans.push(d.clone());
        }
    }

    let mut sans: Vec<SanType> = Vec::new();
    for name in &dns_sans {
        let ia5: Ia5String = name
            .as_str()
            .try_into()
            .with_context(|| format!("invalid DNS name for SAN: {name}"))?;
        sans.push(SanType::DnsName(ia5));
    }
    for ip in &ip_sans {
        sans.push(SanType::IpAddress(*ip));
    }
    params.subject_alt_names = sans;

    // Sign the certificate.
    let cert = params
        .self_signed(&key_pair)
        .context("failed to self-sign certificate")?;

    let cert_pem = cert.pem().into_bytes();
    let key_pem = key_pair.serialize_pem().into_bytes();

    // Compute SHA-256 fingerprint of the DER certificate.
    let fingerprint = compute_fingerprint(&cert_pem)?;

    tracing::info!(
        fingerprint = %fingerprint,
        "generated self-signed certificate"
    );

    Ok(SelfSignedOutput {
        cert_pem,
        key_pem,
        fingerprint,
    })
}

/// Compute the SHA-256 fingerprint (colon-separated hex) of a PEM certificate.
pub fn compute_fingerprint(cert_pem: &[u8]) -> Result<String> {
    let (_, pem) = parse_x509_pem(cert_pem).map_err(|e| anyhow::anyhow!("PEM parse error: {e}"))?;
    let hash = digest::digest(&digest::SHA256, &pem.contents);
    let hex: Vec<String> = hash.as_ref().iter().map(|b| format!("{b:02X}")).collect();
    Ok(hex.join(":"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use x509_parser::pem::parse_x509_pem;

    #[test]
    fn generate_basic_self_signed() {
        let out = generate_self_signed(&[]).unwrap();
        assert!(!out.cert_pem.is_empty());
        assert!(!out.key_pem.is_empty());
        assert!(!out.fingerprint.is_empty());
        // PEM headers present.
        let cert_str = std::str::from_utf8(&out.cert_pem).unwrap();
        assert!(cert_str.contains("BEGIN CERTIFICATE"));
        let key_str = std::str::from_utf8(&out.key_pem).unwrap();
        assert!(key_str.contains("BEGIN PRIVATE KEY"));
    }

    #[test]
    fn default_sans_include_localhost() {
        let out = generate_self_signed(&[]).unwrap();
        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        let san_ext = cert
            .extensions()
            .iter()
            .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME)
            .expect("SAN extension missing");

        // Verify the SAN extension is present and non-critical.
        assert!(!san_ext.critical, "SAN should not be critical");

        // Parse the SAN to check for localhost.
        let san = cert
            .subject_alternative_name()
            .expect("SAN extension parse error")
            .expect("SAN extension missing");

        let has_localhost = san.value.general_names.iter().any(|gn| {
            matches!(
                gn,
                x509_parser::extensions::GeneralName::DNSName("localhost")
            )
        });
        assert!(has_localhost, "SAN should include localhost");
    }

    #[test]
    fn custom_domains_are_included() {
        let domains = vec![
            "example.com".to_string(),
            "www.example.com".to_string(),
            "api.example.com".to_string(),
        ];
        let out = generate_self_signed(&domains).unwrap();

        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        // Extract SAN names.
        let names: Vec<String> = cert
            .subject_alternative_name()
            .expect("SAN extension parse error")
            .expect("SAN extension missing")
            .value
            .general_names
            .iter()
            .filter_map(|gn| match gn {
                x509_parser::extensions::GeneralName::DNSName(s) => Some(s.to_string()),
                _ => None,
            })
            .collect();

        assert!(names.contains(&"localhost".to_string()));
        assert!(names.contains(&"example.com".to_string()));
        assert!(names.contains(&"www.example.com".to_string()));
        assert!(names.contains(&"api.example.com".to_string()));
    }

    #[test]
    fn ip_address_san() {
        let domains = vec!["192.168.1.1".to_string()];
        let out = generate_self_signed(&domains).unwrap();

        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        let san = cert
            .subject_alternative_name()
            .expect("SAN extension parse error")
            .expect("SAN extension missing");

        let has_ip = san.value.general_names.iter().any(|gn| {
            matches!(gn, x509_parser::extensions::GeneralName::IPAddress(bytes) if bytes.len() == 4)
        });
        assert!(has_ip, "expected IP SAN for 192.168.1.1");
    }

    #[test]
    fn deduplication_of_localhost() {
        let domains = vec!["localhost".to_string()];
        let out = generate_self_signed(&domains).unwrap();

        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        let san = cert
            .subject_alternative_name()
            .expect("SAN extension parse error")
            .expect("SAN extension missing");

        let localhost_count = san
            .value
            .general_names
            .iter()
            .filter(|gn| {
                matches!(
                    gn,
                    x509_parser::extensions::GeneralName::DNSName("localhost")
                )
            })
            .count();

        assert_eq!(localhost_count, 1, "localhost should appear exactly once");
    }

    #[test]
    fn fingerprint_is_sha256_hex() {
        let out = generate_self_signed(&[]).unwrap();
        // SHA-256 = 32 bytes → 32 hex pairs joined by colons → 32*3-1 = 95 chars.
        assert_eq!(out.fingerprint.len(), 95);
        assert!(out
            .fingerprint
            .chars()
            .all(|c| c.is_ascii_hexdigit() || c == ':'));
    }

    #[test]
    fn cert_validity_is_365_days() {
        let out = generate_self_signed(&[]).unwrap();
        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        let not_before = cert.validity().not_before.timestamp();
        let not_after = cert.validity().not_after.timestamp();

        let days = (not_after - not_before) / 86400;
        assert!(
            (364..=366).contains(&days),
            "expected ~365 days, got {days}"
        );
    }

    #[test]
    fn cert_is_not_ca() {
        let out = generate_self_signed(&[]).unwrap();
        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        let basic_constraints = cert.basic_constraints();
        match basic_constraints {
            Ok(Some(bc)) => assert!(!bc.value.ca, "cert should not be CA"),
            Ok(None) => { /* no BC extension is fine for non-CA */ }
            Err(e) => panic!("error parsing basic constraints: {e}"),
        }
    }

    #[test]
    fn compute_fingerprint_roundtrip() {
        let out = generate_self_signed(&[]).unwrap();
        let fp = compute_fingerprint(&out.cert_pem).unwrap();
        assert_eq!(fp, out.fingerprint);
    }

    #[test]
    fn empty_domains_still_has_localhost() {
        let out = generate_self_signed(&[]).unwrap();
        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        let san = cert
            .subject_alternative_name()
            .expect("SAN extension parse error")
            .expect("SAN extension missing");

        assert!(
            !san.value.general_names.is_empty(),
            "should have at least localhost + 127.0.0.1"
        );
    }

    #[test]
    fn many_sans() {
        let domains: Vec<String> = (0..50).map(|i| format!("host{i}.example.com")).collect();
        let out = generate_self_signed(&domains).unwrap();

        let (_, pem) = parse_x509_pem(&out.cert_pem).unwrap();
        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).unwrap();

        let san = cert
            .subject_alternative_name()
            .expect("SAN extension parse error")
            .expect("SAN extension missing");

        // 50 custom + localhost = 51 DNS SANs, plus 127.0.0.1 IP SAN = 52 total.
        assert!(
            san.value.general_names.len() >= 52,
            "expected >= 52 SANs, got {}",
            san.value.general_names.len()
        );
    }
}
