Documentation
use crate::{PubkyHttpClient, PublicKey, Result, errors::RequestError};
use reqwest::{Method, RequestBuilder};
use url::Url;

mod features;
pub(crate) use features::HomeserverFeatures;
mod storage;
use storage::StorageAddressing;

#[cfg(not(target_arch = "wasm32"))]
pub mod native;
#[cfg(target_arch = "wasm32")]
pub mod wasm;

#[derive(Debug, Clone, PartialEq, Eq)]
enum TransportHost {
    PubkyQname(PublicKey),
    BarePublicKey(PublicKey),
    Other,
}

fn classify_transport_host(host: &str) -> Result<TransportHost> {
    let (host, is_pubky_qname) = host
        .strip_prefix("_pubky.")
        .map_or((host, false), |host| (host, true));

    if PublicKey::is_pubky_prefixed(host) {
        return Err(RequestError::Validation {
            message: "pubky prefix is not allowed in transport hosts; use raw z32".to_string(),
        }
        .into());
    }

    let Ok(public_key) = PublicKey::try_from_z32(host) else {
        return Ok(TransportHost::Other);
    };

    if is_pubky_qname {
        Ok(TransportHost::PubkyQname(public_key))
    } else {
        Ok(TransportHost::BarePublicKey(public_key))
    }
}

impl PubkyHttpClient {
    /// Build a request after resolving its transport and storage addressing.
    ///
    /// Use this for Pubky and PKDNS URLs. On native targets it selects `PubkyTLS` or
    /// ICANN/X.509 transport. It also falls back to legacy storage addressing when
    /// the homeserver does not advertise path-addressed storage.
    ///
    /// # Errors
    /// Returns a validation or resolution error if the request cannot be prepared.
    pub async fn request_async(&self, method: Method, url: Url) -> Result<RequestBuilder> {
        self.cross_request(method, url).await
    }

    async fn prepare_request_parts(
        &self,
        url: &mut Url,
    ) -> Result<(StorageAddressing, Option<String>)> {
        // Storage addressing must inspect the canonical URL before WASM transport rewrites it.
        let addressing = self.prepare_storage_addressing(url).await?;
        let pubky_host = self.prepare_transport_request(url).await?;
        Ok((addressing, pubky_host))
    }

    /// Prepare a URL's storage addressing and return its `pubky-host` value.
    ///
    /// This may rewrite path-addressed storage URLs for legacy homeservers. When the
    /// return value is `Some`, attach it to the request as the `pubky-host` header.
    /// Callers using `PubkyHttpClient` should prefer [`Self::request_async`], which also
    /// resolves the native transport.
    ///
    /// # Errors
    /// Returns a validation or resolution error if the URL cannot be prepared.
    pub async fn prepare_request(&self, url: &mut Url) -> Result<Option<String>> {
        let (addressing, pubky_host) = self.prepare_request_parts(url).await?;
        Ok(addressing.into_pubky_host(pubky_host))
    }

    /// Prepare a URL and browser-fetch metadata for the JavaScript bindings.
    ///
    /// # Errors
    /// Returns a validation or resolution error if the URL cannot be prepared.
    #[doc(hidden)]
    pub async fn prepare_fetch(&self, url: &mut Url) -> Result<crate::client::core::PreparedFetch> {
        let (addressing, pubky_host) = self.prepare_request_parts(url).await?;
        let is_pubky_target = pubky_host.is_some();

        Ok(crate::client::core::PreparedFetch {
            pubky_host_header: addressing.into_pubky_host(pubky_host),
            is_pubky_target,
        })
    }
}

fn homeserver_url(homeserver: &PublicKey, path: &str) -> Result<Url> {
    let path = if path.starts_with('/') {
        path.to_string()
    } else {
        format!("/{path}")
    };
    Ok(Url::parse(&format!(
        "https://{}{}",
        homeserver.z32(),
        path
    ))?)
}

pub(crate) fn user_endpoint_url(user: &PublicKey, path: &str) -> Result<Url> {
    let path = if path.starts_with('/') {
        path.to_string()
    } else {
        format!("/{path}")
    };
    Ok(Url::parse(&format!(
        "https://_pubky.{}{}",
        user.z32(),
        path
    ))?)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn classifies_transport_hosts() {
        let public_key = crate::Keypair::random().public_key();
        let z32 = public_key.z32();

        assert_eq!(
            classify_transport_host(&format!("_pubky.{z32}")).unwrap(),
            TransportHost::PubkyQname(public_key.clone())
        );
        assert_eq!(
            classify_transport_host(&z32).unwrap(),
            TransportHost::BarePublicKey(public_key)
        );
        assert_eq!(
            classify_transport_host("example.com").unwrap(),
            TransportHost::Other
        );
        assert_eq!(
            classify_transport_host("_pubky.example.com").unwrap(),
            TransportHost::Other
        );
    }

    #[test]
    fn rejects_pubky_prefixed_transport_hosts() {
        let prefixed = crate::Keypair::random().public_key().to_string();

        for host in [prefixed.clone(), format!("_pubky.{prefixed}")] {
            let error = classify_transport_host(&host).unwrap_err();
            assert!(error.to_string().contains("use raw z32"));
        }
    }

    #[test]
    fn user_endpoints_keep_authority_addressing() {
        let user = crate::Keypair::random().public_key();
        let url = user_endpoint_url(&user, "/auth/grant/session").unwrap();
        let expected_host = format!("_pubky.{}", user.z32());

        assert_eq!(url.host_str(), Some(expected_host.as_str()));
        assert_eq!(url.path(), "/auth/grant/session");
    }

    #[tokio::test]
    async fn path_addressed_requests_are_pubky_targets_without_a_header() {
        let client = PubkyHttpClient::builder()
            .isolated_pkarr_test()
            .build()
            .unwrap();
        let homeserver = crate::Keypair::random().public_key();
        let owner = crate::Keypair::random().public_key();
        client.features.insert(
            &homeserver,
            &[pubky_common::constants::features::PATH_ADDRESSED_STORAGE],
        );
        let mut url = Url::parse(&format!(
            "https://{}/storage/{}/pub/file.txt",
            homeserver.z32(),
            owner.z32()
        ))
        .unwrap();

        let mut native_url = url.clone();
        let pubky_host = client.prepare_request(&mut native_url).await.unwrap();
        let prepared = client.prepare_fetch(&mut url).await.unwrap();

        assert_eq!(pubky_host, None);
        assert!(prepared.is_pubky_target);
        assert_eq!(prepared.pubky_host_header, None);
    }

    #[tokio::test]
    async fn legacy_requests_rewrite_the_path_and_return_the_owner_header() {
        let client = PubkyHttpClient::builder()
            .isolated_pkarr_test()
            .build()
            .unwrap();
        let homeserver = crate::Keypair::random().public_key();
        let owner = crate::Keypair::random().public_key();
        client.features.insert(&homeserver, &[]);
        let mut url = Url::parse(&format!(
            "https://{}/storage/{}/pub/file.txt",
            homeserver.z32(),
            owner.z32()
        ))
        .unwrap();

        let pubky_host = client.prepare_request(&mut url).await.unwrap();

        assert_eq!(url.path(), "/pub/file.txt");
        assert_eq!(pubky_host, Some(owner.z32()));
    }
}