///|
pub(all) enum DeliveryState {
  DeliverySucceeded
  DeliveryDeadLettered
  DeliveryDropped(String)
} derive(Eq, Debug)

///|
pub(all) struct DeliveryAttempt {
  number : Int
  event : Event
  report : DispatchReport
} derive(Eq, Debug)

///|
pub(all) struct DeliveryRun {
  original : Event
  attempts : Array[DeliveryAttempt]
  state : DeliveryState
  dead_letter : Event?
  dead_letter_report : DispatchReport?
} derive(Eq, Debug)

///|
pub(all) struct DeliveryBatch {
  runs : Array[DeliveryRun]
  succeeded : Int
  dead_lettered : Int
  dropped : Int
  total_attempts : Int
} derive(Eq, Debug)

///|
fn failure_reason(report : DispatchReport) -> String {
  let failures = collect_failures(report)
  match failures.get(0) {
    Some(reason) => reason
    None => "dispatch failed"
  }
}

///|
fn successful_run(
  original : Event,
  attempts : Array[DeliveryAttempt],
) -> DeliveryRun {
  {
    original,
    attempts,
    state: DeliverySucceeded,
    dead_letter: None,
    dead_letter_report: None,
  }
}

///|
fn dropped_run(
  original : Event,
  attempts : Array[DeliveryAttempt],
  reason : String,
) -> DeliveryRun {
  {
    original,
    attempts,
    state: DeliveryDropped(reason),
    dead_letter: None,
    dead_letter_report: None,
  }
}

///|
fn dead_lettered_run(
  original : Event,
  attempts : Array[DeliveryAttempt],
  dead_letter : Event,
  report : DispatchReport,
) -> DeliveryRun {
  {
    original,
    attempts,
    state: DeliveryDeadLettered,
    dead_letter: Some(dead_letter),
    dead_letter_report: Some(report),
  }
}

///|
/// Publishes an event and executes RetryPolicy decisions until the event
/// succeeds, is converted to a dead-letter event, or is dropped.
pub fn EventBus::publish_with_retry(
  self : EventBus,
  event : Event,
  policy : RetryPolicy,
) -> (EventBus, DeliveryRun) {
  let attempts : Array[DeliveryAttempt] = []
  let mut bus = self
  let mut current = event
  let mut attempt = 1
  while true {
    let (next, report) = bus.publish(current)
    bus = next
    attempts.push({ number: attempt, event: current, report })
    if report.is_ok() {
      return (bus, successful_run(event, attempts))
    }
    let reason = failure_reason(report)
    match policy.decide(current, attempt, reason) {
      Retry(retry_event) => {
        current = retry_event
        attempt = attempt + 1
      }
      DeadLetter(dead_event) => {
        let (after_dead, dead_report) = bus.publish(dead_event)
        return (
          after_dead,
          dead_lettered_run(event, attempts, dead_event, dead_report),
        )
      }
      Drop(drop_reason) =>
        return (bus, dropped_run(event, attempts, drop_reason))
    }
  }
  (bus, dropped_run(event, attempts, "unreachable"))
}

///|
pub fn DeliveryRun::attempt_count(self : DeliveryRun) -> Int {
  self.attempts.length()
}

///|
pub fn DeliveryRun::succeeded(self : DeliveryRun) -> Bool {
  self.state == DeliverySucceeded
}

///|
pub fn DeliveryRun::was_retried(self : DeliveryRun) -> Bool {
  self.attempts.length() > 1
}

///|
pub fn DeliveryRun::last_report(self : DeliveryRun) -> DispatchReport? {
  if self.attempts.length() == 0 {
    None
  } else {
    match self.attempts.get(self.attempts.length() - 1) {
      Some(attempt) => Some(attempt.report)
      None => None
    }
  }
}

///|
pub fn DeliveryRun::failure_reasons(self : DeliveryRun) -> Array[String] {
  let reasons : Array[String] = []
  for attempt in self.attempts {
    for reason in collect_failures(attempt.report) {
      reasons.push(reason)
    }
  }
  reasons
}

///|
fn summarize_delivery_runs(runs : Array[DeliveryRun]) -> DeliveryBatch {
  let mut succeeded = 0
  let mut dead_lettered = 0
  let mut dropped = 0
  let mut total_attempts = 0
  for run in runs {
    total_attempts = total_attempts + run.attempt_count()
    match run.state {
      DeliverySucceeded => succeeded = succeeded + 1
      DeliveryDeadLettered => dead_lettered = dead_lettered + 1
      DeliveryDropped(_) => dropped = dropped + 1
    }
  }
  { runs, succeeded, dead_lettered, dropped, total_attempts }
}

///|
pub fn EventBus::publish_many_with_retry(
  self : EventBus,
  events : Array[Event],
  policy : RetryPolicy,
) -> (EventBus, DeliveryBatch) {
  let runs : Array[DeliveryRun] = []
  let mut bus = self
  for event in events {
    let (next, run) = bus.publish_with_retry(event, policy)
    bus = next
    runs.push(run)
  }
  (bus, summarize_delivery_runs(runs))
}

///|
pub fn DeliveryBatch::is_ok(self : DeliveryBatch) -> Bool {
  self.dead_lettered == 0 && self.dropped == 0
}

///|
pub fn DeliveryBatch::failed_runs(self : DeliveryBatch) -> Array[DeliveryRun] {
  let failed : Array[DeliveryRun] = []
  for run in self.runs {
    if !run.succeeded() {
      failed.push(run)
    }
  }
  failed
}

///|
pub fn DeliveryBatch::reports(self : DeliveryBatch) -> Array[DispatchReport] {
  let reports : Array[DispatchReport] = []
  for run in self.runs {
    for attempt in run.attempts {
      reports.push(attempt.report)
    }
    match run.dead_letter_report {
      Some(report) => reports.push(report)
      None => ()
    }
  }
  reports
}

///|
pub fn DeliveryBatch::metrics(self : DeliveryBatch) -> BusMetrics {
  metrics_from_reports(self.reports())
}