///|
pub fn evaluate_gate(
  budget : RiskBudget,
  metrics : MetricsSnapshot,
  blast : BlastRadius,
) -> GateResult {
  let reasons = Array::new(capacity=8)
  let mut penalty = 0
  let mut hard = false
  if metrics.sample_count < budget.min_samples {
    reasons.push(
      "sample count below budget: " +
      metrics.sample_count.to_string() +
      "/" +
      budget.min_samples.to_string(),
    )
    penalty += 12
  }
  if metrics.error_per_million > budget.max_error_per_million {
    reasons.push(
      "error rate above budget: " +
      metrics.error_per_million.to_string() +
      "ppm > " +
      budget.max_error_per_million.to_string() +
      "ppm",
    )
    penalty += 25
    hard = true
  }
  if metrics.latency_p95_ms > budget.max_latency_p95_ms {
    reasons.push(
      "p95 latency above budget: " +
      metrics.latency_p95_ms.to_string() +
      "ms > " +
      budget.max_latency_p95_ms.to_string() +
      "ms",
    )
    penalty += 18
  }
  if metrics.saturation_bp > budget.max_saturation_bp {
    reasons.push(
      "saturation above budget: " +
      percent_text(metrics.saturation_bp) +
      " > " +
      percent_text(budget.max_saturation_bp),
    )
    penalty += 18
  }
  if budget.require_synthetic_pass && !metrics.synthetic_pass {
    reasons.push("synthetic checks failed")
    penalty += 25
    hard = true
  }
  if metrics.dependency_failures > budget.max_dependency_failures {
    reasons.push(
      "dependency failures above budget: " +
      metrics.dependency_failures.to_string() +
      " > " +
      budget.max_dependency_failures.to_string(),
    )
    penalty += 20
  }
  if blast.percent_bp > budget.max_blast_radius_bp {
    reasons.push(
      "blast radius above budget: " +
      percent_text(blast.percent_bp) +
      " > " +
      percent_text(budget.max_blast_radius_bp),
    )
    penalty += 20
  }
  if reasons.length() == 0 {
    reasons.push("all gates inside risk budget")
  }
  let score = clamp(100 - penalty, 0, 100)
  let decision = if hard {
    Rollback
  } else if score >= 85 {
    Advance
  } else if score >= 65 {
    Hold
  } else {
    Rollback
  }
  let severity = if decision == Advance {
    Pass
  } else if decision == Hold {
    Warn
  } else {
    Fail
  }
  { decision, severity, score, reasons }
}

///|
pub fn combine_gates(left : GateResult, right : GateResult) -> GateResult {
  let reasons = Array::new(
    capacity=left.reasons.length() + right.reasons.length(),
  )
  for reason in left.reasons {
    reasons.push(reason)
  }
  for reason in right.reasons {
    reasons.push(reason)
  }
  let decision = stricter_decision(left.decision, right.decision)
  let severity = if left.severity == Fail || right.severity == Fail {
    Fail
  } else if left.severity == Warn || right.severity == Warn {
    Warn
  } else {
    Pass
  }
  {
    decision,
    severity,
    score: if left.score < right.score {
      left.score
    } else {
      right.score
    },
    reasons,
  }
}

///|
fn stricter_decision(a : GateDecision, b : GateDecision) -> GateDecision {
  if a == Abort || b == Abort {
    Abort
  } else if a == Rollback || b == Rollback {
    Rollback
  } else if a == Hold || b == Hold {
    Hold
  } else {
    Advance
  }
}

///|
pub fn metric_trend_gate(
  budget : RiskBudget,
  previous : MetricsSnapshot,
  current : MetricsSnapshot,
  blast : BlastRadius,
) -> GateResult {
  let base = evaluate_gate(budget, current, blast)
  let reasons = Array::new(capacity=base.reasons.length() + 4)
  for reason in base.reasons {
    reasons.push(reason)
  }
  let mut penalty = 0
  if current.error_per_million > previous.error_per_million * 2 &&
    current.error_per_million > budget.max_error_per_million / 2 {
    reasons.push("error rate doubled compared with previous window")
    penalty += 18
  }
  if current.latency_p95_ms > previous.latency_p95_ms + 200 {
    reasons.push("p95 latency jumped by more than 200ms")
    penalty += 10
  }
  if current.saturation_bp > previous.saturation_bp + 1500 {
    reasons.push("saturation grew by more than 15 percentage points")
    penalty += 10
  }
  let score = clamp(base.score - penalty, 0, 100)
  let decision = if base.decision == Rollback || score < 65 {
    Rollback
  } else if base.decision == Hold || score < 85 {
    Hold
  } else {
    Advance
  }
  let severity = if decision == Advance {
    Pass
  } else if decision == Hold {
    Warn
  } else {
    Fail
  }
  { decision, severity, score, reasons }
}