//
// Syd: rock-solid application kernel
// src/retry.rs: Utilities to handle restarting syscalls
//
// Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

//! Utilities to handle restarting syscalls

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

use std::time::Duration;

use nix::errno::Errno;
use retry::{delay::Exponential, retry, OperationResult};

use crate::config::{
    EAGAIN_BACKOFF_FACTOR, EAGAIN_INITIAL_DELAY, EAGAIN_MAX_DELAY, EAGAIN_MAX_RETRY,
};

/// Retries a closure on `EAGAIN` and `EINTR` errors.
///
/// This function will call the provided closure, and if the closure
/// returns `EAGAIN` or `EINTR` error, it will retry the operation until it
/// succeeds or fails with a different error.
pub fn retry_on_intr<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    let strategy =
        Exponential::from_millis_with_factor(EAGAIN_INITIAL_DELAY, EAGAIN_BACKOFF_FACTOR)
            .map(|d| Duration::from_millis(EAGAIN_MAX_DELAY).min(d))
            .take(EAGAIN_MAX_RETRY);

    retry(strategy, || match retry_on_eintr(&mut f) {
        Ok(v) => OperationResult::Ok(v),
        Err(Errno::EAGAIN) => OperationResult::Retry(Errno::EAGAIN),
        Err(errno) => OperationResult::Err(errno),
    })
    .map_err(|e| e.error)
}

/// Retries a closure on `EINTR` errors.
///
/// This function will call the provided closure, and if the closure
/// returns `EINTR` error, it will retry the operation until it
/// succeeds or fails with a different error.
pub fn retry_on_eintr<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    loop {
        match f() {
            Err(Errno::EINTR) => continue,
            result => return result,
        }
    }
}

/// Retries a closure on `EAGAIN` errors.
///
/// This function will call the provided closure, and if the closure
/// returns `EAGAIN` error, it will retry the operation until it
/// succeeds or fails with a different error.
pub fn retry_on_eagain<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    let strategy =
        Exponential::from_millis_with_factor(EAGAIN_INITIAL_DELAY, EAGAIN_BACKOFF_FACTOR)
            .map(|d| Duration::from_millis(EAGAIN_MAX_DELAY).min(d))
            .take(EAGAIN_MAX_RETRY);

    retry(strategy, || match f() {
        Ok(v) => OperationResult::Ok(v),
        Err(Errno::EAGAIN) => OperationResult::Retry(Errno::EAGAIN),
        Err(errno) => OperationResult::Err(errno),
    })
    .map_err(|e| e.error)
}

/// write! which retries on EINTR and EAGAIN.
#[macro_export]
macro_rules! rwrite {
    ($dst:expr, $($arg:tt)*) => {{
        $crate::retry::retry_on_intr(|| {
            $dst.write_fmt(format_args!($($arg)*))
                .map_err(|err| $crate::err::err2no(&err))
        })
    }};
}

/// writeln! which retries on EINTR and EAGAIN.
#[macro_export]
macro_rules! rwriteln {
    ($dst:expr $(, $($arg:tt)*)?) => {{
        $crate::retry::retry_on_intr(|| {
            let () = $dst
                .write_fmt(format_args!($($($arg)*)?))
                .map_err(|err| $crate::err::err2no(&err))?;
            $dst
                .write_all(b"\n")
                .map_err(|err| $crate::err::err2no(&err))
        })
    }};
}

#[cfg(test)]
mod tests {
    use std::time::Instant;

    use super::*;

    #[test]
    fn test_retry_on_eagain_with_backoff() {
        // Simulate EAGAIN with retrying logic and backoff.
        let start = Instant::now();
        let mut attempts = 3; // Simulate 3 retries on EAGAIN.
        let result = retry_on_intr(move || {
            if attempts > 0 {
                attempts -= 1;
                Err(Errno::EAGAIN) // Simulate EAGAIN.
            } else {
                Ok(42) // Simulate success after retries.
            }
        });

        // Assert that it eventually succeeds after retrying with backoff.
        assert_eq!(result, Ok(42));

        let elapsed = start.elapsed();
        // Check that the elapsed time is at least the expected backoff time.
        let expected_duration = Duration::from_millis(EAGAIN_INITIAL_DELAY as u64 * 7); // 1 + 2 + 4 retries
        assert!(
            elapsed >= expected_duration,
            "Expected delay due to exponential backoff"
        );
    }

    #[test]
    fn test_retry_on_eagain_succeeds_after_max_backoff() {
        // Simulate 7 retries, ensuring we hit max backoff duration.
        let start = Instant::now();
        let mut attempts = EAGAIN_MAX_RETRY; // Simulate 7 retries on EAGAIN.
        let result = retry_on_intr(move || {
            if attempts > 0 {
                attempts -= 1;
                Err(Errno::EAGAIN) // Simulate EAGAIN.
            } else {
                Ok(42) // Simulate success after retries.
            }
        });

        // Assert that it eventually succeeds.
        assert_eq!(result, Ok(42));

        let elapsed = start.elapsed();
        // Ensure that the total duration exceeds the capped maximum delay.
        assert!(
            elapsed >= Duration::from_millis(EAGAIN_MAX_DELAY as u64),
            "Expected delay to exceed max backoff duration"
        );
    }

    #[test]
    fn test_retry_on_non_retryable_error() {
        // Test with a non-retryable error (EINVAL) to ensure it doesn't retry.
        let start = Instant::now();
        let result: Result<(), Errno> = retry_on_intr(|| Err(Errno::EINVAL));

        // Ensure the error is returned immediately without retry
        let elapsed = start.elapsed();
        assert!(
            elapsed < Duration::from_millis(10),
            "Expected immediate termination without delay"
        );
        assert_eq!(result, Err(Errno::EINVAL));
    }
}
