namespace TeamsISO.Engine.Pipeline;

/// <summary>
/// Exponential backoff policy used by <see cref="IsoPipeline"/>'s restart supervisor.
/// Doubles the delay on each successive attempt and caps it at <c>cap</c>. After
/// <c>maxAttempts</c> consecutive failures, <see cref="ShouldGiveUp"/> returns true.
/// </summary>
public sealed class ExponentialBackoff
{
    private readonly int _maxAttempts;
    private readonly TimeSpan _initial;
    private readonly TimeSpan _cap;

    public ExponentialBackoff(int maxAttempts, TimeSpan initial, TimeSpan cap)
    {
        if (maxAttempts <= 0) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
        if (initial <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(initial));
        if (cap < initial) throw new ArgumentOutOfRangeException(nameof(cap));
        _maxAttempts = maxAttempts;
        _initial = initial;
        _cap = cap;
    }

    /// <summary>Default policy: 5 attempts, 1s initial, 30s cap.</summary>
    public static ExponentialBackoff Default { get; } =
        new(maxAttempts: 5, initial: TimeSpan.FromSeconds(1), cap: TimeSpan.FromSeconds(30));

    public int MaxAttempts => _maxAttempts;

    /// <summary>
    /// Returns the delay before the <paramref name="attempt"/>-th retry.
    /// <paramref name="attempt"/> is 1-based.
    /// </summary>
    public TimeSpan NextDelay(int attempt)
    {
        if (attempt <= 0) throw new ArgumentOutOfRangeException(nameof(attempt));
        var multiplier = Math.Pow(2, attempt - 1);
        var delaySeconds = Math.Min(_initial.TotalSeconds * multiplier, _cap.TotalSeconds);
        return TimeSpan.FromSeconds(delaySeconds);
    }

    /// <summary>
    /// Returns true once <paramref name="attemptCount"/> exceeds <c>maxAttempts</c>.
    /// </summary>
    public bool ShouldGiveUp(int attemptCount) => attemptCount > _maxAttempts;
}
