use std::time::Duration;

use rand::{RngExt, rng};

#[derive(Debug, Clone, Copy)]
pub struct RetryBackoffBuilder {
    pub min_base_delay: Duration,
    pub max_base_delay: Duration,
    pub max_retries: u32,
}

impl Default for RetryBackoffBuilder {
    fn default() -> Self {
        Self {
            min_base_delay: Duration::from_millis(100),
            max_base_delay: Duration::from_secs(1),
            max_retries: 3,
        }
    }
}

impl RetryBackoffBuilder {
    pub fn with_min_base_delay(self, min_base_delay: Duration) -> Self {
        Self {
            min_base_delay,
            ..self
        }
    }

    pub fn with_max_base_delay(self, max_base_delay: Duration) -> Self {
        Self {
            max_base_delay,
            ..self
        }
    }

    pub fn with_max_retries(self, max_retries: u32) -> Self {
        Self {
            max_retries,
            ..self
        }
    }

    pub fn build(self) -> RetryBackoff {
        RetryBackoff {
            min_base_delay: self.min_base_delay,
            max_base_delay: self.max_base_delay,
            max_retries: self.max_retries,
            cur_retry: 0,
        }
    }
}

pub struct RetryBackoff {
    min_base_delay: Duration,
    max_base_delay: Duration,
    max_retries: u32,
    cur_retry: u32,
}

impl RetryBackoff {
    pub fn remaining(&self) -> u32 {
        self.max_retries.saturating_sub(self.cur_retry)
    }

    pub fn is_exhausted(&self) -> bool {
        self.cur_retry >= self.max_retries
    }

    pub fn reset(&mut self) {
        self.cur_retry = 0;
    }

    pub fn used(&self) -> u32 {
        self.cur_retry
    }
}

impl Iterator for RetryBackoff {
    type Item = Duration;

    fn next(&mut self) -> Option<Self::Item> {
        if self.cur_retry == self.max_retries {
            return None;
        }
        let base_delay = (self
            .min_base_delay
            .saturating_mul(2u32.saturating_pow(self.cur_retry)))
        .min(self.max_base_delay);
        let jitter =
            Duration::try_from_secs_f64(base_delay.as_secs_f64() * rng().random_range(0.0..=1.0))
                .unwrap_or(Duration::MAX);
        let delay = base_delay.saturating_add(jitter);
        self.cur_retry += 1;
        Some(delay)
    }
}

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

    #[test]
    fn backoffs() {
        let backoffs: Vec<_> = RetryBackoffBuilder::default()
            .with_max_retries(6)
            .build()
            .collect();

        assert_eq!(backoffs.len(), 6);
        assert!(backoffs[0] >= Duration::from_millis(100));
        assert!(backoffs[0] <= Duration::from_millis(200));

        assert!(backoffs[1] >= Duration::from_millis(200));
        assert!(backoffs[1] <= Duration::from_millis(400));

        assert!(backoffs[2] >= Duration::from_millis(400));
        assert!(backoffs[2] <= Duration::from_millis(800));

        assert!(backoffs[3] >= Duration::from_millis(800));
        assert!(backoffs[3] <= Duration::from_millis(1600));

        assert!(backoffs[4] >= Duration::from_millis(1000));
        assert!(backoffs[4] <= Duration::from_millis(2000));

        assert!(backoffs[5] >= Duration::from_millis(1000));
        assert!(backoffs[5] <= Duration::from_millis(2000));
    }

    #[test]
    fn backoff_with_reset() {
        let mut backoff = RetryBackoffBuilder::default().with_max_retries(3).build();

        assert_eq!(backoff.used(), 0);
        assert_eq!(backoff.remaining(), 3);
        assert!(!backoff.is_exhausted());

        assert!(backoff.next().is_some());
        assert_eq!(backoff.used(), 1);
        assert_eq!(backoff.remaining(), 2);
        assert!(!backoff.is_exhausted());

        backoff.reset();

        assert_eq!(backoff.used(), 0);
        assert_eq!(backoff.remaining(), 3);
        assert!(!backoff.is_exhausted());

        assert!(backoff.next().is_some());
        assert_eq!(backoff.used(), 1);
        assert_eq!(backoff.remaining(), 2);

        assert!(backoff.next().is_some());
        assert_eq!(backoff.used(), 2);
        assert_eq!(backoff.remaining(), 1);

        assert!(backoff.next().is_some());
        assert_eq!(backoff.used(), 3);
        assert_eq!(backoff.remaining(), 0);
        assert!(backoff.is_exhausted());

        assert!(backoff.next().is_none());
    }
}
