///|
fn report_signed_integer(
  value : Json,
  location : String,
) -> Result[Int32, String] {
  guard value is Number(_, repr=Some(text)) else {
    return Err("\{location} must be a plain signed 32-bit decimal integer")
  }
  let digits = if text.has_prefix("-") { text[1:].to_owned() } else { text }
  if digits.is_empty() ||
    !digits.code_units().all(c => c >= 48 && c <= 57) ||
    text == "-0" {
    return Err(
      "\{location} must use decimal digits, without fractions, exponents or negative zero",
    )
  }
  let n = @string.parse_int(text) catch {
    _ => return Err("\{location} exceeds the signed 32-bit range")
  }
  Ok(n)
}

///|
/// Restore a version-1 batch without reading SQL files or rerunning analysis.
/// Unknown fields, duplicate keys and inconsistent contexts/exit codes are
/// rejected. Invalid-policy reports are retained with their configuration error.
///
/// ```mbt check
/// test {
///   let original = @migration_lens.review_migrations(
///     [][:],
///     initial_lock_timeout_ms=100,
///   )
///   let loaded = @migration_lens.BatchReport::parse_json(
///     original.to_json().stringify(),
///   ).unwrap()
///   assert_eq(loaded.to_json(), original.to_json())
/// }
/// ```
pub fn BatchReport::parse_json(source : String) -> Result[BatchReport, String] {
  if !valid_unicode(source) {
    return Err("Batch JSON contains invalid Unicode")
  }
  let value = @json.parse(
    source,
    reject_duplicate_keys=true,
    preserve_number_repr=true,
    max_nesting_depth=16,
  ) catch {
    e => return Err("Invalid batch JSON: \{Repr(e)}")
  }
  guard value is Object(root) &&
    value
    is {
      "format_version": version,
      "exit_code": exit,
      "files": Array(items),
      ..
    } else {
    return Err("Batch requires format_version, exit_code and files")
  }
  guard report_integer(version, "format_version") is Ok(1) else {
    return Err("Batch format_version must be the plain integer 1")
  }
  let exit = match report_integer(exit, "exit_code") {
    Ok(n) if n <= 2 => n
    _ => return Err("Batch exit_code must be 0, 1 or 2")
  }
  let initial_lock_timeout_ms = match root.get("initial_lock_timeout_ms") {
    None => None
    Some(value) =>
      match report_signed_integer(value, "initial_lock_timeout_ms") {
        Ok(n) => Some(n)
        Err(e) => return Err(e)
      }
  }
  let files = []
  for i, item in items {
    guard item is Object(fields) &&
      fields.length() == 3 &&
      item is { "path": String(path), "context": context, "report": saved, .. } else {
      return Err("files[\{i}] must contain exactly path, context and report")
    }
    guard context
      is {
        "execution": String(mode),
        "initial_transaction": initial,
        "require_lock_timeout": required,
        "max_lock_timeout_ms": maximum,
        ..
      } else {
      return Err(
        "files[\{i}].context requires execution and every policy field",
      )
    }
    let execution : Execution = match mode {
      "SeparateStatements" => SeparateStatements
      "SimpleQuery" => SimpleQuery
      "ExistingTransaction" => ExistingTransaction
      "Unknown" => Unknown
      _ => return Err("files[\{i}].context.execution is unsupported")
    }
    let initial_transaction = match initial {
      True => true
      False => false
      _ => return Err("files[\{i}].context.initial_transaction must be boolean")
    }
    let require_lock_timeout = match required {
      True => true
      False => false
      _ =>
        return Err("files[\{i}].context.require_lock_timeout must be boolean")
    }
    let max_lock_timeout_ms = match maximum {
      Null => None
      [value] =>
        match
          report_signed_integer(
            value,
            "files[\{i}].context.max_lock_timeout_ms[0]",
          ) {
          Ok(n) => Some(n)
          Err(e) => return Err(e)
        }
      _ =>
        return Err(
          "files[\{i}].context.max_lock_timeout_ms must be null or a one-integer array",
        )
    }
    let report = match Report::parse_json(saved.stringify()) {
      Ok(r) => r
      Err(e) => return Err("files[\{i}].report: \{e}")
    }
    let migration : Migration = {
      path,
      source: report.source,
      execution,
      initial_transaction,
      require_lock_timeout,
      max_lock_timeout_ms,
    }
    if report.complete &&
      (
        execution == Unknown ||
        (max_lock_timeout_ms is Some(n) && n <= 0) ||
        (initial_lock_timeout_ms is Some(n) && n < 0)
      ) {
      return Err(
        "files[\{i}] has a complete report despite unknown execution or an invalid policy",
      )
    }
    if context != migration_context(migration, initial_lock_timeout_ms?) {
      return Err(
        "files[\{i}].context contains unknown/missing fields or inconsistent path/initial timeout",
      )
    }
    files.push({ migration, report, })
  }
  let batch : BatchReport = { files, initial_lock_timeout_ms, }
  if batch.exit_code() != exit || batch.to_json() != value {
    return Err(
      "Batch has unknown/missing fields, an inconsistent exit_code or an invalid configuration_error",
    )
  }
  Ok(batch)
}