//! Retry logic, backoff strategies, and status code evaluation.

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

use reqwest::{
    StatusCode,
    header::{HeaderMap, HeaderValue, RETRY_AFTER},
};
use guise_pacing as pacing;

/// Maximum retry delay in milliseconds.
pub const MAX_RETRY_DELAY_MS: u64 = 60_000;

/// Determines if the given HTTP status code should trigger a retry attempt.
pub fn should_retry_status(status: StatusCode) -> bool {
    matches!(
        status,
        StatusCode::TOO_MANY_REQUESTS
            | StatusCode::REQUEST_TIMEOUT
            | StatusCode::INTERNAL_SERVER_ERROR
            | StatusCode::BAD_GATEWAY
            | StatusCode::SERVICE_UNAVAILABLE
            | StatusCode::GATEWAY_TIMEOUT
    )
}

/// Determines if the given reqwest error should trigger a retry attempt.
pub fn should_retry_error(error: &reqwest::Error) -> bool {
    error.is_timeout() || error.is_connect() || error.is_request() || error.is_body()
}

/// Calculates the retry delay based on status, attempts, and headers.
pub fn retry_delay_for_status(
    base_ms: u64,
    attempt: u32,
    status: StatusCode,
    headers: &HeaderMap,
) -> Duration {
    if matches!(
        status,
        StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE
    ) {
        if let Some(delay) = retry_after_delay(headers.get(RETRY_AFTER)) {
            return delay;
        }
    }
    backoff_delay(base_ms, attempt)
}

/// Calculates an exponential backoff delay.
pub fn backoff_delay(base_ms: u64, attempt: u32) -> Duration {
    pacing::capped_pow2_backoff_ms(base_ms, attempt, MAX_RETRY_DELAY_MS)
}

fn retry_after_delay(value: Option<&HeaderValue>) -> Option<Duration> {
    pacing::parse_retry_after(value?.to_str().ok()?, SystemTime::now())
}

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

    fn headers_with_retry_after(value: &'static str) -> HeaderMap {
        let mut headers = HeaderMap::new();
        headers.insert(RETRY_AFTER, HeaderValue::from_static(value));
        headers
    }

    #[test]
    fn retry_after_delta_seconds_are_capped() {
        let delay = retry_delay_for_status(
            10,
            0,
            StatusCode::TOO_MANY_REQUESTS,
            &headers_with_retry_after("3600"),
        );

        assert_eq!(delay, Duration::from_millis(MAX_RETRY_DELAY_MS));
    }

    #[test]
    fn retry_after_http_date_is_honored() {
        let delay = retry_delay_for_status(
            10,
            0,
            StatusCode::SERVICE_UNAVAILABLE,
            &headers_with_retry_after("Thu, 31 Dec 2099 23:59:59 GMT"),
        );

        assert_eq!(delay, Duration::from_millis(MAX_RETRY_DELAY_MS));
    }

    #[test]
    fn malformed_retry_after_uses_exponential_backoff() {
        let delay = retry_delay_for_status(
            125,
            2,
            StatusCode::TOO_MANY_REQUESTS,
            &headers_with_retry_after("soon"),
        );

        assert_eq!(delay, Duration::from_millis(500));
    }
}
