///|
/// Runtime health classification for an on-device filter.
pub(all) enum FilterStatus {
  WarmingUp
  Healthy
  Degraded
  Faulted
} derive(Debug, Eq)

///|
/// A point-in-time operational view.  It is intentionally independent of a
/// concrete filter type so it can be exported by a telemetry adapter.
pub struct OperationalSnapshot {
  timestamp : Int
  result : UpdateResult
  status : FilterStatus
  score : Double
  nis : Double
  state : Array[Double]
  covariance : Matrix
} derive(Debug)

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

///|
pub fn OperationalSnapshot::result(self : OperationalSnapshot) -> UpdateResult {
  self.result
}

///|
pub fn OperationalSnapshot::status(self : OperationalSnapshot) -> FilterStatus {
  self.status
}

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

///|
pub fn OperationalSnapshot::nis(self : OperationalSnapshot) -> Double {
  self.nis
}

///|
pub fn OperationalSnapshot::state(self : OperationalSnapshot) -> Array[Double] {
  self.state.copy()
}

///|
pub fn OperationalSnapshot::covariance(self : OperationalSnapshot) -> Matrix {
  self.covariance.copy()
}

///|
/// Rolling health monitor suitable for long-running sensor services.
pub struct OperationalMonitor {
  mut samples : Int
  mut accepted : Int
  mut failures : Int
  mut covariance_failures : Int
  mut consecutive_failures : Int
  mut worst_nis : Double
  mut score : Double
  warmup_samples : Int
  failure_limit : Int
  nis_limit : Double
}

///|
pub fn OperationalMonitor::new(
  warmup_samples : Int,
  failure_limit : Int,
  nis_limit : Double,
) -> OperationalMonitor {
  {
    samples: 0,
    accepted: 0,
    failures: 0,
    covariance_failures: 0,
    consecutive_failures: 0,
    worst_nis: 0.0,
    score: 1.0,
    warmup_samples: if warmup_samples < 0 {
      0
    } else {
      warmup_samples
    },
    failure_limit: if failure_limit < 1 {
      1
    } else {
      failure_limit
    },
    nis_limit: if nis_limit <= 0.0 {
      1.0
    } else {
      nis_limit
    },
  }
}

///|
fn operational_result_failed(result : UpdateResult) -> Bool {
  match result {
    Accepted => false
    RejectedByGate
    | InvalidMeasurement
    | SingularInnovation
    | MissingMeasurement => true
  }
}

///|
pub fn OperationalMonitor::observe(
  self : OperationalMonitor,
  timestamp : Int,
  filter : KalmanND,
  result : UpdateResult,
) -> OperationalSnapshot {
  self.samples = self.samples + 1
  let state = filter.state()
  let covariance = filter.covariance()
  let covariance_ok = covariance_is_psd(covariance, 0.001) &&
    covariance.is_finite()
  let nis = filter.normalized_innovation_squared()
  let nis_bad = nis.is_nan() || nis.is_inf() || nis > self.nis_limit
  if result is Accepted {
    self.accepted = self.accepted + 1
    self.consecutive_failures = 0
    self.score = self.score + 0.05 * (1.0 - self.score)
  } else {
    self.failures = self.failures + 1
    self.consecutive_failures = self.consecutive_failures + 1
    self.score = self.score * 0.85
  }
  if !covariance_ok {
    self.covariance_failures = self.covariance_failures + 1
    self.score = self.score * 0.5
  }
  if nis > self.worst_nis && !nis.is_nan() {
    self.worst_nis = nis
  }
  let status = if !vector_is_finite(state) || !covariance_ok {
    Faulted
  } else if self.samples <= self.warmup_samples {
    WarmingUp
  } else if self.consecutive_failures >= self.failure_limit ||
    nis_bad ||
    self.score < 0.25 {
    Faulted
  } else if operational_result_failed(result) || self.score < 0.7 {
    Degraded
  } else {
    Healthy
  }
  { timestamp, result, status, score: self.score, nis, state, covariance }
}

///|
pub fn OperationalMonitor::samples(self : OperationalMonitor) -> Int {
  self.samples
}

///|
pub fn OperationalMonitor::accepted(self : OperationalMonitor) -> Int {
  self.accepted
}

///|
pub fn OperationalMonitor::failures(self : OperationalMonitor) -> Int {
  self.failures
}

///|
pub fn OperationalMonitor::covariance_failures(
  self : OperationalMonitor,
) -> Int {
  self.covariance_failures
}

///|
pub fn OperationalMonitor::consecutive_failures(
  self : OperationalMonitor,
) -> Int {
  self.consecutive_failures
}

///|
pub fn OperationalMonitor::worst_nis(self : OperationalMonitor) -> Double {
  self.worst_nis
}

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

///|
pub fn OperationalMonitor::reset(self : OperationalMonitor) -> Unit {
  self.samples = 0
  self.accepted = 0
  self.failures = 0
  self.covariance_failures = 0
  self.consecutive_failures = 0
  self.worst_nis = 0.0
  self.score = 1.0
}

///|
/// A bounded packet queue.  The oldest packet is dropped when capacity is
/// reached, which is safer than allowing a delayed sensor to exhaust memory.
pub struct ObservationBuffer {
  packets : Array[ObservationPacket]
  capacity : Int
}

///|
pub fn ObservationBuffer::new(capacity : Int) -> ObservationBuffer {
  { packets: [], capacity: if capacity < 0 { 0 } else { capacity } }
}

///|
pub fn ObservationBuffer::push(
  self : ObservationBuffer,
  packet : ObservationPacket,
) -> Bool {
  if self.capacity == 0 || !packet.is_valid() {
    return false
  }
  if self.packets.length() >= self.capacity {
    for i in 1.. Int {
  self.packets.length()
}

///|
pub fn ObservationBuffer::capacity(self : ObservationBuffer) -> Int {
  self.capacity
}

///|
pub fn ObservationBuffer::packets(
  self : ObservationBuffer,
) -> Array[ObservationPacket] {
  self.packets.copy()
}

///|
pub fn ObservationBuffer::oldest(
  self : ObservationBuffer,
) -> ObservationPacket? {
  if self.packets.length() == 0 {
    None
  } else {
    Some(self.packets[0])
  }
}

///|
pub fn ObservationBuffer::newest(
  self : ObservationBuffer,
) -> ObservationPacket? {
  if self.packets.length() == 0 {
    None
  } else {
    Some(self.packets[self.packets.length() - 1])
  }
}

///|
pub fn ObservationBuffer::clear(self : ObservationBuffer) -> Unit {
  self.packets.clear()
}

///|
pub fn observation_time_span(buffer : ObservationBuffer) -> Int {
  guard buffer.oldest() is Some(first) else { return 0 }
  guard buffer.newest() is Some(last) else { return 0 }
  last.timestamp() - first.timestamp()
}

///|
/// Check a state against component-wise bounds without mutating it.
pub fn state_within_bounds(
  state : Array[Double],
  lower : Array[Double],
  upper : Array[Double],
) -> Bool {
  if state.length() != lower.length() || state.length() != upper.length() {
    return false
  }
  for i in 0.. upper[i] {
      return false
    }
  }
  true
}

///|
pub fn clamp_state_to_bounds(
  state : Array[Double],
  lower : Array[Double],
  upper : Array[Double],
) -> Array[Double] {
  if state.length() != lower.length() || state.length() != upper.length() {
    return []
  }
  let result = state.copy()
  for i in 0.. upper[i] {
      result[i] = upper[i]
    }
  }
  result
}