///|
/// A training session aligned with a morning recovery measurement.
pub(all) struct WorkoutSession {
  date : String
  duration_minutes : Double
  intensity : Double
  rmssd_before : Double
  rmssd_after : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Daily training and recovery aggregates.
pub(all) struct TrainingDaySummary {
  date : String
  session_count : Int
  duration_minutes : Double
  total_load : Double
  average_intensity : Double
  rmssd_change : Double
  recovery_cost : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Longitudinal training-load indicators.
pub(all) struct TrainingPlanMetrics {
  days : Array[TrainingDaySummary]
  total_load : Double
  average_daily_load : Double
  load_trend : Double
  monotony : Double
  strain : Double
  recovery_cost : Double
  high_load_days : Int
  rest_days : Int
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Calculate the load of one session.
pub fn workout_load(session : WorkoutSession) -> Double {
  session_training_load(session.duration_minutes, session.intensity)
}

///|
/// Group sessions by their ISO date while preserving input order.
pub fn summarize_training_days(
  sessions : Array[WorkoutSession],
) -> Array[TrainingDaySummary] {
  let result = []
  for session in sessions {
    let existing = sessions_date_index(result, session.date)
    let load = workout_load(session)
    let recovery_cost = (session.rmssd_before - session.rmssd_after).clamp(
      min=0.0,
      max=10000.0,
    )
    match existing {
      Some(index) => {
        let current = result[index]
        let count = current.session_count + 1
        result[index] = {
          date: current.date,
          session_count: count,
          duration_minutes: current.duration_minutes + session.duration_minutes,
          total_load: current.total_load + load,
          average_intensity: (
            current.average_intensity * current.session_count.to_double() +
            session.intensity
          ) /
          count.to_double(),
          rmssd_change: current.rmssd_change +
          session.rmssd_after -
          session.rmssd_before,
          recovery_cost: current.recovery_cost + recovery_cost,
        }
      }
      None =>
        result.push({
          date: session.date,
          session_count: 1,
          duration_minutes: session.duration_minutes,
          total_load: load,
          average_intensity: session.intensity,
          rmssd_change: session.rmssd_after - session.rmssd_before,
          recovery_cost,
        })
    }
  }
  result
}

///|
/// Find a date in a training summary array.
fn sessions_date_index(days : Array[TrainingDaySummary], date : String) -> Int? {
  for i in 0.. TrainingPlanMetrics {
  let days = summarize_training_days(sessions)
  let loads = []
  let mut recovery_cost = 0.0
  let mut high_load_days = 0
  for day in days {
    loads.push(day.total_load)
    recovery_cost += day.recovery_cost
    if day.total_load >= 300.0 {
      high_load_days += 1
    }
  }
  let rest_days = if days.length() == 0 {
    0
  } else {
    days.length() - high_load_days
  }
  {
    days,
    total_load: sum_values(loads),
    average_daily_load: mean_value(loads),
    load_trend: fit_linear_trend(loads).slope,
    monotony: calculate_monotony(loads),
    strain: calculate_strain(loads),
    recovery_cost,
    high_load_days,
    rest_days,
  }
}

///|
/// Recommend a recovery day when load and recent recovery cost are high.
pub fn recommend_recovery_day(metrics : TrainingPlanMetrics) -> Bool {
  metrics.strain > 1000.0 ||
  metrics.recovery_cost > 50.0 ||
  metrics.load_trend > 50.0
}

///|
/// Return the dates with the largest training loads.
pub fn peak_training_days(
  metrics : TrainingPlanMetrics,
  limit : Int,
) -> Array[TrainingDaySummary] {
  let result = []
  for day in metrics.days {
    result.push(day)
  }
  result.sort_by((left, right) => {
    if left.total_load > right.total_load {
      -1
    } else if left.total_load < right.total_load {
      1
    } else {
      0
    }
  })
  if limit >= 0 && result.length() > limit {
    result.truncate(limit)
  }
  result
}

///|
/// Calculate a seven-day load ratio from a session list.
pub fn acute_chronic_workout_ratio(
  sessions : Array[WorkoutSession],
  date_index : Int,
) -> Double {
  let days = summarize_training_days(sessions)
  let loads = []
  for day in days {
    loads.push(day.total_load)
  }
  let acute_start = if date_index > 7 { date_index - 7 } else { 0 }
  let chronic_start = if date_index > 28 { date_index - 28 } else { 0 }
  let acute = []
  let chronic = []
  for i in acute_start..