///|
pub struct CommandResult {
  exit_code : Int
  output : String
} derive(Eq, @debug.Debug)

///|
pub fn CommandResult::exit_code(self : CommandResult) -> Int {
  self.exit_code
}

///|
pub fn CommandResult::output(self : CommandResult) -> String {
  self.output
}

///|
fn command_result(exit_code : Int, output : String) -> CommandResult {
  { exit_code, output, }
}

///|
fn usage() -> String {
  "MoonChange - repository change-governance gates\n\n" +
  "USAGE:\n" +
  "  moonchange audit --policy FILE --manifest FILE\n" +
  "  moonchange audit-diff --policy FILE --evidence FILE --diff FILE\n" +
  "  moonchange explain --policy FILE --path REPOSITORY_PATH\n" +
  "  moonchange lint --policy FILE\n" +
  "  moonchange compare --before FILE --after FILE --paths FILE\n" +
  "  moonchange demo [--format text|json]\n\n" +
  "All report commands accept --format text|json (default: text).\n\n" +
  "EXIT CODES:\n" +
  "  0 passed, 1 needs review or has lint warnings, 2 rejected or invalid input"
}

///|
fn cli_option(
  args : Array[String],
  name : String,
) -> Result[String, Diagnostic] {
  let mut found : String? = None
  let mut index = 1
  while index < args.length() {
    if args[index] == name {
      if found is Some(_) {
        return Err(
          Diagnostic::new(
            "cli.option.duplicate", name, "option appears more than once", "one value",
            "duplicate",
          ),
        )
      }
      if index + 1 >= args.length() {
        return Err(
          Diagnostic::new(
            "cli.option.value", name, "option is missing its value", "value after option",
            "missing",
          ),
        )
      }
      found = Some(args[index + 1])
      index = index + 2
    } else {
      index = index + 1
    }
  }
  match found {
    Some(value) => Ok(value)
    None =>
      Err(
        Diagnostic::new(
          "cli.option.required",
          name,
          "required option is missing",
          name + " VALUE",
          "missing",
        ),
      )
  }
}

///|
fn cli_optional_option(
  args : Array[String],
  name : String,
) -> Result[String?, Diagnostic] {
  match cli_option(args, name) {
    Ok(value) => Ok(Some(value))
    Err(error) if error.code() == "cli.option.required" => Ok(None)
    Err(error) => Err(error)
  }
}

///|
fn cli_output_format(args : Array[String]) -> Result[String, Diagnostic] {
  match cli_optional_option(args, "--format") {
    Ok(Some("text")) | Ok(None) => Ok("text")
    Ok(Some("json")) => Ok("json")
    Ok(Some(value)) =>
      Err(
        Diagnostic::new(
          "cli.format.invalid", "--format", "output format is unsupported", "text or json",
          value,
        ),
      )
    Err(error) => Err(error)
  }
}

///|
fn cli_check_options(
  args : Array[String],
  allowed : Array[String],
) -> Result[Unit, Diagnostic] {
  let mut index = 1
  while index < args.length() {
    let name = args[index]
    if !name.has_prefix("--") {
      return Err(
        Diagnostic::new(
          "cli.argument.unexpected",
          "args[" + index.to_string() + "]",
          "unexpected positional argument",
          "named option",
          name,
        ),
      )
    }
    if !contains_string(allowed, name) {
      return Err(
        Diagnostic::new(
          "cli.option.unknown",
          "args[" + index.to_string() + "]",
          "unknown option",
          allowed.join(","),
          name,
        ),
      )
    }
    if index + 1 >= args.length() {
      return Err(
        Diagnostic::new(
          "cli.option.value", name, "option is missing its value", "value after option",
          "missing",
        ),
      )
    }
    index = index + 2
  }
  Ok(())
}

///|
fn read_cli_file(
  path : String,
  option_name : String,
) -> Result[String, Diagnostic] {
  let content = @fs.read_file_to_string(path) catch {
    @fs.IOError(message) =>
      return Err(
        Diagnostic::new(
          "cli.file.read",
          option_name,
          "cannot read UTF-8 input file",
          "readable file",
          path + ": " + message,
        ),
      )
  }
  Ok(content)
}

///|
fn load_policy_option(
  args : Array[String],
  option_name : String,
) -> Result[Policy, Diagnostic] {
  let path = match cli_option(args, option_name) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let source = match read_cli_file(path, option_name) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  parse_policy(source)
}

///|
fn load_policy(args : Array[String]) -> Result[Policy, Diagnostic] {
  load_policy_option(args, "--policy")
}

///|
fn gate_exit(status : GateStatus) -> Int {
  match status {
    GateStatus::Passed => 0
    Review => 1
    Rejected => 2
  }
}

///|
fn audit_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
  match cli_check_options(args, ["--policy", "--manifest", "--format"]) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let policy = match load_policy(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let manifest_path = match cli_option(args, "--manifest") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let source = match read_cli_file(manifest_path, "--manifest") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let changes = match parse_manifest(source) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let report = evaluate(policy, changes)
  let format = match cli_output_format(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let output = if format == "json" {
    report.to_json()
  } else {
    report.to_text()
  }
  Ok(command_result(gate_exit(report.status), output))
}

///|
fn audit_diff_command(
  args : Array[String],
) -> Result[CommandResult, Diagnostic] {
  match
    cli_check_options(args, ["--policy", "--evidence", "--diff", "--format"]) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let policy = match load_policy(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let evidence_path = match cli_option(args, "--evidence") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let evidence_source = match read_cli_file(evidence_path, "--evidence") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let review = match parse_review_evidence(evidence_source) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let diff_path = match cli_option(args, "--diff") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let diff_source = match read_cli_file(diff_path, "--diff") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let changes = match
    change_set_from_unified_diff(review.id, review.evidence, diff_source) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let report = evaluate(policy, changes)
  let format = match cli_output_format(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let output = if format == "json" {
    report.to_json()
  } else {
    report.to_text()
  }
  Ok(command_result(gate_exit(report.status), output))
}

///|
fn explain_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
  match cli_check_options(args, ["--policy", "--path", "--format"]) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let policy = match load_policy(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let path = match cli_option(args, "--path") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let format = match cli_output_format(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match policy.explain(path) {
    Ok(value) =>
      Ok(
        command_result(
          0,
          if format == "json" {
            value.to_explain_json()
          } else {
            value.to_text()
          },
        ),
      )
    Err(error) => Err(error)
  }
}

///|
fn lint_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
  match cli_check_options(args, ["--policy", "--format"]) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let policy = match load_policy(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let report = lint_policy(policy)
  let exit_code = if report.has_errors() {
    2
  } else if report.is_clean() {
    0
  } else {
    1
  }
  let format = match cli_output_format(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let output = if format == "json" {
    report.to_json()
  } else {
    report.to_text()
  }
  Ok(command_result(exit_code, output))
}

///|
fn compare_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
  match
    cli_check_options(args, ["--before", "--after", "--paths", "--format"]) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let before = match load_policy_option(args, "--before") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let after = match load_policy_option(args, "--after") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let paths_file = match cli_option(args, "--paths") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let paths_source = match read_cli_file(paths_file, "--paths") {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let paths : Array[String] = []
  for line in split_owned_lines(paths_source) {
    if !line.is_empty() && !line.has_prefix("#") {
      paths.push(line)
    }
  }
  let format = match cli_output_format(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match compare_policies(before, after, paths) {
    Ok(report) =>
      Ok(
        command_result(
          0,
          if format == "json" {
            report.to_json()
          } else {
            report.to_text()
          },
        ),
      )
    Err(error) => Err(error)
  }
}

///|
fn demo_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
  match cli_check_options(args, ["--format"]) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let policy = match
    parse_policy(
      "MOONCHANGE_POLICY 1\nDEFAULT approvals=1 max_total=300 require_owned=yes\nOWNER **/* @maintainers\nOWNER src/security/** @security,@maintainers\nRULE source src/** approvals=1 checks=unit labels=code forbid=delete max_lines=200 release_note=no binary=deny\nRULE security src/security/** approvals=2 checks=security labels=security-reviewed forbid=rename max_lines=80 release_note=yes binary=deny",
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let changes = match
    parse_manifest(
      "MOONCHANGE 1\nID DEMO-1\nACTOR @alice\nAPPROVAL @maintainers\nAPPROVAL @security\nCHECK unit passed\nCHECK security passed\nLABEL code\nLABEL security-reviewed\nRELEASE_NOTE yes\nCHANGE modify src/security/auth.mbt 20 4 text",
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let report = evaluate(policy, changes)
  let format = match cli_output_format(args) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let output = if format == "json" {
    report.to_json()
  } else {
    report.to_text()
  }
  Ok(command_result(gate_exit(report.status), output))
}

///|
pub fn execute(args : Array[String]) -> CommandResult {
  if args.is_empty() ||
    args[0] == "help" ||
    args[0] == "--help" ||
    args[0] == "-h" {
    return command_result(0, usage())
  }
  let result = match args[0] {
    "audit" => audit_command(args)
    "audit-diff" => audit_diff_command(args)
    "explain" => explain_command(args)
    "lint" => lint_command(args)
    "compare" => compare_command(args)
    "demo" => demo_command(args)
    command =>
      Err(
        Diagnostic::new(
          "cli.command.unknown", "args[0]", "unknown command", "audit, audit-diff, explain, lint, compare, demo, or help",
          command,
        ),
      )
  }
  match result {
    Ok(value) => value
    Err(error) => command_result(2, error.to_text())
  }
}