///|
/// Quality test applied to a decoded point value.
pub(all) enum QualityTest {
  RangeTest
  FreshnessTest
  StepTest
  DuplicateTest
  EnumTest
  RequiredTest
} derive(Debug, Eq)

///|
/// Decision produced by a quality pipeline.
pub(all) enum QualityDecision {
  QualityAccepted
  QualitySubstituted
  QualityRejected
  QualityDeferred
} derive(Debug, Eq)

///|
/// A field-level quality rule.
pub(all) struct QualityRule {
  id : String
  point_id : String
  check_kind : QualityTest
  lower : Int?
  upper : Int?
  limit : Int
  allowed : Array[Int]
  mut enabled : Bool
}

///|
pub fn QualityRule::new(
  id : String,
  point_id : String,
  check_kind : QualityTest,
  lower? : Int = 0,
  upper? : Int = 0,
  limit? : Int = 0,
) -> Result[QualityRule, ModbusError] {
  if id == "" || point_id == "" || limit < 0 {
    return Err(InvalidData)
  }
  let selected_lower = if lower == 0 { None } else { Some(lower) }
  let selected_upper = if upper == 0 { None } else { Some(upper) }
  match (selected_lower, selected_upper) {
    (Some(low), Some(high)) => if low >= high { return Err(InvalidData) }
    _ => ()
  }
  Ok({
    id,
    point_id,
    check_kind,
    lower: selected_lower,
    upper: selected_upper,
    limit,
    allowed: [],
    enabled: true,
  })
}

///|
pub fn QualityRule::id(self : QualityRule) -> String {
  self.id
}

///|
pub fn QualityRule::point_id(self : QualityRule) -> String {
  self.point_id
}

///|
pub fn QualityRule::check_kind(self : QualityRule) -> QualityTest {
  self.check_kind
}

///|
pub fn QualityRule::lower(self : QualityRule) -> Int? {
  self.lower
}

///|
pub fn QualityRule::upper(self : QualityRule) -> Int? {
  self.upper
}

///|
pub fn QualityRule::limit(self : QualityRule) -> Int {
  self.limit
}

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

///|
pub fn QualityRule::set_enabled(self : QualityRule, enabled : Bool) -> Unit {
  self.enabled = enabled
}

///|
pub fn QualityRule::allow_value(
  self : QualityRule,
  value : Int,
) -> Result[Unit, ModbusError] {
  if self.allowed.length() >= 256 {
    return Err(CapacityExceeded)
  }
  for existing in self.allowed {
    if existing == value {
      return Ok(())
    }
  }
  self.allowed.push(value)
  Ok(())
}

///|
pub fn quality_test_name(kind : QualityTest) -> String {
  match kind {
    RangeTest => "range"
    FreshnessTest => "freshness"
    StepTest => "step"
    DuplicateTest => "duplicate"
    EnumTest => "enum"
    RequiredTest => "required"
  }
}

///|
/// One quality evaluation with stable evidence for the operator.
pub(all) struct QualityFinding {
  rule_id : String
  point_id : String
  check_kind : QualityTest
  decision : QualityDecision
  value : Int
  timestamp : Int
  message : String
}

///|
pub fn QualityFinding::new(
  rule : QualityRule,
  decision : QualityDecision,
  value : Int,
  timestamp : Int,
  message : String,
) -> Result[QualityFinding, ModbusError] {
  if timestamp < 0 || message == "" {
    return Err(InvalidData)
  }
  Ok({
    rule_id: rule.id(),
    point_id: rule.point_id(),
    check_kind: rule.check_kind(),
    decision,
    value,
    timestamp,
    message,
  })
}

///|
pub fn QualityFinding::rule_id(self : QualityFinding) -> String {
  self.rule_id
}

///|
pub fn QualityFinding::point_id(self : QualityFinding) -> String {
  self.point_id
}

///|
pub fn QualityFinding::check_kind(self : QualityFinding) -> QualityTest {
  self.check_kind
}

///|
pub fn QualityFinding::decision(self : QualityFinding) -> QualityDecision {
  self.decision
}

///|
pub fn QualityFinding::value(self : QualityFinding) -> Int {
  self.value
}

///|
pub fn QualityFinding::timestamp(self : QualityFinding) -> Int {
  self.timestamp
}

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

///|
/// Evaluate a rule against current and previous observations.
pub fn QualityRule::evaluate(
  self : QualityRule,
  value : Int,
  timestamp : Int,
  previous : Int?,
  previous_timestamp : Int?,
) -> Result[QualityFinding, ModbusError] {
  if timestamp < 0 {
    return Err(InvalidData)
  }
  if !self.enabled {
    return QualityFinding::new(
      self,
      QualityDeferred,
      value,
      timestamp,
      "rule-disabled",
    )
  }
  match self.check_kind {
    RangeTest => {
      match self.lower {
        Some(low) =>
          if value < low {
            return QualityFinding::new(
              self,
              QualityRejected,
              value,
              timestamp,
              "below-lower-bound",
            )
          }
        None => ()
      }
      match self.upper {
        Some(high) =>
          if value > high {
            return QualityFinding::new(
              self,
              QualityRejected,
              value,
              timestamp,
              "above-upper-bound",
            )
          }
        None => ()
      }
      QualityFinding::new(
        self,
        QualityAccepted,
        value,
        timestamp,
        "within-range",
      )
    }
    FreshnessTest =>
      match previous_timestamp {
        Some(last) =>
          if timestamp - last > self.limit {
            QualityFinding::new(
              self,
              QualitySubstituted,
              value,
              timestamp,
              "stale-input",
            )
          } else {
            QualityFinding::new(
              self,
              QualityAccepted,
              value,
              timestamp,
              "fresh-input",
            )
          }
        None =>
          QualityFinding::new(
            self,
            QualityDeferred,
            value,
            timestamp,
            "no-baseline",
          )
      }
    StepTest =>
      match previous {
        Some(last) =>
          if (value - last).abs() > self.limit {
            QualityFinding::new(
              self,
              QualityRejected,
              value,
              timestamp,
              "step-too-large",
            )
          } else {
            QualityFinding::new(
              self,
              QualityAccepted,
              value,
              timestamp,
              "step-ok",
            )
          }
        None =>
          QualityFinding::new(
            self,
            QualityDeferred,
            value,
            timestamp,
            "no-baseline",
          )
      }
    DuplicateTest =>
      match previous {
        Some(last) =>
          if value == last {
            QualityFinding::new(
              self,
              QualitySubstituted,
              value,
              timestamp,
              "duplicate-value",
            )
          } else {
            QualityFinding::new(
              self,
              QualityAccepted,
              value,
              timestamp,
              "new-value",
            )
          }
        None =>
          QualityFinding::new(
            self,
            QualityAccepted,
            value,
            timestamp,
            "first-value",
          )
      }
    EnumTest => {
      for allowed in self.allowed {
        if value == allowed {
          return QualityFinding::new(
            self,
            QualityAccepted,
            value,
            timestamp,
            "allowed-enum",
          )
        }
      }
      QualityFinding::new(
        self,
        QualityRejected,
        value,
        timestamp,
        "unknown-enum",
      )
    }
    RequiredTest =>
      QualityFinding::new(self, QualityAccepted, value, timestamp, "present")
  }
}

///|
/// Ordered rules and recent findings for a point pipeline.
pub(all) struct QualityPipeline {
  rules : Array[QualityRule]
  findings : Array[QualityFinding]
  max_findings : Int
}

///|
pub fn QualityPipeline::new(
  max_findings? : Int = 4096,
) -> Result[QualityPipeline, ModbusError] {
  if max_findings < 1 {
    Err(InvalidData)
  } else {
    Ok({ rules: [], findings: [], max_findings })
  }
}

///|
pub fn QualityPipeline::rule_count(self : QualityPipeline) -> Int {
  self.rules.length()
}

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

///|
pub fn QualityPipeline::add_rule(
  self : QualityPipeline,
  rule : QualityRule,
) -> Result[Unit, ModbusError] {
  for existing in self.rules {
    if existing.id() == rule.id() {
      return Err(InvalidData)
    }
  }
  self.rules.push(rule)
  Ok(())
}

///|
pub fn QualityPipeline::rule(
  self : QualityPipeline,
  id : String,
) -> QualityRule? {
  for rule in self.rules {
    if rule.id() == id {
      return Some(rule)
    }
  }
  None
}

///|
/// Evaluate all rules for a point and retain every finding.
pub fn QualityPipeline::evaluate(
  self : QualityPipeline,
  point_id : String,
  value : Int,
  timestamp : Int,
  previous? : Int? = None,
  previous_timestamp? : Int? = None,
) -> Result[QualityDecision, ModbusError] {
  if point_id == "" || timestamp < 0 {
    return Err(InvalidData)
  }
  let selected_previous = previous
  let selected_timestamp = previous_timestamp
  let mut decision = QualityAccepted
  for rule in self.rules {
    if rule.point_id() != point_id {
      continue
    }
    let finding = match
      rule.evaluate(value, timestamp, selected_previous, selected_timestamp) {
      Ok(item) => item
      Err(error) => return Err(error)
    }
    if self.findings.length() >= self.max_findings {
      ignore(self.findings.remove(0))
    }
    self.findings.push(finding)
    match finding.decision() {
      QualityRejected => return Ok(QualityRejected)
      QualitySubstituted =>
        if decision == QualityAccepted {
          decision = QualitySubstituted
        }
      QualityDeferred =>
        if decision == QualityAccepted {
          decision = QualityDeferred
        }
      QualityAccepted => ()
    }
  }
  Ok(decision)
}

///|
pub fn QualityPipeline::findings_for(
  self : QualityPipeline,
  point_id : String,
) -> Array[QualityFinding] {
  let result : Array[QualityFinding] = []
  for finding in self.findings {
    if finding.point_id() == point_id {
      result.push(finding)
    }
  }
  result
}

///|
pub fn QualityPipeline::rejected(self : QualityPipeline) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.decision() == QualityRejected {
      count += 1
    }
  }
  count
}

///|
pub fn QualityPipeline::substituted(self : QualityPipeline) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.decision() == QualitySubstituted {
      count += 1
    }
  }
  count
}

///|
pub fn quality_decision_name(decision : QualityDecision) -> String {
  match decision {
    QualityAccepted => "accepted"
    QualitySubstituted => "substituted"
    QualityRejected => "rejected"
    QualityDeferred => "deferred"
  }
}

///|
/// A compact quality summary for one point.
pub(all) struct QualitySummary {
  accepted : Int
  substituted : Int
  rejected : Int
  deferred : Int
}

///|
pub fn QualityPipeline::summary(
  self : QualityPipeline,
  point_id : String,
) -> QualitySummary {
  let mut accepted = 0
  let mut substituted = 0
  let mut rejected = 0
  let mut deferred = 0
  for finding in self.findings {
    if finding.point_id() == point_id {
      match finding.decision() {
        QualityAccepted => accepted += 1
        QualitySubstituted => substituted += 1
        QualityRejected => rejected += 1
        QualityDeferred => deferred += 1
      }
    }
  }
  { accepted, substituted, rejected, deferred }
}

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

///|
pub fn QualitySummary::substituted(self : QualitySummary) -> Int {
  self.substituted
}

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

///|
pub fn QualitySummary::deferred(self : QualitySummary) -> Int {
  self.deferred
}