use serde_json::json;
use crate::{
    acme::{get_header, AcmeError, Auth, Challenge, ChallengeType, Directory, Identifier, Order},
    cache::AcmeCache,
    crypto::EcdsaP256SHA256KeyPair,
    jose::{jose_req, key_authorization_sha256},
    B64_URL_SAFE_NO_PAD,
};
use base64::Engine;
use generic_async_http_client::Response;
#[derive(Debug)]
pub struct Account {
    key_pair: EcdsaP256SHA256KeyPair,
    directory: Directory,
    kid: String,
}
impl Account {
    pub async fn load_or_create<'a, C, S, I>(
        directory: Directory,
        cache: Option<&C>,
        contact: I,
    ) -> Result<Self, AcmeError>
    where
        C: AcmeCache,
        S: AsRef<str> + 'a,
        I: IntoIterator<Item = &'a S>,
    {
        let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect();
        let pkcs8 = match &cache {
            Some(cache) => cache
                .read_account(&contact)
                .await
                .map_err(AcmeError::cache)?,
            None => None,
        };
        let key_pair = match pkcs8 {
            Some(pkcs8) => {
                log::info!("found cached account key");
                EcdsaP256SHA256KeyPair::load(&pkcs8)
            }
            None => {
                log::info!("creating a new account key");
                match EcdsaP256SHA256KeyPair::generate() {
                    Ok(pkcs8) => {
                        let data = pkcs8.as_ref();
                        if let Some(cache) = &cache {
                            cache
                                .write_account(&contact, data)
                                .await
                                .map_err(AcmeError::cache)?;
                        }
                        EcdsaP256SHA256KeyPair::load(data)
                    }
                    Err(_) => Err(()),
                }
            }
        }
        .map_err(|_| {
            AcmeError::Io(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "could not create key pair",
            ))
        })?;
        let payload = json!({
            "termsOfServiceAgreed": true,
            "contact": contact,
        })
        .to_string();
        let response = jose_req(
            &key_pair,
            None,
            &directory.nonce().await?,
            &directory.new_account,
            &payload,
        )
        .await?;
        let kid = get_header(&response, "Location")?;
        Ok(Account {
            key_pair,
            kid,
            directory,
        })
    }
    async fn request(&self, url: impl AsRef<str>, payload: &str) -> Result<Response, AcmeError> {
        jose_req(
            &self.key_pair,
            Some(&self.kid),
            &self.directory.nonce().await?,
            url.as_ref(),
            payload,
        )
        .await
    }
    pub async fn new_order(&self, domains: Vec<String>) -> Result<Order, AcmeError> {
        let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
        let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?);
        let mut response = self.request(&self.directory.new_order, &payload).await?;
        Ok(response.json().await?)
    }
    pub async fn check_auth(&self, url: impl AsRef<str>) -> Result<Auth, AcmeError> {
        let payload = "".to_string();
        let mut response = self.request(url, &payload).await?;
        Ok(response.json().await?)
    }
    pub async fn trigger_challenge(&self, url: impl AsRef<str>) -> Result<(), AcmeError> {
        self.request(&url, "{}").await?;
        Ok(())
    }
    pub async fn send_csr(&self, url: impl AsRef<str>, csr: Vec<u8>) -> Result<Order, AcmeError> {
        let payload = format!("{{\"csr\":\"{}\"}}", B64_URL_SAFE_NO_PAD.encode(csr));
        let mut response = self.request(&url, &payload).await?;
        Ok(response.json().await?)
    }
    pub async fn obtain_certificate(&self, url: impl AsRef<str>) -> Result<String, AcmeError> {
        Ok(self.request(&url, "").await?.text().await?)
    }
    pub fn tls_alpn_01<'a>(
        &self,
        challenges: &'a [Challenge],
    ) -> Result<(&'a Challenge, impl AsRef<[u8]>), AcmeError> {
        let challenge = challenges
            .iter()
            .find(|c| c.typ == ChallengeType::TlsAlpn01);
        let challenge = match challenge {
            Some(challenge) => challenge,
            None => return Err(AcmeError::NoTlsAlpn01Challenge),
        };
        let key_auth = key_authorization_sha256(&self.key_pair, &*challenge.token)?;
        Ok((challenge, key_auth))
    }
}