///|
const MOONGATE_UPDATE_URL = "https://github.com/vectie/moongate/releases/latest"

///|
const MOONGATE_TOOL_VERSION_TIMEOUT_MS = 3000

///|
const MOONGATE_TOOL_LIFECYCLE_TIMEOUT_MS = 600000

///|
const MOONGATE_TOOL_PROBE_TIMEOUT_MS = 5000

///|
priv enum MoonToolLifecycleAction {
  Install
  Update
}

///|
priv struct MoonToolInstallation {
  path : String
  real : String
  version : String?
  runnable : Bool
  source : String
  is_path_default : Bool
}

///|
priv struct MoonToolInstallationReport {
  tool : String
  installs : Array[MoonToolInstallation]
  is_conflict : Bool
  needs_confirmation : Bool
  command : String
  anchored : Bool
}

///|
fn moon_normalize_external_url(url : String) -> String {
  if url.find("http://") == Some(0) || url.find("https://") == Some(0) {
    url
  } else {
    "https://\{url}"
  }
}

///|
fn moon_tool_names() -> Array[String] {
  ["claude", "codex", "gemini", "opencode", "openclaw", "hermes"]
}

///|
fn moon_requested_tool_names(raw : String?) -> Array[String] {
  let valid = moon_tool_names()
  let requested : Array[String] = []
  match raw {
    Some(value) =>
      for part_view in value.split(",") {
        let name = part_view.to_owned().trim().to_lower().to_owned()
        if !name.is_empty() && valid.contains(name) && !requested.contains(name) {
          requested.push(name)
        }
      }
    None => ()
  }
  if requested.is_empty() {
    valid
  } else {
    requested
  }
}

///|
fn moon_requested_tool_names_from_json(value : Json?) -> Array[String] {
  match value {
    Some(Array(items)) => {
      let raw : Array[String] = []
      for item in items {
        if item is String(tool) {
          raw.push(tool)
        }
      }
      let joined = raw.join(",")
      moon_requested_tool_names(Some(joined))
    }
    Some(String(value)) => moon_requested_tool_names(Some(value))
    _ => moon_requested_tool_names(None)
  }
}

///|
fn moon_tool_lifecycle_action(value : String) -> MoonToolLifecycleAction? {
  match value.trim().to_lower() {
    "install" => Some(Install)
    "update" => Some(Update)
    _ => None
  }
}

///|
fn moon_update_url() -> String {
  MOONGATE_UPDATE_URL
}

///|
fn moon_dialog_result_json() -> Json {
  Json::null()
}

///|
async fn moon_portable_mode() -> Bool {
  let exe = @os.executable() catch { _ => return false }
  let marker = @pathx.join(@pathx.dirname(exe), "portable.ini")
  @fsx.exists_as_file(marker) catch {
    _ => false
  }
}

///|
async fn moon_get_tool_versions_json(tools : String?) -> Json {
  let versions : Array[Json] = []
  for tool in moon_requested_tool_names(tools) {
    versions.push(moon_single_tool_version_json(tool))
  }
  Json::array(versions)
}

///|
async fn moon_single_tool_version_json(tool : String) -> Json {
  let result = @spawn.spawn(
    tool,
    ["--version"],
    timeout=MOONGATE_TOOL_VERSION_TIMEOUT_MS,
  ) catch {
    err =>
      return moon_tool_version_json(
        tool,
        None,
        None,
        Some(err.to_string()),
        false,
      )
  }
  let combined = if result.stdout.trim().is_empty() {
    result.stderr
  } else {
    result.stdout
  }
  let version = moon_first_output_line(combined)
  let broken = result.status != 0
  moon_tool_version_json(
    tool,
    version,
    None,
    if broken {
      Some(result.stderr.trim().to_owned())
    } else {
      None
    },
    broken,
  )
}

///|
async fn moon_run_tool_lifecycle_action_json(
  tools : Array[String],
  action : String,
) -> Result[Json, String] {
  guard moon_tool_lifecycle_action(action) is Some(action) else {
    return Err("Unsupported tool action: \{action}")
  }
  let requested = moon_requested_tool_names(Some(tools.join(",")))
  if requested.is_empty() {
    return Err("No supported tools selected")
  }
  let command = moon_build_tool_lifecycle_command(requested, action)
  if command.trim().is_empty() {
    return Err("No supported tool command available")
  }
  let result = @spawn.spawn(
    "bash",
    ["-lc", command],
    timeout=MOONGATE_TOOL_LIFECYCLE_TIMEOUT_MS,
  ) catch {
    err => return Err("启动安装进程失败: \{err}")
  }
  if result.status == 0 {
    Ok(Json::boolean(true))
  } else {
    let raw = if result.stderr.trim().is_empty() {
      result.stdout.trim()
    } else {
      result.stderr.trim()
    }
    let detail = moon_last_lines(raw.to_owned(), 8)
    Err(
      if detail.is_empty() {
        "命令执行失败 (exit code: \{result.status})"
      } else {
        detail
      },
    )
  }
}

///|
async fn moon_probe_tool_installations_json(
  tools : Array[String],
) -> Result[Json, String] {
  let requested = moon_requested_tool_names(Some(tools.join(",")))
  if requested.is_empty() {
    return Err("No supported tools selected")
  }
  let reports : Array[Json] = []
  for tool in requested {
    reports.push(moon_tool_installation_report(tool).to_json())
  }
  Ok(Json::array(reports))
}

///|
fn MoonToolInstallation::to_json(self : MoonToolInstallation) -> Json {
  {
    "path": Json::string(self.path),
    "real": Json::string(self.real),
    "version": optional_string_json(self.version),
    "runnable": Json::boolean(self.runnable),
    "source": Json::string(self.source),
    "is_path_default": Json::boolean(self.is_path_default),
  }.to_json()
}

///|
fn MoonToolInstallationReport::to_json(
  self : MoonToolInstallationReport,
) -> Json {
  {
    "tool": Json::string(self.tool),
    "installs": Json::array(self.installs.map(fn(item) { item.to_json() })),
    "is_conflict": Json::boolean(self.is_conflict),
    "needs_confirmation": Json::boolean(self.needs_confirmation),
    "command": Json::string(self.command),
    "anchored": Json::boolean(self.anchored),
  }.to_json()
}

///|
async fn moon_tool_installation_report(
  tool : String,
) -> MoonToolInstallationReport {
  let installs = moon_enumerate_tool_installations(tool)
  let command = moon_tool_update_command(tool, installs)
  {
    tool,
    installs,
    is_conflict: moon_tool_installations_conflict(installs),
    needs_confirmation: installs.length() >= 2,
    command,
    anchored: moon_default_installation(installs) is Some(_),
  }
}

///|
async fn moon_enumerate_tool_installations(
  tool : String,
) -> Array[MoonToolInstallation] {
  let installs : Array[MoonToolInstallation] = []
  let seen : Map[String, Bool] = {}
  match moon_command_path(tool) {
    Some(path) => {
      let real = @fsx.resolve(path) catch { _ => path }
      installs.push({
        path,
        real,
        version: moon_probe_tool_version_at(path),
        runnable: true,
        source: moon_install_source(path),
        is_path_default: true,
      })
      seen[path] = true
    }
    None => ()
  }
  for path in moon_common_tool_paths(tool) {
    if seen.get(path) is Some(_) {
      continue
    }
    let exists = @fsx.exists_as_file(path) catch { _ => false }
    if exists {
      let real = @fsx.resolve(path) catch { _ => path }
      installs.push({
        path,
        real,
        version: moon_probe_tool_version_at(path),
        runnable: true,
        source: moon_install_source(path),
        is_path_default: false,
      })
      seen[path] = true
    }
  }
  installs
}

///|
async fn moon_command_path(tool : String) -> String? {
  let result = @spawn.spawn(
    "bash",
    ["-lc", "command -v \{moon_shell_single_quote(tool)}"],
    timeout=MOONGATE_TOOL_PROBE_TIMEOUT_MS,
  ) catch {
    _ => return None
  }
  if result.status != 0 {
    return None
  }
  moon_first_output_line(result.stdout)
}

///|
async fn moon_probe_tool_version_at(path : String) -> String? {
  let result = @spawn.spawn(
    path,
    ["--version"],
    timeout=MOONGATE_TOOL_PROBE_TIMEOUT_MS,
  ) catch {
    _ => return None
  }
  if result.status != 0 {
    return None
  }
  let output = if result.stdout.trim().is_empty() {
    result.stderr
  } else {
    result.stdout
  }
  moon_first_output_line(output)
}

///|
fn moon_common_tool_paths(tool : String) -> Array[String] {
  let paths : Array[String] = []
  let home = @os.home() catch { _ => "" }
  for
    dir in [
      "/opt/homebrew/bin",
      "/usr/local/bin",
      "/usr/bin",
      "/bin",
      @pathx.join(home, ".local/bin"),
      @pathx.join(home, ".npm-global/bin"),
      @pathx.join(home, ".volta/bin"),
    ] {
    paths.push(@pathx.join(dir, tool))
  }
  paths
}

///|
fn moon_install_source(path : String) -> String {
  let lower = path.to_lower()
  if lower.contains("/.volta/") {
    "volta"
  } else if lower.contains("/.bun/") {
    "bun"
  } else if lower.contains("/.nvm/") {
    "nvm"
  } else if lower.contains("/.local/share/claude/") {
    "claude-native"
  } else if lower.contains("/homebrew/") ||
    lower.contains("/cellar/") ||
    lower.contains("/opt/homebrew/") {
    "homebrew"
  } else if lower.contains("/npm") || lower.contains("/node") {
    "npm"
  } else {
    "system"
  }
}

///|
fn moon_default_installation(
  installs : Array[MoonToolInstallation],
) -> MoonToolInstallation? {
  for install in installs {
    if install.is_path_default {
      return Some(install)
    }
  }
  match installs {
    [single] => Some(single)
    _ => None
  }
}

///|
fn moon_tool_installations_conflict(
  installs : Array[MoonToolInstallation],
) -> Bool {
  if installs.length() < 2 {
    return false
  }
  let versions : Map[String, Bool] = {}
  let mut runnable_count = 0
  let mut broken_count = 0
  for install in installs {
    versions[install.version.unwrap_or("")] = true
    if install.runnable {
      runnable_count += 1
    } else {
      broken_count += 1
    }
  }
  versions.length() > 1 || (runnable_count > 0 && broken_count > 0)
}

///|
async fn moon_build_tool_lifecycle_command(
  tools : Array[String],
  action : MoonToolLifecycleAction,
) -> String {
  let lines : Array[String] = ["set -e", "set -o pipefail"]
  for tool in tools {
    lines.push("echo ========== \{moon_tool_display_name(tool)} ==========")
    lines.push(
      match action {
        Install => moon_tool_install_command(tool)
        Update =>
          moon_tool_update_command(
            tool,
            moon_enumerate_tool_installations(tool),
          )
      },
    )
    lines.push("")
  }
  lines.join("\n")
}

///|
fn moon_tool_display_name(tool : String) -> String {
  match tool {
    "claude" => "Claude Code"
    "codex" => "Codex"
    "gemini" => "Gemini CLI"
    "opencode" => "OpenCode"
    "openclaw" => "OpenClaw"
    "hermes" => "Hermes"
    _ => "Unknown"
  }
}

///|
fn moon_tool_install_command(tool : String) -> String {
  match tool {
    "claude" =>
      moon_chain_commands(
        "bash -c 'tmp=$(mktemp) && curl -fsSL https://claude.ai/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'",
        "npm i -g @anthropic-ai/claude-code@latest",
      )
    "codex" => "npm i -g @openai/codex@latest"
    "gemini" => "npm i -g @google/gemini-cli@latest"
    "opencode" =>
      moon_chain_commands(
        "bash -c 'tmp=$(mktemp) && curl -fsSL https://opencode.ai/install -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'",
        "npm i -g opencode-ai@latest",
      )
    "openclaw" => "npm i -g openclaw@latest"
    "hermes" =>
      "bash -c 'tmp=$(mktemp) && curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'"
    _ => ""
  }
}

///|
fn moon_tool_update_command(
  tool : String,
  installs : Array[MoonToolInstallation],
) -> String {
  match moon_default_installation(installs) {
    Some(install) =>
      match moon_anchored_tool_update_command(tool, install) {
        Some(command) => command
        None => moon_static_tool_update_command(tool)
      }
    None => moon_static_tool_update_command(tool)
  }
}

///|
fn moon_anchored_tool_update_command(
  tool : String,
  install : MoonToolInstallation,
) -> String? {
  let quoted = moon_quote_path_if_needed(install.path)
  match tool {
    "claude" | "codex" | "openclaw" => Some("\{quoted} update")
    "opencode" => Some("\{quoted} upgrade")
    "hermes" => Some("\{quoted} update")
    "gemini" =>
      match moon_sibling_bin(install.path, "npm") {
        Some(npm) =>
          Some(
            "\{moon_quote_path_if_needed(npm)} i -g @google/gemini-cli@latest",
          )
        None => None
      }
    _ => None
  }
}

///|
fn moon_static_tool_update_command(tool : String) -> String {
  match tool {
    "claude" =>
      moon_chain_commands(
        "claude update", "npm i -g @anthropic-ai/claude-code@latest",
      )
    "codex" =>
      moon_chain_commands("codex update", "npm i -g @openai/codex@latest")
    "gemini" => "npm i -g @google/gemini-cli@latest"
    "opencode" =>
      moon_chain_commands("opencode upgrade", "npm i -g opencode-ai@latest")
    "openclaw" =>
      moon_chain_commands("openclaw update --yes", "npm i -g openclaw@latest")
    "hermes" =>
      moon_chain_commands(
        "hermes update", "bash -c 'tmp=$(mktemp) && curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'",
      )
    _ => ""
  }
}

///|
fn moon_chain_commands(primary : String, fallback : String) -> String {
  if fallback.trim().is_empty() {
    primary
  } else {
    "\{primary} || \{fallback}"
  }
}

///|
fn moon_sibling_bin(path : String, exe : String) -> String? {
  let dir = @pathx.dirname(path)
  if dir.is_empty() {
    None
  } else {
    Some(@pathx.join(dir, exe))
  }
}

///|
fn moon_shell_single_quote(value : String) -> String {
  "'" + value.replace_all(old="'", new="'\\''") + "'"
}

///|
fn moon_quote_path_if_needed(path : String) -> String {
  if path.contains(" ") || path.contains("'") || path.contains("\"") {
    moon_shell_single_quote(path)
  } else {
    path
  }
}

///|
fn moon_last_lines(text : String, n : Int) -> String {
  let lines = text.split("\n").map(fn(line) { line.to_owned() })
  let start = if lines.length() > n { lines.length() - n } else { 0 }
  lines[start:].join("\n")
}

///|
fn moon_first_output_line(output : String) -> String? {
  for line_view in output.split("\n") {
    let line = line_view.to_owned().trim().to_owned()
    if !line.is_empty() {
      return Some(line)
    }
  }
  None
}

///|
fn moon_tool_version_json(
  name : String,
  version : String?,
  latest_version : String?,
  error : String?,
  installed_but_broken : Bool,
) -> Json {
  {
    "name": Json::string(name),
    "version": match version {
      Some(value) => Json::string(value)
      None => Json::null()
    },
    "latest_version": match latest_version {
      Some(value) => Json::string(value)
      None => Json::null()
    },
    "error": match error {
      Some(value) if !value.is_empty() => Json::string(value)
      _ => Json::null()
    },
    "installed_but_broken": Json::boolean(installed_but_broken),
    "env_type": Json::string("macos"),
    "wsl_distro": Json::null(),
  }.to_json()
}