use std::collections::HashMap;

use url::Url;

use crate::mf2;

// NOTE: Expand to allow direct deserialization of this from a response.
// NOTE: Add support for parsing from a string into this type.
/// Provides a wrapper of what relations look like.
#[derive(Debug, Clone)]
pub struct RelMap(HashMap<String, Vec<url::Url>>);

impl From<HashMap<String, Vec<url::Url>>> for RelMap {
    fn from(v: HashMap<String, Vec<url::Url>>) -> Self {
        Self(v)
    }
}

impl std::ops::Deref for RelMap {
    type Target = HashMap<String, Vec<url::Url>>;

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

fn get_rels_from_header(headers: &http::HeaderMap, rel: &str, base_url: &Url) -> Vec<url::Url> {
    headers
        .get_all("link")
        .into_iter()
        .flat_map(|v| String::from_utf8(v.as_bytes().to_vec()))
        .flat_map(|redirect_uri_header_value| {
            redirect_uri_header_value
                .split(',')
                .collect::<Vec<&str>>()
                .into_iter()
                .map(|link_rel: &str| {
                    let mut parameters = link_rel.split(';').collect::<Vec<&str>>();
                    let uri_value = parameters.remove(0).to_owned().trim().to_string();
                    (uri_value, parameters)
                })
                .filter(|(_uri, parameters)| {
                    parameters
                        .iter()
                        .map(|s| s.to_lowercase())
                        .map(|s| s.trim().to_string())
                        .filter(|r| r.starts_with("rel="))
                        .flat_map(|rv| {
                            rv.replace("rel=", "")
                                .trim_matches('"')
                                .split(' ')
                                .map(|s| s.to_string())
                                .collect::<Vec<_>>()
                        })
                        .any(|s| s == rel)
                })
                .filter_map(|(uri, _parameters)| {
                    uri.strip_prefix('<')
                        .map(|u| u.to_string())
                        .unwrap_or_default()
                        .strip_suffix('>')
                        .and_then(|u| u.parse().or_else(|_| base_url.join(u)).ok())
                })
                .collect::<Vec<Url>>()
        })
        .collect::<Vec<_>>()
}

/// Resolves all of the relating links for a particular URL.
// TODO : Send a HEAD request to get headers instead
// TODO: Return the response's status code.
// FIXME: Refactor this to use the mf2 library.
#[tracing::instrument(skip(client))]
pub async fn for_url(
    client: &impl crate::http::Client,
    url: &Url,
    rels: &[&str],
) -> Result<RelMap, crate::Error> {
    let req = http::Request::get(url.as_str())
        .body(Default::default())
        .map_err(crate::Error::Http)?;

    let resp = client.send_request(req).await?;
    let headers = resp.headers().clone();
    let header_rels: HashMap<String, Vec<_>> = HashMap::from_iter(
        rels.iter()
            .map(|rel| (rel.to_string(), get_rels_from_header(&headers, rel, url))),
    );

    let document =
        mf2::parser::http::to_mf2_document(resp.map(|body| body.as_bytes().to_vec()), url.as_str())
            .map_err(mf2::Error::Parser)?;
    let body_rels: HashMap<String, Vec<_>> =
        HashMap::from_iter(document.rels.by_rels().into_iter());

    let mut rels_map = HashMap::from_iter(header_rels);
    for (header, rels) in body_rels {
        if !rels_map.contains_key(&header) {
            rels_map.insert(header.clone(), Vec::default());
        }

        if let Some(r) = rels_map.get_mut(&header) {
            r.extend(rels)
        }
    }

    rels_map = HashMap::from_iter(rels_map.into_iter().filter(|(_, v)| !v.is_empty()));

    if !rels_map.is_empty() {
        tracing::trace!(rels = format!("{:?}", rels_map), "Found relations.");
    } else {
        tracing::trace!(rels = format!("{:?}", rels), "No relations found.");
    }

    Ok(rels_map.into())
}

#[cfg(test)]
mod test;
