// SPDX-License-Identifier: Apache-2.0

///|
fn json_requested() -> Bool {
  for arg in @env.args() {
    if arg == "--" {
      return false
    }
    if arg == "--json" {
      return true
    }
  }
  false
}

///|
fn json_failure(
  code : String,
  message : String,
  status : Int32,
) -> (Json, Int32) {
  (
    Json::object({
      "schema_version": Json(1),
      "ok": Json(false),
      "error": Json::object({
        "code": Json(code),
        "message": Json(message),
        "format": Json::null(),
        "location": Json::null(),
      }),
    }),
    status,
  )
}

///|
fn json_arg(matches : @argparse.Matches, key : String) -> String? {
  match matches.values.get(key) {
    Some([value, ..]) => Some(value)
    _ => None
  }
}

///|
async fn json_execute(matches : @argparse.Matches) -> (Json, Int32) {
  for flag in ["bench", "stream-only", "help"] {
    if matches.flags.get(flag) == Some(true) {
      return json_failure(
        "invalid_arguments",
        "--json cannot be combined with --" + flag,
        2,
      )
    }
  }
  for name in ["fix", "svg"] {
    if json_arg(matches, name) is Some(_) {
      return json_failure(
        "invalid_arguments",
        "--json is inspection-only; cannot combine with --" + name,
        2,
      )
    }
  }
  let path = match json_arg(matches, "file") {
    Some(path) => path
    None =>
      return json_failure(
        "invalid_arguments", "--json requires an input file", 2,
      )
  }
  let limits = match cli_limits(matches) {
    Ok(value) => value
    Err(message) => return json_failure("invalid_arguments", message, 2)
  }
  let samples = match json_arg(matches, "samples") {
    Some(raw) => {
      let value = @string.parse_int(raw) catch {
        _ =>
          return json_failure(
            "invalid_arguments", "samples must be 1..100000", 2,
          )
      }
      if value < 1 || value > 100000 {
        return json_failure("invalid_arguments", "samples must be 1..100000", 2)
      }
      value
    }
    None => 1000
  }
  let minimum = match json_arg(matches, "min-thickness") {
    Some(raw) => {
      let value = @string.parse_double(raw) catch {
        _ =>
          return json_failure(
            "invalid_arguments", "min-thickness must be finite and nonnegative",
            2,
          )
      }
      if value.is_nan() || value.is_inf() || value < 0.0 {
        return json_failure(
          "invalid_arguments", "min-thickness must be finite and nonnegative", 2,
        )
      }
      value
    }
    None => 0.8
  }
  let nozzle = match json_arg(matches, "nozzle") {
    Some(raw) => {
      let value = @string.parse_double(raw) catch {
        _ =>
          return json_failure(
            "invalid_arguments", "nozzle must be finite and positive", 2,
          )
      }
      if value.is_nan() || value.is_inf() || value <= 0.0 {
        return json_failure(
          "invalid_arguments", "nozzle must be finite and positive", 2,
        )
      }
      Json(value)
    }
    None => Json::null()
  }
  let supports = json_arg(matches, "supports").unwrap_or("unknown")
  if !["on", "off", "unknown"].contains(supports) {
    return json_failure(
      "invalid_arguments", "supports must be on/off/unknown", 2,
    )
  }
  let data = match read_input(path, limits) {
    Ok(data) => data
    Err(message) =>
      return json_failure(
        if message.has_prefix("resource_limit: ") {
          "resource_limit"
        } else {
          "input_read_error"
        },
        message,
        3,
      )
  }
  let mesh = match @stlkit.parse_mesh_checked(data, limits~) {
    Ok(mesh) => mesh
    Err(error) =>
      return (
        Json::object({
          "schema_version": Json(1),
          "ok": Json(false),
          "error": error.to_json(),
        }),
        3,
      )
  }
  match sampling_budget_error(mesh.triangle_count(), samples) {
    Some(message) => return json_failure("resource_limit", message, 3)
    None => ()
  }
  let report = match
    @stlkit.inspection_json(
      mesh,
      format=@stlkit.detect_format(data),
      min_thickness=minimum,
      samples~,
    ) {
    Ok(report) => report
    Err(message) => return json_failure("inspection_error", message, 5)
  }
  guard report is Object(fields) else {
    return json_failure("internal_error", "report is not an object", 5)
  }
  fields.set(
    "scenario",
    Json::object({
      "nozzle_mm": nozzle,
      "supports": Json(supports),
      "process_assessed": Json(false),
    }),
  )
  fields.set("path", Json(path))
  let review = match fields.get("review") {
    Some(Object(values)) =>
      match values.get("needs_review") {
        Some(True) => true
        Some(False) => false
        _ =>
          return json_failure(
            "internal_error", "report has no needs_review boolean", 5,
          )
      }
    _ => return json_failure("internal_error", "report has no review", 5)
  }
  let strict = matches.flags.get("strict") == Some(true)
  (report, if strict && review { 4 } else { 0 })
}

///|
async fn json_main() -> Unit {
  let matches = command().parse() catch {
    error => {
      println(
        json_failure("invalid_arguments", error.to_string(), 2).0.stringify(),
      )
      @sys.exit(2)
      return
    }
  }
  let (document, status) = json_execute(matches) catch {
    error => json_failure("internal_error", error.to_string(), 5)
  }
  // The serializer handles all quoting; computed non-finite values are removed
  // by inspection_json, so stdout is exactly one valid JSON document.
  println(@json.to_json(document).stringify())
  if status != 0 {
    @sys.exit(status)
  }
}