use chrono::{DateTime, Utc};
use reqwest::header::{self, HeaderValue};
use reqwest::{Method, Request, Url};

use crate::encoders;
use crate::ext::Extendable;
use crate::{Body, Ezs3Err};

pub(crate) static AMZ_DATE_FMT: &str = "%Y%m%dT%H%M%SZ";
static SHORT_DATE_FMT: &str = "%Y%m%d";
static PROTOCOL: &str = "AWS4";
static ALGO: &str = "AWS4-HMAC-SHA256";
static SERVICE: &str = "s3";
static VERSION: &str = "aws4_request";

impl Extendable for header::InvalidHeaderValue {
    type Output = Ezs3Err;

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

fn current_datetime() -> DateTime<Utc> {
    DateTime::from(std::time::SystemTime::now())
}

fn date_header(date: DateTime<Utc>) -> Result<HeaderValue, Ezs3Err> {
    let header_value = date.to_rfc2822();
    HeaderValue::from_str(&header_value).extend_from(|| format!("Date: {} invalid", &header_value))
}

fn to_x_amz_date(date: DateTime<Utc>) -> String {
    date.format(AMZ_DATE_FMT).to_string()
}

fn x_amz_date_header(date: DateTime<Utc>) -> Result<HeaderValue, Ezs3Err> {
    let header_value = to_x_amz_date(date);
    HeaderValue::from_str(&header_value)
        .extend_from(|| format!("x_amz_date: {} invalid", &header_value))
}
fn short_date(date: DateTime<Utc>) -> String {
    date.format(SHORT_DATE_FMT).to_string()
}

impl crate::Client {
    fn obj_url(&self, object: &str) -> Result<Url, Ezs3Err> {
        let mut url = self.endpoint.clone();
        {
            let mut handle = match url.path_segments_mut() {
                Ok(handle) => handle,
                Err(_) => {
                    let msg = format!("{} + {}", &self.endpoint, object);
                    return Err(Ezs3Err::InvalidUrl(msg));
                }
            };
            handle.push(object);
        }
        Ok(url)
    }

    fn canonical_request(&self, req: &Request, signing_headers: &str) -> Result<String, Ezs3Err> {
        tracing::trace!("Client::canonical_request([...])");

        let canon_uri = encoders::to_canonical_url(req.url())?;
        let canon_headers = encoders::to_canonical_headers(req.headers())?;
        let payload_hash = req
            .headers()
            .get("x-amz-content-sha256")
            .ok_or_else(|| Ezs3Err::InvalidHeader("missing body hash header".into()))?
            .to_str()
            .extend("body hash header not valid string")?
            .to_string();

        let buff = format!(
            "{}\n{}{}\n{}\n{}",
            &req.method(),
            &canon_uri,
            &canon_headers,
            &signing_headers,
            &payload_hash
        );

        tracing::debug!("canonical request:\n->|{}|<-", &buff);
        Ok(buff)
    }

    fn signing_string(
        &self,
        req: &Request,
        signing_headers: &str,
        date: DateTime<Utc>,
        short_date: &str,
    ) -> Result<String, Ezs3Err> {
        tracing::trace!("Client::signing_string([...], {})", &date);

        let can_req_str = self
            .canonical_request(req, signing_headers)
            .extend("assembling canonical header form")?;
        let can_req_hash = encoders::hex_digest(&can_req_str);

        let signing_string = format!(
            "{}\n{}\n{}/{}/{}/{}\n{}",
            ALGO,
            &to_x_amz_date(date),
            short_date,
            &self.region,
            SERVICE,
            VERSION,
            &can_req_hash
        );

        tracing::debug!("signing string:\n\n->|{}|<-", &signing_string);
        Ok(signing_string)
    }

    fn signature(&self, req: &Request, date: DateTime<Utc>) -> Result<String, Ezs3Err> {
        tracing::trace!("Client::signature([...], {})", &date);

        let short_date = short_date(date);
        let signing_headers = encoders::signing_header_list(req.headers())
            .extend("generating signing header list")?;
        let signing_string = self.signing_string(req, &signing_headers, date, &short_date)?;
        let proto_key = format!("{}{}", PROTOCOL, &self.key);
        let date_key = encoders::hmac_to_bytes(&proto_key, &short_date)?;
        let region_key = encoders::hmac_to_bytes(&date_key, &self.region)?;
        let service_key = encoders::hmac_to_bytes(&region_key, SERVICE)?;
        let ultimate_key = encoders::hmac_to_bytes(&service_key, VERSION)?;

        let signature = encoders::hmac_to_hex(&ultimate_key, &signing_string)?;

        let auth = format!(
            "{} Credential={}/{}/{}/{}/{},SignedHeaders={},Signature={}",
            ALGO,
            &self.key_id,
            &short_date,
            &self.region,
            SERVICE,
            VERSION,
            &signing_headers,
            &signature
        );

        tracing::debug!("signature:\n->|{}|<-", &auth);

        Ok(auth)
    }

    /// Prepare and sign a request.
    ///
    /// This method is used internally to prepare and sign requests
    /// generated by other methods. It is exposed in case you want do
    /// do something fancier than just put/get/list/delete; it will _sign_
    /// the request for you, which is the obnoxious part.
    pub fn prepare(
        &self,
        method: Method,
        object: Option<&str>,
        query: &[(&str, &str)],
        body: Option<Body>,
    ) -> Result<Request, Ezs3Err> {
        tracing::trace!("Client::prepare({}, ...)", &method);
        if let Some(object) = object {
            tracing::trace!("    with object: {:?}", object);
        }
        if !query.is_empty() {
            tracing::trace!("    with query: {:?}", query);
        }
        if let Some(ref bod) = body {
            tracing::trace!(
                "    with {} bytes of {:?} body",
                bod.bytes.len(),
                &bod.content_type
            );
        }

        match body.as_ref() {
            None => tracing::trace!("Client::prepare({}, {:?}, [no body])", &method, &object),
            Some(body) => tracing::trace!(
                "Client::prepare({}, {:?}, [{} bytes of {:?} body])",
                &method,
                &object,
                &body.bytes.len(),
                body.content_type
            ),
        }

        let date = current_datetime();
        let datehead = date_header(date)?;
        let amz_datehead = x_amz_date_header(date)?;
        let url = {
            let mut url = match object {
                Some(object) => self.obj_url(object)?,
                None => self.endpoint.clone(),
            };
            {
                let mut qhunk = url.query_pairs_mut();
                for qpair in query.into_iter() {
                    qhunk.append_pair(qpair.0, qpair.1);
                }
            }
            url
        };

        let req = self
            .reqwester
            .request(method, url)
            .header(header::HOST, self.host.clone())
            .header(header::DATE, datehead)
            .header("x-amz-date", amz_datehead);
        let req = match body {
            None => req.header("x-amz-content-sha256", &encoders::hex_digest("")),
            Some(Body {
                content_type,
                bytes,
            }) => {
                let length = bytes.len();
                let digest = encoders::hex_digest(&bytes);
                let digest_header = header::HeaderValue::from_str(&digest)
                    .extend("encoding body digest header value")?;
                req.body(bytes)
                    .header(header::CONTENT_TYPE, content_type)
                    .header(header::CONTENT_LENGTH, length)
                    .header("x-amz-content-sha256", digest_header)
            }
        };
        let mut req = req
            .build()
            .map_err(|e| Ezs3Err::RequestBuildErr(e.to_string()))?;

        let auth_header = self.signature(&req, date)?;
        let auth_header =
            HeaderValue::from_str(&auth_header).extend("signature invalid header value")?;
        req.headers_mut().insert(header::AUTHORIZATION, auth_header);

        Ok(req)
    }
}
