///|
/// Longitudinal recovery records and robust baselines for real-world use.
/// Missing or low-quality mornings remain visible in the audit trail instead
/// of being silently converted into zero-valued physiology.
pub(all) enum RecoveryTrajectory {
  RecoveryImproving
  RecoveryStable
  RecoveryWorsening
  RecoveryInsufficient
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) enum RecoveryDayStatus {
  RecoveryReady
  RecoveryModerate
  RecoveryStrained
  RecoveryUnavailable
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct RecoveryDayRecord {
  date : String
  mean_rr_ms : Double
  rmssd_ms : Double
  sdnn_ms : Double
  resting_hr_bpm : Double
  sleep_hours : Double
  sleep_efficiency : Double
  respiratory_rate : Double
  training_load : Double
  signal_quality : Double
  source : String
  missing : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct RecoveryBaseline {
  sample_count : Int
  mean_rr : Double
  median_rr : Double
  mad_rr : Double
  median_rmssd : Double
  mad_rmssd : Double
  median_resting_hr : Double
  mad_resting_hr : Double
  median_sleep_hours : Double
  quality_ratio : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct RecoveryDayAssessment {
  record : RecoveryDayRecord
  baseline : RecoveryBaseline
  rr_z : Double
  rmssd_z : Double
  heart_rate_z : Double
  sleep_z : Double
  readiness_score : Double
  status : RecoveryDayStatus
  confidence : Double
  reasons : Array[String]
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct RecoveryWeekSummary {
  week_index : Int
  start_date : String
  end_date : String
  record_count : Int
  usable_count : Int
  average_readiness : Double
  minimum_readiness : Double
  average_rmssd : Double
  average_sleep_hours : Double
  total_training_load : Double
  quality_ratio : Double
  missing_count : Int
  mut trajectory : RecoveryTrajectory
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct LongitudinalRecoveryReport {
  records : Array[RecoveryDayRecord]
  assessments : Array[RecoveryDayAssessment]
  baseline : RecoveryBaseline
  trajectory : RecoveryTrajectory
  current_score : Double
  current_status : RecoveryDayStatus
  missing_days : Int
  quality_ratio : Double
  stable_streak : Int
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct RecoveryTrendPoint {
  date : String
  score : Double
  rmssd : Double
  resting_hr : Double
  load : Double
  status : RecoveryDayStatus
} derive(FromJson, ToJson, Debug, Eq)

///|
fn recovery_safe(value : Double, fallback : Double) -> Double {
  if value.is_nan() || value.is_inf() {
    fallback
  } else {
    value
  }
}

///|
fn recovery_bound(value : Double, low : Double, high : Double) -> Double {
  recovery_safe(value, low).clamp(min=low, max=high)
}

///|
pub fn recovery_trajectory_name(value : RecoveryTrajectory) -> String {
  match value {
    RecoveryImproving => "improving"
    RecoveryStable => "stable"
    RecoveryWorsening => "worsening"
    RecoveryInsufficient => "insufficient"
  }
}

///|
pub fn recovery_status_name(value : RecoveryDayStatus) -> String {
  match value {
    RecoveryReady => "ready"
    RecoveryModerate => "moderate"
    RecoveryStrained => "strained"
    RecoveryUnavailable => "unavailable"
  }
}

///|
pub fn make_recovery_day_record(
  date : String,
  mean_rr_ms : Double,
  rmssd_ms : Double,
  sdnn_ms : Double,
  resting_hr_bpm : Double,
  sleep_hours : Double,
  sleep_efficiency : Double,
  respiratory_rate : Double,
  training_load : Double,
  signal_quality : Double,
  source : String,
) -> RecoveryDayRecord {
  {
    date,
    mean_rr_ms: recovery_bound(mean_rr_ms, 0.0, 3000.0),
    rmssd_ms: recovery_bound(rmssd_ms, 0.0, 1000.0),
    sdnn_ms: recovery_bound(sdnn_ms, 0.0, 2000.0),
    resting_hr_bpm: recovery_bound(resting_hr_bpm, 0.0, 240.0),
    sleep_hours: recovery_bound(sleep_hours, 0.0, 24.0),
    sleep_efficiency: recovery_bound(sleep_efficiency, 0.0, 1.0),
    respiratory_rate: recovery_bound(respiratory_rate, 0.0, 80.0),
    training_load: recovery_bound(training_load, 0.0, 100000.0),
    signal_quality: recovery_bound(signal_quality, 0.0, 1.0),
    source,
    missing: false,
  }
}

///|
pub fn missing_recovery_day(date : String) -> RecoveryDayRecord {
  {
    date,
    mean_rr_ms: 0.0,
    rmssd_ms: 0.0,
    sdnn_ms: 0.0,
    resting_hr_bpm: 0.0,
    sleep_hours: 0.0,
    sleep_efficiency: 0.0,
    respiratory_rate: 0.0,
    training_load: 0.0,
    signal_quality: 0.0,
    source: "missing",
    missing: true,
  }
}

///|
pub fn recovery_record_is_usable(record : RecoveryDayRecord) -> Bool {
  !record.missing &&
  record.date.length() > 0 &&
  record.mean_rr_ms > 0.0 &&
  record.rmssd_ms >= 0.0 &&
  record.resting_hr_bpm > 0.0 &&
  record.signal_quality > 0.0
}

///|
pub fn recovery_record_quality(record : RecoveryDayRecord) -> Double {
  if record.missing {
    0.0
  } else {
    let completeness = [
      record.mean_rr_ms > 0.0,
      record.rmssd_ms >= 0.0,
      record.resting_hr_bpm > 0.0,
      record.sleep_hours > 0.0,
      record.signal_quality > 0.0,
    ]
    let count = completeness.filter(value => value).length()
    record.signal_quality * 0.70 + count.to_double() / 5.0 * 0.30
  }
}

///|
fn recovery_copy_records(
  values : Array[RecoveryDayRecord],
) -> Array[RecoveryDayRecord] {
  let result = []
  for value in values {
    result.push(value)
  }
  result
}

///|
pub fn sort_recovery_records(
  values : Array[RecoveryDayRecord],
) -> Array[RecoveryDayRecord] {
  let result = recovery_copy_records(values)
  result.sort_by((left, right) => {
    if left.date < right.date {
      -1
    } else if left.date > right.date {
      1
    } else {
      0
    }
  })
  result
}

///|
fn recovery_valid_values(
  records : Array[RecoveryDayRecord],
  selector : (RecoveryDayRecord) -> Double,
) -> Array[Double] {
  let values = []
  for record in records {
    if recovery_record_is_usable(record) {
      values.push(selector(record))
    }
  }
  values
}

///|
pub fn build_recovery_baseline(
  records : Array[RecoveryDayRecord],
) -> RecoveryBaseline {
  let usable = records.filter(record => recovery_record_is_usable(record))
  let rr = recovery_valid_values(records, record => record.mean_rr_ms)
  let rmssd = recovery_valid_values(records, record => record.rmssd_ms)
  let heart = recovery_valid_values(records, record => record.resting_hr_bpm)
  let sleep = recovery_valid_values(records, record => record.sleep_hours)
  let qualities = records.map(record => recovery_record_quality(record))
  {
    sample_count: usable.length(),
    mean_rr: mean_value(rr),
    median_rr: median_value(rr),
    mad_rr: median_absolute_deviation(rr),
    median_rmssd: median_value(rmssd),
    mad_rmssd: median_absolute_deviation(rmssd),
    median_resting_hr: median_value(heart),
    mad_resting_hr: median_absolute_deviation(heart),
    median_sleep_hours: median_value(sleep),
    quality_ratio: mean_value(qualities),
  }
}

///|
fn recovery_robust_z(value : Double, center : Double, mad : Double) -> Double {
  if mad <= 0.000001 {
    if value > center {
      0.5
    } else if value < center {
      -0.5
    } else {
      0.0
    }
  } else {
    (value - center) / (1.4826 * mad)
  }
}

///|
pub fn recovery_rr_z(
  record : RecoveryDayRecord,
  baseline : RecoveryBaseline,
) -> Double {
  recovery_robust_z(record.mean_rr_ms, baseline.median_rr, baseline.mad_rr)
}

///|
pub fn recovery_rmssd_z(
  record : RecoveryDayRecord,
  baseline : RecoveryBaseline,
) -> Double {
  recovery_robust_z(record.rmssd_ms, baseline.median_rmssd, baseline.mad_rmssd)
}

///|
pub fn recovery_heart_rate_z(
  record : RecoveryDayRecord,
  baseline : RecoveryBaseline,
) -> Double {
  recovery_robust_z(
    record.resting_hr_bpm,
    baseline.median_resting_hr,
    baseline.mad_resting_hr,
  )
}

///|
pub fn recovery_sleep_z(
  record : RecoveryDayRecord,
  baseline : RecoveryBaseline,
) -> Double {
  recovery_robust_z(record.sleep_hours, baseline.median_sleep_hours, 0.5)
}

///|
fn recovery_score_component(value : Double, weight : Double) -> Double {
  (50.0 + value * 12.0).clamp(min=0.0, max=100.0) * weight
}

///|
pub fn calculate_recovery_score(
  record : RecoveryDayRecord,
  baseline : RecoveryBaseline,
) -> Double {
  if !recovery_record_is_usable(record) || baseline.sample_count == 0 {
    0.0
  } else {
    let rr = recovery_rr_z(record, baseline).clamp(min=-4.0, max=4.0)
    let rmssd = recovery_rmssd_z(record, baseline).clamp(min=-4.0, max=4.0)
    let heart = (-recovery_heart_rate_z(record, baseline)).clamp(
      min=-4.0,
      max=4.0,
    )
    let sleep = recovery_sleep_z(record, baseline).clamp(min=-4.0, max=4.0)
    let raw = recovery_score_component(rr, 0.30) +
      recovery_score_component(rmssd, 0.35) +
      recovery_score_component(heart, 0.20) +
      recovery_score_component(sleep, 0.15)
    raw * (0.65 + record.signal_quality * 0.35)
  }
}

///|
pub fn recovery_status_from_score(
  score : Double,
  record : RecoveryDayRecord,
) -> RecoveryDayStatus {
  if !recovery_record_is_usable(record) || score <= 0.0 {
    RecoveryUnavailable
  } else if score >= 70.0 {
    RecoveryReady
  } else if score >= 48.0 {
    RecoveryModerate
  } else {
    RecoveryStrained
  }
}

///|
pub fn recovery_confidence(
  record : RecoveryDayRecord,
  baseline : RecoveryBaseline,
) -> Double {
  if baseline.sample_count == 0 || record.missing {
    0.0
  } else {
    let sample_factor = (baseline.sample_count.to_double() / 14.0)
      .sqrt()
      .clamp(min=0.25, max=1.0)
    recovery_record_quality(record) * sample_factor
  }
}

///|
pub fn assess_recovery_day(
  record : RecoveryDayRecord,
  baseline : RecoveryBaseline,
) -> RecoveryDayAssessment {
  let score = calculate_recovery_score(record, baseline)
  let status = recovery_status_from_score(score, record)
  let confidence = recovery_confidence(record, baseline)
  let reasons = []
  if record.missing {
    reasons.push("record is missing")
  }
  if record.signal_quality < 0.70 {
    reasons.push("signal quality is below the preferred floor")
  }
  if recovery_rr_z(record, baseline) < -1.5 {
    reasons.push("mean RR is below baseline")
  }
  if recovery_rmssd_z(record, baseline) < -1.5 {
    reasons.push("RMSSD is below baseline")
  }
  if recovery_heart_rate_z(record, baseline) > 1.5 {
    reasons.push("resting heart rate is above baseline")
  }
  if record.sleep_hours > 0.0 && record.sleep_hours < 6.0 {
    reasons.push("sleep duration is short")
  }
  {
    record,
    baseline,
    rr_z: recovery_rr_z(record, baseline),
    rmssd_z: recovery_rmssd_z(record, baseline),
    heart_rate_z: recovery_heart_rate_z(record, baseline),
    sleep_z: recovery_sleep_z(record, baseline),
    readiness_score: score,
    status,
    confidence,
    reasons,
  }
}

///|
pub fn recovery_missing_days(records : Array[RecoveryDayRecord]) -> Int {
  records.filter(record => record.missing).length()
}

///|
pub fn recovery_quality_ratio(records : Array[RecoveryDayRecord]) -> Double {
  if records.length() == 0 {
    0.0
  } else {
    records
    .filter(record => recovery_record_is_usable(record))
    .length()
    .to_double() /
    records.length().to_double()
  }
}

///|
pub fn recovery_trajectory_from_scores(
  scores : Array[Double],
) -> RecoveryTrajectory {
  if scores.length() < 2 {
    RecoveryInsufficient
  } else {
    let slope = fit_linear_trend(scores).slope
    if slope > 1.5 {
      RecoveryImproving
    } else if slope < -1.5 {
      RecoveryWorsening
    } else {
      RecoveryStable
    }
  }
}

///|
pub fn recovery_trajectory_from_assessments(
  assessments : Array[RecoveryDayAssessment],
) -> RecoveryTrajectory {
  recovery_trajectory_from_scores(
    assessments
    .filter(item => item.status != RecoveryUnavailable)
    .map(item => item.readiness_score),
  )
}

///|
pub fn recovery_stable_streak(
  assessments : Array[RecoveryDayAssessment],
) -> Int {
  let mut streak = 0
  for i in 0.. LongitudinalRecoveryReport {
  let ordered = sort_recovery_records(records)
  let safe_window = baseline_window.clamp(min=1, max=ordered.length().max(1))
  let start = (ordered.length() - safe_window).max(0)
  let baseline_records = []
  for i in start.. Int {
  match (parse_date(first), parse_date(date)) {
    (Some(left), Some(right)) => (date_distance(left, right) / 7).max(0)
    _ => 0
  }
}

///|
fn recovery_bool_int(value : Bool) -> Int {
  if value {
    1
  } else {
    0
  }
}

///|
fn recovery_week_position(
  weeks : Array[RecoveryWeekSummary],
  index : Int,
) -> Int? {
  for i in 0.. Array[RecoveryWeekSummary] {
  let ordered = sort_recovery_records(records)
  if ordered.length() == 0 {
    return []
  }
  let report = build_longitudinal_recovery_report(ordered, baseline_window)
  let weeks : Array[RecoveryWeekSummary] = []
  for i in 0.. {
        let old = weeks[position]
        let count = old.record_count + 1
        let usable = old.usable_count +
          recovery_bool_int(recovery_record_is_usable(record))
        let missing = old.missing_count + recovery_bool_int(record.missing)
        weeks[position] = {
          week_index: index,
          start_date: old.start_date,
          end_date: record.date,
          record_count: count,
          usable_count: usable,
          average_readiness: (
            old.average_readiness * old.record_count.to_double() +
            assessment.readiness_score
          ) /
          count.to_double(),
          minimum_readiness: old.minimum_readiness.min(
            assessment.readiness_score,
          ),
          average_rmssd: (
            old.average_rmssd * old.record_count.to_double() + record.rmssd_ms
          ) /
          count.to_double(),
          average_sleep_hours: (
            old.average_sleep_hours * old.record_count.to_double() +
            record.sleep_hours
          ) /
          count.to_double(),
          total_training_load: old.total_training_load + record.training_load,
          quality_ratio: (
            old.quality_ratio * old.record_count.to_double() +
            record.signal_quality
          ) /
          count.to_double(),
          missing_count: missing,
          trajectory: old.trajectory,
        }
      }
      None =>
        weeks.push({
          week_index: index,
          start_date: record.date,
          end_date: record.date,
          record_count: 1,
          usable_count: recovery_bool_int(recovery_record_is_usable(record)),
          average_readiness: assessment.readiness_score,
          minimum_readiness: assessment.readiness_score,
          average_rmssd: record.rmssd_ms,
          average_sleep_hours: record.sleep_hours,
          total_training_load: record.training_load,
          quality_ratio: record.signal_quality,
          missing_count: recovery_bool_int(record.missing),
          trajectory: RecoveryStable,
        })
    }
  }
  weeks
}

///|
pub fn recovery_trend_points(
  report : LongitudinalRecoveryReport,
) -> Array[RecoveryTrendPoint] {
  let result = []
  for assessment in report.assessments {
    result.push({
      date: assessment.record.date,
      score: assessment.readiness_score,
      rmssd: assessment.record.rmssd_ms,
      resting_hr: assessment.record.resting_hr_bpm,
      load: assessment.record.training_load,
      status: assessment.status,
    })
  }
  result
}

///|
fn recovery_trajectory_value(value : RecoveryTrajectory) -> Double {
  match value {
    RecoveryImproving => 1.0
    RecoveryStable => 0.0
    RecoveryWorsening => -1.0
    RecoveryInsufficient => 0.0
  }
}

///|
pub fn recovery_report_feature_vector(
  report : LongitudinalRecoveryReport,
) -> Array[Double] {
  [
    report.current_score,
    report.baseline.mean_rr,
    report.baseline.median_rmssd,
    report.baseline.median_resting_hr,
    report.baseline.median_sleep_hours,
    report.baseline.quality_ratio,
    recovery_trajectory_value(report.trajectory),
    report.missing_days.to_double(),
    report.quality_ratio,
    report.stable_streak.to_double(),
    recovery_report_risk_score(report),
  ]
}

///|
pub fn recovery_report_csv(report : LongitudinalRecoveryReport) -> String {
  let grid = [
    [
      "date", "mean_rr_ms", "rmssd_ms", "resting_hr_bpm", "sleep_hours", "training_load",
      "quality", "score", "status", "confidence",
    ],
  ]
  for assessment in report.assessments {
    let record = assessment.record
    grid.push([
      record.date,
      record.mean_rr_ms.to_string(),
      record.rmssd_ms.to_string(),
      record.resting_hr_bpm.to_string(),
      record.sleep_hours.to_string(),
      record.training_load.to_string(),
      record.signal_quality.to_string(),
      assessment.readiness_score.to_string(),
      recovery_status_name(assessment.status),
      assessment.confidence.to_string(),
    ])
  }
  to_csv(grid)
}

///|
pub fn recovery_week_csv(weeks : Array[RecoveryWeekSummary]) -> String {
  let grid = [
    [
      "week_index", "start_date", "end_date", "record_count", "usable_count", "average_readiness",
      "minimum_readiness", "average_rmssd", "average_sleep", "training_load", "quality_ratio",
      "missing_count", "trajectory",
    ],
  ]
  for week in weeks {
    grid.push([
      week.week_index.to_string(),
      week.start_date,
      week.end_date,
      week.record_count.to_string(),
      week.usable_count.to_string(),
      week.average_readiness.to_string(),
      week.minimum_readiness.to_string(),
      week.average_rmssd.to_string(),
      week.average_sleep_hours.to_string(),
      week.total_training_load.to_string(),
      week.quality_ratio.to_string(),
      week.missing_count.to_string(),
      recovery_trajectory_name(week.trajectory),
    ])
  }
  to_csv(grid)
}

///|
pub fn recovery_compare_days(
  current : RecoveryDayRecord,
  previous : RecoveryDayRecord,
) -> Array[Double] {
  [
    current.mean_rr_ms - previous.mean_rr_ms,
    current.rmssd_ms - previous.rmssd_ms,
    current.sdnn_ms - previous.sdnn_ms,
    current.resting_hr_bpm - previous.resting_hr_bpm,
    current.sleep_hours - previous.sleep_hours,
    current.training_load - previous.training_load,
    current.signal_quality - previous.signal_quality,
  ]
}

///|
pub fn recovery_day_is_better(
  current : RecoveryDayAssessment,
  previous : RecoveryDayAssessment,
) -> Bool {
  current.readiness_score > previous.readiness_score &&
  current.confidence >= 0.30
}

///|
pub fn recovery_report_message(report : LongitudinalRecoveryReport) -> String {
  match report.current_status {
    RecoveryReady => "Recovery profile supports the planned session."
    RecoveryModerate =>
      "Recovery is moderate; keep intensity controlled and monitor symptoms."
    RecoveryStrained =>
      "Recovery is strained; prioritize low-intensity work or rest."
    RecoveryUnavailable =>
      "Recovery is unavailable because the recent signal is incomplete."
  }
}

///|
pub fn recovery_report_is_usable(report : LongitudinalRecoveryReport) -> Bool {
  report.assessments.length() > 0 &&
  report.baseline.sample_count > 0 &&
  report.current_status != RecoveryUnavailable
}

///|
pub fn recovery_report_risk_score(
  report : LongitudinalRecoveryReport,
) -> Double {
  if report.current_score <= 0.0 {
    1.0
  } else {
    (1.0 - report.current_score / 100.0).clamp(min=0.0, max=1.0) *
    (0.70 + (1.0 - report.quality_ratio) * 0.30)
  }
}

///|
pub fn recovery_report_load_adjustment(
  report : LongitudinalRecoveryReport,
) -> Double {
  match report.current_status {
    RecoveryReady => 1.0
    RecoveryModerate => 0.75
    RecoveryStrained => 0.45
    RecoveryUnavailable => 0.25
  }
}

///|
pub fn recovery_record_to_row(record : RecoveryDayRecord) -> Array[String] {
  [
    record.date,
    record.mean_rr_ms.to_string(),
    record.rmssd_ms.to_string(),
    record.sdnn_ms.to_string(),
    record.resting_hr_bpm.to_string(),
    record.sleep_hours.to_string(),
    record.sleep_efficiency.to_string(),
    record.respiratory_rate.to_string(),
    record.training_load.to_string(),
    record.signal_quality.to_string(),
    record.source,
    record.missing.to_string(),
  ]
}

///|
pub fn recovery_records_csv(records : Array[RecoveryDayRecord]) -> String {
  let grid = [
    [
      "date", "mean_rr_ms", "rmssd_ms", "sdnn_ms", "resting_hr_bpm", "sleep_hours",
      "sleep_efficiency", "respiratory_rate", "training_load", "signal_quality",
      "source", "missing",
    ],
  ]
  for record in records {
    grid.push(recovery_record_to_row(record))
  }
  to_csv(grid)
}

///|
pub fn recovery_recent_records(
  records : Array[RecoveryDayRecord],
  count : Int,
) -> Array[RecoveryDayRecord] {
  let ordered = sort_recovery_records(records)
  let safe_count = count.max(0)
  if ordered.length() <= safe_count {
    ordered
  } else {
    let result = []
    for i in (ordered.length() - safe_count).. Double {
  let values = assessments
    .filter(item => item.status != RecoveryUnavailable)
    .map(item => item.readiness_score)
  mean_value(values)
}

///|
pub fn recovery_score_sd(assessments : Array[RecoveryDayAssessment]) -> Double {
  let values = assessments
    .filter(item => item.status != RecoveryUnavailable)
    .map(item => item.readiness_score)
  standard_deviation(values)
}

///|
pub fn recovery_score_quantile(
  assessments : Array[RecoveryDayAssessment],
  proportion : Double,
) -> Double {
  let values = assessments
    .filter(item => item.status != RecoveryUnavailable)
    .map(item => item.readiness_score)
  quantile_value(values, proportion.clamp(min=0.0, max=1.0))
}

///|
pub fn recovery_best_day(
  assessments : Array[RecoveryDayAssessment],
) -> RecoveryDayAssessment? {
  if assessments.length() == 0 {
    None
  } else {
    let mut best = assessments[0]
    for item in assessments {
      if item.readiness_score > best.readiness_score {
        best = item
      }
    }
    Some(best)
  }
}

///|
pub fn recovery_worst_day(
  assessments : Array[RecoveryDayAssessment],
) -> RecoveryDayAssessment? {
  if assessments.length() == 0 {
    None
  } else {
    let mut worst = assessments[0]
    for item in assessments {
      if item.readiness_score < worst.readiness_score {
        worst = item
      }
    }
    Some(worst)
  }
}

///|
pub fn recovery_data_completeness(
  records : Array[RecoveryDayRecord],
) -> Array[Double] {
  if records.length() == 0 {
    [0.0, 0.0, 0.0, 0.0, 0.0]
  } else {
    [
      records.filter(record => record.mean_rr_ms > 0.0).length().to_double() /
      records.length().to_double(),
      records
      .filter(record => record.rmssd_ms >= 0.0 && !record.missing)
      .length()
      .to_double() /
      records.length().to_double(),
      records.filter(record => record.resting_hr_bpm > 0.0).length().to_double() /
      records.length().to_double(),
      records.filter(record => record.sleep_hours > 0.0).length().to_double() /
      records.length().to_double(),
      recovery_quality_ratio(records),
    ]
  }
}

///|
pub fn recovery_status_is_actionable(status : RecoveryDayStatus) -> Bool {
  status == RecoveryStrained || status == RecoveryUnavailable
}

///|
pub fn recovery_trajectory_is_positive(trajectory : RecoveryTrajectory) -> Bool {
  trajectory == RecoveryImproving || trajectory == RecoveryStable
}

///|
pub fn recovery_report_headline(report : LongitudinalRecoveryReport) -> String {
  "\{recovery_status_name(report.current_status)} / \{report.current_score.to_string()} / \{recovery_trajectory_name(report.trajectory)}"
}