use serde::{de::DeserializeOwned, Serialize};

use crate::error::{MadeOnSolError, Result};

const BASE_URL: &str = "https://madeonsol.com/api/v1";

#[derive(Debug)]
pub(crate) struct HttpCore {
    client: reqwest::Client,
    base_url: String,
    api_key: String,
}

impl HttpCore {
    pub(crate) fn new(api_key: String) -> Self {
        Self {
            client: reqwest::Client::new(),
            base_url: BASE_URL.to_string(),
            api_key,
        }
    }

    pub(crate) async fn get<T, Q>(&self, path: &str, query: &Q) -> Result<T>
    where
        T: DeserializeOwned,
        Q: Serialize + ?Sized,
    {
        let url = format!("{}{}", self.base_url, path);
        let resp = self
            .client
            .get(&url)
            .bearer_auth(&self.api_key)
            .header("Accept", "application/json")
            .query(query)
            .send()
            .await?;
        Self::handle(resp).await
    }

    pub(crate) async fn post_empty<T>(&self, path: &str) -> Result<T>
    where
        T: DeserializeOwned,
    {
        let url = format!("{}{}", self.base_url, path);
        let resp = self
            .client
            .post(&url)
            .bearer_auth(&self.api_key)
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .send()
            .await?;
        Self::handle(resp).await
    }

    pub(crate) async fn post_json<T, B>(&self, path: &str, body: &B) -> Result<T>
    where
        T: DeserializeOwned,
        B: Serialize + ?Sized,
    {
        let url = format!("{}{}", self.base_url, path);
        let resp = self
            .client
            .post(&url)
            .bearer_auth(&self.api_key)
            .header("Accept", "application/json")
            .json(body)
            .send()
            .await?;
        Self::handle(resp).await
    }

    pub(crate) async fn patch_json<T, B>(&self, path: &str, body: &B) -> Result<T>
    where
        T: DeserializeOwned,
        B: Serialize + ?Sized,
    {
        let url = format!("{}{}", self.base_url, path);
        let resp = self
            .client
            .patch(&url)
            .bearer_auth(&self.api_key)
            .header("Accept", "application/json")
            .json(body)
            .send()
            .await?;
        Self::handle(resp).await
    }

    pub(crate) async fn delete<T>(&self, path: &str) -> Result<T>
    where
        T: DeserializeOwned,
    {
        let url = format!("{}{}", self.base_url, path);
        let resp = self
            .client
            .delete(&url)
            .bearer_auth(&self.api_key)
            .header("Accept", "application/json")
            .send()
            .await?;
        Self::handle(resp).await
    }

    async fn handle<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T> {
        let status = resp.status();
        let body_text = resp.text().await?;

        if !status.is_success() {
            let body_json: serde_json::Value =
                serde_json::from_str(&body_text).unwrap_or_else(|_| {
                    serde_json::Value::String(body_text.clone())
                });
            let message = body_json
                .get("error")
                .or_else(|| body_json.get("message"))
                .and_then(|m| m.as_str())
                .map(String::from)
                .unwrap_or_else(|| format!("Request failed with status {}", status));
            return Err(MadeOnSolError::Api {
                status: status.as_u16(),
                message,
                body: body_json,
            });
        }

        serde_json::from_str(&body_text).map_err(MadeOnSolError::Json)
    }
}
