///|
/// Explainable decision support built on HRV quality, recovery, and load data.
/// It intentionally emits findings and actions instead of opaque medical claims.
pub(all) enum DecisionSeverity {
  DecisionInfo
  DecisionAdvisory
  DecisionWarning
  DecisionUrgent
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) enum DecisionDomain {
  DecisionSignal
  DecisionRecovery
  DecisionTraining
  DecisionSleep
  DecisionConsistency
  DecisionSystem
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct DecisionFinding {
  code : String
  domain : DecisionDomain
  severity : DecisionSeverity
  title : String
  evidence : String
  observed : Double
  reference : Double
  confidence : Double
  action : String
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct DecisionContext {
  quality_report : QualityReport?
  recovery_report : LongitudinalRecoveryReport?
  load_plan : TrainingLoadPlan?
  sleep_hours : Double
  sleep_efficiency : Double
  symptom_score : Double
  user_goal : String
  requested_intensity : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct DecisionAction {
  priority : Int
  title : String
  instruction : String
  duration_minutes : Double
  intensity_ceiling : Double
  requires_recheck : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct DecisionPlan {
  score : Double
  confidence : Double
  level : DecisionSeverity
  findings : Array[DecisionFinding]
  actions : Array[DecisionAction]
  headline : String
  disclaimer : String
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct DecisionThresholds {
  quality_floor : Double
  readiness_floor : Double
  readiness_ceiling : Double
  load_ratio_watch : Double
  load_ratio_warning : Double
  sleep_floor_hours : Double
  confidence_floor : Double
  symptom_warning : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
pub fn DecisionThresholds::default() -> DecisionThresholds {
  {
    quality_floor: 0.70,
    readiness_floor: 48.0,
    readiness_ceiling: 70.0,
    load_ratio_watch: 1.10,
    load_ratio_warning: 1.30,
    sleep_floor_hours: 6.0,
    confidence_floor: 0.35,
    symptom_warning: 5.0,
  }
}

///|
pub fn decision_severity_name(value : DecisionSeverity) -> String {
  match value {
    DecisionInfo => "info"
    DecisionAdvisory => "advisory"
    DecisionWarning => "warning"
    DecisionUrgent => "urgent"
  }
}

///|
pub fn decision_domain_name(value : DecisionDomain) -> String {
  match value {
    DecisionSignal => "signal"
    DecisionRecovery => "recovery"
    DecisionTraining => "training"
    DecisionSleep => "sleep"
    DecisionConsistency => "consistency"
    DecisionSystem => "system"
  }
}

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

///|
fn decision_severity_score(value : DecisionSeverity) -> Double {
  match value {
    DecisionInfo => 0.0
    DecisionAdvisory => 0.33
    DecisionWarning => 0.67
    DecisionUrgent => 1.0
  }
}

///|
fn decision_more_severe(
  left : DecisionSeverity,
  right : DecisionSeverity,
) -> Bool {
  decision_severity_score(left) > decision_severity_score(right)
}

///|
pub fn make_decision_context(
  quality_report : QualityReport?,
  recovery_report : LongitudinalRecoveryReport?,
  load_plan : TrainingLoadPlan?,
  sleep_hours : Double,
  sleep_efficiency : Double,
  symptom_score : Double,
  user_goal : String,
  requested_intensity : Double,
) -> DecisionContext {
  {
    quality_report,
    recovery_report,
    load_plan,
    sleep_hours: decision_bound(sleep_hours, 0.0, 24.0),
    sleep_efficiency: decision_bound(sleep_efficiency, 0.0, 1.0),
    symptom_score: decision_bound(symptom_score, 0.0, 10.0),
    user_goal,
    requested_intensity: decision_bound(requested_intensity, 0.0, 1.0),
  }
}

///|
pub fn decision_finding(
  code : String,
  domain : DecisionDomain,
  severity : DecisionSeverity,
  title : String,
  evidence : String,
  observed : Double,
  reference : Double,
  confidence : Double,
  action : String,
) -> DecisionFinding {
  {
    code,
    domain,
    severity,
    title,
    evidence,
    observed,
    reference,
    confidence: decision_bound(confidence, 0.0, 1.0),
    action,
  }
}

///|
pub fn decision_action(
  priority : Int,
  title : String,
  instruction : String,
  duration_minutes : Double,
  intensity_ceiling : Double,
  requires_recheck : Bool,
) -> DecisionAction {
  {
    priority: priority.max(0),
    title,
    instruction,
    duration_minutes: decision_bound(duration_minutes, 0.0, 1440.0),
    intensity_ceiling: decision_bound(intensity_ceiling, 0.0, 1.0),
    requires_recheck,
  }
}

///|
pub fn decision_quality_finding(
  report : QualityReport,
  thresholds : DecisionThresholds,
) -> DecisionFinding? {
  let ratio = report.clean_ratio
  if ratio >= thresholds.quality_floor {
    None
  } else {
    Some(
      decision_finding(
        "quality_floor",
        DecisionSignal,
        if ratio < thresholds.quality_floor * 0.60 {
          DecisionUrgent
        } else {
          DecisionWarning
        },
        "Signal quality is below the operating floor",
        "Usable beat coverage is insufficient for a confident recovery decision.",
        ratio,
        thresholds.quality_floor,
        ratio.clamp(min=0.0, max=1.0),
        "Repeat the recording with stable sensor contact and minimal movement.",
      ),
    )
  }
}

///|
pub fn decision_recovery_findings(
  report : LongitudinalRecoveryReport,
  thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
  let findings = []
  if report.current_status == RecoveryStrained {
    findings.push(
      decision_finding(
        "recovery_strained",
        DecisionRecovery,
        DecisionWarning,
        "Recovery is below the preferred range",
        recovery_report_message(report),
        report.current_score,
        thresholds.readiness_floor,
        report.quality_ratio,
        "Reduce intensity and prioritize sleep before the next hard session.",
      ),
    )
  } else if report.current_status == RecoveryUnavailable {
    findings.push(
      decision_finding(
        "recovery_unavailable",
        DecisionRecovery,
        DecisionAdvisory,
        "Recovery cannot be scored confidently",
        "The available baseline or current signal is incomplete.",
        report.current_score,
        thresholds.readiness_floor,
        report.quality_ratio,
        "Collect another standardized morning measurement.",
      ),
    )
  } else if report.current_score < thresholds.readiness_ceiling {
    findings.push(
      decision_finding(
        "recovery_moderate",
        DecisionRecovery,
        DecisionAdvisory,
        "Recovery is moderate",
        "The score is usable but does not support maximal intensity.",
        report.current_score,
        thresholds.readiness_ceiling,
        report.quality_ratio,
        "Keep the session aerobic or shorten the planned hard block.",
      ),
    )
  }
  if report.trajectory == RecoveryWorsening {
    findings.push(
      decision_finding(
        "recovery_trend",
        DecisionRecovery,
        DecisionWarning,
        "Recovery trend is worsening",
        "Recent recovery scores have a negative robust trend.",
        -1.0,
        0.0,
        report.quality_ratio,
        "Review cumulative load and sleep over the previous seven days.",
      ),
    )
  }
  findings
}

///|
pub fn decision_load_findings(
  plan : TrainingLoadPlan,
  thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
  let findings = []
  let ratio = plan.profile.acute_chronic_ratio
  if ratio >= thresholds.load_ratio_warning {
    findings.push(
      decision_finding(
        "load_ratio_warning",
        DecisionTraining,
        DecisionWarning,
        "Acute load is above the reference window",
        load_plan_recovery_message(plan),
        ratio,
        thresholds.load_ratio_warning,
        load_plan_quality(plan),
        "Insert a recovery day and avoid stacking another high-load session.",
      ),
    )
  } else if ratio >= thresholds.load_ratio_watch {
    findings.push(
      decision_finding(
        "load_ratio_watch",
        DecisionTraining,
        DecisionAdvisory,
        "Training load is rising",
        load_plan_recovery_message(plan),
        ratio,
        thresholds.load_ratio_watch,
        load_plan_quality(plan),
        "Keep the next session below the recent average intensity.",
      ),
    )
  }
  if plan.profile.monotony >= 2.0 {
    findings.push(
      decision_finding(
        "load_monotony",
        DecisionTraining,
        DecisionAdvisory,
        "Daily load has low variation",
        "Repeated similar loads can reduce the value of nominal rest days.",
        plan.profile.monotony,
        2.0,
        load_plan_quality(plan),
        "Vary easy and hard days and keep at least one true rest day.",
      ),
    )
  }
  findings
}

///|
pub fn decision_sleep_findings(
  context : DecisionContext,
  thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
  let findings = []
  if context.sleep_hours > 0.0 &&
    context.sleep_hours < thresholds.sleep_floor_hours {
    findings.push(
      decision_finding(
        "sleep_duration",
        DecisionSleep,
        DecisionWarning,
        "Sleep duration is short",
        "Short sleep can lower confidence in a next-session intensity decision.",
        context.sleep_hours,
        thresholds.sleep_floor_hours,
        context.sleep_efficiency,
        "Prefer recovery work and protect the next sleep opportunity.",
      ),
    )
  }
  if context.sleep_efficiency > 0.0 && context.sleep_efficiency < 0.75 {
    findings.push(
      decision_finding(
        "sleep_efficiency",
        DecisionSleep,
        DecisionAdvisory,
        "Sleep efficiency is reduced",
        "Fragmented sleep is included as a context signal, not a diagnosis.",
        context.sleep_efficiency,
        0.75,
        context.sleep_efficiency,
        "Review sleep timing and avoid using one night as a long-term conclusion.",
      ),
    )
  }
  findings
}

///|
pub fn decision_symptom_finding(
  context : DecisionContext,
  thresholds : DecisionThresholds,
) -> DecisionFinding? {
  if context.symptom_score < thresholds.symptom_warning {
    None
  } else {
    Some(
      decision_finding(
        "reported_symptoms",
        DecisionSystem,
        if context.symptom_score >= 8.0 {
          DecisionUrgent
        } else {
          DecisionWarning
        },
        "Reported symptoms require priority",
        "Self-reported symptoms take precedence over a favorable wearable score.",
        context.symptom_score,
        thresholds.symptom_warning,
        1.0,
        "Stop the planned hard session and seek appropriate professional advice.",
      ),
    )
  }
}

///|
fn decision_append_all(
  target : Array[DecisionFinding],
  source : Array[DecisionFinding],
) -> Unit {
  for item in source {
    target.push(item)
  }
}

///|
pub fn decision_collect_findings(
  context : DecisionContext,
  thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
  let findings = []
  match context.quality_report {
    Some(report) =>
      match decision_quality_finding(report, thresholds) {
        Some(finding) => findings.push(finding)
        None => ()
      }
    None =>
      findings.push(
        decision_finding(
          "quality_missing",
          DecisionSignal,
          DecisionAdvisory,
          "Signal quality is not available",
          "The decision engine received no quality report.",
          0.0,
          thresholds.quality_floor,
          0.0,
          "Require a quality report before interpreting recovery.",
        ),
      )
  }
  match context.recovery_report {
    Some(report) =>
      decision_append_all(
        findings,
        decision_recovery_findings(report, thresholds),
      )
    None =>
      findings.push(
        decision_finding(
          "recovery_missing",
          DecisionRecovery,
          DecisionAdvisory,
          "Recovery context is missing",
          "No longitudinal recovery report was supplied.",
          0.0,
          thresholds.readiness_floor,
          0.0,
          "Collect a standardized morning measurement first.",
        ),
      )
  }
  match context.load_plan {
    Some(plan) =>
      decision_append_all(findings, decision_load_findings(plan, thresholds))
    None => ()
  }
  decision_append_all(findings, decision_sleep_findings(context, thresholds))
  match decision_symptom_finding(context, thresholds) {
    Some(finding) => findings.push(finding)
    None => ()
  }
  findings
}

///|
pub fn decision_max_severity(
  findings : Array[DecisionFinding],
) -> DecisionSeverity {
  let mut result = DecisionInfo
  for finding in findings {
    if decision_more_severe(finding.severity, result) {
      result = finding.severity
    }
  }
  result
}

///|
pub fn decision_confidence(findings : Array[DecisionFinding]) -> Double {
  if findings.length() == 0 {
    1.0
  } else {
    mean_value(findings.map(finding => finding.confidence))
  }
}

///|
pub fn decision_score(
  context : DecisionContext,
  findings : Array[DecisionFinding],
) -> Double {
  let mut score = 80.0
  for finding in findings {
    score -= decision_severity_score(finding.severity) *
      18.0 *
      finding.confidence
  }
  if context.requested_intensity > 0.80 {
    score -= 5.0
  }
  decision_bound(score, 0.0, 100.0)
}

///|
pub fn decision_actions(
  context : DecisionContext,
  findings : Array[DecisionFinding],
  score : Double,
) -> Array[DecisionAction] {
  let actions = []
  let mut priority = 1
  for finding in findings {
    if finding.severity != DecisionInfo {
      actions.push(
        decision_action(
          priority,
          finding.title,
          finding.action,
          if finding.domain == DecisionTraining {
            45.0
          } else {
            20.0
          },
          if finding.severity == DecisionUrgent {
            0.20
          } else {
            0.60
          },
          finding.severity != DecisionInfo,
        ),
      )
      priority += 1
    }
  }
  if actions.length() == 0 {
    actions.push(
      decision_action(
        1,
        "Proceed with the planned session",
        if score >= 70.0 {
          "Use the planned intensity and continue monitoring."
        } else {
          "Use a controlled version of the planned session."
        },
        60.0,
        if context.requested_intensity < 0.80 {
          context.requested_intensity
        } else {
          0.80
        },
        false,
      ),
    )
  }
  actions.sort_by((left, right) => left.priority - right.priority)
  actions
}

///|
pub fn build_decision_plan(
  context : DecisionContext,
  thresholds : DecisionThresholds,
) -> DecisionPlan {
  let findings = decision_collect_findings(context, thresholds)
  let score = decision_score(context, findings)
  let level = decision_max_severity(findings)
  let confidence = decision_confidence(findings)
  let actions = decision_actions(context, findings, score)
  let headline = match level {
    DecisionInfo => "No decision blockers were detected."
    DecisionAdvisory => "Proceed with a controlled plan."
    DecisionWarning => "Reduce training stress before progressing."
    DecisionUrgent => "Pause the planned intensity and reassess."
  }
  {
    score,
    confidence,
    level,
    findings,
    actions,
    headline,
    disclaimer: "This is a signal-informed training aid, not a medical diagnosis.",
  }
}

///|
pub fn decision_plan_is_actionable(plan : DecisionPlan) -> Bool {
  plan.actions.length() > 0 && plan.confidence >= 0.20
}

///|
pub fn decision_plan_feature_vector(plan : DecisionPlan) -> Array[Double] {
  let mut intensity_sum = 0.0
  for action in plan.actions {
    intensity_sum += action.intensity_ceiling
  }
  [
    plan.score,
    plan.confidence,
    decision_severity_score(plan.level),
    plan.findings.length().to_double(),
    plan.actions.length().to_double(),
    plan.actions.filter(action => action.requires_recheck).length().to_double(),
    intensity_sum,
  ]
}

///|
pub fn decision_plan_csv(plan : DecisionPlan) -> String {
  let grid = [
    [
      "code", "domain", "severity", "title", "observed", "reference", "confidence",
      "action",
    ],
  ]
  for finding in plan.findings {
    grid.push([
      finding.code,
      decision_domain_name(finding.domain),
      decision_severity_name(finding.severity),
      finding.title,
      finding.observed.to_string(),
      finding.reference.to_string(),
      finding.confidence.to_string(),
      finding.action,
    ])
  }
  to_csv(grid)
}

///|
pub fn decision_actions_csv(plan : DecisionPlan) -> String {
  let grid = [
    [
      "priority", "title", "instruction", "duration_minutes", "intensity_ceiling",
      "requires_recheck",
    ],
  ]
  for action in plan.actions {
    grid.push([
      action.priority.to_string(),
      action.title,
      action.instruction,
      action.duration_minutes.to_string(),
      action.intensity_ceiling.to_string(),
      action.requires_recheck.to_string(),
    ])
  }
  to_csv(grid)
}

///|
pub fn decision_plan_summary(plan : DecisionPlan) -> String {
  "\{plan.headline} score=\{plan.score.to_string()} confidence=\{plan.confidence.to_string()} findings=\{plan.findings.length().to_string()}"
}

///|
pub fn decision_plan_recheck_required(plan : DecisionPlan) -> Bool {
  plan.actions.any(action => action.requires_recheck) ||
  plan.level == DecisionUrgent
}

///|
pub fn decision_domain_counts(findings : Array[DecisionFinding]) -> Array[Int] {
  [
    findings.filter(item => item.domain == DecisionSignal).length(),
    findings.filter(item => item.domain == DecisionRecovery).length(),
    findings.filter(item => item.domain == DecisionTraining).length(),
    findings.filter(item => item.domain == DecisionSleep).length(),
    findings.filter(item => item.domain == DecisionConsistency).length(),
    findings.filter(item => item.domain == DecisionSystem).length(),
  ]
}

///|
pub fn decision_plan_has_domain(
  plan : DecisionPlan,
  domain : DecisionDomain,
) -> Bool {
  plan.findings.any(item => item.domain == domain)
}

///|
pub fn decision_plan_risk_score(plan : DecisionPlan) -> Double {
  (1.0 - plan.score / 100.0).clamp(min=0.0, max=1.0) *
  (0.50 + decision_severity_score(plan.level) * 0.50)
}

///|
pub fn decision_plan_intensity_ceiling(plan : DecisionPlan) -> Double {
  if plan.actions.length() == 0 {
    0.0
  } else {
    let mut result = 1.0
    for action in plan.actions {
      if action.intensity_ceiling < result {
        result = action.intensity_ceiling
      }
    }
    result
  }
}

///|
pub fn decision_findings_for_severity(
  plan : DecisionPlan,
  severity : DecisionSeverity,
) -> Array[DecisionFinding] {
  plan.findings.filter(item => item.severity == severity)
}

///|
pub fn decision_plan_message(plan : DecisionPlan) -> String {
  if decision_plan_recheck_required(plan) {
    "Recheck recovery or signal quality before increasing intensity."
  } else {
    "The current evidence supports a controlled continuation."
  }
}