///|
async fn project_root(directory : String) -> String {
  for directory = directory {
    if @fs.exists("\{directory}/moon.mod") ||
      @fs.exists("\{directory}/moon.mod.json") {
      break directory
    }
    let parent = @fs.realpath("\{directory}/..")
    guard parent != directory else {
      raise LaunchError("no MoonBit project found for \{directory}")
    }
    continue parent
  }
}

///|
async fn run_moon(project : String, arguments : Array[String]) -> String {
  let (status, output) = @process.collect_stdout(
    "moon",
    arguments,
    cwd=project,
    inherit_env=true,
    stderr=@stdio.stderr,
  )
  guard status == 0 else {
    // Build scripts may also write diagnostics to stdout. Preserve them on
    // failure, before returning to the CLI's nonzero exit path.
    @stdio.stderr.write(output)
    raise LaunchError("moon \{arguments[0]} failed (exit status \{status})")
  }
  output.text()
}

///|
async fn select_package(
  packages : Array[PackageMetadata],
  target : String,
  directory : String?,
) -> PackageMetadata {
  let matches = []
  for pkg in packages {
    let matches_target = match directory {
      Some(directory) => @fs.realpath(pkg.directory) == directory
      None => pkg.relative_name == target || pkg.name == target
    }
    if matches_target {
      matches.push(pkg)
    }
  }
  guard matches is [pkg] else {
    raise LaunchError(
      "target '\{target}' does not identify one local MoonBit package",
    )
  }
  guard pkg.supports_native else {
    raise LaunchError(
      "package '\{pkg.name}' does not support the native target",
    )
  }
  guard pkg.is_main else {
    raise LaunchError(
      "package '\{pkg.name}' cannot generate an executable; declare pkgtype(kind: \"executable\") in moon.pkg (or is-main: true in moon.pkg.json)",
    )
  }
  pkg
}

///|
async fn prepare_target(target : String) -> (String, @debugger.DebugContext) {
  let directory = if @fs.exists(target) {
    match @fs.kind(target) {
      Regular =>
        return (validate_executable(target), @debugger.DebugContext::empty())
      Directory => Some(@fs.realpath(target))
      _ =>
        raise LaunchError(
          "target is neither a package directory nor an executable: \{target}",
        )
    }
  } else {
    None
  }
  let project = project_root(
    match directory {
      Some(directory) => directory
      None => @fs.realpath(".")
    },
  )
  // Use one explicit output directory for both commands. This also keeps the
  // debug launch artifacts separate from regular project builds.
  let target_dir = "\{project}/_build/moondbg"
  run_moon(project, [
    "check", "--target", "native", "-g", "--target-dir", target_dir,
  ])
  |> ignore
  let metadata = @fs.read_file("\{target_dir}/native/debug/check/packages.json")
  let pkg = select_package(
    parse_package_metadata(metadata.text()),
    target,
    directory,
  )
  let output = run_moon(project, [
    "run",
    "--build-only",
    "--target",
    "native",
    "-g",
    "--target-dir",
    target_dir,
    @fs.realpath(pkg.directory),
  ])
  let artifact = build_artifact(output)
  // moon run --build-only emits an absolute artifact path.
  let executable = validate_executable(artifact)
  (executable, @debugger.DebugContext::new(pkg.name, pkg.aliases))
}