///|
/// What-if planning for recovery-aware training decisions.
/// Scenarios are deterministic transformations of a baseline plan and never
/// mutate the caller's sessions, which makes them safe for UI previews.
pub(all) enum TrainingScenarioKind {
  ScenarioBaseline
  ScenarioRecoveryDay
  ScenarioEasyAerobic
  ScenarioReducedVolume
  ScenarioProgression
  ScenarioReturnToHard
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct ScenarioConfig {
  easy_intensity : Double
  recovery_intensity : Double
  volume_reduction : Double
  progression_limit : Double
  minimum_score_for_hard : Double
  forecast_weight : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
pub fn ScenarioConfig::default() -> ScenarioConfig {
  {
    easy_intensity: 0.45,
    recovery_intensity: 0.25,
    volume_reduction: 0.35,
    progression_limit: 1.10,
    minimum_score_for_hard: 70.0,
    forecast_weight: 0.35,
  }
}

///|
pub(all) struct ScenarioSession {
  date : String
  duration_minutes : Double
  intensity : Double
  expected_load : Double
  purpose : String
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct ScenarioResult {
  kind : TrainingScenarioKind
  sessions : Array[ScenarioSession]
  projected_load : Double
  projected_ratio : Double
  intensity_ceiling : Double
  recovery_cost : Double
  risk_score : Double
  suitable : Bool
  rationale : String
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct ScenarioComparison {
  baseline : ScenarioResult
  alternatives : Array[ScenarioResult]
  recommended_index : Int
  recommendation : String
} derive(FromJson, ToJson, Debug, Eq)

///|
fn scenario_bound(value : Double, low : Double, high : Double) -> Double {
  if value.is_nan() || value.is_inf() {
    low
  } else {
    value.clamp(min=low, max=high)
  }
}

///|
pub fn scenario_name(kind : TrainingScenarioKind) -> String {
  match kind {
    ScenarioBaseline => "baseline"
    ScenarioRecoveryDay => "recovery_day"
    ScenarioEasyAerobic => "easy_aerobic"
    ScenarioReducedVolume => "reduced_volume"
    ScenarioProgression => "progression"
    ScenarioReturnToHard => "return_to_hard"
  }
}

///|
fn scenario_session_load(session : ScenarioSession) -> Double {
  session.expected_load.max(session.duration_minutes * session.intensity)
}

///|
pub fn scenario_session(
  date : String,
  duration_minutes : Double,
  intensity : Double,
  purpose : String,
) -> ScenarioSession {
  let duration = scenario_bound(duration_minutes, 0.0, 1440.0)
  let effort = scenario_bound(intensity, 0.0, 1.0)
  {
    date,
    duration_minutes: duration,
    intensity: effort,
    expected_load: duration * (0.35 + effort) * 1.5,
    purpose,
  }
}

///|
pub fn scenario_from_workout(session : WorkoutSession) -> ScenarioSession {
  scenario_session(
    session.date,
    session.duration_minutes,
    (session.intensity / 10.0).clamp(min=0.0, max=1.0),
    "existing workout",
  )
}

///|
fn scenario_transform(
  sessions : Array[ScenarioSession],
  kind : TrainingScenarioKind,
  config : ScenarioConfig,
) -> Array[ScenarioSession] {
  let result = []
  for session in sessions {
    let factor = match kind {
      ScenarioBaseline => 1.0
      ScenarioRecoveryDay => config.recovery_intensity
      ScenarioEasyAerobic => config.easy_intensity
      ScenarioReducedVolume => 1.0 - config.volume_reduction
      ScenarioProgression => (1.0 + config.progression_limit - 1.0).min(1.15)
      ScenarioReturnToHard => 1.0
    }
    let duration_factor = match kind {
      ScenarioReducedVolume => 1.0 - config.volume_reduction
      ScenarioRecoveryDay => 0.60
      _ => 1.0
    }
    let purpose = match kind {
      ScenarioBaseline => session.purpose
      ScenarioRecoveryDay => "recovery and mobility"
      ScenarioEasyAerobic => "easy aerobic maintenance"
      ScenarioReducedVolume => "reduced-volume technique"
      ScenarioProgression => "controlled progression"
      ScenarioReturnToHard => "return to hard training"
    }
    result.push(
      scenario_session(
        session.date,
        session.duration_minutes * duration_factor,
        session.intensity * factor,
        purpose,
      ),
    )
  }
  result
}

///|
fn scenario_load_average(sessions : Array[ScenarioSession]) -> Double {
  mean_value(sessions.map(session => scenario_session_load(session)))
}

///|
fn scenario_total_load(sessions : Array[ScenarioSession]) -> Double {
  sum_values(sessions.map(session => scenario_session_load(session)))
}

///|
fn scenario_highest_intensity(sessions : Array[ScenarioSession]) -> Double {
  if sessions.length() == 0 {
    0.0
  } else {
    let mut result = 0.0
    for session in sessions {
      if session.intensity > result {
        result = session.intensity
      }
    }
    result
  }
}

///|
fn scenario_recovery_cost(sessions : Array[ScenarioSession]) -> Double {
  sum_values(
    sessions.map(session => session.duration_minutes * session.intensity * 0.85),
  )
}

///|
fn scenario_kind_adjustment(
  kind : TrainingScenarioKind,
  recovery_score : Double,
  current_ratio : Double,
  config : ScenarioConfig,
) -> Double {
  let recovery_factor = if recovery_score <= 0.0 {
    0.40
  } else {
    recovery_score / 100.0
  }
  let ratio_factor = if current_ratio <= 0.0 {
    1.0
  } else {
    (1.0 / current_ratio).clamp(min=0.35, max=1.0)
  }
  let kind_factor = match kind {
    ScenarioBaseline => 1.0
    ScenarioRecoveryDay => 0.20
    ScenarioEasyAerobic => 0.35
    ScenarioReducedVolume => 0.45
    ScenarioProgression => 0.75
    ScenarioReturnToHard => 0.90
  }
  (kind_factor *
  (0.65 * recovery_factor + 0.35 * ratio_factor) *
  (1.0 - config.forecast_weight * 0.20)).clamp(min=0.0, max=1.0)
}

///|
pub fn simulate_training_scenario(
  sessions : Array[ScenarioSession],
  kind : TrainingScenarioKind,
  recovery_score : Double,
  current_ratio : Double,
  config : ScenarioConfig,
) -> ScenarioResult {
  let transformed = scenario_transform(sessions, kind, config)
  let load = scenario_total_load(transformed)
  let average = scenario_load_average(transformed)
  let ratio = if current_ratio <= 0.0 {
    0.0
  } else {
    current_ratio * load / scenario_total_load(sessions).max(1.0)
  }
  let ceiling = scenario_highest_intensity(transformed)
  let cost = scenario_recovery_cost(transformed)
  let adjustment = scenario_kind_adjustment(
    kind, recovery_score, current_ratio, config,
  )
  let risk = (1.0 - adjustment) * 0.80 + ratio.max(1.0) / 3.0 * 0.20
  let suitable = recovery_score >= config.minimum_score_for_hard ||
    kind == ScenarioRecoveryDay ||
    kind == ScenarioEasyAerobic ||
    kind == ScenarioReducedVolume
  let rationale = match kind {
    ScenarioBaseline => "Reference projection without adjustment."
    ScenarioRecoveryDay =>
      "Reduces intensity and volume to create recovery space."
    ScenarioEasyAerobic =>
      "Maintains movement while limiting cardiovascular stress."
    ScenarioReducedVolume => "Preserves session pattern but removes volume."
    ScenarioProgression =>
      "Adds a small dose only when recovery and load support it."
    ScenarioReturnToHard =>
      "Restores hard work after recovery clears the configured floor."
  }
  {
    kind,
    sessions: transformed,
    projected_load: load,
    projected_ratio: ratio,
    intensity_ceiling: ceiling,
    recovery_cost: cost,
    risk_score: scenario_bound(risk, 0.0, 1.0),
    suitable,
    rationale: "\{rationale} average_load=\{average.to_string()}",
  }
}

///|
pub fn scenario_from_plan(
  plan : TrainingLoadPlan,
  recovery_score : Double,
  kind : TrainingScenarioKind,
) -> ScenarioResult {
  let sessions = []
  for day in plan.days {
    for entry in day.entries {
      sessions.push(
        scenario_session(
          entry.date,
          entry.duration_minutes,
          entry.rpe / 10.0,
          load_band_name(entry.band),
        ),
      )
    }
  }
  simulate_training_scenario(
    sessions,
    kind,
    recovery_score,
    plan.profile.acute_chronic_ratio,
    ScenarioConfig::default(),
  )
}

///|
pub fn compare_training_scenarios(
  plan : TrainingLoadPlan,
  recovery_score : Double,
  config : ScenarioConfig,
) -> ScenarioComparison {
  let baseline = scenario_from_plan(plan, recovery_score, ScenarioBaseline)
  let kinds = [
    ScenarioRecoveryDay,
    ScenarioEasyAerobic,
    ScenarioReducedVolume,
    ScenarioProgression,
    ScenarioReturnToHard,
  ]
  let alternatives = []
  for kind in kinds {
    alternatives.push(
      simulate_training_scenario(
        baseline.sessions,
        kind,
        recovery_score,
        plan.profile.acute_chronic_ratio,
        config,
      ),
    )
  }
  let mut recommended = 0
  let mut best_risk = 2.0
  for i in 0.. Array[Double] {
  let result = [
    comparison.baseline.projected_load,
    comparison.baseline.projected_ratio,
    comparison.baseline.risk_score,
    comparison.recommended_index.to_double(),
  ]
  for scenario in comparison.alternatives {
    result.push(scenario.projected_load)
    result.push(scenario.projected_ratio)
    result.push(scenario.risk_score)
    result.push(scenario.intensity_ceiling)
  }
  result
}

///|
pub fn scenario_comparison_csv(comparison : ScenarioComparison) -> String {
  let grid = [
    [
      "scenario", "projected_load", "projected_ratio", "intensity_ceiling", "recovery_cost",
      "risk_score", "suitable", "rationale",
    ],
  ]
  let all = [comparison.baseline]
  for scenario in comparison.alternatives {
    all.push(scenario)
  }
  for scenario in all {
    grid.push([
      scenario_name(scenario.kind),
      scenario.projected_load.to_string(),
      scenario.projected_ratio.to_string(),
      scenario.intensity_ceiling.to_string(),
      scenario.recovery_cost.to_string(),
      scenario.risk_score.to_string(),
      scenario.suitable.to_string(),
      scenario.rationale,
    ])
  }
  to_csv(grid)
}

///|
pub fn scenario_recommended(comparison : ScenarioComparison) -> ScenarioResult {
  if comparison.alternatives.length() == 0 {
    comparison.baseline
  } else {
    comparison.alternatives[comparison.recommended_index.clamp(
      min=0,
      max=comparison.alternatives.length() - 1,
    )]
  }
}

///|
pub fn scenario_requires_recovery(result : ScenarioResult) -> Bool {
  result.risk_score >= 0.65 || result.projected_ratio >= 1.30
}

///|
pub fn scenario_is_lower_load(
  candidate : ScenarioResult,
  baseline : ScenarioResult,
) -> Bool {
  candidate.projected_load <= baseline.projected_load &&
  candidate.risk_score <= baseline.risk_score
}

///|
pub fn scenario_plan_message(comparison : ScenarioComparison) -> String {
  let selected = scenario_recommended(comparison)
  if scenario_requires_recovery(selected) {
    "Selected scenario remains high stress; add a recovery checkpoint."
  } else {
    comparison.recommendation
  }
}