///|
/// Minimum severity that makes a failed rule reject an input.
pub(all) enum FailureThreshold {
  AnyFailure
  WarningOrHigher
  ErrorOnly
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Controls severity handling and early termination for a rule-set evaluation.
pub(all) struct RuleEvaluationOptions {
  failure_threshold : FailureThreshold
  stop_on_blocking_failure : Bool
  stop_on_evaluation_error : Bool
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn RuleEvaluationOptions::default() -> RuleEvaluationOptions {
  {
    failure_threshold: AnyFailure,
    stop_on_blocking_failure: false,
    stop_on_evaluation_error: false,
  }
}

///|
/// Result of evaluating a rule set with an explicit acceptance policy.
pub(all) struct PolicyReport {
  passed : Bool
  evaluated_count : Int
  passed_count : Int
  failed_count : Int
  blocking_count : Int
  advisory_count : Int
  error_count : Int
  stopped_early : Bool
  outcomes : Array[RuleOutcome]
} derive(Eq, Debug, ToJson, FromJson)

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

///|
fn severity_is_blocking(
  threshold : FailureThreshold,
  severity : Severity,
) -> Bool {
  match threshold {
    AnyFailure => true
    WarningOrHigher => severity == Warning || severity == Error
    ErrorOnly => severity == Error
  }
}

///|
fn boolean_result_diagnostic(rule : CompiledRule, value : Json) -> Diagnostic {
  Diagnostic::new(
    Evaluate,
    "E022",
    "rule returned \{value_kind(value)} instead of boolean",
    rule.program.span(),
    hint="Finish the expression with a comparison or boolean operator.",
  )
}

///|
/// Evaluate rules using a configurable failure threshold.
///
/// Informational or warning outcomes remain visible even when the selected
/// policy treats them as advisory. Runtime errors always reject the input.
pub fn RuleSet::evaluate_with_options(
  self : RuleSet,
  context : Json,
  options : RuleEvaluationOptions,
) -> PolicyReport {
  let outcomes = []
  let mut passed_count = 0
  let mut failed_count = 0
  let mut blocking_count = 0
  let mut advisory_count = 0
  let mut error_count = 0
  let mut stopped_early = false
  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
        let blocking = severity_is_blocking(
          options.failure_threshold,
          rule.definition.severity,
        )
        if blocking {
          blocking_count = blocking_count + 1
        } else {
          advisory_count = advisory_count + 1
        }
        outcomes.push({
          name: rule.definition.name,
          status: Failed,
          message: rule.definition.message,
          severity: rule.definition.severity,
          diagnostic: None,
        })
        if blocking && options.stop_on_blocking_failure {
          stopped_early = true
          break
        }
      }
      Ok(other) => {
        error_count = error_count + 1
        outcomes.push({
          name: rule.definition.name,
          status: EvaluationError,
          message: rule.definition.message,
          severity: rule.definition.severity,
          diagnostic: Some(boolean_result_diagnostic(rule, other)),
        })
        if options.stop_on_evaluation_error {
          stopped_early = true
          break
        }
      }
      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),
        })
        if options.stop_on_evaluation_error {
          stopped_early = true
          break
        }
      }
    }
  }
  {
    passed: blocking_count == 0 && error_count == 0,
    evaluated_count: outcomes.length(),
    passed_count,
    failed_count,
    blocking_count,
    advisory_count,
    error_count,
    stopped_early,
    outcomes,
  }
}

///|
/// Options for applying the same compiled rules to several JSON records.
pub(all) struct BatchOptions {
  rule_options : RuleEvaluationOptions
  stop_on_first_rejected_record : Bool
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn BatchOptions::default() -> BatchOptions {
  {
    rule_options: RuleEvaluationOptions::default(),
    stop_on_first_rejected_record: false,
  }
}

///|
/// Result for one zero-based input record.
pub(all) struct BatchItemReport {
  index : Int
  passed : Bool
  report : PolicyReport
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Aggregate result for a batch validation operation.
pub(all) struct BatchReport {
  passed : Bool
  requested_count : Int
  evaluated_count : Int
  accepted_count : Int
  rejected_count : Int
  stopped_early : Bool
  items : Array[BatchItemReport]
} derive(Eq, Debug, ToJson, FromJson)

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

///|
/// Apply one compiled rule set to a batch of independent JSON values.
pub fn RuleSet::evaluate_batch(
  self : RuleSet,
  contexts : Array[Json],
  options? : BatchOptions = BatchOptions::default(),
) -> BatchReport {
  let items = []
  let mut accepted_count = 0
  let mut rejected_count = 0
  let mut stopped_early = false
  for index, context in contexts {
    let report = self.evaluate_with_options(context, options.rule_options)
    if report.passed {
      accepted_count = accepted_count + 1
    } else {
      rejected_count = rejected_count + 1
    }
    items.push({ index, passed: report.passed, report })
    if !report.passed && options.stop_on_first_rejected_record {
      stopped_early = true
      break
    }
  }
  {
    passed: rejected_count == 0,
    requested_count: contexts.length(),
    evaluated_count: items.length(),
    accepted_count,
    rejected_count,
    stopped_early,
    items,
  }
}