///|
async fn run_upgrade_request(path : String) -> Unit raise @core.CliError {
  let project_dir = absolute_path(path)
  ensure_existing_dir(project_dir)
  let moon_mod_path = project_file_path(project_dir, "moon.mod")
  let mod_names = import_modules_from_moon_mod(moon_mod_path)
  println(upgrade_project_modules(project_dir, mod_names))
}

///|
async fn import_modules_from_moon_mod(
  moon_mod_path : String,
) -> Array[String] raise @core.CliError {
  guard path_exists(moon_mod_path) else {
    raise @core.cli_error("moon.mod not found: \{moon_mod_path}")
  }
  try @fs.read_file(moon_mod_path) catch {
    err => raise @core.cli_error("failed to read \{moon_mod_path}: \{err}")
  } noraise {
    content =>
      try content.text() catch {
        err =>
          raise @core.cli_error(
            "failed to decode \{moon_mod_path}: \{Repr(err)}",
          )
      } noraise {
        text => parse_import_modules(text)
      }
  }
}

///|
fn parse_import_modules(content : String) -> Array[String] {
  let mut in_import = false
  let mod_names : Array[String] = []
  for raw_line in content.split("\n") {
    let line = raw_line.trim().to_owned()
    if !in_import {
      guard line.has_prefix("import") && line.contains("{") else { continue }
      in_import = true
    }
    guard quoted_import_content(line) is Some(entry) else {
      if line.contains("}") {
        in_import = false
      }
      continue
    }
    guard module_name_from_import(entry) is Some(mod_name) else { continue }
    mod_names.push(mod_name)
    if line.contains("}") {
      in_import = false
    }
  }
  mod_names
}

///|
fn quoted_import_content(line : String) -> String? {
  guard line.split_once("\"") is Some((_, rest)) else { return None }
  guard rest.split_once("\"") is Some((content, _)) else { return None }
  guard !content.is_empty() else { return None }
  Some(content.to_owned())
}

///|
fn module_name_from_import(entry : String) -> String? {
  guard !entry.has_prefix(".") &&
    !entry.has_prefix("/") &&
    !entry.contains("://") else {
    None
  }
  match entry.split_once("@") {
    Some((name, _)) if !name.is_empty() => Some(name.to_owned())
    None => Some(entry)
    _ => None
  }
}

///|
fn upgrade_outcome_message(
  name : String,
  outcome : Result[Int, String],
) -> String? {
  match outcome {
    Err(message) =>
      Some("failed to run `moon add \{name} --upgrade`: \{message}")
    Ok(status) =>
      if status == 0 {
        None
      } else {
        Some("`moon add \{name} --upgrade` failed with exit code \{status}")
      }
  }
}

///|
async fn upgrade_project_modules(
  project_dir : String,
  mod_names : ArrayView[String],
) -> String raise @core.CliError {
  if mod_names.is_empty() {
    return "no dependencies to upgrade in \{project_dir}"
  }
  let failures : Array[String] = []
  for name in mod_names {
    let outcome = try
      @process.run("moon", ["add", name, "--upgrade"], cwd=project_dir)
    catch {
      err => Err("\{err}")
    } noraise {
      status => Ok(status)
    }
    guard upgrade_outcome_message(name, outcome) is Some(failure) else {
      continue
    }
    failures.push(failure)
  }
  guard failures.is_empty() else {
    raise @core.cli_error(
      "failed to upgrade \{failures.length()} of \{mod_names.length()} dependencies: \{failures.join("; ")}",
    )
  }
  "upgraded \{mod_names.length()} dependencies in \{project_dir}: \{mod_names.join(", ")}"
}