///|
/// Cleaning methods for RR intervals.
pub(all) enum CleaningMethod {
  Remove
  InterpolateLocalMedian
  InterpolateLinear
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Clean RR intervals and generate a quality report.
pub fn clean_rr_intervals(
  intervals : Array[Double],
  cleaning_method : CleaningMethod,
  config : HrvConfig,
) -> (Array[Double], QualityReport) {
  let n = intervals.length()
  let is_ectopic = Array::make(n, false)
  let mut ectopic_count = 0

  for i in 0.. config.max_rr {
      is_ectopic[i] = true
      ectopic_count += 1
    } else {
      match get_local_median(intervals, i, config.min_rr, config.max_rr) {
        Some(med) => {
          let diff = val - med
          let abs_diff = if diff < 0.0 { -diff } else { diff }
          if abs_diff / med > config.relative_threshold {
            is_ectopic[i] = true
            ectopic_count += 1
          }
        }
        None => ()
      }
    }
  }

  let clean_ratio = if n == 0 {
    0.0
  } else {
    (n - ectopic_count).to_double() / n.to_double()
  }
  let is_low_quality = clean_ratio < 0.85
  let quality : QualityReport = {
    total_beats: n,
    valid_beats: n - ectopic_count,
    ectopic_beats: ectopic_count,
    clean_ratio,
    is_low_quality,
  }

  match cleaning_method {
    Remove => {
      let result = []
      for i in 0.. {
      let result = Array::make(n, 0.0)
      for i in 0.. result[i] = med
            None => result[i] = find_nearest_valid(intervals, is_ectopic, i)
          }
        }
      }
      (result, quality)
    }
    InterpolateLinear => {
      let result = Array::make(n, 0.0)
      for i in 0.. {
              let fraction = (i - l_idx).to_double() /
                (r_idx - l_idx).to_double()
              result[i] = l_val + fraction * (r_val - l_val)
            }
            (Some((_, l_val)), None) => result[i] = l_val
            (None, Some((_, r_val))) => result[i] = r_val
            (None, None) => result[i] = 1000.0
          }
        }
      }
      (result, quality)
    }
  }
}

///|
fn get_local_median(
  intervals : Array[Double],
  index : Int,
  min_rr : Double,
  max_rr : Double,
) -> Double? {
  let window = []
  for offset in -2..<=2 {
    let idx = index + offset
    if idx >= 0 && idx < intervals.length() {
      let val = intervals[idx]
      if val >= min_rr && val <= max_rr {
        window.push(val)
      }
    }
  }
  if window.length() > 0 {
    window.sort()
    let mid = window.length() / 2
    if window.length() % 2 == 1 {
      Some(window[mid])
    } else {
      Some((window[mid - 1] + window[mid]) / 2.0)
    }
  } else {
    None
  }
}

///|
fn find_nearest_valid(
  intervals : Array[Double],
  is_ectopic : Array[Bool],
  index : Int,
) -> Double {
  let mut left = index - 1
  let mut right = index + 1
  let n = intervals.length()
  for ;; {
    if left >= 0 && !is_ectopic[left] {
      break intervals[left]
    }
    if right < n && !is_ectopic[right] {
      break intervals[right]
    }
    if left < 0 && right >= n {
      break 1000.0
    }
    left -= 1
    right += 1
  }
}

///|
fn find_left_valid(
  intervals : Array[Double],
  is_ectopic : Array[Bool],
  index : Int,
) -> (Int, Double)? {
  let mut i = index - 1
  while i >= 0 {
    if !is_ectopic[i] {
      return Some((i, intervals[i]))
    }
    i -= 1
  }
  None
}

///|
fn find_right_valid(
  intervals : Array[Double],
  is_ectopic : Array[Bool],
  index : Int,
) -> (Int, Double)? {
  let mut i = index + 1
  let n = intervals.length()
  while i < n {
    if !is_ectopic[i] {
      return Some((i, intervals[i]))
    }
    i += 1
  }
  None
}