/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use std::{
    io::Cursor,
    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
    sync::Arc,
    time::Duration,
};

use ahash::{AHashMap, AHashSet};
use base64::{
    engine::general_purpose::{self, STANDARD},
    Engine,
};
use dns_update::{providers::rfc2136::DnsAddress, DnsUpdater, TsigAlgorithm};
use rcgen::generate_simple_self_signed;
use rustls::{
    crypto::ring::sign::any_supported_type,
    sign::CertifiedKey,
    version::{TLS12, TLS13},
    SupportedProtocolVersion,
};
use rustls_pemfile::{certs, read_one, Item};
use rustls_pki_types::PrivateKeyDer;
use utils::config::Config;
use x509_parser::{
    certificate::X509Certificate,
    der_parser::asn1_rs::FromDer,
    extensions::{GeneralName, ParsedExtension},
};

use crate::listener::{
    acme::{
        directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeProvider, ChallengeSettings, EabSettings,
    },
    tls::AcmeProviders,
};

pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];

impl AcmeProviders {
    pub fn parse(config: &mut Config) -> Self {
        let mut providers = AHashMap::new();

        // Parse ACME providers
        'outer: for acme_id in config
            .sub_keys("acme", ".directory")
            .map(|s| s.to_string())
            .collect::<Vec<_>>()
        {
            let acme_id = acme_id.as_str();
            let directory = config
                .value(("acme", acme_id, "directory"))
                .unwrap_or(LETS_ENCRYPT_PRODUCTION_DIRECTORY)
                .trim()
                .to_string();
            let contact = config
                .values(("acme", acme_id, "contact"))
                .filter_map(|(_, v)| {
                    let v = v.trim().to_string();
                    if !v.is_empty() {
                        Some(v)
                    } else {
                        None
                    }
                })
                .collect::<Vec<_>>();
            let renew_before: Duration = config
                .property_or_default(("acme", acme_id, "renew-before"), "30d")
                .unwrap_or_else(|| Duration::from_secs(30 * 24 * 60 * 60));

            if directory.is_empty() {
                config.new_parse_error(format!("acme.{acme_id}.directory"), "Missing property");
                continue;
            }

            if contact.is_empty() {
                config.new_parse_error(format!("acme.{acme_id}.contact"), "Missing property");
                continue;
            }

            // Parse challenge type
            let challenge = match config
                .value(("acme", acme_id, "challenge"))
                .unwrap_or("tls-alpn-01")
            {
                "tls-alpn-01" => ChallengeSettings::TlsAlpn01,
                "http-01" => ChallengeSettings::Http01,
                "dns-01" => match build_dns_updater(config, acme_id) {
                    Some(updater) => ChallengeSettings::Dns01 {
                        updater,
                        origin: config
                            .value(("acme", acme_id, "origin"))
                            .map(|s| s.to_string()),
                        polling_interval: config
                            .property_or_default(("acme", acme_id, "polling-interval"), "15s")
                            .unwrap_or_else(|| Duration::from_secs(15)),
                        propagation_timeout: config
                            .property_or_default(("acme", acme_id, "propagation-timeout"), "1m")
                            .unwrap_or_else(|| Duration::from_secs(60)),
                        ttl: config
                            .property_or_default(("acme", acme_id, "ttl"), "5m")
                            .unwrap_or_else(|| Duration::from_secs(5 * 60))
                            .as_secs() as u32,
                    },
                    None => {
                        continue;
                    }
                },
                _ => {
                    config
                        .new_parse_error(("acme", acme_id, "challenge"), "Invalid challenge type");
                    continue;
                }
            };

            // Domains covered by this ACME manager
            let domains = config
                .values(("acme", acme_id, "domains"))
                .map(|(_, s)| s.trim().to_string())
                .collect::<Vec<_>>();
            if !matches!(challenge, ChallengeSettings::Dns01 { .. })
                && domains.iter().any(|d| d.starts_with("*."))
            {
                config.new_parse_error(
                    ("acme", acme_id, "domains"),
                    "Wildcard domains are only supported with DNS-01 challenge",
                );
                continue 'outer;
            }

            // Obtain EAB settings
            let eab = if let (Some(eab_kid), Some(eab_hmac_key)) = (
                config
                    .value(("acme", acme_id, "eab.kid"))
                    .filter(|s| !s.is_empty()),
                config
                    .value(("acme", acme_id, "eab.hmac-key"))
                    .filter(|s| !s.is_empty()),
            ) {
                if let Ok(hmac_key) =
                    general_purpose::URL_SAFE_NO_PAD.decode(eab_hmac_key.trim().as_bytes())
                {
                    EabSettings {
                        kid: eab_kid.to_string(),
                        hmac_key,
                    }
                    .into()
                } else {
                    config.new_build_error(
                        format!("acme.{acme_id}.eab.hmac-key"),
                        "Failed to base64 decode HMAC key",
                    );
                    None
                }
            } else {
                None
            };

            // This ACME manager is the default when SNI is not available
            let default = config
                .property::<bool>(("acme", acme_id, "default"))
                .unwrap_or_default();

            if !domains.is_empty() {
                match AcmeProvider::new(
                    acme_id.to_string(),
                    directory,
                    domains,
                    contact,
                    challenge,
                    eab,
                    renew_before,
                    default,
                ) {
                    Ok(acme_provider) => {
                        providers.insert(acme_id.to_string(), acme_provider);
                    }
                    Err(err) => {
                        config.new_build_error(format!("acme.{acme_id}"), err.to_string());
                    }
                }
            }
        }

        AcmeProviders { providers }
    }
}

#[allow(clippy::unnecessary_to_owned)]
fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option<DnsUpdater> {
    match config.value_require(("acme", acme_id, "provider"))? {
        "rfc2136-tsig" => {
            let algorithm: TsigAlgorithm = config
                .value_require(("acme", acme_id, "tsig-algorithm"))?
                .parse()
                .map_err(|_| {
                    config.new_parse_error(("acme", acme_id, "tsig-algorithm"), "Invalid algorithm")
                })
                .ok()?;
            let key = STANDARD
                .decode(config.value_require(("acme", acme_id, "secret"))?.trim())
                .map_err(|_| {
                    config.new_parse_error(
                        ("acme", acme_id, "secret"),
                        "Failed to base64 decode secret",
                    )
                })
                .ok()?;
            let host = config.property_require::<IpAddr>(("acme", acme_id, "host"))?;
            let port = config
                .property_or_default::<u16>(("acme", acme_id, "port"), "53")
                .unwrap_or(53);
            let addr = if config.value(("acme", acme_id, "protocol")) == Some("tcp") {
                DnsAddress::Tcp(SocketAddr::new(host, port))
            } else {
                DnsAddress::Udp(SocketAddr::new(host, port))
            };

            DnsUpdater::new_rfc2136_tsig(
                addr,
                config
                    .value_require(("acme", acme_id, "key"))?
                    .trim()
                    .to_string(),
                key,
                algorithm,
            )
            .map_err(|err| {
                config.new_build_error(
                    ("acme", acme_id, "provider"),
                    format!("Failed to create RFC2136-TSIG DNS updater: {err}"),
                )
            })
            .ok()
        }
        "cloudflare" => {
            let timeout = config
                .property_or_default(("acme", acme_id, "timeout"), "30s")
                .unwrap_or_else(|| Duration::from_secs(30));

            DnsUpdater::new_cloudflare(
                config
                    .value_require(("acme", acme_id, "secret"))?
                    .trim()
                    .to_string(),
                config.value(("acme", acme_id, "user")).map(|s| s.trim()),
                timeout.into(),
            )
            .map_err(|err| {
                config.new_build_error(
                    ("acme", acme_id, "provider"),
                    format!("Failed to create Cloudflare DNS updater: {err}"),
                )
            })
            .ok()
        }
        _ => {
            config.new_parse_error(("acme", acme_id, "provider"), "Unsupported provider");
            None
        }
    }
}

pub(crate) fn parse_certificates(
    config: &mut Config,
    certificates: &mut AHashMap<String, Arc<CertifiedKey>>,
    subject_names: &mut AHashSet<String>,
) {
    // Parse certificates
    for cert_id in config
        .sub_keys("certificate", ".cert")
        .map(|s| s.to_string())
        .collect::<Vec<_>>()
    {
        let cert_id = cert_id.as_str();
        let key_cert = ("certificate", cert_id, "cert");
        let key_pk = ("certificate", cert_id, "private-key");

        let cert = config
            .value_require(key_cert)
            .map(|s| s.as_bytes().to_vec());
        let pk = config.value_require(key_pk).map(|s| s.as_bytes().to_vec());

        if let (Some(cert), Some(pk)) = (cert, pk) {
            match build_certified_key(cert, pk) {
                Ok(cert) => {
                    match cert
                        .end_entity_cert()
                        .map_err(|err| format!("Failed to obtain end entity cert: {err}"))
                        .and_then(|cert| {
                            X509Certificate::from_der(cert.as_ref())
                                .map_err(|err| format!("Failed to parse end entity cert: {err}"))
                        }) {
                        Ok((_, parsed)) => {
                            // Add CNs and SANs to the list of names
                            let mut names = AHashSet::new();
                            for name in parsed.subject().iter_common_name() {
                                if let Ok(name) = name.as_str() {
                                    names.insert(name.to_string());
                                }
                            }
                            for ext in parsed.extensions() {
                                if let ParsedExtension::SubjectAlternativeName(san) =
                                    ext.parsed_extension()
                                {
                                    for name in &san.general_names {
                                        let name = match name {
                                            GeneralName::DNSName(name) => name.to_string(),
                                            GeneralName::IPAddress(ip) => match ip.len() {
                                                4 => Ipv4Addr::from(
                                                    <[u8; 4]>::try_from(*ip).unwrap(),
                                                )
                                                .to_string(),
                                                16 => Ipv6Addr::from(
                                                    <[u8; 16]>::try_from(*ip).unwrap(),
                                                )
                                                .to_string(),
                                                _ => continue,
                                            },
                                            _ => {
                                                continue;
                                            }
                                        };
                                        names.insert(name);
                                    }
                                }
                            }

                            // Add custom SNIs
                            names.extend(
                                config
                                    .values(("certificate", cert_id, "subjects"))
                                    .map(|(_, v)| v.trim().to_string()),
                            );

                            // Add domain names
                            subject_names.extend(names.iter().cloned());

                            // Add certificates
                            let cert = Arc::new(cert);
                            for name in names {
                                certificates.insert(
                                    name.strip_prefix("*.")
                                        .map(|name| name.to_string())
                                        .unwrap_or(name),
                                    cert.clone(),
                                );
                            }

                            // Add default certificate
                            if config
                                .property::<bool>(("certificate", cert_id, "default"))
                                .unwrap_or_default()
                            {
                                certificates.insert("*".to_string(), cert.clone());
                            }
                        }
                        Err(err) => config.new_build_error(format!("certificate.{cert_id}"), err),
                    }
                }
                Err(err) => config.new_build_error(format!("certificate.{cert_id}"), err),
            }
        }
    }
}

pub(crate) fn build_certified_key(cert: Vec<u8>, pk: Vec<u8>) -> Result<CertifiedKey, String> {
    let cert = certs(&mut Cursor::new(cert))
        .collect::<Result<Vec<_>, _>>()
        .map_err(|err| format!("Failed to read certificates: {err}"))?;
    if cert.is_empty() {
        return Err("No certificates found.".to_string());
    }
    let pk = match read_one(&mut Cursor::new(pk))
        .map_err(|err| format!("Failed to read private keys.: {err}",))?
        .into_iter()
        .next()
    {
        Some(Item::Pkcs8Key(key)) => PrivateKeyDer::Pkcs8(key),
        Some(Item::Pkcs1Key(key)) => PrivateKeyDer::Pkcs1(key),
        Some(Item::Sec1Key(key)) => PrivateKeyDer::Sec1(key),
        Some(_) => return Err("Unsupported private keys found.".to_string()),
        None => return Err("No private keys found.".to_string()),
    };

    Ok(CertifiedKey {
        cert,
        key: any_supported_type(&pk)
            .map_err(|err| format!("Failed to sign certificate: {err}",))?,
        ocsp: None,
    })
}

pub(crate) fn build_self_signed_cert(
    domains: impl Into<Vec<String>>,
) -> Result<CertifiedKey, String> {
    let cert = generate_simple_self_signed(domains)
        .map_err(|err| format!("Failed to generate self-signed certificate: {err}",))?;
    build_certified_key(
        cert.serialize_pem().unwrap().into_bytes(),
        cert.serialize_private_key_pem().into_bytes(),
    )
}
