///|
/// Additional time-domain measures used by field and wearable recordings.
/// These helpers deliberately keep the original sample order and return
/// deterministic values for short recordings.

///|
/// Extended time-domain feature set.
pub(all) struct TimeDomainExtended {
  mean_rr : Double
  mean_hr : Double
  sdnn : Double
  rmssd : Double
  sdsd : Double
  cvnn : Double
  pnn20 : Double
  pnn50 : Double
  median_rr : Double
  iqr_rr : Double
  min_rr : Double
  max_rr : Double
  range_rr : Double
  mad_rr : Double
  triangular_index : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Calculate the standard deviation of successive differences.
pub fn calculate_sdsd(intervals : Array[Double]) -> Double {
  if intervals.length() <= 2 {
    return 0.0
  }
  let differences = []
  for i in 0..<(intervals.length() - 1) {
    differences.push(intervals[i + 1] - intervals[i])
  }
  standard_deviation(differences)
}

///|
/// Calculate coefficient of variation of NN intervals in percent.
pub fn calculate_cvnn(intervals : Array[Double]) -> Double {
  let mean = mean_value(intervals)
  if mean == 0.0 {
    0.0
  } else {
    standard_deviation(intervals) / mean * 100.0
  }
}

///|
/// Calculate the median absolute successive difference.
pub fn median_successive_difference(intervals : Array[Double]) -> Double {
  if intervals.length() <= 1 {
    return 0.0
  }
  let differences = []
  for i in 0..<(intervals.length() - 1) {
    differences.push(absolute_difference(intervals[i + 1], intervals[i]))
  }
  median_value(differences)
}

///|
/// Calculate a robust pNN metric using an arbitrary threshold.
pub fn calculate_pnn_fraction(
  intervals : Array[Double],
  threshold_ms : Double,
) -> Double {
  if intervals.length() <= 1 {
    return 0.0
  }
  let threshold = if threshold_ms < 0.0 { 0.0 } else { threshold_ms }
  let mut count = 0
  for i in 0..<(intervals.length() - 1) {
    if absolute_difference(intervals[i + 1], intervals[i]) > threshold {
      count += 1
    }
  }
  count.to_double() / (intervals.length() - 1).to_double()
}

///|
/// Return an extended summary for a cleaned RR sequence.
pub fn summarize_time_domain_extended(
  intervals : Array[Double],
) -> TimeDomainExtended {
  let distribution = summarize_distribution(intervals)
  {
    mean_rr: mean_value(intervals),
    mean_hr: if mean_value(intervals) > 0.0 {
      60000.0 / mean_value(intervals)
    } else {
      0.0
    },
    sdnn: standard_deviation(intervals),
    rmssd: calculate_rmssd(intervals),
    sdsd: calculate_sdsd(intervals),
    cvnn: calculate_cvnn(intervals),
    pnn20: calculate_pnn(intervals, 20.0),
    pnn50: calculate_pnn(intervals, 50.0),
    median_rr: distribution.median,
    iqr_rr: distribution.q3 - distribution.q1,
    min_rr: distribution.minimum,
    max_rr: distribution.maximum,
    range_rr: distribution.maximum - distribution.minimum,
    mad_rr: distribution.median_absolute_deviation,
    triangular_index: calculate_triangular_index(intervals),
  }
}

///|
/// Return absolute beat-to-beat changes for a recording.
pub fn successive_differences(intervals : Array[Double]) -> Array[Double] {
  if intervals.length() <= 1 {
    return []
  }
  let result = []
  for i in 0..<(intervals.length() - 1) {
    result.push(intervals[i + 1] - intervals[i])
  }
  result
}

///|
/// Return absolute successive changes, useful for artifact dashboards.
pub fn absolute_successive_differences(
  intervals : Array[Double],
) -> Array[Double] {
  let result = []
  for value in successive_differences(intervals) {
    result.push(value.abs())
  }
  result
}

///|
/// Count intervals whose local change exceeds a threshold.
pub fn count_large_successive_changes(
  intervals : Array[Double],
  threshold_ms : Double,
) -> Int {
  let mut count = 0
  let threshold = if threshold_ms < 0.0 { 0.0 } else { threshold_ms }
  for change in absolute_successive_differences(intervals) {
    if change > threshold {
      count += 1
    }
  }
  count
}

///|
/// Calculate a short-window RMSSD for every complete window.
pub fn rolling_rmssd_with_stride(
  intervals : Array[Double],
  window_size : Int,
  stride : Int,
) -> Array[Double] {
  if window_size <= 1 || stride <= 0 || intervals.length() < window_size {
    return []
  }
  let result = []
  let mut start = 0
  while start + window_size <= intervals.length() {
    let window = []
    for i in start..<(start + window_size) {
      window.push(intervals[i])
    }
    result.push(calculate_rmssd(window))
    start += stride
  }
  result
}

///|
/// Calculate a robust moving median for display or denoising.
pub fn rolling_median(
  intervals : Array[Double],
  window_size : Int,
) -> Array[Double] {
  if window_size <= 0 {
    return []
  }
  let result = []
  for i in 0.. intervals.length() {
      intervals.length()
    } else {
      start + window_size
    }
    let window = []
    for j in start.. Array[Double] {
  let baseline = rolling_median(intervals, window_size)
  let result = []
  let limit = if intervals.length() < baseline.length() {
    intervals.length()
  } else {
    baseline.length()
  }
  for i in 0.. Array[Double] {
  let summary = summarize_time_domain_extended(intervals)
  [
    summary.mean_rr,
    summary.mean_hr,
    summary.sdnn,
    summary.rmssd,
    summary.sdsd,
    summary.cvnn,
    summary.pnn20,
    summary.pnn50,
    summary.median_rr,
    summary.iqr_rr,
    summary.min_rr,
    summary.max_rr,
    summary.range_rr,
    summary.mad_rr,
    summary.triangular_index,
  ]
}

///|
/// Return a stable list of feature names matching time_domain_feature_vector.
pub fn time_domain_feature_names() -> Array[String] {
  [
    "mean_rr", "mean_hr", "sdnn", "rmssd", "sdsd", "cvnn", "pnn20", "pnn50", "median_rr",
    "iqr_rr", "min_rr", "max_rr", "range_rr", "mad_rr", "triangular_index",
  ]
}

///|
/// Return a threshold-based quality score for successive changes.
pub fn successive_change_quality(
  intervals : Array[Double],
  threshold_ms : Double,
) -> Double {
  if intervals.length() <= 1 {
    return 0.0
  }
  let total = (intervals.length() - 1).to_double()
  1.0 -
  count_large_successive_changes(intervals, threshold_ms).to_double() / total
}

///|
/// Return whether an extended summary is numerically usable.
pub fn time_domain_is_usable(summary : TimeDomainExtended) -> Bool {
  summary.mean_rr > 0.0 &&
  summary.mean_hr > 0.0 &&
  summary.sdnn >= 0.0 &&
  summary.rmssd >= 0.0
}