///|
/// Outcome assigned while replaying a protocol transaction stream.
pub enum ReplayOutcome {
  ReplayAccepted
  ReplayRejected
  ReplayIncomplete
  ReplayOutOfOrder
} derive(Debug, Eq)

///|
/// One replayed transaction with a state-machine sequence number.
pub struct ReplayStep {
  index : Int
  start : Int64
  end : Int64
  state_before : String
  state_after : String
  outcome : ReplayOutcome
  payload : Value?
  note : String
} derive(Debug)

///|
/// A replayed protocol state machine result.
pub struct ProtocolReplay {
  channel : String
  steps : Array[ReplayStep]
  accepted : Int
  rejected : Int
  incomplete : Int
  final_state : String
  deterministic : Bool
} derive(Debug)

///|
/// List replay outcomes.
pub fn replay_outcome_catalog() -> Array[ReplayOutcome] {
  [ReplayAccepted, ReplayRejected, ReplayIncomplete, ReplayOutOfOrder]
}

///|
/// Replay a decoded transaction stream as a small deterministic state machine.
pub fn replay_protocol(
  channel : ProtocolChannel,
  transactions : Array[ProtocolTransaction],
) -> ProtocolReplay {
  let steps : Array[ReplayStep] = []
  let mut state = "idle"
  let mut accepted = 0
  let mut rejected = 0
  let mut incomplete = 0
  let mut previous_end : Int64? = None
  let mut deterministic = true
  for transaction in transactions {
    let order_bad = match previous_end {
      Some(time) => transaction.start < time
      None => false
    }
    let outcome = if order_bad {
      ReplayOutOfOrder
    } else if !transaction.completed {
      ReplayIncomplete
    } else if transaction.accepted {
      ReplayAccepted
    } else {
      ReplayRejected
    }
    let after = match outcome {
      ReplayAccepted => "accepted"
      ReplayRejected => "rejected"
      ReplayIncomplete => "incomplete"
      ReplayOutOfOrder => "out-of-order"
    }
    steps.push({
      index: transaction.index,
      start: transaction.start,
      end: transaction.end,
      state_before: state,
      state_after: after,
      outcome,
      payload: transaction.payload,
      note: if order_bad {
        "transaction starts before previous completion"
      } else {
        "replayed in source order"
      },
    })
    state = after
    match outcome {
      ReplayAccepted => accepted += 1
      ReplayRejected => rejected += 1
      ReplayIncomplete => incomplete += 1
      ReplayOutOfOrder => deterministic = false
    }
    previous_end = Some(transaction.end)
  }
  {
    channel: channel.name,
    steps,
    accepted,
    rejected,
    incomplete,
    final_state: state,
    deterministic,
  }
}

///|
/// Replay a trace directly through a protocol channel.
pub fn replay_protocol_trace(
  trace : Trace,
  channel : ProtocolChannel,
) -> ProtocolReplay {
  replay_protocol(channel, protocol_transactions(trace, channel))
}

///|
/// Return whether the replay was deterministic and complete.
pub fn ProtocolReplay::is_success(self : ProtocolReplay) -> Bool {
  self.deterministic && self.incomplete == 0
}

///|
/// Return replay steps with a particular outcome.
pub fn ProtocolReplay::steps_with(
  self : ProtocolReplay,
  outcome : ReplayOutcome,
) -> Array[ReplayStep] {
  self.steps.filter(fn(item) { item.outcome == outcome })
}

///|
/// Export replay steps as CSV.
pub fn replay_steps_csv(steps : Array[ReplayStep]) -> String {
  let out = StringBuilder()
  out.write_string("index,start,end,before,after,outcome,payload,note\n")
  for step in steps {
    out.write_string(
      step.index.to_string() +
      "," +
      step.start.to_string() +
      "," +
      step.end.to_string() +
      "," +
      step.state_before +
      "," +
      step.state_after +
      "," +
      replay_outcome_name(step.outcome) +
      ",",
    )
    out.write_string(
      match step.payload {
        Some(value) => escape_text(value.display())
        None => ""
      },
    )
    out.write_string("," + escape_text(step.note) + "\n")
  }
  out.to_string()
}

///|
/// Render replay metadata as JSON.
pub fn ProtocolReplay::to_json(self : ProtocolReplay) -> String {
  "{\"channel\":\"" +
  escape_text(self.channel) +
  "\",\"accepted\":" +
  self.accepted.to_string() +
  ",\"rejected\":" +
  self.rejected.to_string() +
  ",\"incomplete\":" +
  self.incomplete.to_string() +
  ",\"deterministic\":" +
  self.deterministic.to_string() +
  ",\"steps\":[" +
  self.steps.map(replay_step_json).join(",") +
  "]}"
}

///|
/// Render replay metadata for a terminal.
pub fn ProtocolReplay::to_text(self : ProtocolReplay) -> String {
  self.channel +
  " accepted=" +
  self.accepted.to_string() +
  " rejected=" +
  self.rejected.to_string() +
  " incomplete=" +
  self.incomplete.to_string() +
  " deterministic=" +
  self.deterministic.to_string()
}

///|
/// Name a replay outcome.
pub fn replay_outcome_name(value : ReplayOutcome) -> String {
  match value {
    ReplayAccepted => "accepted"
    ReplayRejected => "rejected"
    ReplayIncomplete => "incomplete"
    ReplayOutOfOrder => "out-of-order"
  }
}

///|
/// Encode a replay step as JSON.
fn replay_step_json(item : ReplayStep) -> String {
  "{\"index\":" +
  item.index.to_string() +
  ",\"start\":" +
  item.start.to_string() +
  ",\"end\":" +
  item.end.to_string() +
  ",\"before\":\"" +
  item.state_before +
  "\",\"after\":\"" +
  item.state_after +
  "\",\"outcome\":\"" +
  replay_outcome_name(item.outcome) +
  "\",\"note\":\"" +
  escape_text(item.note) +
  "\"}"
}