///|
/// Resolve one local JSON Pointer from the original document.
///
/// External references are rejected so resolution never reads files or
/// performs network requests. A pointer that targets another Reference Object
/// returns that object; callers may resolve its "$ref" separately.
pub fn Document::resolve_ref(
  self : Document,
  reference : String,
) -> Result[Json, Array[Diagnostic]] {
  resolve_json_pointer(self.source, reference, "$ref")
}

///|
/// Validate a JSON value against a schema after expanding its local references.
///
/// Circular schema references are reported instead of being silently treated
/// as empty schemas. The validator remains transport-independent and never
/// resolves a reference outside the original document.
pub fn Document::validate_schema(
  self : Document,
  value : Json,
  schema : Json,
) -> Result[Unit, Array[Diagnostic]] {
  validate_document_schema_at(self, value, schema, "$")
}

///|
/// Validate a method's named parameters, resolving local schema references.
pub fn Document::validate_named_params(
  self : Document,
  method_name : String,
  params : Map[String, Json],
) -> Result[Unit, Array[Diagnostic]] {
  let method_value = match self.find_method(method_name) {
    Some(value) => value
    None =>
      return Err([
        diagnostic(
          "$.method",
          UnknownMethod,
          "method is not declared by the document",
        ),
      ])
  }
  match validate_named_params(method_value.params, params, "$") {
    Err(diagnostics) => return Err(diagnostics)
    Ok(_) => ()
  }
  for parameter in method_value.params {
    match params.get(parameter.name) {
      None => ()
      Some(value) =>
        match
          validate_document_schema_at(
            self,
            value,
            parameter.schema,
            "$.params." + parameter.name,
          ) {
          Ok(_) => ()
          Err(diagnostics) => return Err(diagnostics)
        }
    }
  }
  Ok(())
}

///|
/// Validate positional method parameters with local schema reference support.
pub fn Document::validate_positional_params(
  self : Document,
  method_name : String,
  values : Array[Json],
) -> Result[Unit, Array[Diagnostic]] {
  let method_value = match self.find_method(method_name) {
    Some(value) => value
    None =>
      return Err([
        diagnostic(
          "$.method",
          UnknownMethod,
          "method is not declared by the document",
        ),
      ])
  }
  match validate_positional_params(method_value.params, values, "$") {
    Err(diagnostics) => return Err(diagnostics)
    Ok(_) => ()
  }
  for index, value in values {
    let parameter = method_value.params[index]
    match
      validate_document_schema_at(
        self,
        value,
        parameter.schema,
        "$.params[" + index.to_string() + "]",
      ) {
      Ok(_) => ()
      Err(diagnostics) => return Err(diagnostics)
    }
  }
  Ok(())
}

///|
/// Validate a method result against its declared schema with local references.
pub fn Document::validate_result(
  self : Document,
  method_name : String,
  value : Json,
) -> Result[Unit, Array[Diagnostic]] {
  let method_value = match self.find_method(method_name) {
    Some(value) => value
    None =>
      return Err([
        diagnostic(
          "$.method",
          UnknownMethod,
          "method is not declared by the document",
        ),
      ])
  }
  match method_value.result {
    None => Ok(())
    Some(result) =>
      validate_document_schema_at(self, value, result.schema, "$.result")
  }
}

///|
/// Validate every example pairing attached to a method.
///
/// Parameter examples are checked against the declared parameter order;
/// result examples are checked against the declared result schema.
pub fn Document::validate_examples(
  self : Document,
  method_name : String,
) -> Result[Unit, Array[Diagnostic]] {
  let method_value = match self.find_method(method_name) {
    Some(value) => value
    None =>
      return Err([
        diagnostic(
          "$.method",
          UnknownMethod,
          "method is not declared by the document",
        ),
      ])
  }
  for parameter_index, parameter in method_value.params {
    for example_index, example in parameter.examples {
      match
        validate_document_schema_at(
          self,
          example.value,
          parameter.schema,
          "$.methods." +
          method_name +
          ".params[" +
          parameter_index.to_string() +
          "].examples[" +
          example_index.to_string() +
          "].value",
        ) {
        Ok(_) => ()
        Err(diagnostics) => return Err(diagnostics)
      }
    }
  }
  match method_value.result {
    None => ()
    Some(result) =>
      for example_index, example in result.examples {
        match
          validate_document_schema_at(
            self,
            example.value,
            result.schema,
            "$.methods." +
            method_name +
            ".result.examples[" +
            example_index.to_string() +
            "].value",
          ) {
          Ok(_) => ()
          Err(diagnostics) => return Err(diagnostics)
        }
      }
  }
  for pairing_index, pairing in method_value.examples {
    let path = "$.methods." +
      method_name +
      ".examples[" +
      pairing_index.to_string() +
      "]"
    let values : Array[Json] = []
    for example in pairing.params {
      values.push(example.value)
    }
    match validate_positional_params(method_value.params, values, path) {
      Err(diagnostics) => return Err(diagnostics)
      Ok(_) => ()
    }
    for parameter_index, example in pairing.params {
      let parameter = method_value.params[parameter_index]
      match
        validate_document_schema_at(
          self,
          example.value,
          parameter.schema,
          path + ".params[" + parameter_index.to_string() + "].value",
        ) {
        Ok(_) => ()
        Err(diagnostics) => return Err(diagnostics)
      }
    }
    match pairing.result {
      None => ()
      Some(example) =>
        match method_value.result {
          None =>
            return Err([
              diagnostic(
                path + ".result",
                InvalidExamplePairing,
                "result example is present but the method has no result descriptor",
              ),
            ])
          Some(result) =>
            match
              validate_document_schema_at(
                self,
                example.value,
                result.schema,
                path + ".result.value",
              ) {
              Ok(_) => ()
              Err(diagnostics) => return Err(diagnostics)
            }
        }
    }
  }
  Ok(())
}

///|
/// Check all reference objects in the source document without rewriting it.
pub fn Document::lint_references(self : Document) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  lint_reference_json(self.source, "$", self.source, diagnostics)
  diagnostics
}

///|
fn validate_document_schema_at(
  document : Document,
  value : Json,
  schema : Json,
  path : String,
) -> Result[Unit, Array[Diagnostic]] {
  let expanded = match expand_schema_references(document, schema, path, []) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  validate_schema_at(value, expanded, path)
}

///|
fn expand_schema_references(
  document : Document,
  schema : Json,
  path : String,
  active : Array[String],
) -> Result[Json, Array[Diagnostic]] {
  match schema {
    Object(object) =>
      match object.get("$ref") {
        Some(String(reference)) => {
          for active_reference in active {
            if active_reference == reference {
              return Err([
                diagnostic(
                  path + ".$ref",
                  CircularReference,
                  "circular schema references cannot be evaluated by the subset validator",
                ),
              ])
            }
          }
          let target = match document.resolve_ref(reference) {
            Ok(value) => value
            Err(diagnostics) => return Err(diagnostics)
          }
          let next_active : Array[String] = []
          for active_reference in active {
            next_active.push(active_reference)
          }
          next_active.push(reference)
          let expanded_target = match
            expand_schema_references(document, target, path, next_active) {
            Ok(value) => value
            Err(diagnostics) => return Err(diagnostics)
          }
          let siblings : Map[String, Json] = Map([])
          for keyword, value in object {
            if keyword != "$ref" {
              siblings[keyword] = value
            }
          }
          if siblings.length() == 0 {
            Ok(expanded_target)
          } else {
            let expanded_siblings = match
              expand_schema_object_references(document, siblings, path, active) {
              Ok(value) => value
              Err(diagnostics) => return Err(diagnostics)
            }
            Ok(
              Json::object({
                "allOf": Json::array([expanded_target, expanded_siblings]),
              }),
            )
          }
        }
        Some(_) =>
          Err([
            diagnostic(path + ".$ref", InvalidField, "$ref must be a string"),
          ])
        None => expand_schema_object_references(document, object, path, active)
      }
    True | False => Ok(schema)
    _ =>
      Err([
        diagnostic(
          path,
          InvalidSchemaBoundary,
          "schema must be a boolean or object",
        ),
      ])
  }
}

///|
fn expand_schema_object_references(
  document : Document,
  schema : Map[String, Json],
  path : String,
  active : Array[String],
) -> Result[Json, Array[Diagnostic]] {
  let expanded : Map[String, Json] = Map([])
  for keyword, value in schema {
    match keyword {
      "properties" =>
        match value {
          Object(properties) =>
            match
              expand_schema_map(
                document,
                properties,
                path + ".properties",
                active,
              ) {
              Ok(value) => expanded[keyword] = value
              Err(diagnostics) => return Err(diagnostics)
            }
          _ => expanded[keyword] = value
        }
      "allOf" | "anyOf" | "oneOf" =>
        match value {
          Array(schemas) =>
            match
              expand_schema_array(
                document,
                schemas,
                path + "." + keyword,
                active,
              ) {
              Ok(value) => expanded[keyword] = Json::array(value)
              Err(diagnostics) => return Err(diagnostics)
            }
          _ => expanded[keyword] = value
        }
      "items" | "not" =>
        match value {
          Array(schemas) =>
            match
              expand_schema_array(
                document,
                schemas,
                path + "." + keyword,
                active,
              ) {
              Ok(value) => expanded[keyword] = Json::array(value)
              Err(diagnostics) => return Err(diagnostics)
            }
          _ =>
            match
              expand_schema_references(
                document,
                value,
                path + "." + keyword,
                active,
              ) {
              Ok(value) => expanded[keyword] = value
              Err(diagnostics) => return Err(diagnostics)
            }
        }
      _ => expanded[keyword] = value
    }
  }
  Ok(Json::object(expanded))
}

///|
fn expand_schema_map(
  document : Document,
  schemas : Map[String, Json],
  path : String,
  active : Array[String],
) -> Result[Json, Array[Diagnostic]] {
  let expanded : Map[String, Json] = Map([])
  for name, schema in schemas {
    match
      expand_schema_references(document, schema, path + "." + name, active) {
      Ok(value) => expanded[name] = value
      Err(diagnostics) => return Err(diagnostics)
    }
  }
  Ok(Json::object(expanded))
}

///|
fn expand_schema_array(
  document : Document,
  schemas : Array[Json],
  path : String,
  active : Array[String],
) -> Result[Array[Json], Array[Diagnostic]] {
  let expanded : Array[Json] = []
  for index, schema in schemas {
    match
      expand_schema_references(
        document,
        schema,
        path + "[" + index.to_string() + "]",
        active,
      ) {
      Ok(value) => expanded.push(value)
      Err(diagnostics) => return Err(diagnostics)
    }
  }
  Ok(expanded)
}

///|
fn lint_reference_json(
  value : Json,
  path : String,
  root : Json,
  diagnostics : Array[Diagnostic],
) -> Unit {
  match value {
    Object(object) => {
      match object.get("$ref") {
        None => ()
        Some(String(reference)) =>
          match resolve_reference_chain(root, reference, path, Map([])) {
            Ok(_) => ()
            Err(errors) =>
              for error in errors {
                diagnostics.push(error)
              }
          }
        Some(_) =>
          diagnostics.push(
            diagnostic(path + ".$ref", InvalidField, "$ref must be a string"),
          )
      }
      for key, child in object {
        lint_reference_json(child, path + "." + key, root, diagnostics)
      }
    }
    Array(values) =>
      for index, child in values {
        lint_reference_json(
          child,
          path + "[" + index.to_string() + "]",
          root,
          diagnostics,
        )
      }
    _ => ()
  }
}

///|
/// Resolve a reference object used in an OpenRPC field, following local
/// reference chains and reporting cycles with a diagnostic.
fn resolve_reference_value(
  root : Json,
  value : Json,
  path : String,
) -> Result[Json, Array[Diagnostic]] {
  match value {
    Object(object) =>
      match object.get("$ref") {
        Some(String(reference)) =>
          resolve_reference_chain(root, reference, path, Map([]))
        Some(_) =>
          Err([
            diagnostic(path + ".$ref", InvalidField, "$ref must be a string"),
          ])
        None => Ok(value)
      }
    _ => Ok(value)
  }
}

///|
fn resolve_reference_chain(
  root : Json,
  reference : String,
  path : String,
  seen : Map[String, Bool],
) -> Result[Json, Array[Diagnostic]] {
  if seen.get(reference) is Some(_) {
    return Err([
      diagnostic(
        path + ".$ref",
        CircularReference,
        "reference chain contains a cycle",
      ),
    ])
  }
  seen[reference] = true
  let value = match resolve_json_pointer(root, reference, path + ".$ref") {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  match value {
    Object(object) =>
      match object.get("$ref") {
        Some(String(next)) => resolve_reference_chain(root, next, path, seen)
        Some(_) =>
          Err([
            diagnostic(path + ".$ref", InvalidField, "$ref must be a string"),
          ])
        None => Ok(value)
      }
    _ => Ok(value)
  }
}

///|
fn resolve_json_pointer(
  root : Json,
  reference : String,
  path : String,
) -> Result[Json, Array[Diagnostic]] {
  if reference == "#" {
    return Ok(root)
  }
  if !reference.has_prefix("#/") {
    return Err([
      diagnostic(
        path,
        UnsupportedReference,
        "only local JSON Pointer references are supported",
      ),
    ])
  }
  let tokens : Array[String] = reference
    .split("/")
    .map(fn(token) { token.to_owned() })
    .collect()
  let mut current = root
  for token_index in 1.. value
      Err(diagnostics) => return Err(diagnostics)
    }
    current = match current {
      Object(object) =>
        match object.get(token) {
          Some(value) => value
          None =>
            return Err([
              diagnostic(
                path,
                UnresolvedReference,
                "local reference does not point to a document value",
              ),
            ])
        }
      Array(values) => {
        let index_value = @json.parse(token) catch {
          _ =>
            return Err([
              diagnostic(
                path,
                UnresolvedReference,
                "array reference token must be a canonical non-negative index",
              ),
            ])
        }
        let index = match index_value {
          Number(number, ..) => {
            let index = number.to_int()
            if index < 0 ||
              index.to_double() != number ||
              index.to_string() != token {
              return Err([
                diagnostic(
                  path,
                  UnresolvedReference,
                  "array reference token must be a canonical non-negative index",
                ),
              ])
            }
            index
          }
          _ =>
            return Err([
              diagnostic(
                path,
                UnresolvedReference,
                "array reference token must be a canonical non-negative index",
              ),
            ])
        }
        if index >= values.length() {
          return Err([
            diagnostic(
              path,
              UnresolvedReference,
              "local reference points beyond the end of an array",
            ),
          ])
        }
        values[index]
      }
      _ =>
        return Err([
          diagnostic(
            path,
            UnresolvedReference,
            "local reference traverses a primitive JSON value",
          ),
        ])
    }
  }
  Ok(current)
}

///|
fn decode_pointer_token(
  token : String,
  path : String,
) -> Result[String, Array[Diagnostic]] {
  let source = token.to_array()
  let output : Array[Char] = []
  let mut index = 0
  while index < source.length() {
    if source[index] == '~' {
      if index + 1 >= source.length() {
        return Err([
          diagnostic(
            path,
            UnsupportedReference,
            "JSON Pointer token has an incomplete escape",
          ),
        ])
      }
      match source[index + 1] {
        '0' => output.push('~')
        '1' => output.push('/')
        _ =>
          return Err([
            diagnostic(
              path,
              UnsupportedReference,
              "JSON Pointer tokens only escape tilde as ~0 and slash as ~1",
            ),
          ])
      }
      index = index + 2
    } else {
      output.push(source[index])
      index = index + 1
    }
  }
  Ok(String::from_array(output))
}