use std::{
    fmt::{self, Display, Formatter},
    ops::Deref,
    sync::{Arc, Mutex},
    time::Duration,
};

use bytes::{Bytes, BytesMut};
use reqwest::{
    header::{AsHeaderName, HeaderMap, HeaderValue},
    Body, Client as HttpClient, IntoUrl, Response as HttpResponse, StatusCode,
};
use serde::{Deserialize, Serialize};

use crate::{
    acme::{
        jws::{Message, MessageBuilder, ES256},
        Directory,
    },
    Error,
};

/// Maximum allowed size of a response body.
const MAX_RESPONSE_BODY_SIZE: usize = 10_000_000;

/// ACME client.
#[derive(Clone)]
pub struct Client {
    inner: HttpClient,
    identity: Arc<ES256>,
}

impl Client {
    /// Create a new ACME client.
    pub async fn new() -> Result<Self, Error> {
        let identity = tokio::task::spawn_blocking(ES256::new)
            .await
            .map_err(|_| Error::from_static_msg("terminating"))??
            .into();

        let mut default_headers = HeaderMap::new();

        let user_agent =
            HeaderValue::try_from(format!("GoodCamDeviceProxy/{}", env!("CARGO_PKG_VERSION")));
        let language = HeaderValue::try_from("en");

        default_headers.append("User-Agent", user_agent.unwrap());
        default_headers.append("Accept-Language", language.unwrap());

        let client = HttpClient::builder()
            .timeout(Duration::from_secs(20))
            .default_headers(default_headers)
            .build()?;

        let res = Self {
            inner: client,
            identity,
        };

        Ok(res)
    }

    /// Client identity.
    pub fn identity(&self) -> &ES256 {
        &self.identity
    }

    /// Open a given ACME directory.
    pub async fn open_directory<U>(&self, url: U) -> Result<Directory, Error>
    where
        U: IntoUrl,
    {
        let response = self.get(url).send().await?;

        let response = Response::new(response).await?;

        response.error_for_status()?;

        let directory = response.parse_json::<DirectoryResponse>()?;

        let client = DirectoryClient {
            client: self.clone(),
            new_nonce_url: directory.new_nonce,
            next_nonce: Arc::new(Mutex::new(None)),
        };

        let res = Directory {
            client,
            new_account_url: directory.new_account,
            new_order_url: directory.new_order,
        };

        Ok(res)
    }
}

impl Deref for Client {
    type Target = HttpClient;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

/// ACME directory client.
#[derive(Clone)]
pub struct DirectoryClient {
    client: Client,
    new_nonce_url: String,
    next_nonce: Arc<Mutex<Option<String>>>,
}

impl DirectoryClient {
    /// Create a new account client where the client identity is identified by
    /// a given key ID.
    pub fn to_account_client<T>(&self, kid: T) -> AccountClient
    where
        T: ToString,
    {
        AccountClient {
            client: self.clone(),
            kid: kid.to_string(),
        }
    }

    /// Perform ACME POST request.
    pub async fn post<P>(&self, url: &str, payload: &P) -> Result<Response, Error>
    where
        P: Serialize,
    {
        self.post_jws_message(url, |identity, nonce, url| {
            MessageBuilder::new(identity)
                .with_payload(payload)
                .build_with_jwk_header(url, nonce)
        })
        .await
    }

    /// Post a JWS message generated by a given closure.
    ///
    /// The closure will be called with client identity, nonce and the target
    /// URL (in this order).
    async fn post_jws_message<F>(&self, url: &str, mut f: F) -> Result<Response, Error>
    where
        F: FnMut(&ES256, &str, &str) -> Result<Message, Error>,
    {
        let mut nonce = self.acquire_nonce().await?;

        loop {
            let message = f(&self.identity, &nonce, url)?;

            let body = serde_json::to_string(&message).map_err(|err| {
                Error::from_static_msg_and_cause("unable to serialize a JWS message", err)
            })?;

            let response = self
                .client
                .post(url)
                .header("Content-Type", "application/jose+json")
                .body(Body::from(body))
                .send()
                .await?;

            let response = Response::new(response).await?;

            let status = response.status();
            let headers = response.headers();

            if status.as_u16() == 400 {
                if let Ok(err) = response.parse_json::<ErrorResponse>() {
                    if err.kind == "urn:ietf:params:acme:error:badNonce" {
                        nonce = headers
                            .replay_nonce()?
                            .ok_or_else(|| Error::from_static_msg("replay-nonce not provided"))?
                            .to_string();

                        // let's try it again with the new nonce
                        continue;
                    }
                }
            }

            if let Ok(Some(nonce)) = headers.replay_nonce() {
                *self.next_nonce.lock().unwrap() = Some(nonce.to_string());
            }

            return Ok(response);
        }
    }

    /// Acquire nonce (either cached or from the remote server).
    async fn acquire_nonce(&self) -> Result<String, Error> {
        let next_nonce = self.next_nonce.lock().unwrap().take();

        if let Some(nonce) = next_nonce {
            Ok(nonce)
        } else {
            self.new_nonce().await
        }
    }

    /// Generate a new nonce.
    async fn new_nonce(&self) -> Result<String, Error> {
        self.client
            .head(&self.new_nonce_url)
            .send()
            .await?
            .headers()
            .replay_nonce()?
            .ok_or_else(|| Error::from_static_msg("missing replay-nonce header"))
            .map(String::from)
    }
}

impl Deref for DirectoryClient {
    type Target = Client;

    fn deref(&self) -> &Self::Target {
        &self.client
    }
}

/// ACME account client.
#[derive(Clone)]
pub struct AccountClient {
    client: DirectoryClient,
    kid: String,
}

impl AccountClient {
    /// Perform ACME POST-as-GET request at a given URL.
    pub async fn get(&self, url: &str) -> Result<Response, Error> {
        self.client
            .post_jws_message(url, |identity, nonce, url| {
                MessageBuilder::new(identity).build_with_kid_header(&self.kid, url, nonce)
            })
            .await
    }

    /// Perform ACME POST to a given URL.
    pub async fn post<P>(&self, url: &str, payload: &P) -> Result<Response, Error>
    where
        P: Serialize,
    {
        self.client
            .post_jws_message(url, |identity, nonce, url| {
                MessageBuilder::new(identity)
                    .with_payload(payload)
                    .build_with_kid_header(&self.kid, url, nonce)
            })
            .await
    }
}

impl Deref for AccountClient {
    type Target = DirectoryClient;

    fn deref(&self) -> &Self::Target {
        &self.client
    }
}

/// ACME client response.
pub struct Response {
    inner: HttpResponse,
    body: Bytes,
}

impl Response {
    /// Create a new response.
    async fn new(mut response: HttpResponse) -> Result<Self, Error> {
        let mut body = BytesMut::new();

        while let Some(chunk) = response.chunk().await? {
            if (body.len() + chunk.len()) > MAX_RESPONSE_BODY_SIZE {
                return Err(Error::from_static_msg("response body size exceeded"));
            }

            body.extend_from_slice(&chunk);
        }

        let res = Self {
            inner: response,
            body: body.freeze(),
        };

        Ok(res)
    }

    /// Return an error if the status code is not success.
    pub fn error_for_status(&self) -> Result<(), Error> {
        let status = self.inner.status();

        if status.is_success() {
            Ok(())
        } else {
            Err(Error::from_cause(UnexpectedStatus::from(status)))
        }
    }

    /// Get body.
    pub fn body(&self) -> &Bytes {
        &self.body
    }

    /// Deserialize JSON body.
    pub fn parse_json<'a, T>(&'a self) -> Result<T, Error>
    where
        T: Deserialize<'a>,
    {
        serde_json::from_slice(&self.body)
            .map_err(|err| Error::from_static_msg_and_cause("unable to deserialize JSON body", err))
    }
}

impl Deref for Response {
    type Target = HttpResponse;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

/// Extensions for the HTTP header map.
pub trait HeaderMapExt {
    /// Get a given header value as string.
    fn get_str<K>(&self, key: K) -> Result<Option<&str>, Error>
    where
        K: AsHeaderName;

    /// Get the replay-nonce header value.
    fn replay_nonce(&self) -> Result<Option<&str>, Error>;

    /// Get the location header value.
    fn location(&self) -> Result<Option<&str>, Error>;
}

impl HeaderMapExt for HeaderMap {
    fn get_str<K>(&self, key: K) -> Result<Option<&str>, Error>
    where
        K: AsHeaderName,
    {
        self.get(key)
            .map(|value| value.to_str())
            .transpose()
            .map_err(|_| Error::from_static_msg("invalid response header"))
    }

    fn replay_nonce(&self) -> Result<Option<&str>, Error> {
        self.get_str("replay-nonce")
    }

    fn location(&self) -> Result<Option<&str>, Error> {
        self.get_str("location")
    }
}

/// Error indicating an unexpected status code.
#[derive(Debug, Copy, Clone)]
pub struct UnexpectedStatus {
    status: StatusCode,
}

impl Display for UnexpectedStatus {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "server responded with unexpected status: {}",
            self.status
        )
    }
}

impl std::error::Error for UnexpectedStatus {}

impl From<StatusCode> for UnexpectedStatus {
    fn from(status: StatusCode) -> Self {
        Self { status }
    }
}

impl From<UnexpectedStatus> for Error {
    fn from(err: UnexpectedStatus) -> Self {
        Error::from_cause(err)
    }
}

/// Error response.
#[derive(Deserialize)]
struct ErrorResponse<'a> {
    #[serde(rename = "type")]
    kind: &'a str,
}

/// Directory response.
#[derive(Deserialize)]
struct DirectoryResponse {
    #[serde(rename = "newNonce")]
    new_nonce: String,

    #[serde(rename = "newAccount")]
    new_account: String,

    #[serde(rename = "newOrder")]
    new_order: String,
}
