#!/bin/sh
# LoopCode desktop app installer for macOS.
#
#   curl -fsSL https://loopcode.heksync.in/mac.sh | sh
#
# Downloads the LoopCode app for this Mac from the latest release, checks it the way the app's own
# updater does, and puts it in /Applications (or ~/Applications when /Applications isn't writable):
#   - its size and SHA-512 must match the release manifest (latest-mac.yml)
#   - it is unpacked with ditto, so its signature stays intact
#   - it must be LoopCode (in.heksync.loopcode), the version the manifest names, and correctly signed
# A LoopCode that is open is never replaced, and an older copy goes only once the new one is in place.
#
# Why from the terminal: a browser flags what it downloads as coming from the internet, and macOS then
# asks whether to trust an unidentified developer, because LoopCode is not notarised by Apple yet. A
# download made from the terminal carries no such flag, so the app opens straight away. That check of
# Apple's is skipped, which is why this script makes its own and installs nothing when one fails.
#
# Options (environment variables):
#   LOOPCODE_APP_DIR=/some/dir    install there instead of /Applications
#   LOOPCODE_NO_OPEN=1            don't open LoopCode afterwards
# Uninstall (your conversations and settings are kept):
#   curl -fsSL https://loopcode.heksync.in/mac.sh | sh -s -- --uninstall
#
# The whole script is inside main(), called on the last line, so a download cut off halfway
# cannot run half an installer.

set -eu

SITE="${LOOPCODE_INSTALL_SITE:-https://loopcode.heksync.in}"
FEED="$SITE/api/loopcode/update"
# Builds are only ever taken from the releases repository, whatever the manifest says.
RELEASES="https://github.com/SIDDU2402/heksync-releases/releases/download/"
BUNDLE_ID="in.heksync.loopcode"
HOME_DIR="${HOME:?HOME is not set}"

if [ -t 1 ]; then BOLD="$(printf '\033[1m')"; DIM="$(printf '\033[2m')"; GREEN="$(printf '\033[32m')"; RED="$(printf '\033[31m')"; RESET="$(printf '\033[0m')"; else BOLD=""; DIM=""; GREEN=""; RED=""; RESET=""; fi

say() { printf '%s\n' "$*"; }
step() { printf '  %s%s%s\n' "$DIM" "$*" "$RESET"; }
fail() { printf '\n  %s%s%s\n\n' "$RED" "$*" "$RESET" >&2; exit 1; }

# HTTPS only. Plain HTTP is allowed for localhost alone, so the installer can be tested end to end.
case "$SITE" in
  https://*) SECURE=1 ;;
  http://localhost*|http://127.0.0.1*) SECURE=0 ;;
  *) printf '  Refusing to download over an insecure connection: %s\n' "$SITE" >&2; exit 1 ;;
esac

download() { # url destination
  if [ "$SECURE" = 1 ]; then curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --connect-timeout 15 "$1" -o "$2"
  else curl -fsSL "$1" -o "$2"; fi
}

# The app itself is over 100 MB: show curl's progress bar when someone is watching.
download_app() { # url destination
  if [ -t 2 ]; then curl -fL --progress-bar --proto '=https' --tlsv1.2 --retry 3 --connect-timeout 15 "$1" -o "$2"
  else download "$1" "$2"; fi
}

# SHA-512 in base64, the form electron-builder writes into latest-mac.yml.
sha512_of() { /usr/bin/openssl dgst -sha512 -binary "$1" | /usr/bin/openssl base64 -A; }

plist_value() { /usr/bin/plutil -extract "$2" raw -o - "$1/Contents/Info.plist" 2>/dev/null || true; }

# This Mac's processor. A shell running under Rosetta reports x86_64 on Apple silicon; ask the kernel.
mac_arch() {
  if [ "$(uname -m)" = "arm64" ] || [ "$(/usr/sbin/sysctl -n sysctl.proc_translated 2>/dev/null || true)" = "1" ]; then printf 'arm64'
  else printf 'x64'; fi
}

app_dir() {
  if [ -n "${LOOPCODE_APP_DIR:-}" ]; then printf '%s' "$LOOPCODE_APP_DIR"
  elif [ -w /Applications ]; then printf '%s' /Applications
  else printf '%s' "$HOME_DIR/Applications"; fi
}

is_running() { /usr/bin/pgrep -f "$1/Contents/MacOS/LoopCode" >/dev/null 2>&1; }

# The zip for this processor in latest-mac.yml, as "url sha512 size". The manifest is electron-builder's:
#   files:
#     - url: https://…/LoopCode-0.7.4-mac-arm64.zip
#       sha512: <base64>
#       size: 128680630
pick_build() { # manifest arch
  awk -v want="-mac-$2.zip" '
    function clean(s) { gsub(/^[ \t]+|[ \t\r]+$/, "", s); gsub(/^"|"$/, "", s); return s }
    function done_entry() {
      if (url != "" && !found && length(url) > length(want) && substr(url, length(url) - length(want) + 1) == want) { print url, sha, size; found = 1 }
      url = ""
    }
    /^[ \t]*-[ \t]+url:/ { done_entry(); line = $0; sub(/^[ \t]*-[ \t]+url:/, "", line); url = clean(line); sha = ""; size = ""; next }
    /^[ \t]+sha512:/ { if (url != "") { line = $0; sub(/^[ \t]+sha512:/, "", line); sha = clean(line) } next }
    /^[ \t]+size:/ { if (url != "") { line = $0; sub(/^[ \t]+size:/, "", line); size = clean(line) } next }
    /^[^ \t]/ { done_entry() }
    END { done_entry() }
  ' "$1"
}

uninstall() {
  say ""
  removed=0
  # LOOPCODE_APP_DIR names the one folder to look in; without it, the two places this installer uses.
  if [ -n "${LOOPCODE_APP_DIR:-}" ]; then set -- "$LOOPCODE_APP_DIR"; else set -- /Applications "$HOME_DIR/Applications"; fi
  for dir in "$@"; do
    [ -d "$dir/LoopCode.app" ] || continue
    target="$dir/LoopCode.app"
    if [ "$(plist_value "$target" CFBundleIdentifier)" != "$BUNDLE_ID" ]; then step "left $target alone: it isn't LoopCode"; continue; fi
    is_running "$target" && fail "LoopCode is open. Quit it (⌘Q), then run this again."
    rm -rf "$target" || fail "Could not remove $target. Drag it to the Trash instead."
    step "removed $target"
    removed=1
  done
  if [ "$removed" = 1 ]; then say "  ${GREEN}LoopCode is uninstalled.${RESET}"; else say "  LoopCode isn't installed in $(if [ -n "${LOOPCODE_APP_DIR:-}" ]; then printf '%s' "$LOOPCODE_APP_DIR"; else printf '/Applications or ~/Applications'; fi)."; fi
  say "  ${DIM}Your conversations and settings are still in ~/Library/Application Support/LoopCode; delete that folder to remove them too.${RESET}"
  say ""
}

main() {
  if [ "${1:-}" = "--uninstall" ]; then uninstall; return; fi

  say ""
  say "  ${BOLD}LoopCode${RESET} ${DIM}· desktop app installer for macOS${RESET}"
  say ""

  [ "$(uname -s)" = "Darwin" ] || fail "This installer is for macOS. On Windows and Linux, download LoopCode from $SITE/download"
  MACOS="$(/usr/bin/sw_vers -productVersion 2>/dev/null || printf '0')"
  [ "${MACOS%%.*}" -ge 13 ] 2>/dev/null || fail "LoopCode needs macOS 13 Ventura or newer; this Mac has macOS $MACOS."
  command -v curl >/dev/null 2>&1 || fail "This installer needs curl."

  ARCH="$(mac_arch)"
  if [ "$ARCH" = arm64 ]; then LABEL="Apple silicon"; else LABEL="Intel"; fi

  TMP="$(mktemp -d 2>/dev/null || mktemp -d -t loopcode)"
  STAGE=""
  trap 'rm -rf "$TMP"; [ -z "$STAGE" ] || rm -rf "$STAGE"' EXIT INT TERM

  # Which build is current: the same manifest the app's updater reads.
  download "$FEED/latest-mac.yml" "$TMP/latest-mac.yml" || fail "Could not reach $SITE. Check your connection and try again."
  VERSION="$(sed -n 's/^version:[[:space:]]*//p' "$TMP/latest-mac.yml" | head -n1 | tr -d "\"' \r")"
  case "$VERSION" in ''|*[!0-9.]*) fail "The release server gave an unexpected version: ${VERSION:-none}" ;; esac
  ENTRY="$(pick_build "$TMP/latest-mac.yml" "$ARCH")"
  [ -n "$ENTRY" ] || fail "LoopCode $VERSION has no build for $LABEL Macs yet. Download it from $SITE/download instead."
  URL="${ENTRY%% *}"; REST="${ENTRY#* }"; SHA="${REST%% *}"; SIZE="${REST#* }"
  case "$URL" in "$RELEASES"*) ;; *) fail "The release manifest points outside LoopCode's releases ($URL), so nothing was downloaded." ;; esac
  case "$SIZE" in ''|*[!0-9]*) fail "The release manifest has no size for this build." ;; esac
  [ "${#SHA}" = 88 ] || fail "The release manifest has no checksum for this build, so nothing was downloaded."

  DIR="$(app_dir)"
  TARGET="$DIR/LoopCode.app"
  if [ -d "$TARGET" ]; then
    [ "$(plist_value "$TARGET" CFBundleIdentifier)" = "$BUNDLE_ID" ] || fail "$TARGET isn't LoopCode, so it was left alone. Move it aside, or set LOOPCODE_APP_DIR, and run this again."
    is_running "$TARGET" && fail "LoopCode is open. Quit it (⌘Q), then run this again."
    INSTALLED="$(plist_value "$TARGET" CFBundleShortVersionString)"
    if [ "$INSTALLED" = "$VERSION" ]; then step "LoopCode $VERSION is already installed in $DIR; installing it again"; fi
  fi
  mkdir -p "$DIR" 2>/dev/null || fail "Could not create $DIR."
  [ -w "$DIR" ] || fail "Can't write to $DIR. Set LOOPCODE_APP_DIR to a folder you own, and run this again."
  # Unpacked beside its destination, so putting it in place is a rename, not a copy.
  STAGE="$(mktemp -d "$DIR/.loopcode-install-XXXXXX")" || fail "Could not write to $DIR."

  step "downloading LoopCode $VERSION for $LABEL Macs ($((SIZE / 1000000)) MB)"
  download_app "$URL" "$TMP/LoopCode.zip" || fail "The download failed. Please try again."
  [ "$(/usr/bin/stat -f%z "$TMP/LoopCode.zip")" = "$SIZE" ] || fail "The download was incomplete, so nothing was installed. Please try again."
  [ "$(sha512_of "$TMP/LoopCode.zip")" = "$SHA" ] || fail "The download did not match the release's checksum, so nothing was installed. Please try again, or write to team@heksync.in."
  step "size and SHA-512 match the release"

  /usr/bin/ditto -x -k "$TMP/LoopCode.zip" "$STAGE" || fail "The download could not be unpacked, so nothing was installed."
  NEW="$STAGE/LoopCode.app"
  [ -d "$NEW" ] || fail "The download didn't contain LoopCode.app, so nothing was installed."
  [ "$(plist_value "$NEW" CFBundleIdentifier)" = "$BUNDLE_ID" ] || fail "The download isn't LoopCode, so nothing was installed."
  [ "$(plist_value "$NEW" CFBundleShortVersionString)" = "$VERSION" ] || fail "The download isn't LoopCode $VERSION, so nothing was installed."
  /usr/bin/codesign --verify --deep --strict "$NEW" 2>/dev/null || fail "The app's signature is broken, so nothing was installed. Please write to team@heksync.in."
  step "it is LoopCode $VERSION, and its signature is intact"

  # The old copy goes only once the new one is in place; if that fails, the old one comes back.
  OLD=""
  if [ -d "$TARGET" ]; then OLD="$STAGE/previous.app"; mv "$TARGET" "$OLD" || fail "Could not move the installed LoopCode aside. Nothing was changed."; fi
  if ! mv "$NEW" "$TARGET"; then
    [ -z "$OLD" ] || mv "$OLD" "$TARGET" || true
    fail "Could not put LoopCode in $DIR. Nothing was changed."
  fi
  step "installed $TARGET"

  say ""
  say "  ${GREEN}${BOLD}LoopCode $VERSION is installed.${RESET}"
  say "  ${DIM}Installed from the terminal, so macOS won't ask about an unidentified developer when it opens.${RESET}"
  say "  ${DIM}It keeps itself up to date from now on. Help: team@heksync.in${RESET}"
  say ""
  if [ "${LOOPCODE_NO_OPEN:-}" != "1" ]; then /usr/bin/open "$TARGET" || say "  Open LoopCode from $DIR."; fi
}

main "$@"
