///|
/// Reproducible decision traces for process studies.
pub(all) struct TraceEvent {
  sequence : Int
  category : String
  name : String
  input : String
  output : String
  passed : Bool
  note : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct DecisionTrace {
  study : String
  events : Array[TraceEvent]
  passed : Bool
  failure_count : Int
  warning_count : Int
} derive(Debug, Eq, ToJson)

///|
pub(all) struct TraceMetric {
  name : String
  value : Double
  lower : Double
  upper : Double
  unit : String
  accepted : Bool
} derive(Debug, Eq, ToJson)

///|
pub fn DecisionTrace::new(study~ : String) -> DecisionTrace {
  { study, events: [], passed: true, failure_count: 0, warning_count: 0 }
}

///|
pub fn DecisionTrace::record(
  self : DecisionTrace,
  category : String,
  name : String,
  input : String,
  output : String,
  passed : Bool,
  note : String,
) -> DecisionTrace {
  let events = self.events.copy()
  events.push({
    sequence: events.length() + 1,
    category,
    name,
    input,
    output,
    passed,
    note,
  })
  {
    study: self.study,
    events,
    passed: self.passed && passed,
    failure_count: if passed {
      self.failure_count
    } else {
      self.failure_count + 1
    },
    warning_count: if note.length() == 0 {
      self.warning_count
    } else {
      self.warning_count + 1
    },
  }
}

///|
pub fn DecisionTrace::add_metric(
  self : DecisionTrace,
  metric : TraceMetric,
) -> DecisionTrace {
  self.record(
    "metric",
    metric.name,
    metric.value.to_string(),
    metric.output_text(),
    metric.accepted,
    if metric.accepted {
      ""
    } else {
      "metric outside acceptance band"
    },
  )
}

///|
pub fn TraceMetric::output_text(self : TraceMetric) -> String {
  self.value.to_string() +
  " " +
  self.unit +
  " [" +
  self.lower.to_string() +
  ", " +
  self.upper.to_string() +
  "]"
}

///|
pub fn metric(
  name : String,
  value : Double,
  lower : Double,
  upper : Double,
  unit : String,
) -> TraceMetric {
  {
    name,
    value,
    lower,
    upper,
    unit,
    accepted: value >= lower && value <= upper,
  }
}

///|
pub fn metric_relative_margin(metric : TraceMetric) -> Double {
  if metric.lower <= 0.0 && metric.upper <= 0.0 {
    0.0
  } else if metric.value < metric.lower {
    (metric.value - metric.lower) /
    (metric.upper - metric.lower).abs().max(1.0e-12)
  } else if metric.value > metric.upper {
    (metric.upper - metric.value) /
    (metric.upper - metric.lower).abs().max(1.0e-12)
  } else {
    (metric.value - metric.lower).min(metric.upper - metric.value) /
    (metric.upper - metric.lower).abs().max(1.0e-12)
  }
}

///|
pub fn trace_events_by_category(
  trace : DecisionTrace,
  category : String,
) -> Array[TraceEvent] {
  let result = []
  for event in trace.events {
    if event.category == category {
      result.push(event)
    }
  }
  result
}

///|
pub fn trace_failures(trace : DecisionTrace) -> Array[TraceEvent] {
  let result = []
  for event in trace.events {
    if !event.passed {
      result.push(event)
    }
  }
  result
}

///|
pub fn trace_warnings(trace : DecisionTrace) -> Array[TraceEvent] {
  let result = []
  for event in trace.events {
    if event.note.length() > 0 {
      result.push(event)
    }
  }
  result
}

///|
pub fn trace_has_category(trace : DecisionTrace, category : String) -> Bool {
  for event in trace.events {
    if event.category == category {
      return true
    }
  }
  false
}

///|
pub fn trace_replay_summary(trace : DecisionTrace) -> String {
  "study=" +
  trace.study +
  ",events=" +
  trace.events.length().to_string() +
  ",passed=" +
  trace.passed.to_string() +
  ",failures=" +
  trace.failure_count.to_string() +
  ",warnings=" +
  trace.warning_count.to_string()
}

///|
pub fn trace_csv(trace : DecisionTrace) -> String {
  let lines = ["sequence,category,name,input,output,passed,note"]
  for event in trace.events {
    lines.push(
      event.sequence.to_string() +
      "," +
      event.category +
      "," +
      event.name +
      "," +
      event.input +
      "," +
      event.output +
      "," +
      event.passed.to_string() +
      "," +
      event.note,
    )
  }
  lines.join("\n")
}

///|
pub fn trace_markdown(trace : DecisionTrace) -> String {
  let lines = [
    "# Decision trace: " + trace.study,
    "",
    "| # | Category | Name | Input | Output | Passed | Note |",
    "|---:|---|---|---|---|---|---|",
  ]
  for event in trace.events {
    lines.push(
      "| " +
      event.sequence.to_string() +
      " | " +
      event.category +
      " | " +
      event.name +
      " | " +
      event.input +
      " | " +
      event.output +
      " | " +
      event.passed.to_string() +
      " | " +
      event.note +
      " |",
    )
  }
  lines.push("")
  let status = if trace.passed { "passed" } else { "failed" }
  lines.push("Overall status: " + status)
  lines.join("\n")
}

///|
pub fn trace_merge(
  first : DecisionTrace,
  second : DecisionTrace,
) -> DecisionTrace {
  let mut result = DecisionTrace::new(study=first.study)
  for event in first.events {
    result = result.record(
      event.category,
      event.name,
      event.input,
      event.output,
      event.passed,
      event.note,
    )
  }
  for event in second.events {
    result = result.record(
      event.category,
      event.name,
      event.input,
      event.output,
      event.passed,
      event.note,
    )
  }
  result
}

///|
pub fn trace_check(trace : DecisionTrace, minimum_events : Int) -> Bool {
  trace.events.length() >= minimum_events && trace.passed
}

///|
pub fn trace_event_count(trace : DecisionTrace, passed : Bool) -> Int {
  let mut count = 0
  for event in trace.events {
    if event.passed == passed {
      count += 1
    }
  }
  count
}

///|
pub fn trace_categories(trace : DecisionTrace) -> Array[String] {
  let categories = []
  for event in trace.events {
    let mut found = false
    for category in categories {
      if category == event.category {
        found = true
      }
    }
    if !found {
      categories.push(event.category)
    }
  }
  categories
}

///|
pub fn trace_category_counts(trace : DecisionTrace) -> Array[(String, Int)] {
  let categories = trace_categories(trace)
  let result = []
  for category in categories {
    let count = trace_events_by_category(trace, category).length()
    result.push((category, count))
  }
  result
}

///|
pub fn metric_summary(metrics : ArrayView[TraceMetric]) -> String {
  let mut accepted = 0
  for metric in metrics {
    if metric.accepted {
      accepted += 1
    }
  }
  "metrics=" +
  metrics.length().to_string() +
  ",accepted=" +
  accepted.to_string() +
  ",rejected=" +
  (metrics.length() - accepted).to_string()
}

///|
pub fn metrics_to_trace(
  study : String,
  metrics : ArrayView[TraceMetric],
) -> DecisionTrace {
  let mut trace = DecisionTrace::new(study~)
  for metric in metrics {
    trace = trace.add_metric(metric)
  }
  trace
}

///|
pub fn trace_json_lines(trace : DecisionTrace) -> String {
  let lines = []
  for event in trace.events {
    lines.push(
      "{\"sequence\":" +
      event.sequence.to_string() +
      ",\"category\":\"" +
      event.category +
      "\",\"name\":\"" +
      event.name +
      "\",\"input\":\"" +
      event.input +
      "\",\"output\":\"" +
      event.output +
      "\",\"passed\":" +
      event.passed.to_string() +
      ",\"note\":\"" +
      event.note +
      "\"}",
    )
  }
  lines.join("\n")
}

///|
pub fn trace_status_code(trace : DecisionTrace) -> Int {
  if trace.passed && trace.warning_count == 0 {
    0
  } else if trace.passed {
    1
  } else {
    2
  }
}