///|
/// A numeric diagnostic for a recording and its clean-up result.
pub(all) struct SignalDiagnostic {
  sample_count : Int
  duration_seconds : Double
  valid_ratio : Double
  artifact_ratio : Double
  mean_rr : Double
  median_rr : Double
  mean_hr : Double
  hr_range : Double
  rmssd : Double
  sdnn : Double
  drift_slope : Double
  gap_candidates : Int
  grade : String
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Build a dashboard diagnostic without running the full spectral pipeline.
pub fn diagnose_signal(
  intervals : Array[Double],
  config : HrvConfig,
) -> SignalDiagnostic {
  let validation = validate_intervals(intervals, config)
  let hr = summarize_heart_rate(intervals, 0.0)
  let trend = fit_linear_trend(intervals)
  {
    sample_count: intervals.length(),
    duration_seconds: sum_values(intervals) / 1000.0,
    valid_ratio: if intervals.length() == 0 {
      0.0
    } else {
      validation.valid.to_double() / intervals.length().to_double()
    },
    artifact_ratio: if intervals.length() == 0 {
      0.0
    } else {
      validation.invalid.to_double() / intervals.length().to_double()
    },
    mean_rr: mean_value(intervals),
    median_rr: median_value(intervals),
    mean_hr: hr.mean_bpm,
    hr_range: hr.maximum_bpm - hr.minimum_bpm,
    rmssd: calculate_rmssd(intervals),
    sdnn: calculate_sdnn(intervals),
    drift_slope: trend.slope,
    gap_candidates: count_gap_candidates(intervals, config, 2.0),
    grade: quality_grade(validation),
  }
}

///|
/// Return whether a signal is acceptable for recovery scoring.
pub fn is_recovery_signal_usable(diagnostic : SignalDiagnostic) -> Bool {
  diagnostic.sample_count >= 30 &&
  diagnostic.valid_ratio >= 0.85 &&
  diagnostic.rmssd >= 0.0
}

///|
/// Return a one-line recommendation for a signal-quality card.
pub fn diagnostic_recommendation(diagnostic : SignalDiagnostic) -> String {
  if diagnostic.sample_count == 0 {
    "collect an RR recording before analysis"
  } else if diagnostic.valid_ratio < 0.75 {
    "recording is too noisy; repeat the measurement"
  } else if diagnostic.gap_candidates > diagnostic.sample_count / 10 {
    "check sensor contact and remove long gaps"
  } else if diagnostic.drift_slope.abs() > 5.0 {
    "check for posture or sensor changes during the recording"
  } else {
    "recording is suitable for recovery analysis"
  }
}