///|
let cli_current_version : String = "0.1.16"

///|
let cli_manifest_url : String = "https://mooncakes.io/api/v0/manifest/moonbit-community/proton_cli"

///|
priv struct CliVersion {
  major : Int
  minor : Int
  patch : Int
}

///|
fn env_flag_is_enabled(value : String) -> Bool {
  match value.trim().to_owned().to_lower() {
    "" | "0" | "false" | "no" | "off" => false
    _ => true
  }
}

///|
fn cli_update_check_disabled() -> Bool {
  match @env.get_env_var("PROTON_NO_UPDATE_CHECK") {
    Some(value) => env_flag_is_enabled(value)
    None => false
  }
}

///|
fn parse_cli_version_part(part : StringView) -> Int? {
  let text = part.to_owned()
  guard text != "" else { return None }
  let mut value = 0
  for ch in text {
    guard ch.is_ascii_digit() else { return None }
    value = value * 10 + ch.to_int() - '0'.to_int()
  }
  Some(value)
}

///|
fn parse_cli_version(version : String) -> CliVersion? {
  let core = match version.split_once("-") {
    Some((prefix, _)) => prefix.to_owned()
    None => version
  }
  match core.split(".").collect() {
    [major, minor, patch] =>
      match
        (
          parse_cli_version_part(major),
          parse_cli_version_part(minor),
          parse_cli_version_part(patch),
        ) {
        (Some(major), Some(minor), Some(patch)) =>
          Some(CliVersion::{ major, minor, patch })
        _ => None
      }
    _ => None
  }
}

///|
fn compare_cli_version(left : CliVersion, right : CliVersion) -> Int {
  if left.major != right.major {
    left.major.compare(right.major)
  } else if left.minor != right.minor {
    left.minor.compare(right.minor)
  } else {
    left.patch.compare(right.patch)
  }
}

///|
fn cli_version_is_newer(candidate : String, current : String) -> Bool {
  match (parse_cli_version(candidate), parse_cli_version(current)) {
    (Some(candidate), Some(current)) =>
      compare_cli_version(candidate, current) > 0
    _ => false
  }
}

///|
fn latest_version_from_manifest_text(text : String) -> String? {
  let manifest = @json.parse(text) catch { _ => return None }
  match manifest {
    { "latest_version": String(version), .. } => Some(version)
    _ => None
  }
}

///|
fn cli_version_text() -> String {
  "proton_cli " + cli_current_version
}

///|
async fn fetch_latest_cli_version() -> String? {
  let (code, output) = @process.collect_output_merged("curl", [
    "-fsSL", "--max-time", "2", cli_manifest_url,
  ]) catch {
    _ => return None
  }
  guard code == 0 else { return None }
  let text = output.text() catch { _ => return None }
  latest_version_from_manifest_text(text)
}

///|
async fn check_cli_update() -> Unit {
  guard !cli_update_check_disabled() else { return }
  match fetch_latest_cli_version() {
    Some(latest) =>
      if cli_version_is_newer(latest, cli_current_version) {
        @output.write_stderr_line(
          "warning: proton_cli " +
          latest +
          " is available (current " +
          cli_current_version +
          "). Set PROTON_NO_UPDATE_CHECK=1 to disable this check.",
        )
      }
    None => ()
  }
}

///|
async fn print_error_message(message : String) -> Unit {
  if message.has_prefix("error: ") {
    @output.write_stderr_line(message)
  } else {
    @output.write_stderr_line("error: " + message)
  }
}

///|
fn doctor_command() -> @argparse.Command {
  @argparse.Command(
    "doctor",
    about="Inspect the Proton environment and current project",
    flags=[
      FlagArg("verbose", about="Include detailed project diagnostics"),
      FlagArg("json", about="Render one JSON document", conflicts_with=["quiet"]),
      FlagArg("quiet", about="Render only failing diagnostics", conflicts_with=[
        "json",
      ]),
    ],
    options=[OptionArg("output", about="Write the report to a file")],
  )
}

///|
fn version_command() -> @argparse.Command {
  @argparse.Command("version", about="Print the Proton CLI version")
}

///|
fn cli_command() -> @argparse.Command {
  @argparse.Command(
    "proton_cli",
    about="Build, run, diagnose, and package Proton desktop applications",
    version=cli_version_text(),
    options=[
      OptionArg(
        "cwd",
        short='C',
        default_values=["."],
        allow_hyphen_values=true,
        global=true,
        about="Run as if started in this directory",
      ),
    ],
    subcommands=[
      version_command(),
      @new_project.command(),
      @dev.command(),
      @build.command(),
      @package.command(),
      doctor_command(),
      @cef.command(),
      @updater.command(),
    ],
    arg_required_else_help=true,
    subcommand_required=true,
  )
}

///|
async fn run_doctor(cwd : String, matches : @argparse.Matches) -> Bool {
  guard matches.flags
    is {
      "verbose"? : Some(verbose)
      | (None with verbose = false),
      "json"? : Some(json)
      | (None with json = false),
      "quiet"? : Some(quiet)
      | (None with quiet = false),
      ..
    }
  guard matches.values
    is { "output"? : Some(output_args) | (None with output_args = []), .. }
  let output = match output_args {
    [path, ..] => Some(@fsutil.resolve_path(cwd, path))
    [] => None
  }
  @doctor.run(cwd~, verbose~, json~, quiet~, output~)
}

///|
async fn dispatch_cli(cwd : String, matches : @argparse.Matches) -> Bool {
  match matches.subcommand {
    Some(("version", _)) => println(cli_version_text())
    Some(("doctor", doctor_matches)) => return run_doctor(cwd, doctor_matches)
    Some(("new", new_matches)) => @new_project.run(cwd, new_matches)
    Some(("dev", dev_matches)) => @dev.run(cwd, dev_matches)
    Some(("build", build_matches)) => @build.run(cwd, build_matches)
    Some(("package", package_matches)) => @package.run(cwd, package_matches)
    Some(("cef", cef_matches)) => @cef.run(cwd, cef_matches)
    Some(("updater", updater_matches)) => @updater.run(updater_matches)
    _ => abort("argparse invariant violated: missing subcommand")
  }
  true
}

///|
async fn run(args : ArrayView[String]) -> Int {
  let matches = @argparse.parse(cli_command(), argv=args, env=Map([])) catch {
    error => {
      print_error_message(error.to_string())
      return 2
    }
  }
  guard! matches.values is { "cwd": [cwd, ..], .. }
  if !(matches.subcommand is Some(("version", _))) {
    check_cli_update()
  }
  try dispatch_cli(cwd, matches) catch {
    error => {
      print_error_message(error.to_string())
      1
    }
  } noraise {
    success => if success { 0 } else { 1 }
  }
}

///|
async fn main {
  let code = run(@env.args()[1:])
  if code != 0 {
    @sys.exit(code)
  }
}