///|
/// Readiness interpretation for daily recovery dashboards.
pub(all) enum ReadinessLevel {
  VeryLow
  Low
  Moderate
  High
  VeryHigh
} derive(FromJson, ToJson, Debug, Eq)

///|
/// A bounded readiness score with explainable components.
pub(all) struct ReadinessScore {
  score : Double
  level : ReadinessLevel
  baseline_z : Double
  trend_component : Double
  quality_component : Double
  explanation : String
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Daily training load summary using session duration and intensity.
pub(all) struct TrainingLoadSummary {
  total_minutes : Double
  total_load : Double
  average_intensity : Double
  monotony : Double
  strain : Double
  acute_load : Double
  chronic_load : Double
  acute_chronic_ratio : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Robust baseline statistics for a sequence of morning RMSSD values.
pub(all) struct RobustBaseline {
  center : Double
  spread : Double
  lower : Double
  upper : Double
  retained : Int
  rejected : Int
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Compute a median/MAD baseline with a configurable outlier fence.
pub fn calculate_robust_baseline(
  values : Array[Double],
  fence : Double,
  range_factor : Double,
) -> RobustBaseline {
  if values.length() == 0 {
    return {
      center: 0.0,
      spread: 0.0,
      lower: 0.0,
      upper: 0.0,
      retained: 0,
      rejected: 0,
    }
  }
  let center = median_value(values)
  let mad = median_absolute_deviation(values)
  let scale = if mad == 0.0 { standard_deviation(values) } else { mad * 1.4826 }
  let threshold = (if fence < 0.0 { 0.0 } else { fence }) * scale
  let retained_values = []
  for value in values {
    if absolute_difference(value, center) <= threshold || scale == 0.0 {
      retained_values.push(value)
    }
  }
  let final_center = median_value(retained_values)
  let final_spread = if retained_values.length() <= 1 {
    if final_center == 0.0 {
      0.0
    } else {
      final_center * 0.1
    }
  } else {
    standard_deviation(retained_values)
  }
  let factor = if range_factor < 0.0 { 0.0 } else { range_factor }
  {
    center: final_center,
    spread: final_spread,
    lower: final_center - factor * final_spread,
    upper: final_center + factor * final_spread,
    retained: retained_values.length(),
    rejected: values.length() - retained_values.length(),
  }
}

///|
/// Convert a score into an ordinal readiness level.
pub fn readiness_level(score : Double) -> ReadinessLevel {
  if score < 20.0 {
    VeryLow
  } else if score < 40.0 {
    Low
  } else if score < 60.0 {
    Moderate
  } else if score < 80.0 {
    High
  } else {
    VeryHigh
  }
}

///|
/// Calculate readiness from today's RMSSD, a baseline, and signal quality.
pub fn calculate_readiness(
  today_rmssd : Double,
  baseline : RobustBaseline,
  trend_slope : Double,
  quality_ratio : Double,
) -> ReadinessScore {
  let spread = if baseline.spread <= 0.0 { 1.0 } else { baseline.spread }
  let z = (today_rmssd - baseline.center) / spread
  let baseline_component = (50.0 + z * 15.0).clamp(min=0.0, max=100.0)
  let trend_component = (50.0 + trend_slope * 10.0).clamp(min=0.0, max=100.0)
  let quality_component = quality_ratio.clamp(min=0.0, max=1.0) * 100.0
  let score = (0.65 * baseline_component +
  0.20 * trend_component +
  0.15 * quality_component).clamp(min=0.0, max=100.0)
  let level = readiness_level(score)
  let explanation = if quality_ratio < 0.75 {
    "low signal quality reduces confidence"
  } else if z < -1.0 {
    "RMSSD is below the robust personal baseline"
  } else if z > 1.0 {
    "RMSSD is above the robust personal baseline"
  } else {
    "RMSSD is close to the robust personal baseline"
  }
  {
    score,
    level,
    baseline_z: z,
    trend_component,
    quality_component,
    explanation,
  }
}

///|
/// Calculate a session load as duration multiplied by normalized intensity.
pub fn session_training_load(
  duration_minutes : Double,
  intensity : Double,
) -> Double {
  let duration = if duration_minutes < 0.0 { 0.0 } else { duration_minutes }
  let effort = intensity.clamp(min=0.0, max=10.0)
  duration * effort
}

///|
/// Calculate monotony as mean daily load divided by its standard deviation.
pub fn calculate_monotony(daily_loads : Array[Double]) -> Double {
  let deviation = standard_deviation(daily_loads)
  if deviation == 0.0 {
    0.0
  } else {
    mean_value(daily_loads) / deviation
  }
}

///|
/// Calculate training strain as total load multiplied by monotony.
pub fn calculate_strain(daily_loads : Array[Double]) -> Double {
  sum_values(daily_loads) * calculate_monotony(daily_loads)
}

///|
/// Calculate acute/chronic load using recent and historical windows.
pub fn summarize_training_load(
  daily_loads : Array[Double],
  acute_days : Int,
  chronic_days : Int,
) -> TrainingLoadSummary {
  let n = daily_loads.length()
  let acute_start = if n > acute_days && acute_days > 0 {
    n - acute_days
  } else {
    0
  }
  let chronic_start = if n > chronic_days && chronic_days > 0 {
    n - chronic_days
  } else {
    0
  }
  let acute_values = []
  let chronic_values = []
  for i in acute_start.. Array[ReadinessScore] {
  let result = []
  for i in 0..