#!/usr/bin/env bash
set -euo pipefail

# Clank Cut installer — CLI + agent skill + macOS desktop app.
#
# What this script does. No sudo; everything stays in your home directory
# except the /Applications copy of the app:
#   1. Downloads the clankcut CLI from the public releases repo
#      (github.com/hamzers/clankcut-desktop-releases) and verifies its
#      SHA-256 against the release checksum.
#   2. Installs it under ~/.local/share/clankcut with a symlink at
#      ~/.local/bin/clankcut.
#   3. Installs the clankcut-video Agent Skill (a plain markdown doc) into
#      ~/.claude/skills, ~/.codex/skills, and ~/.agents/skills. It refuses to
#      overwrite any skill it does not manage.
#   4. Writes ~/.config/clankcut/config.json.
#   5. On macOS (unless --no-app, or the app is already installed): downloads
#      the Developer ID-signed, notarized desktop app, verifies its SHA-512
#      against the auto-update manifest, moves it into /Applications, and
#      opens it once so macOS registers it for permission grants.
#
# Uninstall everything: re-run this script with --uninstall.
# Agent-facing docs: https://www.clankcut.com/llms.txt

PUBLIC_APP_URL="https://www.clankcut.com"
VERSION="${CLANKCUT_VERSION:-latest}"
# Pause before the single checksum-mismatch retry; tests set it to 0.
RETRY_DELAY_SECONDS="${CLANKCUT_RETRY_DELAY_SECONDS:-5}"
INSTALL_ROOT="${CLANKCUT_INSTALL_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/clankcut}"
BIN_DIR="${CLANKCUT_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/clankcut"
CODEX_SKILLS_DIR="${CLANKCUT_CODEX_SKILLS_DIR:-${CODEX_HOME:-$HOME/.codex}/skills}"
CLAUDE_SKILLS_DIR="${CLANKCUT_CLAUDE_SKILLS_DIR:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills}"
AGENTS_SKILLS_DIR="${CLANKCUT_AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
DOWNLOAD_ROOT="${CLANKCUT_DOWNLOAD_BASE_URL:-https://github.com/hamzers/clankcut-desktop-releases/releases}"
# The desktop app ships from its own releases repo (electron-builder publish
# target + latest-mac.yml update feed) — distinct from the CLI's feed above.
APP_DOWNLOAD_ROOT="${CLANKCUT_APP_DOWNLOAD_BASE_URL:-https://github.com/hamzers/clankcut-desktop-releases/releases}"
APP_NAME="Clank Cut.app"
# "1" on /install.sh, "0" on /install_no_app.sh; --with-app/--no-app override.
INSTALL_APP="1"
APP_FLAG_SET=0
SKILL_NAME="clankcut-video"
UNINSTALL=0
STAGED_VERSION_DIR=""
STAGED_SKILL_DIR=""

usage() {
  echo "Usage: install.sh [--version VERSION] [--install-dir DIR] [--bin-dir DIR] [--with-app] [--no-app] [--uninstall]" >&2
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --version)
      [ "$#" -ge 2 ] || { usage; exit 2; }
      VERSION="$2"; shift 2 ;;
    --install-dir)
      [ "$#" -ge 2 ] || { usage; exit 2; }
      INSTALL_ROOT="$2"; shift 2 ;;
    --bin-dir)
      [ "$#" -ge 2 ] || { usage; exit 2; }
      BIN_DIR="$2"; shift 2 ;;
    --with-app)
      INSTALL_APP=1; APP_FLAG_SET=1; shift ;;
    --no-app)
      INSTALL_APP=0; APP_FLAG_SET=1; shift ;;
    --uninstall)
      UNINSTALL=1; shift ;;
    -h|--help)
      usage; exit 0 ;;
    *)
      echo "Unknown option: $1" >&2; usage; exit 2 ;;
  esac
done

if [ -z "$INSTALL_ROOT" ] || [ "$INSTALL_ROOT" = "/" ] || [ "$INSTALL_ROOT" = "$HOME" ]; then
  echo "Refusing unsafe install directory: $INSTALL_ROOT" >&2
  exit 2
fi

for skills_dir in "$CODEX_SKILLS_DIR" "$CLAUDE_SKILLS_DIR" "$AGENTS_SKILLS_DIR"; do
  if [ -z "$skills_dir" ] || [ "$skills_dir" = "/" ] || [ "$skills_dir" = "$HOME" ]; then
    echo "Refusing unsafe agent skills directory: $skills_dir" >&2
    exit 2
  fi
done

legacy_managed_skill_link() {
  link_path="$1"
  [ -L "$link_path" ] || return 1
  case "$(readlink "$link_path")" in
    "$INSTALL_ROOT"/versions/*/dist/agent-skills/"$SKILL_NAME") return 0 ;;
    *) return 1 ;;
  esac
}

managed_alias_link() {
  link_path="$1"
  [ -L "$link_path" ] || return 1
  link_target="$(readlink "$link_path")"
  [ "$link_target" = "$AGENTS_SKILLS_DIR/$SKILL_NAME" ] ||     legacy_managed_skill_link "$link_path"
}

managed_canonical_skill() {
  skill_path="$AGENTS_SKILLS_DIR/$SKILL_NAME"
  if legacy_managed_skill_link "$skill_path"; then
    return 0
  fi
  [ -d "$skill_path" ] &&
    [ -f "$skill_path/.clankcut-install" ] &&
    [ "$(cat "$skill_path/.clankcut-install")" = "$INSTALL_ROOT" ]
}

managed_cli_link() {
  link_path="$1"
  [ -L "$link_path" ] || return 1
  case "$(readlink "$link_path")" in
    "$INSTALL_ROOT"/versions/*/dist/index.js) return 0 ;;
    *) return 1 ;;
  esac
}

remove_managed_alias() {
  skills_dir="$1"
  skill_path="$skills_dir/$SKILL_NAME"
  if [ "$skill_path" != "$AGENTS_SKILLS_DIR/$SKILL_NAME" ] &&
    managed_alias_link "$skill_path"; then
    rm -f "$skill_path"
    rmdir "$skills_dir" 2>/dev/null || true
  fi
}

if [ "$UNINSTALL" -eq 1 ]; then
  if [ ! -f "$INSTALL_ROOT/.clankcut-install" ]; then
    echo "Refusing to remove an unrecognized directory: $INSTALL_ROOT" >&2
    exit 2
  fi
  remove_managed_alias "$CODEX_SKILLS_DIR"
  remove_managed_alias "$CLAUDE_SKILLS_DIR"
  if managed_canonical_skill; then
    rm -rf "$AGENTS_SKILLS_DIR/$SKILL_NAME"
    rmdir "$AGENTS_SKILLS_DIR" 2>/dev/null || true
  fi
  if managed_cli_link "$BIN_DIR/clankcut"; then
    rm -f "$BIN_DIR/clankcut"
  fi
  rm -rf "$INSTALL_ROOT"
  rm -f "$CONFIG_DIR/config.json"
  rmdir "$CONFIG_DIR" 2>/dev/null || true
  echo "Clank Cut CLI and agent skill uninstalled."
  exit 0
fi

case "$(uname -s)" in
  Darwin|Linux) ;;
  *) echo "Unsupported operating system: $(uname -s)" >&2; exit 2 ;;
esac

command -v node >/dev/null 2>&1 || {
  echo "Clank Cut CLI requires Node.js 20 or newer." >&2
  exit 2
}
command -v curl >/dev/null 2>&1 || {
  echo "Clank Cut CLI requires curl." >&2
  exit 2
}
command -v tar >/dev/null 2>&1 || {
  echo "Clank Cut CLI requires tar." >&2
  exit 2
}
NODE_MAJOR="$(node -p "Number(process.versions.node.split('.')[0])")"
case "$NODE_MAJOR" in
  *[!0-9]*|"") echo "Could not determine the Node.js version." >&2; exit 2 ;;
esac
[ "$NODE_MAJOR" -ge 20 ] || {
  echo "Clank Cut CLI requires Node.js 20 or newer; found $(node --version)." >&2
  exit 2
}

[ "$VERSION" != "stable" ] || VERSION="latest"
if [ "$VERSION" != "latest" ] && ! node -e "
  process.exit(/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/.test(process.argv[1]) ? 0 : 1)
" "$VERSION"; then
  echo "Invalid release version: $VERSION" >&2
  exit 2
fi

TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/clankcut-install.XXXXXX")"
cleanup() {
  rm -rf "$TMP_DIR"
  if [ -n "$STAGED_VERSION_DIR" ]; then
    rm -rf "$STAGED_VERSION_DIR"
  fi
  if [ -n "$STAGED_SKILL_DIR" ]; then
    rm -rf "$STAGED_SKILL_DIR"
  fi
}
trap cleanup EXIT INT TERM

json_escape() {
  printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# Proxies and captive firewalls that block objects.githubusercontent.com often
# return an HTTP 200 HTML block page, which would otherwise surface as a bare
# checksum failure. Detect it and say what actually happened.
looks_like_html() {
  head -c 256 "$1" | grep -qi '<!doctype html\|<html'
}

report_intercepted_download() {
  echo "The downloaded $1 looks like an HTML page, not a release artifact." >&2
  echo "A proxy or firewall is likely intercepting downloads from" >&2
  echo "objects.githubusercontent.com. Allow that host and re-run." >&2
}

# CLI releases share the public repo with desktop releases, whose auto-update
# feed owns the repo's single "latest" mark — the CLI keeps a rolling
# cli-latest release instead (refreshed by packages/cli/scripts/release.mjs).
# Resolve "latest" through that release's latest-version.txt pointer to the
# immutable cli-v<version> release: the rolling release's tgz + sha256 pair is
# replaced non-atomically on each release (and GitHub's CDN can serve the two
# assets inconsistently for a while after), so downloading the pair from an
# immutable release is the only way the checksum can never mismatch. Fall back
# to the rolling assets when the pointer does not exist yet.
if [ "$VERSION" = "latest" ]; then
  POINTED_VERSION="$(curl -fsSL "$DOWNLOAD_ROOT/download/cli-latest/latest-version.txt" 2>/dev/null | tr -d '[:space:]' || true)"
  if [ -n "$POINTED_VERSION" ] && node -e "
    process.exit(/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/.test(process.argv[1]) ? 0 : 1)
  " "$POINTED_VERSION"; then
    VERSION="$POINTED_VERSION"
  fi
fi
if [ "$VERSION" = "latest" ]; then
  RELEASE_URL="$DOWNLOAD_ROOT/download/cli-latest"
else
  RELEASE_URL="$DOWNLOAD_ROOT/download/cli-v$VERSION"
fi
case "$RELEASE_URL" in
  https://*) ;;
  *)
    if [ "${CLANKCUT_ALLOW_HTTP:-0}" != "1" ]; then
      echo "Refusing non-HTTPS CLI download URL: $RELEASE_URL" >&2
      exit 2
    fi
    ;;
esac

command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1 || {
  echo "Clank Cut CLI installation requires sha256sum or shasum." >&2
  exit 2
}

ARCHIVE="$TMP_DIR/clankcut-cli.tgz"
CHECKSUM_FILE="$TMP_DIR/clankcut-cli.tgz.sha256"
# One retry smooths over truncated transfers and CDN propagation blips; a
# repeat mismatch is reported as the real failure it is.
CLI_FETCH_ATTEMPT=1
while :; do
  if ! curl -fsSL "$RELEASE_URL/clankcut-cli.tgz" -o "$ARCHIVE"; then
    echo "Could not download Clank Cut CLI release '$VERSION'." >&2
    exit 1
  fi
  if ! curl -fsSL "$RELEASE_URL/clankcut-cli.tgz.sha256" -o "$CHECKSUM_FILE"; then
    echo "Could not download the Clank Cut CLI checksum." >&2
    exit 1
  fi
  if looks_like_html "$ARCHIVE"; then
    report_intercepted_download "CLI archive"
    exit 1
  fi

  EXPECTED_SHA="$(awk 'NR == 1 { print $1 }' "$CHECKSUM_FILE")"
  if [ "${#EXPECTED_SHA}" -ne 64 ]; then
    echo "The Clank Cut CLI checksum is invalid." >&2
    exit 1
  fi
  case "$EXPECTED_SHA" in
    *[!0-9a-fA-F]*) echo "The Clank Cut CLI checksum is invalid." >&2; exit 1 ;;
  esac
  if command -v sha256sum >/dev/null 2>&1; then
    ACTUAL_SHA="$(sha256sum "$ARCHIVE" | awk '{ print $1 }')"
  else
    ACTUAL_SHA="$(shasum -a 256 "$ARCHIVE" | awk '{ print $1 }')"
  fi
  if [ "$EXPECTED_SHA" = "$ACTUAL_SHA" ]; then
    break
  fi
  if [ "$CLI_FETCH_ATTEMPT" -ge 2 ]; then
    echo "Clank Cut CLI checksum verification failed." >&2
    exit 1
  fi
  echo "Clank Cut CLI checksum mismatch — retrying the download once…" >&2
  CLI_FETCH_ATTEMPT=2
  sleep "$RETRY_DELAY_SECONDS"
done

EXTRACT_DIR="$TMP_DIR/extracted"
mkdir "$EXTRACT_DIR"
if ! tar -xzf "$ARCHIVE" -C "$EXTRACT_DIR"; then
  echo "Could not extract the Clank Cut CLI release." >&2
  exit 1
fi
PACKAGE_DIR="$EXTRACT_DIR/package"
PACKAGE_JSON="$PACKAGE_DIR/package.json"
ENTRYPOINT="$PACKAGE_DIR/dist/index.js"
SKILL_PACKAGE_DIR="$PACKAGE_DIR/dist/agent-skills/$SKILL_NAME"
[ -f "$PACKAGE_JSON" ] && [ -f "$ENTRYPOINT" ] && [ -f "$SKILL_PACKAGE_DIR/SKILL.md" ] || {
  echo "The release did not contain the Clank Cut CLI and agent skill." >&2
  exit 1
}
if ! RESOLVED_VERSION="$(node -e "
  const fs = require('node:fs');
  const value = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
  if (
    value.name !== 'clankcut' ||
    typeof value.version !== 'string' ||
    !/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/.test(value.version)
  ) process.exit(1);
  process.stdout.write(value.version);
" "$PACKAGE_JSON")"; then
  echo "The release contained an invalid package version." >&2
  exit 1
fi
if [ "$VERSION" != "latest" ] && [ "$VERSION" != "$RESOLVED_VERSION" ]; then
  echo "The downloaded CLI version '$RESOLVED_VERSION' did not match '$VERSION'." >&2
  exit 1
fi

VERSIONS_DIR="$INSTALL_ROOT/versions"
VERSION_DIR="$VERSIONS_DIR/$RESOLVED_VERSION"
mkdir -p "$VERSIONS_DIR" "$BIN_DIR"
if [ ! -e "$VERSION_DIR" ]; then
  STAGED_VERSION_DIR="$VERSIONS_DIR/.$RESOLVED_VERSION.$$"
  mkdir "$STAGED_VERSION_DIR"
  cp -R "$PACKAGE_DIR/." "$STAGED_VERSION_DIR/"
  chmod 0755 "$STAGED_VERSION_DIR/dist/index.js"
  mv "$STAGED_VERSION_DIR" "$VERSION_DIR"
  STAGED_VERSION_DIR=""
fi
[ -f "$VERSION_DIR/dist/index.js" ] || {
  echo "The existing Clank Cut CLI version directory is incomplete: $VERSION_DIR" >&2
  exit 1
}
SKILL_SOURCE="$VERSION_DIR/dist/agent-skills/$SKILL_NAME"
[ -f "$SKILL_SOURCE/SKILL.md" ] || {
  echo "The existing Clank Cut agent skill is incomplete: $SKILL_SOURCE" >&2
  exit 1
}

canonical_skill_path="$AGENTS_SKILLS_DIR/$SKILL_NAME"
if [ -L "$canonical_skill_path" ] || [ -e "$canonical_skill_path" ]; then
  if ! managed_canonical_skill; then
    echo "Refusing to replace an unmanaged agent skill: $canonical_skill_path" >&2
    exit 1
  fi
fi
mkdir -p "$AGENTS_SKILLS_DIR"

for skills_dir in "$CODEX_SKILLS_DIR" "$CLAUDE_SKILLS_DIR"; do
  skill_path="$skills_dir/$SKILL_NAME"
  if [ "$skill_path" = "$canonical_skill_path" ]; then
    continue
  fi
  if [ -L "$skill_path" ]; then
    if ! managed_alias_link "$skill_path"; then
      echo "Refusing to replace an unmanaged agent skill: $skill_path" >&2
      exit 1
    fi
  elif [ -e "$skill_path" ]; then
    echo "Refusing to replace an unmanaged agent skill: $skill_path" >&2
    exit 1
  fi
  mkdir -p "$skills_dir"
done

STAGED_SKILL_DIR="$(mktemp -d "$AGENTS_SKILLS_DIR/.$SKILL_NAME.XXXXXX")"
cp -R "$SKILL_SOURCE/." "$STAGED_SKILL_DIR/"
printf '%s
' "$INSTALL_ROOT" > "$STAGED_SKILL_DIR/.clankcut-install"
node -e "
  const fs = require('node:fs');
  const [staged, target] = process.argv.slice(1);
  const exists = (value) => {
    try {
      fs.lstatSync(value);
      return true;
    } catch (error) {
      if (error && error.code === 'ENOENT') return false;
      throw error;
    }
  };
  const backup = target + '.backup-' + process.pid + '-' + Date.now();
  let movedExisting = false;
  try {
    if (exists(target)) {
      fs.renameSync(target, backup);
      movedExisting = true;
    }
    fs.renameSync(staged, target);
    if (movedExisting) fs.rmSync(backup, { recursive: true, force: true });
  } catch (error) {
    if (!exists(target) && movedExisting) fs.renameSync(backup, target);
    throw error;
  }
" "$STAGED_SKILL_DIR" "$canonical_skill_path"
STAGED_SKILL_DIR=""

touch "$INSTALL_ROOT/.clankcut-install"
mkdir -p "$CONFIG_DIR"
CONFIG_TMP="$CONFIG_DIR/.config.$$"
printf '{"api_url":"%s","install_root":"%s","bin_dir":"%s","codex_skills_dir":"%s","claude_skills_dir":"%s","agents_skills_dir":"%s"}
'   "$(json_escape "$PUBLIC_APP_URL")"   "$(json_escape "$INSTALL_ROOT")"   "$(json_escape "$BIN_DIR")"   "$(json_escape "$CODEX_SKILLS_DIR")"   "$(json_escape "$CLAUDE_SKILLS_DIR")"   "$(json_escape "$AGENTS_SKILLS_DIR")" > "$CONFIG_TMP"
chmod 0600 "$CONFIG_TMP"
mv -f "$CONFIG_TMP" "$CONFIG_DIR/config.json"

LINK_TMP="$BIN_DIR/.clankcut.$$"
if [ -e "$BIN_DIR/clankcut" ] && [ ! -f "$BIN_DIR/clankcut" ] && [ ! -L "$BIN_DIR/clankcut" ]; then
  echo "Refusing to replace non-file destination: $BIN_DIR/clankcut" >&2
  exit 1
fi
rm -f "$LINK_TMP"
ln -s "$VERSION_DIR/dist/index.js" "$LINK_TMP"
mv -f "$LINK_TMP" "$BIN_DIR/clankcut"

install_skill_alias() {
  skills_dir="$1"
  skill_path="$skills_dir/$SKILL_NAME"
  if [ "$skill_path" = "$canonical_skill_path" ]; then
    return
  fi
  skill_tmp="$skills_dir/.$SKILL_NAME.$$"
  rm -f "$skill_tmp"
  ln -s "$canonical_skill_path" "$skill_tmp"
  node -e "
    require('node:fs').renameSync(process.argv[1], process.argv[2]);
  " "$skill_tmp" "$skill_path"
}

install_skill_alias "$CODEX_SKILLS_DIR"
install_skill_alias "$CLAUDE_SKILLS_DIR"

echo "Clank Cut CLI $RESOLVED_VERSION installed at $BIN_DIR/clankcut"
echo "Clank Cut agent skill installed for Codex, Claude Code, and compatible agents."
echo "  Codex: $CODEX_SKILLS_DIR/$SKILL_NAME"
echo "  Claude Code: $CLAUDE_SKILLS_DIR/$SKILL_NAME"
echo "  Agent Skills: $AGENTS_SKILLS_DIR/$SKILL_NAME"
echo "Restart any running agent that does not detect the new skill automatically."
case ":$PATH:" in
  *":$BIN_DIR:"*) ;;
  *) echo "Add $BIN_DIR to PATH to run clankcut." >&2 ;;
esac

# ---- Desktop app (macOS) -----------------------------------------------------
# Recordings execute on an open, signed-in desktop, so a machine without the
# app can queue jobs but never run them. Best-effort: a failure here never
# fails the CLI install above. Upgrades are the app's own auto-updater's job —
# this only ever installs when the app is absent.
install_desktop_app() {
  if [ "$INSTALL_APP" != "1" ]; then
    return 0
  fi
  if [ -n "${CI:-}" ] && [ "$APP_FLAG_SET" != "1" ]; then
    echo "CI environment detected — skipping the desktop app (use --with-app to force)."
    return 0
  fi
  if [ "$(uname -s)" != "Darwin" ]; then
    echo "The Clank Cut desktop app is macOS-only — skipping (recordings run on a Mac that has it)."
    return 0
  fi
  if [ -d "/Applications/$APP_NAME" ] || [ -d "$HOME/Applications/$APP_NAME" ]; then
    echo "Clank Cut.app is already installed — leaving it to its auto-updater."
    return 0
  fi
  case "$APP_DOWNLOAD_ROOT" in
    https://*) ;;
    *)
      if [ "${CLANKCUT_ALLOW_HTTP:-0}" != "1" ]; then
        echo "Refusing non-HTTPS app download URL: $APP_DOWNLOAD_ROOT" >&2
        return 1
      fi
      ;;
  esac
  case "$(uname -m)" in
    arm64) APP_ARCH="arm64" ;;
    x86_64) APP_ARCH="x64" ;;
    *)
      echo "No Clank Cut desktop build for $(uname -m) — skipping the app." >&2
      return 0
      ;;
  esac
  APP_MANIFEST="$TMP_DIR/latest-mac.yml"
  if ! curl -fsSL "$APP_DOWNLOAD_ROOT/latest/download/latest-mac.yml" -o "$APP_MANIFEST"; then
    echo "Could not fetch the Clank Cut desktop release manifest." >&2
    return 1
  fi
  APP_VERSION="$(awk '$1 == "version:" { print $2; exit }' "$APP_MANIFEST")"
  if [ -z "$APP_VERSION" ]; then
    echo "The Clank Cut desktop release manifest is invalid." >&2
    return 1
  fi
  APP_ZIP="Clank-Cut-$APP_VERSION-$APP_ARCH.zip"
  # Per-file sha512 (base64) from the same manifest the auto-updater trusts.
  APP_SHA512="$(awk -v want="$APP_ZIP" '
    $1 == "-" && $2 == "url:" { cur = $3 }
    $1 == "url:" { cur = $2 }
    $1 == "sha512:" { if (cur == want) { print $2; exit } }
  ' "$APP_MANIFEST")"
  if [ -z "$APP_SHA512" ]; then
    echo "No $APP_ARCH desktop build in release $APP_VERSION — skipping the app." >&2
    return 0
  fi
  APP_ARCHIVE="$TMP_DIR/$APP_ZIP"
  echo "Downloading Clank Cut.app $APP_VERSION ($APP_ARCH)…"
  if ! curl -fSL --progress-bar "$APP_DOWNLOAD_ROOT/latest/download/$APP_ZIP" -o "$APP_ARCHIVE"; then
    echo "Could not download the Clank Cut desktop app." >&2
    return 1
  fi
  if looks_like_html "$APP_ARCHIVE"; then
    report_intercepted_download "desktop app archive"
    return 1
  fi
  ACTUAL_APP_SHA="$(openssl dgst -sha512 -binary "$APP_ARCHIVE" | base64 | tr -d '\n')"
  if [ "$ACTUAL_APP_SHA" != "$APP_SHA512" ]; then
    echo "Clank Cut desktop app checksum verification failed." >&2
    return 1
  fi
  APP_EXTRACT_DIR="$TMP_DIR/app"
  mkdir -p "$APP_EXTRACT_DIR"
  # ditto preserves the code signature; the build is notarized, so Gatekeeper
  # accepts it without any quarantine gymnastics.
  if ! ditto -x -k "$APP_ARCHIVE" "$APP_EXTRACT_DIR"; then
    echo "Could not extract the Clank Cut desktop app." >&2
    return 1
  fi
  if [ ! -d "$APP_EXTRACT_DIR/$APP_NAME" ]; then
    echo "The desktop release did not contain $APP_NAME." >&2
    return 1
  fi
  APP_DEST="/Applications"
  if [ ! -w "$APP_DEST" ]; then
    APP_DEST="$HOME/Applications"
    mkdir -p "$APP_DEST"
  fi
  if ! mv "$APP_EXTRACT_DIR/$APP_NAME" "$APP_DEST/$APP_NAME"; then
    echo "Could not move $APP_NAME into $APP_DEST." >&2
    return 1
  fi
  echo "Clank Cut.app $APP_VERSION installed at $APP_DEST/$APP_NAME"
  open -g "$APP_DEST/$APP_NAME" 2>/dev/null || true
  echo "Open Clank Cut and sign in — recordings queue now and run while it is open."
}

if ! install_desktop_app; then
  echo "The CLI is installed, but the desktop app could not be — download it from" >&2
  echo "$APP_DOWNLOAD_ROOT or re-run this script to retry." >&2
fi
