//! Retry logic with exponential backoff.

// WASM is single-threaded, so futures don't need to be Send
#![cfg_attr(target_arch = "wasm32", allow(clippy::future_not_send))]

use std::future::Future;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;

use crate::config::RetryPolicy;
use crate::error::HttpError;

/// Execute an async operation with retry logic.
///
/// Uses exponential backoff with the configured policy. Respects rate limiting
/// by parsing the `Retry-After` header.
///
/// # Type Parameters
///
/// * `F` - Factory function that creates the future to retry
/// * `Fut` - The future type
/// * `T` - Success return type
///
/// # Arguments
///
/// * `policy` - Retry policy configuration
/// * `operation` - Factory that creates the operation to retry
///
/// # Errors
///
/// Returns the last error after all retry attempts are exhausted.
#[cfg(not(target_arch = "wasm32"))]
pub async fn execute_with_retry<F, Fut, T>(
    policy: &RetryPolicy,
    mut operation: F,
) -> Result<T, HttpError>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<T, HttpError>>,
{
    let mut last_error = None;

    for attempt in 0..policy.max_attempts {
        match operation().await {
            Ok(result) => return Ok(result),
            Err(e) => {
                // Check if error is retryable
                if !e.is_retryable() {
                    return Err(e);
                }

                // Handle rate limiting
                if let Some(retry_after) = e.retry_after() {
                    tokio::time::sleep(Duration::from_secs(retry_after)).await;
                    last_error = Some(e);
                    continue;
                }

                last_error = Some(e);

                // Don't sleep after the last attempt
                if attempt < policy.max_attempts - 1 {
                    let delay = policy.delay_for_attempt(attempt);
                    tokio::time::sleep(Duration::from_millis(delay)).await;
                }
            }
        }
    }

    Err(last_error.unwrap_or_else(|| HttpError::network("Retry exhausted")))
}

/// WASM version with setTimeout-based delays
#[cfg(target_arch = "wasm32")]
pub async fn execute_with_retry<F, Fut, T>(
    policy: &RetryPolicy,
    mut operation: F,
) -> Result<T, HttpError>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<T, HttpError>>,
{
    let mut last_error = None;

    for attempt in 0..policy.max_attempts {
        match operation().await {
            Ok(result) => return Ok(result),
            Err(e) => {
                // Check if error is retryable
                if !e.is_retryable() {
                    return Err(e);
                }

                // Handle rate limiting - use Retry-After header
                if let Some(retry_after) = e.retry_after() {
                    wasm_sleep_ms(retry_after * 1000).await;
                    last_error = Some(e);
                    continue;
                }

                last_error = Some(e);

                // Don't sleep after the last attempt
                if attempt < policy.max_attempts - 1 {
                    let delay = policy.delay_for_attempt(attempt);
                    wasm_sleep_ms(delay).await;
                }
            }
        }
    }

    Err(last_error.unwrap_or_else(|| HttpError::network("Retry exhausted")))
}

/// Sleep for the specified milliseconds using browser setTimeout (WASM only).
#[cfg(target_arch = "wasm32")]
async fn wasm_sleep_ms(ms: u64) {
    wasm_bindgen_futures::JsFuture::from(js_sys::Promise::new(&mut |resolve, _| {
        let window = web_sys::window().expect("no global window exists");
        #[allow(clippy::cast_possible_truncation)]
        window
            .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms as i32)
            .expect("setTimeout failed");
    }))
    .await
    .expect("setTimeout promise failed");
}

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use std::sync::Arc;
    use std::sync::atomic::{AtomicU32, Ordering};

    use super::*;

    #[tokio::test]
    async fn test_retry_success_first_attempt() {
        let policy = RetryPolicy::default();
        let result = execute_with_retry(&policy, || async { Ok::<_, HttpError>(42) }).await;
        assert_eq!(result.unwrap(), 42);
    }

    #[tokio::test]
    async fn test_retry_success_after_failure() {
        let policy = RetryPolicy::default();
        let attempts = Arc::new(AtomicU32::new(0));
        let attempts_clone = Arc::clone(&attempts);

        let result = execute_with_retry(&policy, || {
            let attempts = Arc::clone(&attempts_clone);
            async move {
                let attempt = attempts.fetch_add(1, Ordering::SeqCst);
                if attempt < 2 {
                    Err(HttpError::Timeout)
                } else {
                    Ok(42)
                }
            }
        })
        .await;

        assert_eq!(result.unwrap(), 42);
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }

    #[tokio::test]
    async fn test_retry_non_retryable_error() {
        let policy = RetryPolicy::default();
        let attempts = Arc::new(AtomicU32::new(0));
        let attempts_clone = Arc::clone(&attempts);

        let result = execute_with_retry(&policy, || {
            let attempts = Arc::clone(&attempts_clone);
            async move {
                attempts.fetch_add(1, Ordering::SeqCst);
                Err::<i32, _>(HttpError::status(400, "Bad request"))
            }
        })
        .await;

        assert!(result.is_err());
        // Should not retry 400 errors
        assert_eq!(attempts.load(Ordering::SeqCst), 1);
    }

    #[tokio::test]
    async fn test_retry_exhausted() {
        let policy = RetryPolicy {
            max_attempts: 3,
            ..Default::default()
        };
        let attempts = Arc::new(AtomicU32::new(0));
        let attempts_clone = Arc::clone(&attempts);

        let result = execute_with_retry(&policy, || {
            let attempts = Arc::clone(&attempts_clone);
            async move {
                attempts.fetch_add(1, Ordering::SeqCst);
                Err::<i32, _>(HttpError::Timeout)
            }
        })
        .await;

        assert!(result.is_err());
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }
}
