///|
/// Retry delay strategy.
pub(all) enum RetryBackoff {
  NoBackoff
  FixedDelay
  LinearBackoff
  ExponentialBackoff
} derive(Eq, Debug)

///|
/// Declarative retry policy for failed WebHook processing.
pub(all) struct RetryPolicy {
  max_attempts : Int
  base_delay_ms : Int
  max_delay_ms : Int
  jitter_ms : Int
  backoff : RetryBackoff
} derive(Eq, Debug)

///|
/// Create a retry policy.
pub fn RetryPolicy::RetryPolicy(
  max_attempts? : Int = 3,
  base_delay_ms? : Int = 1000,
  max_delay_ms? : Int = 30000,
  jitter_ms? : Int = 0,
  backoff? : RetryBackoff = ExponentialBackoff,
) -> RetryPolicy {
  { max_attempts, base_delay_ms, max_delay_ms, jitter_ms, backoff }
}

///|
/// Disable retries.
pub fn no_retry_policy() -> RetryPolicy {
  RetryPolicy(
    max_attempts=1,
    base_delay_ms=0,
    max_delay_ms=0,
    backoff=NoBackoff,
  )
}

///|
/// Retry with a fixed delay.
pub fn fixed_retry_policy(
  attempts? : Int = 3,
  delay_ms? : Int = 1000,
) -> RetryPolicy {
  RetryPolicy(
    max_attempts=attempts,
    base_delay_ms=delay_ms,
    max_delay_ms=delay_ms,
    backoff=FixedDelay,
  )
}

///|
/// Retry with an exponential delay.
pub fn exponential_retry_policy(
  attempts? : Int = 5,
  base_delay_ms? : Int = 500,
  max_delay_ms? : Int = 60000,
) -> RetryPolicy {
  RetryPolicy(
    max_attempts=attempts,
    base_delay_ms~,
    max_delay_ms~,
    backoff=ExponentialBackoff,
  )
}

///|
/// Decide whether a result should be attempted again.
pub fn RetryPolicy::can_retry(self : RetryPolicy, result : HookResult) -> Bool {
  result.should_retry() && result.attempts < self.max_attempts
}

///|
/// Compute delay for a 1-based attempt number.
pub fn RetryPolicy::delay_for_attempt(self : RetryPolicy, attempt : Int) -> Int {
  let raw = match self.backoff {
    NoBackoff => 0
    FixedDelay => self.base_delay_ms
    LinearBackoff => self.base_delay_ms * attempt
    ExponentialBackoff => self.base_delay_ms * pow2(attempt - 1)
  }
  clamp_delay(raw + self.jitter_ms, self.max_delay_ms)
}

///|
/// Convert a failed result into a scheduled retry when policy allows it.
pub fn RetryPolicy::schedule(
  self : RetryPolicy,
  result : HookResult,
) -> HookResult {
  if self.can_retry(result) {
    retried(
      message="retry in " +
        self.delay_for_attempt(result.attempts + 1).to_string() +
        "ms",
      attempts=result.attempts + 1,
    )
  } else {
    result
  }
}

///|
fn clamp_delay(value : Int, max_value : Int) -> Int {
  if max_value <= 0 {
    value
  } else if value > max_value {
    max_value
  } else {
    value
  }
}

///|
fn pow2(exp : Int) -> Int {
  let mut acc = 1
  let mut i = 0
  while i < exp {
    acc = acc * 2
    i = i + 1
  }
  acc
}