#!/bin/sh
# Abacus installer for macOS and Linux: the abacus CLI and, if you like, the
# desktop app, from their latest GitHub releases.
#
#   curl -fsSL https://abacus.empero.org/install.sh | sh                  # asks
#   curl -fsSL https://abacus.empero.org/install.sh | sh -s -- --cli      # the CLI
#   curl -fsSL https://abacus.empero.org/install.sh | sh -s -- --desktop  # CLI + app
#
# --update (what `abacus update` runs) updates the CLI, and the desktop app if
# it is installed. ABACUS_INSTALL=cli|desktop|update does what the flags do;
# ABACUS_INSTALL_DIR moves the CLI (default ~/.local/bin).
set -eu

CLI_REPO="empero-org/abacus"
DESKTOP_REPO="empero-org/abacus-desktop"
DESKTOP_ID="org.empero.abacus.desktop"
ICON_URL="https://raw.githubusercontent.com/$DESKTOP_REPO/main/packages/desktop/build/icon.png"
INSTALL_DIR="${ABACUS_INSTALL_DIR:-$HOME/.local/bin}"
CHOICE="${ABACUS_INSTALL:-}"

say() { printf '%s\n' "$*"; }
warn() { printf 'warn: %s\n' "$*" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
has() { command -v "$1" >/dev/null 2>&1; }

usage() {
  say "Usage: install.sh [--cli | --desktop | --update]"
  say ""
  say "  --cli      install or update the abacus CLI"
  say "  --desktop  install or update the CLI and the desktop app"
  say "  --update   update the CLI, and the desktop app if it is installed"
  say ""
  say "With no option it asks. ABACUS_INSTALL_DIR moves the CLI (default ~/.local/bin)."
}

for arg in "$@"; do
  case "$arg" in
    --cli) CHOICE=cli ;;
    --desktop) CHOICE=desktop ;;
    --update) CHOICE=update ;;
    -h | --help) usage; exit 0 ;;
    *) die "unknown option: $arg (see --help)" ;;
  esac
done
case "$CHOICE" in
  "" | cli | desktop | update) ;;
  *) die "ABACUS_INSTALL must be cli, desktop or update, not '$CHOICE'" ;;
esac

fetch() {
  if has curl; then curl -fsSL "$1"
  elif has wget; then wget -qO- "$1"
  else die "need curl or wget to download files"
  fi
}

download() {
  if has curl; then curl -fsSL -o "$2" "$1"
  elif has wget; then wget -qO "$2" "$1"
  else die "need curl or wget to download files"
  fi
}

# The newest release's tag. With curl the redirect from releases/latest names
# it, which costs none of GitHub's small API allowance.
latest_tag() {
  if has curl; then
    url=$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/$1/releases/latest") || return 1
    tag=${url##*/tag/}
    [ "$tag" != "$url" ] || return 1
  else
    tag=$(fetch "https://api.github.com/repos/$1/releases/latest" |
      sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
  fi
  [ -n "$tag" ] || return 1
  printf '%s\n' "$tag"
}

sha256_of() {
  if has sha256sum; then sha256sum "$1" | awk '{print $1}'
  elif has shasum; then shasum -a 256 "$1" | awk '{print $1}'
  else return 1
  fi
}

# download_release REPO TAG ASSET FILE: fetch a release file and check it
# against the release's SHA256SUMS.
download_release() {
  base="https://github.com/$1/releases/download/$2"
  download "$base/$3" "$4" || die "could not download $3 from $1 $2"
  sums="$TMP/SHA256SUMS-${1##*/}"
  [ -f "$sums" ] || download "$base/SHA256SUMS" "$sums" 2>/dev/null || : >"$sums"
  want=$(awk -v name="$3" '$2 == name || $2 == "*" name { print $1; exit }' "$sums")
  if [ -z "$want" ]; then
    warn "no checksum published for $3; not verified"
    return 0
  fi
  got=$(sha256_of "$4") || { warn "no sha256 tool found; $3 not verified"; return 0; }
  [ "$got" = "$want" ] || die "checksum mismatch for $3"
}

# --- platform ----------------------------------------------------------------
OS=$(uname -s)
case "$OS" in
  Darwin)
    # A shell under Rosetta reports x86_64 on an Apple silicon Mac.
    if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = 1 ]; then
      CLI_TARGET="aarch64-apple-darwin" DESKTOP_ARCH="arm64"
    else
      CLI_TARGET="x86_64-apple-darwin" DESKTOP_ARCH="x64"
    fi
    ;;
  Linux)
    case "$(uname -m)" in
      x86_64 | amd64) CLI_TARGET="x86_64-unknown-linux-gnu" DESKTOP_ARCH="x64" ;;
      aarch64 | arm64) die "there is no prebuilt release for arm64 Linux yet; build from source: https://github.com/$CLI_REPO" ;;
      *) die "unsupported architecture: $(uname -m)" ;;
    esac
    ;;
  *) die "unsupported operating system: $OS (on Windows, use install.ps1; see https://abacus.empero.org)" ;;
esac

TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
trap 'exit 130' INT TERM

# --- what to install -----------------------------------------------------------
# A desktop session to show an app in: always on a Mac; on Linux, a display.
graphical() {
  [ "$OS" = Darwin ] || [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]
}

# Piped into sh, the script's stdin is the script itself: ask the terminal.
if [ -z "$CHOICE" ] && (exec </dev/tty) 2>/dev/null; then
  if graphical; then default=1; else default=2; fi
  say "Install Abacus:"
  say "  1) the abacus CLI and the desktop app"
  say "  2) the abacus CLI only"
  printf 'Choose 1 or 2 [%s]: ' "$default"
  read -r answer </dev/tty || answer=""
  case "${answer:-$default}" in
    1) CHOICE=desktop ;;
    2) CHOICE=cli ;;
    *) die "please answer 1 or 2" ;;
  esac
  say ""
fi
# Nobody to ask (a script, CI): the CLI, as this installer always did.
[ -n "$CHOICE" ] || CHOICE=cli

# --- dependencies --------------------------------------------------------------
case "$OS" in
  Darwin)
    if ! has git; then
      say "Installing git (Xcode command line tools)..."
      xcode-select --install >/dev/null 2>&1 || true
      has git || die "git is required; run 'xcode-select --install' and retry"
    fi
    ;;
  Linux)
    if ! has git; then
      say "Installing git..."
      if has apt-get; then apt-get install -y git >/dev/null 2>&1 || sudo apt-get install -y git
      elif has dnf; then dnf install -y git || sudo dnf install -y git
      elif has pacman; then pacman -Sy --noconfirm git || sudo pacman -Sy --noconfirm git
      elif has zypper; then zypper --non-interactive install git || sudo zypper --non-interactive install git
      else die "git is required but no supported package manager was found"
      fi
    fi
    ;;
esac
has git || die "git is required"

# Run as root, or through sudo when there is one.
as_root() {
  if [ "$(id -u)" = 0 ]; then "$@"
  elif has sudo; then sudo "$@"
  else return 1
  fi
}

# --- the CLI -------------------------------------------------------------------
cli_version() {
  [ -x "$INSTALL_DIR/abacus" ] || return 0
  "$INSTALL_DIR/abacus" --version 2>/dev/null | sed -n 's/.*abacus[[:space:]]\{1,\}\([0-9][^ ]*\).*/\1/p' || true
}

install_cli() {
  cli_tag=$(latest_tag "$CLI_REPO") || die "could not find the latest abacus release"
  current=$(cli_version)
  if [ "$current" = "${cli_tag#v}" ]; then
    say "abacus $current is up to date."
  else
    say "Downloading abacus ${cli_tag#v} for ${CLI_TARGET}..."
    download_release "$CLI_REPO" "$cli_tag" "abacus-$CLI_TARGET" "$TMP/abacus"
    mkdir -p "$INSTALL_DIR"
    chmod +x "$TMP/abacus"
    # Swap the file in by renaming, so an abacus that is running (the one
    # running `abacus update`, say) keeps the file it started from.
    cp "$TMP/abacus" "$INSTALL_DIR/.abacus.new"
    mv -f "$INSTALL_DIR/.abacus.new" "$INSTALL_DIR/abacus"
    say "Installed abacus ${cli_tag#v} in $INSTALL_DIR."
  fi
  # Earlier installers left an `abacus-update` script; `abacus update` replaces it.
  helper="$INSTALL_DIR/abacus-update"
  if [ -f "$helper" ] && grep -q "Fetches the latest Abacus release" "$helper" 2>/dev/null; then
    rm -f "$helper"
  fi
}

# --- the desktop app: macOS ----------------------------------------------------
mac_app() {
  if [ -d "$HOME/Applications/Abacus.app" ] && [ ! -d /Applications/Abacus.app ]; then
    printf '%s\n' "$HOME/Applications/Abacus.app"
  else
    printf '%s\n' /Applications/Abacus.app
  fi
}

plist_value() {
  /usr/libexec/PlistBuddy -c "Print :$2" "$1/Contents/Info.plist" 2>/dev/null || true
}

desktop_version_mac() {
  app=$(mac_app)
  [ -d "$app" ] && [ "$(plist_value "$app" CFBundleIdentifier)" = "$DESKTOP_ID" ] || return 0
  plist_value "$app" CFBundleShortVersionString
}

install_desktop_mac() {
  app=$(mac_app)
  if [ -d "$app" ] && [ "$(plist_value "$app" CFBundleIdentifier)" != "$DESKTOP_ID" ]; then
    die "$app is another app called Abacus; move it away and run this again"
  fi
  download_release "$DESKTOP_REPO" "$desktop_tag" "Abacus-mac-$DESKTOP_ARCH.zip" "$TMP/Abacus.zip"
  ditto -x -k "$TMP/Abacus.zip" "$TMP/app"
  [ -d "$TMP/app/Abacus.app" ] || die "the download did not contain Abacus.app"
  folder=${app%/*}
  if [ ! -w "$folder" ]; then
    folder="$HOME/Applications" app="$HOME/Applications/Abacus.app"
    mkdir -p "$folder"
  fi
  rm -rf "$app"
  ditto "$TMP/app/Abacus.app" "$app"
  xattr -dr com.apple.quarantine "$app" 2>/dev/null || true
  # The app is not signed with a developer certificate yet. Signing it for
  # this Mac is what lets macOS run a build it did not download in a browser.
  if ! codesign --verify --deep --strict "$app" >/dev/null 2>&1; then
    codesign --force --deep --sign - "$app" >/dev/null 2>&1 ||
      warn "could not sign $app; if macOS will not open it, right-click it and choose Open"
  fi
  say "Installed the desktop app ${desktop_tag#v} in $folder."
  DESKTOP_HINT="Open Abacus from $folder (or Spotlight)."
}

# --- the desktop app: Linux ----------------------------------------------------
LINUX_HOME="$HOME/.local/share/abacus-desktop"

deb_version() {
  has dpkg-query || return 0
  # A package that was removed but not purged still has a version.
  status=$(dpkg-query -W -f='${db:Status-Status} ${Version}' abacus-desktop 2>/dev/null || true)
  case "$status" in installed\ *) printf '%s\n' "${status#installed }" ;; esac
}

desktop_version_linux() {
  version=$(deb_version)
  if [ -n "$version" ]; then printf '%s\n' "$version"; return 0; fi
  [ ! -f "$LINUX_HOME/version" ] || cat "$LINUX_HOME/version"
}

# Debian and Ubuntu: the .deb, which also sets up Chromium's sandbox (and
# its AppArmor profile on Ubuntu 24.04 and later). It needs root.
install_desktop_deb() {
  download_release "$DESKTOP_REPO" "$desktop_tag" "Abacus-linux-amd64.deb" "$TMP/abacus-desktop.deb"
  # apt reads the file as its own unprivileged user.
  chmod 755 "$TMP"
  chmod 644 "$TMP/abacus-desktop.deb"
  say "Installing the desktop app package (this needs administrator rights)..."
  as_root apt-get install -y "$TMP/abacus-desktop.deb" >/dev/null || return 1
  # An earlier per-user copy would shadow the package in the app menu.
  rm -rf "$LINUX_HOME"
  rm -f "$HOME/.local/share/applications/abacus-desktop.desktop" "$INSTALL_DIR/abacus-desktop"
  say "Installed the desktop app ${desktop_tag#v}."
  DESKTOP_HINT="Open Abacus from your app menu, or run: abacus-desktop"
}

# Elsewhere: the tarball, in your home directory, with a menu entry.
install_desktop_tarball() {
  download_release "$DESKTOP_REPO" "$desktop_tag" "Abacus-linux-x64.tar.gz" "$TMP/abacus-desktop.tar.gz"
  mkdir -p "$TMP/desktop"
  tar -xzf "$TMP/abacus-desktop.tar.gz" -C "$TMP/desktop"
  unpacked=$(dirname "$(find "$TMP/desktop" -name abacus-desktop -type f | head -n 1)")
  [ -x "$unpacked/abacus-desktop" ] || die "the download did not contain the abacus-desktop program"
  rm -rf "$LINUX_HOME"
  mkdir -p "${LINUX_HOME%/*}"
  mv "$unpacked" "$LINUX_HOME"
  printf '%s\n' "${desktop_tag#v}" >"$LINUX_HOME/version"
  mkdir -p "$INSTALL_DIR"
  ln -sf "$LINUX_HOME/abacus-desktop" "$INSTALL_DIR/abacus-desktop"
  download "$ICON_URL" "$LINUX_HOME/icon.png" 2>/dev/null || true
  applications="$HOME/.local/share/applications"
  mkdir -p "$applications"
  cat >"$applications/abacus-desktop.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=Abacus
Comment=A desktop for the abacus coding agent
Exec="$LINUX_HOME/abacus-desktop" %U
Icon=$LINUX_HOME/icon.png
Terminal=false
Categories=Development;
StartupWMClass=Abacus
EOF
  has update-desktop-database && update-desktop-database "$applications" >/dev/null 2>&1 || true
  # Chromium sandboxes pages in user namespaces; without them it needs its
  # helper to be setuid root.
  if ! unshare --user true >/dev/null 2>&1; then
    say "Setting up the app's sandbox (this needs administrator rights)..."
    { as_root chown root:root "$LINUX_HOME/chrome-sandbox" && as_root chmod 4755 "$LINUX_HOME/chrome-sandbox"; } ||
      warn "could not set up the sandbox; the app may not start (see $LINUX_HOME/chrome-sandbox)"
  fi
  say "Installed the desktop app ${desktop_tag#v} in $LINUX_HOME."
  DESKTOP_HINT="Open Abacus from your app menu, or run: abacus-desktop"
}

install_desktop_linux() {
  # An app installed in your home directory stays there.
  if [ -f "$LINUX_HOME/version" ] && [ -z "$(deb_version)" ]; then
    install_desktop_tarball
    return 0
  fi
  if has apt-get && has dpkg-query && { [ "$(id -u)" = 0 ] || has sudo; }; then
    install_desktop_deb && return 0
    warn "the package did not install; installing the app in your home directory instead"
  fi
  install_desktop_tarball
}

# --- the desktop app -----------------------------------------------------------
desktop_version() {
  if [ "$OS" = Darwin ]; then desktop_version_mac; else desktop_version_linux; fi
}

DESKTOP_HINT=""
install_desktop() {
  desktop_tag=$(latest_tag "$DESKTOP_REPO") || die "could not find the latest desktop app release"
  current=$(desktop_version)
  if [ "$current" = "${desktop_tag#v}" ]; then
    say "The desktop app $current is up to date."
    return 0
  fi
  say "Downloading the desktop app ${desktop_tag#v}..."
  if [ "$OS" = Darwin ]; then
    install_desktop_mac
    running="Abacus.app/Contents/MacOS/Abacus"
  else
    install_desktop_linux
    running="abacus-desktop"
  fi
  if [ -n "$current" ] && pgrep -f "$running" >/dev/null 2>&1; then
    say "Abacus is open: restart it to use the new version."
  fi
}

# --- go ------------------------------------------------------------------------
install_cli
case "$CHOICE" in
  desktop) install_desktop ;;
  update) [ -z "$(desktop_version)" ] || install_desktop ;;
esac

case ":$PATH:" in
  *":$INSTALL_DIR:"*)
    found=$(command -v abacus 2>/dev/null || true)
    if [ -n "$found" ] && [ "$found" != "$INSTALL_DIR/abacus" ]; then
      warn "$found comes before $INSTALL_DIR/abacus in your PATH"
    fi
    ;;
  *)
    warn "$INSTALL_DIR is not in your PATH. Add it to your shell profile:"
    printf '  export PATH="%s:$PATH"\n' "$INSTALL_DIR" >&2
    ;;
esac

[ "$CHOICE" = update ] && exit 0
say ""
say "Next:"
say "  abacus setup          # pick a provider and a model"
say "  abacus                # start in a project"
[ -z "$DESKTOP_HINT" ] || say "  $DESKTOP_HINT"
say ""
say "Sync sessions across devices with 'abacus sync login'."
say "Update later with 'abacus update'."
