use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;

use serde::de::DeserializeOwned;
use serde_json::Value;

use crate::error::Error;
use crate::jsonrpc::{Request, Response};

const ENDPOINT: &str = "https://api.random.org/json-rpc/4/invoke";

#[derive(Clone)]
pub struct Client {
    pub(crate) api_key: Arc<str>,
    pub(crate) http: reqwest::Client,
    id: Arc<AtomicU64>,
    endpoint: Arc<str>,
}

pub struct Builder {
    api_key: String,
    endpoint: String,
    timeout: Duration,
    user_agent: String,
}

impl Builder {
    pub fn endpoint(mut self, e: impl Into<String>) -> Self {
        self.endpoint = e.into();
        self
    }
    pub fn timeout(mut self, t: Duration) -> Self {
        self.timeout = t;
        self
    }
    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
        self.user_agent = ua.into();
        self
    }

    pub fn build(self) -> Result<Client, Error> {
        let http = reqwest::Client::builder()
            .timeout(self.timeout)
            .user_agent(self.user_agent)
            .build()?;
        Ok(Client {
            api_key: Arc::from(self.api_key),
            http,
            id: Arc::new(AtomicU64::new(1)),
            endpoint: Arc::from(self.endpoint),
        })
    }
}

impl Client {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self::builder(api_key)
            .build()
            .expect("default reqwest client must build")
    }

    pub fn builder(api_key: impl Into<String>) -> Builder {
        Builder {
            api_key: api_key.into(),
            endpoint: ENDPOINT.to_string(),
            timeout: Duration::from_secs(30),
            user_agent: format!("randomorg-rpc/{}", env!("CARGO_PKG_VERSION")),
        }
    }

    pub fn api_key(&self) -> &str {
        &self.api_key
    }

    pub(crate) fn next_id(&self) -> u64 {
        self.id.fetch_add(1, Ordering::Relaxed)
    }

    /// Inject `apiKey` into a params object and dispatch.
    pub(crate) async fn call_keyed<T: DeserializeOwned>(
        &self,
        method: &str,
        mut params: Value,
    ) -> Result<T, Error> {
        if let Some(obj) = params.as_object_mut() {
            obj.insert(
                "apiKey".to_string(),
                Value::String(self.api_key.to_string()),
            );
        }
        self.call(method, params).await
    }

    pub(crate) async fn call<T: DeserializeOwned>(
        &self,
        method: &str,
        params: Value,
    ) -> Result<T, Error> {
        let req = Request {
            jsonrpc: "2.0",
            method,
            params,
            id: self.next_id(),
        };

        let body = self
            .http
            .post(self.endpoint.as_ref())
            .json(&req)
            .send()
            .await?
            .error_for_status()?
            .bytes()
            .await?;

        let resp: Response<T> = serde_json::from_slice(&body)?;
        if let Some(err) = resp.error {
            return Err(Error::Api {
                code: err.code,
                message: err.message,
                data: err.data,
            });
        }
        resp.result
            .ok_or_else(|| Error::Invalid("empty result".into()))
    }
}
