///|
/// Longitudinal session analytics for personal HRV tracking.

///|
/// One analyzed session with quality metadata.
pub(all) struct SessionObservation {
  session_id : String
  date : String
  duration_minutes : Double
  rmssd : Double
  sdnn : Double
  mean_hr : Double
  quality_score : Double
  training_load : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Aggregated session analytics.
pub(all) struct SessionAnalytics {
  count : Int
  usable_count : Int
  mean_rmssd : Double
  median_rmssd : Double
  rmssd_trend : Double
  rmssd_volatility : Double
  mean_hr : Double
  mean_quality : Double
  total_load : Double
  load_trend : Double
  best_session_id : String
  worst_session_id : String
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Per-session status relative to a rolling baseline.
pub(all) struct SessionStatus {
  session_id : String
  baseline_rmssd : Double
  rmssd_z : Double
  load : Double
  quality_score : Double
  recovery_label : String
  should_review : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Return whether an observation is usable for longitudinal scoring.
pub fn session_is_usable(
  observation : SessionObservation,
  minimum_quality : Double,
) -> Bool {
  observation.rmssd > 0.0 &&
  observation.mean_hr > 0.0 &&
  observation.quality_score >= minimum_quality
}

///|
/// Calculate a robust baseline over the preceding sessions.
pub fn session_baseline(
  observations : Array[SessionObservation],
  end_exclusive : Int,
  window_size : Int,
) -> (Double, Double) {
  let end = if end_exclusive < 0 {
    0
  } else if end_exclusive > observations.length() {
    observations.length()
  } else {
    end_exclusive
  }
  let window = if window_size <= 0 {
    end
  } else if end < window_size {
    end
  } else {
    window_size
  }
  let start = end - window
  let values = []
  for i in start.. 0.0 {
      values.push(observations[i].rmssd)
    }
  }
  (median_value(values), median_absolute_deviation(values) * 1.4826)
}

///|
/// Calculate a baseline-relative session status.
pub fn classify_session_status(
  observation : SessionObservation,
  baseline_rmssd : Double,
  baseline_scale : Double,
  load_threshold : Double,
) -> SessionStatus {
  let scale = if baseline_scale <= 1.0 { 1.0 } else { baseline_scale }
  let z = if baseline_rmssd == 0.0 {
    0.0
  } else {
    (observation.rmssd - baseline_rmssd) / scale
  }
  let label = if observation.quality_score < 0.55 {
    "low_quality"
  } else if z <= -1.5 {
    "below_baseline"
  } else if z >= 1.5 {
    "above_baseline"
  } else {
    "within_baseline"
  }
  {
    session_id: observation.session_id,
    baseline_rmssd,
    rmssd_z: z,
    load: observation.training_load,
    quality_score: observation.quality_score,
    recovery_label: label,
    should_review: observation.quality_score < 0.55 ||
    z <= -1.5 ||
    observation.training_load >= load_threshold,
  }
}

///|
/// Create statuses for all observations using a trailing baseline.
pub fn classify_session_history(
  observations : Array[SessionObservation],
  window_size : Int,
  load_threshold : Double,
) -> Array[SessionStatus] {
  let result = []
  for i in 0.. SessionAnalytics {
  let rmssd = []
  let hr = []
  let quality = []
  let loads = []
  let mut usable = 0
  let mut best = ""
  let mut worst = ""
  let mut best_value = -1.0
  let mut worst_value = 1.0e18
  for observation in observations {
    rmssd.push(observation.rmssd)
    hr.push(observation.mean_hr)
    quality.push(observation.quality_score)
    loads.push(observation.training_load)
    if session_is_usable(observation, minimum_quality) {
      usable += 1
    }
    if observation.rmssd > best_value {
      best_value = observation.rmssd
      best = observation.session_id
    }
    if observation.rmssd < worst_value {
      worst_value = observation.rmssd
      worst = observation.session_id
    }
  }
  {
    count: observations.length(),
    usable_count: usable,
    mean_rmssd: mean_value(rmssd),
    median_rmssd: median_value(rmssd),
    rmssd_trend: fit_linear_trend(rmssd).slope,
    rmssd_volatility: standard_deviation(rmssd),
    mean_hr: mean_value(hr),
    mean_quality: mean_value(quality),
    total_load: sum_values(loads),
    load_trend: fit_linear_trend(loads).slope,
    best_session_id: best,
    worst_session_id: worst,
  }
}

///|
/// Return indices of sessions that differ materially from a baseline.
pub fn session_outlier_indices(
  observations : Array[SessionObservation],
  z_threshold : Double,
  window_size : Int,
) -> Array[Int] {
  let result = []
  let threshold = if z_threshold < 0.0 { 0.0 } else { z_threshold }
  for i in 0.. 0.0 &&
      absolute_difference(observations[i].rmssd, baseline) / scale >= threshold {
      result.push(i)
    }
  }
  result
}

///|
/// Calculate a load-adjusted recovery score.
pub fn load_adjusted_recovery(status : SessionStatus) -> Double {
  let recovery = (50.0 + status.rmssd_z * 15.0).clamp(min=0.0, max=100.0)
  let load_penalty = (status.load / 100.0).clamp(min=0.0, max=30.0)
  (recovery - load_penalty).clamp(min=0.0, max=100.0)
}

///|
/// Return a sorted copy from highest recovery score to lowest.
pub fn rank_sessions_by_recovery(
  observations : Array[SessionObservation],
  window_size : Int,
  load_threshold : Double,
) -> Array[SessionStatus] {
  let statuses = classify_session_history(
    observations, window_size, load_threshold,
  )
  statuses.sort_by((left, right) => {
    let l = load_adjusted_recovery(left)
    let r = load_adjusted_recovery(right)
    if l > r {
      -1
    } else if l < r {
      1
    } else {
      0
    }
  })
  statuses
}

///|
/// Calculate a workload-to-recovery correlation.
pub fn load_recovery_correlation(
  observations : Array[SessionObservation],
) -> Double {
  let loads = []
  let recovery = []
  for observation in observations {
    loads.push(observation.training_load)
    recovery.push(observation.rmssd)
  }
  correlation_value(loads, recovery)
}

///|
/// Return the proportion of sessions that need review.
pub fn session_review_ratio(statuses : Array[SessionStatus]) -> Double {
  if statuses.length() == 0 {
    return 0.0
  }
  let mut count = 0
  for status in statuses {
    if status.should_review {
      count += 1
    }
  }
  count.to_double() / statuses.length().to_double()
}

///|
/// Create a fixed-order longitudinal feature vector.
pub fn session_analytics_feature_vector(
  summary : SessionAnalytics,
) -> Array[Double] {
  [
    summary.count.to_double(),
    summary.usable_count.to_double(),
    summary.mean_rmssd,
    summary.median_rmssd,
    summary.rmssd_trend,
    summary.rmssd_volatility,
    summary.mean_hr,
    summary.mean_quality,
    summary.total_load,
    summary.load_trend,
  ]
}

///|
/// Return whether longitudinal analytics contain enough signal.
pub fn session_analytics_is_usable(summary : SessionAnalytics) -> Bool {
  summary.count > 0 &&
  summary.usable_count > 0 &&
  summary.mean_rmssd > 0.0 &&
  summary.mean_quality > 0.0
}