use std::{io::Cursor, sync::Arc};

use magnetar_common::{
    config::MagnetarNetworkingProtocol,
    util::{FediverseTag, FediverseTagDisplay, FediverseTagParseError},
};
use magnetar_core::web_model::{acct::Acct, content_type::ContentJrdJson};
use magnetar_host_meta::{Xrd, XrdXml};
use magnetar_webfinger::webfinger::{WebFinger, WebFingerRel, WebFingerSubject};
use thiserror::Error;
use tracing::trace;
use url::Url;

use crate::{
    client::federation_client::{FederationClient, FederationClientError},
    FederationLookupService, HostMetaResolverService, HostUnmapped, MappedUser, UnmappedUser,
    WebFingerResolverService,
};

pub struct HostMetaResolverProviderDefault {
    client: Arc<FederationClient>,
    protocol: MagnetarNetworkingProtocol,
}

#[async_trait::async_trait]
impl HostMetaResolverService for HostMetaResolverProviderDefault {
    type Error = FederationClientError;

    async fn resolve(&self, HostUnmapped(host): &HostUnmapped) -> Result<Xrd, Self::Error> {
        let host_meta_xml = self
            .client
            .get(Url::parse(&format!(
                "{}://{}/.well-known/host-meta",
                self.protocol.as_ref(),
                host
            ))?)
            .send()
            .await?;

        let XrdXml::Xrd(xrd) = quick_xml::de::from_reader(Cursor::new(host_meta_xml))?;

        Ok(xrd)
    }
}

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

#[async_trait::async_trait]
impl WebFingerResolverService for WebFingerResolverProviderDefault {
    type Error = FederationClientError;

    async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error> {
        let host_meta_xml = self
            .client
            .get(Url::parse(url)?)
            .content_type(ContentJrdJson)
            .send()
            .await?;
        let webfinger = serde_json::from_reader(Cursor::new(host_meta_xml))?;
        Ok(webfinger)
    }
}

#[derive(Debug, Error)]
pub enum FederationLookupErrror {
    #[error("Federation client error: {0}")]
    FederationClientError(#[from] FederationClientError),
    #[error("Fediverse tag parse error: {0}")]
    FediverseTagParseError(#[from] FediverseTagParseError),
    #[error("URL parse error: {0}")]
    UrlParseError(#[from] url::ParseError),
    #[error("Missing ActivityStreams URL in WebFinger")]
    MissingApUrl,
    #[error("Missing Acct URI in WebFinger")]
    MissingAcctUri,
}

#[derive(Clone)]
pub struct FederationLookupServiceProviderDefault {
    host_meta_resolver: Arc<dyn HostMetaResolverService<Error = FederationClientError>>,
    webfinger_resolver: Arc<dyn WebFingerResolverService<Error = FederationClientError>>,
    protocol: MagnetarNetworkingProtocol,
}

impl FederationLookupServiceProviderDefault {
    pub fn new(
        host_meta_resolver: Arc<dyn HostMetaResolverService<Error = FederationClientError>>,
        webfinger_resolver: Arc<dyn WebFingerResolverService<Error = FederationClientError>>,
        protocol: MagnetarNetworkingProtocol,
    ) -> Self {
        Self {
            host_meta_resolver,
            webfinger_resolver,
            protocol,
        }
    }
}

#[async_trait::async_trait]
impl FederationLookupService for FederationLookupServiceProviderDefault {
    type Error = FederationLookupErrror;

    #[tracing::instrument(level = "trace", skip(self))]
    async fn map_fedi_tag(
        &self,
        user: &UnmappedUser,
    ) -> Result<MappedUser, FederationLookupErrror> {
        trace!("Fetching flow initiated");
        let host_meta = self.host_meta_resolver.resolve(&user.host).await;
        let webfinger_template = match &host_meta {
            Ok(h) => {
                trace!("host-meta found: {:?}", h);
                h.get_webfinger_template().map(str::to_owned)
            }
            Err(e) => {
                trace!("host-meta fetch failed: {}", e);
                None
            }
        }
        .unwrap_or_else(|| {
            Xrd::default_host_meta(self.protocol.as_ref(), &user.host.as_ref().to_string())
                .get_webfinger_template()
                .expect("default WebFinger template")
                .to_owned()
        });

        let webfinger = self
            .webfinger_resolver
            .resolve(
                &webfinger_template,
                Acct::from(FediverseTag::from(user)).as_ref(),
            )
            .await?;

        trace!("Webfinger fetched: {:?}", webfinger);

        let real_tag = match &webfinger.subject {
            WebFingerSubject::Acct(acct) => Some(acct.clone()),
            _ => webfinger
                .aliases
                .iter()
                .flatten()
                .find_map(|alias| match alias {
                    WebFingerSubject::Acct(acct) => Some(acct.clone()),
                    _ => None,
                }),
        }
        .ok_or(FederationLookupErrror::MissingAcctUri)?;

        let ap_url = webfinger
            .links
            .iter()
            .find_map(|link| match link {
                WebFingerRel::RelSelf { href, .. } | WebFingerRel::RelSelfAlt { href, .. } => {
                    Some(href)
                }
                _ => None,
            })
            .ok_or(FederationLookupErrror::MissingApUrl)?
            .parse()?;

        Ok(MappedUser {
            host_meta: host_meta.ok(),
            webfinger,
            tag_mapped: FediverseTag::try_from(&FediverseTagDisplay::try_from(&real_tag)?)?,
            ap_url,
        })
    }
}

#[cfg(test)]
mod test {
    use std::sync::Arc;

    use headers::UserAgent;
    use magnetar_common::{config::MagnetarNetworkingProtocol, util::FediverseTag};
    use tracing::{info, Level};

    use crate::{
        client::federation_client::FederationClient, FederationLookupService, HostUnmapped,
        UnmappedUser,
    };

    use super::{
        FederationLookupServiceProviderDefault, HostMetaResolverProviderDefault,
        WebFingerResolverProviderDefault,
    };

    #[tokio::test]
    async fn should_resolve() {
        tracing_subscriber::fmt()
            .with_max_level(Level::TRACE)
            .init();

        let client = Arc::new(
            FederationClient::new(true, 64000, 20, UserAgent::from_static("magnetar/0.42"))
                .unwrap(),
        );

        let federation_lookup = FederationLookupServiceProviderDefault::new(
            Arc::new(HostMetaResolverProviderDefault {
                protocol: MagnetarNetworkingProtocol::Https,
                client: client.clone(),
            }),
            Arc::new(WebFingerResolverProviderDefault { client }),
            MagnetarNetworkingProtocol::Https,
        );
        let tag: FediverseTag = "@natty@astolfo.social".parse().unwrap();
        info!("Resolving: {}", tag);
        let resolved = federation_lookup
            .map_fedi_tag(&UnmappedUser {
                name: tag.name,
                host: HostUnmapped(tag.host.unwrap()),
            })
            .await
            .unwrap();
        info!("Resolved: {:#?}", resolved);
    }
}
