use chrono::{DateTime, Utc};
use indexmap::IndexSet;
use magnetar_common::util::{FediverseTag, ValidName};
use magnetar_host_meta::Xrd;
use magnetar_webfinger::webfinger::WebFinger;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use strum::AsRefStr;
use url::{Host, Url};

use crate::crypto::{ApSigningKey, SigningAlgorithm};

pub mod ap_client;
pub mod client;
pub mod crypto;
pub mod lookup_flow;

/// The *visible* domain of fediverse handles, that gets resolved by WebFinger
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HostUnmapped(Host);

impl AsRef<Host> for HostUnmapped {
    fn as_ref(&self) -> &Host {
        &self.0
    }
}

/// The real domain of fediverse handles used for federation
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HostMapped(Host);

impl AsRef<Host> for HostMapped {
    fn as_ref(&self) -> &Host {
        &self.0
    }
}

#[async_trait::async_trait]
pub trait HostMetaResolverService: Send + Sync {
    type Error;

    async fn resolve(&self, host: &HostUnmapped) -> Result<Xrd, Self::Error>;
}

#[async_trait::async_trait]
pub trait WebFingerResolverService: Send + Sync {
    type Error;

    async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error>;

    async fn resolve(
        &self,
        template_url: &str,
        resolved_uri: &str,
    ) -> Result<WebFinger, Self::Error> {
        self.resolve_url(
            &template_url.replace(
                "{uri}",
                &percent_encoding::utf8_percent_encode(
                    resolved_uri,
                    percent_encoding::NON_ALPHANUMERIC,
                )
                .to_string(),
            ),
        )
        .await
    }
}

#[derive(Debug)]
pub struct UnmappedUser {
    pub name: ValidName,
    pub host: HostUnmapped,
}

impl From<&UnmappedUser> for FediverseTag {
    fn from(user: &UnmappedUser) -> Self {
        FediverseTag {
            name: user.name.clone(),
            host: Some(user.host.as_ref().clone()),
        }
    }
}

#[derive(Debug)]
pub struct MappedUser {
    pub host_meta: Option<Xrd>,
    pub webfinger: WebFinger,
    pub tag_mapped: FediverseTag,
    pub ap_url: Url,
}

#[async_trait::async_trait]
pub trait FederationLookupService {
    type Error;

    async fn map_fedi_tag(&self, user: &UnmappedUser) -> Result<MappedUser, Self::Error>;
}

#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, AsRefStr)]
pub enum ApSigningField {
    #[serde(rename = "(request-target)")]
    #[strum(serialize = "(request-target)")]
    PseudoRequestTarget,
    #[serde(rename = "(expires)")]
    #[strum(serialize = "(expires)")]
    PseudoExpires,
    #[serde(rename = "(created)")]
    #[strum(serialize = "(created)")]
    PseudoCreated,
    #[serde(rename = "date")]
    #[strum(serialize = "date")]
    Date,
    #[serde(rename = "host")]
    #[strum(serialize = "host")]
    Host,
    #[serde(rename = "digest")]
    #[strum(serialize = "digest")]
    Digest,
    #[serde(rename = "content-length")]
    #[strum(serialize = "content-length")]
    ContentLength,
}

#[derive(Debug)]
pub struct ApSignature {
    pub key_id: String,
    pub algorithm: Option<SigningAlgorithm>,
    pub created: Option<DateTime<Utc>>,
    pub expires: Option<DateTime<Utc>>,
    pub headers: Option<ApSigningHeaders>,
    pub signature: String,
}

#[derive(Debug)]
pub struct ApSigningHeaders(pub(crate) IndexSet<ApSigningField>);

pub trait SigningParts {
    fn get_created(&self) -> Option<&DateTime<Utc>>;
    fn get_expires(&self) -> Option<&DateTime<Utc>>;
}

pub trait SigningInput: SigningParts {
    fn create_signing_input(&self) -> Vec<(ApSigningField, String)>;
}

#[async_trait::async_trait]
pub trait ApClientService: Send + Sync {
    type Error;

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

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

    async fn signed_post(
        &self,
        signing_key: ApSigningKey<'_>,
        signing_algorithm: SigningAlgorithm,
        expires: Option<chrono::DateTime<Utc>>,
        url: &str,
        body: &Value,
    ) -> Result<Value, Self::Error>;
}
