//! Retry helper shared by every HTTP-backed provider impl.
//!
//! Anthropic, OpenAI, and Mistral all share the same retry shape:
//! exponential backoff `50/100/200/400ms` on transient errors, honor
//! `retry-after` on 429, give up after `max_retries` attempts. Folding
//! the loop here keeps each provider file under the 400-line module cap
//! and prevents the policy from drifting between provider files.

use std::future::Future;
use std::time::Duration;

use crate::eval::error::ProviderError;
use crate::eval::provider::RetryPolicy;

/// Sleep helper used by the retry loop. Indirected so tests can replace
/// it with a no-op; production wires it to `tokio::time::sleep`.
async fn sleep(duration: Duration) {
    if duration.is_zero() {
        return;
    }
    tokio::time::sleep(duration).await;
}

/// Drive a fallible async operation through a retry policy.
///
/// The closure must return a fresh `Future` per attempt so the request
/// body can be rebuilt without cloning bytes back into a buffer. The
/// helper retries only when [`ProviderError::is_retryable`] is true and
/// stops when:
///
/// - the call succeeds,
/// - the error is non-retryable,
/// - `policy.max_retries` retries have been consumed, or
/// - the cumulative delay across retries exceeds `policy.total_budget`.
pub async fn run_with_retry<F, Fut, T>(
    policy: &RetryPolicy,
    mut attempt: F,
) -> Result<T, ProviderError>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<T, ProviderError>>,
{
    let mut budget = policy.total_budget;
    let mut last_error: Option<ProviderError> = None;
    for n in 0..=policy.max_retries {
        match attempt().await {
            Ok(value) => return Ok(value),
            Err(err) => {
                if !err.is_retryable() || n == policy.max_retries {
                    return Err(err);
                }
                let wait = wait_for(&err, policy, n);
                if budget < wait {
                    return Err(err);
                }
                budget -= wait;
                last_error = Some(err);
                sleep(wait).await;
            }
        }
    }
    // Loop guarantees an early return inside the body; fall through is a
    // belt-and-braces case for the type checker.
    Err(last_error
        .unwrap_or_else(|| ProviderError::ProviderInternal("retry loop exhausted".into())))
}

fn wait_for(err: &ProviderError, policy: &RetryPolicy, attempt: u32) -> Duration {
    if let ProviderError::RateLimit {
        retry_after: Some(d),
    } = err
    {
        return *d;
    }
    policy.delay_for(attempt)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicU32, Ordering};
    use std::sync::Arc;

    fn fast_policy() -> RetryPolicy {
        RetryPolicy {
            max_retries: 4,
            base_delay: Duration::from_millis(1),
            total_budget: Duration::from_secs(10),
        }
    }

    #[tokio::test]
    async fn returns_immediately_on_success() {
        let calls = Arc::new(AtomicU32::new(0));
        let policy = fast_policy();
        let result: Result<u32, _> = run_with_retry(&policy, || {
            let calls = Arc::clone(&calls);
            async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, ProviderError>(7)
            }
        })
        .await;
        assert_eq!(result.unwrap(), 7);
        assert_eq!(calls.load(Ordering::SeqCst), 1);
    }

    #[tokio::test]
    async fn does_not_retry_non_retryable() {
        let calls = Arc::new(AtomicU32::new(0));
        let policy = fast_policy();
        let result: Result<(), _> = run_with_retry(&policy, || {
            let calls = Arc::clone(&calls);
            async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Err(ProviderError::AuthFailure("nope".into()))
            }
        })
        .await;
        assert!(result.is_err());
        assert_eq!(calls.load(Ordering::SeqCst), 1);
    }

    #[tokio::test]
    async fn retries_until_success_on_rate_limit() {
        let calls = Arc::new(AtomicU32::new(0));
        let policy = fast_policy();
        let result: Result<u32, _> = run_with_retry(&policy, || {
            let calls = Arc::clone(&calls);
            async move {
                let n = calls.fetch_add(1, Ordering::SeqCst);
                if n < 2 {
                    Err(ProviderError::RateLimit { retry_after: None })
                } else {
                    Ok(42)
                }
            }
        })
        .await;
        assert_eq!(result.unwrap(), 42);
        assert_eq!(calls.load(Ordering::SeqCst), 3);
    }

    #[tokio::test]
    async fn gives_up_after_max_retries() {
        let calls = Arc::new(AtomicU32::new(0));
        let policy = RetryPolicy {
            max_retries: 2,
            base_delay: Duration::from_millis(1),
            total_budget: Duration::from_secs(10),
        };
        let result: Result<(), _> = run_with_retry(&policy, || {
            let calls = Arc::clone(&calls);
            async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Err(ProviderError::ProviderInternal("500".into()))
            }
        })
        .await;
        assert!(result.is_err());
        // initial + 2 retries = 3 attempts.
        assert_eq!(calls.load(Ordering::SeqCst), 3);
    }
}
