///|
fn append_typed_rule(schema : Schema, rule : Rule) -> Schema {
  let schema_type = match schema.schema_type {
    OptionalType(inner) => OptionalType(append_typed_rule(inner, rule))
    NullableType(inner) => NullableType(append_typed_rule(inner, rule))
    DefaultType(inner, value) =>
      DefaultType(append_typed_rule(inner, rule), value)
    PrefaultType(inner, value) =>
      PrefaultType(append_typed_rule(inner, rule), value)
    PipeType(input, bridge, output) =>
      PipeType(input, bridge, append_typed_rule(output, rule))
    _ => return { ..schema, id: new_schema_id(), rules: schema.rules + [rule], }
  }
  { ..schema, id: new_schema_id(), schema_type, }
}

///|
fn append_builtin_rule(
  schema : Schema,
  check : (Json) -> Bool,
  message : String,
  code : IssueCode,
  annotation : Json,
  explicit_message~ : Bool = false,
  constraint~ : Constraint? = None,
) -> Schema {
  append_typed_rule(schema, {
    check,
    message,
    code,
    annotation,
    explicit_message,
    constraint: constraint.unwrap_or(builtin_constraint(code, annotation)),
  })
}

///|
fn builtin_constraint(code : IssueCode, annotation : Json) -> Constraint {
  match code {
    TooSmall(origin, value, inclusive) =>
      return Minimum(origin, value, inclusive)
    TooBig(origin, value, inclusive) => return Maximum(origin, value, inclusive)
    NotMultipleOf(value) => return Multiple(value)
    InvalidFormat("integer") => return Integer
    _ => ()
  }
  if annotation is Object(fields) {
    if fields.get("type") is Some(String("integer")) {
      return Integer
    }
    for
      pair in [
        ("minLength", "maxLength", "string"),
        ("minItems", "maxItems", "array"),
      ] {
      let (minimum, maximum, origin) = pair
      match (fields.get(minimum), fields.get(maximum)) {
        (Some(Number(a, ..)), Some(Number(b, ..))) =>
          if a == b {
            return ExactLength(origin, a.to_int())
          }
        _ => ()
      }
    }
    if fields.get("pattern") is Some(String(pattern)) {
      return Pattern(pattern)
    }
    if fields.get("format") is Some(String(format)) {
      return Format(format)
    }
  }
  Opaque
}

///|
/// All rules in their execution order, including checks on built-in effects.
/// These describe execution stages; custom output types require an explicit pipe.
pub fn effective_rules(schema : Schema) -> Array[Rule] {
  let inherited = match schema.schema_type {
    OptionalType(inner)
    | NullableType(inner)
    | DefaultType(inner, _)
    | PrefaultType(inner, _)
    | PreprocessType(_, inner) => effective_rules(inner)
    TransformType(inner, _) => effective_rules(inner)
    PipeType(_, _, output) => effective_rules(output)
    _ => []
  }
  inherited + schema.rules
}

///|
fn escape_regex_literal(value : String) -> String {
  let result = StringBuilder()
  for c in value.to_array() {
    if c == '.' ||
      c == '*' ||
      c == '+' ||
      c == '?' ||
      c == '^' ||
      c == '$' ||
      c == '{' ||
      c == '}' ||
      c == '(' ||
      c == ')' ||
      c == '[' ||
      c == ']' ||
      c == '\\' ||
      c == '|' {
      result.write_char('\\')
    }
    result.write_char(c)
  }
  result.to_string()
}