#!/usr/bin/env bash
#
# Turn a freshly-installed macOS guest into Nucleic's GOLDEN base image.
#
#   ./provision-macos-guest.sh [/path/to/nucleic-id_ed25519.pub] [AGENT_PASSWORD]
#
# The optional 2nd arg (or $NUCLEIC_AGENT_PW) is the `agent` account password, needed ONLY to enable
# auto-login for the computer-use phase (Phase 7). Omit it to skip auto-login/computer-use setup.
#
# THIS RUNS INSIDE THE VM, not on the host. It's the second half of the "install + provision locally"
# base flow — scripts/build-macos-base.sh installs a clean macOS and drops you at Setup Assistant;
# after you create the `agent` account by hand, you copy THIS script (plus Nucleic's host PUBLIC key)
# into the guest and run it as `agent`. It:
#
#   1. Enables Remote Login (sshd) so Nucleic can reach the guest as agent@<ip>.
#   2. Authorizes Nucleic's host public key in ~agent/.ssh/authorized_keys.
#   3. Installs the Xcode Command Line Tools (the minimum toolchain) and documents the FULL-Xcode
#      path via `xcodes` (full Xcode + simulators need an Apple ID / manual download).
#   4. Installs Homebrew + node, python, git, and common dev tools.
#   5. CRITICAL: appends the toolchain PATH to /etc/zshenv so NON-login, non-interactive SSH shells
#      (how the engine runs commands) can see node/npm/python/xcodebuild. .zprofile/.zshrc are NOT
#      sourced by those shells; /etc/zshenv always is.
#   6. Warms the iOS simulator.
#   7. COMPUTER USE (GUI automation): installs cliclick at /usr/local/bin, enables auto-login for the
#      agent (so a headless boot has an Aqua session), pre-grants Screen Recording + Accessibility
#      in the SYSTEM TCC.db so screencapture/cliclick work unattended (needs SIP off — see Phase 7),
#      and installs the NATIVE IN-GUEST AGENT (NucleicVMAgent.app + its LaunchAgent + TCC grants)
#      when the app is staged alongside this script — see docs/MACOS_VM_NATIVE_AGENT.md.
#   8. Shuts the guest DOWN cleanly so the base can be cloned.
#
# Run it as the `agent` user (it will `sudo` for the system-level bits). It's idempotent — safe to
# re-run. Some steps (Remote Login, first-launch) may prompt or need Full Disk Access for Terminal.

set -euo pipefail

# The account Nucleic logs in as. Must match MacVMSettings.defaultSSHUser; also the account you
# created in Setup Assistant. Provision under this user even if invoked via sudo.
AGENT_USER="agent"
AGENT_HOME="$(/usr/bin/dscl . -read "/Users/$AGENT_USER" NFSHomeDirectory 2>/dev/null | awk '{print $2}')"
AGENT_HOME="${AGENT_HOME:-/Users/$AGENT_USER}"

# Nucleic's host PUBLIC key to authorize. Prefer the script argument; else a copy dropped next to the
# script or into the agent home; else the shared workspace (virtiofs) if the host staged it there.
PUBKEY_ARG="${1:-}"
SHARED_WORKSPACE="/Volumes/My Shared Files/workspace"
# The host-provisioning share (STATUS/STARTED/provision.log live here). Used to signal the host — e.g.
# to auto-confirm the Command Line Tools install-on-demand dialog via HID (see Phase 3). Only present
# during a host-driven base build; harmless when absent.
PROVISION_SHARE="/Volumes/My Shared Files/nucleic-provision"

echo "▸ Provisioning this macOS guest as Nucleic's golden base (user: $AGENT_USER, home: $AGENT_HOME)"

# ── Phase 0: sanity ──────────────────────────────────────────────────────────────────────────────
if [ "$(uname -s)" != "Darwin" ]; then
  echo "✗ this script runs INSIDE a macOS guest, not on Linux/other." >&2
  exit 1
fi
if ! /usr/bin/dscl . -read "/Users/$AGENT_USER" >/dev/null 2>&1; then
  echo "✗ account '$AGENT_USER' does not exist. Create it in Setup Assistant first (short name: $AGENT_USER)." >&2
  exit 1
fi
# We sudo repeatedly. Under a host-driven base build the bootstrap has already primed passwordless
# sudo (a NOPASSWD sudoers drop-in), so `sudo -v` returns without a prompt; a hands-on run may be
# asked for the password once here.
if sudo -n true 2>/dev/null; then
  echo "▸ sudo is already primed (passwordless) — continuing without a prompt."
else
  echo "▸ This uses sudo for system settings — you may be asked for the $AGENT_USER password once."
  sudo -v
fi

# ── Phases 1–2: legacy SSH access (OFF by default — the control plane is vsock) ───────────────────
# Nucleic reaches the guest entirely over vsock now (exec + computer-use via the native in-guest
# agent installed in Phase 7d); Remote Login and an authorized SSH key are no longer part of the
# control plane and are NOT enabled. Set NUCLEIC_PROVISION_ENABLE_SSH=1 to opt back into the old
# SSH-over-NAT surface (e.g. for manual debugging) — then a public key may be passed as $1.
if [ "${NUCLEIC_PROVISION_ENABLE_SSH:-0}" = "1" ]; then
  echo "▸ [1/8] Enabling Remote Login (sshd) [opt-in] …"
  if sudo systemsetup -setremotelogin on 2>/dev/null; then
    echo "  ✓ Remote Login on."
  else
    echo "  ⚠ Could not toggle Remote Login via systemsetup (needs Full Disk Access for Terminal)." >&2
  fi

  echo "▸ [2/8] Installing Nucleic's host public key into $AGENT_HOME/.ssh/authorized_keys …"
  PUBKEY_SRC=""
  if [ -n "$PUBKEY_ARG" ] && [ -f "$PUBKEY_ARG" ]; then
    PUBKEY_SRC="$PUBKEY_ARG"
  else
    for cand in \
      "$(cd "$(dirname "$0")" && pwd)/id_ed25519.pub" \
      "$AGENT_HOME/id_ed25519.pub" \
      "$SHARED_WORKSPACE/id_ed25519.pub"; do
      [ -f "$cand" ] && { PUBKEY_SRC="$cand"; break; }
    done
  fi
  if [ -n "$PUBKEY_SRC" ] && grep -Eq '^(ssh-ed25519|ssh-rsa|ecdsa-|sk-) ' "$PUBKEY_SRC"; then
    SSH_DIR="$AGENT_HOME/.ssh"; AUTH_KEYS="$SSH_DIR/authorized_keys"
    mkdir -p "$SSH_DIR"; touch "$AUTH_KEYS"
    KEY_LINE="$(tr -d '\r' < "$PUBKEY_SRC")"
    grep -qxF "$KEY_LINE" "$AUTH_KEYS" || printf '%s\n' "$KEY_LINE" >> "$AUTH_KEYS"
    chmod 700 "$SSH_DIR"; chmod 600 "$AUTH_KEYS"; sudo chown -R "$AGENT_USER":staff "$SSH_DIR"
    echo "  ✓ key authorized."
  else
    echo "  ⚠ NUCLEIC_PROVISION_ENABLE_SSH=1 but no valid public key found — skipping key authorization." >&2
  fi
else
  echo "▸ [1–2/8] Skipping Remote Login + SSH key (vsock-only base; set NUCLEIC_PROVISION_ENABLE_SSH=1 to opt in)."
fi

# ── Phase 3: Xcode Command Line Tools (the minimum toolchain) ────────────────────────────────────
# The CLT give us clang, git, and (crucially) `xcodebuild`/`xcrun`/`simctl`. FULL Xcode + iOS
# simulators are a separate, larger, Apple-ID-gated download — automated below via `xcodes` but left
# for the operator to trigger, since it needs credentials.
echo "▸ [3/8] Installing the Xcode Command Line Tools …"
# CLT are "present" only when the developer dir is set AND clang is actually resolvable. `xcode-select
# -p` alone can succeed against a half-registered dir, so check `xcrun --find clang` too.
clt_present() { /usr/bin/xcode-select -p >/dev/null 2>&1 && /usr/bin/xcrun --find clang >/dev/null 2>&1; }
# macOS ships no `timeout(1)`, and `softwareupdate -l` routinely WEDGES at "Finding available software"
# on beta / VM guests. Bound any command with perl's alarm (perl is always present) so one hung network
# call can't stall the whole build. `alarm` survives the exec, so the wrapped process is killed on time.
run_timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; }

if clt_present; then
  echo "  ✓ CLT already installed at $(xcode-select -p)."
else
  # Install HEADLESSLY via softwareupdate — never a bare `xcode-select --install`, which pops a GUI
  # dialog that would block this unattended build. The on-demand trigger file makes the CLT package
  # appear in `softwareupdate -l`; it MUST exist *before* we query.
  TRIGGER=/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
  sudo touch "$TRIGGER" 2>/dev/null || touch "$TRIGGER" 2>/dev/null || true

  # Find the CLT label — each query bounded so a wedged softwareupdate can't hang us. Match the
  # "* Label: Command Line Tools …" line specifically (NOT the "Title:" line), newest version last.
  CLT_LABEL=""
  for attempt in 1 2 3; do
    CLT_LABEL="$(run_timeout 180 softwareupdate -l 2>/dev/null \
      | grep -E 'Label:.*Command Line Tools' \
      | sed -E 's/^.*Label: *//' | sort -V | tail -1)"
    [ -n "$CLT_LABEL" ] && break
    echo "  (softwareupdate hasn't listed the CLT package yet — attempt $attempt) …"
  done

  if [ -n "$CLT_LABEL" ]; then
    echo "  installing via softwareupdate: $CLT_LABEL"
    run_timeout 1800 sudo softwareupdate -i "$CLT_LABEL" --verbose || true
  else
    # softwareupdate can't enumerate the CLT (the norm on betas — it's not in the SU catalog), so the
    # on-demand dialog is the only installer. Signal the host to auto-confirm that dialog via HID
    # (it presses Return → the default "Install"), then trigger it. The host stops when we clear the
    # sentinel below (once the CLT are present).
    echo "  ⚠ softwareupdate couldn't enumerate the CLT; using the on-demand installer (host confirms"
    echo "    its dialog automatically) …" >&2
    : > "$PROVISION_SHARE/CLT_PROMPT" 2>/dev/null || true
    sudo xcode-select --install >/dev/null 2>&1 || xcode-select --install >/dev/null 2>&1 || true
  fi
  sudo rm -f "$TRIGGER" 2>/dev/null || rm -f "$TRIGGER" 2>/dev/null || true

  # Block (bounded, ≤20 min) until the tools actually land, BEFORE anything invokes xcodebuild/xcrun —
  # calling those while the CLT are absent is what pops the "command line developer tools" GUI prompt.
  for i in $(seq 1 80); do
    clt_present && { echo "  ✓ CLT installed at $(xcode-select -p)."; break; }
    sleep 15
  done
  # Tell the host to stop confirming the (now-handled) CLT dialog.
  rm -f "$PROVISION_SHARE/CLT_PROMPT" 2>/dev/null || true
fi
clt_present || echo "  ⚠ CLT still absent after Phase 3 — downstream steps that need them are guarded/skipped." >&2

# Accept license + run first-launch ONLY once the CLT are present — invoking `xcodebuild` without them
# triggers the interactive "install the command line developer tools" dialog that stalls the build.
# (With CLT-only and no full Xcode these just no-op on stderr, which we swallow.)
if clt_present; then
  sudo xcodebuild -runFirstLaunch 2>/dev/null || true
  sudo xcodebuild -license accept 2>/dev/null || true
else
  echo "  ⚠ CLT still not present after waiting — skipping xcodebuild first-launch to avoid a GUI prompt." >&2
fi

# ── Phase 4: Homebrew + dev tools ────────────────────────────────────────────────────────────────
echo "▸ [4/8] Installing Homebrew + dev tools …"
# Apple silicon Homebrew lives in /opt/homebrew. Install non-interactively if absent.
if [ -x /opt/homebrew/bin/brew ]; then
  BREW=/opt/homebrew/bin/brew
elif [ -x /usr/local/bin/brew ]; then
  BREW=/usr/local/bin/brew
else
  echo "  installing Homebrew (non-interactive) …"
  NONINTERACTIVE=1 /bin/bash -c \
    "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  BREW=/opt/homebrew/bin/brew
  [ -x "$BREW" ] || BREW=/usr/local/bin/brew
fi
eval "$("$BREW" shellenv)"

echo "  installing packages via brew …"
# node → node/npm/npx; python → python3/pip3; git + common CLI dev tools. `brew install` is idempotent.
"$BREW" install node python git jq ripgrep coreutils || true
# OPTIONAL tooling for the FULL Xcode path (documented below): `xcodes` + `aria2` for fast, resumable
# downloads. Purely best-effort — `xcodes` has no prebuilt bottle on beta macOS, so Homebrew would try
# to build it from source and fail (harmless `make` error); we don't need it for the base to work.
# Keep it fully quiet so a build-from-source failure never looks like a provisioning error.
if "$BREW" install --quiet aria2 xcodesorg/made/xcodes >/dev/null 2>&1; then
  echo "  ✓ installed xcodes + aria2 (optional full-Xcode helpers)."
else
  echo "  (skipped xcodes — optional, only for the manual full-Xcode install; no beta bottle. aria2 …)"
  "$BREW" install --quiet aria2 >/dev/null 2>&1 || true
fi

cat <<'EOF'
  ── FULL Xcode + iOS simulators (optional, needs an Apple ID) ─────────────────────────────────
  The CLT above are the MINIMUM. To build/run iOS apps in this guest you need full Xcode + a
  simulator runtime. That download is Apple-ID-gated, so it can't be fully unattended here. With
  `xcodes` + `aria2` now installed, run (inside the guest, as agent):
        xcodes install --latest          # prompts for your Apple ID; installs & selects Xcode
        sudo xcodebuild -runFirstLaunch  # installs bundled components
        xcodes runtimes --include-unreleased | grep iOS
        xcodes runtimes install "iOS 18.0"   # or whatever simulator runtime you need
  ──────────────────────────────────────────────────────────────────────────────────────────────
EOF

# ── Phase 5: CRITICAL — toolchain PATH into /etc/zshenv ──────────────────────────────────────────
# Nucleic runs commands over SSH via the guest's default shell (zsh) in NON-login, non-interactive
# mode. Those shells source ONLY /etc/zshenv (not /etc/zprofile, ~/.zprofile, or ~/.zshrc), so PATH
# additions MUST live in /etc/zshenv or the engine won't find node/npm/python/xcodebuild. This is the
# single most load-bearing step in provisioning — get it wrong and remote builds fail with "command
# not found" despite the tools being installed.
echo "▸ [5/8] Appending toolchain PATH to /etc/zshenv (for non-login SSH shells) …"
BREW_PREFIX="$("$BREW" --prefix)"                       # /opt/homebrew (arm64) or /usr/local
ZSHENV=/etc/zshenv
MARKER="# >>> nucleic macvm toolchain PATH >>>"
END_MARKER="# <<< nucleic macvm toolchain PATH <<<"
# Idempotent: strip any existing Nucleic block first, then append a fresh one — so re-runs never
# duplicate the block or leave a stale PATH. awk matches the markers as FIXED strings (index()),
# avoiding all the regex-escaping hazards of doing this with sed.
if [ -f "$ZSHENV" ] && grep -qF "$MARKER" "$ZSHENV"; then
  echo "  ✓ /etc/zshenv already has the Nucleic PATH block (refreshing it)."
  TMP_ZSHENV="$(mktemp)"
  awk -v start="$MARKER" -v end="$END_MARKER" '
    index($0, start) { skip = 1 }
    !skip           { print }
    index($0, end)  { skip = 0 }
  ' "$ZSHENV" > "$TMP_ZSHENV"
  sudo cp "$TMP_ZSHENV" "$ZSHENV"
  rm -f "$TMP_ZSHENV"
fi
sudo tee -a "$ZSHENV" >/dev/null <<EOF
$MARKER
# Added by scripts/provision-macos-guest.sh so Nucleic's non-login SSH zsh shells find the toolchain.
export PATH="$BREW_PREFIX/bin:$BREW_PREFIX/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:\$PATH"
$END_MARKER
EOF
echo "  ✓ PATH written to $ZSHENV (prefix: $BREW_PREFIX)."

# ── Phase 5½: disable sleep / display-blanking (the guest must never nod off) ─────────────────────
# A session clone is a headless, auto-login worker that the host drives and SCREENSHOTS over vsock.
# With Apple's default energy-saver timers, after ~10 idle minutes macOS blanks the (virtual) display
# and can idle-sleep the whole guest — so the host's captures come back BLACK and computer-use no-ops,
# exactly the "monitors go black / VM seems asleep" symptom. The Linux base already pins this off via
# dconf (idle-delay=0, sleep-inactive-*-type='nothing', screensaver off); this is the macOS parallel,
# baked into the golden base so every clone is born awake.
#
# pmset writes /Library/Preferences/SystemConfiguration/com.apple.PowerManagement.plist (persists into
# the base image, hence into every clone). `-a` = all power sources; a VM has no real battery, but -a
# covers whatever source VZ reports. Some keys (e.g. powernap) may be rejected on a VM — tolerated.
echo "▸ [5½/8] Disabling display/system sleep so headless captures never go black …"
sudo pmset -a displaysleep 0 sleep 0 disksleep 0 2>/dev/null || \
  echo "  ⚠ 'pmset -a displaysleep 0 sleep 0 disksleep 0' reported an error (continuing)." >&2
# powernap is best-effort — not all VM builds accept it; never fail provisioning over it.
sudo pmset -a powernap 0 2>/dev/null || true
# The SCREENSAVER blanks the screen independently of display-sleep, so pin its idle timer to 0 (off)
# for the auto-login `agent` (ByHost pref). This script runs AS agent, so no sudo — target agent's db.
defaults -currentHost write com.apple.screensaver idleTime 0 2>/dev/null || \
  echo "  ⚠ could not disable the screensaver idle timer for $AGENT_USER (continuing)." >&2
echo "  ✓ sleep disabled. Current pmset:"
pmset -g custom 2>/dev/null | grep -Ei 'displaysleep|[^k] sleep|disksleep|powernap' | sed 's/^/      /' || true

# ── Phase 5¾: disable the "Tips" app (don't let it auto-launch in the clones) ─────────────────────
# macOS auto-launches the Tips app (via the com.apple.tipsd launch agent) on early logins to show its
# "what's new" window. On a headless, auto-login session clone that window just steals focus and clutters
# the screen we screenshot — so pin tipsd OFF in the golden base. `launchctl disable gui/<uid>/<svc>`
# writes a PERSISTENT override (/var/db/com.apple.xpc.launchd/disabled.<uid>.plist) that lives in the
# base image, hence in every clone. Target the `agent` uid explicitly (not `id -u`) so this is correct
# even if the provisioner is ever invoked via sudo. Best effort — never fail the base over a cosmetic tweak.
echo "▸ [5¾/8] Disabling the Tips app (com.apple.tipsd) so it never auto-launches in clones …"
TIPS_UID="$(id -u "$AGENT_USER" 2>/dev/null || id -u)"
if sudo launchctl disable "gui/$TIPS_UID/com.apple.tipsd" 2>/dev/null \
  || launchctl disable "gui/$TIPS_UID/com.apple.tipsd" 2>/dev/null; then
  echo "  ✓ tipsd disabled for uid $TIPS_UID."
else
  echo "  ⚠ could not disable com.apple.tipsd for uid $TIPS_UID (continuing)." >&2
fi

# ── Phase 6: warm the simulator + shut down clean ────────────────────────────────────────────────
# Pre-accept / warm the iOS simulator first-launch so the first real agent turn isn't slowed by it.
# Harmless (and skipped) if only the CLT are installed and no runtimes exist yet.
echo "▸ [6/8] Warming the iOS simulator (best effort) …"
xcrun simctl list >/dev/null 2>&1 && echo "  ✓ simctl responsive." || echo "  (simctl not available yet — full Xcode/runtimes not installed.)"

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7: COMPUTER USE (GUI automation) ────────────────────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# This phase makes the base drivable by Nucleic's `mac_vm_computer` MCP tool, which SSHes in and
# dispatches screen/input commands into the console user's Aqua GUI session:
#
#     launchctl asuser $(id -u) <ABSOLUTE-PATH> …
#
# using screencapture for screenshots (base64'd back), cliclick for mouse/keyboard, and `open -a` for
# apps. Three things must be true in the guest for that to work; this phase sets up all three,
# IDEMPOTENTLY:
#
#   (a) cliclick lives at the CANONICAL path /usr/local/bin/cliclick (the engine calls it there, and
#       TCC attributes Accessibility to that exact path);
#   (b) `agent` AUTO-LOGS-IN, so a headless boot has an active Aqua session (screencapture and
#       cliclick have no GUI session to talk to otherwise);
#   (c) Screen Recording + Accessibility are PRE-GRANTED in the SYSTEM TCC.db — the only unattended
#       way to do which is SIP-off + a direct sqlite3 write (see the honest caveats below).
#
# See docs/MACOS_VM.md §12 (Computer use) for the full picture and the SIP/TCC reality.
echo "▸ [7/8] Setting up computer use (cliclick + auto-login + TCC pre-grant) …"

# ── 7a: cliclick at the canonical /usr/local/bin/cliclick ─────────────────────────────────────────
# Homebrew on Apple silicon installs to /opt/homebrew/bin, but the Swift engine calls cliclick at the
# EXACT path /usr/local/bin/cliclick, and TCC must attribute Accessibility to that exact path. So we
# `brew install` it, then COPY it (NOT a symlink — a copy preserves the code signature and gives a
# real file at the TCC-granted path; a symlink's target path can confuse TCC attribution).
echo "  ▸ [7a] Installing cliclick and copying it to /usr/local/bin/cliclick …"
"$BREW" install cliclick 2>/dev/null || "$BREW" install cliclick || true
CLICLICK_SRC="$("$BREW" --prefix)/bin/cliclick"
if [ -x "$CLICLICK_SRC" ]; then
  sudo mkdir -p /usr/local/bin
  sudo cp "$CLICLICK_SRC" /usr/local/bin/cliclick
  if /usr/local/bin/cliclick p >/dev/null 2>&1; then
    echo "    ✓ /usr/local/bin/cliclick installed (from $CLICLICK_SRC) and responsive."
  else
    echo "    ⚠ /usr/local/bin/cliclick exists but 'cliclick p' failed — it may need Accessibility (7c)." >&2
  fi
else
  echo "    ✗ brew did not produce cliclick at $CLICLICK_SRC — install it by hand, then re-run." >&2
fi

# ── 7b: auto-login for agent (so a headless boot has an Aqua session) ─────────────────────────────
# screencapture and cliclick need a LIVE console GUI (Aqua) session. A headless clone that boots to
# the login window has none, so screenshots come back black and clicks no-op. Auto-login lands `agent`
# straight into the desktop on every boot.
#
#   ⚠ FileVault MUST be OFF for this to work — auto-login is fundamentally incompatible with FileVault
#     (FV requires the password at pre-boot to unlock the disk, which defeats auto-login). The base is
#     a disposable clone, not a device holding secrets, so FileVault-off is the right trade here.
#
# The password comes from $NUCLEIC_AGENT_PW, else the 2nd script arg ($2). We do NOT hardcode it.
echo "  ▸ [7b] Enabling auto-login for '$AGENT_USER' …"
AGENT_PW="${NUCLEIC_AGENT_PW:-${2:-}}"
if [ -z "$AGENT_PW" ]; then
  echo "    ⚠ No agent password provided (set \$NUCLEIC_AGENT_PW or pass it as the 2nd arg) — SKIPPING"
  echo "      auto-login. Without it, a headless boot has no Aqua session and computer use won't work."
  echo "      Re-run as:  ./provision-macos-guest.sh /path/to/id_ed25519.pub 'AGENT_PASSWORD'" >&2
else
  if sudo sysadminctl -autologin set -userName "$AGENT_USER" -password "$AGENT_PW" 2>/dev/null; then
    echo "    ✓ auto-login set for $AGENT_USER."
    sudo sysadminctl -autologin status 2>&1 | sed 's/^/      /' || true
  else
    echo "    ⚠ sysadminctl could not set auto-login (is FileVault ON? it must be OFF)." >&2
  fi
fi

# ── 7c: pre-grant TCC (Screen Recording + Accessibility) ──────────────────────────────────────────
# This is the hard part, and we're honest about it. The two grants we need:
#
#   • kTCCServiceScreenCapture  for /usr/sbin/screencapture   (so `screencapture -x` isn't black)
#   • kTCCServiceAccessibility  for /usr/local/bin/cliclick   (so cliclick can post events)
#
# Both live in the SYSTEM TCC db at /Library/Application Support/com.apple.TCC/TCC.db (NOT the per-user
# ~/Library one — Screen Recording & Accessibility are system-scoped).
#
# HONEST REALITY about unattended granting:
#   • `tccutil` CANNOT grant — it only RESETs. There is no supported CLI to *add* a TCC grant.
#   • PPPC/MDM profiles can pre-approve Accessibility, but can NEVER silently grant Screen Recording
#     (Apple deliberately forces a manual user click for kTCCServiceScreenCapture — a PPPC payload for
#     it only PRE-CONFIGURES the toggle, it does not turn it on).
#   • The ONLY unattended path is: SIP DISABLED in the guest, then a direct sqlite3 INSERT into the
#     system TCC.db. SIP protects that db from writes; with SIP on, the INSERT is denied.
#
# And `csrutil disable` is a ONE-TIME MANUAL step: it must run from recoveryOS, which has NO SSH and
# no scripting hook — a human boots the BASE VM into recoveryOS once and runs `csrutil disable`. So
# below we DETECT whether SIP is off; if it's on we print exactly what to do and SKIP gracefully
# (never failing the whole provisioner over it).
echo "  ▸ [7c] Pre-granting TCC (Screen Recording + Accessibility) in the system TCC.db …"

SIP_OFF=0
if csrutil status 2>/dev/null | grep -qiE 'disabled|System Integrity Protection status: disabled'; then
  SIP_OFF=1
fi

if [ "$SIP_OFF" -ne 1 ]; then
  echo "    ⚠ SIP is ENABLED — cannot write the system TCC.db, so computer-use TCC grants are SKIPPED."
  echo "      This is a ONE-TIME MANUAL step (recoveryOS has no SSH, so it can't be scripted):"
  echo "        1. Fully shut down this guest."
  echo "        2. Boot the BASE VM into recoveryOS (hold the power button / Options at boot in the"
  echo "           VZ window), open Terminal, and run:   csrutil disable"
  echo "        3. Reboot back into macOS and RE-RUN this provisioner — it will then grant TCC here."
  echo "      (Screen Recording in particular has NO other unattended grant path — PPPC/MDM can't do"
  echo "       it, and tccutil can only reset, never add.)" >&2
else
  echo "    ✓ SIP is disabled — writing TCC grants directly into the system TCC.db."

  TCC_DB="/Library/Application Support/com.apple.TCC/TCC.db"

  # grant_tcc <service> <abs-binary-path>: build the binary's code-signing designated requirement,
  # compile it to a csreq blob, and INSERT (or REPLACE) a fully NAMED-COLUMN row allowing that
  # service for that client path. We ALWAYS name columns because macOS keeps adding columns to
  # `access` across releases (26 added several) — a positional INSERT breaks on the next OS. The
  # csreq is OS-build-specific, so it's (re)generated here per build rather than hardcoded.
  #
  #   client_type = 1  → the client is identified by ABSOLUTE PATH (0 would be a bundle id)
  #   auth_value  = 2  → ALLOWED (0 denied, 1 unknown, 2 allowed, 3 limited)
  #   auth_reason = 4  → system set / MDM-style (a plausible provenance for a pre-grant)
  #   auth_version= 1  → current row schema version
  #   flags       = 0
  grant_tcc() {  # $1=service  $2=abs-binary-path
    local req hexreq
    # Extract the designated requirement; on some platform binaries this yields nothing usable.
    req=$(codesign -d -r- "$2" 2>&1 | sed -n 's/^designated => //p')
    if [ -n "$req" ] && echo "$req" | csreq -r- -b /tmp/csreq.bin 2>/dev/null; then
      hexreq="X'$(xxd -p /tmp/csreq.bin | tr -d '\n')'"
      echo "      • $1 ← $2  (with code requirement)"
    else
      # FALLBACK: /usr/sbin/screencapture is a platform binary whose `codesign -d -r-` can come back
      # empty/unusable. A NULL csreq still allows by PATH on many builds — TCC matches client_type=1
      # rows on the client path alone when csreq is NULL. Less strict, but it unblocks the grant.
      hexreq="NULL"
      echo "      • $1 ← $2  (csreq NULL fallback — no usable designated requirement)"
    fi
    sudo sqlite3 "$TCC_DB" \
      "INSERT OR REPLACE INTO access
       (service,client,client_type,auth_value,auth_reason,auth_version,csreq,policy_id,indirect_object_identifier_type,indirect_object_identifier,indirect_object_code_identity,flags,last_modified)
       VALUES('$1','$2',1,2,4,1,$hexreq,NULL,0,'UNUSED',NULL,0,strftime('%s','now'));"
  }

  grant_tcc kTCCServiceScreenCapture /usr/sbin/screencapture
  grant_tcc kTCCServiceAccessibility /usr/local/bin/cliclick

  # Bounce tccd so it reloads the db and the fresh grants take effect without a reboot.
  sudo killall tccd 2>/dev/null || true
  rm -f /tmp/csreq.bin 2>/dev/null || true

  echo "    ✓ TCC grants written. Current computer-use rows:"
  sudo sqlite3 "$TCC_DB" \
    "SELECT service,client,auth_value FROM access WHERE service IN ('kTCCServiceScreenCapture','kTCCServiceAccessibility');" \
    2>/dev/null | sed 's/^/      /' || true
fi

# ── 7d: the NATIVE IN-GUEST AGENT (NucleicVMAgent.app + LaunchAgent + TCC) ────────────────────────
# The preferred computer-use path (docs/MACOS_VM_NATIVE_AGENT.md): a small signed .app, run as a
# per-user LaunchAgent in the auto-login Aqua session, that the host reaches over VSOCK (no SSH) and
# that drives the guest with Apple's frameworks directly — AXUIElement (semantic observe/control,
# and the ONLY observation channel on a macOS 26 guest whose framebuffer captures come back blank),
# ScreenCaptureKit (capture), CGEvent (raw input). Entirely OPTIONAL: when the app isn't staged, the
# base still works via the SSH + cliclick path above (Nucleic probes and falls back automatically).
#
# Build it on the HOST with scripts/build-vm-agent.sh and stage dist/* (the .app + the LaunchAgent
# plist) next to this script, in the agent home, or in the shared workspace.
echo "  ▸ [7d] Installing the native in-guest agent (NucleicVMAgent.app) …"
AGENT_APP_DEST="/Applications/NucleicVMAgent.app"
AGENT_LA_PLIST="/Library/LaunchAgents/xyz.blakeslee.nucleic.vmagent.plist"
AGENT_BUNDLE_ID="xyz.blakeslee.nucleic.vmagent"
AGENT_APP_SRC=""
for cand in \
  "$(cd "$(dirname "$0")" && pwd)/NucleicVMAgent.app" \
  "$AGENT_HOME/NucleicVMAgent.app" \
  "$SHARED_WORKSPACE/NucleicVMAgent.app"; do
  [ -d "$cand" ] && { AGENT_APP_SRC="$cand"; break; }
done

if [ -z "$AGENT_APP_SRC" ]; then
  echo "    ⚠ NucleicVMAgent.app not staged — SKIPPING the native agent. Computer use will fall back"
  echo "      to the SSH + cliclick path (no ax_* semantic actions, and NO computer-use at all on a"
  echo "      macOS 26 guest whose screenshots are blank). To add it: on the host run"
  echo "      scripts/build-vm-agent.sh, stage its dist/* next to this script, and re-run."
else
  # Install the app (a fresh copy every run — the bundle is tiny) and strip any quarantine xattr.
  sudo rm -rf "$AGENT_APP_DEST"
  sudo cp -R "$AGENT_APP_SRC" "$AGENT_APP_DEST"
  sudo xattr -dr com.apple.quarantine "$AGENT_APP_DEST" 2>/dev/null || true
  echo "    ✓ installed $AGENT_APP_DEST (from $AGENT_APP_SRC)."

  # LaunchAgent: Aqua-session-only, RunAtLoad + KeepAlive (staged copy preferred, generated else).
  AGENT_LA_SRC="$(dirname "$AGENT_APP_SRC")/xyz.blakeslee.nucleic.vmagent.plist"
  if [ -f "$AGENT_LA_SRC" ]; then
    sudo cp "$AGENT_LA_SRC" "$AGENT_LA_PLIST"
  else
    sudo tee "$AGENT_LA_PLIST" >/dev/null <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>xyz.blakeslee.nucleic.vmagent</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Applications/NucleicVMAgent.app/Contents/MacOS/NucleicVMAgent</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>LimitLoadToSessionType</key>
    <string>Aqua</string>
    <key>AssociatedBundleIdentifiers</key>
    <string>xyz.blakeslee.nucleic.vmagent</string>
    <key>StandardOutPath</key>
    <string>/tmp/nucleic-vmagent.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/nucleic-vmagent.log</string>
</dict>
</plist>
PLIST
  fi
  sudo chown root:wheel "$AGENT_LA_PLIST"
  sudo chmod 644 "$AGENT_LA_PLIST"
  echo "    ✓ LaunchAgent at $AGENT_LA_PLIST (Aqua session, RunAtLoad, KeepAlive)."

  # TCC: the agent needs THREE grants (docs/MACOS_VM_NATIVE_AGENT.md §11), each a separate service
  # row, keyed by BUNDLE ID (client_type=0) + the app's designated-requirement csreq blob:
  #   • kTCCServiceAccessibility — AXUIElement observe/control (AXIsProcessTrusted)
  #   • kTCCServicePostEvent     — CGEvent input synthesis (a DISTINCT row from Accessibility)
  #   • kTCCServiceScreenCapture — ScreenCaptureKit
  # Same SIP-off reality as 7c: with SIP on, the system TCC.db is not writable — skip gracefully.
  if [ "$SIP_OFF" -ne 1 ]; then
    echo "    ⚠ SIP is ENABLED — the agent's TCC grants are SKIPPED (see 7c for the one-time"
    echo "      recoveryOS 'csrutil disable' step, then re-run this provisioner)." >&2
  else
    TCC_DB="/Library/Application Support/com.apple.TCC/TCC.db"
    AGENT_HEXREQ="NULL"
    AGENT_REQ=$(codesign -d -r- "$AGENT_APP_DEST" 2>&1 | sed -n 's/^designated => //p')
    if [ -n "$AGENT_REQ" ] && echo "$AGENT_REQ" | csreq -r- -b /tmp/csreq-vmagent.bin 2>/dev/null; then
      AGENT_HEXREQ="X'$(xxd -p /tmp/csreq-vmagent.bin | tr -d '\n')'"
    else
      echo "      ⚠ no usable designated requirement (ad-hoc signed?) — writing csreq NULL rows."
      echo "        NOTE an ad-hoc identity changes every rebuild; sign with a stable identity" >&2
      echo "        (scripts/build-vm-agent.sh + NUCLEIC_VMAGENT_SIGN_IDENTITY) for durable grants." >&2
    fi
    for svc in kTCCServiceAccessibility kTCCServicePostEvent kTCCServiceScreenCapture; do
      echo "      • $svc ← $AGENT_BUNDLE_ID"
      sudo sqlite3 "$TCC_DB" \
        "INSERT OR REPLACE INTO access
         (service,client,client_type,auth_value,auth_reason,auth_version,csreq,policy_id,indirect_object_identifier_type,indirect_object_identifier,indirect_object_code_identity,flags,last_modified)
         VALUES('$svc','$AGENT_BUNDLE_ID',0,2,4,1,$AGENT_HEXREQ,NULL,0,'UNUSED',NULL,0,strftime('%s','now'));"
    done
    rm -f /tmp/csreq-vmagent.bin 2>/dev/null || true
    # tccd must reload the db, and the agent must RESTART to pick the grants up (Screen Recording
    # in particular is only read at process start).
    sudo killall tccd 2>/dev/null || true
    AGENT_UID="$(id -u "$AGENT_USER")"
    sudo launchctl bootout "gui/$AGENT_UID/$AGENT_BUNDLE_ID" 2>/dev/null || true
    sudo launchctl bootstrap "gui/$AGENT_UID" "$AGENT_LA_PLIST" 2>/dev/null || \
      echo "      (LaunchAgent will start on next login/boot — no live Aqua session to bootstrap into.)"
    echo "    ✓ agent TCC grants written; agent (re)started. Log: /tmp/nucleic-vmagent.log"
  fi
fi

echo ""
echo "  ── Computer use needs ALL THREE of the above (cliclick + auto-login + TCC) ─────────────────"
echo "  If screenshots come back BLACK or clicks NO-OP after this base is live, check, in the guest:"
echo "    • SIP is disabled:            csrutil status"
echo "    • TCC rows exist & allowed:   sudo sqlite3 \"/Library/Application Support/com.apple.TCC/TCC.db\" \\"
echo "                                    \"SELECT service,client,auth_value FROM access;\""
echo "    • agent is auto-logged-in:    an Aqua/desktop session is active (not the login window)"
echo "    • native agent alive (7d):    cat /tmp/nucleic-vmagent.log — should show 'starting on"
echo "                                  vsock port 2035' and readiness true×3 (if the app was staged)"
echo "  ──────────────────────────────────────────────────────────────────────────────────────────────"

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7½: user-provided apps → /Applications ────────────────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# Nucleic stages any .app bundles the operator picked in Settings ("Base image" → "Included apps")
# into a `user-apps/` subdir of the provisioning share; bake each into the base's /Applications so
# every session clone has them. Best effort + idempotent: no share/subdir (a hands-on run) or a bad
# bundle just logs and moves on. Quarantine is stripped so the copied apps launch without Gatekeeper
# prompting inside the (disposable, NAT-isolated) guest.
USER_APPS_DIR="$PROVISION_SHARE/user-apps"
if [ -d "$USER_APPS_DIR" ]; then
  echo "▸ Installing user-provided apps into /Applications …"
  shopt -s nullglob
  for app in "$USER_APPS_DIR"/*.app; do
    name="$(basename "$app")"
    dest="/Applications/$name"
    if sudo rm -rf "$dest" && sudo cp -R "$app" "$dest"; then
      sudo xattr -dr com.apple.quarantine "$dest" 2>/dev/null || true
      echo "  ✓ installed $dest"
    else
      echo "  ⚠ failed to install $name — skipping." >&2
    fi
  done
  shopt -u nullglob
fi

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7¾: common packages (operator-selected) → install ─────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# When the operator opts into any "Common packages" in Settings, the host stages a generated
# `install-packages.sh` (from Sources/NucleicCore/MacVM/MacVMPackage.swift) into the provisioning
# share; each package fetches + installs itself into the guest (e.g. the latest Google Chrome, over
# the guest's NAT network — provisioning has network up by this point). Best effort + idempotent: no
# staged installer (a hands-on run, or nothing selected) just skips this. Runs as `agent` (admin, so
# /Applications is writable) — the same script the after-the-fact vsock path runs into a live guest.
PKG_INSTALLER="$PROVISION_SHARE/install-packages.sh"
[ -f "$PKG_INSTALLER" ] || PKG_INSTALLER="$(cd "$(dirname "$0")" && pwd)/install-packages.sh"
if [ -f "$PKG_INSTALLER" ]; then
  echo "▸ Installing operator-selected common packages …"
  /bin/bash "$PKG_INSTALLER" || echo "  ⚠ common-package install reported errors (continuing)." >&2
fi

echo ""
echo "────────────────────────────────────────────────────────────────────────────────────────────────"
echo "✓ Provisioning complete. This guest is now Nucleic's golden base."
echo "  Shutting DOWN cleanly so the host can clone the base bundle (a running/dirty guest can't be)."
echo "  After it powers off, verify from the host with the default macvm-spike round-trip."
echo "────────────────────────────────────────────────────────────────────────────────────────────────"
# When Nucleic drives this over SSH it wants to read back the provisioning result (SIP/TCC/agent
# state) BEFORE the guest powers off, so it sets NUCLEIC_PROVISION_NO_SHUTDOWN=1 and owns the
# shutdown itself. A hands-on run leaves it unset and shuts down here as before.
if [ "${NUCLEIC_PROVISION_NO_SHUTDOWN:-0}" = "1" ]; then
  echo "▸ [8/8] Skipping shutdown (NUCLEIC_PROVISION_NO_SHUTDOWN=1 — the caller will power off)."
  exit 0
fi
echo "▸ [8/8] Shutting down …"
# `shutdown -h now` requires root; run last so nothing after it matters.
sudo shutdown -h now
