///|
/// MoonBit toolchain management, the `mb moon` command group.
///
/// This mirrors `uv python`: toolchains are downloaded from
/// `cli.moonbitlang.com`, unpacked into `$MB_HOME/moon/` and can be
/// selected per invocation with `mb run --moon `.

///|
pub const CLI_BASE : String = "https://cli.moonbitlang.com"

///|
/// Root directory holding all installed MoonBit toolchains.
pub fn moon_root() -> String raise MbError {
  path_join(mb_home(), "moon")
}

///|
/// Directory of one installed toolchain.
pub fn toolchain_dir(version : String) -> String raise MbError {
  path_join(moon_root(), version)
}

///|

///|
/// A resolved install request: which URL key to download and what to name the
/// local directory.
pub struct MoonSpec {
  download_key : String
  store_name : String
}

///|
fn urlencode_plus(s : String) -> String {
  s.replace(old="+", new="%2B")
}

///|
/// Parse the `version.json` published at the toolchain root into (name, version)
/// pairs, stripping the descriptive suffix from each version string.
pub fn parse_version_items(j : Json) -> Array[(String, String)] {
  let out : Array[(String, String)] = []
  match json_array(j, "items") {
    Some(items) =>
      for item in items {
        match (json_string(item, "name"), json_string(item, "version")) {
          (Some(name), Some(ver)) =>
            out.push((name, first_token(strip_name_prefix(ver, name))))
          _ => ()
        }
      }
    None => ()
  }
  out
}

///|
/// Some entries repeat the tool name in their version string, e.g.
/// `moonrun 0.1.20261012 (748b63c ...)`.
fn strip_name_prefix(ver : String, name : String) -> String {
  let prefix = name + " "
  if ver.has_prefix(prefix) {
    ver[prefix.length():].to_owned()
  } else {
    ver
  }
}

///|
fn first_token(s : String) -> String {
  match s.find(" ") {
    Some(i) => s[:i].to_owned()
    None => s
  }
}

///|
/// Drop a leading `v` (version strings are published as `v0.10.15+943464bfa`
/// but stored and passed around without it).
fn strip_v(s : String) -> String {
  if s.has_prefix("v") {
    s[1:].to_owned()
  } else {
    s
  }
}

///|
/// Right-pad `s` with spaces to `width` (no-op when already wider).
fn pad_right(s : String, width : Int) -> String {
  let mut out = s
  while out.length() < width {
    out = out + " "
  }
  out
}

///|
/// Latest published `moon` version (e.g. `0.1.20260920`).
pub fn latest_moon_cli_version() -> String? raise MbError {
  let j = fetch_json(CLI_BASE + "/version.json")
  for (name, ver) in parse_version_items(j) {
    if name == "moon" {
      return Some(ver)
    }
  }
  None
}

///|
/// Latest published `moonc` version including its build hash (e.g.
/// `0.10.14+7d59c7ec9`).
pub fn latest_moonc_version() -> String? raise MbError {
  let j = fetch_json(CLI_BASE + "/version.json")
  for (name, ver) in parse_version_items(j) {
    if name == "moonc" {
      return Some(strip_v(ver))
    }
  }
  None
}

///|
/// Turn a user-supplied version into a concrete download key and store name.
///
/// - `latest` / `nightly` map to those aliases; `latest` is stored under the
///   resolved CLI version so repeated installs are idempotent.
/// - A full `moonc` version such as `0.10.14+7d59c7ec9` downloads directly.
/// - A bare compiler version such as `0.10.14` is upgraded to the matching
///   full version when it corresponds to the current release.
pub fn resolve_moon_spec(arg : String) -> MoonSpec raise MbError {
  let a = if arg == "" { "latest" } else { arg }
  if a == "nightly" {
    { download_key: a, store_name: a, }
  } else if a == "latest" {
    let name = match latest_moon_cli_version() {
      Some(v) => v
      None => "latest"
    }
    { download_key: a, store_name: name, }
  } else if a.contains("+") {
    { download_key: a, store_name: a, }
  } else {
    match latest_moonc_version() {
      Some(full) =>
        if full == a || full.has_prefix(a + "+") {
          { download_key: full, store_name: full, }
        } else {
          { download_key: a, store_name: a, }
        }
      None => { download_key: a, store_name: a, }
    }
  }
}

///|
/// Whether a toolchain directory is fully installed (binary plus bundled core).
pub fn toolchain_complete(version : String) -> Bool raise MbError {
  let home = toolchain_dir(version)
  file_exists(path_join(home, "bin/moon")) &&
  dir_exists(path_join(home, "lib/core"))
}

///|
/// Names of installed toolchains.
pub fn installed_toolchains() -> Array[String] raise MbError {
  let root = moon_root()
  if !dir_exists(root) {
    return []
  }
  let out : Array[String] = []
  for name in list_dir(root) {
    if name == "" || name.has_prefix(".") {
      continue
    }
    if dir_exists(path_join(root, name)) {
      out.push(name)
    }
  }
  out
}

///|
/// Path of the file recording the default toolchain version.
fn default_file() -> String raise MbError {
  path_join(moon_root(), ".default")
}

///|
pub fn default_toolchain() -> String? raise MbError {
  let f = default_file()
  if !file_exists(f) {
    return None
  }
  let v = read_text(f).trim().to_owned()
  if v == "" {
    None
  } else {
    Some(v)
  }
}

///|
pub fn set_default_toolchain(version : String) -> Unit raise MbError {
  mkdir_p(moon_root())
  write_text(default_file(), version + "\n")
}

///|
/// Find an installed toolchain matching a user-supplied version. Exact matches
/// win; a bare compiler version also matches its full `+hash` form.
pub fn find_installed_toolchain(version : String) -> String? raise MbError {
  let installed = installed_toolchains()
  for v in installed {
    if v == version {
      return Some(v)
    }
  }
  for v in installed {
    if v.has_prefix(version + "+") {
      return Some(v)
    }
  }
  None
}

///|
/// Pick the toolchain to use when no explicit version is requested.
pub fn effective_toolchain() -> String? raise MbError {
  match default_toolchain() {
    Some(v) => if toolchain_complete(v) { Some(v) } else { None }
    None =>
      match installed_toolchains() {
        [v, ..] => Some(v)
        [] => None
      }
  }
}

///|
/// Download and install a MoonBit toolchain.
pub fn moon_install(arg : String, force : Bool) -> Unit raise MbError {
  ensure_tools()
  let spec = resolve_moon_spec(arg)
  let home = toolchain_dir(spec.store_name)
  if toolchain_complete(spec.store_name) && !force {
    println("MoonBit toolchain \{spec.store_name} is already installed")
    sync_default_shims()
    return
  }
  println("Installing MoonBit toolchain \{spec.store_name} ...")
  rm_rf(home)
  mkdir_p(home)
  let plat = platform()
  let encoded = urlencode_plus(spec.download_key)
  let tar = temp_path()
  download(
    CLI_BASE + "/binaries/" + encoded + "/moonbit-" + plat + ".tar.gz",
    tar,
    true,
  )
  extract_tar_gz(tar, home)
  rm_rf(tar) catch {
    _ => ()
  }
  // Ensure every shipped binary is executable.
  for rel in walk_files(path_join(home, "bin")) {
    chmod_exec(path_join(path_join(home, "bin"), rel))
  }
  // Fetch and bundle the standard library so `moon build` works offline.
  println("Downloading core ...")
  let core_tar = temp_path()
  download(CLI_BASE + "/cores/core-" + encoded + ".tar.gz", core_tar, true)
  extract_tar_gz(core_tar, path_join(home, "lib"))
  rm_rf(core_tar) catch {
    _ => ()
  }
  println("Bundling core ...")
  let moon = path_join(home, "bin/moon")
  let core_dir = path_join(home, "lib/core")
  let prefix = "MOON_HOME=" +
    shell_quote(home) +
    " PATH=" +
    shell_quote(path_join(home, "bin")) +
    ":\"$PATH\" "
  run_checked(
    "cd " +
    shell_quote(core_dir) +
    " && " +
    prefix +
    shell_quote(moon) +
    " bundle --warn-list -a --all",
  )
  // Nightly also ships an llvm-target core; releases do not.
  if spec.download_key == "nightly" {
    run_checked(
      "cd " +
      shell_quote(core_dir) +
      " && " +
      prefix +
      shell_quote(moon) +
      " bundle --warn-list -a --target llvm",
    )
  }
  run_checked(
    "cd " +
    shell_quote(core_dir) +
    " && " +
    prefix +
    shell_quote(moon) +
    " bundle --warn-list -a --target wasm-gc --quiet",
  )
  // First install becomes the default; otherwise leave the choice alone and
  // just make sure ~/.local/bin reflects whichever version is selected.
  if default_toolchain() is None {
    set_default_toolchain(spec.store_name)
  }
  sync_default_shims()
  println("Installed MoonBit toolchain \{spec.store_name} to " + home)
}

///|
/// Point the `~/.local/bin` symlinks at the currently-selected toolchain.
pub fn sync_default_shims() -> Unit raise MbError {
  match default_toolchain() {
    Some(d) => ensure_shims(d)
    None => ()
  }
}

///|
/// Create or refresh `~/.local/bin` symlinks for the default toolchain's
/// binaries.
pub fn ensure_shims(version : String) -> Unit raise MbError {
  if !toolchain_complete(version) {
    return
  }
  let bin = bin_dir()
  mkdir_p(bin)
  let tbin = path_join(toolchain_dir(version), "bin")
  for name in list_dir_unsafe(tbin) {
    let src = path_join(tbin, name)
    if dir_exists(src) {
      continue
    }
    let link = path_join(bin, name)
    run_ok("ln -sfn " + shell_quote(src) + " " + shell_quote(link)) |> ignore
  }
}

///|
/// Target of a symlink, or `None` when `path` is not a symlink. Works for
/// dangling links too, which is exactly the case that needs cleaning up.
pub fn readlink(path : String) -> String? {
  let out = run_capture("readlink " + shell_quote(path) + " 2>/dev/null")
  let target = out.trim().to_owned()
  if target == "" {
    None
  } else {
    Some(target)
  }
}

///|
/// Whether a shim symlink target lives inside `dir`'s `bin` directory.
pub fn shim_points_into(target : String, dir : String) -> Bool {
  target.has_prefix(dir + "/bin/")
}

///|
fn remove_shims_in(bin : String, dir : String) -> Unit {
  if !dir_exists(bin) {
    return
  }
  for name in list_dir_unsafe(bin) {
    let link = path_join(bin, name)
    match readlink(link) {
      Some(target) =>
        if shim_points_into(target, dir) {
          run_ok("rm -f -- " + shell_quote(link)) |> ignore
        }
      None => ()
    }
  }
}

///|
/// Remove the `~/.local/bin` symlinks pointing into `version`'s toolchain.
///
/// Match by symlink target rather than by the recorded default, so an
/// uninstalled version never leaves dangling `moon`/`moonc`/... links behind.
pub fn remove_shims(version : String) -> Unit raise MbError {
  remove_shims_in(bin_dir(), toolchain_dir(version))
}

///|
/// The `mb moon list` command: installed toolchains, then the versions
/// published on the release channel (`version.json`, i.e. what `latest`
/// resolves to).
pub fn moon_list() -> Unit raise MbError {
  let installed = installed_toolchains()
  let default = default_toolchain()
  println("Installed MoonBit toolchains:")
  if installed.is_empty() {
    println("  (none)")
  } else {
    for v in installed {
      let marker = match default {
        Some(d) if d == v => " (default)"
        _ => ""
      }
      println("  " + v + marker)
    }
  }
  let items = parse_version_items(fetch_json(CLI_BASE + "/version.json")) catch {
    _ => []
  }
  println("")
  if items.is_empty() {
    println("Release channel: unavailable")
  } else {
    let mut width = 0
    for (name, _) in items {
      if name.length() > width {
        width = name.length()
      }
    }
    println("Release channel (version.json):")
    for (name, ver) in items {
      println("  " + pad_right(name, width) + "  " + strip_v(ver))
    }
  }
}

///|
/// The `mb moon uninstall ` command.
pub fn moon_uninstall(version : String) -> Unit raise MbError {
  match find_installed_toolchain(version) {
    Some(v) => {
      // Drop the `~/.local/bin` shims before the toolchain directory goes
      // away, otherwise they are left dangling.
      remove_shims(v)
      rm_rf(toolchain_dir(v))
      println("Uninstalled MoonBit toolchain " + v)
      match default_toolchain() {
        Some(d) if d == v => {
          rm_rf(default_file()) catch {
            _ => ()
          }
          // Fall back to another installed toolchain, if any.
          match installed_toolchains() {
            [next, ..] => {
              set_default_toolchain(next)
              ensure_shims(next)
            }
            [] => ()
          }
        }
        _ => ()
      }
    }
    None =>
      raise MbError("no toolchain matching '" + version + "' is installed")
  }
}

///|
/// The `mb moon default ` command.
pub fn moon_set_default(version : String) -> Unit raise MbError {
  match find_installed_toolchain(version) {
    Some(v) => {
      set_default_toolchain(v)
      ensure_shims(v)
      println("Default MoonBit toolchain set to " + v)
    }
    None =>
      raise MbError(
        "no toolchain matching '" +
        version +
        "' is installed; run `mb moon install " +
        version +
        "` first",
      )
  }
}

///|
/// The `mb moon which [version]` command.
pub fn moon_which(version : String) -> Unit raise MbError {
  let chosen = if version == "" {
    effective_toolchain()
  } else {
    find_installed_toolchain(version)
  }
  match chosen {
    Some(v) => println(path_join(toolchain_dir(v), "bin/moon"))
    None => raise MbError("no MoonBit toolchain is installed")
  }
}

///|
/// Environment describing how to invoke a MoonBit toolchain.
pub struct ToolchainEnv {
  moon : String
  home : String?
  bin : String?
}

///|
/// A system toolchain discovered on `PATH`.
pub fn system_toolchain() -> ToolchainEnv? {
  let which = run_capture("command -v moon 2>/dev/null").trim().to_owned()
  if which == "" {
    None
  } else {
    Some({ moon: which, home: None, bin: None, })
  }
}

///|
/// Shell prefix that points a command at `tc`'s environment. Empty for the
/// system toolchain.
pub fn env_prefix(tc : ToolchainEnv) -> String {
  match (tc.home, tc.bin) {
    (Some(h), Some(b)) =>
      "MOON_HOME=" + shell_quote(h) + " PATH=" + shell_quote(b) + ":\"$PATH\" "
    _ => ""
  }
}

///|
/// Resolve the toolchain to use for building/installing tools. When `explicit`
/// is empty this prefers the managed default, then a system install, and only
/// then installs the latest release.
pub fn resolve_toolchain(explicit : String) -> ToolchainEnv raise MbError {
  if explicit != "" {
    match find_installed_toolchain(explicit) {
      Some(v) =>
        return {
          moon: path_join(toolchain_dir(v), "bin/moon"),
          home: Some(toolchain_dir(v)),
          bin: Some(path_join(toolchain_dir(v), "bin")),
        }
      None => moon_install(explicit, false)
    }
  }
  match effective_toolchain() {
    Some(v) =>
      {
        moon: path_join(toolchain_dir(v), "bin/moon"),
        home: Some(toolchain_dir(v)),
        bin: Some(path_join(toolchain_dir(v), "bin")),
      }
    None =>
      match system_toolchain() {
        Some(tc) => tc
        None => {
          moon_install("latest", false)
          let v = match effective_toolchain() {
            Some(v) => v
            None => raise MbError("failed to install a MoonBit toolchain")
          }
          {
            moon: path_join(toolchain_dir(v), "bin/moon"),
            home: Some(toolchain_dir(v)),
            bin: Some(path_join(toolchain_dir(v), "bin")),
          }
        }
      }
  }
}