//! Adaptive concurrency controller using AIMD (Additive Increase / Multiplicative Decrease).
//!
//! Dynamically adjusts the number of concurrent S3 requests based on success/failure signals,
//! backing off when S3 returns 503 SlowDown and ramping up during sustained success.

use std::sync::atomic::{AtomicU32, Ordering};

use tracing::debug;

/// AIMD-based adaptive concurrency controller.
///
/// Tracks a permit count that a `tokio::sync::Semaphore` should be resized to.
/// The caller is responsible for adjusting the actual semaphore.
#[derive(Debug)]
pub struct AdaptiveConcurrency {
    current: AtomicU32,
    min: u32,
    max: u32,
    increase_step: u32,
    decrease_factor: f32,
    window_size: u32,
    success_count: AtomicU32,
    enabled: bool,
}

impl AdaptiveConcurrency {
    /// Create a new controller.
    pub fn new(initial: u32, min: u32, max: u32, enabled: bool) -> Self {
        Self {
            current: AtomicU32::new(initial),
            min,
            max,
            increase_step: 1,
            decrease_factor: 0.5,
            window_size: 100,
            success_count: AtomicU32::new(0),
            enabled,
        }
    }

    /// Record a successful copy. Returns the new permit count if it changed.
    pub fn on_success(&self) -> Option<u32> {
        if !self.enabled {
            return None;
        }

        let count = self.success_count.fetch_add(1, Ordering::Relaxed) + 1;

        if count >= self.window_size {
            self.success_count.store(0, Ordering::Relaxed);
            let old = self.current.load(Ordering::Relaxed);
            let new = (old + self.increase_step).min(self.max);
            if new != old {
                self.current.store(new, Ordering::Relaxed);
                debug!(old, new, "AIMD: increased concurrency");
                return Some(new);
            }
        }

        None
    }

    /// Record a throttling event (503 SlowDown). Returns the new permit count.
    pub fn on_throttle(&self) -> Option<u32> {
        if !self.enabled {
            return None;
        }

        self.success_count.store(0, Ordering::Relaxed);
        let old = self.current.load(Ordering::Relaxed);
        let new = ((old as f32 * self.decrease_factor) as u32).max(self.min);
        if new != old {
            self.current.store(new, Ordering::Relaxed);
            debug!(old, new, "AIMD: decreased concurrency (throttled)");
            return Some(new);
        }

        None
    }

    /// Current permit target.
    pub fn current(&self) -> u32 {
        self.current.load(Ordering::Relaxed)
    }
}

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

    #[test]
    fn increase_after_window() {
        let ctrl = AdaptiveConcurrency::new(100, 8, 1024, true);

        // Should not increase before window_size successes.
        for _ in 0..99 {
            assert!(ctrl.on_success().is_none());
        }

        // 100th success triggers increase.
        let new = ctrl.on_success();
        assert_eq!(new, Some(101));
        assert_eq!(ctrl.current(), 101);
    }

    #[test]
    fn decrease_on_throttle() {
        let ctrl = AdaptiveConcurrency::new(100, 8, 1024, true);
        let new = ctrl.on_throttle();
        assert_eq!(new, Some(50));
        assert_eq!(ctrl.current(), 50);
    }

    #[test]
    fn does_not_go_below_min() {
        let ctrl = AdaptiveConcurrency::new(10, 8, 1024, true);
        ctrl.on_throttle(); // 10 -> 5, but min is 8
        assert_eq!(ctrl.current(), 8);
    }

    #[test]
    fn does_not_exceed_max() {
        let ctrl = AdaptiveConcurrency::new(1024, 8, 1024, true);
        for _ in 0..200 {
            ctrl.on_success();
        }
        assert!(ctrl.current() <= 1024);
    }

    #[test]
    fn disabled_returns_none() {
        let ctrl = AdaptiveConcurrency::new(100, 8, 1024, false);
        assert!(ctrl.on_success().is_none());
        assert!(ctrl.on_throttle().is_none());
    }
}
