use crate::{algorithms::ptd, mf2, traits::as_string_or_list};
use http::{
    header::{ACCEPT, CONTENT_TYPE},
    StatusCode,
};
use mf2::types::{FindItemById, FindItemByProperty, FindItemByUrl, PropertyValue};
use serde::{Deserialize, Serialize};
use std::cmp::PartialEq;
use url::Url;

/// Resolves the Webmention endpoints for the provided URL.
///
/// # Examples
///
/// ```no_run
/// use indieweb::standards::webmention::endpoint_for;
/// use indieweb::http::reqwest::Client as HttpClient;
/// use url::Url;
///
/// # async fn example() -> Result<(), indieweb::Error> {
/// let the_url: Url = "https://news.indieweb.org".parse().unwrap();
/// let client = HttpClient::default();
/// let endpoint = endpoint_for(&client, &the_url).await?;
/// println!("Found endpoint: {}", endpoint);
/// # Ok(())
/// # }
/// ```
#[tracing::instrument(skip(client))]
pub async fn endpoint_for(
    client: &impl crate::http::Client,
    url: &Url,
) -> Result<url::Url, crate::Error> {
    let rels = crate::algorithms::link_rel::for_url(client, url, &["webmention"], "GET")
        .await?
        .get("webmention")
        .cloned()
        .unwrap_or_default();

    if let Some(endpoint_url) = rels.first().cloned() {
        tracing::debug!(
            rels = format!("{:?}", rels),
            url = format!("{endpoint_url}"),
            "Found the relations for Webmention; selecting the first URL"
        );

        Ok(endpoint_url)
    } else {
        tracing::trace!(
            url = format!("{}", url),
            "No Webmention endpoints were found."
        );

        Err(crate::Error::NoEndpointsFound {
            url: url.to_string(),
            rel: "webmention".to_owned(),
        })
    }
}

/// Handles the work of sending a Webmention from a [request][Request].
///
/// This uses the [request][Request] to send a Webmention.
///
/// # Examples
///
/// ```no_run
/// use indieweb::standards::webmention::{Request, send};
/// use indieweb::http::reqwest::Client as HttpClient;
/// use url::Url;
///
/// # async fn example() -> Result<(), indieweb::Error> {
/// let client = HttpClient::default();
/// let source: Url = "https://example.com/source".parse().unwrap();
/// let target: Url = "https://example.com/target".parse().unwrap();
/// let endpoint: Url = "https://example.com/webmention".parse().unwrap();
///
/// let request = Request {
///     source: source.clone(),
///     target: target.clone(),
///     private: None,
///     vouch: vec![],
///     token: None,
/// };
///
/// let result = send(&client, &endpoint, &request).await?;
/// println!("Sent webmention: {:?}", result);
/// # Ok(())
/// # }
/// ```
#[tracing::instrument(skip(client))]
pub async fn send(
    client: &impl crate::http::Client,
    endpoint: &Url,
    request: &Request,
) -> Result<Response, crate::Error> {
    use std::str::FromStr;

    let local_request = request.clone();
    let mut req: http::Request<crate::http::Body> = local_request.try_into()?;
    *req.uri_mut() =
        http::Uri::from_str(endpoint.as_str()).map_err(|e| crate::Error::Http(e.into()))?;
    client.send_request(req).await.and_then(|r| r.try_into())
}

/// Represents the state of a Webmention for a client or a server.
#[derive(Clone, Debug, Serialize, Deserialize, Eq)]
#[serde(untagged, rename_all = "lowercase")]
pub enum Status {
    /// Used to highlight that a stored Webmention or a response about a Webmention has
    /// been rejected by a remote endpoint or the implementation of a server is informing a client
    /// that it has been rejected.
    Rejected,

    /// Used to highlight that an asynchronous Webmention has been accepted for processing
    /// by a remote endpoint.
    Accepted,

    /// Represents a successful synchronous Webmention.
    Sent,

    /// Reports that the sender failed to satisfy constraints for this Webmention.
    ///
    /// This value is usually in the `4xx` range.
    SenderError(u16),

    /// Reports that the receiver failed to satisfy constraints for this Webmention.
    ///
    /// This value is usually in the `5xx` range.
    ReceiverError(u16),
}

impl PartialEq for Status {
    fn eq(&self, other: &Self) -> bool {
        std::mem::discriminant(self) == std::mem::discriminant(other)
    }
}

impl Status {
    /// Determines if this status is not of an error status.
    ///
    /// # Examples
    ///
    /// ```
    /// use indieweb::standards::webmention::Status;
    ///
    /// assert!(Status::Accepted.is_ok());
    /// assert!(Status::Sent.is_ok());
    /// assert!(!Status::Rejected.is_ok());
    /// assert!(!Status::SenderError(409).is_ok());
    /// assert!(!Status::ReceiverError(503).is_ok());
    /// ```
    pub fn is_ok(&self) -> bool {
        match self {
            Status::Accepted | Status::Sent => true,
            Status::SenderError(_) | Status::ReceiverError(_) | Status::Rejected => false,
        }
    }
}

impl From<u16> for Status {
    fn from(code: u16) -> Self {
        match code {
            200 => Self::Sent,
            201 | 202 => Self::Accepted,
            400..=499 => Self::SenderError(code),
            _ => Self::ReceiverError(code),
        }
    }
}

/// Represents a incoming request to an endpoint for a Webmention.
///
/// The foundation of this request is defined at
/// <https://www.w3.org/TR/webmention/#sender-notifies-receiver-p-1> but
/// additional fields can be used to extend the functionality of this Webmention.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Request {
    /// The URL of the resource that's sending this Webmention.
    ///
    /// See <https://www.w3.org/TR/webmention/#sender-notifies-receiver-p-1> for more information.
    pub source: Url,

    /// The URL of the resource that's receiving this Webmention.
    ///
    /// See <https://www.w3.org/TR/webmention/#sender-notifies-receiver-p-1> for more information.
    pub target: Url,

    /// Parameters for private Webmention support.
    ///
    /// See <https://indieweb.org/Private-Webmention> for more information.
    #[serde(skip_serializing_if = "Option::is_none", flatten)]
    pub private: Option<PrivateRequest>,

    /// A list of URLs that can be used for vouching this URL.
    ///
    /// This is part of the sending flow described at <https://indieweb.org/Vouch#Sending>.
    ///
    /// See <https://indieweb.org/Vouch> for more information.
    #[serde(
        default,
        skip_serializing_if = "Vec::is_empty",
        with = "as_string_or_list"
    )]
    pub vouch: Vec<String>,

    /// A token to use to authorize this request when sending a Webmention.
    #[serde(skip)]
    pub token: Option<String>,
}

impl TryInto<http::Request<crate::http::Body>> for Request {
    type Error = crate::Error;

    fn try_into(mut self) -> Result<http::Request<crate::http::Body>, Self::Error> {
        let mut request_builder = http::Request::builder()
            .method("POST")
            .header(
                ::http::header::ACCEPT,
                "text/plain; q=0.8, text/html, application/json; q=0.8, application/mf2+json; q=0.9, *.*; q=0.7",
            )
            .header(
                ::http::header::CONTENT_TYPE,
                crate::http::CONTENT_TYPE_FORM_URLENCODED,
            );

        if let Some(token) = self.token.take() {
            request_builder =
                request_builder.header(::http::header::AUTHORIZATION, format!("Bearer {}", token));
        }

        let req_qs =
            serde_qs::to_string(&self).map(|s| crate::http::Body::Bytes(s.into_bytes()))?;
        request_builder.body(req_qs).map_err(crate::Error::Http)
    }
}

impl Default for Request {
    fn default() -> Self {
        Self {
            source: "urn:indieweb:invalid-source".parse().unwrap(),
            target: "urn:indieweb:invalid-target".parse().unwrap(),
            private: None,
            vouch: Default::default(),
            token: None,
        }
    }
}

impl Request {
    pub fn validate(&self) -> Result<(), crate::Error> {
        // https://webmention.net/draft/#request-verification-p-2
        if self.source == self.target {
            return Err(crate::Error::WebmentionSourceAndTargetAreSame {
                url: self.source.clone(),
            });
        }
        Ok(())
    }
}

/// Represents parameters to use when sending a private Webmention.
/// See <https://indieweb.org/Private-Webmention> for more information.
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct PrivateRequest {
    /// The code to exchange for a private Webmention.
    ///
    /// See <https://indieweb.org/Private-Webmention#Sending_the_Webmention> for more information.
    #[serde(skip_serializing_if = "String::is_empty")]
    pub code: String,

    /// The realm to exchange for a private Webmention.
    ///
    /// See <https://indieweb.org/Private-Webmention#Sending_the_Webmention>
    #[serde(skip_serializing_if = "Option::is_none")]
    pub realm: Option<String>,
}

/// Represents a incoming response from an endpoint for a Webmention.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Response {
    /// The resulting status of a Webmention.
    pub status: Status,

    /// A optional URL presenting the result of this URL. It can also be used
    /// at time to show status information or acceptance information.
    pub location: Option<Url>,

    /// A optional body representing the immediate body of the response.
    pub body: Option<String>,
}

impl PartialEq for Response {
    fn eq(&self, other: &Self) -> bool {
        self.status == other.status && self.location == other.location && self.body == other.body
    }
}

impl TryFrom<http::Response<crate::http::Body>> for Response {
    type Error = crate::Error;

    fn try_from(resp: http::Response<crate::http::Body>) -> Result<Self, Self::Error> {
        let locations = resp.headers().get_all("location");
        let status = resp.status();

        let location = locations
            .into_iter()
            .filter(|&_| Status::from(status.as_u16()) == Status::Accepted)
            .cloned()
            .filter_map(|v| v.to_str().ok().map(ToString::to_string))
            .collect::<Vec<_>>()
            .first()
            .filter(|l| !l.is_empty())
            .and_then(|v| v.as_str().parse().ok());
        let body = Some(String::from_utf8(resp.into_body().as_bytes().to_vec())?)
            .filter(|b| !b.is_empty());

        match status.as_u16() {
            200..=299 | 400..=499 | 500..=599 => {
                let status = match status.as_u16() {
                    201 | 202 => Status::Accepted,
                    200 | 203..=299 => Status::Sent,
                    400..=499 => Status::SenderError(status.as_u16()),
                    _ => Status::ReceiverError(status.as_u16()),
                };

                Ok(Self {
                    body,
                    location,
                    status,
                })
            }
            _ => Err(crate::Error::WebmentionUnsupportedStatusCode(
                status.as_u16(),
            )),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relationship {
    pub r#type: ptd::Type,
    pub document: mf2::types::Document,
    pub source: Option<microformats::types::Item>,
}

const ACCEPT_HEADER_VALUE: &str = "text/html,text/mf2+html";

/// Determines the Webmention relationship between the source and target URL by processing it.
#[tracing::instrument(skip(client), ret, err)]
pub async fn process_incoming_webmention(
    client: &impl crate::http::Client,
    Request {
        source,
        target,
        token,
        ..
    }: &Request,
) -> Result<Relationship, crate::Error> {
    let mut req_builder = http::Request::builder().method("GET").uri(source.as_str());

    // FIXME: Extract the headers of content to expect.
    req_builder = req_builder.header(ACCEPT, ACCEPT_HEADER_VALUE);

    if let Some(token) = token {
        req_builder = req_builder.header(::http::header::AUTHORIZATION, format!("Bearer {}", token));
    }

    let req = req_builder
        .body(crate::http::Body::default())
        .map_err(crate::Error::Http)?;

    tracing::trace!("Executing Webmention request");

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

    if resp.status() == StatusCode::UNAUTHORIZED {
        // FIXME: Add logic to load remote page with authorization.
        return Err(crate::Error::WebmentionUnauthorized {
            url: source.to_owned(),
        });
    } else if resp.status() == StatusCode::NOT_FOUND {
        return Err(crate::Error::WebmentionNotFound {
            url: source.to_owned(),
        });
    } else if resp.status() == StatusCode::GONE {
        return Err(crate::Error::WebmentionDeleted {
            url: source.to_owned(),
        });
    }

    let incoming_content_type_header_value = resp
        .headers()
        .get(CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .map(ToString::to_string)
        .unwrap_or_default();

    let matches_provided_content_type = !incoming_content_type_header_value.is_empty()
        && ACCEPT_HEADER_VALUE.contains(incoming_content_type_header_value.as_str());

    if !matches_provided_content_type {
        return Err(crate::Error::WebmentionUnsupportedContentType {
            url: source.to_owned(),
            content_type: incoming_content_type_header_value,
        });
    }

    let document = crate::mf2::http::to_mf2_document(
        resp.map(|b| b.as_bytes().to_vec()),
        source.as_str(),
    )
    .map_err(crate::Error::from)?;

    let property_names = document
        .find_items_with_matching_property_value(PropertyValue::Url(target.clone().into()))
        .into_iter()
        .map(|(property_name, _)| property_name)
        .collect::<Vec<_>>();

    tracing::trace!(
        property_names = format!("{property_names:?}"),
        "Mapped matching items by their property name"
    );

    let r#type = if property_names.is_empty() {
        let item = document.find_item_by_url(source).or_else(|| {
            source
                .fragment()
                .and_then(|fragment| document.find_item_by_id(fragment))
        });

        tracing::trace!(
            source_has_url = item.is_some(),
            url = source.to_string(),
            "Did the source expose itself in its MF2?"
        );

        item.and_then(ptd::resolve_from_object)
            .unwrap_or(ptd::Type::Mention)
    } else {
        ptd::resolve_reaction_property_name(
            &property_names
                .iter()
                .map(|property_name| property_name.as_str())
                .collect::<Vec<_>>(),
        )
        .unwrap_or(ptd::Type::Mention)
    };

    let source_item = document.find_item_by_url(source);

    let source_item = if source_item.is_none() && document.items.len() == 1 {
        tracing::warn!("The source item is not in the MF2 by its URL; used the only root item");
        Some(document.items[0].clone())
    } else {
        source_item
    };

    Ok(Relationship {
        document,
        r#type,
        source: source_item,
    })
}

mod test;
