//! Retry logic with exponential backoff
//!
//! This module provides utilities for retrying failed operations with exponential backoff.

use crate::{HetznerError, Result};
use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, warn};

/// Retry configuration
#[derive(Debug, Clone)]
pub struct RetryConfig {
    /// Maximum number of retry attempts
    pub max_attempts: u32,
    /// Initial delay before first retry
    pub initial_delay: Duration,
    /// Maximum delay between retries
    pub max_delay: Duration,
    /// Multiplier for exponential backoff
    pub multiplier: f64,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_attempts: 3,
            initial_delay: Duration::from_secs(1),
            max_delay: Duration::from_secs(30),
            multiplier: 2.0,
        }
    }
}

impl RetryConfig {
    /// Create a new retry configuration
    pub fn new() -> Self {
        Self::default()
    }

    /// Set maximum number of attempts
    pub fn with_max_attempts(mut self, attempts: u32) -> Self {
        self.max_attempts = attempts;
        self
    }

    /// Set initial delay
    pub fn with_initial_delay(mut self, delay: Duration) -> Self {
        self.initial_delay = delay;
        self
    }

    /// Set maximum delay
    pub fn with_max_delay(mut self, delay: Duration) -> Self {
        self.max_delay = delay;
        self
    }

    /// Set backoff multiplier
    pub fn with_multiplier(mut self, multiplier: f64) -> Self {
        self.multiplier = multiplier;
        self
    }

    /// Calculate delay for a given attempt number
    fn calculate_delay(&self, attempt: u32) -> Duration {
        let delay_ms =
            (self.initial_delay.as_millis() as f64) * self.multiplier.powi(attempt as i32);
        let delay = Duration::from_millis(delay_ms as u64);
        delay.min(self.max_delay)
    }
}

/// Retry a fallible async operation with exponential backoff
///
/// # Example
///
/// ```no_run
/// use lmrc_hetzner::retry::{retry_with_backoff, RetryConfig};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let result = retry_with_backoff(
///     RetryConfig::default(),
///     || async {
///         // Your fallible operation here
///         Ok::<_, lmrc_hetzner::HetznerError>(())
///     }
/// ).await?;
/// # Ok(())
/// # }
/// ```
pub async fn retry_with_backoff<F, Fut, T>(config: RetryConfig, mut operation: F) -> Result<T>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = Result<T>>,
{
    let mut attempt = 0;

    loop {
        match operation().await {
            Ok(result) => {
                if attempt > 0 {
                    debug!("Operation succeeded after {} attempts", attempt + 1);
                }
                return Ok(result);
            }
            Err(err) => {
                attempt += 1;

                // Check if we should retry this error
                if !is_retryable(&err) {
                    debug!("Error is not retryable: {}", err);
                    return Err(err);
                }

                // Check if we've exhausted retries
                if attempt >= config.max_attempts {
                    warn!("Operation failed after {} attempts: {}", attempt, err);
                    return Err(err);
                }

                // Calculate delay and wait
                let delay = config.calculate_delay(attempt - 1);
                warn!(
                    "Operation failed (attempt {}/{}), retrying in {:?}: {}",
                    attempt, config.max_attempts, delay, err
                );
                sleep(delay).await;
            }
        }
    }
}

/// Determine if an error is retryable
fn is_retryable(error: &HetznerError) -> bool {
    match error {
        // Network errors are retryable
        HetznerError::Http(_) => true,

        // Rate limiting is retryable
        HetznerError::RateLimited { .. } => true,

        // Server errors (5xx) are retryable
        HetznerError::ServerError { .. } => true,

        // Timeouts are retryable
        HetznerError::Timeout { .. } => true,

        // Client errors are generally not retryable
        HetznerError::Unauthorized => false,
        HetznerError::Forbidden { .. } => false,
        HetznerError::NotFound { .. } => false,
        HetznerError::AlreadyExists { .. } => false,
        HetznerError::InvalidParameter { .. } => false,
        HetznerError::Validation(_) => false,
        HetznerError::InvalidToken(_) => false,
        HetznerError::BuilderError(_) => false,

        // API errors depend on the code
        HetznerError::Api { code, .. } => {
            // Retry on specific API error codes that are transient
            matches!(
                code.as_str(),
                "rate_limit_exceeded" | "service_error" | "conflict"
            )
        }

        // Serialization errors are not retryable
        HetznerError::Serialization(_) => false,
    }
}

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

    #[tokio::test]
    async fn test_retry_succeeds_on_first_attempt() {
        let counter = Arc::new(AtomicU32::new(0));
        let counter_clone = counter.clone();

        let config = RetryConfig::new().with_max_attempts(3);

        let result = retry_with_backoff(config, || {
            let counter = counter_clone.clone();
            async move {
                counter.fetch_add(1, Ordering::SeqCst);
                Ok::<_, HetznerError>(42)
            }
        })
        .await;

        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 42);
        assert_eq!(counter.load(Ordering::SeqCst), 1);
    }

    #[tokio::test]
    async fn test_retry_succeeds_after_failures() {
        let counter = Arc::new(AtomicU32::new(0));
        let counter_clone = counter.clone();

        let config = RetryConfig::new()
            .with_max_attempts(3)
            .with_initial_delay(Duration::from_millis(10));

        let result = retry_with_backoff(config, || {
            let counter = counter_clone.clone();
            async move {
                let attempt = counter.fetch_add(1, Ordering::SeqCst);
                if attempt < 2 {
                    // Fail first two attempts
                    Err(HetznerError::ServerError {
                        status_code: 500,
                        details: "Server error".to_string(),
                    })
                } else {
                    Ok(42)
                }
            }
        })
        .await;

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

    #[tokio::test]
    async fn test_retry_fails_after_max_attempts() {
        let counter = Arc::new(AtomicU32::new(0));
        let counter_clone = counter.clone();

        let config = RetryConfig::new()
            .with_max_attempts(3)
            .with_initial_delay(Duration::from_millis(10));

        let result = retry_with_backoff(config, || {
            let counter = counter_clone.clone();
            async move {
                counter.fetch_add(1, Ordering::SeqCst);
                Err::<(), _>(HetznerError::ServerError {
                    status_code: 500,
                    details: "Server error".to_string(),
                })
            }
        })
        .await;

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

    #[tokio::test]
    async fn test_non_retryable_error() {
        let counter = Arc::new(AtomicU32::new(0));
        let counter_clone = counter.clone();

        let config = RetryConfig::new().with_max_attempts(3);

        let result = retry_with_backoff(config, || {
            let counter = counter_clone.clone();
            async move {
                counter.fetch_add(1, Ordering::SeqCst);
                Err::<(), _>(HetznerError::Unauthorized)
            }
        })
        .await;

        assert!(result.is_err());
        // Should only try once since Unauthorized is not retryable
        assert_eq!(counter.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn test_calculate_delay() {
        let config = RetryConfig::new()
            .with_initial_delay(Duration::from_secs(1))
            .with_multiplier(2.0)
            .with_max_delay(Duration::from_secs(10));

        assert_eq!(config.calculate_delay(0), Duration::from_secs(1));
        assert_eq!(config.calculate_delay(1), Duration::from_secs(2));
        assert_eq!(config.calculate_delay(2), Duration::from_secs(4));
        assert_eq!(config.calculate_delay(3), Duration::from_secs(8));
        // Should cap at max_delay
        assert_eq!(config.calculate_delay(4), Duration::from_secs(10));
        assert_eq!(config.calculate_delay(5), Duration::from_secs(10));
    }
}
