///|
/// A named OpenRPC tag used to group methods.
pub struct Tag {
  name : String
  description : String?
}

///|
/// One variable in an OpenRPC server URL template.
pub struct ServerVariable {
  default_value : String
  enum_values : Array[String]
  description : String?
}

///|
/// A transport-neutral server descriptor from an OpenRPC document.
pub struct Server {
  name : String
  url : String
  description : String?
  variables : Map[String, ServerVariable]
}

///|
/// An inline JSON example attached to a parameter, result, or pairing.
pub struct Example {
  name : String
  summary : String?
  description : String?
  value : Json
}

///|
/// A parameter-to-result example for one method.
pub struct ExamplePairing {
  name : String
  description : String?
  params : Array[Example]
  result : Example?
}

///|
/// Reusable OpenRPC components. Values remain JSON so unsupported schema
/// keywords are preserved instead of discarded.
pub struct Components {
  schemas : Map[String, Json]
  examples : Map[String, Json]
  example_pairings : Map[String, Json]
  content_descriptors : Map[String, Json]
  errors : Map[String, Json]
  links : Map[String, Json]
  tags : Map[String, Json]
}

///|
/// Create an empty component collection.
pub fn Components::empty() -> Components {
  {
    schemas: Map([]),
    examples: Map([]),
    example_pairings: Map([]),
    content_descriptors: Map([]),
    errors: Map([]),
    links: Map([]),
    tags: Map([]),
  }
}

///|
/// Return a named component from a recognized OpenRPC component section.
pub fn Components::find(
  self : Components,
  section : String,
  name : String,
) -> Json? {
  match section {
    "schemas" => self.schemas.get(name)
    "examples" => self.examples.get(name)
    "examplePairings" => self.example_pairings.get(name)
    "contentDescriptors" => self.content_descriptors.get(name)
    "errors" => self.errors.get(name)
    "links" => self.links.get(name)
    "tags" => self.tags.get(name)
    _ => None
  }
}

///|
/// Find a declared server by its stable name.
pub fn Document::find_server(self : Document, name : String) -> Server? {
  for server in self.servers {
    if server.name == name {
      return Some(server)
    }
  }
  None
}

///|
/// Find a document-level tag by its name.
pub fn Document::find_tag(self : Document, name : String) -> Tag? {
  for tag in self.tags {
    if tag.name == name {
      return Some(tag)
    }
  }
  None
}

///|
/// Check whether a method declares a tag with the requested name.
pub fn Method::has_tag(self : Method, name : String) -> Bool {
  for tag in self.tags {
    if tag.name == name {
      return true
    }
  }
  false
}

///|
/// Expand a server URL template using caller values or declared defaults.
///
/// Values are inserted literally; callers remain responsible for URL encoding
/// values that contain reserved URL characters. This function performs no I/O.
pub fn Server::render_url(
  self : Server,
  overrides : Map[String, String],
) -> Result[String, Array[Diagnostic]] {
  for name, _value in overrides {
    if self.variables.get(name) is None {
      return Err([
        diagnostic(
          "$.servers." + self.name + ".variables." + name,
          UnknownServerVariable,
          "server URL override is not declared by this server",
        ),
      ])
    }
  }
  let source = self.url.to_array()
  let output : Array[Char] = []
  let mut index = 0
  while index < source.length() {
    if source[index] == '{' {
      let mut closing : Int? = None
      let first_candidate = index + 1
      for candidate in first_candidate.. value
        None =>
          return Err([
            diagnostic(
              "$.servers." + self.name + ".url",
              InvalidServerTemplate,
              "server URL contains an opening brace without a closing brace",
            ),
          ])
      }
      let name_chars : Array[Char] = []
      let first_name_char = index + 1
      for char_index in first_name_char.. value
        None =>
          return Err([
            diagnostic(
              "$.servers." + self.name + ".variables." + variable_name,
              UnknownServerVariable,
              "server URL references an undeclared variable",
            ),
          ])
      }
      let value = match overrides.get(variable_name) {
        Some(value) => value
        None => variable.default_value
      }
      if variable.enum_values.length() > 0 {
        let mut allowed = false
        for candidate in variable.enum_values {
          if candidate == value {
            allowed = true
            break
          }
        }
        if !allowed {
          return Err([
            diagnostic(
              "$.servers." + self.name + ".variables." + variable_name,
              InvalidServerVariableValue,
              "server URL variable value is not in its declared enum",
            ),
          ])
        }
      }
      for char in value.to_array() {
        output.push(char)
      }
      index = end + 1
    } else if source[index] == '}' {
      return Err([
        diagnostic(
          "$.servers." + self.name + ".url",
          InvalidServerTemplate,
          "server URL contains a closing brace without an opening brace",
        ),
      ])
    } else {
      output.push(source[index])
      index = index + 1
    }
  }
  Ok(String::from_array(output))
}

///|
fn parse_optional_string(
  object : Map[String, Json],
  field : String,
  path : String,
) -> Result[String?, Array[Diagnostic]] {
  match object.get(field) {
    None => Ok(None)
    Some(String(value)) => Ok(Some(value))
    Some(_) =>
      Err([
        diagnostic(
          path + "." + field,
          InvalidField,
          field + " must be a string",
        ),
      ])
  }
}

///|
fn parse_optional_bool(
  object : Map[String, Json],
  field : String,
  path : String,
) -> Result[Bool?, Array[Diagnostic]] {
  match object.get(field) {
    None => Ok(None)
    Some(True) => Ok(Some(true))
    Some(False) => Ok(Some(false))
    Some(_) =>
      Err([
        diagnostic(
          path + "." + field,
          InvalidField,
          field + " must be a boolean",
        ),
      ])
  }
}

///|
fn parse_components(value : Json?) -> Result[Components, Array[Diagnostic]] {
  match value {
    None => Ok(Components::empty())
    Some(Object(object)) => {
      let schemas = match parse_component_group(object, "schemas") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
      let examples = match parse_component_group(object, "examples") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
      let example_pairings = match
        parse_component_group(object, "examplePairings") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
      let content_descriptors = match
        parse_component_group(object, "contentDescriptors") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
      let errors = match parse_component_group(object, "errors") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
      let links = match parse_component_group(object, "links") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
      let tags = match parse_component_group(object, "tags") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
      Ok({
        schemas,
        examples,
        example_pairings,
        content_descriptors,
        errors,
        links,
        tags,
      })
    }
    Some(_) =>
      Err([
        diagnostic("$.components", InvalidField, "components must be an object"),
      ])
  }
}

///|
fn parse_component_group(
  components : Map[String, Json],
  field : String,
) -> Result[Map[String, Json], Array[Diagnostic]] {
  match components.get(field) {
    None => Ok(Map([]))
    Some(Object(values)) => Ok(values)
    Some(_) =>
      Err([
        diagnostic(
          "$.components." + field,
          InvalidField,
          "component section must be an object",
        ),
      ])
  }
}

///|
fn parse_optional_servers(
  value : Json?,
  path : String,
) -> Result[Array[Server], Array[Diagnostic]] {
  match value {
    None => Ok([])
    Some(Array(values)) => {
      let servers : Array[Server] = []
      for index, value in values {
        let server_path = path + "[" + index.to_string() + "]"
        match value {
          Object(object) =>
            match parse_server(object, server_path) {
              Ok(server) => servers.push(server)
              Err(diagnostics) => return Err(diagnostics)
            }
          _ =>
            return Err([
              diagnostic(
                server_path,
                ExpectedObject,
                "each server must be an object",
              ),
            ])
        }
      }
      Ok(servers)
    }
    Some(_) => Err([diagnostic(path, InvalidField, "servers must be an array")])
  }
}

///|
fn required_string(
  object : Map[String, Json],
  field : String,
  path : String,
) -> Result[String, Array[Diagnostic]] {
  match object.get(field) {
    Some(String(value)) => Ok(value)
    Some(_) =>
      Err([
        diagnostic(
          path + "." + field,
          InvalidField,
          field + " must be a string",
        ),
      ])
    None =>
      Err([
        diagnostic(
          path + "." + field,
          MissingField,
          "required field is missing",
        ),
      ])
  }
}

///|
fn parse_server(
  object : Map[String, Json],
  path : String,
) -> Result[Server, Array[Diagnostic]] {
  let name = match required_string(object, "name", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  let url = match required_string(object, "url", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  let description = match parse_optional_string(object, "description", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  let variables = match object.get("variables") {
    None => Map([])
    Some(Object(values)) =>
      match parse_server_variables(values, path + ".variables") {
        Ok(value) => value
        Err(diagnostics) => return Err(diagnostics)
      }
    Some(_) =>
      return Err([
        diagnostic(
          path + ".variables",
          InvalidField,
          "server variables must be an object",
        ),
      ])
  }
  Ok({ name, url, description, variables, })
}

///|
fn parse_server_variables(
  values : Map[String, Json],
  path : String,
) -> Result[Map[String, ServerVariable], Array[Diagnostic]] {
  let variables : Map[String, ServerVariable] = Map([])
  for name, raw_value in values {
    let variable_path = path + "." + name
    guard raw_value is Object(object) else {
      return Err([
        diagnostic(
          variable_path,
          ExpectedObject,
          "server variable must be an object",
        ),
      ])
    }
    let default_value = match
      required_string(object, "default", variable_path) {
      Ok(value) => value
      Err(diagnostics) => return Err(diagnostics)
    }
    let enum_values = match object.get("enum") {
      None => []
      Some(Array(items)) => {
        let values : Array[String] = []
        for index, item in items {
          match item {
            String(value) => values.push(value)
            _ =>
              return Err([
                diagnostic(
                  variable_path + ".enum[" + index.to_string() + "]",
                  InvalidField,
                  "server variable enum entries must be strings",
                ),
              ])
          }
        }
        values
      }
      Some(_) =>
        return Err([
          diagnostic(
            variable_path + ".enum",
            InvalidField,
            "server variable enum must be an array",
          ),
        ])
    }
    let description = match
      parse_optional_string(object, "description", variable_path) {
      Ok(value) => value
      Err(diagnostics) => return Err(diagnostics)
    }
    variables[name] = { default_value, enum_values, description, }
  }
  Ok(variables)
}

///|
fn parse_optional_tags(
  value : Json?,
  path : String,
  root : Json,
) -> Result[Array[Tag], Array[Diagnostic]] {
  match value {
    None => Ok([])
    Some(Array(values)) => {
      let tags : Array[Tag] = []
      let names : Map[String, Bool] = Map([])
      for index, raw_value in values {
        let tag_path = path + "[" + index.to_string() + "]"
        let resolved = match
          resolve_reference_value(root, raw_value, tag_path) {
          Ok(value) => value
          Err(diagnostics) => return Err(diagnostics)
        }
        guard resolved is Object(object) else {
          return Err([
            diagnostic(tag_path, ExpectedObject, "each tag must be an object"),
          ])
        }
        let name = match required_string(object, "name", tag_path) {
          Ok(value) => value
          Err(diagnostics) => return Err(diagnostics)
        }
        if names.get(name) is Some(_) {
          return Err([
            diagnostic(
              tag_path + ".name",
              DuplicateTag,
              "tag names must be unique within the same tag list",
            ),
          ])
        }
        names[name] = true
        let description = match
          parse_optional_string(object, "description", tag_path) {
          Ok(value) => value
          Err(diagnostics) => return Err(diagnostics)
        }
        tags.push({ name, description, })
      }
      Ok(tags)
    }
    Some(_) => Err([diagnostic(path, InvalidField, "tags must be an array")])
  }
}

///|
fn parse_optional_examples(
  value : Json?,
  path : String,
  root : Json,
) -> Result[Array[Example], Array[Diagnostic]] {
  match value {
    None => Ok([])
    Some(Array(values)) => {
      let examples : Array[Example] = []
      for index, value in values {
        let item_path = path + "[" + index.to_string() + "]"
        match parse_example(value, item_path, root) {
          Ok(example) => examples.push(example)
          Err(diagnostics) => return Err(diagnostics)
        }
      }
      Ok(examples)
    }
    Some(_) =>
      Err([diagnostic(path, InvalidField, "examples must be an array")])
  }
}

///|
fn parse_optional_example_pairings(
  value : Json?,
  path : String,
  root : Json,
) -> Result[Array[ExamplePairing], Array[Diagnostic]] {
  match value {
    None => Ok([])
    Some(Array(values)) => {
      let pairings : Array[ExamplePairing] = []
      for index, value in values {
        let item_path = path + "[" + index.to_string() + "]"
        match parse_example_pairing(value, item_path, root) {
          Ok(pairing) => pairings.push(pairing)
          Err(diagnostics) => return Err(diagnostics)
        }
      }
      Ok(pairings)
    }
    Some(_) =>
      Err([diagnostic(path, InvalidField, "examples must be an array")])
  }
}

///|
fn parse_example(
  raw_value : Json,
  path : String,
  root : Json,
) -> Result[Example, Array[Diagnostic]] {
  let value = match resolve_reference_value(root, raw_value, path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  guard value is Object(object) else {
    return Err([diagnostic(path, ExpectedObject, "example must be an object")])
  }
  let name = match required_string(object, "name", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  let value = match object.get("value") {
    Some(value) => value
    None =>
      return Err([
        diagnostic(path + ".value", MissingField, "required field is missing"),
      ])
  }
  let summary = match parse_optional_string(object, "summary", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  let description = match parse_optional_string(object, "description", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  Ok({ name, summary, description, value, })
}

///|
fn parse_example_pairing(
  raw_value : Json,
  path : String,
  root : Json,
) -> Result[ExamplePairing, Array[Diagnostic]] {
  let value = match resolve_reference_value(root, raw_value, path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  guard value is Object(object) else {
    return Err([
      diagnostic(path, ExpectedObject, "example pairing must be an object"),
    ])
  }
  let name = match required_string(object, "name", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  let raw_params = match object.get("params") {
    Some(Array(values)) => values
    Some(_) =>
      return Err([
        diagnostic(
          path + ".params",
          InvalidField,
          "pairing params must be an array",
        ),
      ])
    None =>
      return Err([
        diagnostic(path + ".params", MissingField, "required field is missing"),
      ])
  }
  let params : Array[Example] = []
  for index, raw_param in raw_params {
    let param_path = path + ".params[" + index.to_string() + "]"
    match parse_example(raw_param, param_path, root) {
      Ok(example) => params.push(example)
      Err(diagnostics) => return Err(diagnostics)
    }
  }
  let result = match object.get("result") {
    None => None
    Some(value) =>
      match parse_example(value, path + ".result", root) {
        Ok(example) => Some(example)
        Err(diagnostics) => return Err(diagnostics)
      }
  }
  let description = match parse_optional_string(object, "description", path) {
    Ok(value) => value
    Err(diagnostics) => return Err(diagnostics)
  }
  Ok({ name, description, params, result, })
}