///|
/// Data-quality summary for a timestamped sensor stream.
pub struct DataQualityReport {
  total : Int
  valid : Int
  missing : Int
  non_finite : Int
  non_monotonic_timestamps : Int
  duplicate_timestamps : Int
  finite_fraction : Double
} derive(Debug)

///|
pub fn DataQualityReport::total(self : DataQualityReport) -> Int {
  self.total
}

///|
pub fn DataQualityReport::valid(self : DataQualityReport) -> Int {
  self.valid
}

///|
pub fn DataQualityReport::missing(self : DataQualityReport) -> Int {
  self.missing
}

///|
pub fn DataQualityReport::non_finite(self : DataQualityReport) -> Int {
  self.non_finite
}

///|
pub fn DataQualityReport::non_monotonic_timestamps(
  self : DataQualityReport,
) -> Int {
  self.non_monotonic_timestamps
}

///|
pub fn DataQualityReport::duplicate_timestamps(self : DataQualityReport) -> Int {
  self.duplicate_timestamps
}

///|
pub fn DataQualityReport::finite_fraction(self : DataQualityReport) -> Double {
  self.finite_fraction
}

///|
pub fn assess_sensor_samples(
  samples : Array[SensorSample],
) -> DataQualityReport {
  let mut valid = 0
  let mut missing = 0
  let mut non_finite = 0
  let mut non_monotonic = 0
  let mut duplicate = 0
  let mut previous_timestamp : Int? = None
  for sample in samples {
    if sample.missing() {
      missing = missing + 1
    } else if vector_is_finite(sample.measurement()) {
      valid = valid + 1
    } else {
      non_finite = non_finite + 1
    }
    match previous_timestamp {
      None => ()
      Some(previous) => {
        if sample.timestamp() < previous {
          non_monotonic = non_monotonic + 1
        }
        if sample.timestamp() == previous {
          duplicate = duplicate + 1
        }
      }
    }
    previous_timestamp = Some(sample.timestamp())
  }
  let total = samples.length()
  {
    total,
    valid,
    missing,
    non_finite,
    non_monotonic_timestamps: non_monotonic,
    duplicate_timestamps: duplicate,
    finite_fraction: if total == 0 {
      0.0
    } else {
      valid.to_double() / total.to_double()
    },
  }
}

///|
pub struct PacketQuality {
  sensor : String
  report : DataQualityReport
  accepted : Bool
  reason : String
} derive(Debug)

///|
pub fn PacketQuality::sensor(self : PacketQuality) -> String {
  self.sensor
}

///|
pub fn PacketQuality::report(self : PacketQuality) -> DataQualityReport {
  self.report
}

///|
pub fn PacketQuality::accepted(self : PacketQuality) -> Bool {
  self.accepted
}

///|
pub fn PacketQuality::reason(self : PacketQuality) -> String {
  self.reason
}

///|
pub fn assess_packet(
  packet : ObservationPacket,
  minimum_finite_fraction : Double,
) -> PacketQuality {
  let values = packet.values()
  let finite = vector_is_finite(values)
  let report : DataQualityReport = {
    total: 1,
    valid: if packet.is_valid() && finite {
      1
    } else {
      0
    },
    missing: if values.length() == 0 {
      1
    } else {
      0
    },
    non_finite: if finite {
      0
    } else {
      1
    },
    non_monotonic_timestamps: 0,
    duplicate_timestamps: 0,
    finite_fraction: if packet.is_valid() && finite {
      1.0
    } else {
      0.0
    },
  }
  let threshold = if minimum_finite_fraction < 0.0 {
    0.0
  } else {
    minimum_finite_fraction
  }
  let accepted = report.finite_fraction() >= threshold && packet.is_valid()
  {
    sensor: packet.sensor(),
    report,
    accepted,
    reason: if accepted {
      "ok"
    } else if !finite {
      "non_finite"
    } else if values.length() == 0 {
      "missing"
    } else {
      "invalid_shape"
    },
  }
}

///|
pub fn quality_weight(report : DataQualityReport) -> Double {
  if report.total() == 0 {
    0.0
  } else {
    report.finite_fraction()
  }
}

///|
pub fn quality_adjusted_covariance(
  covariance : Matrix,
  report : DataQualityReport,
  floor : Double,
) -> Matrix {
  let safe_floor = if floor <= 0.0 { 0.000001 } else { floor }
  let weight = quality_weight(report)
  if weight <= 0.000000000001 {
    covariance.add_diagonal(1000000.0)
  } else {
    covariance.scale(1.0 / weight).add_diagonal(safe_floor)
  }
}