// US-003: RestClient — core HTTP client

use reqwest::Client;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::time::Duration;
use url::Url;

use crate::auth::Credentials;
use crate::rest::error::AsterDexError;
use crate::rest::request::parse_response;
use crate::rest::response::ApiResponse;

const DEFAULT_BASE_URL: &str = "https://fapi.asterdex.com";
// Conservative defaults tuned for exchange REST APIs.
// Override by passing a custom `reqwest::Client` if needed in future.
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
const TCP_KEEPALIVE: Duration = Duration::from_secs(60);
const POOL_MAX_IDLE_PER_HOST: usize = 32;

/// Async REST client for AsterDex Futures V3 API.
///
/// Supports both public (unsigned) and authenticated (signed) endpoints.
/// Use `new_public` for public-only access, `from_env` or `new` for authenticated access.
pub struct RestClient {
    pub(crate) base_url: Url,
    pub(crate) credentials: Option<Credentials>,
    pub(crate) http: Client,
}

impl RestClient {
    /// Construct authenticated client from environment variables.
    ///
    /// Reads: `ASTERDEX_USER`, `ASTERDEX_SIGNER_ADDRESS`, `ASTERDEX_SIGNER_PRIVATE_KEY`.
    /// Optional: `ASTERDEX_BASE_URL` (defaults to `https://fapi.asterdex.com`).
    ///
    /// # Errors
    /// - `AsterDexError::ConfigError` if any required env var is missing or invalid
    pub fn from_env() -> Result<Self, AsterDexError> {
        let creds = Credentials::from_env()?;
        let base_url_str = std::env::var("ASTERDEX_BASE_URL")
            .unwrap_or_else(|_| DEFAULT_BASE_URL.to_string());
        Self::new_with_credentials(&base_url_str, creds)
    }

    /// Construct authenticated client with explicit base URL and credentials.
    ///
    /// # Errors
    /// - `AsterDexError::ConfigError` if base URL is invalid
    pub fn new(base_url: &str, credentials: Credentials) -> Result<Self, AsterDexError> {
        Self::new_with_credentials(base_url, credentials)
    }

    /// Construct unauthenticated (public) client.
    ///
    /// Only public endpoints (unsigned GET) will work. Signed endpoints will return
    /// `AsterDexError::ConfigError`.
    ///
    /// # Errors
    /// - `AsterDexError::ConfigError` if base URL is invalid
    pub fn new_public(base_url: &str) -> Result<Self, AsterDexError> {
        let url = normalize_url(base_url)?;
        let http = build_http_client()?;
        Ok(Self {
            base_url: url,
            credentials: None,
            http,
        })
    }

    fn new_with_credentials(
        base_url: &str,
        creds: Credentials,
    ) -> Result<Self, AsterDexError> {
        let url = normalize_url(base_url)?;
        let http = build_http_client()?;
        Ok(Self {
            base_url: url,
            credentials: Some(creds),
            http,
        })
    }

    // --- Internal: unsigned GET ---
    pub(crate) async fn get<T: DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let url = self.build_url(path, params)?;
        tracing::debug!(method = "GET", path = path, signed = false, "REST request");
        let response = self
            .http
            .get(url)
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        let result = parse_response::<T>(response).await;
        self.log_response(&result);
        result
    }

    // --- Internal: unsigned POST ---
    pub(crate) async fn post<T: DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let url = self.build_url(path, params)?;
        tracing::debug!(method = "POST", path = path, signed = false, "REST request");
        let response = self
            .http
            .post(url)
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        parse_response::<T>(response).await
    }

    // --- Internal: unsigned DELETE ---
    pub(crate) async fn delete<T: DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let url = self.build_url(path, params)?;
        tracing::debug!(method = "DELETE", path = path, signed = false, "REST request");
        let response = self
            .http
            .delete(url)
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        parse_response::<T>(response).await
    }

    // --- Internal: SIGNED GET ---
    pub(crate) async fn signed_get<T: DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let full_url = self.build_signed_url(path, params)?;
        tracing::debug!(method = "GET", path = path, signed = true, "REST request");
        let response = self
            .http
            .get(&full_url)
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        let result = parse_response::<T>(response).await;
        self.log_response(&result);
        result
    }

    // --- Internal: SIGNED POST (query string only, empty body) ---
    pub(crate) async fn signed_post<T: DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let full_url = self.build_signed_url(path, params)?;
        tracing::debug!(method = "POST", path = path, signed = true, "REST request");
        let response = self
            .http
            .post(&full_url)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        let result = parse_response::<T>(response).await;
        self.log_response(&result);
        result
    }

    // --- Internal: SIGNED DELETE ---
    pub(crate) async fn signed_delete<T: DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let full_url = self.build_signed_url(path, params)?;
        tracing::debug!(method = "DELETE", path = path, signed = true, "REST request");
        let response = self
            .http
            .delete(&full_url)
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        let result = parse_response::<T>(response).await;
        self.log_response(&result);
        result
    }

    // --- Internal: SIGNED PUT (for listenKey renewal) ---
    pub(crate) async fn signed_put<T: DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let full_url = self.build_signed_url(path, params)?;
        tracing::debug!(method = "PUT", path = path, signed = true, "REST request");
        let response = self
            .http
            .put(&full_url)
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        let result = parse_response::<T>(response).await;
        self.log_response(&result);
        result
    }

    // --- Internal: SIGNED POST with JSON body (for batchOrders) ---
    ///
    /// Generic over `Serialize` to avoid the `serde_json::to_value` → `Value` → bytes
    /// round trip; reqwest serializes the body directly from `&B`.
    pub(crate) async fn signed_post_json<B: Serialize + ?Sized, T: DeserializeOwned>(
        &self,
        path: &str,
        query_params: &[(&str, &str)],
        body: &B,
    ) -> Result<ApiResponse<T>, AsterDexError> {
        let full_url = self.build_signed_url(path, query_params)?;
        tracing::debug!(method = "POST", path = path, signed = true, "REST request (JSON body)");
        let response = self
            .http
            .post(&full_url)
            .json(body)
            .send()
            .await
            .map_err(AsterDexError::from_reqwest)?;
        let result = parse_response::<T>(response).await;
        self.log_response(&result);
        result
    }

    // -------------------------------------------------------------------------
    // US-016: Raw JSON passthrough methods (ADR-011)
    // These delegate to the internal typed methods with T = serde_json::Value.
    // Error mapping (429 → RateLimited, etc.) applies identically to typed methods.
    // -------------------------------------------------------------------------

    /// Make an unsigned GET request and return the raw JSON response.
    ///
    /// Use when the typed response struct does not cover all fields, or for debugging.
    /// Error mapping (429 → `RateLimited`, etc.) applies identically to typed methods.
    ///
    /// # Errors
    /// - `AsterDexError::ApiError` — if the server returns a 4xx error with `{code, msg}`
    /// - `AsterDexError::RateLimited` — if the server returns 429
    /// - `AsterDexError::NetworkError` — if a network-level error occurs
    ///
    /// # Examples
    /// ```no_run
    /// # async fn example(client: &asterdex_sdk::RestClient) -> Result<(), asterdex_sdk::AsterDexError> {
    /// let resp = client.raw_get("/fapi/v3/exchangeInfo", &[]).await?;
    /// println!("{}", resp.data);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn raw_get(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<serde_json::Value>, AsterDexError> {
        self.get(path, params).await
    }

    /// Make an unsigned POST request and return the raw JSON response.
    ///
    /// # Errors
    /// - `AsterDexError::ApiError` — if the server returns a 4xx error with `{code, msg}`
    /// - `AsterDexError::RateLimited` — if the server returns 429
    /// - `AsterDexError::NetworkError` — if a network-level error occurs
    pub async fn raw_post(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<serde_json::Value>, AsterDexError> {
        self.post(path, params).await
    }

    /// Make an unsigned DELETE request and return the raw JSON response.
    ///
    /// # Errors
    /// - `AsterDexError::ApiError` — if the server returns a 4xx error with `{code, msg}`
    /// - `AsterDexError::RateLimited` — if the server returns 429
    /// - `AsterDexError::NetworkError` — if a network-level error occurs
    pub async fn raw_delete(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<serde_json::Value>, AsterDexError> {
        self.delete(path, params).await
    }

    /// Make a signed GET request and return the raw JSON response (EIP-712 authenticated).
    ///
    /// Requires credentials to be set (use `RestClient::from_env()`).
    ///
    /// # Errors
    /// - `AsterDexError::ConfigError` — if credentials are not set
    /// - `AsterDexError::ApiError` — if the server returns a 4xx error with `{code, msg}`
    /// - `AsterDexError::RateLimited` — if the server returns 429
    /// - `AsterDexError::NetworkError` — if a network-level error occurs
    pub async fn raw_signed_get(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<serde_json::Value>, AsterDexError> {
        self.signed_get(path, params).await
    }

    /// Make a signed POST request and return the raw JSON response.
    ///
    /// Requires credentials to be set (use `RestClient::from_env()`).
    ///
    /// # Errors
    /// - `AsterDexError::ConfigError` — if credentials are not set
    /// - `AsterDexError::ApiError` — if the server returns a 4xx error with `{code, msg}`
    /// - `AsterDexError::RateLimited` — if the server returns 429
    /// - `AsterDexError::NetworkError` — if a network-level error occurs
    pub async fn raw_signed_post(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<serde_json::Value>, AsterDexError> {
        self.signed_post(path, params).await
    }

    /// Make a signed DELETE request and return the raw JSON response.
    ///
    /// Requires credentials to be set (use `RestClient::from_env()`).
    ///
    /// # Errors
    /// - `AsterDexError::ConfigError` — if credentials are not set
    /// - `AsterDexError::ApiError` — if the server returns a 4xx error with `{code, msg}`
    /// - `AsterDexError::RateLimited` — if the server returns 429
    /// - `AsterDexError::NetworkError` — if a network-level error occurs
    pub async fn raw_signed_delete(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<ApiResponse<serde_json::Value>, AsterDexError> {
        self.signed_delete(path, params).await
    }

    /// Build a signed URL: sign params with EIP-712 and append `&signature=...`.
    fn build_signed_url(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<String, AsterDexError> {
        let creds = self.require_credentials()?;
        let signed_query = creds.sign_query(params)?;
        let base = self.base_url.as_str().trim_end_matches('/');
        Ok(format!("{base}{path}?{signed_query}"))
    }

    fn require_credentials(&self) -> Result<&Credentials, AsterDexError> {
        self.credentials
            .as_ref()
            .ok_or_else(|| AsterDexError::ConfigError {
                message: "credentials required for signed endpoint".to_string(),
            })
    }

    fn build_url(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<String, AsterDexError> {
        let base = self.base_url.as_str().trim_end_matches('/');
        if params.is_empty() {
            Ok(format!("{base}{path}"))
        } else {
            let qs = url::form_urlencoded::Serializer::new(String::new())
                .extend_pairs(params)
                .finish();
            Ok(format!("{base}{path}?{qs}"))
        }
    }

    fn log_response<T>(&self, result: &Result<ApiResponse<T>, AsterDexError>) {
        match result {
            Ok(r) => tracing::debug!(
                used_weight = ?r.used_weight,
                order_count = ?r.order_count,
                "REST response OK"
            ),
            Err(e) => tracing::warn!(error_kind = ?e, "REST error"),
        }
    }
}

fn normalize_url(url: &str) -> Result<Url, AsterDexError> {
    let url = if url.ends_with('/') {
        url.to_string()
    } else {
        format!("{url}/")
    };
    Url::parse(&url).map_err(|e| AsterDexError::ConfigError {
        message: format!("invalid base URL: {e}"),
    })
}

fn build_http_client() -> Result<Client, AsterDexError> {
    Client::builder()
        .use_rustls_tls()
        .timeout(REQUEST_TIMEOUT)
        .connect_timeout(CONNECT_TIMEOUT)
        .pool_idle_timeout(POOL_IDLE_TIMEOUT)
        .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
        .tcp_keepalive(TCP_KEEPALIVE)
        .build()
        .map_err(|e| AsterDexError::ConfigError {
            message: format!("failed to build HTTP client: {e}"),
        })
}

#[cfg(test)]
mod tests {
    use super::*;
    use mockito::Server;

    // US-003: Test public GET 200 — verify Ok(ApiResponse) with rate limit headers
    #[tokio::test]
    async fn test_public_get_200() {
        let mut server = Server::new_async().await;
        let mock = server
            .mock("GET", "/fapi/v3/ping")
            .with_status(200)
            .with_header("X-MBX-USED-WEIGHT-1MINUTE", "10")
            .with_header("X-MBX-ORDER-COUNT-1MINUTE", "2")
            .with_body("{}")
            .create_async()
            .await;

        let client = RestClient::new_public(&server.url()).unwrap();
        let result = client
            .get::<serde_json::Value>("/fapi/v3/ping", &[])
            .await
            .unwrap();

        assert_eq!(result.used_weight, Some(10));
        assert_eq!(result.order_count, Some(2));
        mock.assert_async().await;
    }

    // US-003: Test 4xx API error maps to AsterDexError::ApiError
    #[tokio::test]
    async fn test_api_error_4xx() {
        let mut server = Server::new_async().await;
        let mock = server
            .mock("GET", "/fapi/v3/depth")
            .with_status(400)
            .with_header("content-type", "application/json")
            .with_body(
                r#"{"code":-1102,"msg":"Mandatory parameter 'symbol' was not sent"}"#,
            )
            .create_async()
            .await;

        let client = RestClient::new_public(&server.url()).unwrap();
        let result: Result<ApiResponse<serde_json::Value>, _> =
            client.get("/fapi/v3/depth", &[]).await;

        assert!(matches!(
            result,
            Err(AsterDexError::ApiError { code: -1102, .. })
        ));
        if let Err(AsterDexError::ApiError { msg, .. }) = &result {
            assert_eq!(msg, "Mandatory parameter 'symbol' was not sent");
        }
        mock.assert_async().await;
    }

    // US-003: Test HTTP 429 maps to AsterDexError::RateLimited
    #[tokio::test]
    async fn test_rate_limited_429() {
        let mut server = Server::new_async().await;
        let mock = server
            .mock("GET", "/fapi/v3/ping")
            .with_status(429)
            .with_header("Retry-After", "30")
            .create_async()
            .await;

        let client = RestClient::new_public(&server.url()).unwrap();
        let result: Result<ApiResponse<serde_json::Value>, _> =
            client.get("/fapi/v3/ping", &[]).await;

        assert!(
            matches!(result, Err(AsterDexError::RateLimited { retry_after: Some(d) }) if d.as_secs() == 30)
        );
        mock.assert_async().await;
    }

    // US-003: Test HTTP 418 maps to AsterDexError::IpBanned
    #[tokio::test]
    async fn test_ip_banned_418() {
        let mut server = Server::new_async().await;
        let mock = server
            .mock("GET", "/fapi/v3/ping")
            .with_status(418)
            .with_header("Retry-After", "7200")
            .create_async()
            .await;

        let client = RestClient::new_public(&server.url()).unwrap();
        let result: Result<ApiResponse<serde_json::Value>, _> =
            client.get("/fapi/v3/ping", &[]).await;

        assert!(
            matches!(result, Err(AsterDexError::IpBanned { retry_after: Some(d) }) if d.as_secs() == 7200)
        );
        mock.assert_async().await;
    }

    // US-003: Test 5xx maps to AsterDexError::HttpError
    #[tokio::test]
    async fn test_server_error_5xx() {
        let mut server = Server::new_async().await;
        let mock = server
            .mock("GET", "/fapi/v3/ping")
            .with_status(500)
            .with_body("Internal Server Error")
            .create_async()
            .await;

        let client = RestClient::new_public(&server.url()).unwrap();
        let result: Result<ApiResponse<serde_json::Value>, _> =
            client.get("/fapi/v3/ping", &[]).await;

        assert!(
            matches!(result, Err(AsterDexError::HttpError { status: 500, ref body }) if body == "Internal Server Error")
        );
        mock.assert_async().await;
    }
}
