///|
/// Final state of a delivery after all attempts.
///
/// `Retry` is reserved for scheduler-driven delivery; the synchronous
/// `DeliveryEngine::deliver` reports `Success`, `DeadLetter`, or `Skipped`.
pub(all) enum DeliveryStatus {
  Success
  Retry
  DeadLetter
  Skipped
} derive(Eq, @debug.Debug)

///|
pub fn DeliveryStatus::describe(self : DeliveryStatus) -> String {
  match self {
    Success => "success"
    Retry => "retry"
    DeadLetter => "dead_letter"
    Skipped => "skipped"
  }
}

///|
/// A single recorded attempt inside a delivery.
pub(all) struct DeliveryAttempt {
  attempt : Int
  status_code : Int
  error : String
  duration_ms : Int
} derive(Eq, @debug.Debug)

///|
/// The complete outcome of `DeliveryEngine::deliver`.
pub(all) struct DeliveryOutcome {
  hook_id : String
  event_id : String
  status : DeliveryStatus
  attempts : Array[DeliveryAttempt]
} derive(Eq, @debug.Debug)

///|
/// Drives delivery: looks up the Hook, executes transport attempts, and
/// applies the Hook's retry policy.
pub struct DeliveryEngine {
  registry : HookRegistry
  transport : Transport
}

///|
pub fn DeliveryEngine::new(
  registry : HookRegistry,
  transport : Transport,
) -> DeliveryEngine {
  { registry, transport, }
}

///|
/// Delivers synchronously. Retry delays are exposed through `RetryPolicy` for
/// external schedulers; this engine performs the attempts back to back and
/// honors `RetryPolicy::should_retry` before starting another attempt.
pub fn DeliveryEngine::deliver(
  self : DeliveryEngine,
  hook_id : String,
  event : WebhookEvent,
) -> DeliveryOutcome {
  let attempts : Array[DeliveryAttempt] = []
  match self.registry.get(hook_id) {
    None => { hook_id, event_id: event.id, status: Skipped, attempts, }
    Some(hook) =>
      if !hook.active {
        { hook_id, event_id: event.id, status: Skipped, attempts, }
      } else {
        let max_attempts = hook.max_attempts
        let mut attempt_number = 1
        let mut final_status = DeadLetter
        while attempt_number <= max_attempts {
          let result = (self.transport)(hook, event)
          attempts.push({
            attempt: attempt_number,
            status_code: result.status_code,
            error: result.error,
            duration_ms: result.duration_ms,
          })
          if result.ok {
            final_status = Success
            break
          } else if !result.retryable ||
            !hook.retry_policy.should_retry(attempt_number, max_attempts) {
            final_status = DeadLetter
            break
          } else {
            attempt_number = attempt_number + 1
          }
        }
        { hook_id, event_id: event.id, status: final_status, attempts, }
      }
  }
}