use super::Body;
use super::Response;

use crate::dto::SelectObjectContentEventStream;
use crate::dto::{Metadata, StreamingBlob, Timestamp, TimestampFormat};
use crate::error::{S3Error, S3Result};
use crate::http::{HeaderName, HeaderValue};
use crate::{utils, xml};

use std::convert::Infallible;
use std::fmt::Write as _;

use hyper::header::{IntoHeaderName, InvalidHeaderValue};

pub fn add_header<N, V>(res: &mut Response, name: N, value: V) -> S3Result
where
    N: IntoHeaderName,
    V: TryIntoHeaderValue,
    V::Error: std::error::Error + Send + Sync + 'static,
{
    let val = value.try_into_header_value().map_err(S3Error::internal_error)?;
    res.headers_mut().insert(name, val);
    Ok(())
}

pub fn add_opt_header<N, V>(res: &mut Response, name: N, value: Option<V>) -> S3Result
where
    N: IntoHeaderName,
    V: TryIntoHeaderValue,
    V::Error: std::error::Error + Send + Sync + 'static,
{
    if let Some(value) = value {
        let val = value.try_into_header_value().map_err(S3Error::internal_error)?;
        res.headers_mut().insert(name, val);
    }
    Ok(())
}

pub fn add_opt_header_timestamp<N>(res: &mut Response, name: N, value: Option<Timestamp>, fmt: TimestampFormat) -> S3Result
where
    N: IntoHeaderName,
{
    if let Some(ref value) = value {
        let val = utils::fmt_timestamp(value, fmt, HeaderValue::from_bytes).map_err(S3Error::internal_error)?;
        res.headers_mut().insert(name, val);
    }
    Ok(())
}

pub trait TryIntoHeaderValue {
    type Error;
    fn try_into_header_value(self) -> Result<HeaderValue, Self::Error>;
}

impl TryIntoHeaderValue for bool {
    type Error = Infallible;

    #[allow(clippy::declare_interior_mutable_const)]
    fn try_into_header_value(self) -> Result<HeaderValue, Self::Error> {
        const TRUE: HeaderValue = HeaderValue::from_static("true");
        const FALSE: HeaderValue = HeaderValue::from_static("false");
        Ok(if self { TRUE } else { FALSE })
    }
}

impl TryIntoHeaderValue for i32 {
    type Error = Infallible;

    fn try_into_header_value(self) -> Result<HeaderValue, Self::Error> {
        Ok(HeaderValue::from(self))
    }
}

impl TryIntoHeaderValue for i64 {
    type Error = Infallible;

    fn try_into_header_value(self) -> Result<HeaderValue, Self::Error> {
        Ok(HeaderValue::from(self))
    }
}

impl TryIntoHeaderValue for String {
    type Error = InvalidHeaderValue;

    fn try_into_header_value(self) -> Result<HeaderValue, Self::Error> {
        HeaderValue::try_from(self)
    }
}

/// See <https://github.com/hyperium/mime/issues/144>
#[allow(clippy::declare_interior_mutable_const)]
const APPLICATION_XML: HeaderValue = HeaderValue::from_static("application/xml");

pub fn set_xml_body<T: xml::Serialize>(res: &mut Response, val: &T) -> S3Result {
    let mut buf = Vec::with_capacity(256);
    {
        let mut ser = xml::Serializer::new(&mut buf);
        ser.decl()
            .and_then(|_| val.serialize(&mut ser))
            .map_err(S3Error::internal_error)?;
    }
    *res.body_mut() = Body::from(buf);
    res.headers_mut().insert(hyper::header::CONTENT_TYPE, APPLICATION_XML);
    Ok(())
}

pub fn set_stream_body(res: &mut Response, stream: StreamingBlob) {
    *res.body_mut() = Body::from(stream);
}

pub fn set_event_stream_body(res: &mut Response, stream: SelectObjectContentEventStream) {
    *res.body_mut() = Body::from(stream.into_byte_stream());
    res.headers_mut()
        .insert(hyper::header::TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
}

pub fn add_opt_metadata(res: &mut Response, metadata: Option<Metadata>) -> S3Result {
    if let Some(map) = metadata {
        let mut buf = String::new();
        for (key, val) in map {
            write!(&mut buf, "x-amz-meta-{key}").unwrap();
            let name = HeaderName::from_bytes(buf.as_bytes()).map_err(S3Error::internal_error)?;
            let value = HeaderValue::try_from(val).map_err(S3Error::internal_error)?;
            res.headers_mut().insert(name, value);
            buf.clear();
        }
    }
    Ok(())
}
