///|
/// A stable, machine-readable severity used by integration quality gates.
pub(all) enum AlertLevel {
  Info
  Warning
  Critical
} derive(Debug)

///|
/// A diagnostic produced from one evaluated reward step.
pub(all) struct RewardAlert {
  step_index : Int
  level : AlertLevel
  code : String
  message : String
  value : Double
} derive(Debug)

///|
pub fn AlertLevel::label(self : Self) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    Critical => "critical"
  }
}

///|
pub fn RewardAlert::render(self : Self) -> String {
  "[\{self.level.label()}] \{self.code} step=\{self.step_index}: \{self.message} (\{self.value})"
}

///|
pub fn inspect_breakdown(
  breakdown : RewardBreakdown,
  sparse_threshold : Double,
  clip_threshold : Double,
) -> Array[RewardAlert] {
  let alerts = Array::new()
  if breakdown.zero_ratio >= sparse_threshold {
    alerts.push(RewardAlert::{
      step_index: breakdown.step_index,
      level: if breakdown.zero_ratio >= 0.8 {
        Critical
      } else {
        Warning
      },
      code: "SPARSE_SIGNAL",
      message: "too many reward terms contributed zero",
      value: breakdown.zero_ratio,
    })
  }
  if breakdown.clipped_total == clip_threshold ||
    breakdown.clipped_total == -clip_threshold {
    alerts.push(RewardAlert::{
      step_index: breakdown.step_index,
      level: Warning,
      code: "CLIPPED_SIGNAL",
      message: "normalized reward reached the configured clip boundary",
      value: breakdown.clipped_total,
    })
  }
  if breakdown.penalty_share >= 0.8 {
    alerts.push(RewardAlert::{
      step_index: breakdown.step_index,
      level: Warning,
      code: "PENALTY_DOMINATED",
      message: "penalty terms dominate the absolute contribution",
      value: breakdown.penalty_share,
    })
  }
  if breakdown.shaping_share >= 0.8 {
    alerts.push(RewardAlert::{
      step_index: breakdown.step_index,
      level: Info,
      code: "SHAPING_DOMINATED",
      message: "shaping terms dominate the absolute contribution",
      value: breakdown.shaping_share,
    })
  }
  alerts
}

///|
pub fn audit_trace(
  steps : Array[RewardStep],
  config : RewardConfig,
  sparse_threshold : Double,
) -> Array[RewardAlert] {
  if sparse_threshold < 0.0 || sparse_threshold > 1.0 {
    panic()
  }
  let (breakdowns, _) = evaluate_trace(steps, config)
  let alerts = Array::new()
  for breakdown in breakdowns {
    let step_alerts = inspect_breakdown(
      breakdown,
      sparse_threshold,
      config.clip_max,
    )
    for alert in step_alerts {
      alerts.push(alert)
    }
  }
  alerts
}

///|
pub fn render_alerts(alerts : Array[RewardAlert]) -> String {
  if alerts.length() == 0 {
    "No reward-quality alerts."
  } else {
    let mut text = "Reward quality alerts\n"
    for alert in alerts {
      text = text + "- " + alert.render() + "\n"
    }
    text
  }
}

///|
pub fn compare_traces(
  left : Array[RewardStep],
  right : Array[RewardStep],
  config : RewardConfig,
) -> (TraceSummary, TraceSummary, Double) {
  let (_, left_summary) = evaluate_trace(left, config)
  let (_, right_summary) = evaluate_trace(right, config)
  let delta = right_summary.total_clipped - left_summary.total_clipped
  (left_summary, right_summary, delta)
}