#!/usr/bin/env bash
# mac-swap.sh — one-shot, on-demand bundle swap for the in-app updater.
#
# Invoked DETACHED by the app (MacUpdateChecker.install()) only after the app has
# already downloaded + sha256-verified + codesign/Team-ID-verified the new build.
# This script does NOT download or fetch anything — it waits for the running app
# to quit, RE-verifies the staged bundle (defense-in-depth, see below), then
# atomically swaps the bundle and relaunches.
#
# It REPLACES the old launchd-driven mac-updater.sh (4h silent auto-updater),
# which Hanson retired on 2026-06-27 in favor of a visible, user-triggered flow.
#
# The app copies this script OUT of the bundle to a stable path before running it
# (so swapping /Applications/Hanson OS.app can't pull the script out from under
# bash). curl/URLSession downloads carry no com.apple.quarantine, so the
# Developer-ID-signed swap relaunches clean with no Gatekeeper prompt.
#
# Args: $1 = PID of the app to wait for
#       $2 = staged (already-verified) .app to install
#       $3 = destination .app path (/Applications/Hanson OS.app)
set -uo pipefail

PID="${1:?missing pid}"
STAGED="${2:?missing staged app}"
DEST="${3:?missing dest path}"

LOG="$HOME/Library/Logs/hansonos-updater.log"
mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') swap: $*" >> "$LOG" 2>/dev/null; }

log "begin pid=$PID staged=$STAGED dest=$DEST"

# ── wait for the app to actually exit (max ~30s) so the running executable is no
#    longer mapped from the bundle we're about to replace ──
for _ in $(seq 1 150); do
  kill -0 "$PID" 2>/dev/null || break
  sleep 0.2
done
if kill -0 "$PID" 2>/dev/null; then
  log "app pid=$PID still alive after 30s — aborting (no swap)"
  open "$DEST" 2>/dev/null || true   # at least bring it back
  exit 1
fi

[ -d "$STAGED" ] || { log "staged app missing — abort"; exit 1; }

# ── re-verify the staged bundle IMMEDIATELY before swapping ──
# Closes the TOCTOU window between the app's verify and this swap: the staging
# dir under ~/Library/Application Support is user-writable, so a local process
# could swap the staged .app after the app verified it. Re-check signature + our
# Team ID adjacent to the ditto so what we install is exactly what we trust.
TEAM_ID="KQZF9JQ9SK"
if ! codesign --verify --deep --strict "$STAGED" 2>>"$LOG"; then
  log "staged codesign --verify FAILED — abort (no swap)"
  open "$DEST" 2>/dev/null || true
  exit 1
fi
STAGED_TEAM="$(codesign -dvv "$STAGED" 2>&1 | sed -n 's/^TeamIdentifier=//p')"
if [ "$STAGED_TEAM" != "$TEAM_ID" ]; then
  log "staged TeamIdentifier '$STAGED_TEAM' != $TEAM_ID — abort (no swap)"
  open "$DEST" 2>/dev/null || true
  exit 1
fi

BAK="${DEST}.update-bak"
rm -rf "$BAK"

# ── back up the current bundle, then ditto the staged one into place ──
if [ -d "$DEST" ]; then
  mv "$DEST" "$BAK" || { log "backup mv failed — abort"; open "$DEST" 2>/dev/null || true; exit 1; }
fi

if ditto "$STAGED" "$DEST"; then
  log "swapped OK → $DEST"
  rm -rf "$BAK"
  rm -rf "$STAGED" "${STAGED%/*}"   # staged .app + its extracted-<build> dir
  open "$DEST" || log "relaunch open failed"
else
  log "ditto failed — rolling back from $BAK"
  rm -rf "$DEST"
  [ -d "$BAK" ] && mv "$BAK" "$DEST"
  open "$DEST" 2>/dev/null || true
  exit 1
fi

log "done"
