///|
/// A named diagnostic value suitable for logs, dashboards and CSV export.
pub struct DiagnosticField {
  name : String
  value : Double
  unit : String
  category : String
  healthy : Bool
} derive(Debug)

///|
pub fn DiagnosticField::new(
  name : String,
  value : Double,
  unit : String,
  category : String,
  healthy : Bool,
) -> DiagnosticField {
  { name, value, unit, category, healthy }
}

///|
pub fn DiagnosticField::name(self : DiagnosticField) -> String {
  self.name
}

///|
pub fn DiagnosticField::value(self : DiagnosticField) -> Double {
  self.value
}

///|
pub fn DiagnosticField::unit(self : DiagnosticField) -> String {
  self.unit
}

///|
pub fn DiagnosticField::category(self : DiagnosticField) -> String {
  self.category
}

///|
pub fn DiagnosticField::healthy(self : DiagnosticField) -> Bool {
  self.healthy
}

///|
pub fn DiagnosticField::csv(self : DiagnosticField) -> String {
  self.name +
  "," +
  self.value.to_string() +
  "," +
  self.unit +
  "," +
  self.category +
  "," +
  self.healthy.to_string()
}

///|
pub struct DiagnosticSnapshot {
  timestamp : Int
  source : String
  fields : Array[DiagnosticField]
  state_dimension : Int
  covariance_dimension : Int
  healthy : Bool
  score : Double
} derive(Debug)

///|
pub fn DiagnosticSnapshot::new(
  timestamp : Int,
  source : String,
  fields : Array[DiagnosticField],
  state_dimension : Int,
  covariance_dimension : Int,
) -> DiagnosticSnapshot {
  let mut healthy = true
  let mut score_sum = 0.0
  for field in fields {
    if !field.healthy() {
      healthy = false
    }
    if field.healthy() {
      score_sum = score_sum + 1.0
    }
  }
  let score = if fields.length() == 0 {
    0.0
  } else {
    score_sum / fields.length().to_double()
  }
  {
    timestamp,
    source,
    fields: fields.copy(),
    state_dimension: state_dimension.max(0),
    covariance_dimension: covariance_dimension.max(0),
    healthy,
    score,
  }
}

///|
pub fn DiagnosticSnapshot::timestamp(self : DiagnosticSnapshot) -> Int {
  self.timestamp
}

///|
pub fn DiagnosticSnapshot::source(self : DiagnosticSnapshot) -> String {
  self.source
}

///|
pub fn DiagnosticSnapshot::fields(
  self : DiagnosticSnapshot,
) -> Array[DiagnosticField] {
  self.fields.copy()
}

///|
pub fn DiagnosticSnapshot::state_dimension(self : DiagnosticSnapshot) -> Int {
  self.state_dimension
}

///|
pub fn DiagnosticSnapshot::covariance_dimension(
  self : DiagnosticSnapshot,
) -> Int {
  self.covariance_dimension
}

///|
pub fn DiagnosticSnapshot::healthy(self : DiagnosticSnapshot) -> Bool {
  self.healthy
}

///|
pub fn DiagnosticSnapshot::score(self : DiagnosticSnapshot) -> Double {
  self.score
}

///|
pub fn DiagnosticSnapshot::field(
  self : DiagnosticSnapshot,
  name : String,
) -> DiagnosticField? {
  for field in self.fields {
    if field.name() == name {
      return Some(field)
    }
  }
  None
}

///|
pub fn DiagnosticSnapshot::csv_header(self : DiagnosticSnapshot) -> String {
  ignore(self)
  "timestamp,source,state_dimension,covariance_dimension,healthy,score"
}

///|
pub fn DiagnosticSnapshot::csv_row(self : DiagnosticSnapshot) -> String {
  self.timestamp.to_string() +
  "," +
  self.source +
  "," +
  self.state_dimension.to_string() +
  "," +
  self.covariance_dimension.to_string() +
  "," +
  self.healthy.to_string() +
  "," +
  self.score.to_string()
}

///|
pub(all) enum DiagnosticSeverity {
  Info
  Warning
  Error
  Critical
} derive(Debug, Eq)

///|
pub struct DiagnosticEvent {
  timestamp : Int
  code : String
  severity : DiagnosticSeverity
  message : String
  value : Double
  mut acknowledged : Bool
} derive(Debug)

///|
pub fn DiagnosticEvent::new(
  timestamp : Int,
  code : String,
  severity : DiagnosticSeverity,
  message : String,
  value : Double,
) -> DiagnosticEvent {
  { timestamp, code, severity, message, value, acknowledged: false }
}

///|
pub fn DiagnosticEvent::timestamp(self : DiagnosticEvent) -> Int {
  self.timestamp
}

///|
pub fn DiagnosticEvent::code(self : DiagnosticEvent) -> String {
  self.code
}

///|
pub fn DiagnosticEvent::severity(self : DiagnosticEvent) -> DiagnosticSeverity {
  self.severity
}

///|
pub fn DiagnosticEvent::message(self : DiagnosticEvent) -> String {
  self.message
}

///|
pub fn DiagnosticEvent::value(self : DiagnosticEvent) -> Double {
  self.value
}

///|
pub fn DiagnosticEvent::acknowledged(self : DiagnosticEvent) -> Bool {
  self.acknowledged
}

///|
pub fn DiagnosticEvent::acknowledge(self : DiagnosticEvent) -> Unit {
  self.acknowledged = true
}

///|
pub fn DiagnosticEvent::severity_text(self : DiagnosticEvent) -> String {
  match self.severity {
    Info => "info"
    Warning => "warning"
    Error => "error"
    Critical => "critical"
  }
}

///|
pub fn DiagnosticEvent::csv(self : DiagnosticEvent) -> String {
  self.timestamp.to_string() +
  "," +
  self.code +
  "," +
  self.severity_text() +
  "," +
  self.message +
  "," +
  self.value.to_string() +
  "," +
  self.acknowledged.to_string()
}

///|
pub struct DiagnosticReport {
  run_id : String
  started_at : Int
  mut finished_at : Int?
  snapshots : Array[DiagnosticSnapshot]
  events : Array[DiagnosticEvent]
  mut closed : Bool
} derive(Debug)

///|
pub fn DiagnosticReport::new(
  run_id : String,
  started_at : Int,
) -> DiagnosticReport {
  {
    run_id,
    started_at,
    finished_at: None,
    snapshots: [],
    events: [],
    closed: false,
  }
}

///|
pub fn DiagnosticReport::run_id(self : DiagnosticReport) -> String {
  self.run_id
}

///|
pub fn DiagnosticReport::started_at(self : DiagnosticReport) -> Int {
  self.started_at
}

///|
pub fn DiagnosticReport::finished_at(self : DiagnosticReport) -> Int? {
  self.finished_at
}

///|
pub fn DiagnosticReport::snapshots(
  self : DiagnosticReport,
) -> Array[DiagnosticSnapshot] {
  self.snapshots.copy()
}

///|
pub fn DiagnosticReport::events(
  self : DiagnosticReport,
) -> Array[DiagnosticEvent] {
  self.events.copy()
}

///|
pub fn DiagnosticReport::closed(self : DiagnosticReport) -> Bool {
  self.closed
}

///|
pub fn DiagnosticReport::add_snapshot(
  self : DiagnosticReport,
  snapshot : DiagnosticSnapshot,
) -> Bool {
  if self.closed {
    false
  } else {
    self.snapshots.push(snapshot)
    true
  }
}

///|
pub fn DiagnosticReport::add_event(
  self : DiagnosticReport,
  event : DiagnosticEvent,
) -> Bool {
  if self.closed {
    false
  } else {
    self.events.push(event)
    true
  }
}

///|
pub fn DiagnosticReport::close(
  self : DiagnosticReport,
  timestamp : Int,
) -> Unit {
  if !self.closed {
    self.finished_at = Some(timestamp)
    self.closed = true
  }
}

///|
pub fn DiagnosticReport::duration(self : DiagnosticReport) -> Int {
  match self.finished_at {
    None => 0
    Some(value) => value - self.started_at
  }
}

///|
pub fn DiagnosticReport::healthy(self : DiagnosticReport) -> Bool {
  for snapshot in self.snapshots {
    if !snapshot.healthy() {
      return false
    }
  }
  for event in self.events {
    match event.severity {
      Error | Critical => if !event.acknowledged() { return false }
      Info | Warning => ()
    }
  }
  true
}

///|
pub fn DiagnosticReport::score(self : DiagnosticReport) -> Double {
  if self.snapshots.length() == 0 {
    return 0.0
  }
  let mut total = 0.0
  for snapshot in self.snapshots {
    total = total + snapshot.score()
  }
  let mut penalty = 0.0
  for event in self.events {
    match event.severity {
      Info => ()
      Warning => penalty = penalty + 0.05
      Error => penalty = penalty + 0.2
      Critical => penalty = penalty + 0.5
    }
  }
  (total / self.snapshots.length().to_double() - penalty).clamp(
    min=0.0,
    max=1.0,
  )
}

///|
pub fn DiagnosticReport::summary(self : DiagnosticReport) -> String {
  "run=" +
  self.run_id +
  ",duration=" +
  self.duration().to_string() +
  ",snapshots=" +
  self.snapshots.length().to_string() +
  ",events=" +
  self.events.length().to_string() +
  ",healthy=" +
  self.healthy().to_string() +
  ",score=" +
  self.score().to_string()
}

///|
pub fn DiagnosticReport::snapshots_csv(self : DiagnosticReport) -> String {
  let mut output = "timestamp,source,state_dimension,covariance_dimension,healthy,score\n"
  for snapshot in self.snapshots {
    output = output + snapshot.csv_row() + "\n"
  }
  output
}

///|
pub fn DiagnosticReport::events_csv(self : DiagnosticReport) -> String {
  let mut output = "timestamp,code,severity,message,value,acknowledged\n"
  for event in self.events {
    output = output + event.csv() + "\n"
  }
  output
}

///|
pub struct DiagnosticAccumulator {
  reports : Array[DiagnosticReport]
  capacity : Int
  mut discarded : Int
} derive(Debug)

///|
pub fn DiagnosticAccumulator::new(capacity : Int) -> DiagnosticAccumulator {
  { reports: [], capacity: capacity.max(1), discarded: 0 }
}

///|
pub fn DiagnosticAccumulator::reports(
  self : DiagnosticAccumulator,
) -> Array[DiagnosticReport] {
  self.reports.copy()
}

///|
pub fn DiagnosticAccumulator::discarded(self : DiagnosticAccumulator) -> Int {
  self.discarded
}

///|
pub fn DiagnosticAccumulator::push(
  self : DiagnosticAccumulator,
  report : DiagnosticReport,
) -> Unit {
  if self.reports.length() >= self.capacity {
    self.reports.remove(0) |> ignore
    self.discarded = self.discarded + 1
  }
  self.reports.push(report)
}

///|
pub fn DiagnosticAccumulator::length(self : DiagnosticAccumulator) -> Int {
  self.reports.length()
}

///|
pub fn DiagnosticAccumulator::average_score(
  self : DiagnosticAccumulator,
) -> Double {
  if self.reports.length() == 0 {
    0.0
  } else {
    let mut total = 0.0
    for report in self.reports {
      total = total + report.score()
    }
    total / self.reports.length().to_double()
  }
}

///|
pub fn DiagnosticAccumulator::healthy_fraction(
  self : DiagnosticAccumulator,
) -> Double {
  if self.reports.length() == 0 {
    0.0
  } else {
    let mut healthy = 0
    for report in self.reports {
      if report.healthy() {
        healthy = healthy + 1
      }
    }
    healthy.to_double() / self.reports.length().to_double()
  }
}

///|
pub fn diagnostic_field_from_covariance(
  name : String,
  covariance : Matrix,
  unit : String,
) -> DiagnosticField {
  let trace = covariance_trace3d(covariance).max(0.0)
  DiagnosticField::new(
    name,
    trace.sqrt(),
    unit,
    "uncertainty",
    covariance_is_psd(covariance, 0.001),
  )
}

///|
pub fn diagnostic_field_from_state(
  name : String,
  state : Array[Double],
  unit : String,
  index : Int,
  minimum : Double,
  maximum : Double,
) -> DiagnosticField {
  let value = if index < 0 || index >= state.length() {
    0.0
  } else {
    state[index]
  }
  DiagnosticField::new(
    name,
    value,
    unit,
    "state",
    value >= minimum && value <= maximum,
  )
}

///|
pub fn diagnostic_snapshot_from_filter(
  timestamp : Int,
  source : String,
  state : Array[Double],
  covariance : Matrix,
) -> DiagnosticSnapshot {
  let fields : Array[DiagnosticField] = []
  for i, value in state {
    fields.push(
      DiagnosticField::new(
        "state_" + i.to_string(),
        value,
        "unitless",
        "state",
        !value.is_nan() && !value.is_inf(),
      ),
    )
  }
  fields.push(
    diagnostic_field_from_covariance("covariance_trace", covariance, "unitless"),
  )
  DiagnosticSnapshot::new(
    timestamp,
    source,
    fields,
    state.length(),
    covariance.rows(),
  )
}

///|
pub fn diagnostic_report_from_quality(
  run_id : String,
  timestamp : Int,
  report : TrajectoryQualityReport,
) -> DiagnosticReport {
  let fields = [
    DiagnosticField::new(
      "quality_score",
      report.score(),
      "ratio",
      "trajectory",
      report.score() >= 0.5,
    ),
    DiagnosticField::new(
      "total_length",
      report.total_length(),
      "unit",
      "trajectory",
      true,
    ),
    DiagnosticField::new(
      "max_speed",
      report.max_speed(),
      "unit_per_s",
      "kinematics",
      true,
    ),
    DiagnosticField::new(
      "mean_uncertainty",
      report.mean_uncertainty(),
      "unit",
      "uncertainty",
      report.mean_uncertainty() >= 0.0,
    ),
  ]
  let snapshot = DiagnosticSnapshot::new(timestamp, "trajectory", fields, 0, 0)
  let result = DiagnosticReport::new(run_id, timestamp)
  result.add_snapshot(snapshot) |> ignore
  for event in report.events() {
    let severity = if event.severity() >= 0.8 {
      Critical
    } else if event.severity() >= 0.6 {
      Error
    } else if event.severity() >= 0.4 {
      Warning
    } else {
      Info
    }
    result.add_event(
      DiagnosticEvent::new(
        event.timestamp(),
        "trajectory-quality",
        severity,
        event.message(),
        event.severity(),
      ),
    )
    |> ignore
  }
  result.close(timestamp)
  result
}

///|
pub fn diagnostic_merge_reports(
  left : DiagnosticReport,
  right : DiagnosticReport,
  run_id : String,
) -> DiagnosticReport {
  let start = left.started_at().min(right.started_at())
  let merged = DiagnosticReport::new(run_id, start)
  for snapshot in left.snapshots() {
    merged.add_snapshot(snapshot) |> ignore
  }
  for snapshot in right.snapshots() {
    merged.add_snapshot(snapshot) |> ignore
  }
  for event in left.events() {
    merged.add_event(event) |> ignore
  }
  for event in right.events() {
    merged.add_event(event) |> ignore
  }
  let end_time : Int? = match left.finished_at() {
    Some(value) => Some(value)
    None => right.finished_at()
  }
  match end_time {
    None => ()
    Some(value) => merged.close(value)
  }
  merged
}

///|
pub fn diagnostic_event_counts(
  events : Array[DiagnosticEvent],
) -> Array[(DiagnosticSeverity, Int)] {
  let result : Array[(DiagnosticSeverity, Int)] = []
  for event in events {
    let mut found = false
    for i in 0.. Double {
  if fields.length() == 0 {
    0.0
  } else {
    let mut score = 0.0
    for field in fields {
      if field.healthy() {
        score = score + 1.0
      }
    }
    score / fields.length().to_double()
  }
}

///|
pub fn diagnostic_value_within(
  value : Double,
  minimum : Double,
  maximum : Double,
) -> Bool {
  !value.is_nan() && !value.is_inf() && value >= minimum && value <= maximum
}

///|
pub fn diagnostic_covariance_health(
  covariance : Matrix,
  tolerance : Double,
) -> Double {
  if covariance.rows() == 0 || covariance.rows() != covariance.cols() {
    0.0
  } else {
    let report = KalmanDiagnostics::new().report(covariance)
    if report.is_healthy() {
      1.0
    } else if report.finite() {
      tolerance.max(0.0).min(1.0)
    } else {
      0.0
    }
  }
}

///|
pub fn diagnostic_snapshot_summary(snapshot : DiagnosticSnapshot) -> String {
  "source=" +
  snapshot.source() +
  ",timestamp=" +
  snapshot.timestamp().to_string() +
  ",fields=" +
  snapshot.fields().length().to_string() +
  ",score=" +
  snapshot.score().to_string()
}