use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::{Context, Result, anyhow};
use rustls::server::WebPkiClientVerifier;
use rustls::{RootCertStore, ServerConfig};
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
use tracing::warn;
use x509_parser::extensions::GeneralName;
use x509_parser::prelude::{FromDer, X509Certificate};

pub fn load_rustls_server_config(
    cert_path: impl AsRef<Path>,
    key_path: impl AsRef<Path>,
    client_ca_path: impl AsRef<Path>,
) -> Result<ServerConfig> {
    load_rustls_server_config_with_options(cert_path, key_path, client_ca_path, false)
}

pub fn load_rustls_server_config_with_options(
    cert_path: impl AsRef<Path>,
    key_path: impl AsRef<Path>,
    client_ca_path: impl AsRef<Path>,
    require_dns_name_san: bool,
) -> Result<ServerConfig> {
    let cert_path: PathBuf = cert_path.as_ref().to_path_buf();
    let key_path: PathBuf = key_path.as_ref().to_path_buf();
    let client_ca_path: PathBuf = client_ca_path.as_ref().to_path_buf();

    let certs = load_certs(&cert_path)
        .with_context(|| format!("failed to load certs from {}", cert_path.display()))?;
    validate_server_certificate_dns_name_san(&certs, &cert_path, require_dns_name_san)?;

    let key = load_private_key(&key_path)
        .with_context(|| format!("failed to load private key from {}", key_path.display()))?;

    let client_ca_certs = load_certs(&client_ca_path).with_context(|| {
        format!(
            "failed to load client CA certs from {}",
            client_ca_path.display()
        )
    })?;
    let mut client_roots = RootCertStore::empty();
    let (added, _) = client_roots.add_parsable_certificates(client_ca_certs);
    if added == 0 {
        return Err(anyhow!(
            "no valid client CA certificates found in {}",
            client_ca_path.display()
        ));
    }
    let client_verifier = WebPkiClientVerifier::builder(Arc::new(client_roots))
        .build()
        .map_err(|e| anyhow!("invalid client certificate verifier configuration: {}", e))?;

    let config = ServerConfig::builder()
        .with_client_cert_verifier(client_verifier)
        .with_single_cert(certs, key)
        .map_err(|e| anyhow!("invalid certificate/key pair: {}", e))?;

    Ok(config)
}

fn validate_server_certificate_dns_name_san(
    certs: &[CertificateDer<'static>],
    cert_path: &Path,
    require_dns_name_san: bool,
) -> Result<()> {
    let leaf = certs
        .first()
        .ok_or_else(|| anyhow!("missing end-entity certificate in {}", cert_path.display()))?;
    let (_, cert) = X509Certificate::from_der(leaf.as_ref())
        .map_err(|err| anyhow!("failed to parse server certificate: {:?}", err))?;
    let has_dns_name_san = cert
        .subject_alternative_name()
        .map_err(|err| anyhow!("failed to parse server certificate SAN: {:?}", err))?
        .map(|san| {
            san.value
                .general_names
                .iter()
                .any(|name| matches!(name, GeneralName::DNSName(_)))
        })
        .unwrap_or(false);

    if has_dns_name_san {
        return Ok(());
    }

    let message = format!(
        "server certificate {} does not contain a subjectAltName dNSName entry; draft-ietf-sidrops-8210bis-25 Section 9.2 requires routers to authenticate the cache using DNS-ID rather than CN-ID",
        cert_path.display()
    );

    if require_dns_name_san {
        Err(anyhow!(message))
    } else {
        warn!("{}", message);
        Ok(())
    }
}

fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);

    let certs = rustls_pemfile::certs(&mut reader).collect::<std::result::Result<Vec<_>, _>>()?;

    if certs.is_empty() {
        return Err(anyhow!("no certificates found in {}", path.display()));
    }

    Ok(certs)
}

fn load_private_key(path: &Path) -> Result<PrivateKeyDer<'static>> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);

    let key = rustls_pemfile::private_key(&mut reader)?
        .ok_or_else(|| anyhow!("no private key found in {}", path.display()))?;

    Ok(key)
}
