///|
/// Versioned CLI report. Locations use SchemaProblem.path's display syntax,
/// not JSON Pointer; non-schema failures have no schema location.
fn schema_check_json(path : String, issues : Array[@pjson.Json]) -> String {
  format_compact(
    @pjson.Object(members=[
      ("reportVersion", @pjson.Number(raw="1")),
      ("schemaFile", @pjson.Text(value=path)),
      (
        "status",
        @pjson.Text(value=if issues.is_empty() { "supported" } else { "error" }),
      ),
      ("issues", @pjson.Array(items=issues)),
    ]),
  )
}

///|
fn schema_report_issue(
  kind : String,
  location : String?,
  message : String,
  stage? : String = "",
) -> @pjson.Json {
  let members = [
    ("kind", @pjson.Text(value=kind)),
    (
      "location",
      match location {
        Some(path) => @pjson.Text(value=path)
        None => @pjson.Null
      },
    ),
    ("message", @pjson.Text(value=message)),
  ]
  @pjson.Object(
    members=if stage == "" {
      members
    } else {
      [("stage", @pjson.Text(value=stage))] + members
    },
  )
}

///|
fn schema_parse_message(diagnostic : JsonDiagnostic) -> String {
  "line " +
  diagnostic.line.to_string() +
  ", column " +
  diagnostic.column.to_string() +
  ": " +
  diagnostic.message
}

///|
fn schema_read_issues(
  error : SchemaReadFailure,
  stage? : String = "",
) -> (String, Array[@pjson.Json]) {
  match error {
    SchemaReadError(path, message) =>
      (path, [schema_report_issue("ReadError", None, message, stage~)])
    SchemaParseError(path, diagnostic) =>
      (
        path,
        [
          schema_report_issue(
            "ParseError",
            None,
            schema_parse_message(diagnostic),
            stage~,
          ),
        ],
      )
    SchemaCompileError(path, problems) =>
      (
        path,
        problems.map(fn(problem) {
          let kind = match problem.kind {
            InvalidSchema => "InvalidSchema"
            UnsupportedFeature => "UnsupportedFeature"
            UnsupportedVersion => "UnsupportedVersion"
          }
          schema_report_issue(kind, Some(problem.path), problem.message, stage~)
        }),
      )
  }
}

///|
fn schema_read_failure_json(error : SchemaReadFailure) -> RunResult {
  let (path, issues) = schema_read_issues(error)
  failure(schema_check_json(path, issues), exit_usage_error)
}

///|
fn schema_validation_report(
  schema_path : String,
  source : String,
  status : String,
  issues : Array[@pjson.Json],
  code : Int,
) -> RunResult {
  let text = format_compact(
    @pjson.Object(members=[
      ("reportVersion", @pjson.Number(raw="1")),
      ("reportType", @pjson.Text(value="validation")),
      ("schemaFile", @pjson.Text(value=schema_path)),
      ("inputFile", @pjson.Text(value=source)),
      ("status", @pjson.Text(value=status)),
      ("issues", @pjson.Array(items=issues)),
    ]),
  )
  if code == exit_success {
    { out: text + "\n", err: "", code, }
  } else {
    failure(text, code)
  }
}

///|
fn schema_evaluation_issue(error : SchemaEvaluationError) -> @pjson.Json {
  let kind = match error.kind {
    ReferenceCycle => "ReferenceCycle"
    ResourceLimit => "ResourceLimit"
  }
  @pjson.Object(members=[
    ("stage", @pjson.Text(value="evaluation")),
    ("kind", @pjson.Text(value=kind)),
    ("location", @pjson.Null),
    ("message", @pjson.Text(value=error.message)),
    ("instancePointer", @pjson.Text(value=error.instance_path)),
    ("schemaPointer", @pjson.Text(value=error.schema_path)),
  ])
}

///|
/// Build the entire batch before choosing a stream. Schema-level failures
/// have no input outcomes; all other issues belong to their input result.
fn schema_batch_report(
  schema_path : String,
  requested : Int,
  results : Array[SchemaInputResult],
  issues : Array[@pjson.Json],
) -> RunResult {
  let mut valid = 0
  let mut invalid = 0
  let mut errors = 0
  let mut code = if issues.is_empty() { exit_success } else { exit_usage_error }
  for result in results {
    match result.status {
      "valid" => valid += 1
      "invalid" => invalid += 1
      _ => errors += 1
    }
    if result.code > code {
      code = result.code
    }
  }
  let status = if !issues.is_empty() || errors > 0 {
    "error"
  } else if invalid > 0 {
    "invalid"
  } else {
    "valid"
  }
  let text = format_compact(
    @pjson.Object(members=[
      ("reportVersion", @pjson.Number(raw="1")),
      ("reportType", @pjson.Text(value="batchValidation")),
      ("schemaFile", @pjson.Text(value=schema_path)),
      ("status", @pjson.Text(value=status)),
      (
        "summary",
        @pjson.Object(members=[
          ("requested", @pjson.Number(raw=requested.to_string())),
          ("processed", @pjson.Number(raw=results.length().to_string())),
          ("valid", @pjson.Number(raw=valid.to_string())),
          ("invalid", @pjson.Number(raw=invalid.to_string())),
          ("errors", @pjson.Number(raw=errors.to_string())),
        ]),
      ),
      ("issues", @pjson.Array(items=issues)),
      (
        "results",
        @pjson.Array(
          items=results.map(fn(result) {
            @pjson.Object(members=[
              ("inputFile", @pjson.Text(value=result.source)),
              ("status", @pjson.Text(value=result.status)),
              ("exitCode", @pjson.Number(raw=result.code.to_string())),
              ("issues", @pjson.Array(items=result.issues)),
            ])
          }),
        ),
      ),
    ]),
  )
  if code == exit_success {
    { out: text + "\n", err: "", code, }
  } else {
    failure(text, code)
  }
}

///|
/// JSONL input still produces one JSON document. Record numbers are actual
/// source lines; schema and whole-input failures have no record results.
fn schema_jsonl_report(
  schema_path : String,
  source : String,
  results : Array[(Int, SchemaInputResult)],
  issues : Array[@pjson.Json],
  code? : Int = exit_success,
) -> RunResult {
  let mut valid = 0
  let mut invalid = 0
  let mut errors = 0
  let mut exit_code = code
  for (_, result) in results {
    match result.status {
      "valid" => valid += 1
      "invalid" => invalid += 1
      _ => errors += 1
    }
    if result.code > exit_code {
      exit_code = result.code
    }
  }
  let status = if !issues.is_empty() || errors > 0 {
    "error"
  } else if invalid > 0 {
    "invalid"
  } else {
    "valid"
  }
  let text = format_compact(
    @pjson.Object(members=[
      ("reportVersion", @pjson.Number(raw="1")),
      ("reportType", @pjson.Text(value="jsonlValidation")),
      ("schemaFile", @pjson.Text(value=schema_path)),
      ("inputFile", @pjson.Text(value=source)),
      ("status", @pjson.Text(value=status)),
      (
        "summary",
        @pjson.Object(members=[
          ("records", @pjson.Number(raw=results.length().to_string())),
          ("valid", @pjson.Number(raw=valid.to_string())),
          ("invalid", @pjson.Number(raw=invalid.to_string())),
          ("errors", @pjson.Number(raw=errors.to_string())),
        ]),
      ),
      ("issues", @pjson.Array(items=issues)),
      (
        "results",
        @pjson.Array(
          items=results.map(fn(entry) {
            let (line, result) = entry
            @pjson.Object(members=[
              ("line", @pjson.Number(raw=line.to_string())),
              ("status", @pjson.Text(value=result.status)),
              ("exitCode", @pjson.Number(raw=result.code.to_string())),
              ("issues", @pjson.Array(items=result.issues)),
            ])
          }),
        ),
      ),
    ]),
  )
  if exit_code == exit_success {
    { out: text + "\n", err: "", code: exit_code, }
  } else {
    failure(text, exit_code)
  }
}