///|
/// Health rules applied to protocol transactions.
pub enum MonitorRule {
  CompletionRequired
  MaximumLatency
  MinimumGap
  PayloadRequired
  AcceptanceRequired
  ThroughputMinimum
} derive(Debug, Eq)

///|
/// Severity of a protocol monitor finding.
pub enum MonitorSeverity {
  MonitorInfo
  MonitorWarning
  MonitorError
} derive(Debug, Eq)

///|
/// One transaction monitor finding.
pub struct MonitorFinding {
  channel : String
  rule : MonitorRule
  severity : MonitorSeverity
  transaction_index : Int?
  time : Int64
  observed : String
  expected : String
  message : String
} derive(Debug)

///|
/// Monitor policy for a protocol channel.
pub struct MonitorOptions {
  require_completed : Bool
  maximum_latency : Int64?
  minimum_gap : Int64?
  require_payload : Bool
  require_acceptance : Bool
  minimum_throughput : Double?
} derive(Debug)

///|
/// Protocol monitor output.
pub struct MonitorReport {
  channel : String
  transactions : Array[ProtocolTransaction]
  findings : Array[MonitorFinding]
  passed : Bool
  score : Int
} derive(Debug)

///|
/// List monitor rules.
pub fn monitor_rule_catalog() -> Array[MonitorRule] {
  [
    CompletionRequired,
    MaximumLatency,
    MinimumGap,
    PayloadRequired,
    AcceptanceRequired,
    ThroughputMinimum,
  ]
}

///|
/// List monitor severities.
pub fn monitor_severity_catalog() -> Array[MonitorSeverity] {
  [MonitorInfo, MonitorWarning, MonitorError]
}

///|
/// Create a permissive monitor policy.
pub fn default_monitor_options() -> MonitorOptions {
  {
    require_completed: true,
    maximum_latency: None,
    minimum_gap: None,
    require_payload: false,
    require_acceptance: false,
    minimum_throughput: None,
  }
}

///|
/// Require a maximum transaction latency.
pub fn MonitorOptions::with_max_latency(
  self : MonitorOptions,
  limit : Int64?,
) -> MonitorOptions {
  { ..self, maximum_latency: limit }
}

///|
/// Require non-empty payloads for accepted transfers.
pub fn MonitorOptions::with_payloads(
  self : MonitorOptions,
  required : Bool,
) -> MonitorOptions {
  { ..self, require_payload: required }
}

///|
/// Require a minimum gap between completed transactions.
pub fn MonitorOptions::with_gap(
  self : MonitorOptions,
  gap : Int64?,
) -> MonitorOptions {
  { ..self, minimum_gap: gap }
}

///|
/// Check one decoded protocol channel.
pub fn monitor_protocol(
  trace : Trace,
  channel : ProtocolChannel,
  options : MonitorOptions,
) -> MonitorReport {
  let transactions = protocol_transactions(trace, channel)
  let findings : Array[MonitorFinding] = []
  for transaction in transactions {
    if options.require_completed && !transaction.completed {
      findings.push({
        channel: channel.name,
        rule: CompletionRequired,
        severity: MonitorError,
        transaction_index: Some(transaction.index),
        time: transaction.start,
        observed: "incomplete",
        expected: "completed",
        message: "transaction has no completion event",
      })
    }
    match options.maximum_latency {
      Some(limit) if transaction.completed && transaction.latency > limit =>
        findings.push({
          channel: channel.name,
          rule: MaximumLatency,
          severity: MonitorError,
          transaction_index: Some(transaction.index),
          time: transaction.end,
          observed: transaction.latency.to_string(),
          expected: limit.to_string(),
          message: "transaction latency exceeded limit",
        })
      _ => ()
    }
    if options.require_payload && transaction.payload is None {
      findings.push({
        channel: channel.name,
        rule: PayloadRequired,
        severity: MonitorWarning,
        transaction_index: Some(transaction.index),
        time: transaction.start,
        observed: "null",
        expected: "payload",
        message: "transaction did not carry a payload",
      })
    }
    if options.require_acceptance && !transaction.accepted {
      findings.push({
        channel: channel.name,
        rule: AcceptanceRequired,
        severity: MonitorError,
        transaction_index: Some(transaction.index),
        time: transaction.start,
        observed: "rejected",
        expected: "accepted",
        message: "transaction was not accepted",
      })
    }
  }
  match options.minimum_gap {
    Some(gap) => {
      let mut previous : Int64? = None
      for transaction in transactions {
        match previous {
          Some(end) if transaction.start - end < gap =>
            findings.push({
              channel: channel.name,
              rule: MinimumGap,
              severity: MonitorWarning,
              transaction_index: Some(transaction.index),
              time: transaction.start,
              observed: (transaction.start - end).to_string(),
              expected: gap.to_string(),
              message: "transactions are closer than the configured gap",
            })
          _ => ()
        }
        previous = Some(transaction.end)
      }
    }
    None => ()
  }
  match options.minimum_throughput {
    Some(minimum) => {
      let actual = if trace.end_time > 0L {
        transactions.length().to_double() / trace.end_time.to_double()
      } else {
        0.0
      }
      if actual < minimum {
        findings.push({
          channel: channel.name,
          rule: ThroughputMinimum,
          severity: MonitorError,
          transaction_index: None,
          time: trace.end_time,
          observed: actual.to_string(),
          expected: minimum.to_string(),
          message: "channel throughput is below the configured minimum",
        })
      }
    }
    None => ()
  }
  let errors = findings
    .filter(fn(item) { item.severity is MonitorError })
    .length()
  {
    channel: channel.name,
    transactions,
    findings,
    passed: errors == 0,
    score: Int::max(0, 100 - errors * 20),
  }
}

///|
/// Return whether the monitor passed without error findings.
pub fn MonitorReport::is_success(self : MonitorReport) -> Bool {
  self.passed
}

///|
/// Return error and warning findings in stable order.
pub fn MonitorReport::findings_by_severity(
  self : MonitorReport,
  severity : MonitorSeverity,
) -> Array[MonitorFinding] {
  self.findings.filter(fn(item) { item.severity == severity })
}

///|
/// Render monitor findings as CSV.
pub fn monitor_findings_csv(findings : Array[MonitorFinding]) -> String {
  let out = StringBuilder()
  out.write_string(
    "channel,rule,severity,index,time,observed,expected,message\n",
  )
  for finding in findings {
    out.write_string(
      escape_text(finding.channel) +
      "," +
      monitor_rule_name(finding.rule) +
      "," +
      monitor_severity_name(finding.severity) +
      ",",
    )
    out.write_string(
      monitor_optional_index(finding.transaction_index) +
      "," +
      finding.time.to_string() +
      "," +
      escape_text(finding.observed) +
      "," +
      escape_text(finding.expected) +
      "," +
      escape_text(finding.message) +
      "\n",
    )
  }
  out.to_string()
}

///|
/// Render a protocol monitor summary.
pub fn MonitorReport::to_text(self : MonitorReport) -> String {
  self.channel +
  " passed=" +
  self.passed.to_string() +
  " score=" +
  self.score.to_string() +
  " findings=" +
  self.findings.length().to_string()
}

///|
/// Render a protocol monitor summary as JSON.
pub fn MonitorReport::to_json(self : MonitorReport) -> String {
  let findings = self.findings.map(monitor_finding_json).join(",")
  "{\"channel\":\"" +
  escape_text(self.channel) +
  "\",\"passed\":" +
  self.passed.to_string() +
  " ,\"score\":" +
  self.score.to_string() +
  ",\"findings\":[" +
  findings +
  "]}"
}

///|
/// Name a monitor rule.
pub fn monitor_rule_name(rule : MonitorRule) -> String {
  match rule {
    CompletionRequired => "completion-required"
    MaximumLatency => "maximum-latency"
    MinimumGap => "minimum-gap"
    PayloadRequired => "payload-required"
    AcceptanceRequired => "acceptance-required"
    ThroughputMinimum => "throughput-minimum"
  }
}

///|
/// Name a monitor severity.
pub fn monitor_severity_name(value : MonitorSeverity) -> String {
  match value {
    MonitorInfo => "info"
    MonitorWarning => "warning"
    MonitorError => "error"
  }
}

///|
/// Encode an optional transaction index.
fn monitor_optional_index(value : Int?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}

///|
/// Encode one finding as JSON.
fn monitor_finding_json(item : MonitorFinding) -> String {
  "{\"rule\":\"" +
  monitor_rule_name(item.rule) +
  "\",\"severity\":\"" +
  monitor_severity_name(item.severity) +
  "\",\"index\":" +
  monitor_optional_index_json(item.transaction_index) +
  ",\"time\":" +
  item.time.to_string() +
  ",\"observed\":\"" +
  escape_text(item.observed) +
  "\",\"expected\":\"" +
  escape_text(item.expected) +
  "\",\"message\":\"" +
  escape_text(item.message) +
  "\"}"
}

///|
/// Encode an optional index as JSON.
fn monitor_optional_index_json(value : Int?) -> String {
  match value {
    Some(item) => item.to_string()
    None => "null"
  }
}