//! The Kranth client. One per process is fine; reqwest::Client multiplexes.

use std::sync::Arc;
use std::time::Duration;

use serde::de::DeserializeOwned;
use serde::Serialize;

use crate::billing::BillingResource;
use crate::errors::{Error, Result};
use crate::recon::ReconResource;
use crate::debates::DebatesResource;
use crate::resources::{ApiKeysResource, ModelsResource, SimsResource};
use crate::types::Me;

const DEFAULT_BASE_URL: &str = "https://api.kranth.ai";
const USER_AGENT: &str = "kranth-rust/0.2.0";

#[derive(Clone)]
pub struct Kranth {
    inner: Arc<Inner>,
}

struct Inner {
    api_key: String,
    base_url: String,
    http: reqwest::Client,
}

impl Kranth {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self::builder(api_key).build()
    }

    pub fn builder(api_key: impl Into<String>) -> KranthBuilder {
        KranthBuilder {
            api_key: api_key.into(),
            base_url: None,
            http: None,
            timeout: Some(Duration::from_secs(30)),
        }
    }

    pub fn api_key(&self) -> &str {
        &self.inner.api_key
    }
    pub fn base_url(&self) -> &str {
        &self.inner.base_url
    }
    pub(crate) fn http(&self) -> &reqwest::Client {
        &self.inner.http
    }

    pub fn sims(&self) -> SimsResource<'_> {
        SimsResource { client: self }
    }
    pub fn api_keys(&self) -> ApiKeysResource<'_> {
        ApiKeysResource { client: self }
    }
    pub fn models(&self) -> ModelsResource<'_> {
        ModelsResource { client: self }
    }
    pub fn billing(&self) -> BillingResource<'_> {
        BillingResource { client: self }
    }
    pub fn recon(&self) -> ReconResource<'_> {
        ReconResource { client: self }
    }
    pub fn debates(&self) -> DebatesResource<'_> {
        DebatesResource { client: self }
    }

    pub async fn me(&self) -> Result<Me> {
        self.get("/v1/me", &[]).await
    }

    // ─── internal helpers ───────────────────────────────────

    pub(crate) async fn get<T: DeserializeOwned>(
        &self,
        path: &str,
        query: &[(&str, String)],
    ) -> Result<T> {
        let mut req = self
            .inner
            .http
            .get(format!("{}{}", self.inner.base_url, path))
            .bearer_auth(&self.inner.api_key)
            .header("user-agent", USER_AGENT)
            .header("accept", "application/json");
        if !query.is_empty() {
            req = req.query(query);
        }
        let resp = req.send().await?;
        decode(resp).await
    }

    pub(crate) async fn post<B: Serialize, T: DeserializeOwned>(
        &self,
        path: &str,
        body: &B,
        idempotency_key: Option<&str>,
    ) -> Result<T> {
        let mut req = self
            .inner
            .http
            .post(format!("{}{}", self.inner.base_url, path))
            .bearer_auth(&self.inner.api_key)
            .header("user-agent", USER_AGENT)
            .header("accept", "application/json")
            .json(body);
        if let Some(k) = idempotency_key {
            req = req.header("idempotency-key", k);
        }
        let resp = req.send().await?;
        decode(resp).await
    }

    #[allow(dead_code)] // wired in the next billing brick (refund / cancel POSTs with no body)
    pub(crate) async fn post_empty(&self, path: &str) -> Result<()> {
        let resp = self
            .inner
            .http
            .post(format!("{}{}", self.inner.base_url, path))
            .bearer_auth(&self.inner.api_key)
            .header("user-agent", USER_AGENT)
            .send()
            .await?;
        decode_empty(resp).await
    }

    pub(crate) async fn patch<B: Serialize, T: DeserializeOwned>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T> {
        let resp = self
            .inner
            .http
            .patch(format!("{}{}", self.inner.base_url, path))
            .bearer_auth(&self.inner.api_key)
            .header("user-agent", USER_AGENT)
            .header("accept", "application/json")
            .json(body)
            .send()
            .await?;
        decode(resp).await
    }

    pub(crate) async fn delete_empty(&self, path: &str) -> Result<()> {
        let resp = self
            .inner
            .http
            .delete(format!("{}{}", self.inner.base_url, path))
            .bearer_auth(&self.inner.api_key)
            .header("user-agent", USER_AGENT)
            .send()
            .await?;
        decode_empty(resp).await
    }
}

pub struct KranthBuilder {
    api_key: String,
    base_url: Option<String>,
    http: Option<reqwest::Client>,
    timeout: Option<Duration>,
}

impl KranthBuilder {
    pub fn base_url(mut self, url: impl Into<String>) -> Self {
        self.base_url = Some(url.into());
        self
    }
    pub fn http_client(mut self, http: reqwest::Client) -> Self {
        self.http = Some(http);
        self
    }
    pub fn timeout(mut self, t: Duration) -> Self {
        self.timeout = Some(t);
        self
    }
    pub fn build(self) -> Kranth {
        let http = self.http.unwrap_or_else(|| {
            reqwest::Client::builder()
                .timeout(self.timeout.unwrap_or(Duration::from_secs(30)))
                .build()
                .unwrap_or_default()
        });
        let mut base = self
            .base_url
            .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
        if base.ends_with('/') {
            base.pop();
        }
        Kranth {
            inner: Arc::new(Inner {
                api_key: self.api_key,
                base_url: base,
                http,
            }),
        }
    }
}

async fn decode<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T> {
    let status = resp.status();
    let retry_after = resp
        .headers()
        .get("retry-after")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<f64>().ok());
    let bytes = resp.bytes().await?;
    if status.is_success() {
        if bytes.is_empty() {
            // The caller asked for T but the server returned 204 — that's
            // a contract bug for typed callers.  Force decode (will likely
            // fail on JSON parse, which is the right error).
        }
        return Ok(serde_json::from_slice(&bytes)?);
    }
    Err(api_error(status.as_u16(), &bytes, retry_after))
}

async fn decode_empty(resp: reqwest::Response) -> Result<()> {
    let status = resp.status();
    let retry_after = resp
        .headers()
        .get("retry-after")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<f64>().ok());
    let bytes = resp.bytes().await?;
    if status.is_success() {
        return Ok(());
    }
    Err(api_error(status.as_u16(), &bytes, retry_after))
}

fn api_error(status: u16, body: &[u8], retry_after: Option<f64>) -> Error {
    let parsed: serde_json::Value = serde_json::from_slice(body).unwrap_or(serde_json::Value::Null);
    let code = parsed
        .get("error")
        .and_then(|v| v.as_str())
        .unwrap_or("api_error")
        .to_string();
    let message = parsed
        .get("message")
        .and_then(|v| v.as_str())
        .unwrap_or("request failed")
        .to_string();
    match status {
        401 => Error::Unauthorized { code, message },
        402 => Error::PaymentRequired { code, message },
        422 => Error::Validation { code, message },
        429 => Error::RateLimited {
            code,
            message,
            retry_after,
        },
        _ => Error::Api {
            status,
            code,
            message,
        },
    }
}
