///|
/// A JSON value proven valid against the codec's JTD document at decode time.
pub(all) struct ValidatedJson {
  value : Json
} derive(Eq, Debug)

///|
pub fn ValidatedJson::value(self : ValidatedJson) -> Json {
  self.value
}

///|
pub fn ValidatedJson::stringify(
  self : ValidatedJson,
  escape_slash? : Bool = false,
) -> String {
  self.value.stringify(escape_slash~)
}

///|
pub(all) suberror CodecError {
  CodecInvalidSchema(Array[Diagnostic])
  CodecInvalidJson(String)
  CodecValidationFailed(Array[Diagnostic])
} derive(Eq, Debug)

///|
/// Schema-bound JSON codec with the same resource limits as direct validation.
pub(all) struct JtdCodec {
  document : SchemaDocument
  options : ValidationOptions
} derive(Debug)

///|
pub fn JtdCodec::new(
  document : SchemaDocument,
  options? : ValidationOptions = ValidationOptions::default(),
) -> Result[JtdCodec, CodecError] {
  let schema_errors = check_schema(document)
  if !schema_errors.is_empty() {
    Err(CodecInvalidSchema(schema_errors))
  } else {
    Ok({ document, options, })
  }
}

///|
pub fn JtdCodec::document(self : JtdCodec) -> SchemaDocument {
  self.document
}

///|
pub fn JtdCodec::options(self : JtdCodec) -> ValidationOptions {
  self.options
}

///|
/// Validate an already parsed JSON value and wrap it on success.
pub fn JtdCodec::decode_json(
  self : JtdCodec,
  value : Json,
) -> Result[ValidatedJson, CodecError] {
  let report = validate_with(self.document, value, self.options)
  if report.is_valid() {
    Ok({ value, })
  } else {
    Err(CodecValidationFailed(report.errors()))
  }
}

///|
/// Parse JSON text, validate it, and return only a schema-conforming value.
pub fn JtdCodec::decode(
  self : JtdCodec,
  text : StringView,
) -> Result[ValidatedJson, CodecError] {
  let value = @json.parse(text) catch {
    error => return Err(CodecInvalidJson(error.to_string()))
  }
  self.decode_json(value)
}

///|
/// Validate a JSON value before serializing it.
pub fn JtdCodec::encode_json(
  self : JtdCodec,
  value : Json,
  escape_slash? : Bool = false,
) -> Result[String, CodecError] {
  match self.decode_json(value) {
    Ok(validated) => Ok(validated.stringify(escape_slash~))
    Err(error) => Err(error)
  }
}

///|
/// A previously validated value can be serialized without another traversal.
pub fn JtdCodec::encode_validated(
  self : JtdCodec,
  value : ValidatedJson,
  escape_slash? : Bool = false,
) -> String {
  ignore(self)
  value.stringify(escape_slash~)
}

///|
/// Decode multiple independent JSON values, preserving input order.
pub fn JtdCodec::decode_batch(
  self : JtdCodec,
  values : Array[Json],
) -> Array[Result[ValidatedJson, CodecError]] {
  values.map(value => self.decode_json(value))
}

///|
/// Decode a JSON array and validate each element as a separate root instance.
pub fn JtdCodec::decode_json_array(
  self : JtdCodec,
  text : StringView,
) -> Result[Array[ValidatedJson], CodecError] {
  let value = @json.parse(text) catch {
    error => return Err(CodecInvalidJson(error.to_string()))
  }
  guard value is Array(values) else {
    return Err(
      CodecInvalidJson("batch input must be a JSON array of root instances"),
    )
  }
  let output : Array[ValidatedJson] = []
  let diagnostics : Array[Diagnostic] = []
  for index, item in values {
    match self.decode_json(item) {
      Ok(validated) => output.push(validated)
      Err(CodecValidationFailed(errors)) =>
        for diagnostic in errors {
          let prefixed = JsonPointer::root()
            .index(index)
            .append(diagnostic.instance_path())
          diagnostics.push(
            Diagnostic::new(
              diagnostic.code(),
              diagnostic.message(),
              instance_path=prefixed,
              schema_path=diagnostic.schema_path(),
            ),
          )
        }
      Err(error) => return Err(error)
    }
  }
  if diagnostics.is_empty() {
    Ok(output)
  } else {
    Err(CodecValidationFailed(diagnostics))
  }
}

///|
pub fn codec_from_schema(
  schema_text : StringView,
  options? : ValidationOptions = ValidationOptions::default(),
) -> Result[JtdCodec, CodecError] {
  let document = match parse_checked_schema(schema_text) {
    Ok(value) => value
    Err(errors) => return Err(CodecInvalidSchema(errors))
  }
  JtdCodec::new(document, options~)
}

///|
pub fn codec_error_diagnostics(error : CodecError) -> Array[Diagnostic] {
  match error {
    CodecInvalidSchema(errors) | CodecValidationFailed(errors) => errors
    CodecInvalidJson(message) => [Diagnostic::new(InvalidJson, message)]
  }
}

///|
pub fn codec_error_message(error : CodecError) -> String {
  match error {
    CodecInvalidJson(message) => "invalid JSON: " + message
    CodecInvalidSchema(errors) =>
      "invalid JTD schema with " + errors.length().to_string() + " finding(s)"
    CodecValidationFailed(errors) =>
      "JSON instance failed JTD validation with " +
      errors.length().to_string() +
      " finding(s)"
  }
}