///|
pub(all) suberror InstallError {
  MissingHome
  InvalidSpec(String)
  InvalidArgs(String)
  IoError(String)
  CommandFailed(cmd~ : String, code~ : Int)
  ArtifactNotFound(String)
  RegistryIndexNotFound(String)
  RegistryIndexInvalid(String)
  UnsupportedTarget(String)
} derive(Eq, Show)

///|
pub fn artifact_path(
  repo_dir : String,
  package_path : String,
  target : Target,
) -> String {
  let build_dir = join_path(
    join_path(
      join_path(join_path(repo_dir, "target"), Target::to_moon_arg(target)),
      "release",
    ),
    "build",
  )
  let package_dir = join_path(build_dir, package_path)
  let file_name = package_file_name(package_path, target)
  join_path(package_dir, file_name)
}

///|
pub fn install(
  spec : Spec,
  name : String,
  target : Target,
) -> Result[Unit, InstallError] {
  let home = match @sys.get_env_var("HOME") {
    Some(value) => value
    None => return Err(InstallError::MissingHome)
  }
  let root_dir = join_path(home, ".mooninstall")
  let bin_dir = join_path(root_dir, "bin")
  let src_root = join_path(root_dir, "src")
  let owner_dir = join_path(src_root, spec.owner)
  let repo_dir = join_path(owner_dir, spec.repo)
  let tmp_root = join_path(join_path(root_dir, "tmp"), spec.owner)
  let tmp_dir = join_path(tmp_root, spec.repo)
  let version = match resolve_version(spec, home, tmp_dir) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let zip_path = join_path(
    join_path(join_path(join_path(home, ".moon"), "registry"), "cache"),
    join_path(spec.owner, join_path(spec.repo, version + ".zip")),
  )
  let repos_dir = join_path(
    tmp_dir,
    join_path(".repos", join_path(spec.owner, join_path(spec.repo, version))),
  )
  ensure_dir_recursive(bin_dir) catch {
    err =>
      return Err(
        InstallError::IoError("ensure bin dir \{bin_dir}: \{err.to_string()}"),
      )
  }
  ensure_dir_recursive(owner_dir) catch {
    err =>
      return Err(
        InstallError::IoError("ensure owner dir \{owner_dir}: \{err.to_string()}"),
      )
  }
  match run_shell(shell_join(["rm", "-rf", repo_dir])) {
    Ok(_) => ()
    Err(ExecError::CommandFailed(cmd~, code~)) =>
      return Err(InstallError::CommandFailed(cmd~, code~))
  }
  if !@fs.path_exists(zip_path) {
    ensure_dir_recursive(tmp_dir) catch {
      err =>
        return Err(
          InstallError::IoError("ensure tmp dir \{tmp_dir}: \{err.to_string()}"),
        )
    }
    if @fs.path_exists(repos_dir) {
      match run_shell(shell_join(["rm", "-rf", repos_dir])) {
        Ok(_) => ()
        Err(ExecError::CommandFailed(cmd~, code~)) =>
          return Err(InstallError::CommandFailed(cmd~, code~))
      }
    }
    let fetch_target = "\{spec.owner}/\{spec.repo}@\{version}"
    match run_shell_in_dir(tmp_dir, ["moon", "fetch", fetch_target]) {
      Ok(_) => ()
      Err(ExecError::CommandFailed(cmd~, code~)) =>
        return Err(InstallError::CommandFailed(cmd~, code~))
    }
  }
  if @fs.path_exists(zip_path) {
    ensure_dir_recursive(repo_dir) catch {
      err =>
        return Err(
          InstallError::IoError("ensure repo dir \{repo_dir}: \{err.to_string()}"),
        )
    }
    match run_shell(shell_join(["unzip", "-q", zip_path, "-d", repo_dir])) {
      Ok(_) => ()
      Err(ExecError::CommandFailed(cmd~, code~)) =>
        return Err(InstallError::CommandFailed(cmd~, code~))
    }
  } else {
    if !@fs.path_exists(repos_dir) {
      return Err(InstallError::ArtifactNotFound(repos_dir))
    }
    match run_shell(shell_join(["cp", "-R", repos_dir, repo_dir])) {
      Ok(_) => ()
      Err(ExecError::CommandFailed(cmd~, code~)) =>
        return Err(InstallError::CommandFailed(cmd~, code~))
    }
  }
  match
    run_shell_in_dir(repo_dir, [
      "moon",
      "build",
      "--target",
      Target::to_moon_arg(target),
      "--release",
    ]) {
    Ok(_) => ()
    Err(ExecError::CommandFailed(cmd~, code~)) =>
      return Err(InstallError::CommandFailed(cmd~, code~))
  }
  let mut artifact = artifact_path(repo_dir, spec.package_path, target)
  if !@fs.path_exists(artifact) {
    match find_artifact(repo_dir, spec.package_name, target) {
      Some(found) => artifact = found
      None => return Err(InstallError::ArtifactNotFound(artifact))
    }
  }
  let alias_path = join_path(bin_dir, name)
  match target {
    Target::Js => {
      let script = "#!/usr/bin/env bash\n" +
        "node " +
        shell_escape(artifact) +
        " \"$@\"\n"
      @fs.write_string_to_file(alias_path, script) catch {
        err => return Err(InstallError::IoError(err.to_string()))
      }
      match run_shell(shell_join(["chmod", "+x", alias_path])) {
        Ok(_) => ()
        Err(ExecError::CommandFailed(cmd~, code~)) =>
          return Err(InstallError::CommandFailed(cmd~, code~))
      }
    }
    Target::Native =>
      match run_shell(shell_join(["ln", "-sf", artifact, alias_path])) {
        Ok(_) => ()
        Err(ExecError::CommandFailed(cmd~, code~)) =>
          return Err(InstallError::CommandFailed(cmd~, code~))
      }
  }
  print_path_hint(bin_dir)
  Ok(())
}

///|
pub fn uninstall(name : String) -> Result[Unit, InstallError] {
  let home = match @sys.get_env_var("HOME") {
    Some(value) => value
    None => return Err(InstallError::MissingHome)
  }
  let bin_dir = join_path(join_path(home, ".mooninstall"), "bin")
  let alias_path = join_path(bin_dir, name)
  if @fs.path_exists(alias_path) {
    @fs.remove_file(alias_path) catch {
      err => return Err(InstallError::IoError(err.to_string()))
    }
  }
  Ok(())
}

///|
fn resolve_version(
  spec : Spec,
  home : String,
  tmp_dir : String,
) -> Result[String, InstallError] {
  match spec.version {
    Some(version) =>
      match select_version(Some(version), version) {
        Ok(value) => Ok(value)
        Err(err) =>
          Err(InstallError::InvalidSpec("invalid requested version: \{err.to_string()}"))
      }
    None => {
      let index_path = join_path(
        join_path(
          join_path(join_path(join_path(home, ".moon"), "registry"), "index"),
          "user",
        ),
        join_path(spec.owner, spec.repo + ".index"),
      )
      if !@fs.path_exists(index_path) {
        ensure_dir_recursive(tmp_dir) catch {
          err => return Err(InstallError::IoError(err.to_string()))
        }
        match run_shell_in_dir(tmp_dir, ["moon", "update"]) {
          Ok(_) => ()
          Err(ExecError::CommandFailed(cmd~, code~)) =>
            return Err(InstallError::CommandFailed(cmd~, code~))
        }
      }
      if !@fs.path_exists(index_path) {
        return Err(InstallError::RegistryIndexNotFound(index_path))
      }
      let content = @fs.read_file_to_string(index_path) catch {
        err =>
          return Err(
            InstallError::IoError("read registry index: \{index_path}: \{err.to_string()}"),
          )
      }
      match parse_registry_latest_version(content) {
        Ok(latest) =>
          match select_version(None, latest) {
            Ok(version) => Ok(version)
            Err(err) =>
              Err(InstallError::InvalidSpec("invalid latest version: \{err.to_string()}"))
          }
        Err(_) => Err(InstallError::RegistryIndexInvalid(index_path))
      }
    }
  }
}

///|
fn print_path_hint(bin_dir : String) -> Unit {
  match @sys.get_env_var("PATH") {
    Some(path) => {
      if !path.contains(bin_dir) {
        println("note: add to PATH: \{bin_dir}")
        println("  export PATH=\"\{bin_dir}:$PATH\"")
      }
    }
    None => {
      println("note: add to PATH: \{bin_dir}")
      println("  export PATH=\"\{bin_dir}:$PATH\"")
    }
  }
}

///|
fn package_file_name(package_path : String, target : Target) -> String {
  let name = basename(package_path)
  name + "." + Target::artifact_ext(target)
}

///|
fn basename(path : String) -> String {
  match path.split("/").last() {
    Some(name) => name.to_string()
    None => path
  }
}

///|
fn find_artifact(
  repo_dir : String,
  package_name : String,
  target : Target,
) -> String? {
  let build_dir = join_path(
    join_path(
      join_path(join_path(repo_dir, "target"), Target::to_moon_arg(target)),
      "release",
    ),
    "build",
  )
  let needle = package_name + "." + Target::artifact_ext(target)
  find_file_recursive(build_dir, needle)
}

///|
fn find_file_recursive(base : String, file_name : String) -> String? {
  if !@fs.path_exists(base) {
    return None
  }
  try @fs.is_dir(base) catch {
    _ => None
  } noraise {
    is_dir =>
      if is_dir {
        try @fs.read_dir(base) catch {
          _ => None
        } noraise {
          entries => {
            for name in entries {
              let path = join_path(base, name)
              match find_file_recursive(path, file_name) {
                Some(found) => return Some(found)
                None => ()
              }
            }
            None
          }
        }
      } else if basename(base) == file_name {
        Some(base)
      } else {
        None
      }
  }
}