///|
/// The importance assigned to a rule.
pub(all) enum Severity {
  Info
  Warning
  Error
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Serializable source configuration for one rule.
pub(all) struct RuleDefinition {
  name : String
  expression : String
  message : String
  severity : Severity
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn RuleDefinition::new(
  name : String,
  expression : String,
  message : String,
  severity? : Severity = Error,
) -> RuleDefinition {
  { name, expression, message, severity }
}

///|
pub struct CompiledRule {
  definition : RuleDefinition
  program : Program
}

///|
/// A collection of rules compiled once and reusable across many JSON values.
pub struct RuleSet {
  rules : Array[CompiledRule]
}

///|
pub(all) enum OutcomeStatus {
  Passed
  Failed
  EvaluationError
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct RuleOutcome {
  name : String
  status : OutcomeStatus
  message : String
  severity : Severity
  diagnostic : Diagnostic?
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct RuleReport {
  passed : Bool
  passed_count : Int
  failed_count : Int
  error_count : Int
  outcomes : Array[RuleOutcome]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn RuleReport::to_json_string(
  self : RuleReport,
  indent? : Int = 2,
) -> String {
  self.to_json().stringify(indent~)
}

///|
pub fn RuleSet::compile(
  definitions : Array[RuleDefinition],
) -> Result[RuleSet, Array[Diagnostic]] {
  RuleSet::compile_with_limits(definitions, RuleSetLimits::default())
}

///|
/// Decode and compile a JSON array of rule definitions.
pub fn RuleSet::from_json(config : Json) -> Result[RuleSet, Array[Diagnostic]] {
  RuleSet::from_json_with_limits(config, RuleSetLimits::default())
}

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

///|
/// Evaluate every rule. Runtime errors are recorded per rule so one malformed
/// input path does not hide the rest of the report.
pub fn RuleSet::evaluate(self : RuleSet, context : Json) -> RuleReport {
  let outcomes = []
  let mut passed_count = 0
  let mut failed_count = 0
  let mut error_count = 0
  for rule in self.rules {
    match evaluate(rule.program, context) {
      Ok(True) => {
        passed_count = passed_count + 1
        outcomes.push({
          name: rule.definition.name,
          status: Passed,
          message: rule.definition.message,
          severity: rule.definition.severity,
          diagnostic: None,
        })
      }
      Ok(False) => {
        failed_count = failed_count + 1
        outcomes.push({
          name: rule.definition.name,
          status: Failed,
          message: rule.definition.message,
          severity: rule.definition.severity,
          diagnostic: None,
        })
      }
      Ok(other) => {
        error_count = error_count + 1
        let diagnostic = Diagnostic::new(
          Evaluate,
          "E022",
          "rule returned \{value_kind(other)} instead of boolean",
          rule.program.span(),
        )
        outcomes.push({
          name: rule.definition.name,
          status: EvaluationError,
          message: rule.definition.message,
          severity: rule.definition.severity,
          diagnostic: Some(diagnostic),
        })
      }
      Err(diagnostic) => {
        error_count = error_count + 1
        outcomes.push({
          name: rule.definition.name,
          status: EvaluationError,
          message: rule.definition.message,
          severity: rule.definition.severity,
          diagnostic: Some(diagnostic),
        })
      }
    }
  }
  {
    passed: failed_count == 0 && error_count == 0,
    passed_count,
    failed_count,
    error_count,
    outcomes,
  }
}