/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 */

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

use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use serde_json::Value;
use ureq::Agent;
use ureq::config::{Config, RedirectAuthHeaders};
use ureq::tls::{RootCerts, TlsConfig};

use crate::jmap::error::JmapError;
use crate::jmap::inflight::{Permit, Semaphore};
use crate::jmap::retry::{self, Disposition, RateLimitState};
use crate::jmap::session::Limits;
use crate::logging::{LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};

const MAX_BODY: u64 = 512 * 1024 * 1024;

const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);

#[derive(Debug, Clone)]
pub enum Auth {
    Basic { user: String, password: String },
    Bearer { token: String },
}

impl Auth {
    pub fn header_value(&self) -> String {
        match self {
            Auth::Basic { user, password } => {
                format!("Basic {}", STANDARD.encode(format!("{user}:{password}")))
            }
            Auth::Bearer { token } => format!("Bearer {token}"),
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct RetryPolicy {
    pub max_retries: u32,
    pub base: Duration,
    pub cap: Duration,
}

impl RetryPolicy {
    pub fn new(max_retries: u32) -> Self {
        RetryPolicy {
            max_retries,
            base: Duration::from_millis(500),
            cap: Duration::from_secs(30),
        }
    }
}

struct Inner {
    agent: Agent,
    auth: Auth,
    retry: RetryPolicy,
    allow_invalid_certs: bool,
    rate_limit: RateLimitState,
    log_level: AtomicU8,
    requests_gate: OnceLock<Semaphore>,
    uploads_gate: OnceLock<Semaphore>,
    max_upload_bytes: AtomicU64,
    retries_total: AtomicU64,
    retry_after_sleeps: AtomicU64,
}

#[derive(Debug, Clone, Copy)]
enum Kind {
    Api,
    Upload,
}

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

enum Attempt {
    Ok {
        status: u16,
        body: Vec<u8>,
        retry_after: Option<Duration>,
        rate_limit_headers: Vec<(String, String)>,
    },
    Transport(JmapError),
}

impl HttpClient {
    pub fn new(auth: Auth, retry: RetryPolicy, allow_invalid_certs: bool) -> Self {
        let config: Config = Config::builder()
            .http_status_as_error(false)
            .redirect_auth_headers(RedirectAuthHeaders::SameHost)
            .tls_config(
                TlsConfig::builder()
                    .root_certs(RootCerts::PlatformVerifier)
                    .disable_verification(allow_invalid_certs)
                    .build(),
            )
            .build();
        HttpClient {
            inner: Arc::new(Inner {
                agent: config.new_agent(),
                auth,
                retry,
                allow_invalid_certs,
                rate_limit: RateLimitState::new(),
                log_level: AtomicU8::new(LEVEL_DEFAULT),
                requests_gate: OnceLock::new(),
                uploads_gate: OnceLock::new(),
                max_upload_bytes: AtomicU64::new(0),
                retries_total: AtomicU64::new(0),
                retry_after_sleeps: AtomicU64::new(0),
            }),
        }
    }

    pub fn set_limits(&self, limits: &Limits) {
        let _ = self
            .inner
            .requests_gate
            .set(Semaphore::new(limits.max_concurrent_requests));
        let _ = self
            .inner
            .uploads_gate
            .set(Semaphore::new(limits.max_concurrent_upload));
        self.inner
            .max_upload_bytes
            .store(limits.max_size_upload, Ordering::Relaxed);
    }

    pub fn retries_observed(&self) -> u64 {
        self.inner.retries_total.load(Ordering::Relaxed)
    }

    pub fn retry_after_sleeps(&self) -> u64 {
        self.inner.retry_after_sleeps.load(Ordering::Relaxed)
    }

    fn acquire_requests(&self) -> Option<Permit<'_>> {
        self.inner.requests_gate.get().map(Semaphore::acquire)
    }

    fn acquire_uploads(&self) -> Option<Permit<'_>> {
        self.inner.uploads_gate.get().map(Semaphore::acquire)
    }

    pub fn auth(&self) -> &Auth {
        &self.inner.auth
    }

    pub fn retry(&self) -> &RetryPolicy {
        &self.inner.retry
    }

    pub fn allow_invalid_certs(&self) -> bool {
        self.inner.allow_invalid_certs
    }

    pub fn rate_limit(&self) -> &RateLimitState {
        &self.inner.rate_limit
    }

    pub fn throttle_level(&self) -> u32 {
        self.inner.rate_limit.level()
    }

    pub fn set_logger(&self, logger: Logger) {
        self.inner
            .log_level
            .store(logger.level(), Ordering::Relaxed);
    }

    fn logger(&self) -> Logger {
        Logger::new(self.inner.log_level.load(Ordering::Relaxed))
    }

    pub fn get(&self, url: &str) -> Result<String, JmapError> {
        let body = self.execute(Kind::Api, "GET", url, None, None)?;
        String::from_utf8(body).map_err(|e| JmapError::Malformed(format!("non-utf8 body: {e}")))
    }

    pub fn post_json(&self, url: &str, body: &Value) -> Result<Value, JmapError> {
        let payload = serde_json::to_vec(body)?;
        let raw = self.execute(
            Kind::Api,
            "POST",
            url,
            Some(&payload),
            Some("application/json"),
        )?;
        serde_json::from_slice(&raw)
            .map_err(|e| JmapError::Malformed(format!("response is not valid json: {e}")))
    }

    pub fn upload(
        &self,
        upload_url: &str,
        content_type: &str,
        bytes: &[u8],
    ) -> Result<Value, JmapError> {
        let cap = self.inner.max_upload_bytes.load(Ordering::Relaxed);
        if cap > 0 && (bytes.len() as u64) > cap {
            return Err(JmapError::SingleObjectTooLarge(format!(
                "blob upload of {} bytes exceeds maxSizeUpload ({cap})",
                bytes.len()
            )));
        }
        let raw = self.execute(
            Kind::Upload,
            "POST",
            upload_url,
            Some(bytes),
            Some(content_type),
        )?;
        serde_json::from_slice(&raw)
            .map_err(|e| JmapError::Malformed(format!("upload response is not valid json: {e}")))
    }

    pub fn download(&self, url: &str) -> Result<Vec<u8>, JmapError> {
        self.execute(Kind::Api, "GET", url, None, None)
    }

    fn execute(
        &self,
        kind: Kind,
        method: &str,
        url: &str,
        body: Option<&[u8]>,
        content_type: Option<&str>,
    ) -> Result<Vec<u8>, JmapError> {
        let policy = self.inner.retry;
        let logger = self.logger();
        let mut attempt: u32 = 0;
        loop {
            self.inner.rate_limit.cooldown().wait();
            let attempt_outcome = {
                let _r = self.acquire_requests();
                let _u = if matches!(kind, Kind::Upload) {
                    self.acquire_uploads()
                } else {
                    None
                };
                self.one_attempt(url, body, content_type)
            };
            match attempt_outcome {
                Attempt::Ok { status, body, .. } if (200..300).contains(&status) => {
                    self.inner.rate_limit.on_success();
                    return Ok(body);
                }
                Attempt::Ok {
                    status,
                    body,
                    retry_after,
                    rate_limit_headers,
                } => {
                    let disposition = self.status_disposition(status, &body);
                    match disposition {
                        StatusOutcome::Auth => {
                            return Err(JmapError::Auth(format!(
                                "server returned {status}: {}",
                                truncate(&body)
                            )));
                        }
                        StatusOutcome::RequestTooLarge => {
                            return Err(JmapError::RequestTooLarge);
                        }
                        StatusOutcome::Fatal => {
                            return Err(JmapError::HttpStatus {
                                status,
                                body: truncate(&body),
                            });
                        }
                        StatusOutcome::Retryable => {
                            attempt += 1;
                            self.inner.retries_total.fetch_add(1, Ordering::Relaxed);
                            let detail = problem_detail(&body);
                            let headers_blurb = format_rate_headers(&rate_limit_headers);
                            let reason = match (&detail, headers_blurb.as_deref()) {
                                (Some(d), Some(h)) => format!("http {status}: {d} [{h}]"),
                                (Some(d), None) => format!("http {status}: {d}"),
                                (None, Some(h)) => format!("http {status} [{h}]"),
                                (None, None) => format!("http {status}"),
                            };
                            if attempt > policy.max_retries {
                                return Err(JmapError::RetriesExhausted(format!(
                                    "{method} {url} kept returning {reason}"
                                )));
                            }
                            if retry_after.is_some() {
                                self.inner
                                    .retry_after_sleeps
                                    .fetch_add(1, Ordering::Relaxed);
                            }
                            let delay = self.inner.rate_limit.on_throttle(&policy, retry_after);
                            if delay >= LONG_RETRY_THRESHOLD {
                                logger.warn(&format!(
                                    "server rate-limited ({reason}); waiting {} before retry {}/{} (shared level {})",
                                    format_retry_wait(delay),
                                    attempt,
                                    policy.max_retries,
                                    self.inner.rate_limit.level(),
                                ));
                            }
                            self.log_retry(
                                &logger,
                                RetryLog {
                                    method,
                                    url,
                                    attempt,
                                    delay,
                                    reason: &reason,
                                    body: &body,
                                },
                            );
                            std::thread::sleep(delay);
                        }
                    }
                }
                Attempt::Transport(err) => match transport_disposition(&err) {
                    Disposition::Fatal => return Err(err),
                    Disposition::Retryable => {
                        attempt += 1;
                        self.inner.retries_total.fetch_add(1, Ordering::Relaxed);
                        if attempt > policy.max_retries {
                            return Err(JmapError::RetriesExhausted(format!(
                                "{method} {url}: {err}"
                            )));
                        }
                        let delay = retry::backoff_delay(&policy, attempt);
                        if delay >= LONG_RETRY_THRESHOLD {
                            logger.warn(&format!(
                                "transient transport failure ({err}); waiting {} before retry {}/{}",
                                format_retry_wait(delay),
                                attempt,
                                policy.max_retries,
                            ));
                        }
                        self.log_retry(
                            &logger,
                            RetryLog {
                                method,
                                url,
                                attempt,
                                delay,
                                reason: &err.to_string(),
                                body: &[],
                            },
                        );
                        std::thread::sleep(delay);
                    }
                },
            }
        }
    }

    fn one_attempt(&self, url: &str, body: Option<&[u8]>, content_type: Option<&str>) -> Attempt {
        let auth = self.inner.auth.header_value();
        let result = if let Some(payload) = body {
            let mut req = self
                .inner
                .agent
                .post(url)
                .header("Authorization", auth)
                .header("Accept", "application/json");
            if let Some(ct) = content_type {
                req = req.header("Content-Type", ct);
            }
            req.send(payload)
        } else {
            self.inner
                .agent
                .get(url)
                .header("Authorization", auth)
                .header("Accept", "application/json")
                .call()
        };
        match result {
            Ok(mut resp) => {
                let status = resp.status().as_u16();
                let retry_after = resp
                    .headers()
                    .get("retry-after")
                    .and_then(|v| v.to_str().ok())
                    .and_then(retry_after_header);
                let rate_limit_headers: Vec<(String, String)> = resp
                    .headers()
                    .iter()
                    .filter_map(|(name, value)| {
                        let lower = name.as_str().to_ascii_lowercase();
                        if matches!(
                            lower.as_str(),
                            "retry-after" | "ratelimit" | "ratelimit-policy" | "content-type"
                        ) {
                            value.to_str().ok().map(|v| (lower, v.to_owned()))
                        } else {
                            None
                        }
                    })
                    .collect();
                match resp.body_mut().with_config().limit(MAX_BODY).read_to_vec() {
                    Ok(bytes) => Attempt::Ok {
                        status,
                        body: bytes,
                        retry_after,
                        rate_limit_headers,
                    },
                    Err(e) => Attempt::Transport(JmapError::Transport(format!(
                        "reading response body: {e}"
                    ))),
                }
            }
            Err(e) => Attempt::Transport(map_ureq_error(e)),
        }
    }

    fn status_disposition(&self, status: u16, body: &[u8]) -> StatusOutcome {
        if status == 401 || status == 403 {
            return StatusOutcome::Auth;
        }
        if status == 413 {
            return StatusOutcome::RequestTooLarge;
        }
        if let Ok(problem) = serde_json::from_slice::<Value>(body)
            && let Some(kind) = problem.get("type").and_then(Value::as_str)
        {
            if kind.ends_with(":requestTooLarge") {
                return StatusOutcome::RequestTooLarge;
            }
            if kind.ends_with(":limit") {
                let which = problem.get("limit").and_then(Value::as_str).unwrap_or("");
                let subtype = problem.get("subType").and_then(Value::as_str).unwrap_or("");
                match which {
                    "maxSizeRequest" | "maxSizeUpload" => {
                        return StatusOutcome::RequestTooLarge;
                    }
                    "maxConcurrentRequests" | "maxConcurrentUpload" => {
                        return StatusOutcome::Retryable;
                    }
                    _ if subtype == "rateLimit" => {
                        return StatusOutcome::Retryable;
                    }
                    _ => {}
                }
            }
        }
        match retry::classify_http_status(status) {
            Disposition::Retryable => StatusOutcome::Retryable,
            Disposition::Fatal => StatusOutcome::Fatal,
        }
    }

    fn log_retry(&self, logger: &Logger, r: RetryLog<'_>) {
        if logger.enabled(LEVEL_BODIES) {
            let snippet = if r.body.is_empty() {
                String::new()
            } else {
                format!(" body={}", truncate(r.body))
            };
            eprintln!(
                "retry {}/{} {} {} after {:?} ({}){snippet}",
                r.attempt, self.inner.retry.max_retries, r.method, r.url, r.delay, r.reason,
            );
        } else if logger.enabled(LEVEL_PROGRESS) {
            eprintln!(
                "retry {}/{} ({})",
                r.attempt, self.inner.retry.max_retries, r.reason
            );
        }
    }
}

enum StatusOutcome {
    Retryable,
    Fatal,
    Auth,
    RequestTooLarge,
}

struct RetryLog<'a> {
    method: &'a str,
    url: &'a str,
    attempt: u32,
    delay: Duration,
    reason: &'a str,
    body: &'a [u8],
}

fn transport_disposition(err: &JmapError) -> Disposition {
    match err {
        JmapError::Connect(_) => Disposition::Fatal,
        _ => Disposition::Retryable,
    }
}

fn map_ureq_error(err: ureq::Error) -> JmapError {
    match err {
        ureq::Error::Io(e) => JmapError::Transport(format!("io: {e}")),
        ureq::Error::Timeout(t) => JmapError::Transport(format!("timeout: {t}")),
        ureq::Error::HostNotFound => JmapError::Transport("host not found".to_owned()),
        ureq::Error::ConnectionFailed => JmapError::Transport("connection failed".to_owned()),
        ureq::Error::BodyStalled => JmapError::Transport("body stalled".to_owned()),
        ureq::Error::Tls(m) => JmapError::Transport(format!("tls: {m}")),
        ureq::Error::TooManyRedirects => JmapError::Connect("too many redirects".to_owned()),
        ureq::Error::RedirectFailed => JmapError::Connect("redirect failed".to_owned()),
        ureq::Error::TlsRequired => {
            JmapError::Connect("server requires TLS but transport is unsecured".to_owned())
        }
        other => JmapError::Connect(other.to_string()),
    }
}

fn truncate(body: &[u8]) -> String {
    let text = String::from_utf8_lossy(body);
    if text.len() <= 512 {
        return text.into_owned();
    }
    let end = text.floor_char_boundary(512);
    format!("{}...", &text[..end])
}

pub fn retry_after_header(value: &str) -> Option<Duration> {
    retry::parse_retry_after(value, SystemTime::now())
}

fn problem_detail(body: &[u8]) -> Option<String> {
    let value: Value = serde_json::from_slice(body).ok()?;
    let title = value.get("title").and_then(Value::as_str);
    let detail = value.get("detail").and_then(Value::as_str);
    match (title, detail) {
        (Some(t), Some(d)) => Some(format!("{t}: {d}")),
        (Some(t), None) => Some(t.to_owned()),
        (None, Some(d)) => Some(d.to_owned()),
        (None, None) => None,
    }
}

fn format_rate_headers(headers: &[(String, String)]) -> Option<String> {
    if headers.is_empty() {
        return None;
    }
    let mut parts: Vec<String> = headers
        .iter()
        .filter(|(name, _)| name != "content-type")
        .map(|(name, value)| format!("{name}={value}"))
        .collect();
    if parts.is_empty() {
        return None;
    }
    parts.sort();
    Some(parts.join("; "))
}

fn format_retry_wait(d: Duration) -> String {
    let total = d.as_secs();
    if total < 60 {
        format!("{total}s")
    } else if total < 3600 {
        format!("{}m{:02}s", total / 60, total % 60)
    } else {
        format!(
            "{}h{:02}m{:02}s",
            total / 3600,
            (total % 3600) / 60,
            total % 60
        )
    }
}

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

    #[test]
    fn basic_header_matches_rfc7617_example() {
        let auth = Auth::Basic {
            user: "Aladdin".to_owned(),
            password: "open sesame".to_owned(),
        };
        assert_eq!(auth.header_value(), "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
    }

    #[test]
    fn bearer_header() {
        let auth = Auth::Bearer {
            token: "abc.def".to_owned(),
        };
        assert_eq!(auth.header_value(), "Bearer abc.def");
    }

    #[test]
    fn truncate_does_not_panic_on_multibyte_boundary() {
        let mut body = vec![b'a'; 510];
        body.extend_from_slice("\u{1F4A9}".as_bytes());
        body.extend_from_slice(&[b'b'; 600]);
        let s = truncate(&body);
        assert!(s.ends_with("..."));
        assert!(s.is_char_boundary(s.len()));
        let inner = s.trim_end_matches('.');
        assert!(inner.is_char_boundary(inner.len()));
    }

    #[test]
    fn truncate_short_body_returns_verbatim() {
        let s = truncate("héllo".as_bytes());
        assert_eq!(s, "héllo");
    }

    #[test]
    fn limit_problem_unknown_kind_on_429_is_retried() {
        let client = HttpClient::new(
            Auth::Bearer {
                token: "t".to_owned(),
            },
            RetryPolicy::new(0),
            false,
        );
        let body = br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"someServerLimit"}"#;
        assert!(matches!(
            client.status_disposition(429, body),
            StatusOutcome::Retryable
        ));
    }

    #[test]
    fn limit_problem_max_size_request_is_resplit() {
        let client = HttpClient::new(
            Auth::Bearer {
                token: "t".to_owned(),
            },
            RetryPolicy::new(0),
            false,
        );
        let body = br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"maxSizeRequest"}"#;
        assert!(matches!(
            client.status_disposition(400, body),
            StatusOutcome::RequestTooLarge
        ));
    }

    #[test]
    fn limit_problem_max_concurrent_is_retried() {
        let client = HttpClient::new(
            Auth::Bearer {
                token: "t".to_owned(),
            },
            RetryPolicy::new(0),
            false,
        );
        let body =
            br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"maxConcurrentRequests"}"#;
        assert!(matches!(
            client.status_disposition(429, body),
            StatusOutcome::Retryable
        ));
    }

    fn limits_with(max_upload: u64, req: u64, up: u64) -> Limits {
        Limits {
            max_objects_in_get: 500,
            max_objects_in_set: 500,
            max_calls_in_request: 16,
            max_concurrent_requests: req,
            max_concurrent_upload: up,
            max_size_request: 10_000_000,
            max_size_upload: max_upload,
        }
    }

    #[test]
    fn upload_rejects_oversize_before_sending() {
        let client = HttpClient::new(
            Auth::Bearer {
                token: "t".to_owned(),
            },
            RetryPolicy::new(0),
            false,
        );
        client.set_limits(&limits_with(10, 4, 4));
        let err = client
            .upload(
                "http://127.0.0.1:1/upload",
                "application/octet-stream",
                &[0u8; 11],
            )
            .unwrap_err();
        assert!(
            matches!(err, JmapError::SingleObjectTooLarge(_)),
            "got {err:?}"
        );
        assert_eq!(client.retries_observed(), 0);
    }

    #[test]
    fn upload_within_max_size_is_not_short_circuited() {
        let client = HttpClient::new(
            Auth::Bearer {
                token: "t".to_owned(),
            },
            RetryPolicy::new(0),
            false,
        );
        client.set_limits(&limits_with(1024, 4, 4));
        let err = client
            .upload(
                "http://127.0.0.1:1/upload",
                "application/octet-stream",
                b"abc",
            )
            .unwrap_err();
        assert!(
            !matches!(err, JmapError::SingleObjectTooLarge(_)),
            "expected the call to proceed to transport (got SingleObjectTooLarge): {err:?}"
        );
    }

    #[test]
    fn problem_detail_extracts_title_and_detail() {
        let body = br#"{"type":"about:blank","status":429,"title":"Quota exceeded","detail":"You have exceeded the blob upload quota of 1000 files or 50000000 bytes."}"#;
        let got = problem_detail(body).expect("title+detail");
        assert!(got.contains("Quota exceeded"));
        assert!(got.contains("50000000 bytes"));
    }

    #[test]
    fn problem_detail_title_only() {
        let body = br#"{"title":"Too Many Requests"}"#;
        assert_eq!(problem_detail(body).as_deref(), Some("Too Many Requests"));
    }

    #[test]
    fn problem_detail_returns_none_for_empty_body() {
        assert_eq!(problem_detail(b"{}"), None);
        assert_eq!(problem_detail(b"not json"), None);
    }

    #[test]
    fn format_retry_wait_short_medium_long() {
        assert_eq!(format_retry_wait(Duration::from_secs(7)), "7s");
        assert_eq!(format_retry_wait(Duration::from_secs(67)), "1m07s");
        assert_eq!(format_retry_wait(Duration::from_secs(1427)), "23m47s");
        assert_eq!(format_retry_wait(Duration::from_secs(3661)), "1h01m01s");
    }

    #[test]
    fn counters_start_at_zero() {
        let client = HttpClient::new(
            Auth::Bearer {
                token: "t".to_owned(),
            },
            RetryPolicy::new(0),
            false,
        );
        assert_eq!(client.retries_observed(), 0);
        assert_eq!(client.retry_after_sleeps(), 0);
    }
}
