#!/usr/bin/env bash
#
# Thumbdesk host agent installer
#
#   curl -fsSL https://thumbdesk.com/install.sh | bash
#
# What it does:
#   1. detects your OS / arch (macOS or Linux, x86_64 or arm64; glibc/musl on Linux)
#   2. fetches the matching pre-built `thumbdesk` binary from the release host
#   3. verifies the SHA-256 checksum
#   4. installs it to ~/.local/bin (or /usr/local/bin if running as root)
#
# On macOS the agent is an app: the app bundle goes to /Applications (or
# ~/Applications), and the `thumbdesk` command in the install dir (plus a
# `pocketshell` alias, which stays forever) is a link to the binary inside it.
# The app is the only copy, so deleting it removes
# the agent. Releases from before the app existed install the plain binary.
#
# Release asset names keep the pre-rename `pocketshell-` prefix: agents already
# in the field self-update from exactly those names.
#
# Releases live on GitHub. $THUMBDESK_BASE_URL is a repo URL; assets are
# resolved via GitHub's standard release URLs:
#
#   <base>/releases/latest                                            # 302 → /releases/tag/vX.Y.Z
#   <base>/releases/download/<tag>/pocketshell-<tag>-<triple>.tar.gz
#   <base>/releases/download/<tag>/pocketshell-<tag>-<triple>.tar.gz.sha256
#   <base>/releases/download/<tag>/pocketshell-<tag>-<triple>.app.tar.gz   # macOS
#
# The in-binary `thumbdesk update` uses the same convention, so a single
# release on GitHub serves both fresh installs and self-updates.
#
# Optional env vars (override the defaults; the older POCKETSHELL_* names are
# still read when the THUMBDESK_* one is unset):
#   THUMBDESK_VERSION       e.g. v0.1.0  (default: latest release tag)
#   THUMBDESK_INSTALL_DIR   e.g. /usr/local/bin  (default: ~/.local/bin, or /usr/local/bin as root)
#   THUMBDESK_APP_DIR       macOS: where the app goes  (default: /Applications if
#                             writable, else ~/Applications)
#   THUMBDESK_VARIANT       gnu | musl  (default: auto-detect on Linux)
#   THUMBDESK_BASE_URL      e.g. https://github.com/<owner>/<repo>  (default: yashagldit/Thumbdesk)
#   THUMBDESK_SKIP_COSIGN   1 to skip Sigstore signature verification (insecure — only use
#                             if you've manually verified the artifact out of band)
#

set -euo pipefail

# THUMBDESK_* wins; POCKETSHELL_* is the pre-rename name, kept so existing
# automation that sets it keeps working.
for v in BASE_URL VERSION INSTALL_DIR APP_DIR VARIANT SKIP_COSIGN NO_PAIR; do
  eval "if [ -z \"\${THUMBDESK_$v:-}\" ] && [ -n \"\${POCKETSHELL_$v:-}\" ]; then THUMBDESK_$v=\"\$POCKETSHELL_$v\"; fi"
done

BASE_URL="${THUMBDESK_BASE_URL:-https://github.com/yashagldit/Thumbdesk}"
# Strip a single trailing slash so concatenation never double-slashes.
BASE_URL="${BASE_URL%/}"
VERSION="${THUMBDESK_VERSION:-latest}"

# ---- pretty output ---------------------------------------------------------
if [ -t 1 ]; then
  R=$'\033[0m'; B=$'\033[1m'; DIM=$'\033[2m'
  G=$'\033[32m'; Y=$'\033[33m'; RED=$'\033[31m'
else
  R=; B=; DIM=; G=; Y=; RED=
fi
say()  { printf '%s\n' "$*"; }
info() { printf '%s==>%s %s\n' "$G" "$R" "$*"; }
warn() { printf '%swarn:%s %s\n' "$Y" "$R" "$*" >&2; }
die()  { printf '%serror:%s %s\n' "$RED" "$R" "$*" >&2; exit 1; }

# ---- preflight -------------------------------------------------------------
need() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
need curl
need tar
need uname
need install
need mktemp

if command -v shasum >/dev/null 2>&1; then
  SHA_CHECK="shasum -a 256 -c"
elif command -v sha256sum >/dev/null 2>&1; then
  SHA_CHECK="sha256sum -c"
else
  die "neither shasum nor sha256sum is available; cannot verify download"
fi

# ---- platform detection ----------------------------------------------------
OS="$(uname -s)"
ARCH="$(uname -m)"

case "$OS" in
  Darwin) os_kind=macos ;;
  Linux)  os_kind=linux ;;
  MINGW*|MSYS*|CYGWIN*)
    die "Windows is not yet supported." ;;
  *) die "unsupported OS: $OS" ;;
esac

case "$ARCH" in
  x86_64|amd64)   arch_kind=x86_64 ;;
  arm64|aarch64)  arch_kind=aarch64 ;;
  *) die "unsupported architecture: $ARCH" ;;
esac

# Detect musl on Linux unless the caller forced a variant. The release
# matrix only ships musl for x86_64, so we only branch on that.
variant="${THUMBDESK_VARIANT:-}"
if [ "$os_kind" = "linux" ] && [ -z "$variant" ]; then
  if [ -f /lib/ld-musl-x86_64.so.1 ] \
      || [ -f /lib/ld-musl-aarch64.so.1 ] \
      || [ -f /etc/alpine-release ] \
      || (ldd --version 2>&1 | grep -qi musl); then
    variant=musl
  else
    variant=gnu
  fi
fi

# ---- map to release target -------------------------------------------------
case "$os_kind/$arch_kind" in
  macos/aarch64)  target=aarch64-apple-darwin ;;
  macos/x86_64)
    die "Intel Macs are not supported. Thumbdesk only ships Apple Silicon (M-series) macOS binaries." ;;
  linux/aarch64)  target=aarch64-unknown-linux-gnu ;;
  linux/x86_64)
    if [ "$variant" = "musl" ]; then
      target=x86_64-unknown-linux-musl
    else
      target=x86_64-unknown-linux-gnu
    fi ;;
  *) die "no prebuilt binary for $os_kind/$arch_kind." ;;
esac

# ---- resolve version -------------------------------------------------------
# GitHub's `/releases/latest` 302s to `/releases/tag/vX.Y.Z`. We don't parse
# the body — just follow the redirect and read the final URL's last path
# segment. Pure curl, no API quota, no JSON parsing.
if [ "$VERSION" = "latest" ]; then
  info "resolving latest version from ${BASE_URL}/releases/latest"
  final="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "${BASE_URL}/releases/latest" || true)"
  resolved="${final##*/}"
  # A repo with no full release redirects to /releases, which is no version.
  case "$resolved" in
    v[0-9]*) ;;
    *) resolved="" ;;
  esac
  if [ -z "$resolved" ]; then
    die "could not resolve latest version — is ${BASE_URL} a GitHub repo with a published release?"
  fi
  VERSION="$resolved"
fi

# Canonicalize so users can pass either `0.1.0` or `v0.1.0` via env.
case "$VERSION" in
  v*) ;;
  *)  VERSION="v${VERSION}" ;;
esac

# ---- install location ------------------------------------------------------
if [ -n "${THUMBDESK_INSTALL_DIR:-}" ]; then
  install_dir="$THUMBDESK_INSTALL_DIR"
elif [ "$(id -u)" = "0" ]; then
  install_dir=/usr/local/bin
else
  install_dir="$HOME/.local/bin"
fi

# ---- download & verify -----------------------------------------------------
archive="pocketshell-${VERSION}-${target}.tar.gz"
url="${BASE_URL}/releases/download/${VERSION}/${archive}"

# macOS installs the app when the release has one. A release without it
# (anything before the app existed) answers 404 here and installs the binary.
package=binary
if [ "$os_kind" = "macos" ]; then
  app_archive="pocketshell-${VERSION}-${target}.app.tar.gz"
  app_url="${BASE_URL}/releases/download/${VERSION}/${app_archive}"
  if curl -fsSLI -o /dev/null "$app_url" 2>/dev/null; then
    package=app
    archive="$app_archive"
    url="$app_url"
  fi
fi

tmp="$(mktemp -d -t thumbdesk.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT

info "downloading ${B}${archive}${R}"
say  "${DIM}${url}${R}"
curl -fL --progress-bar -o "$tmp/$archive"        "$url" \
  || die "download failed. check that ${VERSION} exists at ${BASE_URL}/releases/tag/${VERSION}"
curl -fsSL              -o "$tmp/$archive.sha256" "$url.sha256" \
  || die "checksum file missing for this release"

info "verifying checksum"
( cd "$tmp" && $SHA_CHECK "$archive.sha256" >/dev/null ) || die "checksum mismatch — refusing to install"

# ---- cosign keyless verification -------------------------------------------
# Verifies the Sigstore bundle (signature + Fulcio cert + Rekor inclusion
# proof) against the pinned GitHub workflow identity that produced the
# release. The SHA-256 above only proves integrity against the release origin
# — same origin signed both the binary and the checksum, so a GitHub Releases
# compromise alone would pass that check. The cosign signature is bound to a
# specific OIDC token from a specific workflow path, so forging it requires
# stealing both the GitHub release flow AND minting a fake OIDC token.
#
# cosign is recommended, not required: if it is missing, this script warns and
# the install proceeds, and `thumbdesk update` does the same, so an unattended
# host can still update. If it is present, a failed verification stops here.
# Each repository signs its own copy of a release.
COSIGN_IDENTITY_REGEXP='^https://github\.com/yashagldit/(PocketShell|Thumbdesk)/\.github/workflows/release-host-agent\.yml@refs/tags/v[0-9][0-9A-Za-z.-]*$'
COSIGN_OIDC_ISSUER='https://token.actions.githubusercontent.com'

if [ "${THUMBDESK_SKIP_COSIGN:-0}" = "1" ]; then
  warn "THUMBDESK_SKIP_COSIGN=1 set; skipping Sigstore signature verification."
  warn "The SHA-256 check above only proves integrity against the release origin (GitHub Releases)."
  warn "Continuing without authenticity verification at your explicit request."
elif ! command -v cosign >/dev/null 2>&1; then
  warn "cosign not found — skipping signature verification (SHA-256 checked). Install cosign for stronger guarantees on future updates."
else
  info "verifying Sigstore signature"
  if ! curl -fsSL -o "$tmp/$archive.cosign-bundle.json" "$url.cosign-bundle.json"; then
    die "cosign bundle is missing for this release. If you have manually verified the SHA-256 out of band, set THUMBDESK_SKIP_COSIGN=1 to bypass."
  fi
  if ! cosign verify-blob \
        --new-bundle-format \
        --bundle "$tmp/$archive.cosign-bundle.json" \
        --certificate-identity-regexp "$COSIGN_IDENTITY_REGEXP" \
        --certificate-oidc-issuer "$COSIGN_OIDC_ISSUER" \
        "$tmp/$archive" >/dev/null 2>"$tmp/cosign.err"; then
    say "${DIM}$(cat "$tmp/cosign.err")${R}" >&2
    die "cosign verification failed — refusing to install. Identity expected: $COSIGN_IDENTITY_REGEXP"
  fi
fi

info "extracting"
tar -xzf "$tmp/$archive" -C "$tmp"

# ---- install ---------------------------------------------------------------
mkdir -p "$install_dir" 2>/dev/null || die "cannot create $install_dir"

# Names inside the archives. The release contract (and so the archive layout
# agents already in the field update from) still uses the pre-rename names:
# a `pocketshell` binary, and on macOS a PocketShell.app folder. A newer layout
# with the Thumbdesk names is accepted too.
link_cmd() { # link_cmd <target> <name>   (symlink, via sudo when needed)
  if [ -w "$install_dir" ]; then
    rm -f "$install_dir/$2"; ln -s "$1" "$install_dir/$2"
  else
    sudo rm -f "$install_dir/$2"; sudo ln -s "$1" "$install_dir/$2"
  fi
}
if [ ! -w "$install_dir" ]; then
  command -v sudo >/dev/null 2>&1 \
    || die "$install_dir is not writable and sudo is unavailable. Set THUMBDESK_INSTALL_DIR to a writable path."
  warn "$install_dir is not writable; using sudo"
fi

if [ "$package" = "app" ]; then
  src_app=
  for cand in Thumbdesk.app PocketShell.app; do
    [ -d "$tmp/$cand" ] && { src_app="$tmp/$cand"; break; }
  done
  [ -n "$src_app" ] || die "archive did not contain an app bundle"
  app_name="$(basename "$src_app")"
  src_bin=
  for cand in thumbdesk pocketshell; do
    [ -x "$src_app/Contents/MacOS/$cand" ] && { src_bin="$cand"; break; }
  done
  [ -n "$src_bin" ] || die "archive did not contain the agent binary inside $app_name"

  if [ -n "${THUMBDESK_APP_DIR:-}" ]; then
    app_dir="$THUMBDESK_APP_DIR"
  elif [ -w /Applications ]; then
    app_dir=/Applications
  else
    app_dir="$HOME/Applications"
  fi
  mkdir -p "$app_dir" 2>/dev/null || die "cannot create $app_dir"
  [ -w "$app_dir" ] || die "$app_dir is not writable. Set THUMBDESK_APP_DIR to a writable path."
  app="$app_dir/$app_name"

  info "installing ${B}${app_name}${R} → ${app}"
  # Replaced whole, never merged: a file left over from an older version
  # inside the bundle would break its signature. ditto keeps the signature
  # and the notarization ticket intact.
  rm -rf "$app"
  ditto "$src_app" "$app"

  # The commands are links into the app, replacing any binary from an earlier
  # install, so the app stays the only copy. `pocketshell` stays as an alias.
  bin_in_app="$app/Contents/MacOS/$src_bin"
  link_cmd "$bin_in_app" thumbdesk
  link_cmd "$bin_in_app" pocketshell
else
  src_bin=
  for cand in thumbdesk pocketshell; do
    [ -f "$tmp/$cand" ] && { src_bin="$cand"; break; }
  done
  [ -n "$src_bin" ] || die "tarball did not contain a thumbdesk binary"
  chmod +x "$tmp/$src_bin"

  if [ -w "$install_dir" ]; then
    install -m 0755 "$tmp/$src_bin" "$install_dir/thumbdesk"
  else
    sudo install -m 0755 "$tmp/$src_bin" "$install_dir/thumbdesk"
  fi
  link_cmd thumbdesk pocketshell
fi

# macOS curl-downloaded binaries don't normally carry the quarantine xattr,
# but tar-extracted ones can inherit it on some versions of macOS. Strip it
# so the user doesn't hit Gatekeeper on first run.
if [ "$os_kind" = "macos" ] && command -v xattr >/dev/null 2>&1; then
  if [ "$package" = "app" ]; then
    xattr -dr com.apple.quarantine "$app" 2>/dev/null || true
  else
    xattr -d com.apple.quarantine "$install_dir/thumbdesk" 2>/dev/null || true
  fi
fi

# ---- post-install ----------------------------------------------------------
case ":$PATH:" in
  *":$install_dir:"*) on_path=1 ;;
  *) on_path=0 ;;
esac

say
info "${B}installed${R} thumbdesk ${VERSION} → ${install_dir}/thumbdesk"
"$install_dir/thumbdesk" --version 2>/dev/null || true
say

if [ "$on_path" = "0" ]; then
  warn "$install_dir is not on your PATH. add this to your shell rc (then open a new shell):"
  say  "    ${DIM}export PATH=\"$install_dir:\$PATH\"${R}"
  say  "until then, use the full path below for the commands in 'Next steps'."
  say
  cmd="$install_dir/thumbdesk"
else
  cmd="thumbdesk"
fi

next_steps() {
  cat <<NEXT
${B}Next steps${R}

  1. Pair with your phone:  ${DIM}${cmd} pair${R}
     (a QR appears — scan it from the Thumbdesk app; the daemon installs
      and starts automatically once the scan succeeds)
  2. Check status / logs:   ${DIM}${cmd} status${R}
  3. Self-update later:     ${DIM}${cmd} update${R}

Website: https://thumbdesk.com
NEXT
}

# ---- auto-pair ---------------------------------------------------------------
# One-paste setup: when a real terminal is attached, launch `thumbdesk pair`
# immediately so the QR shows up without the user needing a second command.
# `pair` finishes the whole job itself — after the phone scans, it persists
# auth and installs/starts the daemon.
#
# Skipped (falls back to printed next steps) when:
#   - THUMBDESK_NO_PAIR=1            (automation / config-management opt-out)
#   - no usable /dev/tty               (CI, provisioning scripts)
#   - ~/.thumbdesk/state.json exists   (already paired — a reinstall shouldn't
#                                       surprise the user with a device-add QR)
#     (~/.pocketshell/ is the pre-rename state dir, migrated by the agent on
#     first run, so an install that still has it counts as paired too)
state_file="${HOME}/.thumbdesk/state.json"
[ -f "$state_file" ] || state_file="${HOME}/.pocketshell/state.json"
if [ "${THUMBDESK_NO_PAIR:-0}" = "1" ]; then
  next_steps
elif ! { : < /dev/tty; } 2>/dev/null; then
  next_steps
elif [ -f "$state_file" ]; then
  say "existing Thumbdesk state found (${DIM}${state_file}${R}) — skipping auto-pairing."
  if [ "$package" = "app" ]; then
    # A daemon started before this install is still the old process.
    say "restart the daemon to run it from the app:  ${DIM}${cmd} daemon restart${R}"
  fi
  next_steps
else
  say
  info "starting pairing — scan the QR below with the Thumbdesk app"
  say  "${DIM}(on the phone: Hosts → Pair host → Scan QR · press Ctrl-C to pair later with \`${cmd} pair\`)${R}"
  say
  "$install_dir/thumbdesk" pair < /dev/tty || {
    warn "pairing did not complete — you can retry any time:"
    say
    next_steps
  }
fi
