use std::{fmt::Display, sync::Arc};

use chrono::Utc;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use indexmap::IndexSet;
use magnetar_core::web_model::content_type::ContentActivityStreams;
use serde_json::Value;
use sha2::Digest;
use thiserror::Error;
use url::Url;

use crate::{
    client::federation_client::{FederationClient, FederationClientError},
    crypto::{ApSigningError, ApSigningKey, SigningAlgorithm},
    ApClientService, ApSignature, ApSigningField, ApSigningHeaders, SigningInput, SigningParts,
};

pub struct ApClientServiceDefaultProvider {
    client: Arc<FederationClient>,
}

impl Display for ApSignature {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "keyId=\"{}\"", self.key_id)?;
        if let Some(ref algorithm) = self.algorithm {
            write!(f, ",algorithm=\"{}\"", algorithm)?;
        }
        if let Some(ref created) = self.created {
            write!(f, ",created={}", created.timestamp())?;
        }
        if let Some(ref expires) = self.expires {
            write!(f, ",expires={}", expires.timestamp())?;
        }
        if let Some(ref headers) = self.headers {
            write!(f, ",headers=\"{}\"", headers)?;
        }
        write!(f, ",signature=\"{}\"", self.signature)?;
        Ok(())
    }
}

impl Display for ApSigningHeaders {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.0
                .iter()
                .map(AsRef::as_ref)
                .collect::<Vec<_>>()
                .join(" ")
        )
    }
}

#[derive(Debug, Error)]
pub enum ApClientError {
    #[error("Federation client error: {0}")]
    FederationClientError(#[from] FederationClientError),
    #[error("Signing error: {0}")]
    SigningError(#[from] ApSigningError),
    #[error("URL parse error: {0}")]
    UrlParseError(#[from] url::ParseError),
    #[error("Failed to serialize JSON: {0}")]
    SerializerError(#[from] serde_json::Error),
    #[error("Invalid header value: {0}")]
    InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
}

trait CreateField {
    fn create_field(&self) -> Option<(ApSigningField, String)>;
}

#[derive(Debug)]
struct RequestTarget<'a> {
    url: &'a Url,
    method: Method,
}

impl CreateField for RequestTarget<'_> {
    fn create_field(&self) -> Option<(ApSigningField, String)> {
        Some((
            ApSigningField::PseudoRequestTarget,
            format!(
                "{} {}",
                self.method.as_str().to_lowercase(),
                self.url.path()
            ),
        ))
    }
}

#[derive(Debug)]
struct HostPseudoHeader<'a>(&'a Url);

impl CreateField for HostPseudoHeader<'_> {
    fn create_field(&self) -> Option<(ApSigningField, String)> {
        Some((ApSigningField::Host, self.0.host()?.to_string()))
    }
}

#[derive(Debug)]
struct DateHeader(chrono::DateTime<Utc>);

impl CreateField for DateHeader {
    fn create_field(&self) -> Option<(ApSigningField, String)> {
        Some((ApSigningField::Date, httpdate::fmt_http_date(self.0.into())))
    }
}

#[derive(Debug)]
struct CreatedPseudoHeader(chrono::DateTime<Utc>);

impl CreateField for CreatedPseudoHeader {
    fn create_field(&self) -> Option<(ApSigningField, String)> {
        Some((
            ApSigningField::PseudoCreated,
            self.0.timestamp().to_string(),
        ))
    }
}

#[derive(Debug)]
struct ExpiresPseudoHeader(chrono::DateTime<Utc>);

impl CreateField for ExpiresPseudoHeader {
    fn create_field(&self) -> Option<(ApSigningField, String)> {
        Some((
            ApSigningField::PseudoExpires,
            self.0.timestamp().to_string(),
        ))
    }
}

#[derive(Debug)]
struct DigestHeader<'a>(&'a str);

impl CreateField for DigestHeader<'_> {
    fn create_field(&self) -> Option<(ApSigningField, String)> {
        Some((ApSigningField::Digest, self.0.to_owned()))
    }
}

impl<T: CreateField> CreateField for Option<T> {
    fn create_field(&self) -> Option<(ApSigningField, String)> {
        self.as_ref().and_then(T::create_field)
    }
}

macro_rules! signing_input {
    ($name:ident, $($field_name:ident => $field_type:path),+ ) => {
        #[derive(Debug)]
        struct $name<'a> {
            $($field_name: $field_type),+
        }

        impl SigningInput for $name<'_> {
            fn create_signing_input(&self) -> Vec<(ApSigningField, String)> {
                [$(self.$field_name.create_field()),+].into_iter().flatten().collect::<Vec<_>>()
            }
        }
    }
}

signing_input!(SigningInputGetRsaSha256,
    request_target => RequestTarget<'a>,
    host => HostPseudoHeader<'a>,
    date => DateHeader,
    expires => Option<ExpiresPseudoHeader>);

impl SigningParts for SigningInputGetRsaSha256<'_> {
    fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        None
    }

    fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
    }
}

signing_input!(SigningInputGetHs2019,
    request_target => RequestTarget<'a>,
    host => HostPseudoHeader<'a>,
    created => CreatedPseudoHeader,
    expires => Option<ExpiresPseudoHeader>);

impl SigningParts for SigningInputGetHs2019<'_> {
    fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        Some(&self.created.0)
    }

    fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
    }
}

signing_input!(SigningInputPostRsaSha256,
    request_target => RequestTarget<'a>,
    host => HostPseudoHeader<'a>,
    date => DateHeader,
    digest => DigestHeader<'a>,
    expires => Option<ExpiresPseudoHeader>);

impl SigningParts for SigningInputPostRsaSha256<'_> {
    fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        None
    }

    fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
    }
}

signing_input!(SigningInputPostHs2019,
    request_target => RequestTarget<'a>,
    host => HostPseudoHeader<'a>,
    created => CreatedPseudoHeader,
    digest => DigestHeader<'a>,
    expires => Option<ExpiresPseudoHeader>);

impl SigningParts for SigningInputPostHs2019<'_> {
    fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        Some(&self.created.0)
    }

    fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
        self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
    }
}

#[async_trait::async_trait]
impl ApClientService for ApClientServiceDefaultProvider {
    type Error = ApClientError;

    fn sign_request(
        &self,
        signing_key: ApSigningKey<'_>,
        signing_algorithm: SigningAlgorithm,
        request: impl SigningInput,
    ) -> Result<ApSignature, Self::Error> {
        let components = request.create_signing_input();

        let message = components
            .iter()
            .map(|(k, v)| format!("{}: {}", k.as_ref(), v))
            .collect::<Vec<_>>()
            .join("\n");

        let signature = signing_key
            .key
            .sign_base64(signing_algorithm, &message.into_bytes())?;

        Ok(ApSignature {
            key_id: signing_key.key_id.clone().into_owned(),
            algorithm: Some(signing_algorithm),
            created: request.get_created().cloned(),
            expires: request.get_expires().cloned(),
            headers: Some(ApSigningHeaders(IndexSet::from_iter(
                components.iter().map(|(k, _)| k).copied(),
            ))),
            signature,
        })
    }

    async fn signed_get(
        &self,
        signing_key: ApSigningKey<'_>,
        signing_algorithm: SigningAlgorithm,
        expires: Option<chrono::DateTime<Utc>>,
        url: &str,
    ) -> Result<Value, Self::Error> {
        let url = url.parse()?;

        let time_created = Utc::now();
        let signed = match signing_algorithm {
            SigningAlgorithm::RsaSha256 => self.sign_request(
                signing_key,
                signing_algorithm,
                SigningInputGetRsaSha256 {
                    request_target: RequestTarget {
                        url: &url,
                        method: Method::GET,
                    },
                    host: HostPseudoHeader(&url),
                    date: DateHeader(time_created),
                    expires: expires.map(ExpiresPseudoHeader),
                },
            )?,
            SigningAlgorithm::Hs2019 => self.sign_request(
                signing_key,
                signing_algorithm,
                SigningInputGetHs2019 {
                    request_target: RequestTarget {
                        url: &url,
                        method: Method::GET,
                    },
                    host: HostPseudoHeader(&url),
                    created: CreatedPseudoHeader(time_created),
                    expires: expires.map(ExpiresPseudoHeader),
                },
            )?,
        };
        let mut headers = HeaderMap::new();

        if !matches!(signing_algorithm, SigningAlgorithm::Hs2019) {
            headers.insert(
                http::header::DATE,
                HeaderValue::try_from(httpdate::fmt_http_date(time_created.into()))
                    .expect("date should always be a valid header value"),
            );
        }

        headers.insert(
            HeaderName::from_lowercase(b"signature").unwrap(),
            HeaderValue::try_from(signed.to_string())?,
        );

        Ok(self
            .client
            .get(url)
            .accept(ContentActivityStreams)
            .headers(headers)
            .json()
            .await?)
    }

    async fn signed_post(
        &self,
        signing_key: ApSigningKey<'_>,
        signing_algorithm: SigningAlgorithm,
        expires: Option<chrono::DateTime<Utc>>,
        url: &str,
        body: &Value,
    ) -> Result<Value, Self::Error> {
        let url = url.parse()?;
        let body_bytes = serde_json::to_vec(body)?;
        let mut sha = sha2::Sha256::new();
        sha.update(&body_bytes);
        let digest_raw = sha.finalize();
        use base64::prelude::*;
        let digest_base64 = format!("sha-256={}", BASE64_STANDARD.encode(digest_raw));
        let time_created = Utc::now();
        let signed = match signing_algorithm {
            SigningAlgorithm::RsaSha256 => self.sign_request(
                signing_key,
                signing_algorithm,
                SigningInputPostRsaSha256 {
                    request_target: RequestTarget {
                        url: &url,
                        method: Method::POST,
                    },
                    host: HostPseudoHeader(&url),
                    date: DateHeader(time_created),
                    digest: DigestHeader(&digest_base64),
                    expires: expires.map(ExpiresPseudoHeader),
                },
            )?,
            SigningAlgorithm::Hs2019 => self.sign_request(
                signing_key,
                signing_algorithm,
                SigningInputPostHs2019 {
                    request_target: RequestTarget {
                        url: &url,
                        method: Method::POST,
                    },
                    host: HostPseudoHeader(&url),
                    created: CreatedPseudoHeader(time_created),
                    digest: DigestHeader(&digest_base64),
                    expires: expires.map(ExpiresPseudoHeader),
                },
            )?,
        };

        let mut headers = HeaderMap::new();

        if !matches!(signing_algorithm, SigningAlgorithm::Hs2019) {
            headers.insert(
                http::header::DATE,
                HeaderValue::try_from(httpdate::fmt_http_date(time_created.into()))
                    .expect("date should always be a valid header value"),
            );
        }

        headers.insert(
            HeaderName::from_lowercase(b"digest").unwrap(),
            HeaderValue::try_from(digest_base64)?,
        );

        headers.insert(
            HeaderName::from_lowercase(b"signature").unwrap(),
            HeaderValue::try_from(signed.to_string())?,
        );

        Ok(self
            .client
            .builder(Method::POST, url)
            .accept(ContentActivityStreams)
            .content_type(ContentActivityStreams)
            .headers(headers)
            .body(body_bytes)
            .json()
            .await?)
    }
}

#[cfg(test)]
mod test {
    use std::{borrow::Cow, sync::Arc};

    use headers::UserAgent;
    use miette::IntoDiagnostic;
    use rsa::pkcs8::DecodePrivateKey;

    use crate::{
        ap_client::ApClientServiceDefaultProvider,
        client::federation_client::FederationClient,
        crypto::{ApHttpPrivateKey, SigningAlgorithm},
        ApClientService,
    };

    #[tokio::test]
    async fn should_request() -> miette::Result<()> {
        let key_id = std::env::var("MAG_TEST_KEY_ID").into_diagnostic()?;
        let key = std::env::var("MAG_TEST_PRIVATE_KEY").into_diagnostic()?;
        let url = std::env::var("MAG_TEST_FETCH_URL").into_diagnostic()?;

        let rsa_key = rsa::RsaPrivateKey::from_pkcs8_pem(key.trim()).into_diagnostic()?;
        let ap_client = ApClientServiceDefaultProvider {
            client: Arc::new(
                FederationClient::new(true, 128_000, 25, UserAgent::from_static("magnetar/0.42"))
                    .into_diagnostic()?,
            ),
        };

        let val = ap_client
            .signed_get(
                ApHttpPrivateKey::Rsa(Box::new(Cow::Owned(rsa_key)))
                    .create_signing_key(&key_id, SigningAlgorithm::Hs2019)
                    .into_diagnostic()?,
                SigningAlgorithm::Hs2019,
                None,
                &url,
            )
            .await
            .into_diagnostic()?;

        println!("{:#?}", val);
        Ok(())
    }
}
