use std::cmp::Ordering;

use hmac::digest::InvalidLength;
use hmac::{Hmac, KeyInit, Mac};
use reqwest::Url;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ToStrError};
use sha2::{Digest, Sha256};

use crate::Ezs3Err;
use crate::ext::Extendable;

type Hmac256 = Hmac<Sha256>;

impl Extendable for ToStrError {
    type Output = Ezs3Err;

    fn extend<S: AsRef<str>>(self, s: S) -> Self::Output {
        let msg = format!("{}: header value unrepresentable: {}", s.as_ref(), &self);
        Ezs3Err::InvalidHeader(msg)
    }
}

pub(crate) struct EncodeBuffer(Vec<u8>);

impl EncodeBuffer {
    pub fn new() -> EncodeBuffer {
        EncodeBuffer(Vec::new())
    }

    pub fn with_capacity(capacity: usize) -> EncodeBuffer {
        EncodeBuffer(Vec::with_capacity(capacity))
    }

    fn hexcaped(&mut self, b: u8) {
        use std::io::Write;
        write!(&mut self.0, "%{:02X}", b).unwrap();
    }

    pub fn push_path(&mut self, b: u8) {
        match b {
            c if (c <= b'Z' && c >= b'A') => self.0.push(b),
            c if (c <= b'z' && c >= b'a') => self.0.push(b),
            c if (c <= b'9' && c >= b'0') => self.0.push(b),
            b'-' | b'.' | b'_' | b'~' | b'/' => self.0.push(b),
            _ => self.hexcaped(b),
        }
    }

    pub fn push(&mut self, b: u8) {
        if b == b'/' {
            self.hexcaped(b)
        } else {
            self.push_path(b)
        }
    }

    pub fn push_raw(&mut self, b: u8) {
        self.0.push(b)
    }

    pub fn extend_raw<T: AsRef<[u8]>>(&mut self, t: T) {
        self.0.extend(t.as_ref())
    }
}

impl<T> From<T> for EncodeBuffer
where
    T: AsRef<[u8]>,
{
    fn from(bytes: T) -> Self {
        let bytes = bytes.as_ref();
        let mut buff = EncodeBuffer::with_capacity(bytes.len());
        for &b in bytes.iter() {
            buff.push(b);
        }
        buff
    }
}

impl TryFrom<EncodeBuffer> for String {
    type Error = Ezs3Err;

    fn try_from(buff: EncodeBuffer) -> Result<String, Self::Error> {
        String::from_utf8(buff.0).map_err(|e| Ezs3Err::Utf8Err(format!("{}", &e)))
    }
}

#[derive(PartialEq, Eq, Debug)]
pub(crate) struct QParam {
    k: String,
    v: String,
}

impl<T> TryFrom<(T, T)> for QParam
where
    T: AsRef<str>,
{
    type Error = Ezs3Err;

    fn try_from(pair: (T, T)) -> Result<QParam, Self::Error> {
        let kbuff = EncodeBuffer::from(pair.0.as_ref().as_bytes());
        let vbuff = EncodeBuffer::from(pair.1.as_ref().as_bytes());
        let k = String::try_from(kbuff).extend("query key")?;
        let v = String::try_from(vbuff).extend("query value")?;
        Ok(QParam { k, v })
    }
}

impl Ord for QParam {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.k.cmp(&other.k) {
            Ordering::Equal => self.v.cmp(&other.v),
            x => x,
        }
    }
}
impl PartialOrd for QParam {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

#[derive(PartialEq, Eq, Debug)]
pub(crate) struct HeaderPair {
    name: String,
    value: String,
}

impl TryFrom<(&HeaderName, &HeaderValue)> for HeaderPair {
    type Error = Ezs3Err;

    fn try_from(pair: (&HeaderName, &HeaderValue)) -> Result<Self, Self::Error> {
        let value = pair
            .1
            .to_str()
            .extend_from(|| format!("header {}", &pair.0))?
            .trim()
            .to_string();
        let name = pair.0.as_str().to_string();
        Ok(HeaderPair { name, value })
    }
}

impl Ord for HeaderPair {
    fn cmp(&self, other: &HeaderPair) -> Ordering {
        match self.name.cmp(&other.name) {
            Ordering::Equal => self.value.cmp(&other.value),
            x => x,
        }
    }
}
impl PartialOrd for HeaderPair {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl HeaderPair {
    fn list(map: &HeaderMap) -> Result<Vec<HeaderPair>, Ezs3Err> {
        let mut pairs: Vec<HeaderPair> = map
            .iter()
            .map(HeaderPair::try_from)
            .collect::<Result<Vec<_>, _>>()?;
        pairs.sort();
        Ok(pairs)
    }
}

pub(crate) fn to_canonical_url(url: &Url) -> Result<String, Ezs3Err> {
    let mut buff = EncodeBuffer::new();
    for &c in url.path().as_bytes().iter() {
        buff.push_path(c);
    }
    buff.push_raw(b'\n');

    let mut qpairs: Vec<QParam> = url
        .query_pairs()
        .map(QParam::try_from)
        .collect::<Result<Vec<_>, _>>()?;
    qpairs.sort();

    let mut qpiter = qpairs.into_iter();
    if let Some(pair) = qpiter.next() {
        buff.extend_raw(pair.k.as_bytes());
        buff.push_raw(b'=');
        buff.extend_raw(pair.v.as_bytes());
    }
    for pair in qpiter {
        buff.push_raw(b'&');
        buff.extend_raw(pair.k.as_bytes());
        buff.push_raw(b'=');
        buff.extend_raw(pair.v.as_bytes());
    }
    buff.push_raw(b'\n');

    String::try_from(buff).extend("canonical URL string not UTF-8")
}

pub(crate) fn signing_header_list(headers: &HeaderMap) -> Result<String, Ezs3Err> {
    let pairs = HeaderPair::list(headers)?;
    let bufflen = pairs.iter().map(|p| p.name.len() + p.value.len() + 1).sum();

    let mut buff = String::with_capacity(bufflen);
    let mut piter = pairs.iter();
    if let Some(pair) = piter.next() {
        buff.push_str(&pair.name);
    }
    for pair in piter {
        buff.push(';');
        buff.push_str(&pair.name);
    }

    Ok(buff)
}

pub(crate) fn to_canonical_headers(headers: &HeaderMap) -> Result<String, Ezs3Err> {
    let pairs = HeaderPair::list(headers)?;

    let bufflen = pairs.iter().map(|p| p.name.len() + p.value.len() + 2).sum();
    let mut buff = String::with_capacity(bufflen);

    for pair in pairs.iter() {
        buff.push_str(&pair.name);
        buff.push(':');
        buff.push_str(&pair.value);
        buff.push('\n');
    }

    Ok(buff)
}

impl Extendable for InvalidLength {
    type Output = Ezs3Err;

    fn extend<S: AsRef<str>>(self, s: S) -> Self::Output {
        let msg = format!("{}: {}", s.as_ref(), &self);
        Ezs3Err::HashError(msg)
    }
}

pub(crate) fn hex_digest<B: AsRef<[u8]>>(bytes: B) -> String {
    let digest = Sha256::digest(bytes.as_ref());
    hex::encode(digest)
}

pub(crate) fn hmac_to_bytes<K, D>(key: K, data: D) -> Result<Vec<u8>, Ezs3Err>
where
    K: AsRef<[u8]>,
    D: AsRef<[u8]>,
{
    let mut mac = Hmac256::new_from_slice(key.as_ref()).extend("HMAC key has invalid length")?;
    mac.update(data.as_ref());
    let bytes = mac.finalize().into_bytes().as_slice().to_vec();
    Ok(bytes)
}

pub(crate) fn hmac_to_hex<K, D>(key: K, data: D) -> Result<String, Ezs3Err>
where
    K: AsRef<[u8]>,
    D: AsRef<[u8]>,
{
    let mut mac = Hmac256::new_from_slice(key.as_ref()).extend("HMAC key has invalid length")?;
    mac.update(data.as_ref());
    Ok(hex::encode(mac.finalize().into_bytes().as_slice()))
}
