//! Rust types corresponding to those returned by the GCS API.

use reqwest::StatusCode;
use serde::{de, Deserialize, Serialize};

#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
    #[error(transparent)]
    Http(#[from] reqwest::Error),

    #[error(transparent)]
    Google(#[from] GoogleError),
}

#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum GoogleErrorReponse<T> {
    Error { error: GoogleError },
    Ok(T),
}

/// A raw error returned by the GCS API. This is only returned when there's no more
/// specific error known to this library (e.g., server error, but not `NotFound`).
#[derive(Debug, Deserialize, thiserror::Error)]
#[error("request failed with status {status} and message: {message}")]
pub struct GoogleError {
    #[serde(rename = "code", deserialize_with = "deserialize_status_code_from_u16")]
    pub(crate) status: StatusCode,
    pub(crate) message: String,
}

fn deserialize_status_code_from_u16<'de, D: de::Deserializer<'de>>(
    d: D,
) -> Result<StatusCode, D::Error> {
    StatusCode::from_u16(u16::deserialize(d)?).map_err(de::Error::custom)
}

#[async_trait::async_trait]
pub(crate) trait DecodeResponse {
    async fn decode_response<T: de::DeserializeOwned + Default>(self) -> Result<T, Error>;
}

#[async_trait::async_trait]
impl DecodeResponse for reqwest::Response {
    async fn decode_response<T: de::DeserializeOwned + Default>(self) -> Result<T, Error> {
        let res_status = self.status();
        if res_status == StatusCode::NO_CONTENT {
            return Ok(Default::default());
        }
        let res: GoogleErrorReponse<T> =
            self.json()
                .await
                .unwrap_or_else(|_| GoogleErrorReponse::Error {
                    error: GoogleError {
                        status: res_status,
                        message: "".into(),
                    },
                });
        match res {
            GoogleErrorReponse::Ok(t) => Ok(t),
            GoogleErrorReponse::Error { error } => Err(error.into()),
        }
    }
}

/// A page of results.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Page<T> {
    /// If present, the value to set as the `page_token` when making a subsequent `list` call
    /// that causes the next page of results to be fetched.
    #[serde(default)]
    pub next_page_token: Option<String>,

    /// The returned results.
    #[serde(default = "Vec::new")]
    pub items: Vec<T>,
}

/// A parsimonious representation of an object's metadata.
#[derive(Debug, Clone, Default, Deserialize, Hash, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Object {
    /// The object's fully qualified ID.
    pub id: String,

    /// The object's name.
    pub name: String,

    /// The object's size in bytes.
    #[serde(deserialize_with = "serde_with::rust::display_fromstr::deserialize")]
    pub size: u64,
}

/// Options for filtering which objects are returned from a `list` call.
#[serde_with::skip_serializing_none]
#[derive(Debug, Default, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ListObjectOptions<'a> {
    /// Return only objects having keys starting with this prefix.
    pub prefix: Option<&'a str>,

    /// The start offset of results.
    pub start_offset: Option<&'a str>,

    /// The end offset of results.
    pub end_offset: Option<&'a str>,

    /// The maximum number of results to return.
    pub max_results: Option<u32>,

    /// The page token.
    pub page_token: Option<&'a str>,
}

impl<'a> From<&'a str> for ListObjectOptions<'a> {
    fn from(prefix: &'a str) -> Self {
        Self {
            prefix: Some(prefix),
            ..Default::default()
        }
    }
}

const PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
    .remove(b'*')
    .remove(b'-')
    .remove(b'.')
    .remove(b'_');

pub(crate) fn percent_encode(input: &str) -> std::borrow::Cow<'_, str> {
    percent_encoding::utf8_percent_encode(input, PERCENT_ENCODE_SET).into()
}
