///|
/// Escapes a string for JSON output.
pub fn json_escape(value : String) -> String {
  let builder = StringBuilder(size_hint=value.length())
  for index = 0; index < value.length(); index = index + 1 {
    match value.unsafe_get(index) {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      unit => builder.write_char(unit.to_int().unsafe_to_char())
    }
  }
  builder.to_string()
}

///|
/// Stable snake-case name for a decision kind.
pub fn decision_kind_name(kind : DecisionKind) -> String {
  match kind {
    Acquired => "acquired"
    Renewed => "renewed"
    Released => "released"
    Transferred => "transferred"
    Reaped => "reaped"
    Rejected => "rejected"
  }
}

///|
/// Encodes a lease as stable JSON.
pub fn Lease::to_json(self : Lease) -> String {
  "{\"resource\":\"\{json_escape(self.resource)}\",\"holder\":\"\{json_escape(self.holder)}\",\"token\":\{self.token},\"revision\":\{self.revision},\"issued_at\":\{self.issued_at},\"expires_at\":\{self.expires_at}}"
}

///|
/// Encodes an audit event as stable JSON.
pub fn LeaseEvent::to_json(self : LeaseEvent) -> String {
  "{\"resource\":\"\{json_escape(self.resource)}\",\"kind\":\"\{decision_kind_name(self.kind)}\",\"at\":\{self.at},\"holder\":\"\{json_escape(self.holder)}\",\"token\":\{self.token},\"revision\":\{self.revision},\"detail\":\"\{json_escape(self.detail)}\"}"
}

///|
/// Computes point-in-time table metrics.
pub fn LeaseTable::metrics(self : LeaseTable, now : Int) -> LeaseMetrics {
  let mut stored = 0
  let mut active = 0
  let mut expired = 0
  let mut highest = 0
  for state in self.states {
    match state.lease {
      Some(lease) => {
        stored = stored + 1
        if lease.is_active(now) {
          active = active + 1
        } else {
          expired = expired + 1
        }
        if lease.token > highest {
          highest = lease.token
        }
      }
      None => ()
    }
    if state.next_token - 1 > highest {
      highest = state.next_token - 1
    }
  }
  {
    resources: self.states.length(),
    stored_leases: stored,
    active_leases: active,
    expired_leases: expired,
    highest_token: highest,
    events: self.events.length(),
    accepted_commands: self.accepted_commands,
    rejected_commands: self.rejected_commands,
  }
}

///|
/// Encodes metrics as stable JSON.
pub fn LeaseMetrics::to_json(self : LeaseMetrics) -> String {
  "{\"resources\":\{self.resources},\"stored_leases\":\{self.stored_leases},\"active_leases\":\{self.active_leases},\"expired_leases\":\{self.expired_leases},\"highest_token\":\{self.highest_token},\"events\":\{self.events},\"accepted_commands\":\{self.accepted_commands},\"rejected_commands\":\{self.rejected_commands}}"
}

///|
/// Encodes a safety audit as stable JSON.
pub fn AuditReport::to_json(self : AuditReport) -> String {
  let builder = StringBuilder()
  builder.write_string(
    "{\"valid\":\{self.valid},\"checked_events\":\{self.checked_events},\"findings\":[",
  )
  for index = 0; index < self.findings.length(); index = index + 1 {
    if index > 0 {
      builder.write_char(',')
    }
    let finding = self.findings[index]
    builder.write_string(
      "{\"code\":\"\{json_escape(finding.code)}\",\"resource\":\"\{json_escape(finding.resource)}\",\"event_index\":\{finding.event_index},\"message\":\"\{json_escape(finding.message)}\"}",
    )
  }
  builder.write_string("]}")
  builder.to_string()
}