///|
pub(all) enum ValueType {
  Text
  Integer
  Boolean
} derive(Eq, Debug)

///|
/// Per-key INI constraint, not JSON Schema/OpenAPI or cross-format configuration.
pub(all) struct Rule {
  section : String
  key : String
  required : Bool
  kind : ValueType
  minimum : Int?
  maximum : Int?
  choices : Array[String]
} derive(Eq, Debug)

///|
pub fn rule(
  section : String,
  key : String,
  kind : ValueType,
  required? : Bool = false,
  minimum? : Int? = None,
  maximum? : Int? = None,
  choices? : Array[String] = [],
) -> Rule {
  { section, key, kind, required, minimum, maximum, choices: choices.copy() }
}

///|
/// Validate effective values. Syntax diagnostics remain on Document separately.
/// Output order: rule order, then unknown keys in physical order.
pub fn Document::validate_schema(
  self : Document,
  rules : Array[Rule],
  allow_unknown? : Bool = true,
) -> Array[Diagnostic] {
  let ds : Array[Diagnostic] = []
  let seen : Array[Rule] = []
  for r in rules {
    if !valid_names(r.section, r.key, self.dialect) ||
      (r.kind != Integer && (r.minimum != None || r.maximum != None)) ||
      (match (r.minimum, r.maximum) {
        (Some(a), Some(b)) => a > b
        _ => false
      }) ||
      seen.any(x => {
        same_name(x.section, r.section, self.dialect) &&
        same_name(x.key, r.key, self.dialect)
      }) {
      ds.push(edit_problem("SCHEMA001", "invalid or duplicate schema rule"))
      continue
    }
    seen.push(r)
    if self.entries(r.section, r.key).is_empty() {
      if r.required {
        ds.push(
          edit_problem(
            "SCHEMA002",
            "required key is missing: [" + r.section + "] " + r.key,
          ),
        )
      }
      continue
    }
    match selected_node(self, r.section, r.key) {
      Err(e) => ds.push(e)
      Ok(n) => {
        match r.kind {
          Text => ()
          Boolean =>
            if boolean(n.value) == None {
              ds.push(diagnostic("SCHEMA003", "expected boolean", n.value_span))
            }
          Integer =>
            match decimal_int(n.value) {
              None =>
                ds.push(
                  diagnostic(
                    "SCHEMA003",
                    "expected decimal signed 32-bit integer",
                    n.value_span,
                  ),
                )
              Some(v) => {
                let below = match r.minimum {
                  Some(min) => v < min
                  None => false
                }
                let above = match r.maximum {
                  Some(max) => v > max
                  None => false
                }
                if below || above {
                  ds.push(
                    diagnostic(
                      "SCHEMA004",
                      "integer outside allowed range",
                      n.value_span,
                    ),
                  )
                }
              }
            }
        }
        if !r.choices.is_empty() && !r.choices.contains(n.value) {
          ds.push(
            diagnostic(
              "SCHEMA005",
              "value is not an allowed choice",
              n.value_span,
            ),
          )
        }
      }
    }
  }
  if !allow_unknown {
    for n in self.nodes {
      if n.kind == Entry &&
        !rules.any(r => {
          same_name(r.section, n.section, self.dialect) &&
          same_name(r.key, n.key, self.dialect)
        }) {
        ds.push(diagnostic("SCHEMA006", "unknown key", n.key_span))
      }
    }
  }
  ds
}