#![doc = include_str!("../README.md")]

mod answers;
pub(crate) mod config;
pub(crate) mod encoders;
pub(crate) mod ext;
mod prepare;

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

use crate::ext::{Extendable, GetStr};

pub use answers::{ObjectData, StorageClass};

type Timestamp = DateTime<Utc>;

/// Errors returned by the functions in this crate; the variant names
/// should be self-explanatory.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Ezs3Err {
    HashError(String),
    InvalidHeader(String),
    InvalidUrl(String),
    ConfigErr(String),
    Utf8Err(String),
    RequestBuildErr(String),
    RequestErr(String),
    HttpErr(String),
    XmlErr(String),
    Other(String),
}

impl std::fmt::Display for Ezs3Err {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use Ezs3Err::*;

        match self {
            HashError(msg) => write!(f, "hash error: {}", &msg),
            InvalidHeader(msg) => write!(f, "invalid header: {}", &msg),
            InvalidUrl(msg) => write!(f, "invalid URL: {}", &msg),
            ConfigErr(msg) => write!(f, "configuration error: {}", &msg),
            Utf8Err(msg) => write!(f, "UTF-8 error: {}", &msg),
            RequestBuildErr(msg) => write!(f, "error building request: {}", &msg),
            RequestErr(msg) => write!(f, "error sending/receiving request: {}", &msg),
            HttpErr(msg) => write!(f, "HTTP error: {}", &msg),
            XmlErr(msg) => write!(f, "error parsing response body XML: {}", &msg),
            Other(msg) => write!(f, "error: {}", &msg),
        }
    }
}

impl std::error::Error for Ezs3Err {}

/// Body bytes, together with a content-type.
#[derive(Debug, Clone)]
pub struct Body {
    content_type: HeaderValue,
    bytes: Vec<u8>,
}

impl Body {
    /// Instantiate a new request body.
    pub fn new<V: Into<Vec<u8>>>(v: V, content_type: &str) -> Result<Body, Ezs3Err> {
        let content_type_header = HeaderValue::from_str(content_type)
            .extend_from(|| format!("Content-Type: {} invalid", content_type))?;

        let body = Body {
            content_type: content_type_header,
            bytes: v.into(),
        };
        Ok(body)
    }
}

/// The bytes of an object, together with a little metadata.
///
/// As returned by [`Client::download`].
#[derive(Debug, Clone)]
pub struct Download {
    pub data: Vec<u8>,
    pub last_modified: DateTime<Utc>,
    pub content_type: String,
    pub etag: String,
}

impl Download {
    async fn from_response(resp: Response) -> Result<Download, Ezs3Err> {
        if !resp.status().is_success() {
            return response_to_error(resp).await;
        }

        let last_modified = resp.headers().require_str(&header::LAST_MODIFIED)?;
        // let last_modified: Timestamp = last_modified
        //     .parse()
        //     .map_err(|e| Ezs3Err::InvalidHeader(format!("last-modified: {}", &e)))?;
        let last_modified: Timestamp = chrono::DateTime::parse_from_rfc2822(&last_modified)
            .map_err(|e| Ezs3Err::InvalidHeader(format!("last-modified: {}", &e)))?
            .into();
        let content_type = resp
            .headers()
            .require_str(&header::CONTENT_TYPE)?
            .to_string();
        let etag = resp.headers().require_str(&header::ETAG)?.to_string();
        let data = match resp.bytes().await {
            Ok(data) => data.to_vec(),
            Err(e) => {
                let msg = format!("retrieving response body: {}", &e);
                return Err(Ezs3Err::Other(msg));
            }
        };

        let download = Download {
            data,
            last_modified,
            content_type,
            etag,
        };

        Ok(download)
    }
}

async fn response_to_error<T>(resp: Response) -> Result<T, Ezs3Err> {
    let status = resp.status();
    let errmsg = match resp.text().await {
        Ok(body_msg) => format!(
            "response {} ({}): {}",
            status.as_u16(),
            status.canonical_reason().unwrap_or("?"),
            &body_msg
        ),
        Err(e) => format!(
            "response {} ({}); also, reading response body: {}",
            status.as_u16(),
            status.canonical_reason().unwrap_or("?"),
            &e
        ),
    };
    Err(Ezs3Err::HttpErr(errmsg))
}

/// The client for making requests to the S3 endpoint. It holds all the
/// data and credentials required.
#[derive(Debug, Clone)]
pub struct Client {
    pub(crate) reqwester: reqwest::Client,
    pub(crate) endpoint: Url,
    pub(crate) region: String,
    #[allow(dead_code)]
    pub(crate) bucket: String,
    pub(crate) key_id: String,
    pub(crate) key: String,
    pub(crate) host: HeaderValue,
}

impl Client {
    /// Upload the given data to the `Client`'s S3 bucket.
    pub async fn upload<V>(&self, key: &str, data: V, content_type: &str) -> Result<(), Ezs3Err>
    where
        V: Into<Vec<u8>>,
    {
        let body = Body::new(data, content_type)?;
        let req = self.prepare(Method::PUT, Some(key), &[], Some(body))?;
        let resp = self
            .reqwester
            .execute(req)
            .await
            .map_err(|e| Ezs3Err::RequestErr(e.to_string()))?;

        if !resp.status().is_success() {
            return response_to_error(resp).await;
        }

        Ok(())
    }

    /// Retrieve the object denoted by `key` from the `Client`'s S3
    /// storage bucket.
    pub async fn download(&self, key: &str) -> Result<Download, Ezs3Err> {
        let req = self.prepare(Method::GET, Some(key), &[], None)?;
        let resp = self
            .reqwester
            .execute(req)
            .await
            .map_err(|e| Ezs3Err::RequestErr(e.to_string()))?;

        if !resp.status().is_success() {
            return response_to_error(resp).await;
        }

        Download::from_response(resp).await
    }

    /// List all of the objects in the `Client`'s S3 storage bucket.
    pub async fn list(&self) -> Result<Vec<ObjectData>, Ezs3Err> {
        let req = self.prepare(Method::GET, None, &[("list-type", "2")], None)?;
        let resp = self
            .reqwester
            .execute(req)
            .await
            .map_err(|e| Ezs3Err::RequestErr(e.to_string()))?;

        if !resp.status().is_success() {
            return response_to_error(resp).await;
        }

        let body = resp
            .text()
            .await
            .map_err(|e| Ezs3Err::RequestErr(format!("getting response body as string: {}", &e)))?;

        answers::parse_list_response_body(&body)
    }

    /// Delete the given object from the `Client`'s S3 storage bucket.
    ///
    /// If the bucket has versioning, you can supply a `version` ID to
    /// delete a specific version.
    pub async fn delete(&self, key: &str, version: Option<&str>) -> Result<(), Ezs3Err> {
        let qparms: &[(&str, &str)] = match version {
            None => &[],
            Some(vid) => &[("versionId", vid)],
        };
        let req = self.prepare(Method::DELETE, Some(key), qparms, None)?;
        let resp = self
            .reqwester
            .execute(req)
            .await
            .map_err(|e| Ezs3Err::RequestErr(e.to_string()))?;

        if !resp.status().is_success() {
            response_to_error(resp).await
        } else {
            Ok(())
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use std::sync::OnceLock;

    static SECRETS_PATH: &str = "/home/dan/dev/pl-site/secrets/tigris_cfg.json";
    static ALREADY_LOGGING: OnceLock<bool> = OnceLock::new();
    static TEST_DATA: &str = r#"# Imagine that this is some test data.

frogs = "blue"
wings = "leathery"

[salamander]
color = "orange"
legs = 4
disposition = { appearance = "cute", behavior = "shy" }
"#;

    pub fn ensure_logging() {
        use tracing_subscriber::{EnvFilter, fmt, prelude::*};

        if let Err(_) = ALREADY_LOGGING.set(true) {
            return;
        }

        tracing_subscriber::registry()
            .with(fmt::layer())
            .with(EnvFilter::from_env("EZS3_LOG"))
            .init();
    }

    fn test_client() -> Client {
        Client::configure(Some(SECRETS_PATH)).expect("building test client")
    }

    fn get_random_bytes(n: usize) -> Result<Vec<u8>, String> {
        use std::io::Read;
        let mut buff: Vec<u8> = vec![0u8; n];
        let mut src = std::fs::File::open("/dev/random")
            .map_err(|e| format!("opening /dev/random: {}", &e))?;
        src.read_exact(&mut buff[0..n])
            .map_err(|e| format!("reading from /dev/urandom: {}", &e))?;
        Ok(buff)
    }

    #[tokio::test]
    async fn test_put() -> Result<(), Ezs3Err> {
        ensure_logging();
        let client = test_client();
        client
            .upload("test_payload.toml", TEST_DATA, "text/toml")
            .await
            .expect("the data to be uploaded");
        Ok(())
    }

    #[tokio::test]
    async fn test_get() -> Result<(), Ezs3Err> {
        ensure_logging();
        let client = test_client();
        client.download("test_payload.toml").await?;

        Ok(())
    }

    #[tokio::test]
    async fn test_list() -> Result<(), Ezs3Err> {
        ensure_logging();
        let client = test_client();
        let list = client.list().await?;

        println!("{:#?}", &list);
        Ok(())
    }

    #[tokio::test]
    async fn doc_example() -> Result<(), Ezs3Err> {
        ensure_logging();
        let client = test_client();

        let bytes = std::fs::read("portrait_of_cat.jpg").unwrap();
        client
            .upload("portrait_of_cat.jpg", bytes, "image/jpeg")
            .await
            .unwrap();

        let listing = client.list().await.unwrap();

        println!("{:#?}", &listing);

        Ok(())
    }
}
