///|
/// Validation helper for non-negative integer bounds.
fn validate_bounds(
  minimum : Int?,
  maximum : Int?,
  label : String,
) -> Unit raise SchemaError {
  if minimum is Some(value) && value < 0 {
    raise InvalidSchema("min\{label} must be non-negative")
  }
  if maximum is Some(value) && value < 0 {
    raise InvalidSchema("max\{label} must be non-negative")
  }
  if minimum is Some(min) && maximum is Some(max) && min > max {
    raise InvalidSchema("min\{label} must not exceed max\{label}")
  }
}

///|
fn set_description(fields : Map[String, Json], description : String?) -> Unit {
  if description is Some(desc) {
    fields["description"] = Json::string(desc)
  }
}

///|
/// A JSON Schema boolean schema: `true` (always pass) or `false` (always fail).
priv struct BooleanSchema {
  value : Bool
} derive(Debug)

///|
fn BooleanSchema::always() -> BooleanSchema {
  { value: true }
}

///|
fn BooleanSchema::never() -> BooleanSchema {
  { value: false }
}

///|
impl ToJson for BooleanSchema with fn to_json(self) -> Json {
  Json::boolean(self.value)
}

///|
impl SchemaNode for BooleanSchema with fn validate(
  self : BooleanSchema,
  _instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  if !self.value {
    ctx.push_issue(path, "boolean schema is false")
  }
}

///|
/// A JSON Schema null type schema.
priv struct NullSchema {
  description : String?
} derive(Debug)

///|
fn NullSchema::NullSchema(description? : String) -> NullSchema {
  { description, }
}

///|
impl ToJson for NullSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "type": Json::string("null") }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for NullSchema with fn validate(
  _self : NullSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  if !(instance is Null) {
    ctx.push_issue(path, "expected null")
  }
}

///|
/// A JSON Schema boolean type schema (`type: "boolean"`).
priv struct BoolTypeSchema {
  description : String?
} derive(Debug)

///|
fn BoolTypeSchema::BoolTypeSchema(description? : String) -> BoolTypeSchema {
  { description, }
}

///|
impl ToJson for BoolTypeSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "type": Json::string("boolean") }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for BoolTypeSchema with fn validate(
  _self : BoolTypeSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  if !(instance is (True | False)) {
    ctx.push_issue(path, "expected boolean")
  }
}

///|
/// A JSON Schema string type schema with optional length constraints and description.
priv struct StringSchema {
  min_length : Int?
  max_length : Int?
  description : String?
} derive(Debug)

///|
fn StringSchema::StringSchema(
  min_length? : Int,
  max_length? : Int,
  description? : String,
) -> StringSchema raise SchemaError {
  validate_bounds(min_length, max_length, "Length")
  { min_length, max_length, description }
}

///|
impl ToJson for StringSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "type": Json::string("string") }
  if self.min_length is Some(value) {
    fields["minLength"] = Json::number(
      value.to_double(),
      repr=value.to_string(),
    )
  }
  if self.max_length is Some(value) {
    fields["maxLength"] = Json::number(
      value.to_double(),
      repr=value.to_string(),
    )
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for StringSchema with fn validate(
  self : StringSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  guard instance is String(s) else {
    ctx.push_issue(path, "value does not match type string")
  }
  if self.min_length is Some(min) && s.length() < min {
    ctx.push_issue(path, "string is shorter than minLength")
  }
  if self.max_length is Some(max) && s.length() > max {
    ctx.push_issue(path, "string is longer than maxLength")
  }
}

///|
/// A JSON Schema integer type schema.
priv struct IntegerSchema {
  minimum : Int?
  maximum : Int?
  description : String?
} derive(Debug)

///|
fn IntegerSchema::IntegerSchema(
  minimum? : Int,
  maximum? : Int,
  description? : String,
) -> IntegerSchema raise SchemaError {
  if minimum is Some(min) && maximum is Some(max) && min > max {
    raise InvalidSchema("minimum must not exceed maximum")
  }
  { minimum, maximum, description }
}

///|
impl ToJson for IntegerSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "type": Json::string("integer") }
  if self.minimum is Some(min) {
    fields["minimum"] = Json::number(min.to_double(), repr=min.to_string())
  }
  if self.maximum is Some(max) {
    fields["maximum"] = Json::number(max.to_double(), repr=max.to_string())
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for IntegerSchema with fn validate(
  self : IntegerSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  guard instance is Number(value, ..) && value == value.to_int().to_double() else {
    ctx.push_issue(path, "value does not match type integer")
    return
  }
  if self.minimum is Some(min) && value.to_int() < min {
    ctx.push_issue(path, "number is less than minimum")
  }
  if self.maximum is Some(max) && value.to_int() > max {
    ctx.push_issue(path, "number is greater than maximum")
  }
}

///|
/// A JSON Schema number type schema.
priv struct NumberSchema {
  minimum : Double?
  maximum : Double?
  exclusive_minimum : Double?
  exclusive_maximum : Double?
  multiple_of : Double?
  description : String?
} derive(Debug)

///|
fn NumberSchema::NumberSchema(
  minimum? : Double,
  maximum? : Double,
  exclusive_minimum? : Double,
  exclusive_maximum? : Double,
  multiple_of? : Double,
  description? : String,
) -> NumberSchema raise SchemaError {
  if minimum is Some(min) && maximum is Some(max) && min > max {
    raise InvalidSchema("minimum must not exceed maximum")
  }
  if multiple_of is Some(m) && m <= 0.0 {
    raise InvalidSchema("multipleOf must be positive")
  }
  {
    minimum,
    maximum,
    exclusive_minimum,
    exclusive_maximum,
    multiple_of,
    description,
  }
}

///|
impl ToJson for NumberSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "type": Json::string("number") }
  if self.minimum is Some(min) {
    fields["minimum"] = Json::number(min)
  }
  if self.maximum is Some(max) {
    fields["maximum"] = Json::number(max)
  }
  if self.exclusive_minimum is Some(min) {
    fields["exclusiveMinimum"] = Json::number(min)
  }
  if self.exclusive_maximum is Some(max) {
    fields["exclusiveMaximum"] = Json::number(max)
  }
  if self.multiple_of is Some(m) {
    fields["multipleOf"] = Json::number(m)
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for NumberSchema with fn validate(
  self : NumberSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  guard instance is Number(value, ..) else {
    ctx.push_issue(path, "value does not match type number")
    return
  }
  if self.minimum is Some(min) && value < min {
    ctx.push_issue(path, "number is less than minimum")
  }
  if self.maximum is Some(max) && value > max {
    ctx.push_issue(path, "number is greater than maximum")
  }
  if self.exclusive_minimum is Some(min) && value <= min {
    ctx.push_issue(path, "number is not greater than exclusiveMinimum")
  }
  if self.exclusive_maximum is Some(max) && value >= max {
    ctx.push_issue(path, "number is not less than exclusiveMaximum")
  }
  if self.multiple_of is Some(divisor) && divisor > 0.0 {
    let quotient = value / divisor
    if quotient != quotient.to_int().to_double() {
      ctx.push_issue(path, "number is not a multipleOf value")
    }
  }
}

///|
/// A JSON Schema array type schema.
priv struct ArraySchema {
  items : Schema?
  min_items : Int?
  max_items : Int?
  unique_items : Bool
  description : String?
} derive(Debug)

///|
fn ArraySchema::ArraySchema(
  items? : Schema,
  min_items? : Int,
  max_items? : Int,
  unique_items? : Bool = false,
  description? : String,
) -> ArraySchema raise SchemaError {
  validate_bounds(min_items, max_items, "Items")
  { items, min_items, max_items, unique_items, description }
}

///|
impl ToJson for ArraySchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "type": Json::string("array") }
  if self.items is Some(item_schema) {
    fields["items"] = item_schema.to_json()
  }
  if self.unique_items {
    fields["uniqueItems"] = Json::boolean(true)
  }
  if self.min_items is Some(n) {
    fields["minItems"] = Json::number(n.to_double(), repr=n.to_string())
  }
  if self.max_items is Some(n) {
    fields["maxItems"] = Json::number(n.to_double(), repr=n.to_string())
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for ArraySchema with fn validate(
  self : ArraySchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  guard depth <= ctx.max_depth else {
    ctx.push_issue(path, "validation exceeded max_depth")
    return
  }
  guard instance is Array(values) else {
    ctx.push_issue(path, "value does not match type array")
    return
  }
  if self.min_items is Some(min) && values.length() < min {
    ctx.push_issue(path, "array is shorter than minItems")
  }
  if self.max_items is Some(max) && values.length() > max {
    ctx.push_issue(path, "array is longer than maxItems")
  }
  if self.unique_items {
    let seen : Array[Json] = []
    for value in values {
      if seen.contains(value) {
        ctx.push_issue(path, "array items must be unique")
      }
      seen.push(value)
    }
  }
  if self.items is Some(item_schema) {
    for index, value in values {
      item_schema.node.validate(
        value,
        path=path.add_index(index),
        depth=depth + 1,
        ctx~,
      )
    }
  }
}

///|
/// A JSON Schema object type schema.
priv struct ObjectSchema {
  properties : Map[String, Schema]
  required : Array[String]
  additional_properties : Bool
  min_properties : Int?
  max_properties : Int?
  description : String?
} derive(Debug)

///|
fn ObjectSchema::ObjectSchema(
  properties? : Map[String, Schema] = {},
  required? : Array[String] = [],
  additional_properties? : Bool = true,
  min_properties? : Int,
  max_properties? : Int,
  description? : String,
) -> ObjectSchema raise SchemaError {
  validate_bounds(min_properties, max_properties, "Properties")
  let required_names : Map[String, Bool] = Map([])
  for name in required {
    if required_names.contains(name) {
      raise InvalidSchema("required property names must be unique")
    }
    required_names[name] = true
    if properties.get(name) is None {
      raise InvalidSchema("required property is not declared: \{name}")
    }
  }
  {
    properties,
    required: required.copy(),
    additional_properties,
    min_properties,
    max_properties,
    description,
  }
}

///|
impl ToJson for ObjectSchema with fn to_json(self) -> Json {
  let property_fields : Map[String, Json] = Map([])
  for name, schema in self.properties {
    property_fields[name] = schema.to_json()
  }
  let fields : Map[String, Json] = {
    "type": Json::string("object"),
    "properties": Json::object(property_fields),
    "required": self.required.to_json(),
    "additionalProperties": Json::boolean(self.additional_properties),
  }
  if self.min_properties is Some(n) {
    fields["minProperties"] = Json::number(n.to_double(), repr=n.to_string())
  }
  if self.max_properties is Some(n) {
    fields["maxProperties"] = Json::number(n.to_double(), repr=n.to_string())
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for ObjectSchema with fn validate(
  self : ObjectSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  guard depth <= ctx.max_depth else {
    ctx.push_issue(path, "validation exceeded max_depth")
    return
  }
  guard instance is Object(fields) else {
    ctx.push_issue(path, "value does not match type object")
    return
  }
  if self.min_properties is Some(min) && fields.length() < min {
    ctx.push_issue(path, "object has fewer than minProperties")
  }
  if self.max_properties is Some(max) && fields.length() > max {
    ctx.push_issue(path, "object has more than maxProperties")
  }
  for name in self.required {
    if fields.get(name) is None {
      ctx.push_issue(
        path.add_key(name),
        "required property is missing: \{name}",
      )
    }
  }
  for name, prop_schema in self.properties {
    if fields.get(name) is Some(prop_val) {
      prop_schema.node.validate(
        prop_val,
        path=path.add_key(name),
        depth=depth + 1,
        ctx~,
      )
    }
  }
  if !self.additional_properties {
    for name, _ in fields {
      if !self.properties.contains(name) {
        ctx.push_issue(
          path.add_key(name),
          "additional property is not allowed: \{name}",
        )
      }
    }
  }
}

///|
/// A JSON Schema const assertion schema.
priv struct ConstSchema {
  value : Json
  description : String?
} derive(Debug)

///|
fn[T : ToJson] ConstSchema::from_value(
  value : T,
  description? : String,
) -> ConstSchema {
  { value: value.to_json(), description }
}

///|
impl ToJson for ConstSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "const": self.value }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for ConstSchema with fn validate(
  self : ConstSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  guard instance == self.value else {
    ctx.push_issue(path, "value does not match const")
  }
}

///|
/// A JSON Schema enum assertion schema.
priv struct EnumSchema {
  values : Array[Json]
  description : String?
} derive(Debug)

///|
fn EnumSchema::EnumSchema(
  values : Array[Json],
  description? : String,
) -> EnumSchema raise SchemaError {
  if values.is_empty() {
    raise InvalidSchema("JSON Schema enum must not be empty")
  }
  let seen : Array[Json] = []
  for value in values {
    if seen.contains(value) {
      raise InvalidSchema("JSON Schema enum values must be unique")
    }
    seen.push(value)
  }
  { values: values.copy(), description }
}

///|
fn[T : ToJson] EnumSchema::from_values(
  values : Array[T],
  description? : String,
) -> EnumSchema raise SchemaError {
  let encoded = values.map(fn(v) { v.to_json() })
  EnumSchema(encoded, description?)
}

///|
impl ToJson for EnumSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "enum": self.values.to_json() }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for EnumSchema with fn validate(
  self : EnumSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  ignore(depth)
  guard self.values.contains(instance) else {
    ctx.push_issue(path, "value is not in enum")
  }
}

///|
/// A JSON Schema allOf combinator schema.
priv struct AllOfSchema {
  values : Array[Schema]
  description : String?
} derive(Debug)

///|
fn AllOfSchema::AllOfSchema(
  values : Array[Schema],
  description? : String,
) -> AllOfSchema {
  { values: values.copy(), description }
}

///|
impl ToJson for AllOfSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = {
    "allOf": self.values.map(fn(s) { s.to_json() }).to_json(),
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for AllOfSchema with fn validate(
  self : AllOfSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  for child in self.values {
    child.node.validate(instance, path~, depth=depth + 1, ctx~)
  }
}

///|
/// A JSON Schema anyOf combinator schema.
priv struct AnyOfSchema {
  values : Array[Schema]
  description : String?
} derive(Debug)

///|
fn AnyOfSchema::AnyOfSchema(
  values : Array[Schema],
  description? : String,
) -> AnyOfSchema raise SchemaError {
  if values.is_empty() {
    raise InvalidSchema("JSON Schema anyOf must not be empty")
  }
  { values: values.copy(), description }
}

///|
impl ToJson for AnyOfSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = {
    "anyOf": self.values.map(fn(s) { s.to_json() }).to_json(),
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for AnyOfSchema with fn validate(
  self : AnyOfSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  let mut matched = false
  for child in self.values {
    let sub_ctx : ValidationContext = {
      defs: ctx.defs,
      max_depth: ctx.max_depth,
      issues: [],
    }
    child.node.validate(instance, path~, depth=depth + 1, ctx=sub_ctx)
    if sub_ctx.issues.is_empty() {
      matched = true
    }
  }
  if !matched {
    ctx.push_issue(path, "value does not match anyOf")
  }
}

///|
/// A JSON Schema oneOf combinator schema.
priv struct OneOfSchema {
  values : Array[Schema]
  description : String?
} derive(Debug)

///|
fn OneOfSchema::OneOfSchema(
  values : Array[Schema],
  description? : String,
) -> OneOfSchema raise SchemaError {
  if values.is_empty() {
    raise InvalidSchema("JSON Schema oneOf must not be empty")
  }
  { values: values.copy(), description }
}

///|
impl ToJson for OneOfSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = {
    "oneOf": self.values.map(fn(s) { s.to_json() }).to_json(),
  }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for OneOfSchema with fn validate(
  self : OneOfSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  let mut matches = 0
  for child in self.values {
    let sub_ctx : ValidationContext = {
      defs: ctx.defs,
      max_depth: ctx.max_depth,
      issues: [],
    }
    child.node.validate(instance, path~, depth=depth + 1, ctx=sub_ctx)
    if sub_ctx.issues.is_empty() {
      matches += 1
    }
  }
  if matches != 1 {
    ctx.push_issue(path, "value must match exactly one oneOf branch")
  }
}

///|
/// A JSON Schema not combinator schema.
priv struct NotSchema {
  value : Schema
  description : String?
} derive(Debug)

///|
fn NotSchema::NotSchema(value : Schema, description? : String) -> NotSchema {
  { value, description }
}

///|
impl ToJson for NotSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "not": self.value.to_json() }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for NotSchema with fn validate(
  self : NotSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  let sub_ctx : ValidationContext = {
    defs: ctx.defs,
    max_depth: ctx.max_depth,
    issues: [],
  }
  self.value.node.validate(instance, path~, depth=depth + 1, ctx=sub_ctx)
  if sub_ctx.issues.is_empty() {
    ctx.push_issue(path, "value matches a not schema")
  }
}

///|
/// A JSON Schema reference schema (`$ref`).
priv struct RefSchema {
  reference : String
  description : String?
} derive(Debug)

///|
fn RefSchema::RefSchema(
  reference : String,
  description? : String,
) -> RefSchema raise SchemaError {
  if !reference.has_prefix("#") {
    raise InvalidSchema("network JSON Schema references are disabled")
  }
  { reference, description }
}

///|
impl ToJson for RefSchema with fn to_json(self) -> Json {
  let fields : Map[String, Json] = { "$ref": Json::string(self.reference) }
  set_description(fields, self.description)
  Json::object(fields)
}

///|
impl SchemaNode for RefSchema with fn validate(
  self : RefSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  if depth > ctx.max_depth {
    ctx.push_issue(path, "validation exceeded max_depth")
    return
  }
  match ctx.defs.get(self.reference) {
    Some(target) => target.node.validate(instance, path~, depth=depth + 1, ctx~)
    None => ctx.push_issue(path, "unresolved reference: \{self.reference}")
  }
}

///|
/// An unconstrained escape hatch schema for raw JSON.
priv struct RawSchema {
  value : Json
  parsed : Schema
} derive(Debug)

///|
fn RawSchema::RawSchema(value : Json) -> RawSchema raise SchemaError {
  validate_document_syntax(value)
  let parsed = parse_schema_json(value) catch { _ => Schema::always() }
  { value, parsed }
}

///|
impl ToJson for RawSchema with fn to_json(self) -> Json {
  self.value
}

///|
impl SchemaNode for RawSchema with fn validate(
  self : RawSchema,
  instance : Json,
  path~ : @json.JsonPath,
  depth~ : Int,
  ctx~ : ValidationContext,
) -> Unit {
  self.parsed.node.validate(instance, path~, depth~, ctx~)
}