///|
fn coveralls_error(path : String, message : String) -> CoverageError {
  InvalidCoveralls("\{path}: \{message}")
}

///|
fn is_json_space(char : Char) -> Bool {
  char == ' ' || char == '\t' || char == '\r' || char == '\n'
}

///|
fn is_json_hex_digit(char : Char) -> Bool {
  char.is_ascii_digit() ||
  (char >= 'a' && char <= 'f') ||
  (char >= 'A' && char <= 'F')
}

///|
fn json_value_start_after_key(
  chars : Array[Char],
  index : Int,
  key : String,
) -> Int? {
  let marker = "\"\{key}\""
  if !chars_have_prefix(chars, index, marker) {
    return None
  }
  let mut cursor = index + marker.to_array().length()
  while cursor < chars.length() && is_json_space(chars[cursor]) {
    cursor = cursor + 1
  }
  if cursor >= chars.length() || chars[cursor] != ':' {
    return None
  }
  cursor = cursor + 1
  while cursor < chars.length() && is_json_space(chars[cursor]) {
    cursor = cursor + 1
  }
  Some(cursor)
}

///|
fn is_json_unicode_escape(chars : Array[Char], index : Int) -> Bool {
  if index + 5 >= chars.length() ||
    chars[index] != '\\' ||
    chars[index + 1] != 'u' {
    return false
  }
  for offset in 2..<6 {
    if !is_json_hex_digit(chars[index + offset]) {
      return false
    }
  }
  true
}

///|
fn is_json_simple_escape(char : Char) -> Bool {
  char == '"' ||
  char == '\\' ||
  char == '/' ||
  char == 'b' ||
  char == 'f' ||
  char == 'n' ||
  char == 'r' ||
  char == 't'
}

///|
fn copy_json_string(
  chars : Array[Char],
  start : Int,
  output : StringBuilder,
) -> Int {
  output.write_char('"')
  let mut index = start + 1
  while index < chars.length() {
    output.write_char(chars[index])
    if chars[index] == '"' {
      return index + 1
    }
    if chars[index] == '\\' && index + 1 < chars.length() {
      output.write_char(chars[index + 1])
      index = index + 2
    } else {
      index = index + 1
    }
  }
  index
}

///|
fn copy_repaired_path_string(
  chars : Array[Char],
  opening_quote : Int,
  output : StringBuilder,
) -> Int {
  output.write_char('"')
  let mut index = opening_quote + 1
  while index < chars.length() {
    let char = chars[index]
    if char == '"' {
      output.write_char(char)
      return index + 1
    }
    if char != '\\' {
      output.write_char(char)
      index = index + 1
      continue
    }
    if index + 1 < chars.length() && is_json_simple_escape(chars[index + 1]) {
      output.write_char(chars[index])
      output.write_char(chars[index + 1])
      index = index + 2
    } else if is_json_unicode_escape(chars, index) {
      for offset in 0..<6 {
        output.write_char(chars[index + offset])
      }
      index = index + 6
    } else {
      // MoonBit 0.1.20260703 on Windows writes source-file separators as
      // bare backslashes. Only an otherwise-invalid JSON escape is doubled.
      output.write_char('\\')
      output.write_char('\\')
      index = index + 1
    }
  }
  index
}

///|
fn copy_repaired_source_files_array(
  chars : Array[Char],
  start : Int,
  output : StringBuilder,
) -> Int {
  let mut index = start
  let mut array_depth = 0
  let mut object_depth = 0
  while index < chars.length() {
    let char = chars[index]
    if char == '"' {
      if array_depth == 1 && object_depth == 1 {
        match json_value_start_after_key(chars, index, "name") {
          Some(value_start) =>
            if value_start < chars.length() && chars[value_start] == '"' {
              for copy_index in index.. ()
        }
      }
      index = copy_json_string(chars, index, output)
      continue
    }
    output.write_char(char)
    match char {
      '[' => array_depth = array_depth + 1
      ']' => {
        array_depth = array_depth - 1
        if array_depth == 0 {
          return index + 1
        }
      }
      '{' => object_depth = object_depth + 1
      '}' => object_depth = object_depth - 1
      _ => ()
    }
    index = index + 1
  }
  index
}

///|
fn repair_moon_windows_coveralls_names(input : String) -> String {
  let chars = input.to_array()
  let output = StringBuilder(size_hint=input.length())
  let mut index = 0
  let mut object_depth = 0
  let mut array_depth = 0
  while index < chars.length() {
    if chars[index] == '"' {
      if object_depth == 1 && array_depth == 0 {
        match json_value_start_after_key(chars, index, "source_files") {
          Some(value_start) =>
            if value_start < chars.length() && chars[value_start] == '[' {
              for copy_index in index.. ()
        }
      }
      index = copy_json_string(chars, index, output)
    } else {
      let char = chars[index]
      output.write_char(char)
      match char {
        '{' => object_depth = object_depth + 1
        '}' => object_depth = object_depth - 1
        '[' => array_depth = array_depth + 1
        ']' => array_depth = array_depth - 1
        _ => ()
      }
      index = index + 1
    }
  }
  output.to_string()
}

///|
fn coveralls_object(
  value : Json,
  path : String,
) -> Map[String, Json] raise CoverageError {
  match value {
    Object(fields) => fields
    _ => raise coveralls_error(path, "expected object")
  }
}

///|
fn coveralls_array(
  value : Json,
  path : String,
) -> Array[Json] raise CoverageError {
  match value {
    Array(values) => values
    _ => raise coveralls_error(path, "expected array")
  }
}

///|
fn coveralls_string(value : Json, path : String) -> String raise CoverageError {
  match value {
    String(text) => text
    _ => raise coveralls_error(path, "expected string")
  }
}

///|
fn coveralls_int(value : Json, path : String) -> Int raise CoverageError {
  match value {
    Number(number, repr=_) =>
      if number.is_nan() || number.is_inf() || number.trunc() != number {
        raise coveralls_error(path, "expected finite integer")
      } else {
        let integer = number.to_int()
        if integer.to_double() != number {
          raise coveralls_error(path, "integer is outside MoonBit Int range")
        }
        integer
      }
    _ => raise coveralls_error(path, "expected number")
  }
}

///|
fn required_coveralls_field(
  object : Map[String, Json],
  key : String,
  path : String,
) -> Json raise CoverageError {
  match object.get(key) {
    Some(value) => value
    None => raise coveralls_error(path, "missing field \"\{key}\"")
  }
}

///|
fn branch_identifier(value : Json, path : String) -> String raise CoverageError {
  match value {
    String(text) => text
    Number(number, repr=_) =>
      if number.is_nan() || number.is_inf() || number.trunc() != number {
        raise coveralls_error(path, "expected integer or string")
      } else {
        let integer = number.to_int()
        if integer.to_double() != number {
          raise coveralls_error(
            path, "integer identifier is outside MoonBit Int range",
          )
        }
        integer.to_string()
      }
    _ => raise coveralls_error(path, "expected integer or string")
  }
}

///|
fn parse_coveralls_coverage(
  file : FileCoverage,
  coverage : Array[Json],
  path : String,
) -> Unit raise CoverageError {
  for index, value in coverage {
    match value {
      Null => ()
      _ => {
        let hits = coveralls_int(value, "\{path}[\{index}]")
        if hits < 0 {
          raise coveralls_error(
            "\{path}[\{index}]",
            "hit count must not be negative",
          )
        }
        file.lines.push({ line: index + 1, hits })
      }
    }
  }
}

///|
fn parse_coveralls_branches(
  file : FileCoverage,
  branches : Array[Json],
  path : String,
) -> Unit raise CoverageError {
  if branches.length() % 4 != 0 {
    raise coveralls_error(
      path, "flattened branch array length must be divisible by four",
    )
  }
  for group in 0..<(branches.length() / 4) {
    let index = group * 4
    let line = coveralls_int(branches[index], "\{path}[\{index}]")
    if line <= 0 {
      raise coveralls_error("\{path}[\{index}]", "branch line must be positive")
    }
    let block = branch_identifier(branches[index + 1], "\{path}[\{index + 1}]")
    let branch = branch_identifier(branches[index + 2], "\{path}[\{index + 2}]")
    let taken = match branches[index + 3] {
      Null => None
      value => {
        let count = coveralls_int(value, "\{path}[\{index + 3}]")
        if count < 0 {
          raise coveralls_error(
            "\{path}[\{index + 3}]",
            "branch count must not be negative",
          )
        }
        Some(count)
      }
    }
    file.branches.push({ line, block, branch, taken })
  }
}

///|
fn parse_coveralls_file(
  value : Json,
  index : Int,
  strip_prefix : String,
) -> FileCoverage raise CoverageError {
  let path = "$.source_files[\{index}]"
  let object = coveralls_object(value, path)
  let name = coveralls_string(
    required_coveralls_field(object, "name", path),
    "\{path}.name",
  )
  let coverage = coveralls_array(
    required_coveralls_field(object, "coverage", path),
    "\{path}.coverage",
  )
  let file = FileCoverage::new(normalize_path(name, strip_prefix~))
  parse_coveralls_coverage(file, coverage, "\{path}.coverage")
  match object.get("branches") {
    Some(branch_json) =>
      parse_coveralls_branches(
        file,
        coveralls_array(branch_json, "\{path}.branches"),
        "\{path}.branches",
      )
    None => ()
  }
  file
}

///|
fn parse_coveralls_json(input : String) -> Json raise CoverageError {
  try @json.parse(input) catch {
    original_error => {
      let repaired = repair_moon_windows_coveralls_names(input)
      if repaired == input {
        raise InvalidCoveralls("invalid JSON: \{original_error}")
      }
      @json.parse(repaired) catch {
        _ => raise InvalidCoveralls("invalid JSON: \{original_error}")
      }
    }
  } noraise {
    root => root
  }
}

///|
/// Parse Coveralls JSON produced by `moon coverage report -f coveralls`.
///
/// The standard `source_files[].coverage` array and flattened `branches`
/// array are supported. `null` coverage slots are preserved as
/// non-instrumented lines rather than counted as misses. If strict decoding
/// fails, invalid bare Windows separators emitted by MoonBit 0.1.20260703 are
/// repaired only in `source_files[].name` before one strict retry.
pub fn parse_coveralls(
  input : String,
  strip_prefix? : String = "",
) -> CoverageReport raise CoverageError {
  let root = parse_coveralls_json(input)
  let object = coveralls_object(root, "$")
  let source_files = coveralls_array(
    required_coveralls_field(object, "source_files", "$"),
    "$.source_files",
  )
  let report = CoverageReport::new()
  for index, source_file in source_files {
    report.files.push(parse_coveralls_file(source_file, index, strip_prefix))
  }
  report
}