///|
/// Policy thresholds for deciding whether a recording can be scored.
pub(all) struct QualityPolicy {
  minimum_beats : Int
  minimum_valid_ratio : Double
  maximum_gap_candidates : Int
  maximum_drift_ms_per_beat : Double
  reject_non_finite : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
/// An explainable policy decision.
pub(all) struct QualityDecision {
  accepted : Bool
  confidence : Double
  reasons : Array[String]
  recommended_method : CleaningMethod
  validation : IntervalValidation
  diagnostic : SignalDiagnostic
} derive(FromJson, ToJson, Debug, Eq)

///|
/// A balanced policy for daily recovery measurements.
pub fn QualityPolicy::daily_recovery() -> QualityPolicy {
  {
    minimum_beats: 60,
    minimum_valid_ratio: 0.85,
    maximum_gap_candidates: 6,
    maximum_drift_ms_per_beat: 5.0,
    reject_non_finite: true,
  }
}

///|
/// Choose a correction method from the observed artifact ratio.
pub fn recommend_cleaning_method(
  validation : IntervalValidation,
) -> CleaningMethod {
  if validation.total == 0 || validation.invalid == 0 {
    Remove
  } else if validation.invalid.to_double() / validation.total.to_double() <=
    0.10 {
    InterpolateLocalMedian
  } else {
    InterpolateLinear
  }
}

///|
/// Evaluate a signal against a policy and return rejection reasons.
pub fn evaluate_quality_policy(
  intervals : Array[Double],
  config : HrvConfig,
  policy : QualityPolicy,
) -> QualityDecision {
  let validation = validate_intervals(intervals, config)
  let diagnostic = diagnose_signal(intervals, config)
  let reasons = []
  if validation.total < policy.minimum_beats {
    reasons.push("too_few_beats")
  }
  if diagnostic.valid_ratio < policy.minimum_valid_ratio {
    reasons.push("low_valid_ratio")
  }
  if diagnostic.gap_candidates > policy.maximum_gap_candidates {
    reasons.push("too_many_gaps")
  }
  if diagnostic.drift_slope.abs() > policy.maximum_drift_ms_per_beat {
    reasons.push("excessive_drift")
  }
  if policy.reject_non_finite && validation.non_finite > 0 {
    reasons.push("non_finite_values")
  }
  let confidence = (diagnostic.valid_ratio * 100.0 -
  diagnostic.gap_candidates.to_double() * 2.0).clamp(min=0.0, max=100.0)
  {
    accepted: reasons.length() == 0,
    confidence,
    reasons,
    recommended_method: recommend_cleaning_method(validation),
    validation,
    diagnostic,
  }
}

///|
/// Convert a policy decision into a multiplicative score weight.
pub fn quality_weight(decision : QualityDecision) -> Double {
  if !decision.accepted {
    0.0
  } else {
    decision.confidence / 100.0
  }
}

///|
/// Return whether a report is safe to publish to a dashboard.
pub fn is_publishable(decision : QualityDecision) -> Bool {
  decision.accepted && decision.confidence >= 70.0
}

///|
/// Combine policy decisions from a batch conservatively.
pub fn batch_quality_decision(
  decisions : Array[QualityDecision],
) -> QualityDecision? {
  if decisions.length() == 0 {
    return None
  }
  let mut accepted = true
  let mut confidence = 100.0
  let reasons = []
  let mut recommended = Remove
  for decision in decisions {
    if !decision.accepted {
      accepted = false
    }
    if decision.confidence < confidence {
      confidence = decision.confidence
    }
    for reason in decision.reasons {
      reasons.push(reason)
    }
    if decision.recommended_method is InterpolateLinear {
      recommended = InterpolateLinear
    }
  }
  Some({
    accepted,
    confidence,
    reasons,
    recommended_method: recommended,
    validation: decisions[0].validation,
    diagnostic: decisions[0].diagnostic,
  })
}