#!/usr/bin/env bash
set -e
set -o pipefail  # Ensure pipeline failures are caught (critical for nixos-rebuild | nom)

# Structured event log + step helpers. Sourced early — before cleanup() and its
# trap below — so the trap can emit fail events / write last-status even if the
# script aborts during argument parsing or setup. Shared with the unit test
# (tests/bash/rebuild-nixos/test-event-log.bats) so the two never drift.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/event-log.sh
source "$SCRIPT_DIR/lib/event-log.sh"

# Cleanup handler for interrupts (Ctrl+C) and errors
TEMP_FILES=()
# shellcheck disable=SC2329  # invoked indirectly via trap
cleanup() {
    local exit_code=$?
    # Remove any temp files we created
    for f in "${TEMP_FILES[@]}"; do
        rm -f "$f" 2>/dev/null
    done
    # Remove common temp file patterns
    rm -f /tmp/nixos-build-$$ /tmp/nixbuild-output-$$.tmp /tmp/changelog-draft-$$.md /tmp/changelog-updated-$$.md /tmp/gc-output-$$.tmp 2>/dev/null
    if [ "$exit_code" -ne 0 ] && [ "$exit_code" -ne 130 ]; then
        echo ""
        if [ "${REBUILD_ACTIVATED:-false}" = true ]; then
            if [ "${BOOT_MODE:-false}" = true ]; then
                echo -e "\033[1;33m⚠️  Post-rebuild step had issues (exit code: $exit_code) — build staged for next boot successfully\033[0m"
            else
                echo -e "\033[1;33m⚠️  Post-rebuild step had issues (exit code: $exit_code) — system was activated successfully\033[0m"
            fi
            # Activation succeeded; only a post-rebuild step (changelog, GC, etc.)
            # failed. Tag the fail event with post_rebuild_warning so consumers
            # can disambiguate, but keep last-status=succ so the statusline
            # doesn't claim the rebuild itself failed.
            log_event fail "${CURRENT_PHASE_NAME:-unknown}" "{\"exit_code\":$exit_code,\"post_rebuild_warning\":true}"
            write_last_status succ
        else
            echo -e "\033[0;31m❌ Rebuild failed (exit code: $exit_code)\033[0m"
            # Emit fail event tagged with the phase that was in flight (if any),
            # then mark last-status so statusline reflects the failure.
            log_event fail "${CURRENT_PHASE_NAME:-unknown}" "{\"exit_code\":$exit_code}"
            write_last_status fail
        fi
    elif [ "$exit_code" -eq 130 ]; then
        # SIGINT: caller hit Ctrl+C. Don't overwrite last-status (per spec)
        # but still record the cancel as a structured event.
        log_event fail "${CURRENT_PHASE_NAME:-unknown}" '{"exit_code":130,"reason":"sigint"}'
    fi
    exit "$exit_code"
}
trap cleanup EXIT INT TERM

# Mark that we're running inside the rebuild-nixos wrapper
# This allows Claude Code hooks to permit nixos-rebuild calls from within this script
export NIXOS_REBUILD_WRAPPER=1

# Source user secrets from environment.d if not already in environment
# This handles the case where the script is run before re-login after adding secrets
if [ -z "$ANTHROPIC_API_KEY" ] && [ -f ~/.config/environment.d/50-secrets.conf ]; then
    # shellcheck source=/dev/null
    source ~/.config/environment.d/50-secrets.conf
    # Export so subprocesses (like nix run) can see it
    export ANTHROPIC_API_KEY
fi

# Parse command line arguments
VERBOSE=false
DRY_RUN=false
QUICK=false
YES=false
AUDIT=false
VERIFY_BOOTSTRAP=false
FRESH=false
BOOT_MODE=false
GC_ONLY=false

while [[ $# -gt 0 ]]; do
    case "$1" in
        --verbose|-v) VERBOSE=true; shift ;;
        --dry-run|-n) DRY_RUN=true; shift ;;
        --quick|-q) QUICK=true; shift ;;
        --yes|-y) YES=true; shift ;;
        --audit|-a) AUDIT=true; shift ;;
        --verify-bootstrap) VERIFY_BOOTSTRAP=true; shift ;;
        --fresh|-f) FRESH=true; shift ;;
        --boot|-b) BOOT_MODE=true; shift ;;
        --gc|-g) GC_ONLY=true; shift ;;
        --help|-h)
            echo "Usage: rebuild-nixos [OPTIONS]"
            echo ""
            echo "Options:"
            echo "  -v, --verbose    Show detailed output during build"
            echo "  -n, --dry-run    Show what would happen without making changes"
            echo "  -q, --quick      Skip changelog and cleanup (fastest rebuild)"
            echo "  -y, --yes        Auto-accept all prompts (non-interactive)"
            echo "  -f, --fresh      Clear and bypass eval-cache (use when changes seem ignored)"
            echo "  -a, --audit      Export source closure for forensic audit trail (~5GB)"
            echo "  -b, --boot       Stage for next boot instead of live activation"
            echo "                   (use when switch inhibitor blocks dbus/init/glibc swap)"
            echo "  -g, --gc         Skip rebuild — run generation cleanup only"
            echo "  --verify-bootstrap  Deep reproducibility check of bootstrap packages"
            echo "  -h, --help       Show this help message"
            echo ""
            echo "Examples:"
            echo "  ./rebuild-nixos              # Full rebuild"
            echo "  ./rebuild-nixos --gc         # Clean old generations without rebuilding"
            echo "  ./rebuild-nixos --quick      # Fastest: skip changelog + cleanup"
            echo "  ./rebuild-nixos --yes        # Auto-accept all prompts"
            echo "  ./rebuild-nixos --fresh      # Bypass eval-cache for clean rebuild"
            echo "  ./rebuild-nixos --audit      # Include forensic audit trail export"
            echo "  ./rebuild-nixos --boot       # Stage for next reboot (not live)"
            exit 0
            ;;
        *)
            echo "Unknown option: $1"
            echo "Use --help for usage information"
            exit 1
            ;;
    esac
done

# Setup logging
LOG_DIR="$HOME/.claude/.logs"
mkdir -p "$LOG_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
LOG_FILE="$LOG_DIR/rebuild-$TIMESTAMP.log"

# Colors for output
BLUE='\033[0;34m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# Tracking arrays for summary
declare -a WARNINGS=()
declare -a ACTIONS=()
declare -a STATS=()

# Helper functions
log_step() {
    local step_time
    step_time=$(date +%H:%M:%S)
    echo -e "${BLUE}[$step_time]${NC} $1"
}

log_success() {
    echo -e "${GREEN}✅${NC} $1"
}

log_warning() {
    echo -e "${YELLOW}⚠️${NC}  $1"
    WARNINGS+=("$1")
}

log_error() {
    echo -e "${RED}❌${NC} $1"
}

add_action() {
    ACTIONS+=("$1")
}

add_stat() {
    STATS+=("$1")
}

# Called at the end of a successful --boot rebuild instead of the switch-mode
# "Are you satisfied? / rollback" prompt. The new generation is staged in the
# bootloader but not yet running, so testing happens after reboot. We surface
# the rollback commands explicitly so the user can cancel before rebooting
# without having to remember the two-step bootloader-pointer dance.
handle_boot_mode_completion() {
    local new_gen=$1
    add_action "Staged generation $new_gen for next boot"

    log_success "Build complete. Generation $new_gen will activate on next reboot."
    log_step "To cancel before rebooting:"
    echo "    sudo nix-env -p /nix/var/nix/profiles/system --rollback"
    echo "    sudo /run/current-system/bin/switch-to-configuration boot"
}

# now_ms, step, step_complete, step_skip are provided by lib/event-log.sh
# (sourced near the top of this script).

# Prompt user with auto-accept support
# Usage: prompt_user "Question?" "default" → returns 0 (yes) or 1 (no)
# With --yes flag: auto-accepts (returns 0)
prompt_user() {
    local prompt="$1"
    if [ "$YES" = true ]; then
        echo -e "$prompt ${GREEN}[auto-accepted]${NC}"
        return 0  # Yes
    fi

    read -p "$prompt " -n 1 -r
    echo
    [[ $REPLY =~ ^[Yy]$ ]]
}

# Dry-run helper - shows what would run without executing
dry_run_skip() {
    local description="$1"
    if [ "$DRY_RUN" = true ]; then
        echo -e "${YELLOW}[DRY-RUN]${NC} Would: $description"
        return 0
    fi
    return 1
}

# Progress indicator with time estimates and status
show_progress() {
    local step_name="$1"
    local pid="$2"
    local history_file="$LOG_DIR/build-times.log"
    local start_time
    start_time=$(date +%s)

    # Calculate average from last 5 builds if available
    local avg_time=""
    if [ -f "$history_file" ]; then
        avg_time=$(tail -5 "$history_file" | awk '{sum+=$1; count++} END {if(count>0) print int(sum/count)}')
    fi

    local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
    local last_status=""

    while kill -0 "$pid" 2>/dev/null; do
        local elapsed=$(($(date +%s) - start_time))
        local mins=$((elapsed / 60))
        local secs=$((elapsed % 60))

        # Build progress message
        local msg
        msg=$(printf "⏱️  %02d:%02d" "$mins" "$secs")

        # Add ETA if we have historical data
        if [ -n "$avg_time" ] && [ "$avg_time" -gt 0 ]; then
            local remaining=$((avg_time - elapsed))
            if [ $remaining -gt 0 ]; then
                local eta_mins=$((remaining / 60))
                local eta_secs=$((remaining % 60))
                msg=$(printf "%s | ~%02d:%02d remaining" "$msg" "$eta_mins" "$eta_secs")
            fi
        fi

        # Try to get current status from log file
        if [ -f "$LOG_FILE" ]; then
            local current_status
            current_status=$(tail -5 "$LOG_FILE" 2>/dev/null | grep -E "building|copying|installing|activating|running|switching|updating" | tail -1)

            if [ -n "$current_status" ]; then
                # Extract package name from nix store path if present
                if echo "$current_status" | grep -q "/nix/store/"; then
                    # Extract just the package name (hash-packagename.drv -> packagename)
                    local pkg_name
                    pkg_name=$(echo "$current_status" | grep -oP '/nix/store/[a-z0-9]+-\K[^/]+' | head -1 | sed 's/\.drv$//')
                    if [ -n "$pkg_name" ]; then
                        # Get the action (building, copying, etc.)
                        local action
                        action=$(echo "$current_status" | grep -oP '^[a-z]+' | head -1)
                        last_status="$action $pkg_name"
                    else
                        last_status=$(echo "$current_status" | cut -c1-60)
                    fi
                else
                    last_status=$(echo "$current_status" | cut -c1-60)
                fi
            fi
        fi

        # Show status if available
        local status_display=""
        if [ -n "$last_status" ]; then
            status_display=" | ${last_status}"
        fi

        # Show spinner and message (allow long lines)
        local spinchar="${spinstr:0:1}"
        spinstr="${spinstr:1}${spinchar}"
        printf "\r%s  %s%s" "$spinchar" "$msg" "$status_display"
        tput el  # Clear to end of line
        sleep 0.2
    done

    # Clear progress line
    printf "\r"
    tput el

    # Record build time for future estimates
    local total_time=$(($(date +%s) - start_time))
    echo "$total_time" >> "$history_file"

    # Keep only last 10 builds
    if [ -f "$history_file" ]; then
        tail -10 "$history_file" > "$history_file.tmp"
        mv "$history_file.tmp" "$history_file"
    fi
}

# Interactive generation + profile cleanup.
# Called from Phase 10 (post-rebuild) and standalone via --gc.
run_generation_cleanup() {
    echo ""
    log_step "Listing system generations..."
    sudo nixos-rebuild list-generations

    read -rp "Enter generations to delete (space-separated), 'a' for auto cleanup, or Enter to skip: " generations_to_delete
    if [ -n "$generations_to_delete" ]; then
        if [ "$generations_to_delete" = "auto" ] || [ "$generations_to_delete" = "a" ]; then
            log_step "Running automatic generation cleanup (keeping last 5)..."

            sudo nix-env -p /nix/var/nix/profiles/system --delete-generations +5 2>/dev/null || true

            local root_channels="/nix/var/nix/profiles/per-user/root/channels"
            if [ -L "$root_channels" ]; then
                local before after cleaned
                before=$(find "${root_channels%/*}" -maxdepth 1 -name "channels-*-link" 2>/dev/null | wc -l)
                sudo nix-env -p "$root_channels" --delete-generations +5 2>/dev/null || true
                after=$(find "${root_channels%/*}" -maxdepth 1 -name "channels-*-link" 2>/dev/null | wc -l)
                cleaned=$((before - after))
                [ "$cleaned" -gt 0 ] && echo "  ✓ Cleaned $cleaned root channel generations"
            fi

            local default_profile="/nix/var/nix/profiles/default"
            if [ -L "$default_profile" ]; then
                local before after cleaned
                before=$(find "${default_profile%/*}" -maxdepth 1 -name "default-*-link" 2>/dev/null | wc -l)
                sudo nix-env -p "$default_profile" --delete-generations +5 2>/dev/null || true
                after=$(find "${default_profile%/*}" -maxdepth 1 -name "default-*-link" 2>/dev/null | wc -l)
                cleaned=$((before - after))
                [ "$cleaned" -gt 0 ] && echo "  ✓ Cleaned $cleaned default profile generations"
            fi

            local user_profile="$HOME/.local/state/nix/profiles/profile"
            if [ -L "$user_profile" ]; then
                local before after cleaned
                before=$(find "${user_profile%/*}" -maxdepth 1 -name "profile-*-link" 2>/dev/null | wc -l)
                nix-env -p "$user_profile" --delete-generations +5 2>/dev/null || true
                after=$(find "${user_profile%/*}" -maxdepth 1 -name "profile-*-link" 2>/dev/null | wc -l)
                cleaned=$((before - after))
                [ "$cleaned" -gt 0 ] && echo "  ✓ Cleaned $cleaned user profile generations"
            fi

            log_success "Automatic cleanup complete (kept last 5 of each profile)"
        else
            log_step "Deleting generations: $generations_to_delete"
            # shellcheck disable=SC2086  # word splitting intended: space-separated generation numbers
            sudo nix-env -p /nix/var/nix/profiles/system --delete-generations $generations_to_delete
            log_success "Selected generations deleted"
        fi

        log_step "Running garbage collection..."

        local gc_tmp="/tmp/gc-output-$$.tmp"
        nix-collect-garbage > "$gc_tmp" 2>&1 &
        local gc_pid=$!
        show_progress "garbage-collection" $gc_pid
        local gc_exit=0
        wait $gc_pid || gc_exit=$?

        local gc_summary="" hardlink_savings=""
        if [ -f "$gc_tmp" ]; then
            cat "$gc_tmp" >> "$LOG_FILE"
            gc_summary=$(tail -5 "$gc_tmp" | grep -E "store paths deleted|MiB freed|GiB freed" | tail -1)
            hardlink_savings=$(tail -5 "$gc_tmp" | grep "hard linking saves" | grep -oP '[\d.]+\s*(MiB|GiB)' | tail -1)
            rm -f "$gc_tmp"
        fi

        if [ "$gc_exit" -eq 0 ]; then
            if [ -n "$gc_summary" ]; then
                log_success "Garbage collection: $gc_summary"
            else
                log_success "Garbage collection complete"
            fi
            [ -n "$hardlink_savings" ] && echo "  ↳ Hard linking saves: $hardlink_savings"
        else
            log_warning "Garbage collection finished with issues (check logs)"
        fi
        add_action "Cleaned up old generations"
    else
        log_warning "No generations selected for deletion"
    fi
}

# Detect business vs admin host from hostname pattern (*-biz-*)
# Business hosts get a streamlined rebuild: build → activate → cleanup only
NIXOS_HOSTNAME_EARLY=$(hostname)
if [[ "$NIXOS_HOSTNAME_EARLY" == *-biz-* ]]; then
    IS_BUSINESS_HOST=true
else
    IS_BUSINESS_HOST=false
fi

# Step counter for "Step X/Y" progress indicator
# Dynamically count phases based on flags and available tools
if [ "$IS_BUSINESS_HOST" = true ]; then
    TOTAL_STEPS=2  # Business: Build, Activate
else
    TOTAL_STEPS=4  # Admin: Update inputs, Build, Activate, Claude config check
fi
# nvd diff preview adds a step (nvd is installed on this system)
command -v nvd &>/dev/null && TOTAL_STEPS=$((TOTAL_STEPS + 1))
# Bootstrap verification adds a step with --verify-bootstrap (admin only)
[ "$VERIFY_BOOTSTRAP" = true ] && [ "$IS_BUSINESS_HOST" = false ] && TOTAL_STEPS=$((TOTAL_STEPS + 1))
# CURRENT_STEP is initialized to 0 by lib/event-log.sh (sourced at top).

# Track total rebuild duration
REBUILD_START_TIME=$(date +%s)
REBUILD_START_MS=$(now_ms)

# --gc: run generation cleanup only, skip the full rebuild
if [ "$GC_ONLY" = true ]; then
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║              NixOS Generation Cleanup                      ║"
    echo "╚════════════════════════════════════════════════════════════╝"
    run_generation_cleanup
    echo ""
    echo "📁 Logs: $LOG_FILE"
    echo ""
    exit 0
fi

# Top-level "rebuild_start" event — distinct from individual phase boundaries.
# Captures the mode flags so downstream consumers (telemetry MCP, status dash)
# can correlate this run with its dry-run/quick/boot context.
log_event start rebuild "{\"dry_run\":$DRY_RUN,\"quick\":$QUICK,\"yes\":$YES,\"audit\":$AUDIT,\"verify_bootstrap\":$VERIFY_BOOTSTRAP,\"fresh\":$FRESH,\"boot_mode\":$BOOT_MODE,\"total_steps\":$TOTAL_STEPS}"

# Detect hostname for flake configuration (avoids hardcoding "nixos")
NIXOS_HOSTNAME=$(hostname)
if ! nix eval ".#nixosConfigurations.\"$NIXOS_HOSTNAME\"" --apply 'x: true' &>/dev/null; then
    # No matching flake config for this hostname — list available and fail
    AVAILABLE_HOSTS=$(nix flake show --json 2>/dev/null | jq -r '.nixosConfigurations | keys[]' 2>/dev/null || true)
    log_error "No flake configuration found for hostname '$NIXOS_HOSTNAME'"
    if [ -n "$AVAILABLE_HOSTS" ]; then
        echo ""
        echo "Available configurations:"
        echo "$AVAILABLE_HOSTS" | while read -r host; do echo "  - $host"; done
        echo ""
        echo "Re-run with the correct hostname set, or add a config for '$NIXOS_HOSTNAME' to flake.nix"
    fi
    exit 1
fi

# Start rebuild
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║            NixOS System Rebuild & Configuration            ║"
echo "╠════════════════════════════════════════════════════════════╣"
if [ "$DRY_RUN" = true ]; then
echo "║  Mode: Dry-run (no changes will be made)                   ║"
elif [ "$IS_BUSINESS_HOST" = true ]; then
echo "║  Mode: Business (streamlined rebuild)                      ║"
elif [ "$QUICK" = true ] && [ "$YES" = true ]; then
echo "║  Mode: Quick + Auto-accept (fastest)                       ║"
elif [ "$QUICK" = true ]; then
echo "║  Mode: Quick (skip cleanup phases)                         ║"
elif [ "$YES" = true ]; then
echo "║  Mode: Auto-accept (non-interactive)                       ║"
else
echo "║  Mode: Full (interactive)                                  ║"
fi
echo "╚════════════════════════════════════════════════════════════╝"
echo ""

if [ "$VERBOSE" = true ]; then
    echo "🔍 Verbose mode enabled"
    echo "📁 Logs: $LOG_FILE"
fi

if [ "$DRY_RUN" = true ]; then
    echo "🔍 Dry-run mode: showing what would happen without making changes"
    echo ""
fi

# Cache sudo credentials early (activation step needs sudo)
if [ "$DRY_RUN" = false ]; then
    echo "🔐 This rebuild requires sudo privileges for system activation."
    sudo -v || { log_error "Failed to obtain sudo access"; exit 1; }
fi

# === PHASE 0: Eval-Cache Management ===
# Manages Nix eval-cache to prevent stale cache from causing "changes ignored" issues
# The eval-cache stores evaluated Nix expressions and can become stale over time,
# causing rebuilds to incorrectly report "no changes" even when flake.nix was modified.
# This is the root cause of the "phantom generation" problem.
#
# IMPORTANT: Also manages ~/.local/share/nix/trusted-settings.json which can contain
# a "eval-cache": {"true": true} entry that OVERRIDES --option eval-cache false!
# Exit on failure: No (cache clearing is best-effort)

# Dynamic eval-cache directory detection (handles v5, v6, future versions)
# Finds the latest versioned eval-cache directory
EVAL_CACHE_DIR=$(find "$HOME/.cache/nix" -maxdepth 1 -type d -name "eval-cache-v*" 2>/dev/null | sort -V | tail -1)
if [ -z "$EVAL_CACHE_DIR" ]; then
    EVAL_CACHE_DIR="$HOME/.cache/nix/eval-cache-v6"  # Fallback to current version
fi
TRUSTED_SETTINGS="$HOME/.local/share/nix/trusted-settings.json"
CACHE_MAX_AGE_DAYS=7

manage_eval_cache() {
    local cache_cleared=false
    local reason=""

    # Case 1: --fresh flag - always clear cache
    if [ "$FRESH" = true ]; then
        reason="--fresh flag requested"
        cache_cleared=true
    # Case 2: Auto-detect stale cache (files older than threshold)
    elif [ -d "$EVAL_CACHE_DIR" ]; then
        # Find files older than threshold
        local stale_files
        stale_files=$(find "$EVAL_CACHE_DIR" -type f -mtime +"$CACHE_MAX_AGE_DAYS" -print -quit 2>/dev/null)
        if [ -n "$stale_files" ]; then
            # Get the oldest file's age for reporting
            local oldest_file
            oldest_file=$(find "$EVAL_CACHE_DIR" -type f -printf '%T+ %p\n' 2>/dev/null | sort | head -1 | cut -d' ' -f2 || true)
            if [ -n "$oldest_file" ]; then
                local oldest_age
                oldest_age=$(stat -c %Y "$oldest_file" 2>/dev/null)
                local now
                now=$(date +%s)
                local age_days=$(( (now - oldest_age) / 86400 ))
                reason="stale cache detected (oldest entry: ${age_days} days old)"
                cache_cleared=true
            fi
        fi
    fi

    # Clear eval-cache directory (handles all versioned directories when --fresh)
    if [ "$cache_cleared" = true ]; then
        # Clear ALL eval-cache versions when --fresh is used
        for cache_dir in "$HOME"/.cache/nix/eval-cache-v*; do
            if [ -d "$cache_dir" ]; then
                local cache_size
                cache_size=$(du -sh "$cache_dir" 2>/dev/null | cut -f1)
                if [ "$DRY_RUN" = true ]; then
                    echo -e "${YELLOW}[DRY-RUN]${NC} Would clear $(basename "$cache_dir") (${cache_size}): ${reason}"
                else
                    rm -rf "$cache_dir"
                    log_step "Cleared $(basename "$cache_dir") (${cache_size}): ${reason}"
                    add_action "Cleared stale eval-cache to ensure fresh evaluation"
                fi
            fi
        done
    elif [ "$VERBOSE" = true ] && [ -d "$EVAL_CACHE_DIR" ]; then
        local cache_size
        cache_size=$(du -sh "$EVAL_CACHE_DIR" 2>/dev/null | cut -f1)
        log_step "Eval-cache (${cache_size}) is fresh (< ${CACHE_MAX_AGE_DAYS} days old)"
    fi

    # CRITICAL: ALWAYS remove eval-cache from trusted-settings.json if present
    # This setting can OVERRIDE --option eval-cache false, causing phantom generation issues
    # Runs on EVERY rebuild, not just --fresh, to prevent the setting from accumulating
    if [ -f "$TRUSTED_SETTINGS" ]; then
        if grep -q '"eval-cache"' "$TRUSTED_SETTINGS" 2>/dev/null; then
            if [ "$DRY_RUN" = true ]; then
                echo -e "${YELLOW}[DRY-RUN]${NC} Would remove eval-cache from trusted-settings.json"
            else
                # Use jq if available, otherwise sed
                if command -v jq &>/dev/null; then
                    local tmp
                    tmp=$(mktemp)
                    jq 'del(."eval-cache")' "$TRUSTED_SETTINGS" > "$tmp" && mv "$tmp" "$TRUSTED_SETTINGS"
                else
                    # Fallback: remove eval-cache entry with sed
                    sed -i 's/,"eval-cache":{[^}]*}//g; s/"eval-cache":{[^}]*},//g' "$TRUSTED_SETTINGS"
                fi
                log_step "Removed eval-cache from trusted-settings.json (prevents --fresh override)"
                add_action "Removed eval-cache trusted setting to prevent cache override"
            fi
        fi
    fi
}

# Run eval-cache management
manage_eval_cache

# === PHASE 1: Update Flake Inputs (admin only) ===
# Updates nix flake lock file to get latest package versions
# SMART UPDATE: Uses --refresh for locally-maintained repos to bypass cache
# Side effects: Modifies flake.lock, may pull in breaking changes
# Exit on failure: Yes (can't build without valid inputs)
# Business hosts skip this — admin pushes flake.lock updates

if [ "$IS_BUSINESS_HOST" = true ]; then
    log_step "Skipping flake update (business host — admin manages inputs)"
    step_skip "Updating flake inputs" "business_host"
else
step "Updating flake inputs"

if dry_run_skip "update flake inputs (nixpkgs, home-manager, local repos)"; then
    : # skip
else
    # Define locally-maintained inputs that need cache bypass
    LOCAL_INPUTS=(
        "code-cursor-nix"
        "whisper-dictation"
        "claude-code-nix"  # Also locally maintained by you
        "antigravity-nix"
    )

    if [ "$VERBOSE" = true ]; then
        echo "📦 Updating external inputs (using cache)..."
        nix flake update nixpkgs home-manager devenv 2>&1 | tee -a "$LOG_FILE"

        echo "🚀 Updating local inputs (bypassing cache with --refresh)..."
        # Batch update: single nix invocation for all local inputs (faster than per-input)
        nix flake update "${LOCAL_INPUTS[@]}" --refresh 2>&1 | tee -a "$LOG_FILE"
    else
        # Quiet mode: Update external inputs first (cached)
        nix flake update nixpkgs home-manager devenv &>>"$LOG_FILE"

        # Batch update local inputs with --refresh to bypass cache
        nix flake update "${LOCAL_INPUTS[@]}" --refresh &>>"$LOG_FILE"
    fi

    log_success "Flake inputs updated (local repos refreshed)"
    add_action "Updated flake.lock with cache bypass for local repos"
fi
fi  # End admin-only Phase 1

# === PHASE 2: Test Build ===
# Builds new configuration WITHOUT activating it (safe to fail)
# This catches build errors before touching running system
# Exit on failure: Yes (don't activate broken config)
# Perform a test build
step "Building configuration"

# Check if nom (nix-output-monitor) is available for beautiful build visualization
if command -v nom &>/dev/null; then
    USE_NOM=true
else
    USE_NOM=false
    [ "$VERBOSE" = false ] && log_warning "nom not found - using basic progress indicator (install nix-output-monitor for better UX)"
fi

if [ "$VERBOSE" = true ]; then
    # Verbose mode - show all output
    NEW_CONFIG_PATH=$(NIXPKGS_ALLOW_UNFREE=1 nix build --no-link --print-out-paths --impure .#nixosConfigurations."$NIXOS_HOSTNAME".config.system.build.toplevel 2>&1 | tee -a "$LOG_FILE" | tail -1)
elif [ "$USE_NOM" = true ]; then
    # Use nix-output-monitor with explicit JSON mode for clean tree-only output
    # --log-format internal-json: nix outputs JSON (not raw text)
    # nom --json: parses JSON and shows ONLY the dependency tree
    # NOTE: -v flag removed - it was causing verbose derivation list to be dumped
    # The internal-json format works without -v and gives nom what it needs
    BUILD_LINK="/tmp/nixos-build-$$"
    echo ""
    # Temporarily disable set -e: nom/tee can exit before nix finishes writing,
    # causing SIGPIPE (exit 141) which pipefail would treat as a fatal error.
    set +e
    NIXPKGS_ALLOW_UNFREE=1 nix build --log-format internal-json --out-link "$BUILD_LINK" --impure .#nixosConfigurations."$NIXOS_HOSTNAME".config.system.build.toplevel 2>&1 | nom --json 2>&1 | tee -a "$LOG_FILE"
    BUILD_EXIT=${PIPESTATUS[0]}
    set -e

    if [ "$BUILD_EXIT" -ne 0 ]; then
        log_error "Test build failed (exit code: $BUILD_EXIT)"
        echo "Check logs: $LOG_FILE"
        rm -f "$BUILD_LINK"
        exit 1
    fi

    # Get path from symlink and clean up
    NEW_CONFIG_PATH=$(readlink -f "$BUILD_LINK")
    rm -f "$BUILD_LINK"
else
    # Fallback: custom progress indicator (when nom not available)
    NIXPKGS_ALLOW_UNFREE=1 nix build --no-link --print-out-paths --impure .#nixosConfigurations."$NIXOS_HOSTNAME".config.system.build.toplevel > /tmp/nixbuild-output-$$.tmp 2>>"$LOG_FILE" &
    BUILD_PID=$!
    show_progress "test-build" $BUILD_PID
    wait $BUILD_PID
    BUILD_EXIT=$?

    if [ $BUILD_EXIT -ne 0 ]; then
        log_error "Test build failed (exit code: $BUILD_EXIT)"
        echo "Check logs: $LOG_FILE"
        rm -f /tmp/nixbuild-output-$$.tmp
        exit 1
    fi

    NEW_CONFIG_PATH=$(cat /tmp/nixbuild-output-$$.tmp)
    rm -f /tmp/nixbuild-output-$$.tmp
fi

log_success "Test build successful"

# === PHASE 2.5: Package Diff Preview ===
# Shows what packages will change before activation (using nvd)
# This helps users understand the impact before committing
if command -v nvd &>/dev/null; then
    CURRENT_SYSTEM=$(readlink -f /run/current-system)
    step "Analyzing package changes"
    echo ""

    # Run nvd diff and capture output
    NVD_OUTPUT=$(nvd diff "$CURRENT_SYSTEM" "$NEW_CONFIG_PATH" 2>/dev/null || true)

    if [ -n "$NVD_OUTPUT" ]; then
        # Count changes - nvd uses format: [A+] added, [R-] removed, [U ↑] upgrade, [D ↓] downgrade
        # Example: "[A+]  #1  linggen   <none>"
        # NOTE: grep -c outputs "0" AND exits 1 when no matches, so || echo "0" would give "00"
        # Fix: Use separate assignment to avoid concatenating both outputs
        PKG_ADDED=$(echo "$NVD_OUTPUT" | grep -c '\[A[+]\]' 2>/dev/null) || PKG_ADDED=0
        PKG_REMOVED=$(echo "$NVD_OUTPUT" | grep -c '\[R[-]\]' 2>/dev/null) || PKG_REMOVED=0
        PKG_UPGRADED=$(echo "$NVD_OUTPUT" | grep -c '\[U' 2>/dev/null) || PKG_UPGRADED=0
        PKG_DOWNGRADED=$(echo "$NVD_OUTPUT" | grep -c '\[D' 2>/dev/null) || PKG_DOWNGRADED=0

        echo -e "  ${GREEN}+${NC} Added: $PKG_ADDED  ${RED}-${NC} Removed: $PKG_REMOVED  ${BLUE}↑${NC} Upgraded: $PKG_UPGRADED  ${YELLOW}↓${NC} Downgraded: $PKG_DOWNGRADED"
        echo ""

        # Show first 20 changes
        CHANGE_LINES=$(echo "$NVD_OUTPUT" | wc -l)
        CHANGE_LINES="${CHANGE_LINES//[^0-9]/}"
        if [ "$CHANGE_LINES" -gt 20 ]; then
            echo "$NVD_OUTPUT" | head -20
            echo "  ... and $((CHANGE_LINES - 20)) more changes"
        else
            echo "$NVD_OUTPUT"
        fi
        echo ""

        # Store for summary (single line, no newlines)
        PKG_CHANGES="${PKG_ADDED} added, ${PKG_REMOVED} removed, ${PKG_UPGRADED} upgraded"
        add_stat "Packages: $PKG_CHANGES"
    else
        echo "  No package changes detected"
        PKG_CHANGES=""
    fi
fi

# Supply chain security: relies on flake.lock pinning + cache.nixos.org signing.
# DIY git-vs-tarball and reproducibility checks were removed — they gave false
# confidence (only covered 5 of thousands of packages, excluded the exact dirs
# where XZ hid its backdoor, and couldn't distinguish nixpkgs patches from attacks).
# For real supply chain guarantees, consider Determinate Systems' FlakeHub.

# === PHASE 2.7.4-2.7.5: Audit Trail Export (--audit flag only) ===
# These are slow forensic operations, only run when explicitly requested
if [ "$AUDIT" = true ]; then
    # Ensure AUDIT_DIR and TIMESTAMP are set if we skipped security checks
    AUDIT_DIR="${AUDIT_DIR:-$HOME/.nixos-audit}"
    mkdir -p "$AUDIT_DIR"
    # TIMESTAMP is set once at script start — reuse it for consistent naming

    # === PHASE 2.7.4: Source Manifest (lightweight) ===
    log_step "Extracting source derivations for manifest..."
    # Nix 2.x added a {"version": N, "derivations": {...}} envelope around the
    # old flat {drv_path: drv_data} shape. `(.derivations // .)` handles both.
    FODS=$(nix derivation show -r "$NEW_CONFIG_PATH" 2>/dev/null | \
        jq -r '(.derivations // .) | to_entries[] | select(.value.outputs.out.hash != null) | .key')

    FOD_COUNT=$(echo "$FODS" | grep -c '^' || echo "0")
    echo "$FODS" > "$AUDIT_DIR/sources-$TIMESTAMP.manifest"
    log_success "Source manifest saved: $AUDIT_DIR/sources-$TIMESTAMP.manifest ($FOD_COUNT sources)"
    add_stat "Source derivations: $FOD_COUNT"

    # === PHASE 2.7.5: Optional Closure Export (audit trail) ===
    echo ""
    EXPORT_PROMPT="Export source closure for audit trail (~5GB, 30-60 min)? (y/n)"

    if prompt_user "$EXPORT_PROMPT"; then
        log_step "Realizing source derivations (this may take a while)..."

        TIMEOUT_PER_DRV=${AUDIT_TIMEOUT_PER_DRV:-60}
        OVERALL_TIMEOUT=${AUDIT_OVERALL_TIMEOUT:-14400}

        CLOSURE_FILE="$AUDIT_DIR/source-closure-$TIMESTAMP.nar"
        REALIZE_LOG="$AUDIT_DIR/realize-$TIMESTAMP.log"
        OUTPUT_PATHS_FILE="$AUDIT_DIR/output-paths-$TIMESTAMP.txt"

        log_step "Building output-to-derivation mapping for $FOD_COUNT derivations..."
        DRV_MAP_FILE="$AUDIT_DIR/drv-map-$TIMESTAMP.txt"
        DRV_MAP_SKIP_LOG="$AUDIT_DIR/drv-map-skipped-$TIMESTAMP.log"
        # Per-drv lookups must tolerate failures: any single `nix derivation show`
        # failure under `set -e -o pipefail` would otherwise abort the whole
        # mapping across thousands of derivations.
        echo "$FODS" | while read -r drv; do
            out=$(nix derivation show "$drv" 2>/dev/null | jq -r '(.derivations // .) | .[].outputs.out.path' 2>/dev/null || true)
            if [ -n "$out" ] && [ "$out" != "null" ]; then
                echo "$out $drv"
            else
                echo "$drv" >> "$DRV_MAP_SKIP_LOG"
            fi
        done > "$DRV_MAP_FILE"
        OUTPUT_COUNT=$(wc -l < "$DRV_MAP_FILE")
        SKIP_COUNT=0
        [ -f "$DRV_MAP_SKIP_LOG" ] && SKIP_COUNT=$(wc -l < "$DRV_MAP_SKIP_LOG")
        if [ "$SKIP_COUNT" -gt 0 ]; then
            log_warning "Mapped $OUTPUT_COUNT output paths ($SKIP_COUNT skipped — see $DRV_MAP_SKIP_LOG)"
        else
            log_success "Mapped $OUTPUT_COUNT output paths"
        fi

        cut -d' ' -f1 "$DRV_MAP_FILE" > "$OUTPUT_PATHS_FILE"

        log_step "Realizing outputs (timeout: ${TIMEOUT_PER_DRV}s per item, ${OVERALL_TIMEOUT}s total)..."

        REALIZED=0
        FAILED=0
        SKIPPED=0
        START_TIME=$(date +%s)

        while read -r output_path drv_path; do
            ELAPSED=$(($(date +%s) - START_TIME))
            if [ "$ELAPSED" -ge "$OVERALL_TIMEOUT" ]; then
                log_warning "Overall timeout reached (${OVERALL_TIMEOUT}s). Stopping realization."
                break
            fi

            if [ -e "$output_path" ]; then
                SKIPPED=$((SKIPPED + 1))
            else
                if timeout "$TIMEOUT_PER_DRV" nix-store --realise "$drv_path" >> "$REALIZE_LOG" 2>&1; then
                    REALIZED=$((REALIZED + 1))
                else
                    EXIT_CODE=$?
                    if [ "$EXIT_CODE" -eq 124 ]; then
                        echo "[TIMEOUT] $drv_path" >> "$REALIZE_LOG"
                    else
                        echo "[FAILED:$EXIT_CODE] $drv_path" >> "$REALIZE_LOG"
                    fi
                    FAILED=$((FAILED + 1))
                fi
            fi

            TOTAL=$((REALIZED + FAILED + SKIPPED))
            if [ $((TOTAL % 100)) -eq 0 ]; then
                ELAPSED=$(($(date +%s) - START_TIME))
                printf "  Progress: %d/%d (realized: %d, skipped: %d, failed: %d) [%ds elapsed]\n" \
                    "$TOTAL" "$OUTPUT_COUNT" "$REALIZED" "$SKIPPED" "$FAILED" "$ELAPSED"
            fi
        done < "$DRV_MAP_FILE"

        TOTAL_TIME=$(($(date +%s) - START_TIME))
        log_success "Realization complete: $REALIZED realized, $SKIPPED already present, $FAILED failed (${TOTAL_TIME}s)"

        if [ "$FAILED" -gt 0 ]; then
            log_warning "Some derivations failed. Check log: $REALIZE_LOG"
        fi

        log_step "Exporting realized outputs to NAR..."
        # Filter existing output paths to a temp file (avoids subshell variable scoping)
        EXPORT_LIST=$(mktemp)
        while read -r output_path; do
            if [ -e "$output_path" ]; then
                echo "$output_path"
            fi
        done < "$OUTPUT_PATHS_FILE" > "$EXPORT_LIST"
        EXPORT_COUNT=$(wc -l < "$EXPORT_LIST")

        xargs nix-store --export < "$EXPORT_LIST" > "$CLOSURE_FILE" 2>> "$REALIZE_LOG"
        rm -f "$EXPORT_LIST"

        if [ -f "$CLOSURE_FILE" ]; then
            CLOSURE_SIZE=$(du -h "$CLOSURE_FILE" | cut -f1)
            log_success "Source closure exported: $CLOSURE_FILE ($CLOSURE_SIZE, $EXPORT_COUNT paths)"
            add_action "Exported source closure for offline verification"
        else
            log_error "Failed to create closure file"
        fi

        add_stat "Realized: $REALIZED, Skipped: $SKIPPED, Failed: $FAILED"
    fi

    add_action "Audit trail exported (source manifest + closure)"
fi

# === PHASE 2.8: Bootstrap Package Verification ===
# Deep reproducibility verification of bootstrap-critical packages
# Inspired by: https://luj.fr/blog/how-nixos-could-have-detected-xz.html
if [ "$VERIFY_BOOTSTRAP" = true ]; then
    step "Deep verification of bootstrap packages"

    # Ensure NIXPKGS_EXPR is set (may be unset if --quick was also passed)
    if [ -z "${NIXPKGS_EXPR:-}" ]; then
        FLAKE_NIXPKGS=$(nix flake archive --json 2>/dev/null | jq -r '.inputs.nixpkgs.path' 2>/dev/null || true)
        if [ -n "$FLAKE_NIXPKGS" ] && [ -d "$FLAKE_NIXPKGS" ]; then
            NIXPKGS_EXPR="(import $FLAKE_NIXPKGS {})"
        else
            NIXPKGS_EXPR="(import <nixpkgs> {})"
        fi
    fi

    VERIFY_DIR="$HOME/.nixos-audit/bootstrap-verify-$TIMESTAMP"
    mkdir -p "$VERIFY_DIR"

    # Bootstrap-critical packages (first-stage build dependencies)
    BOOTSTRAP_PKGS=(
        "xz"           # Compression (xz backdoor target)
        "gzip"         # Compression
        "bzip2"        # Compression
        "coreutils"    # Basic utilities
        "gnugrep"      # Pattern matching
        "gnused"       # Stream editor
        "gawk"         # Text processing
        "bash"         # Shell
        "gnumake"      # Build system
        "binutils"     # Linker/assembler
    )

    declare -a VERIFY_RESULTS=()

    for pkg in "${BOOTSTRAP_PKGS[@]}"; do
        echo -n "  Verifying $pkg... "

        # Build with --check to compare against existing store path
        CHECK_OUTPUT=$(nix-build --expr "${NIXPKGS_EXPR}.${pkg}" --check --keep-failed 2>&1) || true

        if echo "$CHECK_OUTPUT" | grep -q "output .* is identical"; then
            echo -e "${GREEN}REPRODUCIBLE ✓${NC}"
            VERIFY_RESULTS+=("$pkg:PASS")
        elif echo "$CHECK_OUTPUT" | grep -q "output .* differs"; then
            echo -e "${RED}DIVERGENT OUTPUT - POTENTIAL TAMPERING${NC}"
            VERIFY_RESULTS+=("$pkg:FAIL")

            # Find the .check path for analysis
            CHECK_PATH=$(echo "$CHECK_OUTPUT" | grep -oP '/nix/store/[^"]+\.check' | head -1)
            if [ -n "$CHECK_PATH" ]; then
                echo "$pkg diverged. Check path: $CHECK_PATH" >> "$VERIFY_DIR/divergent-packages.txt"

                # Run nix-diff for root cause if available
                if command -v nix-diff &>/dev/null; then
                    ORIG_PATH="${CHECK_PATH%.check}"
                    nix-diff "$ORIG_PATH" "$CHECK_PATH" > "$VERIFY_DIR/$pkg-diff.txt" 2>&1 || true
                fi
            fi
        else
            echo -e "${YELLOW}skipped (not in store or build failed)${NC}"
            VERIFY_RESULTS+=("$pkg:SKIP")
        fi
    done

    # Summary report
    PASS_COUNT=$(printf '%s\n' "${VERIFY_RESULTS[@]}" | grep -c ':PASS' || echo 0)
    FAIL_COUNT=$(printf '%s\n' "${VERIFY_RESULTS[@]}" | grep -c ':FAIL' || echo 0)
    SKIP_COUNT=$(printf '%s\n' "${VERIFY_RESULTS[@]}" | grep -c ':SKIP' || echo 0)

    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║         Bootstrap Verification Summary                     ║"
    echo "╚════════════════════════════════════════════════════════════╝"
    echo "  Reproducible: $PASS_COUNT"
    echo "  Divergent:    $FAIL_COUNT"
    echo "  Skipped:      $SKIP_COUNT"

    if [ "$FAIL_COUNT" -gt 0 ]; then
        log_error "SECURITY ALERT: $FAIL_COUNT package(s) failed reproducibility check!"
        echo "  Review: $VERIFY_DIR/divergent-packages.txt"

        if ! prompt_user "Continue despite reproducibility failures? (y/n)"; then
            log_error "Aborting due to reproducibility failures"
            exit 1
        fi
    fi

    # Save results
    printf '%s\n' "${VERIFY_RESULTS[@]}" > "$VERIFY_DIR/results.txt"
    add_stat "Bootstrap verification: $PASS_COUNT/$((PASS_COUNT + FAIL_COUNT)) reproducible"
    add_action "Bootstrap packages verified"
fi

# === PHASE 3: Activate Configuration ===
# Switches running system to new configuration (requires sudo)
# This is the critical step - system state changes here
# Exit on failure: Yes (activation failed, system unchanged)
# Rollback available: Use 'sudo nixos-rebuild switch --rollback'

# Build extra options based on flags
NIXOS_REBUILD_OPTS=""
if [ "$FRESH" = true ]; then
    NIXOS_REBUILD_OPTS="--option eval-cache false"
    log_step "Fresh mode: eval-cache cleared (Phase 0) + bypassing cache writes"
fi

# Activate the new configuration
step "Activating configuration"

# Refresh sudo credentials before activation (may have expired during security checks)
if [ "$DRY_RUN" = false ]; then
    sudo -v || { log_error "Failed to refresh sudo access"; exit 1; }
fi

REBUILD_MODE="switch"
if [ "$BOOT_MODE" = true ]; then
    REBUILD_MODE="boot"
fi

if dry_run_skip "activate new NixOS configuration (nixos-rebuild $REBUILD_MODE)"; then
    : # skip
else
    # Temporarily disable set -e for the activation pipeline.
    # switch-to-configuration returns exit code 4 for non-fatal unit activation issues
    # (e.g., a service restarting during switch). With set -e + pipefail, this would
    # abort the script before we can inspect the exit code.
    set +e
    if [ "$VERBOSE" = true ]; then
        # Verbose mode - show all output, capture exit code properly
        # shellcheck disable=SC2086  # NIXOS_REBUILD_OPTS may contain multiple flags
        sudo NIXPKGS_ALLOW_UNFREE=1 nixos-rebuild "$REBUILD_MODE" --flake . --impure $NIXOS_REBUILD_OPTS 2>&1 | tee -a "$LOG_FILE"
        ACTIVATE_EXIT=${PIPESTATUS[0]}
    elif [ "$USE_NOM" = true ]; then
        # Use nix-output-monitor for beautiful activation visualization
        echo ""
        # shellcheck disable=SC2086  # NIXOS_REBUILD_OPTS may contain multiple flags
        sudo NIXPKGS_ALLOW_UNFREE=1 nixos-rebuild "$REBUILD_MODE" --flake . --impure $NIXOS_REBUILD_OPTS 2>&1 | nom | tee -a "$LOG_FILE"
        ACTIVATE_EXIT=${PIPESTATUS[0]}
    else
        # Fallback: custom progress indicator (capture both stdout and stderr)
        # shellcheck disable=SC2086,SC2024  # word splitting intended; redirect on backgrounded process
        sudo NIXPKGS_ALLOW_UNFREE=1 nixos-rebuild "$REBUILD_MODE" --flake . --impure $NIXOS_REBUILD_OPTS >>"$LOG_FILE" 2>&1 &
        ACTIVATE_PID=$!
        show_progress "activation" $ACTIVATE_PID
        wait $ACTIVATE_PID
        ACTIVATE_EXIT=$?
    fi
    set -e

    # Check activation result
    # Exit code 4 from switch-to-configuration = some units had non-fatal activation
    # issues (e.g., a service briefly interrupted during switch). The system switched fine.
    if [ "$ACTIVATE_EXIT" -eq 4 ]; then
        echo ""
        log_warning "Some systemd units had non-fatal activation issues (exit code 4)"
        log_step "This is common and harmless — check 'journalctl -b --priority=warning' if curious"
    elif [ "$ACTIVATE_EXIT" -ne 0 ]; then
        echo ""  # Ensure we're on a new line
        log_error "Activation failed (exit code: $ACTIVATE_EXIT)"
        echo "📁 Check logs: $LOG_FILE"
        exit 1
    fi

    echo ""  # Ensure we're on a new line after progress indicator

    # Verify activation succeeded - check the current generation
    NEW_GEN=$(readlink /nix/var/nix/profiles/system | sed 's/system-\([0-9]*\)-link/\1/' || true)
    CURRENT_SYSTEM=$(readlink /run/current-system)

    if [ -n "$NEW_GEN" ]; then
        REBUILD_ACTIVATED=true
        if [ "$BOOT_MODE" = true ]; then
            log_success "Configuration staged for next boot (generation $NEW_GEN)"
            log_step "Currently running: $(basename "$CURRENT_SYSTEM")"
            add_action "NixOS configuration staged for boot (gen $NEW_GEN)"
        else
            log_success "Configuration activated (generation $NEW_GEN)"
            log_step "System: $(basename "$CURRENT_SYSTEM")"
            add_action "NixOS configuration activated (gen $NEW_GEN)"
        fi
    else
        log_warning "Could not determine current generation"
        log_step "Current: $(basename "$CURRENT_SYSTEM")"
    fi
fi

# === PHASE 4: Claude Config Validator (admin only) ===
# Invokes scripts/check-claude-config.sh to catch drift between declarative
# intent (NixOS / Home Manager) and the actual state of ~/.claude/.
# Source: docs/plans/2026-05-18-claudeos-p0-implementation.md Task 22
if [ "$IS_BUSINESS_HOST" = false ]; then
    step "Validating Claude Code configuration"
    if dry_run_skip "validate Claude Code configurations"; then
        : # skip
    else
        if [ -x "./scripts/check-claude-config.sh" ]; then
            # check-claude-config.sh exits 1 (warnings) / 2 (errors) by design,
            # and the ladder below handles each non-fatally. But under `set -e`
            # a bare `VAR=$(cmd)` assignment aborts the whole script the instant
            # cmd returns non-zero — which killed the rebuild here before Phase
            # 10 (generation cleanup) could run. Capture the code via
            # `|| VALIDATOR_EXIT=$?` so a non-zero validator stays non-fatal.
            VALIDATOR_EXIT=0
            VALIDATOR_OUT=$(./scripts/check-claude-config.sh 2>&1) || VALIDATOR_EXIT=$?

            # Count events by level
            ERRORS=$(echo "$VALIDATOR_OUT" | jq '[.[] | select(.level == "error")] | length' 2>/dev/null || echo 0)
            WARNS=$(echo "$VALIDATOR_OUT" | jq '[.[] | select(.level == "warn")] | length' 2>/dev/null || echo 0)
            INFOS=$(echo "$VALIDATOR_OUT" | jq '[.[] | select(.level == "info")] | length' 2>/dev/null || echo 0)

            if [ "$VALIDATOR_EXIT" -eq 2 ]; then
                log_warning "Claude config validator: $ERRORS errors, $WARNS warnings"
                echo "$VALIDATOR_OUT" | jq -r '.[] | select(.level == "error") | "  [error] [\(.check)] \(.message)"'
                add_stat "Claude configs: $ERRORS errors, $WARNS warnings"
            elif [ "$VALIDATOR_EXIT" -eq 1 ]; then
                log_warning "Claude config validator: $WARNS warnings ($INFOS info)"
                echo "$VALIDATOR_OUT" | jq -r '.[] | select(.level == "warn") | "  [warn] [\(.check)] \(.message)"'
                add_stat "Claude configs: $WARNS warnings, $INFOS info"
            else
                log_success "Claude config validated: $INFOS checks passed"
                add_stat "Claude configs: $INFOS checks OK"
            fi
        else
            log_warning "Claude config validator missing at ./scripts/check-claude-config.sh"
        fi
    fi
else
    step_skip "Validating Claude Code configuration" "business_host"
fi  # End admin-only Phase 4

# === PHASE 7: Changelog Draft Generation (admin only) ===
# Generates draft changelog entries from commits since last update
# Exit on failure: No (non-critical, system already activated)
# User interaction: Yes (review and approve draft)
# Skip with: --quick flag or business host
echo ""

if [ "$IS_BUSINESS_HOST" = true ]; then
    : # Skip silently on business hosts
elif [ "$QUICK" = true ]; then
    log_step "Skipping changelog generation (--quick mode)"
elif dry_run_skip "generate changelog draft from recent commits"; then
    : # skip
else
    log_step "Checking for changelog updates..."
    # Track last processed commit to avoid re-proposing same changes
    CHANGELOG_STATE_FILE=".claude/.changelog-last-processed"
    LAST_PROCESSED_COMMIT=""

    if [ -f "$CHANGELOG_STATE_FILE" ]; then
        LAST_PROCESSED_COMMIT=$(cat "$CHANGELOG_STATE_FILE" 2>/dev/null | head -1)
    fi

    # Count new commits since last processed (or since last dated release as fallback)
    if [ -n "$LAST_PROCESSED_COMMIT" ] && git rev-parse --verify "$LAST_PROCESSED_COMMIT" >/dev/null 2>&1; then
        NEW_COMMITS=$(git rev-list "$LAST_PROCESSED_COMMIT"..HEAD --count 2>/dev/null || echo "0")
        SINCE_MARKER="commit ${LAST_PROCESSED_COMMIT:0:7}"
    else
        # Fallback to dated release if no state file
        LAST_CHANGELOG_DATE=$(grep -oP '^\#\# \[\d{4}-\d{2}-\d{2}\]' CHANGELOG.md 2>/dev/null | head -1 | grep -oP '\d{4}-\d{2}-\d{2}' || echo "")
        if [ -n "$LAST_CHANGELOG_DATE" ]; then
            NEW_COMMITS=$(git log --since="$LAST_CHANGELOG_DATE" --oneline 2>/dev/null | wc -l)
            SINCE_MARKER="$LAST_CHANGELOG_DATE"
        else
            NEW_COMMITS=0
            SINCE_MARKER="unknown"
        fi
    fi

    if [ "$NEW_COMMITS" -gt 0 ]; then
            echo "  Found $NEW_COMMITS commits since $SINCE_MARKER"
            if prompt_user "  Generate changelog draft? [y/N]"; then
                # Run changelog generator script (pass commit hash for accurate filtering)
                if [ -x "./scripts/generate-changelog-draft.sh" ]; then
                    if ./scripts/generate-changelog-draft.sh "$LAST_PROCESSED_COMMIT" > /tmp/changelog-draft-$$.md 2>/dev/null; then
                        echo ""
                        echo -e "${BLUE}════════════════════ Changelog Draft ════════════════════${NC}"
                        bat --style=plain --paging=never /tmp/changelog-draft-$$.md 2>/dev/null || cat /tmp/changelog-draft-$$.md
                        echo -e "${BLUE}══════════════════════════════════════════════════════════${NC}"
                        echo ""
                        echo "Options:"
                        echo "  1) Append to CHANGELOG.md [Unreleased]"
                        echo "  2) Edit first in \$EDITOR, then append"
                        echo "  3) Skip (keep draft at /tmp/changelog-draft-$$.md)"
                        read -p "Choose option (1-3): " -n 1 -r CHANGELOG_ACTION
                        echo

                        case $CHANGELOG_ACTION in
                            1)
                                # Insert draft after [Unreleased] line (before ### Planned)
                                # Using awk to insert content at the right place
                                awk -v draft="$(cat /tmp/changelog-draft-$$.md)" '
                                    /^### Planned/ && !inserted {
                                        print draft
                                        print ""
                                        inserted=1
                                    }
                                    {print}
                                ' CHANGELOG.md > /tmp/changelog-updated-$$.md && mv /tmp/changelog-updated-$$.md CHANGELOG.md
                                # Save current commit as last processed to avoid re-proposing
                                git rev-parse HEAD > "$CHANGELOG_STATE_FILE"
                                log_success "Added draft to CHANGELOG.md [Unreleased]"
                                add_action "Generated changelog draft ($NEW_COMMITS commits)"
                                rm -f /tmp/changelog-draft-$$.md
                                ;;
                            2)
                                ${EDITOR:-nano} /tmp/changelog-draft-$$.md
                                awk -v draft="$(cat /tmp/changelog-draft-$$.md)" '
                                    /^### Planned/ && !inserted {
                                        print draft
                                        print ""
                                        inserted=1
                                    }
                                    {print}
                                ' CHANGELOG.md > /tmp/changelog-updated-$$.md && mv /tmp/changelog-updated-$$.md CHANGELOG.md
                                # Save current commit as last processed to avoid re-proposing
                                git rev-parse HEAD > "$CHANGELOG_STATE_FILE"
                                log_success "Added edited draft to CHANGELOG.md"
                                add_action "Generated and edited changelog ($NEW_COMMITS commits)"
                                rm -f /tmp/changelog-draft-$$.md
                                ;;
                            *)
                                log_warning "Changelog update skipped (draft saved at /tmp/changelog-draft-$$.md)"
                                ;;
                        esac
                    else
                        log_warning "Changelog generator script failed"
                    fi
                else
                    log_warning "Changelog generator script not found at ./scripts/generate-changelog-draft.sh"
                fi
            else
                log_warning "Changelog update skipped ($NEW_COMMITS pending commits)"
            fi
        else
            log_success "Changelog up to date (no commits since $SINCE_MARKER)"
        fi
fi

# Phase 8 (Adaptive Learning) removed — Claude configs are now hand-maintained

# === PHASE 9: User Acceptance ===
# User tests the activated configuration
# Offers rollback if issues found
echo ""

if [ "$DRY_RUN" = true ]; then
    log_step "Dry-run complete. No changes were made."
elif [ "$BOOT_MODE" = true ]; then
    # In boot mode the new system isn't live — user can't test yet.
    # The switch-mode "Are you satisfied? / rollback" flow doesn't apply:
    # rollback before reboot means pointing the bootloader at the previous
    # generation, not running `switch --rollback` (which would activate it).
    handle_boot_mode_completion "$NEW_GEN"
else
    log_step "Configuration activated. Please test the changes now."
    if [ "$YES" = true ]; then
        log_step "Auto-accepting configuration (--yes mode)"
        log_success "Changes accepted"
        add_action "Configuration auto-accepted (--yes mode)"
    elif prompt_user "Are you satisfied with the changes? (y/n)"; then
        log_success "Changes accepted"
        add_action "Configuration tested and accepted"
    else
        echo -e "${YELLOW}⚠️  WARNING: This will rollback to the PREVIOUS system generation.${NC}"
        if prompt_user "Confirm rollback? (y/n)"; then
            log_step "Rolling back to previous configuration..."
            sudo nixos-rebuild switch --rollback --flake ".#$NIXOS_HOSTNAME"
            log_success "Rollback complete"
            step_complete
            log_event complete rebuild "{\"outcome\":\"rolled_back\"}"
            write_last_status succ
            exit 0
        else
            log_warning "Rollback cancelled — system remains on new configuration"
        fi
    fi
fi

# === PHASE 10: Generation Cleanup ===
# Deletes old NixOS generations to free disk space
# Options:
#   - Enter specific generation numbers to delete
#   - Enter 'auto' for automatic cleanup (keeps last 5 of each profile type)
# Auto mode cleans:
#   - System generations (/nix/var/nix/profiles/system)
#   - Root channel profiles (accumulate with each nixpkgs update!)
#   - Default profiles
#   - User/home-manager profiles
# Runs garbage collection after deletion
# Exit on failure: No (cleanup is optional)
# Skip with: --quick flag

if [ "$QUICK" = true ]; then
    : # Skip silently in quick mode
elif [ "$DRY_RUN" = false ]; then
    run_generation_cleanup
fi

# === PHASES 11-14: Admin Maintenance (admin only) ===
# Business hosts skip disk analysis, cache cleanup, and Claude temp/backup cleanup
if [ "$IS_BUSINESS_HOST" = false ]; then

# === PHASE 11: Disk Space Analysis ===
# Scans for problematic disk usage patterns:
# - Oversized logs (>100MB = investigate)
# - Large Claude sessions (>5MB = stuck session?)
# - Bloated configs (>1MB = corruption?)
# - Old backups (cleanup candidates)
# Checks learning data health (GREEN/YELLOW/RED risk levels)
# Exit on failure: No (analysis only, no changes made)
# Skip with: --quick flag

if [ "$QUICK" = true ]; then
    : # Skip silently in quick mode
else
    echo ""
    log_step "Analyzing disk space for issues..."
declare -a DISK_WARNINGS=()

# Check for oversized log files (>100MB = problematic)
# Note: fd returns 1 when no matches - protect with || true for pipefail
LARGE_LOGS=$( (fd -t f -s +100M '\.log$' ~ --max-depth 4 2>/dev/null || true) | wc -l)
if [ "$LARGE_LOGS" -gt 0 ]; then
    LARGEST_LOG=$( (fd -t f -s +100M '\.log$' ~ --max-depth 4 2>/dev/null || true) | xargs du -h 2>/dev/null | sort -rh | head -1)
    DISK_WARNINGS+=("$LARGE_LOGS log file(s) >100MB: ${LARGEST_LOG}")
fi

# Check Claude data disk usage
echo ""
log_step "Checking Claude data disk usage..."
CLAUDE_DIR_SIZE=$(du -sm ~/.claude 2>/dev/null | cut -f1)
echo "  • ~/.claude total: ${CLAUDE_DIR_SIZE}MB"
if [ "$CLAUDE_DIR_SIZE" -gt 500 ] 2>/dev/null; then
    DISK_WARNINGS+=("$HOME/.claude is ${CLAUDE_DIR_SIZE}MB — consider cleaning old sessions")
else
    echo -e "${GREEN}  ✅ Claude data size is reasonable${NC}"
fi

# Still check for very large individual session files (may indicate stuck sessions)
LARGE_CLAUDE=$( (fd -t f -s +5M '\.jsonl$' ~/.claude/projects 2>/dev/null || true) | wc -l)
if [ "$LARGE_CLAUDE" -gt 0 ]; then
    DISK_WARNINGS+=("$LARGE_CLAUDE Claude session(s) >5MB (may indicate stuck sessions)")
fi

# Check for oversized config files (>1MB = investigate)
# Note: fd returns 1 when no matches; grep -v returns 1 when all excluded - protect both
LARGE_CONFIGS=$( (fd -t f -s +1M '\.json$' ~/.config ~/.claude --max-depth 3 2>/dev/null || true) | { grep -v "IndexedDB\|chrome" || true; } | wc -l)
if [ "$LARGE_CONFIGS" -gt 0 ]; then
    DISK_WARNINGS+=("$LARGE_CONFIGS config file(s) >1MB (should be <100KB)")
fi

# Check for old backup files (>1MB)
LARGE_BACKUPS=$( (fd -t f -s +1M -e bak -e backup -e old ~ --max-depth 3 2>/dev/null || true) | wc -l)
if [ "$LARGE_BACKUPS" -gt 0 ]; then
    DISK_WARNINGS+=("$LARGE_BACKUPS backup file(s) >1MB (should archive/delete)")
fi

if [ ${#DISK_WARNINGS[@]} -gt 0 ]; then
    echo ""
    echo -e "${YELLOW}⚠️  Disk space issues detected (size vs expected):${NC}"
    for warning in "${DISK_WARNINGS[@]}"; do
        echo "   • $warning"
        log_warning "Disk: $warning"
    done
    echo ""
    echo "💡 Run these commands to investigate:"
    [ "$LARGE_LOGS" -gt 0 ] && echo "   fd -t f -s +100M '\.log$' ~ --max-depth 4 -x du -h | sort -rh"
    [ "$LARGE_CLAUDE" -gt 0 ] && echo "   fd -t f -s +5M '\.jsonl$' ~/.claude/projects -x du -h | sort -rh"
    [ "$LARGE_CONFIGS" -gt 0 ] && echo "   fd -t f -s +1M '\.json$' ~/.config ~/.claude --max-depth 3 | rg -v 'IndexedDB|chrome'"
    echo ""
fi
fi  # End QUICK mode skip for Phase 11 (Disk Analysis)

# === PHASE 12: Cache Cleanup ===
# Offers to clean large cache directories:
# - UV Python cache, Chrome cache, Yarn/npm/pnpm caches
# User confirms before deletion
# Safe to delete (caches regenerate on demand)
# Exit on failure: No (cleanup is optional)
# Skip with: --quick flag

if [ "$QUICK" = true ]; then
    : # Skip silently in quick mode
elif [ "$DRY_RUN" = false ]; then
    echo ""
    log_step "Cache cleanup options..."
    echo "Large cache directories detected:"
    TOTAL_CACHE_SIZE=0
    if [ -d ~/.cache/uv ]; then
    SIZE=$(du -sb ~/.cache/uv 2>/dev/null | cut -f1 || echo "0")
    SIZE=${SIZE:-0}
    TOTAL_CACHE_SIZE=$((TOTAL_CACHE_SIZE + SIZE))
    echo "  - UV Python cache: $(du -sh ~/.cache/uv 2>/dev/null | cut -f1)"
fi
if [ -d ~/.cache/google-chrome ]; then
    SIZE=$(du -sb ~/.cache/google-chrome 2>/dev/null | cut -f1 || echo "0")
    SIZE=${SIZE:-0}
    TOTAL_CACHE_SIZE=$((TOTAL_CACHE_SIZE + SIZE))
    echo "  - Google Chrome cache: $(du -sh ~/.cache/google-chrome 2>/dev/null | cut -f1)"
fi
if [ -d ~/.cache/yarn ]; then
    SIZE=$(du -sb ~/.cache/yarn 2>/dev/null | cut -f1 || echo "0")
    SIZE=${SIZE:-0}
    TOTAL_CACHE_SIZE=$((TOTAL_CACHE_SIZE + SIZE))
    echo "  - Yarn cache: $(du -sh ~/.cache/yarn 2>/dev/null | cut -f1)"
fi
if [ -d ~/.cache/ms-playwright ]; then
    SIZE=$(du -sb ~/.cache/ms-playwright 2>/dev/null | cut -f1 || echo "0")
    SIZE=${SIZE:-0}
    TOTAL_CACHE_SIZE=$((TOTAL_CACHE_SIZE + SIZE))
    echo "  - MS Playwright cache: $(du -sh ~/.cache/ms-playwright 2>/dev/null | cut -f1)"
fi
if [ -d ~/.cache/pnpm ]; then
    SIZE=$(du -sb ~/.cache/pnpm 2>/dev/null | cut -f1 || echo "0")
    SIZE=${SIZE:-0}
    TOTAL_CACHE_SIZE=$((TOTAL_CACHE_SIZE + SIZE))
    echo "  - PNPM cache: $(du -sh ~/.cache/pnpm 2>/dev/null | cut -f1)"
fi
if [ -d ~/.npm ]; then
    SIZE=$(du -sb ~/.npm 2>/dev/null | cut -f1 || echo "0")
    SIZE=${SIZE:-0}
    TOTAL_CACHE_SIZE=$((TOTAL_CACHE_SIZE + SIZE))
    echo "  - npm cache: $(du -sh ~/.npm 2>/dev/null | cut -f1)"
fi

TOTAL_CACHE_MB=$((TOTAL_CACHE_SIZE / 1024 / 1024))
if [ $TOTAL_CACHE_MB -gt 0 ]; then
    echo "  Total: ${TOTAL_CACHE_MB}MB"
fi

if prompt_user "Clean these cache directories? (y/n)"; then
    log_step "Cleaning cache directories..."
    [ -d ~/.cache/uv ] && rm -rf ~/.cache/uv && echo "  ✓ UV cache"
    [ -d ~/.cache/google-chrome ] && rm -rf ~/.cache/google-chrome && echo "  ✓ Chrome cache"
    [ -d ~/.cache/yarn ] && rm -rf ~/.cache/yarn && echo "  ✓ Yarn cache"
    [ -d ~/.cache/ms-playwright ] && rm -rf ~/.cache/ms-playwright && echo "  ✓ Playwright cache"
    [ -d ~/.cache/pnpm ] && rm -rf ~/.cache/pnpm && echo "  ✓ PNPM cache"
    [ -d ~/.npm ] && rm -rf ~/.npm && echo "  ✓ npm cache"
    [ -d ~/.cache/chromium ] && rm -rf ~/.cache/chromium && echo "  ✓ Chromium cache"
    log_success "Cache cleanup complete (freed ~${TOTAL_CACHE_MB}MB)"
    add_action "Cleared ${TOTAL_CACHE_MB}MB of cache"
else
    log_warning "Cache cleanup skipped"
fi
fi  # End dry-run skip for Phase 11

# === PHASE 13: Claude Temp File Cleanup ===
# Cleans orphaned temp files from ~/.claude/
# Exit on failure: No (cleanup is optional)
# Skip with: --quick flag
if [ "$QUICK" = true ]; then
    : # Skip silently in quick mode
elif [ "$DRY_RUN" = false ] && [ -d ~/.claude ]; then
    echo ""
    log_step "Cleaning orphaned Claude temp files..."

    ORPHAN_COUNT=0

    # Clean orphaned learner counter files (from old hook system)
    LEARNER_COUNT=$(find ~/.claude -maxdepth 1 -name "learner_counter_*" 2>/dev/null | wc -l)
    if [ "$LEARNER_COUNT" -gt 0 ]; then
        find ~/.claude -maxdepth 1 -name "learner_counter_*" -delete 2>/dev/null
        ORPHAN_COUNT=$((ORPHAN_COUNT + LEARNER_COUNT))
    fi

    # Clean orphaned security warnings state files
    SEC_COUNT=$(find ~/.claude -maxdepth 1 -name "security_warnings_state_*" 2>/dev/null | wc -l)
    if [ "$SEC_COUNT" -gt 0 ]; then
        find ~/.claude -maxdepth 1 -name "security_warnings_state_*" -delete 2>/dev/null
        ORPHAN_COUNT=$((ORPHAN_COUNT + SEC_COUNT))
    fi

    if [ "$ORPHAN_COUNT" -gt 0 ]; then
        log_success "Cleaned $ORPHAN_COUNT orphaned temp files"
    else
        log_success "No orphaned temp files found"
    fi
fi

# === PHASE 14: Claude Backup Cleanup ===
# Cleans .backups/ directory (old Claude Code backup files)
# Options: delete all, delete >7 days old, or skip
# Exit on failure: No (cleanup is optional)
# Skip with: --quick flag
if [ "$QUICK" = true ]; then
    : # Skip silently in quick mode
elif [ "$DRY_RUN" = false ] && [ -d .backups ]; then
    backup_count=$( (fd -t f 'backup-' .backups 2>/dev/null || true) | wc -l)
    if [ "$backup_count" -gt 0 ]; then
        echo ""
        log_step "Found $backup_count Claude backup files in .backups/"
        echo "Options:"
        echo "  1) Delete all backup files (keep only latest version in git)"
        echo "  2) Delete backups older than 7 days"
        echo "  3) Skip cleanup"
        read -p "Choose option (1-3): " -n 1 -r
        echo

        if [[ $REPLY == "1" ]]; then
            # Delete all backup files
            FREED_SPACE=$(fd -t f 'backup-' .backups 2>/dev/null -x du -sb | awk '{sum+=$1} END {print int(sum/1024/1024)}' || echo "0")
            fd -t f 'backup-' .backups 2>/dev/null -X rm
            log_success "Deleted $backup_count backups (freed ~${FREED_SPACE}MB)"
            add_action "Cleaned $backup_count backup files"
        elif [[ $REPLY == "2" ]]; then
            # Delete only old backups
            OLD_COUNT=$( (fd -t f 'backup-' .backups --changed-before 7d 2>/dev/null || true) | wc -l)
            FREED_SPACE=$(fd -t f 'backup-' .backups --changed-before 7d 2>/dev/null -x du -sb | awk '{sum+=$1} END {print int(sum/1024/1024)}' || echo "0")
            fd -t f 'backup-' .backups --changed-before 7d 2>/dev/null -X rm
            log_success "Deleted $OLD_COUNT old backups (freed ~${FREED_SPACE}MB)"
            add_action "Cleaned $OLD_COUNT old backup files"
        else
            log_warning "Claude backup cleanup skipped"
        fi
    fi
fi

fi  # End admin-only Phases 11-14

# Calculate total duration
REBUILD_END_TIME=$(date +%s)
TOTAL_DURATION=$((REBUILD_END_TIME - REBUILD_START_TIME))
DURATION_MINS=$((TOTAL_DURATION / 60))
DURATION_SECS=$((TOTAL_DURATION % 60))

if [ $DURATION_MINS -gt 0 ]; then
    DURATION_STR="${DURATION_MINS}m ${DURATION_SECS}s"
else
    DURATION_STR="${DURATION_SECS}s"
fi

# Print summary
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
if [ "$DRY_RUN" = true ]; then
echo "║                   Dry-Run Complete                         ║"
else
echo "║                   Rebuild Complete                         ║"
fi
echo "╠════════════════════════════════════════════════════════════╣"
printf "║  Duration: %-47s║\n" "$DURATION_STR"
if [ "$QUICK" = true ]; then
printf "║  Mode: %-50s║\n" "Quick (cleanup skipped)"
fi
echo "╚════════════════════════════════════════════════════════════╝"

if [ ${#ACTIONS[@]} -gt 0 ]; then
    echo ""
    echo "📋 Actions:"
    for action in "${ACTIONS[@]}"; do
        echo "   ✓ $action"
    done
fi

if [ ${#STATS[@]} -gt 0 ]; then
    echo ""
    echo "📊 Stats:"
    for stat in "${STATS[@]}"; do
        echo "   • $stat"
    done
fi

if [ "$AUDIT" = true ]; then
    echo ""
    echo "📋 Audit Files:"
    # shellcheck disable=SC2086,SC2012  # glob expansion intended; ls formatting for display
    ls -la "$AUDIT_DIR"/*-$TIMESTAMP* 2>/dev/null | while read -r line; do
        echo "   $line"
    done || echo "   (none generated)"
fi

if [ ${#WARNINGS[@]} -gt 0 ]; then
    echo ""
    echo -e "${YELLOW}⚠️  Warnings:${NC}"
    for warning in "${WARNINGS[@]}"; do
        echo "   • $warning"
    done
fi

if [ "$VERBOSE" = false ]; then
    echo ""
    echo "📁 Logs: $LOG_FILE"
fi

# Close the final phase + record overall completion before exit.
# These run AFTER the human-readable summary so the trap fires last and
# can attribute any teardown failures correctly.
step_complete
TOTAL_DURATION_MS=$(($(now_ms) - REBUILD_START_MS))
log_event complete rebuild "{\"duration_ms\":$TOTAL_DURATION_MS,\"dry_run\":$DRY_RUN,\"boot_mode\":$BOOT_MODE}"
write_last_status succ

echo ""
exit 0
