use super::base::PlatzClient;
use super::error::PlatzClientError;
use async_trait::async_trait;
use reqwest::{ClientBuilder, RequestBuilder};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::collections::HashMap;
use tracing::instrument;

#[derive(Clone)]
pub struct PlatzRequest<'a> {
    client: &'a PlatzClient,
    method: reqwest::Method,
    path: String,
    query: HashMap<String, String>,
}

#[derive(Deserialize)]
pub struct Paginated<T> {
    pub page: i64,
    pub per_page: i64,
    pub items: Vec<T>,
    pub num_total: i64,
}

lazy_static::lazy_static! {
    static ref HTTP_USER_AGENT: String = format!(
        "{}/{}/{}",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        option_env!("CARGO_BIN_NAME").unwrap_or("lib")
    );
}

impl<'a> PlatzRequest<'a> {
    pub fn new<S>(client: &'a PlatzClient, method: reqwest::Method, path: S) -> Self
    where
        S: AsRef<str>,
    {
        Self {
            client,
            method,
            path: path.as_ref().to_owned(),
            query: Default::default(),
        }
    }

    pub fn query<K, V>(mut self, key: K, value: V) -> Self
    where
        K: AsRef<str>,
        V: AsRef<str>,
    {
        self.query
            .insert(key.as_ref().to_owned(), value.as_ref().to_owned());
        self
    }

    pub fn add_to_query<I, K, V>(mut self, iter: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        for (key, value) in iter.into_iter() {
            self.query
                .insert(key.as_ref().to_owned(), value.as_ref().to_owned());
        }
        self
    }

    pub async fn request_builder(&self) -> Result<RequestBuilder, PlatzClientError> {
        let (header_key, header_value) = self.client.authorization().await?;
        Ok(ClientBuilder::new()
            .user_agent(HTTP_USER_AGENT.clone())
            .gzip(true)
            .brotli(true)
            .deflate(true)
            .build()?
            .request(
                self.method.clone(),
                self.client.build_url(&self.path).await?,
            )
            .header(header_key, header_value)
            .query(&self.query))
    }

    pub async fn send_with_no_response(self) -> Result<(), PlatzClientError> {
        self.request_builder()
            .await?
            .send()
            .await?
            .error_for_status_with_body()
            .await?;
        Ok(())
    }

    pub async fn send<T>(self) -> Result<T, PlatzClientError>
    where
        T: DeserializeOwned + Send,
    {
        Ok(self
            .request_builder()
            .await?
            .send()
            .await?
            .error_for_status_with_body()
            .await?
            .json()
            .await?)
    }

    #[instrument(skip_all, fields(path=self.path))]
    pub async fn send_with_body<T, R>(self, body: T) -> Result<R, PlatzClientError>
    where
        T: Serialize,
        R: DeserializeOwned + Send,
    {
        Ok(self
            .request_builder()
            .await?
            .json(&body)
            .send()
            .await?
            .error_for_status_with_body()
            .await?
            .json()
            .await?)
    }

    pub async fn single_page<T>(
        &self,
        page_index: i64,
        page_size: Option<i64>,
    ) -> Result<Paginated<T>, PlatzClientError>
    where
        T: DeserializeOwned + Send,
        Paginated<T>: DeserializeOwned + Send,
    {
        let mut paging_info = vec![("page", page_index.to_string())];
        if let Some(size) = page_size {
            paging_info.push(("page_size", size.to_string()))
        }
        let page = self
            .request_builder()
            .await?
            .query(&paging_info)
            .send()
            .await?
            .error_for_status_with_body()
            .await?
            .json()
            .await?;
        Ok(page)
    }

    #[instrument(skip_all, fields(path=self.path))]
    pub async fn paginated<T>(self) -> Result<Vec<T>, PlatzClientError>
    where
        T: DeserializeOwned + Send,
        Paginated<T>: DeserializeOwned + Send,
    {
        let page_size: Option<i64> = None;
        let mut cur_page = self.single_page(1, page_size).await?;
        let mut items = cur_page.items;

        while cur_page.page * cur_page.per_page < cur_page.num_total {
            let next_page = cur_page.page + 1;
            cur_page = self.single_page(next_page, page_size).await?;
            items.extend(cur_page.items.into_iter());
        }

        Ok(items)
    }

    #[instrument(skip_all, fields(path=self.path))]
    pub async fn paginated_expect_one<T>(self) -> Result<T, PlatzClientError>
    where
        T: DeserializeOwned + Send,
        Paginated<T>: DeserializeOwned + Send,
    {
        let items = self.paginated().await?;
        match items.len() {
            0 => Err(PlatzClientError::ExpectedOneGotNone),
            1 => Ok(items.into_iter().next().unwrap()),
            n => Err(PlatzClientError::ExpectedOneGotMany(n)),
        }
    }
}
#[async_trait]
trait ResponseExt {
    async fn error_for_status_with_body(self) -> Result<reqwest::Response, PlatzClientError>;
}

#[async_trait]
impl ResponseExt for reqwest::Response {
    async fn error_for_status_with_body(self) -> Result<reqwest::Response, PlatzClientError> {
        let status = self.status();
        if status.is_success() {
            Ok(self)
        } else {
            let body = self
                .text()
                .await
                .ok()
                .map(|s| format!(": {s:?}"))
                .unwrap_or_default();
            let err_msg = format!("{status}{body}");
            Err(PlatzClientError::HttpError(err_msg))
        }
    }
}
