use std::time::{SystemTime, UNIX_EPOCH};

use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE};
use reqwest::Method;
use serde::de::DeserializeOwned;

use crate::error::Error;
use crate::signature::{canonical, new_nonce, sign};

mod auth;
mod billing;
mod devices;
mod downloads;
mod github;
mod license;
mod usage;

const H_PRODUCT: HeaderName = HeaderName::from_static("x-akira-product");
const H_TIMESTAMP: HeaderName = HeaderName::from_static("x-akira-timestamp");
const H_NONCE: HeaderName = HeaderName::from_static("x-akira-nonce");
const H_SIGNATURE: HeaderName = HeaderName::from_static("x-akira-signature");

#[derive(Debug, Clone)]
pub struct Client {
    base_url: String,
    product_slug: String,
    product_secret: String,
    customer_token: Option<String>,
    http: reqwest::Client,
}

impl Client {
    pub fn new(
        base_url: impl Into<String>,
        product_slug: impl Into<String>,
        product_secret: impl Into<String>,
    ) -> Self {
        Self {
            base_url: base_url.into(),
            product_slug: product_slug.into(),
            product_secret: product_secret.into(),
            customer_token: None,
            http: reqwest::Client::builder()
                .redirect(reqwest::redirect::Policy::none())
                .build()
                .unwrap_or_default(),
        }
    }

    pub fn set_customer_token(&mut self, token: impl Into<String>) {
        self.customer_token = Some(token.into());
    }

    pub fn clear_customer_token(&mut self) {
        self.customer_token = None;
    }

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

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

    pub fn customer_token(&self) -> Option<&str> {
        self.customer_token.as_deref()
    }

    async fn do_request<B: serde::Serialize, T: DeserializeOwned>(
        &self,
        method: Method,
        path: &str,
        body: Option<&B>,
    ) -> Result<T, Error> {
        let body_bytes = match body {
            Some(b) => serde_json::to_vec(b).map_err(|e| Error::Api {
                status: 0,
                code: format!("serialize body: {e}"),
                body: None,
            })?,
            None => Vec::new(),
        };

        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs() as i64)
            .unwrap_or(0);
        let nonce = new_nonce();
        let canonical_str = canonical(
            &self.product_slug,
            timestamp,
            &nonce,
            method.as_str(),
            path,
            &body_bytes,
        );
        let signature = sign(&self.product_secret, &canonical_str);

        let mut headers = HeaderMap::new();
        headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
        if !body_bytes.is_empty() {
            headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
        }
        headers.insert(H_PRODUCT, header_value(&self.product_slug)?);
        headers.insert(H_TIMESTAMP, header_value(&timestamp.to_string())?);
        headers.insert(H_NONCE, header_value(&nonce)?);
        headers.insert(H_SIGNATURE, header_value(&signature)?);
        if let Some(token) = &self.customer_token {
            headers.insert(AUTHORIZATION, header_value(&format!("Bearer {token}"))?);
        }

        let url = format!("{}{}", self.base_url.trim_end_matches('/'), path);
        let resp = self
            .http
            .request(method, &url)
            .headers(headers)
            .body(body_bytes)
            .send()
            .await?;
        let status = resp.status();
        let retry_after = parse_retry_after(resp.headers());
        let bytes = resp.bytes().await?;

        if status.as_u16() == 429 {
            return Err(Error::Throttled { retry_after });
        }

        if !status.is_success() {
            let parsed = serde_json::from_slice::<serde_json::Value>(&bytes).ok();
            let code = match parsed.as_ref() {
                Some(v) => v
                    .get("error")
                    .and_then(|e| e.as_str())
                    .or_else(|| v.get("message").and_then(|e| e.as_str()))
                    .unwrap_or("")
                    .to_string(),
                None => String::from_utf8_lossy(&bytes).into_owned(),
            };
            return Err(Error::Api {
                status: status.as_u16(),
                code,
                body: parsed,
            });
        }

        if bytes.is_empty() {
            return serde_json::from_value::<T>(serde_json::Value::Null).map_err(|e| Error::Api {
                status: status.as_u16(),
                code: format!("decode response: {e}"),
                body: None,
            });
        }

        serde_json::from_slice::<T>(&bytes).map_err(|e| Error::Api {
            status: status.as_u16(),
            code: format!("decode response: {e}"),
            body: None,
        })
    }

    async fn get_public<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
        let url = format!("{}{}", self.base_url.trim_end_matches('/'), path);
        let resp = self
            .http
            .get(&url)
            .header(ACCEPT, "application/json")
            .send()
            .await?;
        let status = resp.status();
        let bytes = resp.bytes().await?;
        if !status.is_success() {
            let code = serde_json::from_slice::<serde_json::Value>(&bytes)
                .ok()
                .and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from))
                .unwrap_or_default();
            return Err(Error::Api {
                status: status.as_u16(),
                code,
                body: None,
            });
        }
        serde_json::from_slice::<T>(&bytes).map_err(|e| Error::Api {
            status: status.as_u16(),
            code: format!("decode response: {e}"),
            body: None,
        })
    }
}

fn header_value(s: &str) -> Result<HeaderValue, Error> {
    HeaderValue::from_str(s).map_err(|_| Error::Api {
        status: 0,
        code: "invalid header value".into(),
        body: None,
    })
}

fn parse_retry_after(headers: &HeaderMap) -> Option<u64> {
    headers
        .get(reqwest::header::RETRY_AFTER)?
        .to_str()
        .ok()?
        .trim()
        .parse()
        .ok()
}

fn urlencode(value: &str) -> String {
    value
        .bytes()
        .map(|b| match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                (b as char).to_string()
            }
            _ => format!("%{b:02X}"),
        })
        .collect()
}

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

    #[test]
    fn retry_after_parses_seconds() {
        let mut headers = HeaderMap::new();
        headers.insert(reqwest::header::RETRY_AFTER, HeaderValue::from_static("30"));
        assert_eq!(parse_retry_after(&headers), Some(30));
    }

    #[test]
    fn retry_after_absent_is_none() {
        assert_eq!(parse_retry_after(&HeaderMap::new()), None);
    }

    #[test]
    fn retry_after_http_date_is_none() {
        let mut headers = HeaderMap::new();
        headers.insert(
            reqwest::header::RETRY_AFTER,
            HeaderValue::from_static("Wed, 21 Oct 2026 07:28:00 GMT"),
        );
        assert_eq!(parse_retry_after(&headers), None);
    }
}
