///|
/// Sensor calibration parameters for vendor-neutral RR streams.
pub(all) struct SensorCalibration {
  scale : Double
  offset_ms : Double
  minimum_rr : Double
  maximum_rr : Double
  timestamp_jitter_ms : Double
  name : String
} derive(FromJson, ToJson, Debug, Eq)

///|
/// A calibration audit record.
pub(all) struct CalibrationReport {
  input_count : Int
  output_count : Int
  changed_count : Int
  rejected_count : Int
  mean_shift_ms : Double
  max_shift_ms : Double
  jitter_rms_ms : Double
  passed : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Return a neutral calibration profile.
pub fn SensorCalibration::identity() -> SensorCalibration {
  {
    scale: 1.0,
    offset_ms: 0.0,
    minimum_rr: 300.0,
    maximum_rr: 2000.0,
    timestamp_jitter_ms: 0.0,
    name: "identity",
  }
}

///|
/// Return a calibration profile for a common millisecond sensor feed.
pub fn SensorCalibration::millisecond_sensor() -> SensorCalibration {
  {
    scale: 1.0,
    offset_ms: 0.0,
    minimum_rr: 250.0,
    maximum_rr: 2500.0,
    timestamp_jitter_ms: 2.0,
    name: "millisecond_sensor",
  }
}

///|
/// Convert one raw sensor reading into milliseconds.
pub fn calibrate_value(raw : Double, calibration : SensorCalibration) -> Double {
  raw * calibration.scale + calibration.offset_ms
}

///|
/// Convert a calibrated value into beats per minute.
pub fn rr_to_bpm(interval_ms : Double) -> Double {
  if interval_ms <= 0.0 || interval_ms.is_nan() {
    0.0
  } else {
    60000.0 / interval_ms
  }
}

///|
/// Convert beats per minute into milliseconds per beat.
pub fn bpm_to_rr(rate_bpm : Double) -> Double {
  if rate_bpm <= 0.0 || rate_bpm.is_nan() {
    0.0
  } else {
    60000.0 / rate_bpm
  }
}

///|
/// Calibrate and retain only values in the profile's range.
pub fn calibrate_intervals(
  raw : Array[Double],
  calibration : SensorCalibration,
) -> Array[Double] {
  let result = []
  for value in raw {
    let calibrated = calibrate_value(value, calibration)
    if calibrated >= calibration.minimum_rr &&
      calibrated <= calibration.maximum_rr {
      result.push(calibrated)
    }
  }
  result
}

///|
/// Calculate the audit report for one calibration pass.
pub fn audit_calibration(
  raw : Array[Double],
  calibration : SensorCalibration,
) -> CalibrationReport {
  let calibrated = []
  let mut changed = 0
  let mut rejected = 0
  let mut shift_sum = 0.0
  let mut max_shift = 0.0
  for value in raw {
    let converted = calibrate_value(value, calibration)
    let shift = absolute_difference(converted, value)
    if shift > 0.0 {
      changed += 1
    }
    shift_sum += shift
    if shift > max_shift {
      max_shift = shift
    }
    if converted >= calibration.minimum_rr &&
      converted <= calibration.maximum_rr {
      calibrated.push(converted)
    } else {
      rejected += 1
    }
  }
  let jitter = if calibration.timestamp_jitter_ms < 0.0 {
    0.0
  } else {
    calibration.timestamp_jitter_ms
  }
  {
    input_count: raw.length(),
    output_count: calibrated.length(),
    changed_count: changed,
    rejected_count: rejected,
    mean_shift_ms: if raw.length() == 0 {
      0.0
    } else {
      shift_sum / raw.length().to_double()
    },
    max_shift_ms: max_shift,
    jitter_rms_ms: jitter,
    passed: rejected == 0 && max_shift <= 100.0 && jitter <= 10.0,
  }
}

///|
/// Apply an offset correction estimated from a reference sequence.
pub fn estimate_offset(
  reference : Array[Double],
  observed : Array[Double],
) -> Double {
  let n = if reference.length() < observed.length() {
    reference.length()
  } else {
    observed.length()
  }
  if n == 0 {
    0.0
  } else {
    let differences = []
    for i in 0.. Double {
  let n = if reference.length() < observed.length() {
    reference.length()
  } else {
    observed.length()
  }
  if n == 0 {
    1.0
  } else {
    let ratios = []
    for i in 0.. Double {
  if resolution_ms <= 0.0 {
    value
  } else {
    (value / resolution_ms).round() * resolution_ms
  }
}

///|
/// Quantize an entire recording.
pub fn quantize_intervals(
  values : Array[Double],
  resolution_ms : Double,
) -> Array[Double] {
  let result = []
  for value in values {
    result.push(quantize_interval(value, resolution_ms))
  }
  result
}