///|
/// Result applied when an incoming point violates an operational quality gate.
pub enum QualityDisposition {
  QualityAccepted
  QualityDegraded
  QualityRejected
} derive(Eq, Debug)

///|
/// Machine-readable findings retained in the quality audit stream.
pub enum QualityFinding {
  QualityInvalidValue
  QualityStaleValue
  QualityFutureTimestamp
  QualityMissingSource
  QualityUnknownStation
  QualityControlBlocked
  QualityNoFinding
} derive(Eq, Debug)

///|
pub fn quality_disposition_text(disposition : QualityDisposition) -> String {
  match disposition {
    QualityAccepted => "accepted"
    QualityDegraded => "degraded"
    QualityRejected => "rejected"
  }
}

///|
pub fn quality_finding_text(finding : QualityFinding) -> String {
  match finding {
    QualityInvalidValue => "invalid-value"
    QualityStaleValue => "stale-value"
    QualityFutureTimestamp => "future-timestamp"
    QualityMissingSource => "missing-source"
    QualityUnknownStation => "unknown-station"
    QualityControlBlocked => "control-blocked"
    QualityNoFinding => "none"
  }
}

///|
/// A configurable quality gate for one station or an entire deployment.
pub struct QualityGateRule {
  id : Int
  name : String
  max_age : Int
  future_skew : Int
  require_source : Bool
  reject_invalid : Bool
  reject_stale : Bool
  reject_future : Bool
  reject_control : Bool
  invalid_penalty : Int
  stale_penalty : Int
  future_penalty : Int
  missing_source_penalty : Int
  enabled : Bool
} derive(Eq, Debug)

///|
pub fn QualityGateRule::new(
  id : Int,
  name : String,
  max_age? : Int = 300,
  future_skew? : Int = 30,
  require_source? : Bool = false,
  reject_invalid? : Bool = true,
  reject_stale? : Bool = false,
  reject_future? : Bool = true,
  reject_control? : Bool = false,
  invalid_penalty? : Int = 40,
  stale_penalty? : Int = 30,
  future_penalty? : Int = 20,
  missing_source_penalty? : Int = 15,
  enabled? : Bool = true,
) -> Result[QualityGateRule, String] {
  if id < 1 {
    Err("quality gate id must be positive")
  } else if name.length() == 0 || name.length() > 128 {
    Err("quality gate name must contain between 1 and 128 characters")
  } else if max_age < 0 || max_age > 31536000 {
    Err("quality gate maximum age is outside the supported range")
  } else if future_skew < 0 || future_skew > 31536000 {
    Err("quality gate future skew is outside the supported range")
  } else if invalid_penalty < 0 ||
    invalid_penalty > 100 ||
    stale_penalty < 0 ||
    stale_penalty > 100 ||
    future_penalty < 0 ||
    future_penalty > 100 ||
    missing_source_penalty < 0 ||
    missing_source_penalty > 100 {
    Err("quality gate penalty must be between 0 and 100")
  } else {
    Ok({
      id,
      name,
      max_age,
      future_skew,
      require_source,
      reject_invalid,
      reject_stale,
      reject_future,
      reject_control,
      invalid_penalty,
      stale_penalty,
      future_penalty,
      missing_source_penalty,
      enabled,
    })
  }
}

///|
pub fn QualityGateRule::default_rule() -> QualityGateRule {
  QualityGateRule::new(1, "default").unwrap()
}

///|
pub fn QualityGateRule::id(self : QualityGateRule) -> Int {
  self.id
}

///|
pub fn QualityGateRule::name(self : QualityGateRule) -> String {
  self.name
}

///|
pub fn QualityGateRule::max_age(self : QualityGateRule) -> Int {
  self.max_age
}

///|
pub fn QualityGateRule::future_skew(self : QualityGateRule) -> Int {
  self.future_skew
}

///|
pub fn QualityGateRule::require_source(self : QualityGateRule) -> Bool {
  self.require_source
}

///|
pub fn QualityGateRule::reject_invalid(self : QualityGateRule) -> Bool {
  self.reject_invalid
}

///|
pub fn QualityGateRule::reject_stale(self : QualityGateRule) -> Bool {
  self.reject_stale
}

///|
pub fn QualityGateRule::reject_future(self : QualityGateRule) -> Bool {
  self.reject_future
}

///|
pub fn QualityGateRule::reject_control(self : QualityGateRule) -> Bool {
  self.reject_control
}

///|
pub fn QualityGateRule::invalid_penalty(self : QualityGateRule) -> Int {
  self.invalid_penalty
}

///|
pub fn QualityGateRule::stale_penalty(self : QualityGateRule) -> Int {
  self.stale_penalty
}

///|
pub fn QualityGateRule::future_penalty(self : QualityGateRule) -> Int {
  self.future_penalty
}

///|
pub fn QualityGateRule::missing_source_penalty(self : QualityGateRule) -> Int {
  self.missing_source_penalty
}

///|
pub fn QualityGateRule::enabled(self : QualityGateRule) -> Bool {
  self.enabled
}

///|
pub fn QualityGateRule::with_enabled(
  self : QualityGateRule,
  enabled : Bool,
) -> QualityGateRule {
  {
    id: self.id,
    name: self.name,
    max_age: self.max_age,
    future_skew: self.future_skew,
    require_source: self.require_source,
    reject_invalid: self.reject_invalid,
    reject_stale: self.reject_stale,
    reject_future: self.reject_future,
    reject_control: self.reject_control,
    invalid_penalty: self.invalid_penalty,
    stale_penalty: self.stale_penalty,
    future_penalty: self.future_penalty,
    missing_source_penalty: self.missing_source_penalty,
    enabled,
  }
}

///|
/// Evaluation of one point, including all reasons retained for audit.
pub struct QualityEvaluation {
  station : CommonAddress?
  address : InformationAddress?
  disposition : QualityDisposition
  score : Int
  age : Int
  evaluated_at : Int
  rule_id : Int
  findings : Array[QualityFinding]
  message : String
} derive(Eq, Debug)

///|
pub fn QualityEvaluation::station(self : QualityEvaluation) -> CommonAddress? {
  self.station
}

///|
pub fn QualityEvaluation::address(
  self : QualityEvaluation,
) -> InformationAddress? {
  self.address
}

///|
pub fn QualityEvaluation::disposition(
  self : QualityEvaluation,
) -> QualityDisposition {
  self.disposition
}

///|
pub fn QualityEvaluation::accepted(self : QualityEvaluation) -> Bool {
  self.disposition != QualityRejected
}

///|
pub fn QualityEvaluation::score(self : QualityEvaluation) -> Int {
  self.score
}

///|
pub fn QualityEvaluation::age(self : QualityEvaluation) -> Int {
  self.age
}

///|
pub fn QualityEvaluation::evaluated_at(self : QualityEvaluation) -> Int {
  self.evaluated_at
}

///|
pub fn QualityEvaluation::rule_id(self : QualityEvaluation) -> Int {
  self.rule_id
}

///|
pub fn QualityEvaluation::findings(
  self : QualityEvaluation,
) -> Array[QualityFinding] {
  self.findings.copy()
}

///|
pub fn QualityEvaluation::message(self : QualityEvaluation) -> String {
  self.message
}

///|
/// Aggregate counters used by dashboards and acceptance reports.
pub struct QualityPolicyMetrics {
  mut evaluated : Int
  mut accepted : Int
  mut degraded : Int
  mut rejected : Int
  mut invalid : Int
  mut stale : Int
  mut future : Int
  mut missing_source : Int
  mut control_blocked : Int
} derive(Eq, Debug)

///|
pub fn QualityPolicyMetrics::new() -> QualityPolicyMetrics {
  {
    evaluated: 0,
    accepted: 0,
    degraded: 0,
    rejected: 0,
    invalid: 0,
    stale: 0,
    future: 0,
    missing_source: 0,
    control_blocked: 0,
  }
}

///|
pub fn QualityPolicyMetrics::evaluated(self : QualityPolicyMetrics) -> Int {
  self.evaluated
}

///|
pub fn QualityPolicyMetrics::accepted(self : QualityPolicyMetrics) -> Int {
  self.accepted
}

///|
pub fn QualityPolicyMetrics::degraded(self : QualityPolicyMetrics) -> Int {
  self.degraded
}

///|
pub fn QualityPolicyMetrics::rejected(self : QualityPolicyMetrics) -> Int {
  self.rejected
}

///|
pub fn QualityPolicyMetrics::invalid(self : QualityPolicyMetrics) -> Int {
  self.invalid
}

///|
pub fn QualityPolicyMetrics::stale(self : QualityPolicyMetrics) -> Int {
  self.stale
}

///|
pub fn QualityPolicyMetrics::future(self : QualityPolicyMetrics) -> Int {
  self.future
}

///|
pub fn QualityPolicyMetrics::missing_source(self : QualityPolicyMetrics) -> Int {
  self.missing_source
}

///|
pub fn QualityPolicyMetrics::control_blocked(
  self : QualityPolicyMetrics,
) -> Int {
  self.control_blocked
}

///|
/// Summary returned after evaluating a point-store snapshot.
pub struct QualityPolicyReport {
  total : Int
  accepted : Int
  degraded : Int
  rejected : Int
  evaluations : Array[QualityEvaluation]
} derive(Debug)

///|
pub fn QualityPolicyReport::total(self : QualityPolicyReport) -> Int {
  self.total
}

///|
pub fn QualityPolicyReport::accepted(self : QualityPolicyReport) -> Int {
  self.accepted
}

///|
pub fn QualityPolicyReport::degraded(self : QualityPolicyReport) -> Int {
  self.degraded
}

///|
pub fn QualityPolicyReport::rejected(self : QualityPolicyReport) -> Int {
  self.rejected
}

///|
pub fn QualityPolicyReport::evaluations(
  self : QualityPolicyReport,
) -> Array[QualityEvaluation] {
  self.evaluations.copy()
}

///|
/// Bounded policy engine keyed by rule id and station common address.
pub struct QualityPolicyEngine {
  rules : Map[Int, QualityGateRule]
  station_rules : Map[Int, Int]
  history : Array[QualityEvaluation]
  history_limit : Int
  metrics : QualityPolicyMetrics
} derive(Debug)

///|
pub fn QualityPolicyEngine::new(
  history_limit? : Int = 8192,
) -> Result[QualityPolicyEngine, String] {
  if history_limit < 1 {
    Err("quality policy history limit must be positive")
  } else {
    let engine = {
      rules: Map([]),
      station_rules: Map([]),
      history: [],
      history_limit,
      metrics: QualityPolicyMetrics::new(),
    }
    engine.rules[1] = QualityGateRule::default_rule()
    Ok(engine)
  }
}

///|
pub fn QualityPolicyEngine::metrics(
  self : QualityPolicyEngine,
) -> QualityPolicyMetrics {
  self.metrics
}

///|
pub fn QualityPolicyEngine::history(
  self : QualityPolicyEngine,
) -> Array[QualityEvaluation] {
  self.history.copy()
}

///|
pub fn QualityPolicyEngine::rule(
  self : QualityPolicyEngine,
  id : Int,
) -> QualityGateRule? {
  self.rules.get(id)
}

///|
pub fn QualityPolicyEngine::rule_ids(self : QualityPolicyEngine) -> Array[Int] {
  let result : Array[Int] = []
  for id, _ in self.rules {
    result.push(id)
  }
  result.sort()
  result
}

///|
pub fn QualityPolicyEngine::register_rule(
  self : QualityPolicyEngine,
  rule : QualityGateRule,
) -> Result[Unit, String] {
  if self.rules.contains(rule.id()) {
    Err("quality gate rule already exists")
  } else {
    self.rules[rule.id()] = rule
    Ok(())
  }
}

///|
pub fn QualityPolicyEngine::replace_rule(
  self : QualityPolicyEngine,
  rule : QualityGateRule,
) -> Result[Unit, String] {
  if !self.rules.contains(rule.id()) {
    Err("quality gate rule does not exist")
  } else {
    self.rules[rule.id()] = rule
    Ok(())
  }
}

///|
pub fn QualityPolicyEngine::remove_rule(
  self : QualityPolicyEngine,
  id : Int,
) -> Result[Unit, String] {
  if id == 1 {
    Err("default quality gate rule cannot be removed")
  } else if !self.rules.contains(id) {
    Err("quality gate rule does not exist")
  } else {
    for _, assigned in self.station_rules {
      if assigned == id {
        return Err("quality gate rule is assigned to a station")
      }
    }
    self.rules.remove(id)
    Ok(())
  }
}

///|
pub fn QualityPolicyEngine::assign(
  self : QualityPolicyEngine,
  station : CommonAddress,
  rule_id : Int,
) -> Result[Unit, String] {
  if station.is_global() {
    Err("quality gate assignment cannot target the global station")
  } else if !self.rules.contains(rule_id) {
    Err("quality gate rule does not exist")
  } else {
    self.station_rules[station.number()] = rule_id
    Ok(())
  }
}

///|
pub fn QualityPolicyEngine::unassign(
  self : QualityPolicyEngine,
  station : CommonAddress,
) -> Unit {
  self.station_rules.remove(station.number())
}

///|
pub fn QualityPolicyEngine::assigned_rule(
  self : QualityPolicyEngine,
  station : CommonAddress,
) -> Int {
  match self.station_rules.get(station.number()) {
    None => 1
    Some(id) => id
  }
}

///|
fn clamp_quality_score(score : Int) -> Int {
  if score < 0 {
    0
  } else if score > 100 {
    100
  } else {
    score
  }
}

///|
fn quality_message(findings : Array[QualityFinding]) -> String {
  if findings.length() == 0 {
    "quality gate accepted value"
  } else {
    let parts : Array[String] = []
    for finding in findings {
      parts.push(quality_finding_text(finding))
    }
    "quality gate findings: " + parts.join(",")
  }
}

///|
fn QualityPolicyEngine::record_evaluation(
  self : QualityPolicyEngine,
  evaluation : QualityEvaluation,
) -> Unit {
  self.history.push(evaluation)
  while self.history.length() > self.history_limit {
    ignore(self.history.remove(0))
  }
  self.metrics.evaluated += 1
  match evaluation.disposition() {
    QualityAccepted => self.metrics.accepted += 1
    QualityDegraded => self.metrics.degraded += 1
    QualityRejected => self.metrics.rejected += 1
  }
  for finding in evaluation.findings() {
    match finding {
      QualityInvalidValue => self.metrics.invalid += 1
      QualityStaleValue => self.metrics.stale += 1
      QualityFutureTimestamp => self.metrics.future += 1
      QualityMissingSource => self.metrics.missing_source += 1
      QualityControlBlocked => self.metrics.control_blocked += 1
      QualityUnknownStation => ()
      QualityNoFinding => ()
    }
  }
}

///|
fn evaluate_quality_fields(
  station : CommonAddress?,
  address : InformationAddress?,
  valid : Bool,
  control : Bool,
  timestamp : Int,
  now : Int,
  source : String,
  rule : QualityGateRule,
) -> QualityEvaluation {
  let findings : Array[QualityFinding] = []
  let mut score = 100
  let age = if now >= timestamp { now - timestamp } else { 0 }
  if !valid {
    findings.push(QualityInvalidValue)
    score -= rule.invalid_penalty()
  }
  if age > rule.max_age() {
    findings.push(QualityStaleValue)
    score -= rule.stale_penalty()
  }
  if timestamp > now + rule.future_skew() {
    findings.push(QualityFutureTimestamp)
    score -= rule.future_penalty()
  }
  if rule.require_source() && source.length() == 0 {
    findings.push(QualityMissingSource)
    score -= rule.missing_source_penalty()
  }
  if control && rule.reject_control() {
    findings.push(QualityControlBlocked)
    score = 0
  }
  let mut rejected = false
  for finding in findings {
    match finding {
      QualityInvalidValue => rejected = rejected || rule.reject_invalid()
      QualityStaleValue => rejected = rejected || rule.reject_stale()
      QualityFutureTimestamp => rejected = rejected || rule.reject_future()
      QualityControlBlocked => rejected = true
      _ => ()
    }
  }
  let disposition = if rejected {
    QualityRejected
  } else if findings.length() > 0 {
    QualityDegraded
  } else {
    QualityAccepted
  }
  {
    station,
    address,
    disposition,
    score: clamp_quality_score(score),
    age,
    evaluated_at: now,
    rule_id: rule.id(),
    findings: if findings.length() == 0 {
      [QualityNoFinding]
    } else {
      findings
    },
    message: quality_message(findings),
  }
}

///|
pub fn QualityPolicyEngine::evaluate_point(
  self : QualityPolicyEngine,
  station : CommonAddress,
  record : PointRecord,
  now : Int,
) -> QualityEvaluation {
  let rule_id = self.assigned_rule(station)
  let rule = self.rules.get(rule_id).unwrap()
  let evaluation = evaluate_quality_fields(
    Some(station),
    Some(record.address()),
    record.is_valid(),
    record.direction() == ControlDirection,
    record.updated_at(),
    now,
    record.source(),
    rule,
  )
  self.record_evaluation(evaluation)
  evaluation
}

///|
pub fn QualityPolicyEngine::evaluate_sample(
  self : QualityPolicyEngine,
  sample : TelemetrySample,
  now : Int,
) -> QualityEvaluation {
  let rule_id = self.assigned_rule(sample.station())
  let rule = self.rules.get(rule_id).unwrap()
  let evaluation = evaluate_quality_fields(
    Some(sample.station()),
    Some(sample.address()),
    sample.state() != InvalidTelemetry,
    false,
    sample.timestamp(),
    now,
    sample.source(),
    rule,
  )
  self.record_evaluation(evaluation)
  evaluation
}

///|
pub fn QualityPolicyEngine::evaluate_store(
  self : QualityPolicyEngine,
  store : PointStore,
  now : Int,
) -> QualityPolicyReport {
  let evaluations : Array[QualityEvaluation] = []
  let mut accepted = 0
  let mut degraded = 0
  let mut rejected = 0
  for record in store.query(PointFilter::all()) {
    let evaluation = self.evaluate_point(store.common_address(), record, now)
    evaluations.push(evaluation)
    match evaluation.disposition() {
      QualityAccepted => accepted += 1
      QualityDegraded => degraded += 1
      QualityRejected => rejected += 1
    }
  }
  { total: evaluations.length(), accepted, degraded, rejected, evaluations }
}

///|
pub fn QualityPolicyEngine::history_since(
  self : QualityPolicyEngine,
  evaluated_at : Int,
) -> Array[QualityEvaluation] {
  let result : Array[QualityEvaluation] = []
  for evaluation in self.history {
    if evaluation.evaluated_at() >= evaluated_at {
      result.push(evaluation)
    }
  }
  result
}

///|
pub fn QualityPolicyEngine::rejected_history(
  self : QualityPolicyEngine,
) -> Array[QualityEvaluation] {
  let result : Array[QualityEvaluation] = []
  for evaluation in self.history {
    if evaluation.disposition() == QualityRejected {
      result.push(evaluation)
    }
  }
  result
}

///|
pub fn QualityPolicyEngine::audit_lines(
  self : QualityPolicyEngine,
) -> Array[String] {
  let result : Array[String] = []
  for evaluation in self.history {
    let station_text = match evaluation.station() {
      None => "global"
      Some(station) => "\{station.number()}"
    }
    let address_text = match evaluation.address() {
      None => "none"
      Some(address) => "\{address.number()}"
    }
    result.push(
      "\{evaluation.evaluated_at()} station=\{station_text} " +
      "address=\{address_text} rule=\{evaluation.rule_id()} " +
      "disposition=\{quality_disposition_text(evaluation.disposition())} " +
      "score=\{evaluation.score()} \{evaluation.message()}",
    )
  }
  result
}

///|
pub fn quality_finding_examples() -> Array[QualityFinding] {
  [
    QualityInvalidValue,
    QualityStaleValue,
    QualityFutureTimestamp,
    QualityMissingSource,
    QualityUnknownStation,
    QualityControlBlocked,
    QualityNoFinding,
  ]
}

///|
pub fn quality_policy_examples() -> Array[QualityDisposition] {
  [QualityAccepted, QualityDegraded, QualityRejected]
}

///|
pub fn QualityEvaluation::finding_count(self : QualityEvaluation) -> Int {
  self.findings.length()
}

///|
pub fn QualityEvaluation::has_finding(
  self : QualityEvaluation,
  finding : QualityFinding,
) -> Bool {
  for candidate in self.findings {
    if candidate == finding {
      return true
    }
  }
  false
}

///|
pub fn QualityEvaluation::is_hard_failure(self : QualityEvaluation) -> Bool {
  self.disposition == QualityRejected ||
  self.has_finding(QualityInvalidValue) ||
  self.has_finding(QualityControlBlocked)
}

///|
pub fn QualityEvaluation::worst_finding(
  self : QualityEvaluation,
) -> QualityFinding {
  if self.has_finding(QualityInvalidValue) {
    QualityInvalidValue
  } else if self.has_finding(QualityControlBlocked) {
    QualityControlBlocked
  } else if self.has_finding(QualityFutureTimestamp) {
    QualityFutureTimestamp
  } else if self.has_finding(QualityStaleValue) {
    QualityStaleValue
  } else if self.has_finding(QualityMissingSource) {
    QualityMissingSource
  } else {
    QualityNoFinding
  }
}

///|
pub fn QualityEvaluation::summary_line(self : QualityEvaluation) -> String {
  let station_text = match self.station {
    None => "global"
    Some(station) => "\{station.number()}"
  }
  let address_text = match self.address {
    None => "none"
    Some(address) => "\{address.number()}"
  }
  "\{station_text}/\{address_text} \{quality_disposition_text(self.disposition)} " +
  "score=\{self.score} age=\{self.age} \{self.message}"
}

///|
pub fn QualityPolicyMetrics::has_rejections(
  self : QualityPolicyMetrics,
) -> Bool {
  self.rejected > 0
}

///|
pub fn QualityPolicyMetrics::total_findings(self : QualityPolicyMetrics) -> Int {
  self.invalid +
  self.stale +
  self.future +
  self.missing_source +
  self.control_blocked
}

///|
pub fn QualityPolicyMetrics::acceptance_percent(
  self : QualityPolicyMetrics,
) -> Int {
  if self.evaluated == 0 {
    100
  } else {
    self.accepted * 100 / self.evaluated
  }
}

///|
pub fn QualityPolicyMetrics::quality_percent(
  self : QualityPolicyMetrics,
) -> Int {
  if self.evaluated == 0 {
    100
  } else {
    (self.accepted * 100 + self.degraded * 50) / self.evaluated
  }
}

///|
pub fn QualityPolicyReport::has_rejections(self : QualityPolicyReport) -> Bool {
  self.rejected > 0
}

///|
pub fn QualityPolicyReport::quality_percent(self : QualityPolicyReport) -> Int {
  if self.total == 0 {
    100
  } else {
    (self.accepted * 100 + self.degraded * 50) / self.total
  }
}

///|
pub fn QualityPolicyEngine::assigned_stations(
  self : QualityPolicyEngine,
) -> Array[CommonAddress] {
  let result : Array[CommonAddress] = []
  for number, _ in self.station_rules {
    match CommonAddress::new(number) {
      Ok(station) => result.push(station)
      Err(_) => ()
    }
  }
  result.sort_by((left, right) => left.number() - right.number())
  result
}

///|
pub fn QualityPolicyEngine::stations_for_rule(
  self : QualityPolicyEngine,
  rule_id : Int,
) -> Array[CommonAddress] {
  let result : Array[CommonAddress] = []
  for number, assigned in self.station_rules {
    if assigned == rule_id {
      match CommonAddress::new(number) {
        Ok(station) => result.push(station)
        Err(_) => ()
      }
    }
  }
  result.sort_by((left, right) => left.number() - right.number())
  result
}

///|
pub fn QualityPolicyEngine::remove_assignment(
  self : QualityPolicyEngine,
  station : CommonAddress,
) -> Bool {
  if self.station_rules.contains(station.number()) {
    self.station_rules.remove(station.number())
    true
  } else {
    false
  }
}

///|
pub fn QualityPolicyEngine::clear_history(self : QualityPolicyEngine) -> Unit {
  self.history.clear()
}

///|
pub fn QualityPolicyEngine::history_len(self : QualityPolicyEngine) -> Int {
  self.history.length()
}

///|
pub fn QualityPolicyEngine::enabled_rule_ids(
  self : QualityPolicyEngine,
) -> Array[Int] {
  let result : Array[Int] = []
  for id, rule in self.rules {
    if rule.enabled() {
      result.push(id)
    }
  }
  result.sort()
  result
}

///|
pub fn QualityPolicyEngine::validate_configuration(
  self : QualityPolicyEngine,
) -> Array[String] {
  let issues : Array[String] = []
  if !self.rules.contains(1) {
    issues.push("default quality gate is missing")
  }
  for station, rule_id in self.station_rules {
    if !self.rules.contains(rule_id) {
      issues.push(
        "station \{station} points to unknown quality gate \{rule_id}",
      )
    }
  }
  for id, rule in self.rules {
    if !rule.enabled() {
      for _, assigned in self.station_rules {
        if assigned == id {
          issues.push("disabled quality gate is still assigned")
        }
      }
    }
  }
  issues.sort_by((left, right) => left.compare(right))
  issues
}

///|
pub struct QualityPolicySnapshot {
  rule_count : Int
  assignment_count : Int
  history_count : Int
  metrics : QualityPolicyMetrics
  configuration_issues : Array[String]
} derive(Debug)

///|
pub fn QualityPolicySnapshot::rule_count(self : QualityPolicySnapshot) -> Int {
  self.rule_count
}

///|
pub fn QualityPolicySnapshot::assignment_count(
  self : QualityPolicySnapshot,
) -> Int {
  self.assignment_count
}

///|
pub fn QualityPolicySnapshot::history_count(
  self : QualityPolicySnapshot,
) -> Int {
  self.history_count
}

///|
pub fn QualityPolicySnapshot::metrics(
  self : QualityPolicySnapshot,
) -> QualityPolicyMetrics {
  self.metrics
}

///|
pub fn QualityPolicySnapshot::configuration_issues(
  self : QualityPolicySnapshot,
) -> Array[String] {
  self.configuration_issues.copy()
}

///|
pub fn QualityPolicyEngine::snapshot(
  self : QualityPolicyEngine,
) -> QualityPolicySnapshot {
  {
    rule_count: self.rules.length(),
    assignment_count: self.station_rules.length(),
    history_count: self.history.length(),
    metrics: self.metrics,
    configuration_issues: self.validate_configuration(),
  }
}

///|
pub fn QualityPolicyEngine::evaluate_records(
  self : QualityPolicyEngine,
  station : CommonAddress,
  records : Array[PointRecord],
  now : Int,
) -> QualityPolicyReport {
  let evaluations : Array[QualityEvaluation] = []
  let mut accepted = 0
  let mut degraded = 0
  let mut rejected = 0
  for record in records {
    let evaluation = self.evaluate_point(station, record, now)
    evaluations.push(evaluation)
    match evaluation.disposition() {
      QualityAccepted => accepted += 1
      QualityDegraded => degraded += 1
      QualityRejected => rejected += 1
    }
  }
  { total: evaluations.length(), accepted, degraded, rejected, evaluations }
}

///|
pub fn QualityPolicyEngine::evaluate_object(
  self : QualityPolicyEngine,
  station : CommonAddress,
  object : ApplicationObject,
  source : String,
  timestamp : Int,
  now : Int,
) -> QualityEvaluation {
  let valid = match object.validate() {
    Ok(_) => true
    Err(_) => false
  }
  let rule_id = self.assigned_rule(station)
  let rule = self.rules.get(rule_id).unwrap()
  let evaluation = evaluate_quality_fields(
    Some(station),
    Some(object.address()),
    valid,
    object.type_id().is_control(),
    timestamp,
    now,
    source,
    rule,
  )
  self.record_evaluation(evaluation)
  evaluation
}

///|
pub fn QualityPolicyEngine::filter_accepted(
  _self : QualityPolicyEngine,
  evaluations : Array[QualityEvaluation],
) -> Array[QualityEvaluation] {
  let result : Array[QualityEvaluation] = []
  for evaluation in evaluations {
    if evaluation.accepted() {
      result.push(evaluation)
    }
  }
  result
}

///|
pub fn QualityPolicyEngine::filter_rejected(
  _self : QualityPolicyEngine,
  evaluations : Array[QualityEvaluation],
) -> Array[QualityEvaluation] {
  let result : Array[QualityEvaluation] = []
  for evaluation in evaluations {
    if !evaluation.accepted() {
      result.push(evaluation)
    }
  }
  result
}

///|
pub fn QualityPolicyEngine::finding_counts(
  _self : QualityPolicyEngine,
  evaluations : Array[QualityEvaluation],
) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for evaluation in evaluations {
    for finding in evaluation.findings() {
      let key = quality_finding_text(finding)
      match counts.get(key) {
        None => counts[key] = 1
        Some(count) => counts[key] = count + 1
      }
    }
  }
  counts
}

///|
pub fn QualityPolicyEngine::rule_lines(
  self : QualityPolicyEngine,
) -> Array[String] {
  let lines : Array[String] = []
  for id in self.rule_ids() {
    let rule = self.rules.get(id).unwrap()
    lines.push(
      "\{rule.id()} \{rule.name()} age=\{rule.max_age()} " +
      "future=\{rule.future_skew()} source=\{rule.require_source()} " +
      "enabled=\{rule.enabled()}",
    )
  }
  lines
}

///|
pub fn QualityPolicyEngine::station_rule(
  self : QualityPolicyEngine,
  station : CommonAddress,
) -> QualityGateRule {
  let id = self.assigned_rule(station)
  self.rules.get(id).unwrap()
}

///|
pub fn QualityPolicyEngine::accepts_point(
  self : QualityPolicyEngine,
  station : CommonAddress,
  record : PointRecord,
  now : Int,
) -> Bool {
  self.evaluate_point(station, record, now).accepted()
}

///|
pub fn QualityPolicyEngine::accepts_sample(
  self : QualityPolicyEngine,
  sample : TelemetrySample,
  now : Int,
) -> Bool {
  self.evaluate_sample(sample, now).accepted()
}