//! Retry policy configuration.

#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::error::LastIDError;

/// Retry configuration for transient HTTP failures.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[serde(default)]
pub struct RetryPolicy {
    /// Maximum retry attempts
    #[serde(default = "default_max_attempts")]
    pub max_attempts: u32,

    /// Initial retry delay (milliseconds)
    #[serde(default = "default_initial_delay")]
    pub initial_delay_ms: u64,

    /// Maximum retry delay (milliseconds)
    #[serde(default = "default_max_delay")]
    pub max_delay_ms: u64,

    /// Exponential backoff base
    #[serde(default = "default_backoff_base")]
    pub backoff_base: f64,

    /// Jitter factor (0.0-1.0) to randomize delays and prevent thundering herd
    #[serde(default = "default_jitter_factor")]
    pub jitter_factor: f64,
}

const fn default_max_attempts() -> u32 {
    3
}
const fn default_initial_delay() -> u64 {
    1000
}
const fn default_max_delay() -> u64 {
    30000
}
const fn default_backoff_base() -> f64 {
    2.0
}
const fn default_jitter_factor() -> f64 {
    0.1 // 10% jitter by default
}

impl Default for RetryPolicy {
    fn default() -> Self {
        Self {
            max_attempts: default_max_attempts(),
            initial_delay_ms: default_initial_delay(),
            max_delay_ms: default_max_delay(),
            backoff_base: default_backoff_base(),
            jitter_factor: default_jitter_factor(),
        }
    }
}

impl RetryPolicy {
    /// Validate the retry policy.
    pub fn validate(&self) -> Result<(), LastIDError> {
        if self.max_attempts == 0 {
            return Err(LastIDError::config("max_attempts must be > 0"));
        }

        if self.initial_delay_ms == 0 {
            return Err(LastIDError::config("initial_delay_ms must be > 0"));
        }

        if self.max_delay_ms < self.initial_delay_ms {
            return Err(LastIDError::config(
                "max_delay_ms must be >= initial_delay_ms",
            ));
        }

        if self.backoff_base < 1.0 {
            return Err(LastIDError::config("backoff_base must be >= 1.0"));
        }

        if !(0.0..=1.0).contains(&self.jitter_factor) {
            return Err(LastIDError::config(
                "jitter_factor must be between 0.0 and 1.0",
            ));
        }

        Ok(())
    }

    /// Calculate delay for a given attempt number (0-indexed).
    ///
    /// Includes jitter to prevent thundering herd when multiple clients retry
    /// simultaneously.
    #[must_use]
    #[allow(
        clippy::cast_precision_loss,
        clippy::cast_possible_truncation,
        clippy::cast_possible_wrap,
        clippy::cast_sign_loss
    )]
    pub fn delay_for_attempt(&self, attempt: u32) -> u64 {
        let base_delay = if attempt == 0 {
            self.initial_delay_ms
        } else {
            // Precision loss is acceptable for backoff calculations.
            // Wrap is safe because attempt is limited by max_attempts (typically < 10).
            // Truncation and sign loss safe because result is capped at max_delay.
            let delay = self.initial_delay_ms as f64 * self.backoff_base.powi(attempt as i32);
            (delay as u64).min(self.max_delay_ms)
        };

        // Apply jitter: delay * (1 - jitter_factor + random * 2 * jitter_factor)
        // This gives a range of [delay * (1 - jitter), delay * (1 + jitter)]
        if self.jitter_factor > 0.0 {
            let jitter_range = base_delay as f64 * self.jitter_factor;
            let random_factor = rand::random::<f64>(); // 0.0 to 1.0
            let jitter = random_factor.mul_add(2.0, -1.0) * jitter_range;
            ((base_delay as f64 + jitter) as u64).max(1) // Ensure at least 1ms
        } else {
            base_delay
        }
    }

    /// Check if we should retry after the given attempt.
    #[must_use]
    pub const fn should_retry(&self, attempt: u32) -> bool {
        attempt < self.max_attempts
    }

    /// Merge another configuration into this one.
    pub fn merge(&mut self, other: &Self) {
        if other.max_attempts != default_max_attempts() {
            self.max_attempts = other.max_attempts;
        }
        if other.initial_delay_ms != default_initial_delay() {
            self.initial_delay_ms = other.initial_delay_ms;
        }
        if other.max_delay_ms != default_max_delay() {
            self.max_delay_ms = other.max_delay_ms;
        }
        if (other.backoff_base - default_backoff_base()).abs() > f64::EPSILON {
            self.backoff_base = other.backoff_base;
        }
        if (other.jitter_factor - default_jitter_factor()).abs() > f64::EPSILON {
            self.jitter_factor = other.jitter_factor;
        }
    }
}

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

    #[test]
    fn test_default_values() {
        let policy = RetryPolicy::default();
        assert_eq!(policy.max_attempts, 3);
        assert_eq!(policy.initial_delay_ms, 1000);
        assert_eq!(policy.max_delay_ms, 30000);
        assert!((policy.backoff_base - 2.0).abs() < f64::EPSILON);
        assert!((policy.jitter_factor - 0.1).abs() < f64::EPSILON);
    }

    #[test]
    fn test_delay_for_attempt_no_jitter() {
        // Use zero jitter for deterministic test
        let policy = RetryPolicy {
            jitter_factor: 0.0,
            ..Default::default()
        };

        // Attempt 0: 1000ms
        assert_eq!(policy.delay_for_attempt(0), 1000);

        // Attempt 1: 1000 * 2 = 2000ms
        assert_eq!(policy.delay_for_attempt(1), 2000);

        // Attempt 2: 1000 * 4 = 4000ms
        assert_eq!(policy.delay_for_attempt(2), 4000);

        // Attempt 3: 1000 * 8 = 8000ms
        assert_eq!(policy.delay_for_attempt(3), 8000);

        // Attempt 5: 1000 * 32 = 32000ms -> capped to 30000ms
        assert_eq!(policy.delay_for_attempt(5), 30000);
    }

    #[test]
    fn test_delay_for_attempt_with_jitter() {
        let policy = RetryPolicy::default(); // 10% jitter

        // Run multiple times and verify delays are within expected range
        for _ in 0..10 {
            let delay = policy.delay_for_attempt(1); // base is 2000ms
            // With 10% jitter: range is [1800, 2200]
            assert!(delay >= 1800, "delay {} below minimum", delay);
            assert!(delay <= 2200, "delay {} above maximum", delay);
        }
    }

    #[test]
    fn test_should_retry() {
        let policy = RetryPolicy::default(); // max_attempts = 3

        assert!(policy.should_retry(0)); // First attempt
        assert!(policy.should_retry(1)); // Second attempt
        assert!(policy.should_retry(2)); // Third attempt
        assert!(!policy.should_retry(3)); // Exceeded
    }

    #[test]
    fn test_validate_invalid() {
        let mut policy = RetryPolicy::default();

        policy.max_attempts = 0;
        assert!(policy.validate().is_err());

        policy.max_attempts = 3;
        policy.initial_delay_ms = 0;
        assert!(policy.validate().is_err());

        policy.initial_delay_ms = 1000;
        policy.max_delay_ms = 500; // Less than initial
        assert!(policy.validate().is_err());

        policy.max_delay_ms = 30000;
        policy.backoff_base = 0.5; // Less than 1.0
        assert!(policy.validate().is_err());

        policy.backoff_base = 2.0;
        policy.jitter_factor = 1.5; // Greater than 1.0
        assert!(policy.validate().is_err());

        policy.jitter_factor = -0.1; // Negative
        assert!(policy.validate().is_err());
    }

    #[test]
    fn test_validate_valid() {
        let policy = RetryPolicy::default();
        assert!(policy.validate().is_ok());
    }
}
