///|
/// Artifact-quality primitives for ambulatory RR streams.

///|
/// Reason assigned to a suspicious sample.
pub(all) enum ArtifactReason {
  None
  PhysiologicalRange
  LocalOutlier
  SuddenJump
  RepeatedValue
  MissingValue
} derive(FromJson, ToJson, Debug, Eq)

///|
/// One sample-level quality flag with a normalized severity.
pub(all) struct QualityFlag {
  index : Int
  value : Double
  reason : ArtifactReason
  severity : Double
  replacement : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Quality profile of an RR stream.
pub(all) struct ArtifactProfile {
  sample_count : Int
  flagged_count : Int
  range_violations : Int
  local_outliers : Int
  sudden_jumps : Int
  repeated_values : Int
  missing_values : Int
  artifact_ratio : Double
  median_step : Double
  mad_step : Double
  flags : Array[QualityFlag]
} derive(FromJson, ToJson, Debug, Eq)

///|
fn artifact_median_step(intervals : Array[Double]) -> Double {
  median_value(absolute_successive_differences(intervals))
}

///|
fn artifact_mad_step(intervals : Array[Double]) -> Double {
  median_absolute_deviation(absolute_successive_differences(intervals))
}

///|
fn artifact_scale(intervals : Array[Double]) -> Double {
  let mad = artifact_mad_step(intervals)
  if mad < 1.0 {
    1.0
  } else {
    mad
  }
}

///|
fn nearest_replacement(
  intervals : Array[Double],
  index : Int,
  config : HrvConfig,
) -> Double {
  let left = if index > 0 { Some(intervals[index - 1]) } else { None }
  let right = if index + 1 < intervals.length() {
    Some(intervals[index + 1])
  } else {
    None
  }
  match (left, right) {
    (Some(l), Some(r)) =>
      if classify_interval(l, config) is Normal &&
        classify_interval(r, config) is Normal {
        (l + r) / 2.0
      } else {
        interpolate_artifact(intervals, index, config)
      }
    _ => interpolate_artifact(intervals, index, config)
  }
}

///|
fn artifact_reason_for(
  intervals : Array[Double],
  index : Int,
  config : HrvConfig,
  step_median : Double,
  step_scale : Double,
) -> (ArtifactReason, Double) {
  let value = intervals[index]
  match classify_interval(value, config) {
    NonFinite => (MissingValue, 1.0)
    TooShort | TooLong => (PhysiologicalRange, 1.0)
    Normal => {
      let mut reason = None
      let mut severity = 0.0
      if index > 0 {
        let step = absolute_difference(value, intervals[index - 1])
        let robust_limit = step_median + 5.0 * step_scale
        if step > robust_limit && step > config.relative_threshold * value {
          reason = SuddenJump
          severity = (step / (robust_limit + 1.0)).clamp(min=0.0, max=1.0)
        }
        if value == intervals[index - 1] {
          reason = RepeatedValue
          severity = 0.25
        }
      }
      if reason is None && index > 0 && index + 1 < intervals.length() {
        let local_midpoint = (intervals[index - 1] + intervals[index + 1]) / 2.0
        let distance = absolute_difference(value, local_midpoint)
        let limit = step_median + 4.0 * step_scale
        if distance > limit {
          reason = LocalOutlier
          severity = (distance / (limit + 1.0)).clamp(min=0.0, max=1.0)
        }
      }
      (reason, severity)
    }
  }
}

///|
/// Profile artifacts with robust step-based and physiological checks.
pub fn profile_artifacts(
  intervals : Array[Double],
  config : HrvConfig,
) -> ArtifactProfile {
  let step_median = artifact_median_step(intervals)
  let step_scale = artifact_scale(intervals)
  let flags = []
  let mut range_violations = 0
  let mut local_outliers = 0
  let mut sudden_jumps = 0
  let mut repeated_values = 0
  let mut missing_values = 0
  for i in 0.. range_violations += 1
        LocalOutlier => local_outliers += 1
        SuddenJump => sudden_jumps += 1
        RepeatedValue => repeated_values += 1
        MissingValue => missing_values += 1
        None => ()
      }
      let replacement = if reason is RepeatedValue {
        intervals[i]
      } else {
        nearest_replacement(intervals, i, config)
      }
      flags.push({
        index: i,
        value: intervals[i],
        reason,
        severity,
        replacement,
      })
    }
  }
  let flagged = flags.length()
  {
    sample_count: intervals.length(),
    flagged_count: flagged,
    range_violations,
    local_outliers,
    sudden_jumps,
    repeated_values,
    missing_values,
    artifact_ratio: if intervals.length() == 0 {
      0.0
    } else {
      flagged.to_double() / intervals.length().to_double()
    },
    median_step: step_median,
    mad_step: step_scale,
    flags,
  }
}

///|
/// Repair only the flagged samples and preserve the original length.
pub fn repair_profiled_artifacts(
  intervals : Array[Double],
  profile : ArtifactProfile,
) -> Array[Double] {
  let result = []
  for value in intervals {
    result.push(value)
  }
  for flag in profile.flags {
    if flag.index >= 0 &&
      flag.index < result.length() &&
      flag.reason != RepeatedValue {
      result[flag.index] = flag.replacement
    }
  }
  result
}

///|
/// Calculate a quality score that penalizes severe and repeated artifacts.
pub fn artifact_quality_score(profile : ArtifactProfile) -> Double {
  if profile.sample_count == 0 {
    return 0.0
  }
  let severity_sum = sum_values(profile.flags.map(fn(flag) { flag.severity }))
  (1.0 - severity_sum / profile.sample_count.to_double()).clamp(
    min=0.0,
    max=1.0,
  )
}

///|
/// Return the proportion of the stream retained after removing flags.
pub fn retained_ratio(profile : ArtifactProfile) -> Double {
  if profile.sample_count == 0 {
    0.0
  } else {
    (profile.sample_count - profile.flagged_count).to_double() /
    profile.sample_count.to_double()
  }
}

///|
/// Group contiguous flags into artifact runs.
pub fn artifact_runs(profile : ArtifactProfile) -> Array[SegmentRange] {
  let result = []
  if profile.flags.length() == 0 {
    return result
  }
  let mut start = profile.flags[0].index
  let mut previous = start
  for i in 1.. Int {
  if profile.sample_count == 0 {
    return 0
  }
  let runs = artifact_runs(profile)
  if runs.length() == 0 {
    return profile.sample_count
  }
  let mut longest = 0
  let mut cursor = 0
  for run in runs {
    let length = run.start - cursor
    if length > longest {
      longest = length
    }
    cursor = run.end
  }
  let tail = profile.sample_count - cursor
  if tail > longest {
    longest = tail
  }
  longest
}

///|
/// Create a stable artifact feature vector for model input.
pub fn artifact_feature_vector(profile : ArtifactProfile) -> Array[Double] {
  [
    profile.sample_count.to_double(),
    profile.flagged_count.to_double(),
    profile.artifact_ratio,
    profile.range_violations.to_double(),
    profile.local_outliers.to_double(),
    profile.sudden_jumps.to_double(),
    profile.repeated_values.to_double(),
    profile.missing_values.to_double(),
    profile.median_step,
    profile.mad_step,
    artifact_quality_score(profile),
    retained_ratio(profile),
    longest_clean_run(profile).to_double(),
  ]
}

///|
/// Check that all replacements are within the configured physiological range.
pub fn replacements_are_plausible(
  profile : ArtifactProfile,
  config : HrvConfig,
) -> Bool {
  for flag in profile.flags {
    if classify_interval(flag.replacement, config) != Normal {
      return false
    }
  }
  true
}