// Pydantic-style field value constraints (FastAPI's `Query`/`Field(ge=…, le=…, min_length=…,
// pattern=…, …)`), the MoonBit-idiomatic equivalent of the keyword arguments FastAPI reads off a
// parameter's `Annotated` metadata: since MoonBit has no reflection, a field carries its constraints
// as an explicit list. Each constraint is both emitted into the field's OpenAPI/JSON-Schema (so the
// generated spec advertises it) and enforced when an inbound value is validated (so a violation is a
// 422 with the matching Pydantic error type). One source of truth, exactly as pydantic derives both.

///|
/// A value constraint on a field (JSON-Schema keyword ↔ Pydantic argument).
pub(all) enum Constraint {
  Minimum(Double) // ge:  x >= n
  Maximum(Double) // le:  x <= n
  ExclusiveMinimum(Double) // gt:  x > n
  ExclusiveMaximum(Double) // lt:  x < n
  MultipleOf(Double) // multiple_of
  MinLength(Int) // min_length (characters)
  MaxLength(Int) // max_length
  Pattern(String) // pattern (regular expression)
  MinItems(Int) // min_length on a list
  MaxItems(Int) // max_length on a list
} derive(Eq)

///|
/// Merge the constraints into a scalar/array schema object for OpenAPI emission. `exclusiveMinimum`
/// / `exclusiveMaximum` are numeric under OpenAPI 3.1 (JSON-Schema 2020-12) but a boolean flag
/// alongside `minimum` / `maximum` under Swagger 2.0 and OpenAPI 3.0, so the form is version-aware.
pub fn with_constraints(
  j : Json,
  constraints : Array[Constraint],
  version : OpenApiVersion,
) -> Json {
  if constraints.length() == 0 {
    return j
  }
  let m = match j {
    Object(o) => {
      let copy : Map[String, Json] = Map([])
      for k, v in o {
        copy[k] = v
      }
      copy
    }
    _ => return j
  }
  let numeric_exclusive = match version {
    OpenApi31 => true
    _ => false
  }
  for c in constraints {
    match c {
      Minimum(x) => m["minimum"] = x.to_json()
      Maximum(x) => m["maximum"] = x.to_json()
      ExclusiveMinimum(x) =>
        if numeric_exclusive {
          m["exclusiveMinimum"] = x.to_json()
        } else {
          m["minimum"] = x.to_json()
          m["exclusiveMinimum"] = true.to_json()
        }
      ExclusiveMaximum(x) =>
        if numeric_exclusive {
          m["exclusiveMaximum"] = x.to_json()
        } else {
          m["maximum"] = x.to_json()
          m["exclusiveMaximum"] = true.to_json()
        }
      MultipleOf(x) => m["multipleOf"] = x.to_json()
      MinLength(n) => m["minLength"] = n.to_json()
      MaxLength(n) => m["maxLength"] = n.to_json()
      Pattern(p) => m["pattern"] = p.to_json()
      MinItems(n) => m["minItems"] = n.to_json()
      MaxItems(n) => m["maxItems"] = n.to_json()
    }
  }
  m.to_json()
}

///|
/// Enforce the constraints on an inbound `value`, appending a Pydantic-shaped `ValidationError`
/// (located at `loc`) for each violation. A constraint that does not apply to the value's kind
/// (a length bound on a number, say) is simply skipped, as pydantic does.
pub fn check_constraints(
  value : Json,
  constraints : Array[Constraint],
  loc : Array[String],
  errs : Array[ValidationError],
) -> Unit {
  for c in constraints {
    match c {
      Minimum(x) =>
        if value is Number(n, ..) && n < x {
          errs.push(
            constraint_err(
              loc,
              "greater_than_equal",
              "Input should be greater than or equal to " + num_str(x),
            ),
          )
        }
      Maximum(x) =>
        if value is Number(n, ..) && n > x {
          errs.push(
            constraint_err(
              loc,
              "less_than_equal",
              "Input should be less than or equal to " + num_str(x),
            ),
          )
        }
      ExclusiveMinimum(x) =>
        if value is Number(n, ..) && n <= x {
          errs.push(
            constraint_err(
              loc,
              "greater_than",
              "Input should be greater than " + num_str(x),
            ),
          )
        }
      ExclusiveMaximum(x) =>
        if value is Number(n, ..) && n >= x {
          errs.push(
            constraint_err(
              loc,
              "less_than",
              "Input should be less than " + num_str(x),
            ),
          )
        }
      MultipleOf(x) =>
        if value is Number(n, ..) && (x == 0.0 || !is_integral(n / x)) {
          errs.push(
            constraint_err(
              loc,
              "multiple_of",
              "Input should be a multiple of " + num_str(x),
            ),
          )
        }
      MinLength(k) =>
        if value is String(s) && char_len(s) < k {
          errs.push(
            constraint_err(
              loc,
              "string_too_short",
              "String should have at least " + k.to_string() + " characters",
            ),
          )
        }
      MaxLength(k) =>
        if value is String(s) && char_len(s) > k {
          errs.push(
            constraint_err(
              loc,
              "string_too_long",
              "String should have at most " + k.to_string() + " characters",
            ),
          )
        }
      Pattern(p) =>
        if value is String(s) && !pattern_matches(s, p) {
          errs.push(
            constraint_err(
              loc,
              "string_pattern_mismatch",
              "String should match pattern '" + p + "'",
            ),
          )
        }
      MinItems(k) =>
        if value is Array(a) && a.length() < k {
          errs.push(
            constraint_err(
              loc,
              "too_short",
              "List should have at least " + k.to_string() + " items",
            ),
          )
        }
      MaxItems(k) =>
        if value is Array(a) && a.length() > k {
          errs.push(
            constraint_err(
              loc,
              "too_long",
              "List should have at most " + k.to_string() + " items",
            ),
          )
        }
    }
  }
}

///|
/// A Pydantic-shaped constraint `ValidationError` (same shape as `type_error`).
fn constraint_err(
  loc : Array[String],
  kind : String,
  msg : String,
) -> ValidationError {
  ValidationError::type_error(loc, kind, msg)
}

///|
/// Format a constraint bound for a message: an integral value without its `.0`.
fn num_str(x : Double) -> String {
  if is_integral(x) {
    x.to_int().to_string()
  } else {
    x.to_string()
  }
}

///|
/// The character length of a string (code points, not UTF-16 units).
fn char_len(s : String) -> Int {
  let mut n = 0
  for _ in s {
    n = n + 1
  }
  n
}

///|
/// Whether `s` matches the regular expression `p` (a malformed pattern never matches).
fn pattern_matches(s : String, p : String) -> Bool {
  let re = @string.Regex::Regex(p) catch { _ => return false }
  re.execute(s) is Some(_)
}