use std::str::FromStr;

use base64::Engine;
use http::header::{ACCEPT, CONTENT_TYPE};
use rand::rand_core::OsError;

use rand::TryRngCore;
pub use secrecy::ExposeSecret;
use secrecy::SecretString;
use sha2::Digest;
use sha2::Sha256;
use sha2::Sha512;
use url::Url;

use crate::http::CONTENT_TYPE_FORM_URLENCODED;
use crate::{
    algorithms::link_rel,
    http::{Body, CONTENT_TYPE_JSON},
    traits::as_string_or_list,
};

#[derive(thiserror::Error, Debug, miette::Diagnostic)]
pub enum Error {
    #[error("No metadata endpoint could be found.")]
    NoMetadataEndpoint,

    #[error("The metadata endpoint did not declare itself as JSON but as {content_type:?}")]
    MetadataContentTypeInvalid { content_type: String },

    #[error("Failed to parse the JSON of the metadata endpoint.")]
    ParseMetadataJson(#[source] serde_json::Error),

    #[error("Client IDs using IndieAuth have to be a URL.")]
    ClientIdMustBeUrl,

    #[error("A scope can't be an empty string.")]
    EmptyScope,

    #[error("The size of the raw challenge does not comply with the specification.")]
    ChallengeOutOfBounds(usize),

    #[error("Failed to parse the JSON of the redemption response.")]
    ParseRedemption {
        #[source]
        source: serde_json::Error,
        body: String,
    },

    #[error("Failed to parse the JSON of the metadata endpoint.")]
    ParseTokenRedemption(#[source] serde_json::Error),

    #[error("The redemeption endpoint at {url:?} did not declare itself as JSON but as {content_type:?}")]
    RedemptionResponseContentType { url: Url, content_type: String },

    #[error("Failed to invoke the random number generator: {0:#?}")]
    Random(OsError),

    #[error("The client ID must be a HTTP-based URL.")]
    ClientIdMustBeHttp,
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub struct Scope(String);

#[derive(Clone, Debug, serde::Deserialize)]
pub struct AccessToken(SecretString);

impl std::ops::Deref for AccessToken {
    type Target = SecretString;

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

impl AccessToken {
    pub fn new(token: impl ToString) -> Self {
        Self(SecretString::new(token.to_string().into_boxed_str()))
    }
}

impl Scope {
    pub fn new(scope: impl ToString) -> Result<Self, crate::Error> {
        let scope_str = scope.to_string();
        if scope_str.is_empty() {
            return Err(Error::EmptyScope.into());
        }

        Ok(Self(scope_str))
    }
}

impl From<String> for Scope {
    fn from(scope: String) -> Self {
        Self(scope)
    }
}

impl std::ops::Deref for Scope {
    type Target = String;

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

/// A representation of a list of [scopes][Scope].
///
/// This structure allows for the normalization of common behaviors when working
/// with scopes.
///
/// # Examples
/// ```
/// # use indieweb::standards::indieauth::Scopes;
/// # use std::str::FromStr;
/// #
/// let scopes = Scopes::from_str("read create:read").unwrap();
///
/// assert!(scopes.has("read"),
///         "can report if it has a scope");
///
/// assert_eq!(scopes.assert("aircraft"),
///            Err(indieweb::Error::MissingScope(Scopes::from_str("aircraft").unwrap())),
///            "can report if it does not have a scope");
///
/// assert_eq!(scopes.to_string(),
///            "read create:read".to_string(),
///            "easy to pass as a string");
/// ```
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Scopes(Vec<Scope>);

impl std::fmt::Display for Scopes {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(
            &self
                .0
                .iter()
                .map(|s| s.to_string())
                .collect::<Vec<_>>()
                .join(" "),
        )
    }
}

impl FromStr for Scopes {
    type Err = crate::Error;

    fn from_str(scopes_string: &str) -> Result<Self, Self::Err> {
        scopes_string
            .split(' ')
            .try_fold(Vec::default(), |mut acc, scope_str| {
                acc.push(Scope::new(scope_str)?);
                Ok(acc)
            })
            .map(Self)
    }
}

impl Scopes {
    /// Reports if this has no scopes defined.
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// assert_eq!(
    ///     Ok(false),
    ///     Scopes::from_str("read")
    ///         .map(|scopes| scopes.is_empty()),
    ///     "there's at least one scope in here"
    ///     )
    /// ```
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Confirms if there's a scope matching or beginning to match with the provided value.
    ///
    /// This works with matching a direct scope (like 'create') or a scope with prefixes in
    /// its authored order (like 'update:note'). There's no real "format" to what a scope
    /// can look like, that's completely up to your implementation. For example, capitalist
    /// companies like [Slack](https://en.wikipedia.org/wiki/Slack_(software)) use a combination
    /// of colons and dots for scopes (like "files.metadata:read") and monopolies like
    /// [Google](https://en.wikipedia.org/wiki/Google) use URLs as scopes.
    ///
    /// # Examples
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// #
    /// let scopes = Scopes::from_str("read update:note").unwrap();
    /// assert!(scopes.has("read"));
    /// assert!(scopes.has("update:note"));
    /// ```
    pub fn has(&self, scope: &str) -> bool {
        self.0.iter().any(|s| s.to_string().starts_with(scope))
    }

    /// Reports if this has no scopes defined.
    ///
    /// Alias for [`has()`][Scopes::has] for more idiomatic usage.
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// #
    /// let scopes = Scopes::from_str("read update:note").unwrap();
    /// assert!(scopes.contains("read"));
    /// assert!(scopes.contains("update:note"));
    /// ```
    pub fn contains(&self, scope: &str) -> bool {
        self.has(scope)
    }

    /// Returns the scopes as a list.
    pub fn as_vec(&self) -> &Vec<Scope> {
        &self.0
    }

    /// Confirm the presence of a provided scope.
    ///
    /// # Examples
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// #
    /// let scopes = "read create:note".parse::<Scopes>();
    ///
    /// assert_eq!(
    ///     Ok(()),
    ///     scopes.as_ref().unwrap().assert("read"),
    ///     "confirms explicit scope match without prefix");
    /// assert_eq!(
    ///     Ok(()),
    ///     scopes.as_ref().unwrap().assert("create:note"),
    ///     "confirms explicit scope match with prefix");
    /// assert_eq!(
    ///     Err(indieweb::Error::MissingScope(Scopes::from_str("airplane").unwrap())),
    ///     scopes.as_ref().unwrap().assert("create:note airplane"),
    ///     "denies provided scopes if one is not valid");
    /// ```
    pub fn assert(&self, needed_scopes: &str) -> Result<(), crate::Error> {
        tracing::debug!("Asserting that {:?} exists in {:?}", needed_scopes, self);
        let scope_list: Scopes = needed_scopes.parse().unwrap_or_else(|_| Scopes::default());

        let missing_scopes: Scopes = scope_list
            .as_vec()
            .iter()
            .filter(|expected_scope| !self.has(expected_scope))
            .cloned()
            .collect::<Vec<_>>()
            .into();

        if missing_scopes.is_empty() {
            Ok(())
        } else {
            Err(crate::Error::MissingScope(missing_scopes))
        }
    }

    /// Confirm the presence of a provided scope.
    ///
    /// Alias for [`assert()`][Scopes::assert] for more idiomatic usage.
    ///
    /// # Examples
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    /// #
    /// let scopes = "read create:note".parse::<Scopes>();
    ///
    /// assert_eq!(
    ///     Ok(()),
    ///     scopes.as_ref().unwrap().require("read"),
    ///     "confirms explicit scope match without prefix");
    /// assert_eq!(
    ///     Ok(()),
    ///     scopes.as_ref().unwrap().require("create:note"),
    ///     "confirms explicit scope match with prefix");
    /// assert_eq!(
    ///     Err(indieweb::Error::MissingScope(Scopes::from_str("airplane").unwrap())),
    ///     scopes.as_ref().unwrap().require("create:note airplane"),
    ///     "denies provided scopes if one is not valid");
    /// ```
    pub fn require(&self, needed_scopes: &str) -> Result<(), crate::Error> {
        self.assert(needed_scopes)
    }

    /// Returns a list of scopes that are matching the list of scopes; a union of the two lists.
    ///
    /// # Examples
    ///
    /// ```
    /// # use indieweb::standards::indieauth::Scopes;
    /// # use std::str::FromStr;
    ///
    /// let wanted_scopes = Scopes::from_str("read update:note").unwrap();
    /// let scopes = Scopes::from_str("read create update:note").unwrap();
    /// assert_eq!(scopes.matching("read update:note"), wanted_scopes);
    /// ```
    pub fn matching(&self, wanted_scopes: &str) -> Self {
        wanted_scopes
            .parse::<Self>()
            .unwrap_or_default()
            .as_vec()
            .iter()
            .filter(|expected_scope| self.has(expected_scope))
            .cloned()
            .collect::<Scopes>()
    }

    pub fn minimal() -> Self {
        Self(vec![Scope("read".to_string())])
    }
}

impl From<Vec<String>> for Scopes {
    fn from(scopes: Vec<String>) -> Self {
        scopes.into_iter().map(Scope::from).collect()
    }
}

impl FromIterator<Scope> for Scopes {
    fn from_iter<T: IntoIterator<Item = Scope>>(iter: T) -> Self {
        Self(iter.into_iter().collect::<Vec<_>>())
    }
}

impl From<Vec<Scope>> for Scopes {
    fn from(scopes: Vec<Scope>) -> Self {
        Self(scopes)
    }
}

impl From<Scopes> for Vec<Scope> {
    fn from(val: Scopes) -> Self {
        val.0
    }
}

/// Represents how IndieAuth endpoints should be discovered.
///
/// This enum provides two modes of operation:
/// - **Classic**: Explicitly provide authorization, token, and optionally ticket endpoints
/// - **Metadata**: Provide a metadata URL that will be used to auto-discover all endpoints
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EndpointDiscovery {
    /// Classic discovery with explicit endpoints
    Classic {
        /// The authorization endpoint URL
        authorization: Url,
        /// The token endpoint URL
        token: Url,
        /// Optional ticket endpoint URL
        ticket: Option<Url>,
    },
    /// Metadata-based discovery from a metadata endpoint
    Metadata {
        /// The metadata endpoint URL
        metadata: Url,
    },
}

impl serde::Serialize for Scopes {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> serde::de::Deserialize<'de> for Scopes {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        as_string_or_list::deserialize(deserializer)?
            .into_iter()
            .filter(|s: &String| !s.is_empty())
            .collect::<Vec<_>>()
            .join(" ")
            .parse()
            .map_err(serde::de::Error::custom)
    }
}

#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
pub enum CodeChallengeMethod {
    S256,
    S512,
    Plain,
}

impl CodeChallengeMethod {
    pub fn recommended() -> Self {
        Self::S256
    }

    pub fn validate(
        &self,
        CodeChallenge { challenge, .. }: &CodeChallenge,
        verifier: &str,
    ) -> bool {
        let CodeChallenge {
            challenge: expected_challenge,
            ..
        } = CodeChallenge::from_verifier(self, verifier.to_string());

        expected_challenge == *challenge
    }
}

#[allow(clippy::to_string_trait_impl)]
impl ToString for CodeChallengeMethod {
    fn to_string(&self) -> String {
        match self {
            Self::S256 => "S256".to_string(),
            Self::S512 => "S512".to_string(),
            Self::Plain => "PLAIN".to_string(),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CodeChallenge {
    challenge: String,
    verifier: String,
}

impl serde::Serialize for CodeChallenge {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.challenge.serialize(serializer)
    }
}

impl<'de> serde::Deserialize<'de> for CodeChallenge {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        Ok(Self {
            challenge: String::deserialize(deserializer)?,
            verifier: Default::default(),
        })
    }
}

impl std::ops::Deref for CodeChallenge {
    type Target = String;

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

impl CodeChallenge {
    pub fn from_verifier(method: &CodeChallengeMethod, verifier: String) -> Self {
        let challenge_bytes = match method {
            CodeChallengeMethod::S256 => {
                let mut hasher = Sha256::new();
                hasher.update(verifier.as_bytes());
                hasher.finalize().to_vec()
            }
            CodeChallengeMethod::S512 => {
                let mut hasher = Sha512::new();
                hasher.update(verifier.as_bytes());
                hasher.finalize().to_vec()
            }
            CodeChallengeMethod::Plain => verifier.as_bytes().to_vec(),
        };
        let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(challenge_bytes);

        Self {
            challenge,
            verifier,
        }
    }

    pub fn generate(method: CodeChallengeMethod) -> Result<(Self, CodeChallengeMethod), Error> {
        let mut bytes = [0u8; 64];
        let mut rng = rand::rngs::OsRng {};
        rng.try_fill_bytes(&mut bytes[..]).map_err(Error::Random)?;
        let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
        let challenge = Self::from_verifier(&method, verifier);

        Ok((challenge, method))
    }

    pub fn verifier(&self) -> &str {
        &self.verifier
    }
}

#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq)]
pub struct AuthorizationRequestFields {
    pub client_id: ClientId,
    pub redirect_uri: RedirectUri,
    pub state: String,

    #[serde(rename = "code_challenge")]
    pub challenge: CodeChallenge,

    #[serde(rename = "code_challenge_method")]
    pub challenge_method: CodeChallengeMethod,

    #[serde(rename = "scope", default = "Scopes::minimal")]
    pub scope: Scopes,
}

impl AuthorizationRequestFields {
    /// Forms a new authorization request for the intending client to the expecting location.
    pub fn new(
        client_id: &ClientId,
        redirect_uri: &Url,
        state: impl ToString,
    ) -> Result<Self, crate::Error> {
        let (code_challenge, code_challenge_method) =
            CodeChallenge::generate(CodeChallengeMethod::recommended())?;

        Ok(Self {
            client_id: client_id.clone(),
            redirect_uri: redirect_uri.clone().into(),
            state: state.to_string(),
            challenge: code_challenge,
            challenge_method: code_challenge_method,
            scope: Default::default(),
        })
    }

    pub fn into_authorization_url(
        self,
        authorization_endpoint_url: impl Into<Url>,
        extra_fields: Vec<(String, String)>,
    ) -> Result<Url, Error> {
        let mut signed_authorization_endpoint_url = authorization_endpoint_url.into();
        let mut fields = signed_authorization_endpoint_url.query_pairs_mut();
        fields
            .append_pair("response_type", "code")
            .append_pair("client_id", &self.client_id)
            .append_pair("redirect_uri", self.redirect_uri.as_str())
            .append_pair("state", &self.state)
            .append_pair("code_challenge", &self.challenge)
            .append_pair("code_challenge_method", &self.challenge_method.to_string());

        if !self.scope.is_empty() {
            fields.append_pair("scope", &self.scope.to_string());
        }

        for (name, value) in extra_fields {
            fields.append_pair(&name, &value);
        }

        fields.finish();
        drop(fields);

        Ok(signed_authorization_endpoint_url)
    }
}

#[derive(Clone, serde::Deserialize, Debug)]
pub struct RedemptionFields {
    pub code: String,
    pub client_id: ClientId,
    pub redirect_uri: RedirectUri,
    #[serde(rename = "code_verifier")]
    pub verifier: String,
}

impl RedemptionFields {
    /// Convert these fields into query parameters for form-encoded POST.
    pub fn into_query_parameters(self) -> Vec<(String, String)> {
        vec![
            ("grant_type".to_string(), "authorization_code".to_string()),
            ("code".to_string(), self.code),
            ("client_id".to_string(), self.client_id.0),
            ("redirect_uri".to_string(), self.redirect_uri.to_string()),
            ("code_verifier".to_string(), self.verifier),
        ]
    }

    /// Convert these fields into an HTTP POST request for token redemption.
    ///
    /// This follows the idiomatic pattern used by popular Rust API clients
    /// like the `gitlab` crate, allowing request construction separate from execution.
    pub fn into_request(self, endpoint: &Url) -> Result<http::Request<crate::http::Body>, crate::Error> {
        use http::header;
        
        let body = self
            .into_query_parameters()
            .into_iter()
            .map(|(name, value)| format!("{name}={value}"))
            .collect::<Vec<_>>()
            .join("&");

        http::Request::post(endpoint.as_str())
            .method(http::Method::POST)
            .header(header::ACCEPT, "application/json")
            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
            .body(crate::http::Body::Bytes(body.into_bytes()))
            .map_err(|e| crate::Error::from(e))
    }
}

#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ServerMetadata {
    pub issuer: Url,
    pub authorization_endpoint: Url,
    pub token_endpoint: Url,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ticket_endpoint: Option<Url>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub introspection_endpoint: Option<Url>,

    #[serde(default = "ServerMetadata::recommended_code_challenge_methods")]
    pub code_challenge_methods_supported: Vec<String>,

    #[serde(default, skip_serializing_if = "Scopes::is_empty")]
    pub scopes_supported: Scopes,
}

impl ServerMetadata {
    pub fn recommended_code_challenge_methods() -> Vec<String> {
        vec!["S256".to_string()]
    }

    /// A helper method for starting an authorization request with a [request payload][AuthorizationRequestPayload].
    pub fn new_authorization_request_url(
        &self,
        request: AuthorizationRequestFields,
        extra_fields: Vec<(String, String)>,
    ) -> Result<Url, crate::Error> {
        Ok(request.into_authorization_url(self.authorization_endpoint.clone(), extra_fields)?)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ClientId(String);

impl ClientId {
    pub fn new(client_id: impl ToString) -> Result<Self, crate::Error> {
        let client_id_str = client_id.to_string();

        if !(client_id_str.starts_with("http://") || client_id_str.starts_with("https://")) {
            return Err(Error::ClientIdMustBeHttp.into());
        }

        if client_id_str.parse::<Url>().is_ok() {
            Ok(Self(client_id_str))
        } else {
            Err(Error::ClientIdMustBeUrl.into())
        }
    }
}



impl std::ops::Deref for ClientId {
    type Target = String;

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

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RedirectUri(Url);

impl std::ops::Deref for RedirectUri {
    type Target = Url;

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

impl From<Url> for RedirectUri {
    fn from(value: Url) -> Self {
        Self(value)
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseErrorCode {
    InvalidRequest,
    UnauthorizedClient,
    AccessDenied,
    UnsupportedResponseType,
    InvalidScope,
    ServerError,
    TemporarilyUnavailable,
}

impl std::fmt::Display for ResponseErrorCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            ResponseErrorCode::InvalidRequest => "invalid_request",
            ResponseErrorCode::UnauthorizedClient => "unauthorized_client",
            ResponseErrorCode::AccessDenied => "access_denied",
            ResponseErrorCode::UnsupportedResponseType => "unsupported_response_type",
            ResponseErrorCode::InvalidScope => "invalid_scope",
            ResponseErrorCode::ServerError => "server_error",
            ResponseErrorCode::TemporarilyUnavailable => "temporarily_unavailable",
        })
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
pub struct RedirectErrorFields {
    pub state: String,

    #[serde(rename = "error")]
    pub reason: ResponseErrorCode,

    #[serde(rename = "error_description", skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    #[serde(rename = "error_uri", skip_serializing_if = "Option::is_none")]
    pub uri: Option<Url>,
}

impl RedirectErrorFields {
    pub fn into_query_parameters(self) -> Vec<(String, String)> {
        let mut qps = Vec::default();

        qps.push(("state".to_string(), self.state));
        if let Some(description) = self.description {
            qps.push(("error_description".to_string(), description));
        }

        if let Some(uri) = self.uri {
            qps.push(("error_uri".to_string(), uri.to_string()));
        }

        qps.push(("error".to_string(), self.reason.to_string()));

        qps
    }
}

#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
pub struct SignedRedirectFields {
    pub state: String,
    pub code: String,
    #[serde(rename = "iss")]
    pub issuer: Url,
}

impl SignedRedirectFields {
    pub fn into_query_parameters(self) -> Vec<(String, String)> {
        vec![
            ("state".to_string(), self.state),
            ("code".to_string(), self.code),
            ("iss".to_string(), self.issuer.to_string()),
        ]
    }
}

impl RedirectUri {
    pub fn for_approved_request(&self, fields: SignedRedirectFields) -> Url {
        let mut u = self.0.clone();
        let mut qp = u.query_pairs_mut();
        qp.extend_pairs(fields.into_query_parameters());

        qp.finish();
        drop(qp);
        u
    }

    pub fn for_rejected_request(&self, fields: RedirectErrorFields) -> Url {
        let mut u = self.0.clone();
        let mut qp = u.query_pairs_mut();
        qp.extend_pairs(fields.into_query_parameters());
        qp.finish();
        drop(qp);
        u
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum RedemptionTokenType {
    Bearer,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct RedemptionClaim<Payload> {
    pub token_type: RedemptionTokenType,
    pub access_token: String,
    pub scope: Scopes,
    pub me: Url,

    #[serde(default)]
    pub expires_in: i64,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub refresh_token: Option<String>,

    pub payload: Payload,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct RedemptionError {
    #[serde(rename = "error")]
    pub code: ResponseErrorCode,

    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        rename = "error_description"
    )]
    pub description: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none", rename = "error_uri")]
    pub uri: Option<String>,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged, rename_all = "snake_case")]
pub enum RedemptionResponse<Payload> {
    /// A [successful response][RedemptionClaim] from a code redemption endpoint.
    Claim(RedemptionClaim<Payload>),

    /// A [failing resonse][RedemptionError] from a code redemption endpoint.
    Error(RedemptionError),
}

#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct ProfileRedemptionFields {
    pub name: String,
    pub url: Url,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub photo: Option<Url>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
}

#[derive(serde::Deserialize, serde::Serialize, Default, Debug, Clone, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
#[serde(untagged, rename_all = "snake_case")]
pub enum CommonRedemptionFields {
    Profile(ProfileRedemptionFields),
    #[default]
    Empty,
}

pub struct Client<HttpClient: crate::http::Client> {
    pub id: ClientId,
    client: HttpClient,
    #[allow(dead_code)]
    pub(crate) discovery: Option<EndpointDiscovery>,
}

/// Type alias for a Client with the default reqwest HTTP client.
///
/// This provides a more ergonomic API for common use cases without requiring
/// explicit type parameters.
///
/// # Example
///
/// ```
/// # use indieweb::standards::indieauth::IndieAuthClient;
/// # use indieweb::http::reqwest::Client as HttpClient;
/// # async {
/// let http_client = HttpClient::default();
/// let client = IndieAuthClient::builder()
///     .id("https://example.com")
///     .client(http_client)
///     .build()?;
/// # Ok::<_, indieweb::Error>(()) }.
/// ```
#[cfg(feature = "reqwest")]
pub type IndieAuthClient = Client<crate::http::reqwest::Client>;

/// Builder for creating a [`Client`] with optional configuration.
///
/// # Example
/// ```
/// # use indieweb::http::reqwest::Client as HttpClient;
/// # use indieweb::standards::indieauth::Client;
/// # async {
/// let http_client = HttpClient::default();
/// let client = Client::builder()
///     .id("https://example.com")
///     .client(http_client)
///     .build()?;
/// # Ok::<_, indieweb::Error>(()) }.
/// ```
#[derive(Debug)]
pub struct ClientBuilder<HttpClient: crate::http::Client> {
    id: Option<ClientId>,
    client: Option<HttpClient>,
    discovery: Option<EndpointDiscovery>,
}

impl<HttpClient: crate::http::Client> ClientBuilder<HttpClient> {
    /// Create a new builder.
    pub fn new() -> Self {
        Self {
            id: None,
            client: None,
            discovery: None,
        }
    }

    /// Set the client ID.
    pub fn id(mut self, id: impl ToString) -> Self {
        self.id = ClientId::new(id.to_string()).ok();
        self
    }

    /// Set the HTTP client.
    pub fn client(mut self, client: HttpClient) -> Self {
        self.client = Some(client);
        self
    }

    /// Set the endpoint discovery method.
    ///
    /// This allows you to specify either explicit endpoints (Classic) or a metadata URL
    /// for automatic endpoint discovery.
    pub fn discovery(mut self, discovery: EndpointDiscovery) -> Self {
        self.discovery = Some(discovery);
        self
    }

    /// Build the client.
    ///
    /// Returns an error if `id` or `client` were not set.
    pub fn build(self) -> Result<Client<HttpClient>, crate::Error> {
        let id = self.id.ok_or(Error::ClientIdMustBeUrl)?;
        let client = self.client.ok_or_else(|| {
            crate::Error::from(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "HTTP client not provided",
            ))
        })?;
        Ok(Client {
            id,
            client,
            discovery: self.discovery,
        })
    }
}

impl<HttpClient: crate::http::Client> Default for ClientBuilder<HttpClient> {
    fn default() -> Self {
        Self::new()
    }
}

impl<HttpClient: crate::http::Client> Client<HttpClient> {
    const LINK_REL: &'static str = "indieauth-metadata";

    /// Create a new client with the given ID and HTTP client.
    #[deprecated(since = "0.8.0", note = "Use Client::builder() instead")]
    pub fn new(id: impl ToString, http_client: HttpClient) -> Result<Self, crate::Error> {
        Self::builder()
            .id(id)
            .client(http_client)
            .build()
    }

    /// Create a new builder for a [`Client`].
    pub fn builder<HC: crate::http::Client>() -> ClientBuilder<HC> {
        ClientBuilder::new()
    }

    pub async fn obtain_metadata(&self, remote_url: &Url) -> Result<ServerMetadata, crate::Error> {
        // First, try HEAD request for all rel types
        let head_rels = link_rel::for_url(
            &self.client,
            remote_url,
            &[Self::LINK_REL, "authorization_endpoint", "token_endpoint"],
            "HEAD",
        )
        .await?;

        let mut rels = head_rels
            .get(Self::LINK_REL)
            .cloned()
            .unwrap_or_default();

        let mut individual_endpoints = (
            head_rels
                .get("authorization_endpoint")
                .cloned()
                .unwrap_or_default(),
            head_rels
                .get("token_endpoint")
                .cloned()
                .unwrap_or_default(),
        );

        // If we found individual endpoints from HEAD and no metadata, use them
        if rels.is_empty() && !individual_endpoints.0.is_empty() && !individual_endpoints.1.is_empty()
            && let (Some(auth_endpoint), Some(token_endpoint)) =
                (individual_endpoints.0.first(), individual_endpoints.1.first()) {
                let issuer = if remote_url.has_authority() {
                    format!("{}://{}", remote_url.scheme(), remote_url.authority())
                } else {
                    format!(
                        "{}://{}",
                        remote_url.scheme(),
                        remote_url.host_str().unwrap_or_default()
                    )
                }
                .parse()?;

                let metadata = ServerMetadata {
                    issuer,
                    authorization_endpoint: auth_endpoint.clone(),
                    token_endpoint: token_endpoint.clone(),
                    ticket_endpoint: None,
                    introspection_endpoint: None,
                    code_challenge_methods_supported: ServerMetadata::recommended_code_challenge_methods(),
                    scopes_supported: Scopes::default(),
                };

                return Ok(metadata);
        }

        if rels.is_empty() {
            // If HEAD didn't find indieauth-metadata, try GET for all rel types
            let all_rels = link_rel::for_url(
                &self.client,
                remote_url,
                &[Self::LINK_REL, "authorization_endpoint", "token_endpoint"],
                "GET",
            )
            .await?;

            // Check for indieauth-metadata first
            rels = all_rels
                .get(Self::LINK_REL)
                .cloned()
                .unwrap_or_default();

            // Update individual endpoints (GET might find more in body)
            let auth_endpoints = all_rels
                .get("authorization_endpoint")
                .cloned()
                .unwrap_or_default();
            let token_endpoints = all_rels
                .get("token_endpoint")
                .cloned()
                .unwrap_or_default();

            individual_endpoints = (auth_endpoints, token_endpoints);
        }

        let metadata_endpoint_url = if let Some(rel) = rels.first().cloned() {
            rel
        } else {
            let well_known_url: Url = if remote_url.has_authority() {
                format!(
                    "{}://{}/.well-known/oauth-authorization-server",
                    remote_url.scheme(),
                    remote_url.authority()
                )
            } else {
                format!(
                    "{}://{}/.well-known/oauth-authorization-server",
                    remote_url.scheme(),
                    remote_url.host_str().unwrap_or_default()
                )
            }
            .parse()?;
            let req = crate::http::Request::builder()
                .uri(well_known_url.as_str())
                .header(ACCEPT, CONTENT_TYPE_JSON)
                .body(Body::Empty)?;

            let resp = self.client.send_request(req).await?;

            let content_type_header_value = resp
                .headers()
                .get(CONTENT_TYPE)
                .and_then(|hv| hv.to_str().ok())
                .unwrap_or_default();

            if content_type_header_value.starts_with(CONTENT_TYPE_JSON) {
                let body = resp.body();
                if let Ok(metadata) = serde_json::from_slice::<ServerMetadata>(body.as_bytes()) {
                    return Ok(metadata);
                }
            }

            // Fallback: Try to find individual authorization_endpoint and token_endpoint rels
            // (already fetched in the GET request above)
            let (auth_endpoints, token_endpoints) = &individual_endpoints;
            if let (Some(auth_endpoint), Some(token_endpoint)) =
                (auth_endpoints.first(), token_endpoints.first())
            {
                // Construct issuer URL from the profile URL
                let issuer = if remote_url.has_authority() {
                    format!("{}://{}", remote_url.scheme(), remote_url.authority())
                } else {
                    format!(
                        "{}://{}",
                        remote_url.scheme(),
                        remote_url.host_str().unwrap_or_default()
                    )
                }
                .parse()?;

                let metadata = ServerMetadata {
                    issuer,
                    authorization_endpoint: auth_endpoint.clone(),
                    token_endpoint: token_endpoint.clone(),
                    ticket_endpoint: None,
                    introspection_endpoint: None,
                    code_challenge_methods_supported: ServerMetadata::recommended_code_challenge_methods(),
                    scopes_supported: Scopes::default(),
                };

                return Ok(metadata);
            }

            return Err(Error::NoMetadataEndpoint.into());
        };

        let req = crate::http::Request::builder()
            .uri(metadata_endpoint_url.as_str())
            .header(ACCEPT, CONTENT_TYPE_JSON)
            .body(Body::Empty)?;

        let resp = self
            .client
            .send_request(req)
            .await?
            .map(|body| body.as_bytes().to_vec());

        let content_type_header_value = resp
            .headers()
            .get(CONTENT_TYPE)
            .map(|hv| hv.to_str().unwrap_or_default().to_string())
            .unwrap_or_default();

        if !content_type_header_value.starts_with(CONTENT_TYPE_JSON) {
            return Err(Error::MetadataContentTypeInvalid {
                content_type: content_type_header_value,
            }
            .into());
        }

        Ok(serde_json::from_slice(&resp.into_body()).map_err(Error::ParseMetadataJson)?)
    }

    pub async fn redeem<R>(
        &self,
        endpoint: &Url,
        redemption_fields: RedemptionFields,
    ) -> Result<RedemptionResponse<R>, crate::Error>
    where
        R: serde::de::DeserializeOwned,
    {
        let fields = redemption_fields
            .into_query_parameters()
            .into_iter()
            .map(|(name, value)| format!("{name}={value}"))
            .collect::<Vec<_>>()
            .join("&");
        let req = crate::http::Request::builder()
            .uri(endpoint.as_str())
            .method("POST")
            .header(ACCEPT, CONTENT_TYPE_JSON)
            .header(CONTENT_TYPE, CONTENT_TYPE_FORM_URLENCODED)
            .body(Body::Bytes(fields.into_bytes()))?;

        let resp = self
            .client
            .send_request(req)
            .await?
            .map(|b| b.as_bytes().to_vec());

        let content_type_header_value = resp
            .headers()
            .get(CONTENT_TYPE)
            .map(|hv| hv.to_str().unwrap_or_default().to_string())
            .unwrap_or_default();

        let body_str = String::from_utf8(resp.body().to_vec())?;

        if !content_type_header_value.starts_with(CONTENT_TYPE_JSON) {
            return Err(Error::RedemptionResponseContentType {
                url: endpoint.to_owned(),
                content_type: content_type_header_value,
            }
            .into());
        }

        serde_json::from_str(&body_str).map_err(|e| {
            Error::ParseRedemption {
                source: e,
                body: body_str,
            }
            .into()
        })
    }
}
mod test;
