use anyhow::{Context, anyhow};
use log::debug;
use rustls::pki_types::pem::PemObject;
use std::{path::PathBuf, sync::Arc};
use tokio_rustls::rustls::{
    ClientConfig, RootCertStore, ServerConfig, SignatureScheme,
    client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
    pki_types::{CertificateDer, PrivateKeyDer, ServerName},
};

/// Build a TLS acceptor from certificate and private key material.
pub fn make_tls_acceptor(
    certs: Vec<CertificateDer<'static>>,
    key: PrivateKeyDer<'static>,
) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
    let config = ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs, key)
        .context("bad certificates/private key")?;

    Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
}

/// Build a client TLS configuration, optionally disabling certificate verification.
pub fn make_tls_client_config(insecure: bool) -> ClientConfig {
    let mut root_cert_store = RootCertStore::empty();
    root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
    let mut config = ClientConfig::builder()
        .with_root_certificates(root_cert_store)
        .with_no_client_auth();

    if insecure {
        config
            .dangerous()
            .set_certificate_verifier(Arc::new(NoVerifier));
    }
    config
}

/// Build a TLS connector from the client TLS configuration.
pub fn make_tls_connector(insecure: bool) -> tokio_rustls::TlsConnector {
    tokio_rustls::TlsConnector::from(Arc::new(make_tls_client_config(insecure)))
}

/// Convert a host or `host:port` string into a rustls server name.
pub fn make_server_name<'a>(domain: &str) -> anyhow::Result<ServerName<'a>> {
    let domain = domain
        .split(':')
        .next()
        .ok_or_else(|| anyhow!("domain parse error : {}", domain))?;
    debug!("server name parse is : {}", domain);
    let server_name = ServerName::try_from(domain)
        .map_err(|e| anyhow!("try from domain [{}] to server name err : {}", &domain, e))?
        .to_owned();
    Ok(server_name)
}

/// Load PEM-encoded certificates from disk.
pub fn load_certs(path: PathBuf) -> anyhow::Result<Vec<CertificateDer<'static>>> {
    CertificateDer::pem_file_iter(&path)
        .context("pem_file_iter failed")?
        .collect::<Result<Vec<_>, _>>()
        .context("pem_file_iter collect failed")
}

/// Load a PEM-encoded private key from disk.
pub fn load_private_key(path: PathBuf) -> anyhow::Result<PrivateKeyDer<'static>> {
    PrivateKeyDer::from_pem_file(&path).context("from_pem_file failed")
}

#[derive(Debug)]
pub(crate) struct NoVerifier;

impl ServerCertVerifier for NoVerifier {
    fn verify_server_cert(
        &self,
        _end_entity: &CertificateDer<'_>,
        _intermediates: &[CertificateDer<'_>],
        _server_name: &ServerName<'_>,
        _ocsp_response: &[u8],
        _now: tokio_rustls::rustls::pki_types::UnixTime,
    ) -> Result<tokio_rustls::rustls::client::danger::ServerCertVerified, tokio_rustls::rustls::Error>
    {
        Ok(ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &tokio_rustls::rustls::DigitallySignedStruct,
    ) -> Result<
        tokio_rustls::rustls::client::danger::HandshakeSignatureValid,
        tokio_rustls::rustls::Error,
    > {
        Ok(HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        _message: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &tokio_rustls::rustls::DigitallySignedStruct,
    ) -> Result<
        tokio_rustls::rustls::client::danger::HandshakeSignatureValid,
        tokio_rustls::rustls::Error,
    > {
        Ok(HandshakeSignatureValid::assertion())
    }

    fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
        vec![
            SignatureScheme::ECDSA_NISTP384_SHA384,
            SignatureScheme::ECDSA_NISTP256_SHA256,
            SignatureScheme::ED25519,
            SignatureScheme::RSA_PSS_SHA512,
            SignatureScheme::RSA_PSS_SHA384,
            SignatureScheme::RSA_PSS_SHA256,
            SignatureScheme::RSA_PKCS1_SHA512,
            SignatureScheme::RSA_PKCS1_SHA384,
            SignatureScheme::RSA_PKCS1_SHA256,
        ]
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use tokio_rustls::rustls::pki_types::ServerName;

    use super::{load_certs, load_private_key, make_server_name, make_tls_client_config};

    fn example_path(name: &str) -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../borer-cli/examples")
            .join(name)
    }

    #[test]
    fn make_server_name_strips_port() {
        let server_name = make_server_name("example.com:443").unwrap();

        assert_eq!(server_name, ServerName::try_from("example.com").unwrap());
    }

    #[test]
    fn make_server_name_accepts_plain_domain() {
        let server_name = make_server_name("example.com").unwrap();

        assert_eq!(server_name, ServerName::try_from("example.com").unwrap());
    }

    #[test]
    fn make_server_name_rejects_invalid_input() {
        assert!(make_server_name(":443").is_err());
    }

    #[test]
    fn make_tls_client_config_builds_for_both_modes() {
        let _secure = make_tls_client_config(false);
        let _insecure = make_tls_client_config(true);
    }

    #[test]
    fn load_certs_and_private_key_from_example_files() {
        let certs = load_certs(example_path("cert.pem")).unwrap();
        let key = load_private_key(example_path("key.pem")).unwrap();

        assert!(!certs.is_empty());
        assert!(matches!(
            key,
            tokio_rustls::rustls::pki_types::PrivateKeyDer::Pkcs8(_)
                | tokio_rustls::rustls::pki_types::PrivateKeyDer::Pkcs1(_)
                | tokio_rustls::rustls::pki_types::PrivateKeyDer::Sec1(_)
        ));
    }

    #[test]
    fn load_certs_and_private_key_fail_for_missing_files() {
        assert!(load_certs(PathBuf::from("missing-cert.pem")).is_err());
        assert!(load_private_key(PathBuf::from("missing-key.pem")).is_err());
    }
}
