///|
/// Replays a complete validated journal from an initial state.
///
/// `payload_fingerprint` and `state_fingerprint` must be deterministic.
/// `reducer` must not depend on time, randomness, I/O, or mutable global state.
pub fn[P, S] replay(
  journal : Journal[P],
  initial_state : S,
  payload_fingerprint : (P) -> String,
  state_fingerprint : (S) -> String,
  reducer : (S, JournalEvent[P]) -> Transition[S],
) -> ReplayReport[S] {
  let initial_hash = fingerprint_text(state_fingerprint(initial_state))
  let state_hashes : Array[Int] = [initial_hash]
  match journal.validate(payload_fingerprint) {
    Invalid(issue) =>
      return {
        status: InvalidJournal(issue),
        initial_sequence: 0,
        applied_events: 0,
        final_sequence: 0,
        final_state: initial_state,
        final_state_hash: initial_hash,
        journal_tail_hash: journal.tail_hash(),
        state_hashes,
      }
    Valid(_, _) => ()
  }
  let mut state = initial_state
  let mut applied = 0
  for index = 0; index < journal.length(); index = index + 1 {
    let event = match journal.get(index) {
      Some(event) => event
      None => abort("validated journal index disappeared")
    }
    match reducer(state, event) {
      Accepted(next_state) => {
        state = next_state
        applied = applied + 1
        state_hashes.push(fingerprint_text(state_fingerprint(state)))
      }
      Rejected(reason) =>
        return {
          status: Rejected(event.sequence, reason),
          initial_sequence: 0,
          applied_events: applied,
          final_sequence: event.sequence - 1,
          final_state: state,
          final_state_hash: fingerprint_text(state_fingerprint(state)),
          journal_tail_hash: journal.tail_hash(),
          state_hashes,
        }
    }
  }
  {
    status: Completed,
    initial_sequence: 0,
    applied_events: applied,
    final_sequence: applied,
    final_state: state,
    final_state_hash: fingerprint_text(state_fingerprint(state)),
    journal_tail_hash: journal.tail_hash(),
    state_hashes,
  }
}

///|
/// Runs the same replay twice and checks the complete transition evidence.
///
/// This catches reducers that accidentally read time, randomness, or mutable
/// ambient state even when their final states happen to match.
pub fn[P, S] verify_determinism(
  journal : Journal[P],
  initial_state : S,
  payload_fingerprint : (P) -> String,
  state_fingerprint : (S) -> String,
  reducer : (S, JournalEvent[P]) -> Transition[S],
) -> Bool {
  let first = replay(
    journal, initial_state, payload_fingerprint, state_fingerprint, reducer,
  )
  let second = replay(
    journal, initial_state, payload_fingerprint, state_fingerprint, reducer,
  )
  if first.status != second.status ||
    first.final_state_hash != second.final_state_hash ||
    first.state_hashes.length() != second.state_hashes.length() {
    return false
  }
  for index = 0; index < first.state_hashes.length(); index = index + 1 {
    if first.state_hashes[index] != second.state_hashes[index] {
      return false
    }
  }
  true
}