///|
/// Non-fatal interpretation diagnostic required by RFC 9457 consumer semantics.
pub(all) enum ProblemDiagnosticKind {
  IgnoredMemberWrongType
} derive(Eq, Debug)

///|
pub struct ProblemDiagnostic {
  kind : ProblemDiagnosticKind
  member_name : String
  expected : String
  actual : String
} derive(Eq, Debug)

///|
/// Successful parse plus non-fatal diagnostics.
pub struct ProblemParseResult {
  problem : ProblemDetails
  diagnostics : Array[ProblemDiagnostic]
} derive(Eq, Debug)

///|
pub fn ProblemDiagnostic::kind(
  self : ProblemDiagnostic,
) -> ProblemDiagnosticKind {
  self.kind
}

///|
pub fn ProblemDiagnostic::member_name(self : ProblemDiagnostic) -> String {
  self.member_name
}

///|
pub fn ProblemDiagnostic::expected(self : ProblemDiagnostic) -> String {
  self.expected
}

///|
pub fn ProblemDiagnostic::actual(self : ProblemDiagnostic) -> String {
  self.actual
}

///|
pub fn ProblemParseResult::problem(self : ProblemParseResult) -> ProblemDetails {
  self.problem
}

///|
pub fn ProblemParseResult::diagnostics(
  self : ProblemParseResult,
) -> Array[ProblemDiagnostic] {
  self.diagnostics.copy()
}

///|
pub fn parse_problem_json(
  input : String,
) -> Result[ProblemParseResult, ProblemError] {
  parse_problem_json_with_limits(input, Limits::default())
}

///|
/// Byte-oriented entry point for network payloads and truncation testing.
pub fn parse_problem_json_bytes(
  input : Bytes,
  limits? : Limits = Limits::default(),
) -> Result[ProblemParseResult, ProblemError] {
  if input.length() > limits.max_input_bytes() {
    return Err(
      problem_error(Input, LimitExceeded, "input exceeds max_input_bytes"),
    )
  }
  let text = @utf8.decode(input) catch {
    _ =>
      return Err(problem_error(Input, InvalidUtf8, "input is not valid UTF-8"))
  }
  parse_problem_json_with_limits(text, limits)
}

///|
/// Parse JSON using core's parser, then interpret RFC 9457 members.
pub fn parse_problem_json_with_limits(
  input : String,
  limits : Limits,
) -> Result[ProblemParseResult, ProblemError] {
  let input_bytes = utf8_length(input)
  if input_bytes > limits.max_input_bytes() {
    return Err(
      problem_error(Input, LimitExceeded, "input exceeds max_input_bytes"),
    )
  }
  let max_nesting_depth = limits.max_nesting_depth()
  let root = @json.parse(input, max_nesting_depth~) catch {
    _ => return Err(problem_error(Json, InvalidJson, "invalid JSON document"))
  }
  let members = match json_object(root) {
    Some(value) => value
    None =>
      return Err(
        problem_error(
          Json,
          RootNotObject,
          "top-level JSON value must be an object",
        ),
      )
  }
  if members.length() > limits.max_members() {
    return Err(
      problem_error(Limit, LimitExceeded, "object exceeds max_members"),
    )
  }
  let diagnostics : Array[ProblemDiagnostic] = []
  let mut type_raw : String? = None
  let mut title : String? = None
  let mut status : Int? = None
  let mut detail : String? = None
  let mut instance : String? = None
  let extensions : Array[ExtensionMember] = []
  let keys = map_keys(members)
  keys.sort()
  for key in keys {
    let value = members[key]
    match key {
      "type" =>
        match json_string(value) {
          Some(text) => type_raw = Some(text)
          None => diagnostics.push(wrong_type(key, "string", value))
        }
      "title" =>
        match json_string(value) {
          Some(text) => title = Some(text)
          None => diagnostics.push(wrong_type(key, "string", value))
        }
      "status" =>
        match json_int(value) {
          Some(code) => status = Some(code)
          None => diagnostics.push(wrong_type(key, "integer number", value))
        }
      "detail" =>
        match json_string(value) {
          Some(text) => detail = Some(text)
          None => diagnostics.push(wrong_type(key, "string", value))
        }
      "instance" =>
        match json_string(value) {
          Some(text) => instance = Some(text)
          None => diagnostics.push(wrong_type(key, "string", value))
        }
      _ => extensions.push(extension_member(key, value))
    }
  }
  if extensions.length() > limits.max_extension_members() {
    return Err(
      problem_error(
        Limit,
        LimitExceeded,
        "object exceeds max_extension_members",
      ),
    )
  }
  diagnostics.sort_by(fn(a, b) { lexical_compare(a.member_name, b.member_name) })
  let parsed = problem_details(
    type_raw, title, status, detail, instance, extensions,
  )
  check_parsed_limits(parsed, limits) catch {
    error => return Err(unwrap_problem_error(error))
  }
  Ok({ problem: parsed, diagnostics })
}

///|
/// Convenience API preserving errors while discarding diagnostics.
pub fn parse_problem(input : String) -> Result[ProblemDetails, ProblemError] {
  match parse_problem_json(input) {
    Ok(parsed) => Ok(parsed.problem())
    Err(error) => Err(error)
  }
}

///|
fn wrong_type(
  member_name : String,
  expected : String,
  value : Json,
) -> ProblemDiagnostic {
  {
    kind: IgnoredMemberWrongType,
    member_name,
    expected,
    actual: json_kind(value),
  }
}

///|
/// Bounded traversal after parse protects strings and aggregate shape.
fn check_parsed_limits(
  problem : ProblemDetails,
  limits : Limits,
) -> Unit raise ProblemError {
  match problem.title() {
    Some(text) if utf8_length(text) > limits.max_title_bytes() =>
      raise problem_error(Limit, LimitExceeded, "title exceeds max_title_bytes")
    _ => ()
  }
  match problem.detail() {
    Some(text) if utf8_length(text) > limits.max_detail_bytes() =>
      raise problem_error(
        Limit,
        LimitExceeded,
        "detail exceeds max_detail_bytes",
      )
    _ => ()
  }
  match problem.type_raw() {
    Some(text) => check_string_limit(text, limits)
    None => ()
  }
  match problem.instance() {
    Some(text) => check_string_limit(text, limits)
    None => ()
  }
  for extension in problem.extensions() {
    check_string_limit(extension.name(), limits)
    check_json_value(extension.value(), limits, 1)
  }
}

///|
fn check_string_limit(
  text : String,
  limits : Limits,
) -> Unit raise ProblemError {
  if utf8_length(text) > limits.max_string_bytes() {
    raise problem_error(Limit, LimitExceeded, "string exceeds max_string_bytes")
  }
}

///|
fn check_json_value(
  value : Json,
  limits : Limits,
  depth : Int,
) -> Unit raise ProblemError {
  if depth > limits.max_nesting_depth() {
    raise problem_error(
      Limit,
      LimitExceeded,
      "extension exceeds max_nesting_depth",
    )
  }
  match value {
    String(text) => check_string_limit(text, limits)
    Array(values) => {
      if values.length() > limits.max_members() {
        raise problem_error(Limit, LimitExceeded, "array exceeds max_members")
      }
      for child in values {
        check_json_value(child, limits, depth + 1)
      }
    }
    Object(members) => {
      if members.length() > limits.max_members() {
        raise problem_error(Limit, LimitExceeded, "object exceeds max_members")
      }
      for key in members.keys() {
        check_string_limit(key, limits)
        check_json_value(members[key], limits, depth + 1)
      }
    }
    _ => ()
  }
}