///|
priv suberror ReportCliError {
  MissingArgument(String)
  UnknownFormat(String)
  UnknownSendTarget(String)
} derive(Debug)

///|
priv enum OutputFormat {
  Bisect
  SimplifiedCaret
  Caret
  Html
  Coveralls
  Cobertura
  Summary
  FullSummary
  SortedSummary
}

///|
fn compiler_command() -> @argparse.Command {
  Command(
    "moon_cove",
    about="Generate reports from MoonBit compiler coverage data.",
    version="0.3.2",
    flags=[
      FlagArg(
        "ignore-missing-files",
        about="Skip missing source files.",
        negatable=true,
      ),
      FlagArg("absolute-file-paths", about="Keep absolute paths in reports."),
      FlagArg("verbose", about="Print collection diagnostics."),
      FlagArg("coveralls-parallel", about="Mark a parallel Coveralls build."),
      FlagArg(
        "coveralls-include-git-info",
        about="Include Git metadata in Coveralls output.",
      ),
    ],
    options=[
      OptionArg(
        "runtime-log",
        short='t',
        action=Append,
        about="Coverage runtime log.",
      ),
      OptionArg(
        "format",
        short='f',
        action=Append,
        default_values=["bisect"],
        about="Report format: bisect, caret, simp_caret, coveralls, cobertura, html, summary, full_summary, or sorted_summary.",
      ),
      OptionArg("output", short='o', about="Output file or directory."),
      OptionArg("package", short='p', about="Only include this package."),
      OptionArg("file", short='F', about="Only include this source file."),
      OptionArg(
        "source-paths",
        default_values=["."],
        about="Comma-separated source roots.",
      ),
      OptionArg(
        "service-name",
        default_values=[""],
        about="Service name for Coveralls metadata.",
      ),
      OptionArg(
        "service-number",
        default_values=[""],
        about="Build number for Coveralls metadata.",
      ),
      OptionArg(
        "service-job-id",
        default_values=[""],
        about="CI job identifier for uploads.",
      ),
      OptionArg(
        "service-pull-request",
        default_values=[""],
        about="Pull request number for uploads.",
      ),
      OptionArg(
        "coveralls-token",
        default_values=[""],
        about="Coveralls or Codecov upload token.",
      ),
      OptionArg(
        "send-to",
        about="Upload the coveralls report to coveralls or codecov.",
      ),
    ],
    positionals=[
      PositionArg(
        "trace-sources",
        num_args=ValueRange(),
        about=".trace.source files",
      ),
    ],
  )
}

///|
fn cli_values(matches : @argparse.Matches, name : String) -> Array[String] {
  matches.values.get(name).unwrap_or([])
}

///|
fn cli_value(
  matches : @argparse.Matches,
  name : String,
) -> String raise ReportCliError {
  guard cli_values(matches, name) is [value, ..] else {
    raise MissingArgument(name)
  }
  value
}

///|
fn cli_optional(matches : @argparse.Matches, name : String) -> String? {
  cli_values(matches, name).get(0)
}

///|
fn cli_flag(
  matches : @argparse.Matches,
  name : String,
  default? : Bool = false,
) -> Bool {
  matches.flags.get(name).unwrap_or(default)
}

///|
fn parse_format(value : String) -> OutputFormat raise ReportCliError {
  match value.to_lower() {
    "bisect" => Bisect
    "simp_caret" => SimplifiedCaret
    "caret" => Caret
    "html" => Html
    "coveralls" => Coveralls
    "cobertura" => Cobertura
    "summary" => Summary
    "full_summary" => FullSummary
    "sorted_summary" => SortedSummary
    _ => raise UnknownFormat(value)
  }
}

///|
fn default_output(format : OutputFormat) -> String? {
  match format {
    Bisect => Some("bisect.coverage")
    Html => Some("_coverage")
    Coveralls => Some("coveralls.json")
    Cobertura => Some("cobertura.xml")
    SimplifiedCaret | Caret | Summary | FullSummary | SortedSummary => None
  }
}

///|
async fn write_output(path : String, data : &@io.Data) -> Unit {
  @report.mkdirs(@path.Path(path).dirname().to_string())
  @fs.write_file(path, data, create_mode=CreateOrTruncate)
}

///|
fn output_path(
  matches : @argparse.Matches,
  format : OutputFormat,
) -> String raise ReportCliError {
  match cli_optional(matches, "output") {
    Some(path) => path
    None => default_output(format).unwrap_or_error(MissingArgument("output"))
  }
}

///|
async fn run_compiler_report(
  matches : @argparse.Matches,
  current_directory? : String = ".",
) -> Unit {
  // Preserve the legacy reporter's last-format-wins behavior.
  guard cli_values(matches, "format") is [.., value] else {
    raise MissingArgument("format")
  }
  let format = parse_format(value)
  let source_paths = cli_value(matches, "source-paths")
    .split(",")
    .map(part => part.to_owned())
    .to_array()
  let prepared = @collector.prepare({
    trace_sources: cli_values(matches, "trace-sources"),
    runtime_logs: cli_values(matches, "runtime-log"),
    source_paths,
    package_filter: cli_optional(matches, "package"),
    file_filter: cli_optional(matches, "file"),
    absolute_paths: cli_flag(matches, "absolute-file-paths"),
    current_directory,
  })
  let coverage = prepared.to_bisect()
  if cli_flag(matches, "verbose") {
    @stdio.stderr.write("Collected \{coverage.length()} source file(s)\n")
  }
  match format {
    Bisect =>
      write_output(
        output_path(matches, format),
        @common.write_coverage(coverage),
      )
    Html =>
      @html.output(
        to_directory=output_path(matches, format),
        title="",
        tab_size=2,
        theme=Auto,
        coverage~,
        source_paths~,
        ignore_missing_files=cli_flag(
          matches,
          "ignore-missing-files",
          default=true,
        ),
      )
    SimplifiedCaret =>
      @stdio.stdout.write(
        @caret.render(
          coverage~,
          style=Simplified,
          source_paths~,
          ignore_missing_files=cli_flag(
            matches,
            "ignore-missing-files",
            default=true,
          ),
        ),
      )
    Caret =>
      @stdio.stdout.write(
        @caret.render(
          coverage~,
          style=Detailed,
          source_paths~,
          ignore_missing_files=cli_flag(
            matches,
            "ignore-missing-files",
            default=true,
          ),
        ),
      )
    Coveralls => run_coveralls(matches, coverage, source_paths)
    Cobertura => {
      let reader = @io.MemoryReader() <| writer => {
        @cobertura.output(
          writer~,
          coverage~,
          source_paths~,
          ignore_missing_files=cli_flag(
            matches,
            "ignore-missing-files",
            default=true,
          ),
        )
      }
      defer reader.close()
      write_output(output_path(matches, format), reader.read_all())
    }
    Summary =>
      @stdio.stdout.write(@summary.render(coverage, include_full=false))
    FullSummary =>
      @stdio.stdout.write(@summary.render(coverage, include_full=true))
    SortedSummary => @stdio.stdout.write(prepared.sorted_summary())
  }
}