#!/bin/bash
# install.sh — Full setup for PRE (Personal Reasoning Engine)
#
# This script handles everything from Ollama to ready-to-run:
#   0. Checks system requirements (macOS, arch, RAM, eGPU/VRAM, disk)
#   1. Installs/verifies Ollama
#   1b. Configures Ollama environment (FA, keep-alive, parallelism)
#   2. Pulls the base model (q8_0 ~28GB or q4_K_M ~15GB, auto-selected)
#   2b. Pulls embedding model (nomic-embed-text for experience ledger + RAG)
#   3. Creates optimized custom model (pre-gemma4) from Modelfile
#   4. Installs Xcode CLI tools if needed
#   5. Compiles PRE CLI binary
#   6. Sets up Web GUI (npm install, terminal-notifier)
#   6b. Configures MCP integration (Claude, Codex, Antigravity)
#   7. Installs pre-launch command to ~/.local/bin
#   8. Sets up ~/.pre/ directories (sessions, memory, rag, workflows, triggers)
#   8b. Optional: ComfyUI image generation setup
#   8c. Optional: cliclick for Computer Use (desktop automation)
#   8d. Optional: Whisper + FFmpeg for voice interface
#   9. Pre-warms model into GPU memory
#   10. Optional: auto-start at login (LaunchAgent)
#
# Run time: 5-20 minutes depending on internet speed.
# Disk space required: ~28GB for model + negligible for binaries.
#
# Usage:
#   ./install.sh           Interactive install (prompts for optional steps)
#   ./install.sh --yes     Non-interactive: accept all defaults, skip optional steps
#   ./install.sh -y        Same as --yes
#   ./install.sh --q4      Use q4_K_M quantization (~15GB, lighter on RAM)
#   ./install.sh --q8      Use q8_0 quantization (~28GB, near-lossless)
#   ./install.sh --llamacpp  Use llama.cpp server as inference backend (instead of Ollama)

set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'

REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
ENGINE_DIR="$REPO_DIR/engine"
# BASE_MODEL is set dynamically in Step 0 based on VRAM/RAM detection
CUSTOM_MODEL="pre-gemma4"

# --- Flag parsing ---
AUTO_YES=false
FORCE_Q4=false
FORCE_Q8=false
FORCE_LLAMACPP=false
for arg in "$@"; do
    case "$arg" in
        --yes|-y) AUTO_YES=true ;;
        --q4) FORCE_Q4=true ;;
        --q8) FORCE_Q8=true ;;
        --llamacpp) FORCE_LLAMACPP=true ;;
    esac
done

# In --yes mode, prompts auto-accept Y/n defaults and skip y/N defaults
ask_yn() {
    local prompt="$1" default="$2"
    if [ "$AUTO_YES" = true ]; then
        [ "$default" = "Y" ] && return 0 || return 1
    fi
    read -p "$prompt" -n 1 -r
    echo ""
    if [ "$default" = "Y" ]; then
        [[ ! $REPLY =~ ^[Nn]$ ]]
    else
        [[ $REPLY =~ ^[Yy]$ ]]
    fi
}

step() { echo -e "\n${BOLD}${CYAN}=== $1 ===${RESET}\n"; }
ok()   { echo -e "${GREEN}$1${RESET}"; }
warn() { echo -e "${YELLOW}$1${RESET}"; }
fail() { echo -e "${RED}$1${RESET}"; exit 1; }

# Error trap — show which step failed
trap 'echo -e "\n${RED}Installation failed at line $LINENO.${RESET}\n${DIM}Re-run the script to retry — it will pick up where it left off.${RESET}"' ERR

# ============================================================================
# Step 0: System requirements
# ============================================================================
step "Checking system requirements"

# macOS version
OS=$(uname -s)
if [ "$OS" != "Darwin" ]; then
    fail "PRE requires macOS. Detected: $OS"
fi
MACOS_VER=$(sw_vers -productVersion)
MACOS_MAJOR=$(echo "$MACOS_VER" | cut -d. -f1)
ok "  macOS: $MACOS_VER"

# Architecture detection
ARCH=$(uname -m)
IS_APPLE_SILICON=false
IS_INTEL=false
if [ "$ARCH" = "arm64" ]; then
    IS_APPLE_SILICON=true
    ok "  Architecture: Apple Silicon ($ARCH)"
elif [ "$ARCH" = "x86_64" ]; then
    IS_INTEL=true
    ok "  Architecture: Intel ($ARCH)"
else
    fail "Unsupported architecture: $ARCH"
fi

# ── eGPU detection (TinyGPU: NVIDIA/AMD via Thunderbolt/USB4) ──
# TinyGPU is Tiny Corp's Apple-signed driver enabling NVIDIA Ampere+ and
# AMD RDNA3+ eGPUs for compute on macOS. Ollama 0.4+ auto-detects it.
EGPU_DETECTED=false
EGPU_VENDOR=""
EGPU_NAME=""
EGPU_VRAM_GB=0
GPU_BACKEND="metal"   # metal (Apple Silicon) or cuda (NVIDIA eGPU)

# Method 1: Check if TinyGPU driver extension is loaded
TINYGPU_LOADED=false
if systemextensionsctl list 2>/dev/null | grep -qi "tinygpu"; then
    TINYGPU_LOADED=true
fi

# Method 2: Check for TinyGPU.app
TINYGPU_APP=false
if [ -d "/Applications/TinyGPU.app" ] || [ -d "$HOME/Applications/TinyGPU.app" ]; then
    TINYGPU_APP=true
fi

# Method 3: Check system_profiler for Thunderbolt GPU devices
THUNDERBOLT_GPU=""
THUNDERBOLT_GPU=$(system_profiler SPThunderboltDataType 2>/dev/null | grep -i "NVIDIA\|GeForce\|RTX\|Radeon\|RX " | head -1 | sed 's/.*: //' || true)

if [ "$TINYGPU_LOADED" = true ] || [ "$TINYGPU_APP" = true ] || [ -n "$THUNDERBOLT_GPU" ]; then
    EGPU_DETECTED=true

    # Determine vendor from Thunderbolt info or Docker nvidia-smi
    if [ -n "$THUNDERBOLT_GPU" ]; then
        EGPU_NAME="$THUNDERBOLT_GPU"
        if echo "$THUNDERBOLT_GPU" | grep -qi "NVIDIA\|GeForce\|RTX"; then
            EGPU_VENDOR="nvidia"
        elif echo "$THUNDERBOLT_GPU" | grep -qi "AMD\|Radeon\|RX"; then
            EGPU_VENDOR="amd"
        fi
    fi

    # NVIDIA VRAM detection via Docker nvidia-smi (CUDA path requires Docker)
    if [ "$EGPU_VENDOR" = "nvidia" ] || [ "$EGPU_VENDOR" = "" ]; then
        if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then
            NVOUT=$(docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 \
                nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null || true)
            if [ -n "$NVOUT" ]; then
                EGPU_VENDOR="nvidia"
                [ -z "$EGPU_NAME" ] && EGPU_NAME=$(echo "$NVOUT" | cut -d',' -f1 | xargs)
                VRAM_MIB=$(echo "$NVOUT" | grep -oE '[0-9]+' | tail -1)
                if [ -n "$VRAM_MIB" ] && [ "$VRAM_MIB" -gt 0 ] 2>/dev/null; then
                    EGPU_VRAM_GB=$((VRAM_MIB / 1024))
                fi
            fi
        fi
    fi

    if [ "$EGPU_VENDOR" = "nvidia" ]; then
        GPU_BACKEND="cuda"
    fi

    ok "  eGPU: ${EGPU_NAME:-detected} (${EGPU_VENDOR:-unknown}, TinyGPU)"
    if [ "$EGPU_VRAM_GB" -gt 0 ]; then
        ok "  eGPU VRAM: ${EGPU_VRAM_GB}GB"
    fi
fi

# ── Validate requirements based on hardware profile ──
if [ "$IS_INTEL" = true ] && [ "$EGPU_DETECTED" = false ]; then
    fail "Intel Macs require an eGPU (NVIDIA Ampere+ or AMD RDNA3+) with the TinyGPU driver.
  Install TinyGPU: curl -fsSL https://raw.githubusercontent.com/tinygrad/tinygrad/master/extra/setup_tinygpu_osx.sh | sh
  More info: https://docs.tinygrad.org/tinygpu/"
fi

# macOS version: Apple Silicon needs 14+, Intel eGPU needs 12.1+ (TinyGPU minimum)
if [ "$IS_APPLE_SILICON" = true ] && [ "$MACOS_MAJOR" -lt 14 ]; then
    fail "Apple Silicon PRE requires macOS 14 (Sonoma) or later. Detected: $MACOS_VER"
elif [ "$IS_INTEL" = true ] && [ "$MACOS_MAJOR" -lt 12 ]; then
    fail "Intel eGPU PRE requires macOS 12.1 (Monterey) or later. Detected: $MACOS_VER"
fi

# Check Docker for NVIDIA eGPU (required for CUDA compilation)
if [ "$EGPU_VENDOR" = "nvidia" ]; then
    if ! command -v docker &>/dev/null; then
        warn "  Docker Desktop not found. Required for NVIDIA eGPU CUDA path."
        warn "  Install from: https://www.docker.com/products/docker-desktop/"
        if ! ask_yn "  Continue without Docker? (eGPU may not work) [y/N] " "N"; then exit 1; fi
    elif ! docker info &>/dev/null 2>&1; then
        warn "  Docker Desktop is installed but not running. Start it for NVIDIA eGPU support."
    else
        ok "  Docker: running (required for NVIDIA CUDA on macOS)"
    fi
fi

# Internal GPU info (Apple Silicon Metal)
GPU_INFO=$(system_profiler SPDisplaysDataType 2>/dev/null | grep "Chipset Model" | head -1 | sed 's/.*: //')
if [ -n "$GPU_INFO" ]; then
    ok "  GPU: $GPU_INFO (Metal)"
fi

# RAM
RAM_BYTES=$(sysctl -n hw.memsize)
RAM_GB=$((RAM_BYTES / 1073741824))

# RAM requirements differ based on hardware profile:
#   Apple Silicon (unified memory): model + KV cache + OS all share RAM → need 32GB+
#   Intel + eGPU: model lives on eGPU VRAM, RAM just for KV cache + OS → 16GB+
if [ "$IS_APPLE_SILICON" = true ]; then
    if [ "$RAM_GB" -lt 32 ]; then
        fail "Apple Silicon PRE requires at least 32GB unified memory. Detected: ${RAM_GB}GB"
    fi
elif [ "$IS_INTEL" = true ]; then
    if [ "$RAM_GB" -lt 16 ]; then
        fail "PRE requires at least 16GB RAM. Detected: ${RAM_GB}GB"
    fi
fi
if [ "$RAM_GB" -lt 64 ]; then
    warn "  RAM: ${RAM_GB}GB — functional, but 64GB+ recommended for large context windows."
else
    ok "  RAM: ${RAM_GB}GB"
fi

# ── Quantization selection ──
# Apple Silicon: unified memory, use RAM-based selection (current behavior)
# eGPU systems: model must fit in eGPU VRAM for full GPU acceleration
if [ "$EGPU_DETECTED" = true ] && [ "$EGPU_VRAM_GB" -gt 0 ]; then
    # eGPU with known VRAM — same logic as Windows installer
    if [ "$EGPU_VRAM_GB" -ge 28 ]; then
        BASE_MODEL="gemma4:26b-a4b-it-q8_0"
        QUANT="q8_0"
        MODEL_SIZE_GB=28
        ok "  Quant: q8_0 (near-lossless, ~28GB) — fits in ${EGPU_VRAM_GB}GB eGPU VRAM"
    else
        BASE_MODEL="gemma4:26b-a4b-it-q4_K_M"
        QUANT="q4_K_M"
        MODEL_SIZE_GB=15
        ok "  Quant: q4_K_M (~15GB) — fits in ${EGPU_VRAM_GB}GB eGPU VRAM for full acceleration"
    fi
elif [ "$EGPU_DETECTED" = true ]; then
    # eGPU detected but VRAM unknown (no Docker or AMD) — default to q4_K_M (safe)
    BASE_MODEL="gemma4:26b-a4b-it-q4_K_M"
    QUANT="q4_K_M"
    MODEL_SIZE_GB=15
    warn "  Quant: q4_K_M (~15GB) — eGPU VRAM unknown, using safe default"
    warn "  To use q8_0, ensure Docker is running and re-run the installer."
else
    # Apple Silicon unified memory — user chooses, with smart default
    if [ "$FORCE_Q4" = true ]; then
        BASE_MODEL="gemma4:26b-a4b-it-q4_K_M"
        QUANT="q4_K_M"
        MODEL_SIZE_GB=15
        ok "  Quant: q4_K_M (~15GB) — selected via --q4 flag"
    elif [ "$FORCE_Q8" = true ]; then
        BASE_MODEL="gemma4:26b-a4b-it-q8_0"
        QUANT="q8_0"
        MODEL_SIZE_GB=28
        ok "  Quant: q8_0 (near-lossless, ~28GB) — selected via --q8 flag"
    elif [ "$AUTO_YES" = true ]; then
        BASE_MODEL="gemma4:26b-a4b-it-q8_0"
        QUANT="q8_0"
        MODEL_SIZE_GB=28
        ok "  Quant: q8_0 (near-lossless, ~28GB)"
    else
        echo ""
        echo -e "  ${BOLD}Choose model quantization:${RESET}"
        echo ""
        echo -e "  ${CYAN}1)${RESET} ${BOLD}q8_0${RESET} (~28GB) — Near-lossless (<1% quality loss)"
        echo -e "     Best reasoning accuracy. Uses more RAM, may cause pressure on 64GB systems."
        echo ""
        echo -e "  ${CYAN}2)${RESET} ${BOLD}q4_K_M${RESET} (~15GB) — Good quality (~3% quality loss)"
        echo -e "     Lighter on RAM, faster decode. Good choice if you multitask heavily."
        echo ""
        QUANT_CHOICE=""
        read -p "  Choice [1/2, default=1]: " -n 1 -r QUANT_CHOICE
        echo ""
        if [ "$QUANT_CHOICE" = "2" ]; then
            BASE_MODEL="gemma4:26b-a4b-it-q4_K_M"
            QUANT="q4_K_M"
            MODEL_SIZE_GB=15
            ok "  Quant: q4_K_M (~15GB)"
        else
            BASE_MODEL="gemma4:26b-a4b-it-q8_0"
            QUANT="q8_0"
            MODEL_SIZE_GB=28
            ok "  Quant: q8_0 (near-lossless, ~28GB)"
        fi
    fi
fi

# Auto-size context window based on memory headroom after model weights.
# Headroom = total RAM - model size. KV cache + OS must fit in the remainder.
HEADROOM=$((RAM_GB - MODEL_SIZE_GB))
if [ "$HEADROOM" -ge 68 ]; then
    OPTIMAL_CTX=131072   # 128K
elif [ "$HEADROOM" -ge 36 ]; then
    OPTIMAL_CTX=65536    # 64K
elif [ "$HEADROOM" -ge 20 ]; then
    OPTIMAL_CTX=32768    # 32K
elif [ "$HEADROOM" -ge 8 ]; then
    OPTIMAL_CTX=16384    # 16K
elif [ "$HEADROOM" -ge 4 ]; then
    OPTIMAL_CTX=8192     # 8K
else
    OPTIMAL_CTX=4096     # 4K
fi
CTX_HUMAN="$(( OPTIMAL_CTX / 1024 ))K"
ok "  Context window: ${CTX_HUMAN} (${OPTIMAL_CTX} tokens, ${HEADROOM}GB headroom after ${QUANT} model)"

# Disk space
AVAIL_KB=$(df -k "$REPO_DIR" | tail -1 | awk '{print $4}')
AVAIL_GB=$((AVAIL_KB / 1048576))
DISK_NEEDED=$((MODEL_SIZE_GB + 5))
if [ "$AVAIL_GB" -lt "$DISK_NEEDED" ]; then
    warn "  Disk: ${AVAIL_GB}GB available — need ~${MODEL_SIZE_GB}GB for ${QUANT} model download."
    if ! ask_yn "  Continue anyway? [y/N] " "N"; then exit 1; fi
else
    ok "  Disk: ${AVAIL_GB}GB available"
fi

echo ""
ok "System requirements met."

# ============================================================================
# Step 0b: Inference backend selection (Ollama vs llama.cpp)
# ============================================================================
step "Inference backend"

# Smart default: NVIDIA eGPU (CUDA) → llama.cpp for Flash Attention + grammar;
# Apple Silicon (Metal) → Ollama (simpler, similar performance on unified memory).
if [ "$GPU_BACKEND" = "cuda" ]; then
    SMART_DEFAULT="llamacpp"
    SMART_REASON="NVIDIA GPU detected — llama.cpp enables Flash Attention and grammar constraints for faster, more reliable inference."
else
    SMART_DEFAULT="ollama"
    SMART_REASON="Apple Silicon detected — Ollama provides similar performance with simpler model management."
fi

BACKEND="$SMART_DEFAULT"
if [ "$FORCE_LLAMACPP" = true ]; then
    BACKEND="llamacpp"
    ok "  Backend: llama.cpp (--llamacpp flag)"
elif [ "$AUTO_YES" = true ]; then
    BACKEND="$SMART_DEFAULT"
    ok "  Backend: $([ "$SMART_DEFAULT" = "llamacpp" ] && echo "llama.cpp" || echo "Ollama") (auto — $SMART_REASON)"
else
    echo ""
    echo -e "  ${DIM}$SMART_REASON${RESET}"
    echo ""
    echo -e "  ${BOLD}Choose inference backend:${RESET}"
    echo ""
    if [ "$SMART_DEFAULT" = "llamacpp" ]; then
        echo -e "  ${CYAN}1)${RESET} ${BOLD}llama.cpp${RESET} (recommended) — Direct GPU inference"
        echo -e "     Flash Attention, grammar-constrained output, lower overhead."
        echo -e "     Requires Homebrew. Ollama still installed for embeddings."
        echo ""
        echo -e "  ${CYAN}2)${RESET} ${BOLD}Ollama${RESET} — Managed model server"
        echo -e "     Easy setup, automatic model management, wide compatibility."
    else
        echo -e "  ${CYAN}1)${RESET} ${BOLD}Ollama${RESET} (recommended) — Managed model server"
        echo -e "     Easy setup, automatic model management, wide compatibility."
        echo ""
        echo -e "  ${CYAN}2)${RESET} ${BOLD}llama.cpp${RESET} — Direct GPU inference"
        echo -e "     Lower overhead, grammar-constrained output, speculative decoding."
        echo -e "     Requires Homebrew. Ollama still installed for embeddings."
    fi
    echo ""
    BACKEND_CHOICE=""
    read -p "  Choice [1/2, default=1]: " -n 1 -r BACKEND_CHOICE
    echo ""
    if [ "$BACKEND_CHOICE" = "2" ]; then
        # User chose the non-default option
        if [ "$SMART_DEFAULT" = "llamacpp" ]; then
            BACKEND="ollama"
            ok "  Backend: Ollama"
        else
            BACKEND="llamacpp"
            ok "  Backend: llama.cpp"
        fi
    else
        BACKEND="$SMART_DEFAULT"
        if [ "$SMART_DEFAULT" = "llamacpp" ]; then
            ok "  Backend: llama.cpp (recommended for NVIDIA GPU)"
        else
            ok "  Backend: Ollama (recommended for Apple Silicon)"
        fi
    fi
fi

# Ollama optimization settings derived from hardware — computed unconditionally
# so that Step 10's LaunchAgent plist always has valid values regardless of
# which backend (ollama vs llamacpp) was chosen.  Step 1b re-applies them via
# launchctl and shell profile when BACKEND=ollama.
FA_VAL=0
[ "$GPU_BACKEND" = "cuda" ] && FA_VAL=1
KV_CACHE_TYPE="q8_0"
[ "$QUANT" = "q4_K_M" ] && KV_CACHE_TYPE="q4_0"
NUM_PARALLEL=1
[ "$HEADROOM" -ge 16 ] 2>/dev/null && NUM_PARALLEL=2

# ============================================================================
# Step 1: Ollama (always installed — primary backend or embeddings-only)
# ============================================================================
if [ "$BACKEND" = "llamacpp" ]; then
    step "Checking Ollama (for embeddings)"
    echo -e "  ${DIM}llama.cpp is the primary backend — Ollama provides embedding model only.${RESET}"
else
    step "Checking Ollama"
fi

if command -v ollama &>/dev/null; then
    OLLAMA_VER=$(ollama --version 2>&1 | head -1)
    ok "  Ollama installed: $OLLAMA_VER"
else
    echo "  Ollama is not installed."
    if command -v brew &>/dev/null; then
        if ! ask_yn "  Install Ollama via Homebrew? [Y/n] " "Y"; then
            if [ "$BACKEND" = "ollama" ]; then
                echo ""
                echo "  Install Ollama manually:"
                echo "    brew install ollama"
                echo "    — or —"
                echo "    Download from https://ollama.com/download"
                exit 1
            else
                warn "  Skipping Ollama — embeddings will use keyword search fallback."
            fi
        else
            echo "  Installing Ollama..."
            brew install ollama
            ok "  Ollama installed."
        fi
    else
        if [ "$BACKEND" = "ollama" ]; then
            echo ""
            echo "  Install Ollama:"
            echo "    Download from https://ollama.com/download"
            echo "    — or —"
            echo "    Install Homebrew first: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
            echo "    Then: brew install ollama"
            exit 1
        else
            warn "  Homebrew and Ollama not found — embeddings will use keyword search fallback."
        fi
    fi
fi

# Ensure Ollama is running (if installed)
PORT="${PRE_PORT:-11434}"
if command -v ollama &>/dev/null; then
    if ! curl -sf "http://localhost:${PORT}/v1/models" >/dev/null 2>&1; then
        echo "  Starting Ollama..."
        ollama serve >/dev/null 2>&1 &
        sleep 3
        if ! curl -sf "http://localhost:${PORT}/v1/models" >/dev/null 2>&1; then
            if [ "$BACKEND" = "ollama" ]; then
                warn "  Could not start Ollama automatically."
                echo "  Please start Ollama.app or run 'ollama serve' in another terminal, then re-run this script."
                exit 1
            else
                warn "  Could not start Ollama — embeddings may be limited."
            fi
        fi
    fi
    ok "  Ollama running on port $PORT."
fi

# ============================================================================
# Step 1b: Ollama environment optimizations for Gemma 4 MoE
# ============================================================================
if [ "$BACKEND" = "ollama" ]; then
step "Configuring Ollama environment"

# Settings tuned for Gemma 4 26B MoE:
#   FA:              0 for Metal (hybrid attention is slower/unstable), 1 for CUDA (eGPU benefits)
#   KV_CACHE_TYPE:   Match KV cache precision to model quant
#   KEEP_ALIVE=24h:  Keep model loaded in GPU memory between requests
#   NUM_PARALLEL:    Dynamic — 2 when headroom >= 16GB (concurrent background agents), else 1
#   MAX_LOADED=2:    Inference model + nomic-embed-text stay resident

# Flash Attention: off for Metal (Apple Silicon), on for CUDA (NVIDIA eGPU)
FA_VAL=0
if [ "$GPU_BACKEND" = "cuda" ]; then
    FA_VAL=1
fi

# KV cache type: match to model quant for best quality/memory tradeoff
KV_CACHE_TYPE="q8_0"
if [ "$QUANT" = "q4_K_M" ]; then
    KV_CACHE_TYPE="q4_0"
fi

# Match OLLAMA_NUM_PARALLEL to the inference-queue slot count
# 2 parallel when headroom >= 16GB (enables concurrent background agents)
NUM_PARALLEL=1
if [ "$HEADROOM" -ge 16 ] 2>/dev/null; then
    NUM_PARALLEL=2
fi

OLLAMA_ENVS=(
    "OLLAMA_FLASH_ATTENTION:${FA_VAL}"
    "OLLAMA_KV_CACHE_TYPE:${KV_CACHE_TYPE}"
    "OLLAMA_KEEP_ALIVE:24h"
    "OLLAMA_NUM_PARALLEL:${NUM_PARALLEL}"
    "OLLAMA_MAX_LOADED_MODELS:2"
)
# CUDA eGPU: reserve 256MB overhead so Ollama doesn't over-commit VRAM
if [ "$GPU_BACKEND" = "cuda" ]; then
    OLLAMA_ENVS+=("OLLAMA_GPU_OVERHEAD:256000000")
fi

# Set via launchctl for the macOS Ollama.app
NEEDS_RESTART=false
for entry in "${OLLAMA_ENVS[@]}"; do
    KEY="${entry%%:*}"
    VAL="${entry#*:}"
    CURRENT=$(launchctl getenv "$KEY" 2>/dev/null || echo "")
    if [ "$CURRENT" != "$VAL" ]; then
        launchctl setenv "$KEY" "$VAL"
        NEEDS_RESTART=true
    fi
done
ok "  launchctl environment set (FA=${FA_VAL}, KV=${KV_CACHE_TYPE}, KEEP_ALIVE=24h)"

# Persist in shell profile so new terminals inherit the values
SHELL_RC=""
if [ -f "$HOME/.zshrc" ]; then
    SHELL_RC="$HOME/.zshrc"
elif [ -f "$HOME/.bash_profile" ]; then
    SHELL_RC="$HOME/.bash_profile"
elif [ -f "$HOME/.bashrc" ]; then
    SHELL_RC="$HOME/.bashrc"
fi

if [ -n "$SHELL_RC" ]; then
    CHANGED=false
    for entry in "${OLLAMA_ENVS[@]}"; do
        KEY="${entry%%:*}"
        VAL="${entry#*:}"
        if ! grep -q "^export ${KEY}=" "$SHELL_RC" 2>/dev/null; then
            echo "export ${KEY}=${VAL}" >> "$SHELL_RC"
            CHANGED=true
        fi
    done
    if [ "$CHANGED" = true ]; then
        ok "  Ollama env vars added to $SHELL_RC"
    else
        ok "  Ollama env vars already in $SHELL_RC"
    fi
fi

# Restart Ollama if env vars changed and it's running
if [ "$NEEDS_RESTART" = true ]; then
    if pgrep -f "Ollama.app" >/dev/null 2>&1; then
        echo "  Restarting Ollama to pick up new environment..."
        pkill -f "Ollama.app" 2>/dev/null
        sleep 3
        open -a Ollama 2>/dev/null || ollama serve >/dev/null 2>&1 &
        # Wait for API
        for i in $(seq 1 30); do
            if curl -sf "http://localhost:${PORT}/v1/models" >/dev/null 2>&1; then break; fi
            sleep 1
        done
        ok "  Ollama restarted with optimized settings"
    fi
fi
fi  # end BACKEND=ollama check for Step 1b

# ============================================================================
# Step 1c: llama.cpp server installation (when llamacpp backend selected)
# ============================================================================
if [ "$BACKEND" = "llamacpp" ]; then
    step "Installing llama.cpp"

    if command -v llama-server &>/dev/null; then
        LLAMA_VER=$(llama-server --version 2>&1 | head -1 || echo "installed")
        ok "  llama-server already installed: $LLAMA_VER"
    elif command -v brew &>/dev/null; then
        echo "  Installing llama.cpp via Homebrew..."
        brew install llama.cpp --quiet 2>&1 | tail -3
        if command -v llama-server &>/dev/null; then
            ok "  llama.cpp installed (llama-server ready)"
        else
            fail "  llama.cpp installation failed. Install manually: brew install llama.cpp"
        fi
    else
        fail "  Homebrew required to install llama.cpp. Install Homebrew first."
    fi

    # Also install ggml (separate Homebrew formula, provides libggml for CLI build)
    if ! brew --prefix ggml &>/dev/null 2>&1; then
        echo "  Installing ggml library..."
        brew install ggml --quiet 2>&1 | tail -3
    fi
    ok "  ggml library available"

    # Resolve the GGUF model path from Ollama's blob store
    GGUF_MODEL_PATH=""
    if command -v ollama &>/dev/null; then
        # If Ollama has the model, resolve the GGUF blob path
        MANIFEST_DIR="$HOME/.ollama/models/manifests/registry.ollama.ai/library"
        MODEL_NAME=$(echo "$BASE_MODEL" | cut -d: -f1)
        MODEL_TAG=$(echo "$BASE_MODEL" | cut -d: -f2)
        MANIFEST_FILE="$MANIFEST_DIR/$MODEL_NAME/$MODEL_TAG"

        if [ -f "$MANIFEST_FILE" ]; then
            # Extract the model layer digest from the manifest
            DIGEST=$(grep -o '"sha256:[a-f0-9]*"' "$MANIFEST_FILE" 2>/dev/null | head -1 | tr -d '"')
            if [ -n "$DIGEST" ]; then
                BLOB_NAME=$(echo "$DIGEST" | sed 's/:/-/')
                BLOB_PATH="$HOME/.ollama/models/blobs/$BLOB_NAME"
                if [ -f "$BLOB_PATH" ]; then
                    GGUF_MODEL_PATH="$BLOB_PATH"
                    ok "  GGUF model found in Ollama store: $(du -sh "$BLOB_PATH" | cut -f1)"
                fi
            fi
        fi
    fi

    if [ -z "$GGUF_MODEL_PATH" ]; then
        warn "  GGUF model not found in Ollama store."
        echo "  PRE will resolve the model path at runtime from Ollama's manifest."
        echo "  Make sure the base model is pulled: ollama pull $BASE_MODEL"
    fi

    # Determine llama-server port (default 8080)
    LLAMACPP_PORT=8080
fi

# ============================================================================
# Step 2: Pull base model (via Ollama — both backends need the GGUF)
# ============================================================================
if command -v ollama &>/dev/null; then
step "Pulling base model ($BASE_MODEL)"

MODEL_TAG=$(echo "$BASE_MODEL" | sed 's/.*://')
if ollama list 2>/dev/null | grep -q "gemma4.*${MODEL_TAG}"; then
    ok "  Base model already available."
else
    echo "  Downloading ~${MODEL_SIZE_GB}GB model ($QUANT). This may take a while..."
    echo "  (Download is resumable — safe to interrupt and re-run)"
    ollama pull "$BASE_MODEL" || fail "Failed to pull $BASE_MODEL"
    ok "  Base model downloaded."
fi
fi

# ============================================================================
# Step 2b: Pull embedding model (for experience ledger semantic search)
# ============================================================================
if command -v ollama &>/dev/null; then
step "Pulling embedding model (nomic-embed-text — experience ledger + RAG)"

if ollama list 2>/dev/null | grep -q "nomic-embed-text"; then
    ok "  Embedding model already available."
else
    echo "  Downloading nomic-embed-text (~274MB)..."
    ollama pull nomic-embed-text || warn "  Failed to pull nomic-embed-text — experience ledger and RAG will use keyword search fallback."
    if ollama list 2>/dev/null | grep -q "nomic-embed-text"; then
        ok "  Embedding model downloaded."
    fi
fi
fi

# ============================================================================
# Step 3: Create custom model from Modelfile (Ollama backend only)
# ============================================================================
if [ "$BACKEND" = "ollama" ] && command -v ollama &>/dev/null; then
step "Creating optimized model ($CUSTOM_MODEL)"

# The Modelfile uses q8_0 as its FROM line. If we're using q4_K_M, generate
# a modified Modelfile with the correct base model.
EFFECTIVE_MODELFILE="$ENGINE_DIR/Modelfile"
if [ ! -f "$EFFECTIVE_MODELFILE" ]; then
    fail "Modelfile not found at $EFFECTIVE_MODELFILE"
fi
if [ "$QUANT" != "q8_0" ]; then
    TEMP_MODELFILE="/tmp/pre-Modelfile-${QUANT}"
    sed "s|FROM gemma4:26b-a4b-it-q8_0|FROM $BASE_MODEL|" "$EFFECTIVE_MODELFILE" > "$TEMP_MODELFILE"
    EFFECTIVE_MODELFILE="$TEMP_MODELFILE"
    ok "  Using modified Modelfile with $QUANT base"
fi

if ollama list 2>/dev/null | grep -q "$CUSTOM_MODEL"; then
    ok "  Custom model already exists."
    if ask_yn "  Recreate from Modelfile? [y/N] " "N"; then
        ollama create "$CUSTOM_MODEL" -f "$EFFECTIVE_MODELFILE" || fail "Failed to create $CUSTOM_MODEL"
        ok "  Custom model recreated."
    fi
else
    ollama create "$CUSTOM_MODEL" -f "$EFFECTIVE_MODELFILE" || fail "Failed to create $CUSTOM_MODEL"
    ok "  Custom model created ($QUANT, dynamic context, optimized batch size)."
fi

# Fast tier model — Gemma 4 E2B for quick Q&A (~140 tok/s, 2x main model)
FAST_MODEL="pre-gemma4-fast"
FAST_MODELFILE="$ENGINE_DIR/Modelfile.fast"
if [ -f "$FAST_MODELFILE" ]; then
    if ! ollama list 2>/dev/null | grep -q "$FAST_MODEL"; then
        echo -e "  ${DIM}Pulling fast tier base model (gemma4:e2b, ~6GB)...${RESET}"
        ollama pull gemma4:e2b 2>/dev/null || warn "  Failed to pull gemma4:e2b — fast tier will be unavailable"
        if ollama list 2>/dev/null | grep -q "gemma4:e2b"; then
            ollama create "$FAST_MODEL" -f "$FAST_MODELFILE" 2>/dev/null && \
                ok "  Fast tier model created ($FAST_MODEL — Gemma 4 E2B)" || \
                warn "  Failed to create $FAST_MODEL — fast tier will fall back to main model"
        fi
    else
        ok "  Fast tier model already exists ($FAST_MODEL)"
    fi
fi

# ----------------------------------------------------------------------------
# Optional: Joby IT Service Desk model (private, password-gated)
# ----------------------------------------------------------------------------
# A LoRA fine-tune of pre-gemma4 specialized for Joby Aviation's IT service
# desk. Distributed via HuggingFace private repo, gated here by a simple
# install-time password. Internal Joby use only.
#
# Note: the password is an intent-gate, not a crypto-gate. Actual access
# control is the HF private-repo token. Anyone with this script can read
# the password hash; the hash just keeps casual installers from grabbing it.
JOBY_MODEL_REPO="sunkencity/joby-it-servicedesk"
JOBY_MODEL_TAG="q8_0"
JOBY_PASSWORD_HASH="18278044341fbdb26d8b0efae30087a1d38c1283c4af7448abf638580dd269cb"

if ask_yn "  Install Joby IT service desk model? (Joby internal only) [y/N] " "N"; then
    read -s -r -p "    Access password: " joby_input_password
    echo
    joby_input_hash=$(echo -n "$joby_input_password" | shasum -a 256 | awk '{print $1}')
    unset joby_input_password
    if [ "$joby_input_hash" != "$JOBY_PASSWORD_HASH" ]; then
        warn "  Incorrect password — skipping Joby model."
    else
        ok "  Password accepted."

        # We download the GGUF locally then `ollama create`, instead of
        # `ollama pull hf.co/...`, because Ollama's HF integration trips on a
        # redirect realm-mismatch ("hf.co" vs "huggingface.co") when the repo
        # is private. The `hf` CLI handles auth correctly.

        # ── Check prerequisites: hf CLI + HuggingFace token ──
        _joby_ready=false
        while true; do
            _missing=""
            if ! command -v hf >/dev/null 2>&1; then
                _missing="${_missing}  • HuggingFace CLI ('hf') is not installed.\n"
            fi
            # Check for HF token (env var or cached file)
            _hf_token="${HF_TOKEN:-}"
            if [ -z "$_hf_token" ] && [ -f "$HOME/.cache/huggingface/token" ]; then
                _hf_token="$(cat "$HOME/.cache/huggingface/token")"
            fi
            if [ -z "$_hf_token" ]; then
                _missing="${_missing}  • HuggingFace auth token not found.\n"
            fi

            if [ -z "$_missing" ]; then
                export HF_TOKEN="$_hf_token"
                _joby_ready=true
                break
            fi

            echo ""
            warn "  Missing prerequisites:"
            printf "$_missing"
            echo ""
            echo "  To fix, open another terminal and run:"
            echo ""
            if ! command -v hf >/dev/null 2>&1; then
                echo "    pip install --upgrade huggingface_hub    # install the hf CLI"
            fi
            if [ -z "$_hf_token" ]; then
                echo "    hf auth login                            # authenticate with HuggingFace"
                echo "    (paste your token from https://huggingface.co/settings/tokens)"
            fi
            echo ""
            if ask_yn "  Press Y when ready to re-check, or N to skip the Joby model [Y/n] " "Y"; then
                echo "  Re-checking prerequisites..."
                # Re-source PATH in case user installed hf in this shell session
                export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
                continue
            else
                warn "  Skipping Joby model."
                break
            fi
        done

        if [ "$_joby_ready" = true ]; then
            JOBY_GGUF_NAME="joby-it-servicedesk.${JOBY_MODEL_TAG}.gguf"
            JOBY_WORKDIR="$(mktemp -d -t pre-joby-XXXXXX)"
            trap 'rm -rf "$JOBY_WORKDIR"' EXIT

            echo "    Downloading ${JOBY_GGUF_NAME} (~30 GB)..."
            if hf download "$JOBY_MODEL_REPO" "$JOBY_GGUF_NAME" --local-dir "$JOBY_WORKDIR"; then
                cat > "$JOBY_WORKDIR/Modelfile.joby" <<MODELFILE
FROM $JOBY_WORKDIR/$JOBY_GGUF_NAME
PARAMETER num_ctx 8192
PARAMETER num_batch 512
PARAMETER temperature 1.0
PARAMETER top_k 64
PARAMETER top_p 0.95
PARAMETER min_p 0.05
PARAMETER repeat_penalty 1.1
PARAMETER repeat_last_n 256
MODELFILE
                if ollama create pre-gemma4-itsd -f "$JOBY_WORKDIR/Modelfile.joby"; then
                    ok "  Joby IT model installed as: pre-gemma4-itsd"

                    # Flip the Joby service-desk mode flag in router.json so
                    # PRE's system prompt auto-injects the anti-hallucination
                    # guard (see web/src/context.js → isJobyServiceDeskMode).
                    mkdir -p "$HOME/.pre"
                    ROUTER_JSON="$HOME/.pre/router.json"
                    if [ -f "$ROUTER_JSON" ]; then
                        # In-place merge: add the key, preserve everything else
                        python3 -c "
import json, sys
p = '$ROUTER_JSON'
try:
    with open(p) as f: cfg = json.load(f)
except Exception:
    cfg = {}
cfg['joby_servicedesk_mode'] = True
with open(p, 'w') as f: json.dump(cfg, f, indent=2)
" 2>/dev/null && ok "  Set joby_servicedesk_mode=true in router.json"
                    else
                        echo '{"joby_servicedesk_mode": true}' > "$ROUTER_JSON"
                        ok "  Wrote router.json with joby_servicedesk_mode=true"
                    fi
                else
                    warn "  ollama create failed. The GGUF is at $JOBY_WORKDIR — retry manually."
                    trap - EXIT  # don't wipe their download
                fi
            else
                warn "  hf download failed. Check HF access to $JOBY_MODEL_REPO."
            fi
        fi
    fi
fi

fi

# ============================================================================
# Step 3.5: Optional escalation model (eGPU offload via tinygrad)
# ============================================================================
# Installs a second, larger model that PRE can route to per-message via the
# /escalate command or the ↑GPU toggle in the chat input. macOS uses tinygrad
# as the runtime (because Ollama's Gemma path holds the eGPU). VRAM headroom
# picks the recommended tier; the user can decline or pick differently.

if [ "$EGPU_DETECTED" = true ] && [ "$EGPU_VRAM_GB" -ge 16 ]; then
    step "Optional: escalation model (eGPU offload)"

    ESC_REPO=""; ESC_FILE=""; ESC_LABEL=""; ESC_TOKENIZER=""; ESC_SIZE_GB=0; ESC_GATED=false
    if [ "$EGPU_VRAM_GB" -ge 80 ]; then
        ESC_REPO="bartowski/Llama-3.3-70B-Instruct-GGUF"
        ESC_FILE="Llama-3.3-70B-Instruct-Q6_K.gguf"
        ESC_TOKENIZER="meta-llama/Llama-3.3-70B-Instruct"
        ESC_LABEL="Llama-3.3-70B Q6_K"
        ESC_SIZE_GB=57; ESC_GATED=true
    elif [ "$EGPU_VRAM_GB" -ge 48 ]; then
        ESC_REPO="bartowski/Llama-3.3-70B-Instruct-GGUF"
        ESC_FILE="Llama-3.3-70B-Instruct-Q4_K_M.gguf"
        ESC_TOKENIZER="meta-llama/Llama-3.3-70B-Instruct"
        ESC_LABEL="Llama-3.3-70B Q4_K_M"
        ESC_SIZE_GB=42; ESC_GATED=true
    elif [ "$EGPU_VRAM_GB" -ge 24 ]; then
        ESC_REPO="bartowski/THUDM_GLM-4-32B-0414-GGUF"
        ESC_FILE="THUDM_GLM-4-32B-0414-Q4_K_M.gguf"
        ESC_TOKENIZER="THUDM/GLM-4-32B-0414"
        ESC_LABEL="GLM-4-32B Q4_K_M"
        ESC_SIZE_GB=20; ESC_GATED=false
    else
        ESC_REPO="bartowski/DeepSeek-Coder-V2-Lite-Instruct-GGUF"
        ESC_FILE="DeepSeek-Coder-V2-Lite-Instruct-Q4_K_M.gguf"
        ESC_TOKENIZER="deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"
        ESC_LABEL="DeepSeek-Coder-V2-Lite Q4_K_M"
        ESC_SIZE_GB=10; ESC_GATED=false
    fi

    echo "  Detected ${EGPU_VRAM_GB}GB eGPU VRAM"
    echo "  Recommended escalation model: $ESC_LABEL (~${ESC_SIZE_GB}GB)"
    echo "  Runtime: tinygrad on the eGPU. Triggered by /escalate or the ↑GPU toggle."
    if [ "$ESC_GATED" = true ]; then
        echo "  Note: this model requires accepting Meta's Llama license on HuggingFace."
    fi

    if ask_yn "  Install escalation model? [Y/n] " "Y"; then
        # hf CLI + HF_TOKEN check (same pattern as Joby model)
        _esc_ready=false
        while true; do
            _missing=""
            if ! command -v hf >/dev/null 2>&1; then
                _missing="${_missing}  • HuggingFace CLI ('hf') is not installed.\n"
            fi
            _hf_token="${HF_TOKEN:-}"
            if [ -z "$_hf_token" ] && [ -f "$HOME/.cache/huggingface/token" ]; then
                _hf_token="$(cat "$HOME/.cache/huggingface/token")"
            fi
            if [ -z "$_hf_token" ] && [ "$ESC_GATED" = true ]; then
                _missing="${_missing}  • HuggingFace auth token not found (required for gated model).\n"
            fi
            if [ -z "$_missing" ]; then
                [ -n "$_hf_token" ] && export HF_TOKEN="$_hf_token"
                _esc_ready=true
                break
            fi
            echo ""
            warn "  Missing prerequisites:"
            printf "$_missing"
            echo ""
            echo "  To fix, open another terminal and run:"
            if ! command -v hf >/dev/null 2>&1; then
                echo "    pip install --upgrade huggingface_hub    # install the hf CLI"
            fi
            if [ -z "$_hf_token" ] && [ "$ESC_GATED" = true ]; then
                echo "    hf auth login                            # paste token from https://huggingface.co/settings/tokens"
            fi
            echo ""
            if ask_yn "  Press Y to re-check, or N to skip the escalation model [Y/n] " "Y"; then
                export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
                continue
            else
                warn "  Skipping escalation model."
                break
            fi
        done

        if [ "$_esc_ready" = true ]; then
            ESC_DIR="$HOME/.pre/models/escalation"
            mkdir -p "$ESC_DIR"

            # First attempt — may fail with 403 on gated models the user hasn't accepted
            _esc_dl_log="$(mktemp)"
            if ! hf download "$ESC_REPO" "$ESC_FILE" --local-dir "$ESC_DIR" 2>&1 | tee "$_esc_dl_log"; then
                if grep -qiE "403|gated|access denied|restricted" "$_esc_dl_log"; then
                    warn "  This model is gated — you need to accept the license on HuggingFace."
                    echo "    GGUF mirror: https://huggingface.co/$ESC_REPO"
                    echo "    Upstream:    https://huggingface.co/$ESC_TOKENIZER"
                    open "https://huggingface.co/$ESC_TOKENIZER" 2>/dev/null || true
                    read -r -p "  Press Enter once you've accepted the license to retry, or Ctrl+C to skip: "
                    hf download "$ESC_REPO" "$ESC_FILE" --local-dir "$ESC_DIR" || \
                        warn "  Download still failed — escalation model not installed."
                fi
            fi
            rm -f "$_esc_dl_log"

            ESC_GGUF_PATH="$ESC_DIR/$ESC_FILE"
            if [ -f "$ESC_GGUF_PATH" ]; then
                # Write _provider.escalation to ~/.pre/connections.json (tinygrad runtime)
                mkdir -p "$HOME/.pre"
                CONN_JSON="$HOME/.pre/connections.json"
                python3 - "$CONN_JSON" "$ESC_GGUF_PATH" "$ESC_TOKENIZER" "$ESC_LABEL" <<'PYEOF'
import json, os, sys
p, gguf, tok, label = sys.argv[1:5]
data = {}
if os.path.exists(p):
    try:
        with open(p) as f: data = json.load(f)
    except Exception: data = {}
prov = data.setdefault("_provider", {})
prov["escalation"] = {
    "enabled": True,
    "runtime": "tinygrad",
    "model_path": gguf,
    "tokenizer_name": tok,
    "label": label,
}
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "w") as f: json.dump(data, f, indent=2)
print("OK")
PYEOF
                ok "  Escalation model registered: $ESC_LABEL"
                echo "    GGUF: $ESC_GGUF_PATH"
                echo "    Tokenizer (downloads on first use): $ESC_TOKENIZER"
            else
                warn "  Escalation GGUF not found at $ESC_GGUF_PATH — skipping registration."
            fi
        fi
    fi
fi

# ============================================================================
# Step 4: Xcode Command Line Tools
# ============================================================================
step "Checking build tools"

if ! command -v clang &>/dev/null; then
    if [ "$IS_APPLE_SILICON" = true ]; then
        warn "  Clang not found. Installing Xcode Command Line Tools..."
        xcode-select --install 2>/dev/null || true
        echo "  Please complete the Xcode installation and re-run this script."
        exit 1
    else
        warn "  Clang not found — CLI binary will not be built (Web GUI only)."
    fi
else
    ok "  Compiler: $(clang --version | head -1)"
fi

# Swift compiler (needed for EventKit-based Calendar/Reminders tools)
if command -v swiftc &>/dev/null; then
    ok "  Swift: $(swiftc --version 2>&1 | head -1 | sed 's/.*version: //')"
else
    warn "  swiftc not found — Calendar/Reminders tools will compile on first use."
    if [ "$IS_APPLE_SILICON" = true ]; then
        warn "  Install Xcode CLI tools to resolve: xcode-select --install"
    fi
fi

# Check for Metal framework (Apple Silicon only)
if [ "$IS_APPLE_SILICON" = true ]; then
    if ! xcrun --sdk macosx --show-sdk-path &>/dev/null; then
        warn "  macOS SDK not found. You may need to run: xcode-select --install"
    fi
fi

# ============================================================================
# Step 5: Build PRE
# ============================================================================
if [ "$IS_APPLE_SILICON" = true ]; then
    step "Building PRE"

    cd "$ENGINE_DIR"
    make clean 2>/dev/null || true
    make pre telegram menubar desktop 2>&1 | tail -5
    if [ ! -x "$ENGINE_DIR/pre" ]; then
        fail "Build failed — 'pre' binary not found."
    fi
    ok "  Built: pre ($(du -sh pre | cut -f1))"
    if [ -x "$ENGINE_DIR/pre-telegram" ]; then
        ok "  Built: pre-telegram ($(du -sh pre-telegram | cut -f1))"
    fi
    if [ -x "$ENGINE_DIR/PRE.app/Contents/MacOS/pre-menubar" ]; then
        ok "  Built: PRE.app (menu bar helper)"
    fi
    if [ -x "$ENGINE_DIR/PRE Desktop.app/Contents/MacOS/pre-desktop" ]; then
        ok "  Built: PRE Desktop.app (native desktop wrapper)"
    fi
else
    step "Skipping CLI build (Intel — Web GUI only)"
    ok "  CLI requires Apple Silicon (arm64). Web GUI will be your interface."
    ok "  Access PRE at http://localhost:7749 after installation."

    # Still build the desktop app and menu bar — they work on Intel
    if command -v swiftc >/dev/null 2>&1; then
        cd "$ENGINE_DIR"
        make menubar desktop 2>&1 | tail -3
        if [ -x "$ENGINE_DIR/PRE.app/Contents/MacOS/pre-menubar" ]; then
            ok "  Built: PRE.app (menu bar helper)"
        fi
        if [ -x "$ENGINE_DIR/PRE Desktop.app/Contents/MacOS/pre-desktop" ]; then
            ok "  Built: PRE Desktop.app (native desktop wrapper)"
        fi
    fi
fi

# ============================================================================
# Step 6: Web GUI dependencies
# ============================================================================
step "Setting up Web GUI"

WEB_DIR="$REPO_DIR/web"
if [ -f "$WEB_DIR/package.json" ]; then
    if command -v node &>/dev/null; then
        NODE_VER=$(node --version)
        NODE_MAJOR=$(echo "$NODE_VER" | sed 's/v//' | cut -d. -f1)
        if [ "$NODE_MAJOR" -ge 18 ]; then
            ok "  Node.js: $NODE_VER"
            echo "  Installing web dependencies..."
            cd "$WEB_DIR"
            npm install --silent 2>&1 | tail -3
            ok "  Web GUI dependencies installed"

            # Install terminal-notifier for clickable cron notifications
            if ! command -v terminal-notifier &>/dev/null; then
                if command -v brew &>/dev/null; then
                    echo "  Installing terminal-notifier (clickable cron notifications)..."
                    brew install terminal-notifier --quiet 2>&1 | tail -1
                    ok "  terminal-notifier installed"
                else
                    warn "  terminal-notifier not found — cron notifications will use basic osascript."
                    echo -e "  ${DIM}Install later with: brew install terminal-notifier${RESET}"
                fi
            else
                ok "  terminal-notifier available (clickable cron notifications)"
            fi
            cd "$REPO_DIR"
        else
            warn "  Node.js $NODE_VER is too old — need v18+. Web GUI will not be available."
            warn "  Install a newer version: brew install node"
        fi
    else
        warn "  Node.js not found — Web GUI will not be available."
        warn "  Install Node.js: brew install node"
        echo -e "  ${DIM}The CLI works without Node.js. Web GUI is optional.${RESET}"
    fi
else
    warn "  Web GUI not found at $WEB_DIR — skipping."
fi

# ============================================================================
# Step 6b: MCP integration (Claude Code / Claude Desktop / Codex / Antigravity)
# ============================================================================
step "MCP Integration (use PRE from Claude, Codex, Antigravity, or other AI tools)"

MCP_STDIO_PATH="$WEB_DIR/mcp-stdio.js"
MCP_SNIPPET="{\"mcpServers\":{\"pre\":{\"command\":\"node\",\"args\":[\"$MCP_STDIO_PATH\"]}}}"
MCP_TOOLS_CONFIGURED=0

# Shared delegation instructions — written to each tool's instructions file
PRE_DELEGATION_BLOCK="## PRE — Local AI Agent (MCP)

PRE is a local AI agent running on this machine (Ollama + Gemma 4, ${CTX_HUMAN} context).
It is available as an MCP tool to offload execution-heavy tasks at zero API token cost.

### When to delegate to PRE (\`pre_agent\` or \`pre_chat\`):
- System info, disk, network, process checks
- File search, reading, and directory listing
- Git operations and status checks
- Shell command execution
- Summarization, translation, data formatting
- Simple factual questions
- Memory search (\`pre_memory_search\`)

### When to handle directly (do NOT delegate):
- Complex code generation and software architecture
- Security reviews and vulnerability analysis
- Nuanced multi-factor analysis and bug reasoning
- Any task where frontier reasoning quality is critical

### MCP tools:
- \`pre_agent\` — Full agentic task with 70+ tools (file I/O, shell, web, macOS apps, RAG, etc.)
- \`pre_chat\` — Quick Q&A without tools (fastest)
- \`pre_memory_search\` — Search PRE's persistent memory store
- \`pre_sessions\` — List recent PRE sessions"

# Helper: append PRE delegation instructions to a file if not already present
write_delegation_instructions() {
    local target_file="$1"
    local tool_name="$2"
    if [ -f "$target_file" ] && grep -q "## PRE — Local AI Agent" "$target_file" 2>/dev/null; then
        ok "  PRE delegation instructions already in $tool_name instructions."
    else
        # Append with a blank line separator
        if [ -f "$target_file" ] && [ -s "$target_file" ]; then
            printf '\n\n%s\n' "$PRE_DELEGATION_BLOCK" >> "$target_file"
        else
            printf '%s\n' "$PRE_DELEGATION_BLOCK" > "$target_file"
        fi
        ok "  Wrote PRE delegation guidelines to $tool_name instructions."
    fi
}

# ── Claude ──────────────────────────────────────────────────────────────────

CLAUDE_DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
CLAUDE_DETECTED=0

# Claude Desktop
if [ -d "$HOME/Library/Application Support/Claude" ]; then
    CLAUDE_DETECTED=1
    echo "  Claude Desktop detected."
    echo ""
    echo -e "  PRE can serve as an MCP tool for Claude, letting Claude delegate"
    echo -e "  execution-heavy tasks to your local model ${BOLD}at zero token cost${RESET}."
    echo ""
    if ask_yn "  Add PRE to Claude Desktop? [Y/n] " "Y"; then
        if [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
            if grep -q '"pre"' "$CLAUDE_DESKTOP_CONFIG" 2>/dev/null; then
                ok "  PRE already configured in Claude Desktop."
            else
                node -e "
const fs = require('fs');
const p = '$CLAUDE_DESKTOP_CONFIG';
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(p, 'utf-8')); } catch {}
if (!cfg.mcpServers) cfg.mcpServers = {};
cfg.mcpServers.pre = { command: 'node', args: ['$MCP_STDIO_PATH'] };
fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
"
                ok "  PRE added to Claude Desktop config."
                warn "  Restart Claude Desktop to activate."
            fi
        else
            mkdir -p "$(dirname "$CLAUDE_DESKTOP_CONFIG")"
            echo "$MCP_SNIPPET" | node -e "process.stdout.write(JSON.stringify(JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8')),null,2)+'\n')" > "$CLAUDE_DESKTOP_CONFIG" 2>/dev/null \
                || echo "$MCP_SNIPPET" > "$CLAUDE_DESKTOP_CONFIG"
            ok "  Created Claude Desktop config with PRE."
            warn "  Restart Claude Desktop to activate."
        fi
        MCP_TOOLS_CONFIGURED=$((MCP_TOOLS_CONFIGURED + 1))
    else
        echo "  Skipped. You can add it later — see the README."
    fi
fi

# Claude Code
if [ -d "$HOME/.claude" ]; then
    CLAUDE_DETECTED=1
    ok "  Claude Code detected."
    echo ""
    echo -e "  ${BOLD}To use PRE from Claude Code:${RESET}"
    echo -e "  Add to your project ${CYAN}.mcp.json${RESET} or global settings:"
    echo ""
    echo -e "    ${DIM}{${RESET}"
    echo -e "    ${DIM}  \"mcpServers\": {${RESET}"
    echo -e "    ${DIM}    \"pre\": {${RESET}"
    echo -e "    ${DIM}      \"command\": \"node\",${RESET}"
    echo -e "    ${DIM}      \"args\": [\"${MCP_STDIO_PATH}\"]${RESET}"
    echo -e "    ${DIM}    }${RESET}"
    echo -e "    ${DIM}  }${RESET}"
    echo -e "    ${DIM}}${RESET}"
    echo ""
fi

# Write delegation instructions for Claude (CLAUDE.md in home dir)
if [ "$CLAUDE_DETECTED" -eq 1 ]; then
    CLAUDE_MD="$HOME/CLAUDE.md"
    write_delegation_instructions "$CLAUDE_MD" "CLAUDE.md"
fi

# ── Codex (OpenAI) ──────────────────────────────────────────────────────────

CODEX_CONFIG="$HOME/.codex/config.toml"
if [ -d "$HOME/.codex" ] || command -v codex &>/dev/null; then
    echo ""
    echo "  Codex detected."
    echo ""
    if ask_yn "  Add PRE as an MCP server for Codex? [Y/n] " "Y"; then
        # Add MCP server to config.toml
        if [ -f "$CODEX_CONFIG" ] && grep -q '\[mcp_servers\.pre\]' "$CODEX_CONFIG" 2>/dev/null; then
            ok "  PRE already configured in Codex."
        else
            # Append TOML block to config
            {
                echo ""
                echo "[mcp_servers.pre]"
                echo "command = \"node\""
                echo "args = [\"$MCP_STDIO_PATH\"]"
            } >> "$CODEX_CONFIG"
            ok "  PRE added to Codex config ($CODEX_CONFIG)."
            MCP_TOOLS_CONFIGURED=$((MCP_TOOLS_CONFIGURED + 1))
        fi

        # Write delegation instructions to Codex instructions file
        CODEX_INSTRUCTIONS="$HOME/.codex/instructions.md"
        write_delegation_instructions "$CODEX_INSTRUCTIONS" "Codex (instructions.md)"
    else
        echo "  Skipped. Add manually to ~/.codex/config.toml — see the README."
    fi
fi

# ── Antigravity (Google) ────────────────────────────────────────────────────

ANTIGRAVITY_SETTINGS="$HOME/.gemini/settings.json"
if [ -d "$HOME/.gemini" ] || [ -d "$HOME/.antigravity" ] || command -v agy &>/dev/null; then
    echo ""
    echo "  Antigravity detected."
    echo ""
    if ask_yn "  Add PRE as an MCP server for Antigravity? [Y/n] " "Y"; then
        # Merge PRE into settings.json mcpServers
        if [ -f "$ANTIGRAVITY_SETTINGS" ] && grep -q '"pre"' "$ANTIGRAVITY_SETTINGS" 2>/dev/null; then
            ok "  PRE already configured in Antigravity."
        else
            if command -v node &>/dev/null; then
                node -e "
const fs = require('fs');
const p = '$ANTIGRAVITY_SETTINGS';
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(p, 'utf-8')); } catch {}
if (!cfg.mcpServers) cfg.mcpServers = {};
cfg.mcpServers.pre = { command: 'node', args: ['$MCP_STDIO_PATH'] };
fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
"
                ok "  PRE added to Antigravity settings ($ANTIGRAVITY_SETTINGS)."
                MCP_TOOLS_CONFIGURED=$((MCP_TOOLS_CONFIGURED + 1))
            else
                warn "  Node.js needed to update Antigravity config — add manually."
            fi
        fi

        # Write delegation instructions to GEMINI.md
        GEMINI_MD="$HOME/.gemini/GEMINI.md"
        write_delegation_instructions "$GEMINI_MD" "Antigravity (GEMINI.md)"
    else
        echo "  Skipped. Add manually to ~/.gemini/settings.json — see the README."
    fi
fi

# ── Summary ─────────────────────────────────────────────────────────────────

echo ""
if [ "$MCP_TOOLS_CONFIGURED" -gt 0 ]; then
    ok "  Configured PRE as MCP tool for $MCP_TOOLS_CONFIGURED AI tool(s)."
fi
echo -e "  The MCP server auto-starts the inference backend and PRE — no manual launch needed."
echo -e "  See ${CYAN}web/README.md${RESET} for delegation guidelines and cost savings data."

# ============================================================================
# Step 7: Install pre-launch command + menu bar app
# ============================================================================
step "Installing pre-launch command + apps"

chmod +x "$ENGINE_DIR/pre-launch"
make -C "$ENGINE_DIR" install 2>&1

# Install PRE.app menu bar helper
if [ -d "$ENGINE_DIR/PRE.app" ] && [ -x "$ENGINE_DIR/PRE.app/Contents/MacOS/pre-menubar" ]; then
    mkdir -p "$HOME/Applications"
    rm -rf "$HOME/Applications/PRE.app"
    cp -R "$ENGINE_DIR/PRE.app" "$HOME/Applications/PRE.app"
    ok "  Installed PRE.app → ~/Applications/ (menu bar helper)"
    echo -e "  ${DIM}Start/stop PRE from the menu bar — no terminal needed.${RESET}"
fi

# Install PRE Desktop.app native wrapper
if [ -d "$ENGINE_DIR/PRE Desktop.app" ] && [ -x "$ENGINE_DIR/PRE Desktop.app/Contents/MacOS/pre-desktop" ]; then
    mkdir -p "$HOME/Applications"
    rm -rf "$HOME/Applications/PRE Desktop.app"
    cp -R "$ENGINE_DIR/PRE Desktop.app" "$HOME/Applications/PRE Desktop.app"
    ok "  Installed PRE Desktop.app → ~/Applications/ (native desktop app)"
    echo -e "  ${DIM}Full desktop app with dock icon — launch from Launchpad or Spotlight.${RESET}"
fi

# Ensure ~/.local/bin is in PATH
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
    SHELL_RC=""
    if [ -f "$HOME/.zshrc" ]; then
        SHELL_RC="$HOME/.zshrc"
    elif [ -f "$HOME/.bash_profile" ]; then
        SHELL_RC="$HOME/.bash_profile"
    elif [ -f "$HOME/.bashrc" ]; then
        SHELL_RC="$HOME/.bashrc"
    fi
    if [ -n "$SHELL_RC" ]; then
        if ! grep -q '.local/bin' "$SHELL_RC" 2>/dev/null; then
            echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_RC"
            ok "  Added ~/.local/bin to PATH in $SHELL_RC"
            warn "  Run 'source $SHELL_RC' or open a new terminal to use pre-launch."
        fi
    else
        warn "  Add to your shell profile: export PATH=\"\$HOME/.local/bin:\$PATH\""
    fi
fi

# ============================================================================
# Step 8: Set up ~/.pre/ directories
# ============================================================================
step "Setting up PRE data directories"

mkdir -p "$HOME/.pre/sessions"
mkdir -p "$HOME/.pre/memory"
mkdir -p "$HOME/.pre/memory/experience"
mkdir -p "$HOME/.pre/checkpoints"
mkdir -p "$HOME/.pre/artifacts"
mkdir -p "$HOME/.pre/rag"
mkdir -p "$HOME/.pre/workflows"
mkdir -p "$HOME/.pre/custom_tools"
mkdir -p "$HOME/.pre/training/exports"
mkdir -p "$HOME/.pre/agent-checkpoints"
ok "  Created ~/.pre/sessions/"
ok "  Created ~/.pre/memory/"
ok "  Created ~/.pre/memory/experience/"
ok "  Created ~/.pre/checkpoints/"
ok "  Created ~/.pre/artifacts/"
ok "  Created ~/.pre/rag/"
ok "  Created ~/.pre/workflows/"
ok "  Created ~/.pre/custom_tools/"
ok "  Created ~/.pre/training/exports/"
ok "  Created ~/.pre/agent-checkpoints/"

# Write context window config (read at runtime by constants.js and pre-launch)
echo "$OPTIMAL_CTX" > "$HOME/.pre/context"
ok "  Context window: ${CTX_HUMAN} → ~/.pre/context"

# Write provider configuration for llama.cpp backend
if [ "$BACKEND" = "llamacpp" ]; then
    CONN_FILE="$HOME/.pre/connections.json"
    if command -v node &>/dev/null; then
        node -e "
const fs = require('fs');
const p = '$CONN_FILE';
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(p, 'utf-8')); } catch {}
if (!cfg._provider) cfg._provider = {};
cfg._provider.active = 'llamacpp';
cfg._provider.llamacpp = {
  base_url: 'http://127.0.0.1:${LLAMACPP_PORT:-8080}',
  port: ${LLAMACPP_PORT:-8080},
  model_path: '${GGUF_MODEL_PATH:-}',
  draft_model_path: '',
  grammar_enabled: true,
  flash_attn: true,
  n_gpu_layers: -1,
  context_size: $OPTIMAL_CTX,
  parallel: 1
};
fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
"
        ok "  Provider configured: llama.cpp → ~/.pre/connections.json"
    else
        warn "  Node.js needed to write provider config — configure manually in Web GUI settings."
    fi
fi

# Create default config files if they don't exist
if [ ! -f "$HOME/.pre/hooks.json" ]; then
    echo '{"hooks":[]}' > "$HOME/.pre/hooks.json"
    ok "  Created ~/.pre/hooks.json"
fi
if [ ! -f "$HOME/.pre/mcp.json" ]; then
    echo '{"servers":{}}' > "$HOME/.pre/mcp.json"
    ok "  Created ~/.pre/mcp.json"
fi
if [ ! -f "$HOME/.pre/cron.json" ]; then
    echo '[]' > "$HOME/.pre/cron.json"
    ok "  Created ~/.pre/cron.json"
fi
if [ ! -f "$HOME/.pre/triggers.json" ]; then
    echo '[]' > "$HOME/.pre/triggers.json"
    ok "  Created ~/.pre/triggers.json"
fi

# Migrate from old Flash-MoE layout if present
if [ -d "$HOME/.flash-moe" ] && [ ! -f "$HOME/.pre/.migrated" ]; then
    if [ -f "$HOME/.flash-moe/pre_history" ]; then
        cp "$HOME/.flash-moe/pre_history" "$HOME/.pre/pre_history" 2>/dev/null || true
        ok "  Migrated command history from ~/.flash-moe/"
    fi
    touch "$HOME/.pre/.migrated"
fi

# ============================================================================
# Step 8b: ComfyUI setup (optional — local image generation)
# ============================================================================
step "Image Generation Setup (optional)"

COMFYUI_DIR="$HOME/.pre/comfyui"
COMFYUI_VENV="$HOME/.pre/comfyui-venv"
COMFYUI_CONFIG="$HOME/.pre/comfyui.json"

# Checkpoint options
TURBO_CHECKPOINT="sd_xl_turbo_1.0_fp16.safetensors"
TURBO_URL="https://huggingface.co/stabilityai/sdxl-turbo/resolve/main/sd_xl_turbo_1.0_fp16.safetensors"
QUALITY_CHECKPOINT="Juggernaut-XL_v9_RunDiffusionPhoto_v2.safetensors"
QUALITY_REPO="RunDiffusion/Juggernaut-XL-v9"

if [ -f "$COMFYUI_CONFIG" ]; then
    ok "  ComfyUI already configured — skipping."
else
    # Check available RAM
    RAM_GB=$(sysctl -n hw.memsize 2>/dev/null | awk '{printf "%.0f", $1/1073741824}')
    if [ "$RAM_GB" -lt 64 ] 2>/dev/null; then
        warn "  Note: Your system has ${RAM_GB}GB RAM. Image generation (SDXL + Gemma 4 q8_0) works"
        warn "  best with 64GB+. You can skip this and add it later with /connections."
    fi

    echo ""
    echo -e "  ${BOLD}Install local image generation?${RESET}"
    echo -e "  This sets up ComfyUI + Stable Diffusion XL for generating images from text."
    echo -e "  ${DIM}Requires: ~8GB disk, Python 3.10-3.13 (PyTorch), ~6.5GB model download${RESET}"
    echo ""
    if ask_yn "  Install ComfyUI? [y/N] " "N"; then
        # Find a compatible Python (3.10-3.13; 3.14+ lacks PyTorch support)
        COMFYUI_PYTHON=""
        for pyver in python3.12 python3.13 python3.11 python3.10; do
            if command -v "$pyver" &>/dev/null; then
                COMFYUI_PYTHON="$pyver"
                break
            fi
        done
        # Fallback to python3 if it's in the supported range
        if [ -z "$COMFYUI_PYTHON" ] && command -v python3 &>/dev/null; then
            PY_MINOR=$(python3 -c 'import sys; print(sys.version_info.minor)')
            if [ "$PY_MINOR" -ge 10 ] && [ "$PY_MINOR" -le 13 ]; then
                COMFYUI_PYTHON="python3"
            fi
        fi

        if [ -z "$COMFYUI_PYTHON" ]; then
            warn "  Python 3.10-3.13 not found — skipping ComfyUI. PyTorch requires Python <=3.13."
            warn "  Install Python 3.12 (brew install python@3.12) and re-run."
        else
            PYTHON_VERSION=$($COMFYUI_PYTHON -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
            ok "  Python $PYTHON_VERSION found ($COMFYUI_PYTHON)"

            # Create virtual environment
            VENV_OK=true
            echo "  Creating Python virtual environment..."
            $COMFYUI_PYTHON -m venv "$COMFYUI_VENV" || { warn "  Failed to create venv — skipping."; VENV_OK=false; }

            if [ "$VENV_OK" = true ]; then
                # Install PyTorch with MPS support
                echo "  Installing PyTorch (Metal/MPS)..."
                "$COMFYUI_VENV/bin/pip" install --quiet torch torchvision torchaudio 2>&1 | tail -1
                ok "  PyTorch installed with MPS support"

                # Clone ComfyUI
                if [ ! -d "$COMFYUI_DIR" ]; then
                    echo "  Cloning ComfyUI..."
                    git clone --depth 1 https://github.com/comfyanonymous/ComfyUI "$COMFYUI_DIR" 2>&1 | tail -1
                fi
                ok "  ComfyUI cloned"

                # Install ComfyUI requirements
                echo "  Installing ComfyUI dependencies..."
                "$COMFYUI_VENV/bin/pip" install --quiet -r "$COMFYUI_DIR/requirements.txt" 2>&1 | tail -1
                ok "  Dependencies installed"

                # Choose checkpoint: Quality (Juggernaut XL) or Speed (SDXL Turbo)
                CKPT_DIR="$COMFYUI_DIR/models/checkpoints"
                mkdir -p "$CKPT_DIR"

                echo ""
                echo -e "  ${BOLD}Choose image model:${RESET}"
                echo -e "  ${CYAN}1)${RESET} ${BOLD}Juggernaut XL${RESET} (recommended) — Photorealistic, excellent faces"
                echo -e "     ~30-45s per image, 25 steps at 1024x1024"
                echo -e "  ${CYAN}2)${RESET} ${BOLD}SDXL Turbo${RESET} — Fast but lower quality"
                echo -e "     ~5s per image, 4 steps at 512x512"
                echo ""
                IMG_CHOICE="1"
                if [ "$AUTO_YES" = false ]; then
                    read -p "  Choice [1/2, default=1]: " -n 1 -r IMG_CHOICE
                    echo ""
                fi

                SELECTED_CHECKPOINT=""
                if [ "$IMG_CHOICE" = "2" ]; then
                    # SDXL Turbo (speed)
                    if [ -f "$CKPT_DIR/$TURBO_CHECKPOINT" ]; then
                        ok "  SDXL Turbo checkpoint already downloaded"
                    else
                        echo "  Downloading SDXL Turbo (~6.5GB)..."
                        if command -v wget &>/dev/null; then
                            wget -q --show-progress -O "$CKPT_DIR/$TURBO_CHECKPOINT" "$TURBO_URL"
                        else
                            curl -L --progress-bar -o "$CKPT_DIR/$TURBO_CHECKPOINT" "$TURBO_URL"
                        fi
                    fi
                    if [ -f "$CKPT_DIR/$TURBO_CHECKPOINT" ]; then
                        CKPT_SIZE=$(du -sh "$CKPT_DIR/$TURBO_CHECKPOINT" | cut -f1)
                        ok "  SDXL Turbo downloaded ($CKPT_SIZE)"
                        SELECTED_CHECKPOINT="$TURBO_CHECKPOINT"
                    else
                        warn "  Download failed — you can download manually later."
                    fi
                else
                    # Juggernaut XL (quality — default)
                    if [ -f "$CKPT_DIR/$QUALITY_CHECKPOINT" ]; then
                        ok "  Juggernaut XL checkpoint already downloaded"
                        SELECTED_CHECKPOINT="$QUALITY_CHECKPOINT"
                    else
                        echo "  Downloading Juggernaut XL v9 (~6.6GB)..."
                        if command -v huggingface-cli &>/dev/null; then
                            huggingface-cli download "$QUALITY_REPO" "$QUALITY_CHECKPOINT" \
                                --local-dir "$CKPT_DIR" 2>&1 | tail -3
                            # Clean up HF cache
                            rm -rf "$CKPT_DIR/.cache" 2>/dev/null
                        elif command -v wget &>/dev/null; then
                            wget -q --show-progress -O "$CKPT_DIR/$QUALITY_CHECKPOINT" \
                                "https://huggingface.co/$QUALITY_REPO/resolve/main/$QUALITY_CHECKPOINT"
                        else
                            curl -L --progress-bar -o "$CKPT_DIR/$QUALITY_CHECKPOINT" \
                                "https://huggingface.co/$QUALITY_REPO/resolve/main/$QUALITY_CHECKPOINT"
                        fi
                    fi
                    if [ -f "$CKPT_DIR/$QUALITY_CHECKPOINT" ]; then
                        CKPT_SIZE=$(du -sh "$CKPT_DIR/$QUALITY_CHECKPOINT" | cut -f1)
                        ok "  Juggernaut XL downloaded ($CKPT_SIZE)"
                        SELECTED_CHECKPOINT="$QUALITY_CHECKPOINT"
                    else
                        warn "  Download failed — falling back to SDXL Turbo..."
                        echo "  Downloading SDXL Turbo (~6.5GB)..."
                        if command -v wget &>/dev/null; then
                            wget -q --show-progress -O "$CKPT_DIR/$TURBO_CHECKPOINT" "$TURBO_URL"
                        else
                            curl -L --progress-bar -o "$CKPT_DIR/$TURBO_CHECKPOINT" "$TURBO_URL"
                        fi
                        if [ -f "$CKPT_DIR/$TURBO_CHECKPOINT" ]; then
                            SELECTED_CHECKPOINT="$TURBO_CHECKPOINT"
                            ok "  SDXL Turbo downloaded as fallback"
                        else
                            warn "  Both downloads failed. You can download manually later."
                        fi
                    fi
                fi

                if [ -n "$SELECTED_CHECKPOINT" ]; then

                    # Create config file
                    cat > "$COMFYUI_CONFIG" << CFGEOF
{
  "installed": true,
  "port": 8188,
  "checkpoint": "$SELECTED_CHECKPOINT",
  "venv": "~/.pre/comfyui-venv",
  "comfyui_dir": "~/.pre/comfyui"
}
CFGEOF
                    ok "  ComfyUI configuration saved (using $SELECTED_CHECKPOINT)"
                    echo ""
                    ok "  Image generation ready! PRE will start ComfyUI automatically when needed."
                    echo -e "  ${DIM}Tip: To switch models later, edit ~/.pre/comfyui.json${RESET}"
                fi
            fi
        fi
    else
        echo "  Skipped. You can install later by re-running this script."
    fi
fi

# ============================================================================
# Step 8c: Computer Use setup (optional — desktop automation)
# ============================================================================
step "Desktop Automation Setup (optional)"

echo -e "  ${BOLD}Computer Use${RESET} lets PRE see your screen and control any application"
echo -e "  — click, type, scroll, drag, press key combos. It uses ${CYAN}cliclick${RESET}"
echo -e "  for mouse/keyboard control and ${CYAN}screencapture${RESET} (built-in) for screenshots."
echo ""

if command -v cliclick &>/dev/null; then
    ok "  cliclick already installed: $(cliclick -V 2>&1 | head -1)"
    echo -e "  ${DIM}macOS will prompt for Accessibility permissions on first use.${RESET}"
else
    if command -v brew &>/dev/null; then
        if ask_yn "  Install cliclick for desktop automation? [Y/n] " "Y"; then
            echo "  Installing cliclick..."
            brew install cliclick --quiet 2>&1 | tail -1
            if command -v cliclick &>/dev/null; then
                ok "  cliclick installed."
                echo -e "  ${DIM}macOS will prompt for Accessibility permissions on first use.${RESET}"
            else
                warn "  cliclick install failed — Computer Use will be unavailable."
                echo -e "  ${DIM}Install later with: brew install cliclick${RESET}"
            fi
        else
            echo "  Skipped. Install later with: brew install cliclick"
        fi
    else
        warn "  Homebrew not found — install cliclick manually for Computer Use."
        echo -e "  ${DIM}brew install cliclick${RESET}"
    fi
fi

# ============================================================================
# Step 8d: Voice interface setup (optional — speech-to-text)
# ============================================================================
step "Voice Interface Setup (optional)"

echo -e "  ${BOLD}Voice input${RESET} lets you talk to PRE — hold the microphone button in the"
echo -e "  web GUI to record, release to transcribe. Uses ${CYAN}OpenAI Whisper${RESET} (local,"
echo -e "  no cloud API) for speech-to-text and macOS ${CYAN}say${RESET} for text-to-speech."
echo ""

VOICE_CHANGED=false

if command -v whisper &>/dev/null; then
    ok "  Whisper already installed: $(whisper --version 2>&1 || echo 'available')"
else
    if command -v brew &>/dev/null; then
        if ask_yn "  Install Whisper for voice input? [Y/n] " "Y"; then
            echo "  Installing OpenAI Whisper via Homebrew (~46MB + dependencies)..."
            echo -e "  ${DIM}This may take a minute — Homebrew manages the Python environment.${RESET}"
            brew install openai-whisper --quiet 2>&1 | tail -3
            if command -v whisper &>/dev/null; then
                ok "  Whisper installed."
                VOICE_CHANGED=true
            else
                warn "  Whisper install failed."
                echo -e "  ${DIM}Install later with: brew install openai-whisper${RESET}"
            fi
        else
            echo "  Skipped. Install later with: brew install openai-whisper"
        fi
    else
        warn "  Homebrew not found — install Whisper manually for voice input."
        echo -e "  ${DIM}brew install openai-whisper${RESET}"
    fi
fi

# FFmpeg improves audio format handling (WebM → WAV conversion)
if command -v ffmpeg &>/dev/null; then
    ok "  FFmpeg available (audio format conversion)"
else
    if command -v brew &>/dev/null; then
        if ask_yn "  Install FFmpeg for audio format conversion? [Y/n] " "Y"; then
            echo "  Installing FFmpeg..."
            brew install ffmpeg --quiet 2>&1 | tail -1
            if command -v ffmpeg &>/dev/null; then
                ok "  FFmpeg installed."
                VOICE_CHANGED=true
            else
                warn "  FFmpeg install failed — voice input will still work, but some audio formats may not convert."
            fi
        else
            echo "  Skipped. Voice input works without FFmpeg, but some audio formats may not convert."
        fi
    fi
fi

if [ "$VOICE_CHANGED" = true ]; then
    echo ""
    ok "  Voice interface ready! Look for the microphone icon in the web GUI."
    echo -e "  ${DIM}Whisper downloads its model (~140MB) on first transcription.${RESET}"
fi

# ============================================================================
# Step 8e: tinygrad setup (optional — eGPU inference + training)
# ============================================================================
if [ "$EGPU_DETECTED" = true ] && [ "$EGPU_VENDOR" = "nvidia" ]; then
    step "tinygrad Setup (eGPU inference + training)"

    echo -e "  ${BOLD}tinygrad${RESET} enables GGUF model inference (Llama, Qwen, DeepSeek) and LoRA fine-tuning on your eGPU."
    echo -e "  PRE auto-detects your eGPU at runtime — tinygrad activates when GPU is available."
    echo ""

    # Check for Python 3.11+ (tinygrad requirement)
    TINYGRAD_PYTHON=""
    for PY_CMD in python3.13 python3.12 python3.11 python3; do
        if command -v "$PY_CMD" &>/dev/null; then
            PY_VER=$($PY_CMD --version 2>&1 | grep -oE '[0-9]+\.[0-9]+')
            PY_MAJOR=$(echo "$PY_VER" | cut -d. -f1)
            PY_MINOR=$(echo "$PY_VER" | cut -d. -f2)
            if [ "$PY_MAJOR" -ge 3 ] && [ "$PY_MINOR" -ge 11 ]; then
                TINYGRAD_PYTHON=$(command -v "$PY_CMD")
                break
            fi
        fi
    done

    if [ -z "$TINYGRAD_PYTHON" ]; then
        warn "  Python 3.11+ not found — tinygrad requires Python 3.11 or newer."
        echo -e "  ${DIM}Install via: brew install python@3.12${RESET}"
        echo -e "  ${DIM}Then re-run the installer to set up tinygrad.${RESET}"
    else
        ok "  Python: $($TINYGRAD_PYTHON --version 2>&1) ($TINYGRAD_PYTHON)"

        # Check if tinygrad is already installed
        if $TINYGRAD_PYTHON -c "import tinygrad" 2>/dev/null; then
            TINYGRAD_VER=$($TINYGRAD_PYTHON -c "import tinygrad; print(tinygrad.__version__)" 2>/dev/null || echo "unknown")
            ok "  tinygrad already installed (v${TINYGRAD_VER})"
        else
            echo "  Installing tinygrad..."
            $TINYGRAD_PYTHON -m pip install tinygrad --quiet 2>&1 | tail -3
            if $TINYGRAD_PYTHON -c "import tinygrad" 2>/dev/null; then
                TINYGRAD_VER=$($TINYGRAD_PYTHON -c "import tinygrad; print(tinygrad.__version__)" 2>/dev/null || echo "unknown")
                ok "  tinygrad installed (v${TINYGRAD_VER})"
            else
                warn "  tinygrad installation failed. Install manually: pip3 install tinygrad"
            fi
        fi

        # Install server dependencies (transformers for tokenizer, bottle for HTTP, jinja2 for chat templates)
        echo "  Installing tinygrad server dependencies..."
        $TINYGRAD_PYTHON -m pip install transformers bottle jinja2 --quiet 2>&1 | tail -3
        if $TINYGRAD_PYTHON -c "import transformers, bottle, jinja2" 2>/dev/null; then
            ok "  Server dependencies installed (transformers, bottle, jinja2)"
        else
            warn "  Some dependencies missing. Install manually: pip3 install transformers bottle jinja2"
        fi

        # Verify GPU access via tinygrad
        TINYGRAD_DEV=$(DEV=NV $TINYGRAD_PYTHON -c "from tinygrad import Device; print(Device.DEFAULT)" 2>/dev/null || true)
        if echo "$TINYGRAD_DEV" | grep -qi "NV\|CUDA"; then
            ok "  GPU access verified via tinygrad (device: $TINYGRAD_DEV)"
            # Write status marker for PRE runtime
            echo "{\"tinygrad\":true,\"python\":\"$TINYGRAD_PYTHON\",\"device\":\"$TINYGRAD_DEV\",\"verified\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
                > "$HOME/.pre/tinygrad-status.json"
        else
            warn "  tinygrad installed but GPU not accessible via DEV=NV (device: ${TINYGRAD_DEV:-none})"
            echo -e "  ${DIM}Ensure Docker Desktop is running (required for NVIDIA CUDA via TinyGPU on macOS).${RESET}"
        fi
    fi
fi

# ============================================================================
# Step 9: Pre-warm the model
# ============================================================================
if [ "$BACKEND" = "ollama" ] && command -v ollama &>/dev/null; then
step "Pre-warming model into GPU memory"

echo "  Loading $CUSTOM_MODEL into GPU with ${CTX_HUMAN} context (this takes 30-60 seconds)..."
# Send a real message with full num_ctx to force Ollama to allocate the complete KV cache.
# An empty messages array defers KV allocation to the first real request.
curl -sf --max-time 300 "http://localhost:${PORT}/api/chat" \
    -d "{\"model\":\"$CUSTOM_MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"keep_alive\":\"24h\",\"options\":{\"num_predict\":1,\"num_ctx\":$OPTIMAL_CTX}}" \
    >/dev/null 2>&1 || true

for i in $(seq 1 60); do
    if ollama ps 2>/dev/null | grep -q "$CUSTOM_MODEL"; then
        MEM=$(ollama ps 2>/dev/null | grep "$CUSTOM_MODEL" | awk '{print $4}')
        ok "  Model loaded into GPU memory (${MEM})."
        break
    fi
    sleep 1
done

if ! ollama ps 2>/dev/null | grep -q "$CUSTOM_MODEL"; then
    warn "  Model may not be fully loaded yet — it will load on first launch."
fi
fi

# ============================================================================
# Step 10: Auto-start at login (optional LaunchAgent)
# ============================================================================
step "Auto-start at login (optional)"

PLIST_LABEL="com.pre.server"
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
PRE_SERVER_SH="$WEB_DIR/pre-server.sh"
NODE_BIN=$(command -v node 2>/dev/null || echo "/opt/homebrew/bin/node")

echo -e "  PRE can start automatically when you log in, so the server and"
echo -e "  model are always ready. The web GUI, MCP server, and cron jobs"
echo -e "  will all be available immediately — no manual launch needed."
echo ""

if [ -f "$PLIST_PATH" ]; then
    ok "  LaunchAgent already installed ($PLIST_LABEL)."
    if ask_yn "  Reinstall with current paths? [y/N] " "N"; then
        launchctl unload "$PLIST_PATH" 2>/dev/null || true
    else
        echo "  Keeping existing LaunchAgent."
        # Skip to Done
        SKIP_LAUNCHAGENT=true
    fi
fi

if [ "${SKIP_LAUNCHAGENT:-}" != "true" ]; then
    if ask_yn "  Start PRE automatically at login? [Y/n] " "Y"; then
        chmod +x "$PRE_SERVER_SH"
        mkdir -p "$HOME/Library/LaunchAgents"

        # Build optional CUDA env var block for LaunchAgent plist
        PLIST_GPU_OVERHEAD=""
        if [ "$GPU_BACKEND" = "cuda" ]; then
            PLIST_GPU_OVERHEAD="
        <key>OLLAMA_GPU_OVERHEAD</key>
        <string>256000000</string>"
        fi

        cat > "$PLIST_PATH" << PLISTEOF
<?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>${PLIST_LABEL}</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>${PRE_SERVER_SH}</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
    <key>WorkingDirectory</key>
    <string>${WEB_DIR}</string>
    <key>StandardOutPath</key>
    <string>/tmp/pre-server.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/pre-server.log</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
        <key>HOME</key>
        <string>${HOME}</string>
        <key>OLLAMA_FLASH_ATTENTION</key>
        <string>${FA_VAL}</string>
        <key>OLLAMA_KV_CACHE_TYPE</key>
        <string>${KV_CACHE_TYPE}</string>
        <key>OLLAMA_KEEP_ALIVE</key>
        <string>24h</string>
        <key>OLLAMA_NUM_PARALLEL</key>
        <string>${NUM_PARALLEL}</string>
        <key>OLLAMA_MAX_LOADED_MODELS</key>
        <string>2</string>${PLIST_GPU_OVERHEAD}
    </dict>
</dict>
</plist>
PLISTEOF

        # Load the LaunchAgent (starts immediately)
        launchctl load "$PLIST_PATH" 2>/dev/null || true

        ok "  LaunchAgent installed — PRE will start automatically at login."
        echo -e "  ${DIM}Server log: /tmp/pre-server.log${RESET}"
        echo -e "  ${DIM}To stop:    pre-server.sh --stop${RESET}"
        echo -e "  ${DIM}To disable: launchctl unload ~/Library/LaunchAgents/${PLIST_LABEL}.plist${RESET}"
    else
        echo "  Skipped. Launch PRE manually with: pre-launch"
    fi
fi

# ============================================================================
# Done!
# ============================================================================
echo ""
echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}${GREEN}║${RESET}${BOLD}  PRE installation complete!                              ${GREEN}║${RESET}"
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════╝${RESET}"
echo ""
if [ -f "$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist" ]; then
echo -e "  ${BOLD}Launch:${RESET}  ${GREEN}PRE starts automatically at login${RESET}"
echo -e "    Web GUI is at ${CYAN}http://localhost:7749${RESET}"
echo -e "    ${CYAN}pre-launch${RESET}                  Open CLI (server already running)"
echo -e "    ${CYAN}pre-launch --show-think${RESET}     CLI with visible reasoning"
echo -e "    ${CYAN}pre-server.sh --status${RESET}      Check server status"
echo -e "    ${CYAN}pre-server.sh --stop${RESET}        Stop the server"
else
echo -e "  ${BOLD}Launch:${RESET}"
echo -e "    ${CYAN}pre-launch${RESET}                  Start PRE (CLI + Web GUI)"
echo -e "    ${CYAN}pre-launch --show-think${RESET}     Start with visible reasoning"
echo -e "    Web GUI auto-starts at ${CYAN}http://localhost:7749${RESET}"
fi
if [ -d "$HOME/Applications/PRE.app" ]; then
echo -e "    ${GREEN}PRE.app${RESET}                     Menu bar helper (~/Applications/)"
echo -e "    ${DIM}Start, stop, restart, and open PRE from the menu bar.${RESET}"
fi
if [ -d "$HOME/Applications/PRE Desktop.app" ]; then
echo -e "    ${GREEN}PRE Desktop.app${RESET}             Native desktop app with dock icon"
echo -e "    ${DIM}Launch from Spotlight, Launchpad, or the Dock.${RESET}"
fi
echo ""
echo -e "  ${BOLD}Hardware Profile:${RESET}"
echo -e "    ${RAM_GB}GB unified memory → ${GREEN}${CTX_HUMAN} context window${RESET} (${OPTIMAL_CTX} tokens)"
echo -e "    ${DIM}To change: edit ~/.pre/context and restart PRE${RESET}"
echo ""
echo -e "  ${BOLD}First Launch:${RESET}"
echo -e "    PRE will ask you to name your agent and optionally"
echo -e "    configure cloud connections via ${BOLD}/connections${RESET} or Web GUI Settings."
echo ""
echo -e "  ${BOLD}Native macOS (works immediately — no setup):${RESET}"
echo -e "    • ${GREEN}Mail, Calendar, Contacts, Reminders, Notes, Spotlight${RESET}"
echo -e "    ${DIM}Uses your existing macOS accounts (iCloud, Gmail, Exchange, etc.)${RESET}"
echo -e "    ${DIM}macOS will prompt for Automation permissions on first use.${RESET}"
echo ""
echo -e "  ${BOLD}Cloud Connections (optional):${RESET}"
echo -e "    Type ${BOLD}/connections${RESET} inside PRE or use the Web GUI Settings to set up:"
echo -e "    • ${DIM}Google         — Gmail, Drive, Docs (built-in OAuth)${RESET}"
echo -e "    • ${DIM}Microsoft      — SharePoint, OneDrive (Azure AD OAuth)${RESET}"
echo -e "    • ${DIM}Slack          — channel messaging${RESET}"
echo -e "    • ${DIM}Telegram       — chat from your phone + cron delivery${RESET}"
echo -e "    • ${DIM}GitHub         — repos, issues, PRs${RESET}"
echo -e "    • ${DIM}Jira           — issues, projects, transitions${RESET}"
echo -e "    • ${DIM}Confluence     — wiki pages, search${RESET}"
echo -e "    • ${DIM}Smartsheet     — spreadsheets, rows, search${RESET}"
echo -e "    • ${DIM}Linear         — issue tracking, projects, cycles${RESET}"
echo -e "    • ${DIM}Zoom           — meetings, recordings${RESET}"
echo -e "    • ${DIM}Figma          — design files, comments, export${RESET}"
echo -e "    • ${DIM}Asana          — tasks, projects, search${RESET}"
echo -e "    • ${DIM}Brave Search   — web search${RESET}"
echo -e "    • ${DIM}Wolfram Alpha  — computation${RESET}"
if [ -f "$HOME/.pre/comfyui.json" ]; then
echo -e "    • ${GREEN}ComfyUI        — image generation (installed ✓)${RESET}"
else
echo -e "    • ${DIM}ComfyUI        — image generation (re-run install.sh to add)${RESET}"
fi
echo ""
echo -e "  ${BOLD}Features:${RESET}"
echo -e "    70+ tools including computer use (desktop automation), local RAG,"
echo -e "    event-driven triggers, voice interface, workflow capture and replay,"
echo -e "    sub-agents, browser automation, document export, hooks, experience"
echo -e "    ledger, and temporal memory awareness."
echo ""
echo -e "  ${BOLD}MCP Integration:${RESET}"
echo -e "    PRE serves as an MCP tool for Claude, Codex, Antigravity, and other AI tools."
echo -e "    Delegate execution-heavy tasks to your local model at ${GREEN}zero token cost${RESET}."
echo -e "    Delegation instructions auto-written to CLAUDE.md, instructions.md, GEMINI.md."
echo -e "    See ${CYAN}web/README.md${RESET} for setup and cost savings data."
echo ""
echo -e "  ${BOLD}Data:${RESET}"
echo -e "    ${DIM}~/.pre/identity.json${RESET}      Agent name"
echo -e "    ${DIM}~/.pre/sessions/${RESET}          Session history (shared by CLI + Web)"
echo -e "    ${DIM}~/.pre/memory/${RESET}            Persistent memory"
echo -e "    ${DIM}~/.pre/memory/experience/${RESET} Experience ledger (lessons learned)"
echo -e "    ${DIM}~/.pre/artifacts/${RESET}         Generated documents, images & artifacts"
echo -e "    ${DIM}~/.pre/cron.json${RESET}          Scheduled recurring tasks"
echo -e "    ${DIM}~/.pre/hooks.json${RESET}         Pre/post tool execution hooks"
echo -e "    ${DIM}~/.pre/mcp.json${RESET}           MCP server configuration"
echo -e "    ${DIM}~/.pre/triggers.json${RESET}      Event-driven triggers"
echo -e "    ${DIM}~/.pre/argus.json${RESET}         Argus companion config"
echo -e "    ${DIM}~/.pre/custom_tools/${RESET}      Dynamic virtual tool definitions"
echo -e "    ${DIM}~/.pre/rag/${RESET}               RAG indexes (semantic search)"
echo -e "    ${DIM}~/.pre/workflows/${RESET}          Recorded workflow sequences"
echo -e "    ${DIM}~/.pre/telegram.log${RESET}       Telegram bot log"
if ! command -v cliclick &>/dev/null || ! command -v whisper &>/dev/null; then
echo -e "  ${BOLD}Skipped Optional Features:${RESET}"
if ! command -v cliclick &>/dev/null; then
echo -e "    ${DIM}brew install cliclick${RESET}           Desktop automation (Computer Use)"
fi
if ! command -v whisper &>/dev/null; then
echo -e "    ${DIM}brew install openai-whisper${RESET}     Voice input (speech-to-text)"
fi
if ! command -v ffmpeg &>/dev/null; then
echo -e "    ${DIM}brew install ffmpeg${RESET}             Audio format conversion"
fi
echo -e "    ${DIM}Re-run install.sh to add these interactively.${RESET}"
fi
echo ""
echo -e "  Type ${BOLD}/help${RESET} inside PRE for all commands."
echo ""

# Keep Terminal window open if launched via Finder double-click
if [ "$AUTO_YES" = false ] && [ -t 0 ]; then
    read -n 1 -s -r -p "  Press any key to close this window."
    echo ""
fi
