#!/usr/bin/env bash
#
# install.sh — install or upgrade the RemotiveLabs CLI (`remotive`).
#
# Prefers the native package manager and otherwise falls back to the
# self-contained bundle (its own CPython — no Python needed on the host):
#
#   macOS            -> Homebrew tap        (fallback: signed .pkg installer)
#                       When Homebrew is present it asks which to use; use
#                       --method brew|pkg to skip the prompt.
#   Debian/Ubuntu    -> apt  + RemotiveLabs apt repo   (fallback: tar bundle)
#   RHEL/Fedora/SUSE -> dnf/yum + RemotiveLabs yum repo (fallback: tar bundle)
#
# Running it again upgrades an existing install to the latest version when a
# newer one is available (brew/apt/yum upgrade in place; the .pkg/tar paths
# compare the installed version and only replace it when newer or forced).
#
# Usage:
#   curl -fsSL https://storage.googleapis.com/beamy-public-releases/remotivelabs-cli/install.sh | bash
#   ./install.sh                 # install or upgrade to the latest version
#   ./install.sh 0.23.3          # install/pin a specific version
#   ./install.sh -f              # force reinstall even if already current
#   ./install.sh --method tar    # force a backend (brew|apt|yum|pkg|tar)
#   ./install.sh --dry-run       # print what would happen, change nothing
#
# Environment overrides:
#   REMOTIVE_INSTALL_METHOD=brew|apt|yum|pkg|tar   same as --method
#
set -euo pipefail

# ---------------------------------------------------------------------------
# Constants — the RemotiveLabs distribution channels
# ---------------------------------------------------------------------------
BREW_FORMULA="remotivelabs/tap/remotivelabs-cli"   # macOS Homebrew tap
PKG_NAME="remotivelabs-cli"                     # deb/rpm package name
CMD="remotive"                                  # the installed command
BUCKET_BASE="https://storage.googleapis.com/beamy-public-releases/remotivelabs-cli"
# Stable pointer to the newest release — a bare version string, overwritten by
# every release. Resolving "latest" from the same bucket we download from keeps
# the installer self-consistent (mirrors remotive-studio-desktop's install.sh).
LATEST_URL="$BUCKET_BASE/latest/latest-version.txt"

APT_REPO_URL="https://packages.remotivelabs.com"
APT_DIST="remotivelabs-apt"
APT_COMPONENT="main"
APT_KEY_URL="https://packages.remotivelabs.com/apt-repo-signing-key.gpg"
APT_KEYRING="/usr/share/keyrings/remotivelabs-apt.gpg"       # dearmored (binary)
APT_KEYRING_ASC="/usr/share/keyrings/remotivelabs-apt.asc"   # armored (no gpg tool)
APT_LIST="/etc/apt/sources.list.d/remotivelabs.list"

YUM_BASEURL="https://packages.remotivelabs.com/yum/remotivelabs-yum"
YUM_REPO_FILE="/etc/yum.repos.d/remotivelabs.repo"

# ---------------------------------------------------------------------------
# Args
# ---------------------------------------------------------------------------
VERSION=""
FORCE=""
DRY_RUN=""
METHOD="${REMOTIVE_INSTALL_METHOD:-auto}"

# Self-contained usage text (not sed on "$0": under `curl ... | bash` $0 is
# the shell, not this script, and hardcoded line ranges rot as the header grows).
usage() {
  cat >&2 <<'USAGE'
install.sh — install or upgrade the RemotiveLabs CLI (`remotive`).

Prefers the native package manager and otherwise falls back to the
self-contained bundle (its own CPython — no Python needed on the host):

  macOS            -> Homebrew tap        (fallback: signed .pkg installer)
                      When Homebrew is present it asks which to use; use
                      --method brew|pkg to skip the prompt.
  Debian/Ubuntu    -> apt  + RemotiveLabs apt repo   (fallback: tar bundle)
  RHEL/Fedora/SUSE -> dnf/yum + RemotiveLabs yum repo (fallback: tar bundle)

Running it again upgrades an existing install to the latest version when a
newer one is available (brew/apt/yum upgrade in place; the .pkg/tar paths
compare the installed version and only replace it when newer or forced).

Usage:
  curl -fsSL https://storage.googleapis.com/beamy-public-releases/remotivelabs-cli/install.sh | bash
  ./install.sh                 # install or upgrade to the latest version
  ./install.sh 0.23.3          # install/pin a specific version
  ./install.sh -f              # force reinstall even if already current
  ./install.sh --method tar    # force a backend (brew|apt|yum|pkg|tar)
  ./install.sh --dry-run       # print what would happen, change nothing

Environment overrides:
  REMOTIVE_INSTALL_METHOD=brew|apt|yum|pkg|tar   same as --method
USAGE
  exit "${1:-0}"
}

while [ $# -gt 0 ]; do
  case "$1" in
    -f|--force)   FORCE="1" ;;
    --dry-run)    DRY_RUN="1" ;;
    --method)     shift; METHOD="${1:-}" ;;
    --method=*)   METHOD="${1#*=}" ;;
    -h|--help)    usage 0 ;;
    -*)           echo "Unknown option: $1" >&2; usage 1 ;;
    *)            VERSION="$1" ;;
  esac
  shift
done

OS="$(uname -s)"
ARCH="$(uname -m)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

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

# run <cmd...> — execute, or print under --dry-run.
run() {
  if [ -n "$DRY_RUN" ]; then printf '+ %s\n' "$*" >&2; else "$@"; fi
}

# root_do / root_write — run as root (directly if root, else sudo).
SUDO=""
init_sudo() {
  if [ "$(id -u)" -eq 0 ]; then SUDO=""
  elif have sudo; then SUDO="sudo"
  else die "root privileges are required (install 'sudo' or run as root)."; fi
}
root_do() { if [ -n "$SUDO" ]; then run "$SUDO" "$@"; else run "$@"; fi; }
root_write() {
  local dest="$1"
  if [ -n "$DRY_RUN" ]; then
    printf '+ write %s:\n' "$dest" >&2; sed 's/^/  | /' >&2; return 0
  fi
  if [ -n "$SUDO" ]; then $SUDO tee "$dest" >/dev/null; else tee "$dest" >/dev/null; fi
}

require_curl() { have curl || die "curl is required."; }

# fetch <url> <dest> — download to $dest.
fetch() {
  log "Downloading $(basename "$1") ..."
  curl -fL --progress-bar -o "$2" "$1" || die "download failed: $1"
}

# resolve_version — echo the target version: the explicit arg, or the latest
# read from the GCS pointer file (a bare version string, so no Python/JSON
# parsing needed on the host).
RESOLVED_VERSION=""
resolve_version() {
  if [ -n "$RESOLVED_VERSION" ]; then echo "$RESOLVED_VERSION"; return; fi
  if [ -n "$VERSION" ]; then RESOLVED_VERSION="$VERSION"; echo "$RESOLVED_VERSION"; return; fi
  require_curl
  local v
  v="$(curl -fsSL --max-time 15 "$LATEST_URL" | tr -d '[:space:]')"
  [ -n "$v" ] || die "could not resolve the latest version from $LATEST_URL"
  RESOLVED_VERSION="$v"; echo "$v"
}

# installed_version — echo the currently installed CLI version, or "" if none.
# Falls back to the user-local launcher so tar upgrades are detected even when
# ~/.local/bin is not on PATH.
installed_version() {
  local bin=""
  if have "$CMD"; then bin="$CMD"
  elif [ -x "${HOME:-/nonexistent}/.local/bin/$CMD" ]; then bin="$HOME/.local/bin/$CMD"
  else echo ""; return; fi
  "$bin" --version 2>/dev/null | awk '{print $2}'
}

# verify_sha256 <file> <sha256_url> — fail-closed integrity check.
verify_sha256() {
  local file="$1" sha_url="$2" sha expected actual
  sha="$TMP/$(basename "$file").sha256"
  curl -fsSL -o "$sha" "$sha_url" || die "missing checksum file: $sha_url"
  expected="$(awk '{print $1; exit}' "$sha")"
  [ -n "$expected" ] || die "empty checksum for $(basename "$file")."
  if have sha256sum; then actual="$(sha256sum "$file" | awk '{print $1}')"
  else actual="$(shasum -a 256 "$file" | awk '{print $1}')"; fi
  [ "$expected" = "$actual" ] || die "SHA-256 mismatch for $(basename "$file") — refusing to install."
  log "Checksum OK for $(basename "$file")"
}

# up_to_date <target> — true when the installed version already equals target
# (and --force was not given), so the .pkg/tar paths can skip a reinstall.
up_to_date() {
  local target="$1" current; current="$(installed_version)"
  [ -z "$FORCE" ] && [ -n "$current" ] && [ "$current" = "$target" ]
}

# validate_home — sanitize $HOME before it is used to derive the tar install
# and uninstaller paths, and echo the symlink-resolved result. The generated
# uninstall.sh runs `rm -rf` on paths derived from this value, so a malicious
# or misconfigured $HOME (/, /etc, another user's home, a symlink into a
# system tree, a ".." traversal) must abort the install — fail closed, never
# generate an uninstaller for it. Note: shell variables cannot contain NUL
# bytes, so null-byte injection into $HOME is structurally impossible here.
validate_home() {
  local home="${HOME:-}" resolved pw_home
  [ -n "$home" ] || die "\$HOME is unset or empty — cannot determine the install location."
  case "$home" in
    /*) : ;;
    *)  die "\$HOME ('$home') is not an absolute path — refusing to install." ;;
  esac
  case "/$home/" in
    */../*) die "\$HOME ('$home') contains a '..' path traversal component — refusing to install." ;;
  esac
  resolved="$(readlink -f -- "$home" 2>/dev/null)" && [ -n "$resolved" ] \
    || die "cannot resolve \$HOME ('$home') to a real path — refusing to install."

  # $HOME (after resolving symlinks) must be the current user's real home from
  # the passwd database, or a directory inside it. This is the primary gate:
  # it rejects '/', system directories, and other users' homes in one check.
  # Skipped only when the uid has no passwd entry (e.g. containers running
  # with an arbitrary uid), where there is nothing to compare against.
  pw_home="$(getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6)"
  if [ -n "$pw_home" ]; then
    pw_home="$(readlink -f -- "$pw_home" 2>/dev/null || echo "$pw_home")"
    case "$resolved" in
      "$pw_home"|"$pw_home"/*) : ;;
      *) die "\$HOME ('$home' -> '$resolved') is not your home directory ('$pw_home') — refusing to install." ;;
    esac
  else
    warn "no passwd entry for uid $(id -u) — cannot verify \$HOME against the user database."
  fi

  # Defense in depth even when the passwd check passed or was skipped: never
  # treat the filesystem root or a system directory as a home.
  case "$resolved" in
    /|/bin|/boot|/dev|/etc|/lib|/lib32|/lib64|/opt|/proc|/run|/sbin|/srv|/sys|/tmp|/usr|/var| \
    /bin/*|/boot/*|/dev/*|/etc/*|/lib/*|/lib32/*|/lib64/*|/proc/*|/run/*|/sbin/*|/sys/*|/usr/*)
      die "\$HOME ('$home' -> '$resolved') is a system directory — refusing to install." ;;
  esac
  echo "$resolved"
}

# ---------------------------------------------------------------------------
# macOS — Homebrew (primary)
# ---------------------------------------------------------------------------
install_brew() {
  if [ -n "$VERSION" ]; then
    warn "Homebrew installs the latest version only; ignoring requested version $VERSION."
    warn "To pin a version use '--method pkg $VERSION'."
  fi
  run brew update
  if brew list --formula "$PKG_NAME" >/dev/null 2>&1; then
    if [ -n "$FORCE" ]; then run brew reinstall "$BREW_FORMULA"
    else run brew upgrade "$BREW_FORMULA" || true; fi   # upgrade is a no-op when current
  else
    run brew install "$BREW_FORMULA"
  fi
}

# ---------------------------------------------------------------------------
# macOS — signed .pkg installer (fallback, no Homebrew needed)
# ---------------------------------------------------------------------------
install_macos_pkg() {
  require_curl
  local ver pkg_arch
  ver="$(resolve_version)"
  case "$ARCH" in
    arm64) pkg_arch=arm64 ;;
    x86_64) die "macOS builds are Apple Silicon (arm64) only; no Intel/x86_64 package is published." ;;
    *) die "unsupported macOS arch '$ARCH'." ;;
  esac
  if up_to_date "$ver"; then
    log "remotive $ver is already installed. Use -f to force reinstall."; return 0
  fi
  local cur; cur="$(installed_version)"
  [ -n "$cur" ] && log "Upgrading remotive: $cur -> $ver"

  local base="$BUCKET_BASE/$ver" file="$TMP/remotivelabs-cli-$ver-$pkg_arch.pkg"
  local url="$base/remotivelabs-cli-$ver-$pkg_arch.pkg"
  if [ -n "$DRY_RUN" ]; then
    log "+ fetch $url (+ .sha256), verify signature + notarization, sudo installer -pkg ... -target /"; return 0
  fi
  # 404 here means the macOS packages for this version are not published yet.
  curl -fIsS "$url" >/dev/null 2>&1 || die "no .pkg published for $ver ($pkg_arch). Install Homebrew, or try once macOS packages are published."
  fetch "$url" "$file"
  verify_sha256 "$file" "$url.sha256"

  # The .pkg is Developer ID Installer signed + notarized. `sudo installer`
  # bypasses Gatekeeper, so verify explicitly before installing.
  pkgutil --check-signature "$file" >/dev/null 2>&1 || die "signature check failed for the .pkg — refusing to install."
  spctl --assess --type install "$file" >/dev/null 2>&1 || die "notarization/Gatekeeper check failed for the .pkg — refusing to install."
  log "Signature + notarization OK"

  init_sudo
  root_do installer -pkg "$file" -target /
}

# ---------------------------------------------------------------------------
# Linux — apt (Debian/Ubuntu)
# ---------------------------------------------------------------------------
configure_apt_repo() {
  require_curl
  root_do install -d -m 0755 /usr/share/keyrings /etc/apt/sources.list.d
  local keyref
  if have gpg; then
    if [ -n "$DRY_RUN" ]; then
      log "+ curl $APT_KEY_URL | ${SUDO:+$SUDO }gpg --dearmor -o $APT_KEYRING"
    elif [ -n "$SUDO" ]; then
      curl -fsSL "$APT_KEY_URL" | $SUDO gpg --dearmor --yes -o "$APT_KEYRING"
    else
      curl -fsSL "$APT_KEY_URL" | gpg --dearmor --yes -o "$APT_KEYRING"
    fi
    keyref="$APT_KEYRING"
  else
    if [ -n "$DRY_RUN" ]; then log "+ curl $APT_KEY_URL > $APT_KEYRING_ASC"
    else curl -fsSL "$APT_KEY_URL" | root_write "$APT_KEYRING_ASC"; fi
    keyref="$APT_KEYRING_ASC"
  fi
  printf 'deb [signed-by=%s] %s %s %s\n' "$keyref" "$APT_REPO_URL" "$APT_DIST" "$APT_COMPONENT" \
    | root_write "$APT_LIST"
  root_do apt-get update
}

install_apt() {
  init_sudo
  configure_apt_repo
  local spec="$PKG_NAME"
  [ -n "$VERSION" ] && spec="$PKG_NAME=$VERSION"
  local extra=""
  [ -n "$FORCE" ] && extra="--reinstall"
  # `apt-get install` upgrades to the newest available when already installed.
  # shellcheck disable=SC2086
  root_do env DEBIAN_FRONTEND=noninteractive apt-get install -y $extra "$spec"
}

# ---------------------------------------------------------------------------
# Linux — yum/dnf (RHEL/Fedora/SUSE)
# ---------------------------------------------------------------------------
configure_yum_repo() {
  # gpgcheck=0 / repo_gpgcheck=0: Artifact Registry does not package-sign the
  # RPMs, and its repomd signature is not verifiable via dnf's repo_gpgcheck
  # (dnf rejects it as a "Bad PGP signature" even with the AR signer key
  # imported). Integrity therefore rests on HTTPS TLS to packages.remotivelabs.com
  # — the same config the RemotiveLabs infra documents as known-good.
  root_write "$YUM_REPO_FILE" <<EOF
[remotivelabs]
name=RemotiveLabs
baseurl=$YUM_BASEURL
enabled=1
repo_gpgcheck=0
gpgcheck=0
EOF
}

install_yum() {
  init_sudo
  configure_yum_repo
  local mgr
  if have dnf; then mgr="dnf"; elif have yum; then mgr="yum"; else die "neither dnf nor yum found."; fi
  if [ -n "$FORCE" ]; then
    local spec="$PKG_NAME"; [ -n "$VERSION" ] && spec="$PKG_NAME-$VERSION"
    root_do "$mgr" -y reinstall "$spec"
  elif [ -n "$VERSION" ]; then
    # Pin to an explicit version (upgrade/install as needed).
    root_do "$mgr" -y install "$PKG_NAME-$VERSION"
  elif rpm -q "$PKG_NAME" >/dev/null 2>&1; then
    # Already installed: `dnf install` is a no-op on an installed package, so
    # upgrade explicitly to pull the newest available version.
    root_do "$mgr" -y upgrade "$PKG_NAME"
  else
    root_do "$mgr" -y install "$PKG_NAME"
  fi
}

# ---------------------------------------------------------------------------
# Linux — self-contained tar bundle (fallback, no package manager needed)
# ---------------------------------------------------------------------------
install_linux_tar() {
  require_curl
  # The bundle ships a glibc CPython (python-build-standalone *-linux-gnu), so it
  # cannot run on musl-based distros (e.g. Alpine). Fail clearly rather than
  # leave a launcher that dies with a missing-loader error.
  if ls /lib/ld-musl-* >/dev/null 2>&1 || { have ldd && ldd --version 2>&1 | grep -qi musl; }; then
    die "the self-contained bundle requires glibc; musl distros (e.g. Alpine) are not supported. Install from PyPI instead: pipx install remotivelabs-cli"
  fi
  # Validate $HOME before anything else: all install/uninstall paths derive
  # from the validated, symlink-resolved value — never from the raw
  # environment — and nothing under an unvalidated $HOME is even probed.
  local real_home; real_home="$(validate_home)"
  local dest="$real_home/.local/opt" bindir="$real_home/.local/bin"

  local ver tar_arch
  ver="$(resolve_version)"
  case "$ARCH" in
    x86_64|amd64)   tar_arch=x86_64 ;;
    aarch64|arm64)  tar_arch=aarch64 ;;
    *) die "unsupported Linux arch '$ARCH'." ;;
  esac
  if up_to_date "$ver"; then
    log "remotive $ver is already installed. Use -f to force reinstall."; return 0
  fi
  local cur; cur="$(installed_version)"
  [ -n "$cur" ] && log "Upgrading remotive: $cur -> $ver"

  local url="$BUCKET_BASE/$ver/remotivelabs-cli-$ver-$tar_arch.tar.gz"
  local file="$TMP/remotivelabs-cli-$ver-$tar_arch.tar.gz"
  if [ -n "$DRY_RUN" ]; then
    log "+ fetch $url (+ .sha256), verify, extract to $dest, write $bindir/remotive + $bindir/remotive-uninstall"; return 0
  fi
  curl -fIsS "$url" >/dev/null 2>&1 || die "no tar bundle published for $ver ($tar_arch)."
  fetch "$url" "$file"
  verify_sha256 "$file" "$url.sha256"

  mkdir -p "$dest" "$bindir"
  # Clean replace on upgrade. Check-then-delete with no gap: a symlink here is
  # not ours (we always create a real directory), and --one-file-system keeps
  # the recursive delete from crossing a mount planted underneath.
  if [ -L "$dest/remotivelabs-cli" ]; then
    die "$dest/remotivelabs-cli is a symlink, not a directory created by this installer — refusing to replace it."
  fi
  rm -rf --one-file-system -- "$dest/remotivelabs-cli"
  tar -xzf "$file" -C "$dest"                      # yields $dest/remotivelabs-cli/{python,remotive}
  # Write an absolute-path launcher (same pattern as the deb/rpm/pkg/brew
  # wrappers) rather than symlinking the bundle's own wrapper — robust
  # regardless of how the shell resolves the launcher's path.
  cat > "$bindir/remotive" <<EOF
#!/bin/sh
exec "$dest/remotivelabs-cli/python/bin/python" "$dest/remotivelabs-cli/python/bin/remotive" "\$@"
EOF
  chmod 0755 "$bindir/remotive"

  # Ship an uninstaller inside the install dir and expose it on the PATH as
  # `remotive-uninstall`, mirroring the macOS .pkg. Written here rather than
  # baked into the tarball because the paths depend on the user's $HOME. It
  # undoes the three things this install creates: the bundle dir, the
  # launcher, and the uninstaller link itself.
  cat > "$dest/remotivelabs-cli/uninstall.sh" <<EOF
#!/bin/bash
#
# Uninstalls the RemotiveLabs CLI installed via the self-contained tar bundle.
# (For an apt/yum install, use the package manager instead:
#  sudo apt-get remove remotivelabs-cli / sudo dnf remove remotivelabs-cli.)
#
# Usage: remotive-uninstall [--dry-run]
#   -n, --dry-run   print what would be removed without changing anything
#
set -euo pipefail

INSTALL_DIR="$dest/remotivelabs-cli"
LAUNCHER="$bindir/remotive"
UNINSTALL_LINK="$bindir/remotive-uninstall"

DRY_RUN=""
for arg in "\$@"; do
    case "\$arg" in
        -n|--dry-run) DRY_RUN=1 ;;
        *) echo "Usage: \$0 [--dry-run]" >&2; exit 1 ;;
    esac
done

# run <cmd...> — execute, or just print under --dry-run.
run() {
    if [ -n "\$DRY_RUN" ]; then echo "would run: \$*"; else "\$@"; fi
}

# Every removal below re-validates its target immediately before the rm — no
# code between the check and the delete — so a target swapped for a symlink
# or foreign file after an earlier check is still caught (TOCTOU hardening).
# The paths are install-time literals derived from the validated home
# directory; the script takes no path input and never consults the runtime
# environment for them.

# Up-front gate: refuse before removing ANYTHING if the install dir is not a
# regular directory we own. The installer creates a real directory, so a
# symlink here is not ours — removal must never be redirectable to another
# tree via a planted link — and a foreign-owned dir means this is not our
# install to remove. (Each removal below still re-checks at delete time.)
if [ -L "\$INSTALL_DIR" ]; then
    echo "error: \$INSTALL_DIR is a symlink, not the directory created by install.sh — refusing to uninstall." >&2
    exit 1
fi
if [ -d "\$INSTALL_DIR" ] && [ ! -O "\$INSTALL_DIR" ]; then
    echo "error: \$INSTALL_DIR is not owned by \$(id -un) — refusing to uninstall." >&2
    exit 1
fi

# Only remove the launcher if it is a regular file we own pointing at our
# install, so we never clobber a hand-made \`remotive\` that happens to live
# there. A symlink is not ours either: install.sh writes a real file.
if [ -L "\$LAUNCHER" ]; then
    echo "warning: \$LAUNCHER is a symlink (not the launcher install.sh writes) — leaving it in place." >&2
elif [ -f "\$LAUNCHER" ] && [ -O "\$LAUNCHER" ] && grep -qF -- "\$INSTALL_DIR" "\$LAUNCHER" 2>/dev/null; then
    echo "Removing \$LAUNCHER ..."
    run rm -f -- "\$LAUNCHER"
fi

# Remove our uninstaller symlink only if it still points into our install dir
# (otherwise it would be left dangling once INSTALL_DIR is gone). rm on a
# symlink removes the link itself, never the target.
if [ -L "\$UNINSTALL_LINK" ] && [ "\$(readlink "\$UNINSTALL_LINK")" = "\$INSTALL_DIR/uninstall.sh" ]; then
    echo "Removing \$UNINSTALL_LINK ..."
    run rm -f -- "\$UNINSTALL_LINK"
fi

# Final check-then-delete with no gap: still a real directory (not a symlink
# swapped in since the check above), and owned by us — never escalate onto a
# root-owned or foreign tree. --one-file-system keeps the recursive delete
# from crossing into a filesystem mounted underneath the install dir.
if [ -e "\$INSTALL_DIR" ] || [ -L "\$INSTALL_DIR" ]; then
    if [ -L "\$INSTALL_DIR" ] || [ ! -d "\$INSTALL_DIR" ]; then
        echo "error: \$INSTALL_DIR is no longer a regular directory — refusing to remove it." >&2
        exit 1
    fi
    if [ ! -O "\$INSTALL_DIR" ]; then
        echo "error: \$INSTALL_DIR is not owned by \$(id -un) — refusing to remove it." >&2
        exit 1
    fi
    echo "Removing \$INSTALL_DIR ..."
    run rm -rf --one-file-system -- "\$INSTALL_DIR"
fi

if [ -n "\$DRY_RUN" ]; then
    echo "Dry run complete — nothing was changed."
else
    echo "✓ RemotiveLabs CLI uninstalled."
fi
EOF
  chmod 0755 "$dest/remotivelabs-cli/uninstall.sh"
  # Same rm-before-link rationale as the .pkg postinstall: remove any
  # pre-existing entry (incl. a planted symlink) before creating our own.
  rm -f "$bindir/remotive-uninstall"
  ln -s "$dest/remotivelabs-cli/uninstall.sh" "$bindir/remotive-uninstall"

  log "Installed to $dest/remotivelabs-cli"
  log "Uninstall with: remotive-uninstall"
}

# ---------------------------------------------------------------------------
# Method selection
# ---------------------------------------------------------------------------

# choose_macos_method — when Homebrew is present, ask whether to install via
# Homebrew or the standalone .pkg. Reads from the controlling terminal so it
# still works when the script is piped (curl ... | bash), where stdin is the
# script itself. Falls back to brew (the previous default) when there is no
# terminal to prompt on. Echoes the chosen method on stdout; prompt goes to
# stderr so it does not pollute the captured value.
choose_macos_method() {
  # Probe by actually OPENING the terminal (fd 3): `[ -r /dev/tty ]` is not
  # enough — on a host with no controlling terminal the node tests readable but
  # opening it fails ("Device not configured"), which would print the prompt and
  # then error. If it can't be opened, silently default to brew.
  if ! { exec 3</dev/tty; } 2>/dev/null; then echo brew; return; fi
  local ans
  log ""
  log "Homebrew detected. How would you like to install $CMD?"
  log "  [1] Homebrew  (brew install $BREW_FORMULA) — recommended"
  log "  [2] Standalone .pkg installer (no Homebrew)"
  printf 'Choose [1/2] (default 1): ' >&2
  read -r ans <&3 || ans=""
  exec 3<&-
  case "$ans" in
    2|pkg|p|P) echo pkg ;;
    *)         echo brew ;;
  esac
}

detect_method() {
  case "$OS" in
    Darwin)
      # An explicit version can only be honored by the .pkg path (Homebrew
      # installs the tap's latest formula), so skip the prompt and pick pkg.
      if [ -n "$VERSION" ]; then echo pkg
      elif have brew; then choose_macos_method
      else echo pkg; fi ;;
    Linux)
      if have apt-get; then echo apt
      elif have dnf || have yum; then echo yum
      else echo tar; fi ;;
    *) die "unsupported OS '$OS'." ;;
  esac
}

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
case "$ARCH" in
  x86_64|amd64|arm64|aarch64) : ;;
  *) warn "unrecognized architecture '$ARCH' — install may fail." ;;
esac

[ "$METHOD" = "auto" ] && METHOD="$(detect_method)"
log "==> Installing $PKG_NAME via '$METHOD'${VERSION:+ (version $VERSION)}${DRY_RUN:+ [dry-run]}"

case "$METHOD" in
  brew) install_brew ;;
  apt)  install_apt ;;
  yum)  install_yum ;;
  pkg)  install_macos_pkg ;;
  tar)  install_linux_tar ;;
  *)    die "unknown method '$METHOD' (expected: brew|apt|yum|pkg|tar)" ;;
esac

# ---------------------------------------------------------------------------
# Verify
# ---------------------------------------------------------------------------
if [ -n "$DRY_RUN" ]; then log "Dry run complete."; exit 0; fi

if have "$CMD"; then
  log ""
  log "✓ Installed: $("$CMD" --version 2>/dev/null || echo "$CMD")"
  log "  $(command -v "$CMD")"
elif [ -x "${HOME:-/nonexistent}/.local/bin/$CMD" ]; then
  log ""
  log "✓ Installed: $("$HOME/.local/bin/$CMD" --version 2>/dev/null || echo "$CMD")"
  warn "$HOME/.local/bin is not on your PATH."
  log "  Add it, e.g.:  export PATH=\"\$HOME/.local/bin:\$PATH\""
else
  log ""
  warn "'$CMD' is not on your PATH yet — open a new terminal or check your PATH."
fi
