//! Extension traits for foreign types.

use reqwest::header::{AsHeaderName, HeaderMap};

use crate::Ezs3Err;

/// This is an eyre-like trait for error-handling and message-layering.
///
/// Implementing it for a type (in particular an error type) allows
/// use like this:
///
/// ```ignore
/// // Imagine `Extendable` is implemented for the error type returned
/// // by `returns_other_error().`
///
/// fn returns_my_error(arg) -> Result<Type, MyError> {
///     returns_other_error(arg)
///         .extend("attempting to call fallable function")?;
/// }
/// ````
pub(crate) trait Extendable
where
    Self: Sized,
{
    type Output;

    fn extend<S: AsRef<str>>(self, s: S) -> Self::Output;

    fn extend_from<F: FnOnce() -> String>(self, f: F) -> Self::Output {
        let extent = f();
        self.extend(extent)
    }
}

/// You now only need to implement `Extendable` for your error type,
/// and you get all `Result`s which return it.
impl<T, E> Extendable for Result<T, E>
where
    E: Extendable,
{
    type Output = Result<T, E::Output>;

    fn extend<S: AsRef<str>>(self, s: S) -> Self::Output {
        match self {
            Ok(x) => Ok(x),
            Err(e) => Err(e.extend(s)),
        }
    }
}

fn modify<F>(err: Ezs3Err, mut f: F) -> Ezs3Err
where
    F: FnMut(String) -> String,
{
    use Ezs3Err::*;
    match err {
        HashError(s) => HashError(f(s)),
        InvalidHeader(s) => InvalidHeader(f(s)),
        InvalidUrl(s) => InvalidUrl(f(s)),
        ConfigErr(s) => ConfigErr(f(s)),
        Utf8Err(s) => Utf8Err(f(s)),
        RequestBuildErr(s) => RequestBuildErr(f(s)),
        RequestErr(s) => RequestErr(f(s)),
        HttpErr(s) => HttpErr(f(s)),
        XmlErr(s) => XmlErr(f(s)),
        Other(s) => Other(f(s)),
    }
}

/// This permits annotating errors at different levels of the call stack.
impl Extendable for Ezs3Err {
    type Output = Ezs3Err;

    fn extend<S: AsRef<str>>(self, s: S) -> Self::Output {
        modify(self, |msg| format!("{}: {}", s.as_ref(), &msg))
    }
}

#[cfg(test)]
pub async fn debug_dump_response(resp: reqwest::Response) -> Result<(), String> {
    const BODY_SIZE_LIMIT: usize = 1024;

    let status = resp.status();
    println!(
        "{:?} {} (({})",
        resp.version(),
        status.as_u16(),
        status.canonical_reason().unwrap_or("?")
    );
    for (name, value) in resp.headers().iter() {
        println!("    {:?}: {:?}", &name, &value);
    }
    println!();
    let body_bytes = resp.bytes().await.map_err(|e| e.to_string())?;
    if !body_bytes.is_empty() {
        println!("{} bytes of body:\n", body_bytes.len());
        let body_text = if body_bytes.len() < BODY_SIZE_LIMIT {
            String::from_utf8_lossy(&body_bytes).to_string()
        } else {
            let mut body_text = String::from_utf8_lossy(&body_bytes[..BODY_SIZE_LIMIT]).to_string();
            if body_bytes.len() > BODY_SIZE_LIMIT {
                body_text.push_str("...");
            }
            body_text
        };
        println!("{}", &body_text);
    }

    Ok(())
}

/// Getting a header value out of a `HeaderMap` is almost always an annoying
/// two-step process:
///
///   1. check to see if the header exists in the map
///
///   2. check to see if you can turn that header into a string
///
/// This trait just allows us to do both at once.
#[allow(dead_code)]
pub(crate) trait GetStr {
    /// Return the given header as an `$str` if it exists and the value is
    /// successfully stringable.
    fn get_str<K: AsHeaderName>(&self, name: K) -> Option<&str>;

    /// Return the given header as an `&str` if it exists and the value is
    /// successfully stringable, and return an error otherwise.
    fn require_str<'me, K>(&'me self, name: &'me K) -> Result<&'me str, Ezs3Err>
    where
        &'me K: AsHeaderName + std::fmt::Display + 'me;
}

impl GetStr for HeaderMap {
    fn get_str<K>(&self, name: K) -> Option<&str>
    where
        K: AsHeaderName,
    {
        let val = self.get(name)?;
        val.to_str().ok()
    }

    fn require_str<'me, K>(&'me self, name: &'me K) -> Result<&'me str, Ezs3Err>
    where
        &'me K: AsHeaderName + std::fmt::Display + 'me,
    {
        let val = match self.get(name) {
            None => {
                let msg = format!("response has no {} header", name);
                return Err(Ezs3Err::InvalidHeader(msg));
            }
            Some(val) => val,
        };
        match val.to_str() {
            Ok(s) => Ok(s),
            Err(e) => {
                let msg = format!("{} header value contains invalid bytes: {}", name, &e);
                Err(Ezs3Err::InvalidHeader(msg))
            }
        }
    }
}
