Ctrl K
ComunidadGuías…

install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash

# Pulse Installer Script
# Supports: Ubuntu 20.04+, Debian 11+, Proxmox VE 7+

set -euo pipefail

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

# Keep scripted transport independent of implicit per-user curl configuration.
# Every curl call uses standalone first-option -q; explicit flags and CA/proxy
# environment remain effective, including on legacy curl versions.

# Configuration helpers
default_install_dir_for_service() {
    local service_name="${1:-pulse}"
    if [[ "$service_name" == "pulse" ]]; then
        printf '/opt/pulse\n'
    else
        printf '/opt/%s\n' "$service_name"
    fi
}

default_config_dir_for_service() {
    local service_name="${1:-pulse}"
    if [[ "$service_name" == "pulse" ]]; then
        printf '/etc/pulse\n'
    else
        printf '/etc/%s\n' "$service_name"
    fi
}

default_binary_link_path_for_service() {
    local service_name="${1:-pulse}"
    if [[ "$service_name" == "pulse" ]]; then
        printf '/usr/local/bin/pulse\n'
    else
        printf '/usr/local/bin/%s\n' "$service_name"
    fi
}

default_update_helper_path_for_service() {
    local service_name="${1:-pulse}"
    if [[ "$service_name" == "pulse" ]]; then
        printf '/bin/update\n'
    else
        printf '/usr/local/bin/update-%s\n' "$service_name"
    fi
}

default_auto_update_dest_for_service() {
    local service_name="${1:-pulse}"
    if [[ "$service_name" == "pulse" ]]; then
        printf '/usr/local/bin/pulse-auto-update.sh\n'
    else
        printf '/usr/local/bin/%s-auto-update.sh\n' "$service_name"
    fi
}

default_update_service_path_for_service() {
    local service_name="${1:-pulse}"
    printf '/etc/systemd/system/%s-update.service\n' "$service_name"
}

default_update_timer_path_for_service() {
    local service_name="${1:-pulse}"
    printf '/etc/systemd/system/%s-update.timer\n' "$service_name"
}

# Configuration
DEFAULT_SERVICE_NAME="pulse"
SERVICE_NAME_EXPLICIT="false"
if [[ -n "${PULSE_SERVICE_NAME:-}" ]]; then
    SERVICE_NAME_EXPLICIT="true"
fi
SERVICE_NAME="${PULSE_SERVICE_NAME:-$DEFAULT_SERVICE_NAME}"
INSTALL_DIR="${PULSE_INSTALL_DIR:-$(default_install_dir_for_service "$SERVICE_NAME")}"
CONFIG_DIR="${PULSE_CONFIG_DIR:-$(default_config_dir_for_service "$SERVICE_NAME")}"  # All config and data goes here for manual installs
BINARY_LINK_PATH="${PULSE_BINARY_LINK_PATH:-$(default_binary_link_path_for_service "$SERVICE_NAME")}"
UPDATE_HELPER_PATH="${PULSE_UPDATE_HELPER_PATH:-$(default_update_helper_path_for_service "$SERVICE_NAME")}"
AUTO_UPDATE_DEST="${PULSE_AUTO_UPDATE_DEST:-$(default_auto_update_dest_for_service "$SERVICE_NAME")}"
UPDATE_SERVICE_PATH="${PULSE_UPDATE_SERVICE_PATH:-$(default_update_service_path_for_service "$SERVICE_NAME")}"
UPDATE_TIMER_PATH="${PULSE_UPDATE_TIMER_PATH:-$(default_update_timer_path_for_service "$SERVICE_NAME")}"
UPDATE_SERVICE_UNIT="$(basename "$UPDATE_SERVICE_PATH")"
UPDATE_TIMER_UNIT="$(basename "$UPDATE_TIMER_PATH")"
GITHUB_REPO="rcourtman/Pulse"
DOCKER_IMAGE_REPO="${DOCKER_IMAGE_REPO:-rcourtman/pulse}"
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
BUILD_FROM_SOURCE=false
SKIP_DOWNLOAD=false
IN_CONTAINER=false
IN_DOCKER=false
ENABLE_AUTO_UPDATES=false
AUTO_UPDATE_CHOICE_EXPLICIT=false
FORCE_VERSION=""
FORCE_CHANNEL=""
SKIP_UPGRADE_PREFLIGHT="${PULSE_SKIP_UPGRADE_PREFLIGHT:-false}"
UPGRADE_PREFLIGHT_RAN=false
ARCHIVE_OVERRIDE="${PULSE_ARCHIVE_PATH:-}"
SOURCE_BRANCH="main"
CURRENT_INSTALL_CTID=""
INSTALL_CONTAINER_CREATED=false
BUILD_FROM_SOURCE_MARKER="$INSTALL_DIR/BUILD_FROM_SOURCE"
DETECTED_CTID=""
UPDATE_MIN_TEMP_FREE_BYTES=$((900 * 1024 * 1024))
UPDATE_MIN_INSTALL_FREE_BYTES=$((256 * 1024 * 1024))
SERVER_STAGING_STALE_MINUTES=360
CONFIG_BACKUP_MIN_EXTRA_BYTES=$((64 * 1024 * 1024))

release_signature_key_available() {
    [[ -n "${PINNED_RELEASE_SSH_PUBLIC_KEY:-}" ]]
}

require_release_signature_verifier() {
    if ! release_signature_key_available; then
        echo "Pinned release signature key is not configured." >&2
        return 1
    fi
    if ! command -v ssh-keygen >/dev/null 2>&1; then
        echo "ssh-keygen is required to verify signed Pulse release assets." >&2
        return 1
    fi
}

verify_release_signature() {
    local target_path="$1"
    local signature_path="$2"
    local context="${3:-downloaded file}"
    local allowed_signers=""

    if ! require_release_signature_verifier; then
        return 1
    fi

    allowed_signers=$(mktemp /tmp/pulse-release-signers.XXXXXX)
    printf '%s %s\n' "$INSTALL_SIGNATURE_IDENTITY" "$PINNED_RELEASE_SSH_PUBLIC_KEY" > "$allowed_signers"

    if ! ssh-keygen -Y verify \
        -f "$allowed_signers" \
        -I "$INSTALL_SIGNATURE_IDENTITY" \
        -n "$INSTALL_SIGNATURE_NAMESPACE" \
        -s "$signature_path" < "$target_path" >/dev/null 2>&1; then
        rm -f "$allowed_signers"
        echo "Cryptographic signature verification failed for ${context}." >&2
        return 1
    fi

    rm -f "$allowed_signers"
    return 0
}

# Installer version - the major version this script is bundled with
INSTALLER_MAJOR_VERSION=5

AUTO_NODE_REGISTERED=false
AUTO_NODE_REGISTERED_NAME=""
AUTO_NODE_REGISTER_ERROR=""

DEBIAN_TEMPLATE_FALLBACK="debian-12-standard_12.12-1_amd64.tar.zst"
DEBIAN_TEMPLATE=""

bytes_to_human() {
    local bytes="${1:-0}"

    if [[ ! "$bytes" =~ ^[0-9]+$ ]]; then
        printf '%s\n' "$bytes"
        return 0
    fi

    local units=("B" "KB" "MB" "GB" "TB")
    local value="$bytes"
    local unit_index=0

    while (( value >= 1024 && unit_index < ${#units[@]} - 1 )); do
        value=$((value / 1024))
        ((unit_index += 1))
    done

    printf '%s%s\n' "$value" "${units[$unit_index]}"
}

get_available_bytes_for_path() {
    local path="$1"
    local available_kb=""

    available_kb=$(df -Pk "$path" 2>/dev/null | awk 'NR==2 {print $4}')
    if [[ ! "$available_kb" =~ ^[0-9]+$ ]]; then
        return 1
    fi

    printf '%s\n' $((available_kb * 1024))
}

get_filesystem_device_for_path() {
    local path="$1"
    local filesystem=""

    filesystem=$(df -Pk "$path" 2>/dev/null | awk 'NR==2 {print $1}')
    if [[ -z "$filesystem" ]]; then
        return 1
    fi

    printf '%s\n' "$filesystem"
}

get_directory_size_bytes() {
    local path="$1"
    local used_kb=""

    used_kb=$(du -sk "$path" 2>/dev/null | awk 'NR==1 {print $1}')
    if [[ ! "$used_kb" =~ ^[0-9]+$ ]]; then
        return 1
    fi

    printf '%s\n' $((used_kb * 1024))
}

ensure_update_disk_headroom() {
    local temp_path="${1:-/tmp}"
    local install_path="${2:-$INSTALL_DIR}"
    local temp_fs=""
    local install_fs=""
    local temp_free_bytes=""
    local install_free_bytes=""
    local combined_required_bytes=$((UPDATE_MIN_TEMP_FREE_BYTES + UPDATE_MIN_INSTALL_FREE_BYTES))

    temp_fs=$(get_filesystem_device_for_path "$temp_path" 2>/dev/null || true)
    install_fs=$(get_filesystem_device_for_path "$install_path" 2>/dev/null || true)
    temp_free_bytes=$(get_available_bytes_for_path "$temp_path" 2>/dev/null || true)
    install_free_bytes=$(get_available_bytes_for_path "$install_path" 2>/dev/null || true)

    if [[ -z "$temp_free_bytes" || -z "$install_free_bytes" ]]; then
        print_warn "Could not determine available disk space for the update preflight; continuing anyway"
        return 0
    fi

    if [[ -n "$temp_fs" && "$temp_fs" == "$install_fs" ]]; then
        if (( temp_free_bytes < combined_required_bytes )); then
            print_error "Not enough free disk space to stage the Pulse update"
            print_info "The same filesystem backs $temp_path and $install_path"
            print_info "Available: $(bytes_to_human "$temp_free_bytes"), required: $(bytes_to_human "$combined_required_bytes")"
            print_info "Free disk space and retry the update"
            return 1
        fi
        return 0
    fi

    if (( temp_free_bytes < UPDATE_MIN_TEMP_FREE_BYTES )); then
        print_error "Not enough free disk space in $temp_path to stage the Pulse update"
        print_info "Available: $(bytes_to_human "$temp_free_bytes"), required: $(bytes_to_human "$UPDATE_MIN_TEMP_FREE_BYTES")"
        print_info "Free disk space under $temp_path and retry the update"
        return 1
    fi

    if (( install_free_bytes < UPDATE_MIN_INSTALL_FREE_BYTES )); then
        print_error "Not enough free disk space in $install_path to apply the Pulse update"
        print_info "Available: $(bytes_to_human "$install_free_bytes"), required: $(bytes_to_human "$UPDATE_MIN_INSTALL_FREE_BYTES")"
        print_info "Free disk space under $install_path and retry the update"
        return 1
    fi

    return 0
}

ensure_config_backup_headroom() {
    local config_path="${1:-$CONFIG_DIR}"
    local backup_parent="${2:-}"
    local free_bytes=""
    local config_bytes=""
    local required_bytes=""

    if [[ ! -d "$config_path" ]]; then
        return 0
    fi

    if [[ -z "$backup_parent" ]]; then
        backup_parent="$(dirname "$config_path")"
    fi
    free_bytes=$(get_available_bytes_for_path "$backup_parent" 2>/dev/null || true)
    config_bytes=$(get_directory_size_bytes "$config_path" 2>/dev/null || true)

    if [[ -z "$free_bytes" || -z "$config_bytes" ]]; then
        print_warn "Could not determine disk headroom for configuration backup; continuing anyway"
        return 0
    fi

    required_bytes=$((config_bytes + CONFIG_BACKUP_MIN_EXTRA_BYTES))
    if (( free_bytes < required_bytes )); then
        print_error "Not enough free disk space to back up the existing Pulse configuration"
        print_info "Configuration size: $(bytes_to_human "$config_bytes")"
        print_info "Available in $backup_parent: $(bytes_to_human "$free_bytes"), required: $(bytes_to_human "$required_bytes")"
        print_info "Free disk space or remove old configuration backups under $backup_parent, then retry the update"
        return 1
    fi

    return 0
}

get_latest_release_from_redirect() {
    # Follow the GitHub "latest" redirect and extract the tag in a way that
    # tolerates intermediate redirects that omit /tag/ (issue #698).
    local target_url="${1:-https://github.com/$GITHUB_REPO/releases/latest}"
    local effective_url=""
    local curl_cmd=(curl -q -fsSL --connect-timeout 5 --max-time 10 -o /dev/null -w '%{url_effective}' "$target_url")

    if command -v timeout >/dev/null 2>&1; then
        effective_url=$(timeout 10 "${curl_cmd[@]}" 2>/dev/null || true)
    else
        effective_url=$("${curl_cmd[@]}" 2>/dev/null || true)
    fi

    # Strip stray carriage returns so string comparisons behave under set -u
    effective_url="${effective_url//$'\r'/}"

    if [[ -z "$effective_url" ]]; then
        return 1
    fi

    local tag=""
    if [[ "$effective_url" =~ /tag/([^/?#]+) ]]; then
        tag="${BASH_REMATCH[1]}"
    elif [[ "$effective_url" =~ /download/([^/?#]+)/ ]]; then
        tag="${BASH_REMATCH[1]}"
    fi

    if [[ -z "$tag" ]]; then
        return 1
    fi

    printf '%s\n' "$tag"
    return 0
}

is_stable_release_tag() {
    local tag="${1:-}"
    tag="${tag#v}"

    [[ "$tag" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]
}

is_pulse_release_tag() {
    local tag="${1:-}"
    tag="${tag#v}"

    [[ "$tag" =~ ^[0-9]+[.][0-9]+[.][0-9]+(-(beta|rc)[.][0-9]+)?$ ]]
}

latest_pulse_release_tag_from_json() {
    local releases_json="${1:-}"
    local channel="${2:-stable}"
    local latest_release=""

    if [[ -z "$releases_json" ]] || [[ "$channel" != stable && "$channel" != rc ]]; then
        return 1
    fi

    # Tag-shaped text is not release metadata: regex extraction loses draft
    # and prerelease ownership. Without a parser, use only the caller's checked
    # public redirect (or an explicitly pinned version), never a guessed list.
    command -v jq >/dev/null 2>&1 || return 1
    latest_release=$(printf '%s' "$releases_json" | jq -er -s --arg channel "$channel" '
        if length != 1 or (.[0] | type) != "array" then
            error("Expected one release array")
        else .[0] end
        | [.[] | select(type == "object")
            | select(.draft == false and (.prerelease | type) == "boolean")
            | select(.tag_name | type == "string")
            | select(.tag_name | test("^v?[0-9]+\\.[0-9]+\\.[0-9]+(-(beta|rc)\\.[0-9]+)?\\z"))
            | select($channel == "rc" or (.prerelease == false
                and (.tag_name | test("^v?[0-9]+\\.[0-9]+\\.[0-9]+\\z"))))]
        | .[0].tag_name // empty
    ' 2>/dev/null) || return 1

    if is_pulse_release_tag "$latest_release"; then
        printf '%s\n' "$latest_release"
        return 0
    fi

    return 1
}

latest_stable_release_tag_from_json() {
    latest_pulse_release_tag_from_json "${1:-}" stable
}

read_configured_update_channel() {
    if [[ "${IGNORE_CONFIGURED_UPDATE_CHANNEL:-false}" == "true" ]]; then
        return 1
    fi

    if [[ ! -f "$CONFIG_DIR/system.json" ]]; then
        return 1
    fi

    local configured_channel=""
    configured_channel=$(grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/system.json" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/' || true)
    if [[ -z "$configured_channel" ]]; then
        return 1
    fi

    printf '%s\n' "$configured_channel"
}

resolve_install_script_download_url() {
    if [[ -n "${FORCE_VERSION:-}" ]]; then
        printf 'https://github.com/%s/releases/download/%s/install.sh\n' "$GITHUB_REPO" "$FORCE_VERSION"
        return 0
    fi

    local channel="${FORCE_CHANNEL:-${UPDATE_CHANNEL:-stable}}"
    if [[ "$channel" != "rc" ]]; then
        local stable_release=""
        stable_release=$(resolve_latest_release_tag_for_channel stable 2>/dev/null || true)
        if [[ -z "$stable_release" || "$stable_release" == "null" ]]; then
            return 1
        fi
        printf 'https://github.com/%s/releases/download/%s/install.sh\n' "$GITHUB_REPO" "$stable_release"
        return 0
    fi

    local rc_release=""
    rc_release=$(resolve_latest_release_tag_for_channel rc 2>/dev/null || true)
    if [[ -z "$rc_release" || "$rc_release" == "null" ]]; then
        return 1
    fi

    printf 'https://github.com/%s/releases/download/%s/install.sh\n' "$GITHUB_REPO" "$rc_release"
}

resolve_release_asset_base_url() {
    if [[ -n "${LATEST_RELEASE:-}" ]]; then
        printf 'https://github.com/%s/releases/download/%s\n' "$GITHUB_REPO" "$LATEST_RELEASE"
        return 0
    fi

    printf 'https://github.com/%s/releases/latest/download\n' "$GITHUB_REPO"
}

selected_update_channel() {
    if [[ -n "${FORCE_CHANNEL:-}" ]]; then
        printf '%s\n' "$FORCE_CHANNEL"
        return 0
    fi

    if [[ -n "${FORCE_VERSION:-}" ]]; then
        if [[ "$FORCE_VERSION" == *-* ]]; then
            printf 'rc\n'
        else
            printf 'stable\n'
        fi
        return 0
    fi

    if [[ -n "${UPDATE_CHANNEL:-}" ]]; then
        printf '%s\n' "$UPDATE_CHANNEL"
        return 0
    fi

    local configured_channel=""
    configured_channel=$(read_configured_update_channel 2>/dev/null || true)
    if [[ -n "$configured_channel" ]]; then
        printf '%s\n' "$configured_channel"
        return 0
    fi

    printf 'stable\n'
}

build_container_install_command() {
    local install_cmd="bash /tmp/install.sh --in-container"
    local quoted_archive=""

    if [[ -n "${FORCE_VERSION:-}" ]]; then
        install_cmd="$install_cmd --version '$FORCE_VERSION'"
    elif [[ "$(selected_update_channel)" == "rc" ]]; then
        install_cmd="$install_cmd --rc"
    fi

    if [[ -n "${container_archive_dest:-}" ]]; then
        printf -v quoted_archive '%q' "$container_archive_dest"
        install_cmd="$install_cmd --archive $quoted_archive"
    fi
    if [[ -n "${auto_updates_flag:-}" ]]; then
        install_cmd="$install_cmd $auto_updates_flag"
    fi
    if [[ "${BUILD_FROM_SOURCE:-false}" == "true" ]]; then
        install_cmd="$install_cmd --source '$SOURCE_BRANCH'"
    fi
    if [[ "${frontend_port:-7655}" != "7655" ]]; then
        install_cmd="FRONTEND_PORT=$frontend_port $install_cmd"
    fi

    printf '%s\n' "$install_cmd"
}

print_container_recovery_command() {
    local install_cmd
    install_cmd=$(build_container_install_command)
    print_info "  $install_cmd"
}

detect_lxc_ctid() {
    local ctid=""

    if [[ -r /proc/1/cgroup ]]; then
        ctid=$(sed 's/\\x2d/-/g' /proc/1/cgroup 2>/dev/null | grep -Eo '(lxc|machine-lxc)-[0-9]+' | tail -n1 | grep -Eo '[0-9]+' | tail -n1)
        if [[ -n "$ctid" ]]; then
            echo "$ctid"
            return
        fi
    fi

    if command -v hostname >/dev/null 2>&1; then
        ctid=$(hostname 2>/dev/null || true)
        if [[ "$ctid" =~ ^[0-9]+$ ]]; then
            echo "$ctid"
            return
        fi
    fi

    if command -v hostnamectl >/dev/null 2>&1; then
        ctid=$(hostnamectl hostname 2>/dev/null || true)
        if [[ "$ctid" =~ ^[0-9]+$ ]]; then
            echo "$ctid"
            return
        fi
    fi

    if command -v findmnt >/dev/null 2>&1; then
        local mount_src
        mount_src=$(findmnt -no SOURCE / 2>/dev/null || true)
        if [[ "$mount_src" =~ -([0-9]+)-disk ]]; then
            echo "${BASH_REMATCH[1]}"
            return
        fi
    fi

    if command -v df >/dev/null 2>&1; then
        local root_src
        root_src=$(df -P / 2>/dev/null | awk 'NR==2 {print $1}')
        if [[ "$root_src" =~ -([0-9]+)-disk ]]; then
            echo "${BASH_REMATCH[1]}"
            return
        fi
    fi
}

auto_detect_container_environment() {
    if [[ "$IN_CONTAINER" == "true" ]]; then
        if [[ -z "$DETECTED_CTID" ]]; then
            DETECTED_CTID=$(detect_lxc_ctid 2>/dev/null || true)
        fi
        return
    fi

    local virt_type=""
    if command -v systemd-detect-virt >/dev/null 2>&1; then
        virt_type=$(systemd-detect-virt --container 2>/dev/null || true)
        if [[ -n "$virt_type" && "$virt_type" != "none" ]]; then
            IN_CONTAINER=true
            if [[ "$virt_type" == "docker" ]]; then
                IN_DOCKER=true
            fi
        fi
    fi

    if [[ "$IN_CONTAINER" != "true" ]]; then
        if [[ -f /.dockerenv ]] || grep -qa docker /proc/1/cgroup 2>/dev/null || grep -qa docker /proc/self/cgroup 2>/dev/null; then
            IN_CONTAINER=true
            IN_DOCKER=true
        elif grep -qaE '(lxc|machine-lxc)' /proc/1/cgroup 2>/dev/null; then
            IN_CONTAINER=true
        elif [[ -r /proc/1/environ ]] && grep -qa 'container=lxc' /proc/1/environ 2>/dev/null; then
            IN_CONTAINER=true
        fi
    fi

    if [[ "$IN_CONTAINER" == "true" ]]; then
        if [[ -z "$DETECTED_CTID" ]]; then
            DETECTED_CTID=$(detect_lxc_ctid 2>/dev/null || true)
        fi
    fi
}

handle_container_install_exit() {
    local install_status="$1"
    # Preserve the original failure, even if removing owned input scratch fails.
    if ! cleanup_container_install_inputs "${container_script_temp_dir:-}" "${container_archive_source:-}" "${container_archive_temp:-false}"; then
        print_warn "Could not remove all temporary installation inputs."
        if [[ "$install_status" -eq 0 ]]; then
            install_status=1
        fi
    fi
    if [[ "$install_status" -ne 0 && "$INSTALL_CONTAINER_CREATED" == "true" && -n "$CURRENT_INSTALL_CTID" ]]; then
        print_warn "Container $CURRENT_INSTALL_CTID has been retained for inspection and recovery."
        print_info "No automatic stop or deletion was requested; an interrupted command may still be running."
        print_info "Check its state and logs before continuing:"
        print_info "  pct status $CURRENT_INSTALL_CTID"
        print_info "  pct enter $CURRENT_INSTALL_CTID"
        print_info "Do not rerun the host installer to recover this container: that creates another instance."
        print_info "If this new container is no longer needed, remove it explicitly in Proxmox."
    fi
    exit "$install_status"
}

handle_install_interrupt() {
    echo ""
    print_error "Installation interrupted"
    # The EXIT handler removes only owned temporary inputs, never the container
    # or state a partially successful installation may already have created.
    exit "${1:-1}"
}

# A TERM-only timeout can wait forever for a stuck manager. Bound its process
# group too; killing this client does not establish the service's actual state.
safe_systemctl() {
    local action="$1"
    shift
    timeout -k 1 5 systemctl "$action" "$@" 2>/dev/null || {
        if [[ "$action" == "daemon-reload" ]]; then
            # daemon-reload hanging is common in unprivileged containers, silent fail is OK
            return 0
        elif [[ "$action" == "start" || "$action" == "enable" ]]; then
            print_info "Note: systemctl $action failed (may be in unprivileged container)"
            return 1
        else
            return 1
        fi
    }
}

# A failed/timed-out is-active query is not evidence that replacement is safe.
# Require an explicit stopped state, and read it back after a successful stop.
# Do not restart after an uncertain stop: the operator must reconcile that state.
stop_pulse_for_replacement() {
    local service_name="$1"
    local state=""
    if ! state=$(timeout -k 1 5 systemctl show "$service_name" --property=ActiveState --value 2>/dev/null); then
        print_error "Could not read Pulse service state ($service_name); refusing binary replacement"
        return 1
    fi
    case "$state" in
        inactive) return 0 ;;
        active) PULSE_WAS_ACTIVE="true" ;;
        failed) ;; # A failed unit can retain processes; stop it without claiming prior liveness.
        *)
            print_error "Pulse service state is not settled ($service_name); refusing binary replacement"
            return 1
            ;;
    esac

    print_info "Stopping existing Pulse service ($service_name)..."
    if ! safe_systemctl stop "$service_name"; then
        print_error "Could not stop Pulse ($service_name); refusing binary replacement. Check service state before retrying."
        return 1
    fi
    if ! state=$(timeout -k 1 5 systemctl show "$service_name" --property=ActiveState --value 2>/dev/null); then
        print_error "Could not confirm Pulse stopped ($service_name); refusing binary replacement"
        return 1
    fi
    case "$state" in
        inactive) return 0 ;;
        *)
            print_error "Pulse has not reached a stopped state ($service_name); refusing binary replacement"
            return 1
            ;;
    esac
}

# Removal is destructive: neither an is-active error nor a successful stop
# proves the process is gone. Timers and in-flight updaters must be quiescent
# too, before deleting their executable or the server's persistent files.
confirm_pulse_unit_removable() {
    local unit="$1"
    local load_state="" active_state="" unit_file_state=""
    if ! load_state=$(timeout -k 1 5 systemctl show "$unit" --property=LoadState --value 2>/dev/null) ||
       ! active_state=$(timeout -k 1 5 systemctl show "$unit" --property=ActiveState --value 2>/dev/null); then
        print_error "Cannot confirm service state ($unit); refusing removal. Files are preserved; reconcile service state before retrying."
        return 1
    fi
    if [[ "$active_state" != "inactive" ]]; then
        print_error "Unit is not confirmed inactive ($unit); refusing removal. Files are preserved; reconcile service state before retrying."
        return 1
    fi
    [[ "$load_state" == "not-found" ]] && return 0
    case "$load_state" in
        loaded|masked) ;;
        *) print_error "Unknown unit load state ($unit); refusing removal"; return 1 ;;
    esac
    if ! unit_file_state=$(timeout -k 1 5 systemctl show "$unit" --property=UnitFileState --value 2>/dev/null); then
        print_error "Cannot confirm unit disabled ($unit); refusing removal"
        return 1
    fi
    case "$unit_file_state" in
        disabled|static|indirect|masked|masked-runtime) return 0 ;;
        *) print_error "Unit is not confirmed disabled ($unit); refusing removal"; return 1 ;;
    esac
}

stop_pulse_unit_for_removal() {
    local unit="$1"
    local load_state="" active_state=""
    if ! load_state=$(timeout -k 1 5 systemctl show "$unit" --property=LoadState --value 2>/dev/null) ||
       ! active_state=$(timeout -k 1 5 systemctl show "$unit" --property=ActiveState --value 2>/dev/null); then
        print_error "Cannot inspect unit ($unit); refusing removal"
        return 1
    fi
    if [[ "$load_state" == "not-found" && "$active_state" == "inactive" ]]; then
        return 0
    fi
    case "$load_state" in
        loaded|masked) ;;
        *) print_error "Unknown unit load state ($unit); refusing removal"; return 1 ;;
    esac
    case "$active_state" in
        inactive) ;;
        active|failed)
            if ! timeout -k 1 5 systemctl stop "$unit"; then
                print_error "Cannot stop unit ($unit); refusing removal. Reconcile service state before retrying."
                return 1
            fi
            ;;
        *) print_error "Unsettled unit state ($unit); refusing removal"; return 1 ;;
    esac
    if ! timeout -k 1 5 systemctl disable "$unit"; then
        print_error "Cannot disable unit ($unit); refusing removal"
        return 1
    fi
    confirm_pulse_unit_removable "$unit"
}

quiesce_pulse_for_removal() {
    local service_name="$1" unit=""
    local units=("$UPDATE_TIMER_UNIT" "$UPDATE_SERVICE_UNIT" "${service_name%.service}.service")
    if [[ "$SERVICE_NAME_EXPLICIT" != "true" ]]; then
        units+=(pulse.service pulse-backend.service)
    fi
    # A full uninstall also removes the local legacy footprint. Refuse before
    # any server deletion if that footprint cannot be safely quiesced.
    if local_sensor_proxy_present; then
        units+=(pulse-sensor-proxy-selfheal.timer pulse-sensor-cleanup.path
            pulse-sensor-proxy-selfheal.service pulse-sensor-cleanup.service pulse-sensor-proxy.service)
    fi
    for unit in "${units[@]}"; do
        stop_pulse_unit_for_removal "$unit" || return 1
    done
    # Reconcile all targets again, not just the last one stopped. An earlier
    # unit that reactivated/re-enabled during this sequence blocks deletion.
    for unit in "${units[@]}"; do
        confirm_pulse_unit_removable "$unit" || return 1
    done
}

# These helpers are explicitly checked by both removal entries. Bash suppresses
# errexit inside a function called from an if/OR-list; set -e is not a deletion
# result. Stop at the first failure, without restarting or claiming rollback.
remove_pulse_path_for_removal() {
    local path="$1" kind="${2:-file}"
    if [[ "$kind" == directory ]]; then
        if rm -rf -- "$path"; then return 0; fi
    else
        if rm -f -- "$path"; then return 0; fi
    fi
    print_error "Cannot remove $path; Pulse removal is incomplete. Some files may already have been removed. Inspect retained files before recovery; no automatic restart will be attempted."
    return 1
}

remove_pulse_runtime_files() {
    local service_name="$1" path resolved config_path
    local paths=("/etc/systemd/system/${service_name%.service}.service"
        "$UPDATE_SERVICE_PATH" "$UPDATE_TIMER_PATH"
        "$BINARY_LINK_PATH" "$AUTO_UPDATE_DEST")
    if [[ "$SERVICE_NAME_EXPLICIT" != true ]]; then
        paths+=(/etc/systemd/system/pulse.service /etc/systemd/system/pulse-backend.service)
    fi
    if [[ "$UPDATE_HELPER_PATH" != /bin/update ]]; then
        paths+=("$UPDATE_HELPER_PATH")
    fi
    paths+=("$INSTALL_DIR")

    # Configuration is a separate, last deletion phase. Custom paths (including
    # symlinked parents) must not let runtime removal consume it before consent
    # or a later failure. Validate the whole layout before the first deletion.
    if ! config_path=$(readlink -m -- "$CONFIG_DIR") || [[ "$config_path" == / ]]; then
        print_error "Cannot safely separate configuration from runtime files; refusing removal"
        return 1
    fi
    for path in "${paths[@]}"; do
        if ! resolved=$(readlink -m -- "$path") ||
           [[ "$resolved" == / || "$resolved" == "$config_path" ||
              "$resolved" == "$config_path/"* || "$config_path" == "$resolved/"* ]]; then
            print_error "Runtime path overlaps configuration or is unsafe ($path); refusing removal. No files have been removed."
            return 1
        fi
    done
    for path in "${paths[@]}"; do
        if [[ "$path" == "$INSTALL_DIR" ]]; then
            remove_pulse_path_for_removal "$path" directory || return 1
        else
            remove_pulse_path_for_removal "$path" || return 1
        fi
    done
}

reload_pulse_units_after_removal() {
    # Unlike update-time best-effort reloads, removal cannot declare completion
    # while systemd's view of deleted units is unknown.
    if ! timeout -k 1 5 systemctl daemon-reload; then
        print_error "Cannot reload service definitions; Pulse removal is incomplete. Configuration is retained; reconcile service state before recovery."
        return 1
    fi
}

remove_pulse_user_for_removal() {
    if id pulse &>/dev/null; then
        if ! userdel pulse; then
            print_error "Cannot remove the pulse account; Pulse removal is incomplete. Configuration is retained; inspect remaining account use before recovery."
            return 1
        fi
    fi
}

# Called only after a confirmed stop and a failed atomic rename. The previous
# executable is still in place. An uncertain stop never reaches this recovery,
# and an intentionally inactive/failed service must not be started for the user.
recover_pulse_after_failed_replacement() {
    local service_name="$1"
    [[ "$PULSE_WAS_ACTIVE" == "true" ]] || return 0
    if safe_systemctl start "$service_name" && wait_for_service_active "$service_name" 20; then
        PULSE_WAS_ACTIVE="false"
        print_info "Pulse service is running again; the update failed"
    else
        print_error "The update failed and the Pulse service could not be confirmed running. Check systemctl status $service_name before retrying."
        return 1
    fi
}

# Read the unit-file inventory for exact unit names. systemd exits 1 and prints
# nothing at all, on stdout or stderr, when none of the named unit files exist,
# which is every fresh host. That is an empty inventory. A timeout, a signal,
# another failed status, or exit 1 with output or a message remains a failure.
# Capture both streams in the same read:
# a later silent sample must never discard an earlier diagnostic.
unit_file_inventory() {
    local out="" rc=0
    # Keep trailing newlines: an emitted blank line is not a silent exit 1.
    out=$(LC_ALL=C timeout -k 1 5 systemctl list-unit-files --no-legend --no-pager --full -- "$@" 2>&1; rc=$?; printf '.'; exit "$rc") || rc=$?
    out="${out%.}"
    if (( rc == 1 )) && [[ -z "$out" ]]; then
        rc=0
    fi
    printf '%s' "$out"
    return "$rc"
}

# Detect existing service name (pulse or pulse-backend)
detect_service_name() {
    if [[ "$SERVICE_NAME_EXPLICIT" == "true" ]]; then
        echo "$SERVICE_NAME"
        return
    fi

    if ! command -v systemctl >/dev/null 2>&1; then
        echo "$DEFAULT_SERVICE_NAME"
        return
    fi

    local unit_files="" unit="" state="" preset="" extra=""
    local seen_backend=false seen_pulse=false
    # grep -q closes a full inventory early: systemctl can report Broken pipe,
    # and pipefail then hides a real pulse-backend unit behind the default.
    # Consume one bounded inventory of the two exact historical names instead.
    if ! unit_files=$(unit_file_inventory pulse-backend.service pulse.service); then
        print_error "Cannot identify the Pulse systemd service: unit inventory failed. Reconcile service state before retrying." >&2
        return 1
    fi
    while read -r unit state preset extra; do
        [[ -n "$unit" ]] || continue
        if [[ "$unit" == pulse-backend.service && "$seen_backend" == false ]]; then
            seen_backend=true
        elif [[ "$unit" == pulse.service && "$seen_pulse" == false ]]; then
            seen_pulse=true
        else
            print_error "Cannot identify the Pulse systemd service: unexpected unit inventory. No default service was selected." >&2
            return 1
        fi
        case "$state" in
            enabled|enabled-runtime|disabled|static|indirect|linked|linked-runtime|alias|generated|transient|masked|masked-runtime) ;;
            *)
                print_error "Cannot identify the Pulse systemd service: unknown unit state. No default service was selected." >&2
                return 1 ;;
        esac
        if [[ -n "$extra" ]]; then
            print_error "Cannot identify the Pulse systemd service: malformed unit inventory. No default service was selected." >&2
            return 1
        fi
    done <<< "$unit_files"
    if [[ "$seen_backend" == true ]]; then
        echo "pulse-backend"
    else
        echo "$DEFAULT_SERVICE_NAME"  # A successful empty read permits a new installation.
    fi
}

update_timer_exists() {
    command -v systemctl >/dev/null 2>&1 || return 1
    local unit_files=""
    # Rows also contain enablement/preset columns. Match the exact first field,
    # not the whole row or a regex, and keep long custom unit names untruncated.
    # A failed inventory must not admit even plausible partial stdout.
    if ! unit_files=$(LC_ALL=C timeout -k 1 5 systemctl list-unit-files --no-legend --no-pager --full -- "$UPDATE_TIMER_UNIT" 2>/dev/null); then
        return 1
    fi
    awk -v unit="$UPDATE_TIMER_UNIT" '$1 == unit { found = 1 } END { exit !found }' <<< "$unit_files"
}

print_auto_update_status() {
    local properties="" key="" value="" load="" setting="" active=""
    local seen_load=0 seen_setting=0 seen_active=0 valid=false
    # One bounded snapshot separates boot enablement from the running timer.
    # A failed read (even with plausible partial output) is not disabled or
    # absent, and must not suggest changing the operator's saved choice.
    if command -v systemctl >/dev/null 2>&1 &&
       properties=$(LC_ALL=C timeout -k 1 5 systemctl show --no-pager --all --property=LoadState --property=UnitFileState --property=ActiveState -- "$UPDATE_TIMER_UNIT" 2>/dev/null); then
        valid=true
        while IFS='=' read -r key value; do
            case "$key" in
                LoadState) load="$value"; seen_load=$((seen_load + 1)) ;;
                UnitFileState) setting="$value"; seen_setting=$((seen_setting + 1)) ;;
                ActiveState) active="$value"; seen_active=$((seen_active + 1)) ;;
                *) valid=false ;;
            esac
        done <<< "$properties"
        if [[ "$seen_load" != 1 || "$seen_setting" != 1 || "$seen_active" != 1 ]]; then
            valid=false
        fi
        if [[ "$valid" == true && "$load" == not-found && -z "$setting" && "$active" == inactive ]]; then
            return 0  # A confirmed absent timer needs no completion section.
        fi
        case "$load" in loaded|masked) ;; *) valid=false ;; esac
        case "$setting" in
            enabled|enabled-runtime|disabled|static|indirect|linked|linked-runtime|alias|generated|transient|masked|masked-runtime) ;;
            *) valid=false ;;
        esac
        case "$active" in
            active|reloading|inactive|failed|activating|deactivating|maintenance|refreshing) ;;
            *) valid=false ;;
        esac
    fi

    echo
    echo -e "${YELLOW}Auto-update timer:${NC}"
    if [[ "$valid" != true ]]; then
        echo -e "  Status:     ${YELLOW}Unknown (could not read timer state)${NC}"
    else
        echo "  Boot setting: $setting"
        echo "  Timer state:  $active"
        if [[ "$load" == loaded && "$setting" == enabled && "$active" == active ]]; then
            echo "  Schedule:   daily check between 2-6 AM"
            echo "  Disable:    systemctl disable --now $UPDATE_TIMER_UNIT"
        elif [[ "$load" == loaded && "$setting" == disabled && "$active" == inactive ]]; then
            echo "  Enable:     systemctl enable --now $UPDATE_TIMER_UNIT"
        fi
    fi
    # Inspection, not enable/start/unmask advice, for failed or unsettled reads.
    echo "  Check:      systemctl status $UPDATE_TIMER_UNIT"
}

current_frontend_port() {
    local port="${FRONTEND_PORT:-7655}"
    if [[ -z "${FRONTEND_PORT:-}" ]] && [[ -f "/etc/systemd/system/$SERVICE_NAME.service" ]]; then
        port=$(grep -oP 'FRONTEND_PORT=\K\d+' "/etc/systemd/system/$SERVICE_NAME.service" 2>/dev/null || echo "7655")
    fi
    printf '%s\n' "${port:-7655}"
}

# Functions
print_header() {
    echo -e "${BLUE}=================================================${NC}"
    echo -e "${BLUE}           Pulse Installation Script${NC}"
    echo -e "${BLUE}=================================================${NC}"
    echo
}

# Safe read function that works with or without TTY
safe_read() {
    local prompt="$1"
    local var_name="$2"
    shift 2
    local read_args="$@"  # Allow passing additional args like -n 1
    
    # When script is piped (curl | bash), stdin is the pipe, not the terminal
    # We need to read from /dev/tty for user input
    if [[ -t 0 ]]; then
        # stdin is a terminal, read normally
        echo -n "$prompt"
        IFS= read -r $read_args "$var_name"
        return 0
    else
        # stdin is not a terminal (piped), try /dev/tty if available
        if { exec 3< /dev/tty; } 2>/dev/null; then
            # /dev/tty is available and usable
            echo -n "$prompt"
            IFS= read -r $read_args "$var_name" <&3
            exec 3<&-
            return 0
        else
            # No TTY available at all - truly non-interactive
            # Don't try to read from piped stdin as it will hang
            return 1
        fi
    fi
}

# Wrapper that handles safe_read with automatic error handling
safe_read_with_default() {
    local prompt="$1"
    local var_name="$2"
    local default_value="$3"
    shift 3
    local read_args="$@"
    
    # Temporarily disable errexit
    set +e
    safe_read "$prompt" "$var_name" $read_args
    local read_result=$?
    set -e
    
    if [[ $read_result -ne 0 ]]; then
        # Failed to read - use default
        eval "$var_name='$default_value'"
        # Only print default message in truly non-interactive mode
        if [[ ! -t 0 ]] && [[ -n "$default_value" ]]; then
            print_info "Using default: $default_value"
        fi
        return 0  # Return success since we handled it with a default
    fi
    
    # Check if empty and use default
    local current_value
    eval "current_value=\$$var_name"
    if [[ -z "$current_value" ]]; then
        eval "$var_name='$default_value'"
    fi
    
    return 0
}

# A version change is not consent to unattended updates. Keep the existing
# choice (including a disabled timer) and require an affirmative answer when
# offering updates on an installation without an enabled configuration.
offer_existing_auto_updates() {
    if [[ "$AUTO_UPDATE_CHOICE_EXPLICIT" == "true" ]] || [[ "$ENABLE_AUTO_UPDATES" == "true" ]] || [[ "$IN_DOCKER" == "true" ]]; then
        return 0
    fi

    local prompt_reason=""
    if ! update_timer_exists; then
        prompt_reason="missing"
    elif [[ -f "$CONFIG_DIR/system.json" ]] && grep -Eq '"autoUpdateEnabled"[[:space:]]*:[[:space:]]*false' "$CONFIG_DIR/system.json"; then
        prompt_reason="disabled"
    fi
    [[ -n "$prompt_reason" ]] || return 0

    echo
    if [[ "$prompt_reason" == "disabled" ]]; then
        echo -e "${YELLOW}Auto-updates are currently disabled.${NC}"
    else
        echo "Automatic updates are not configured."
    fi
    echo "Pulse can automatically install stable updates daily (between 2-6 AM)"
    echo "Leave this disabled to keep control of version changes and rollbacks."
    local enable_updates=""
    safe_read_with_default "Enable auto-updates? [y/N]: " enable_updates "n"
    if [[ "$enable_updates" =~ ^([Yy]|[Yy][Ee][Ss])$ ]]; then
        ENABLE_AUTO_UPDATES=true
    fi
    return 0
}

wait_for_pulse_ready() {
    local pulse_url="$1"
    local retries="${2:-60}"
    local delay="${3:-1}"

    if [[ -z "$pulse_url" ]] || ! command -v curl >/dev/null 2>&1; then
        return 0
    fi

    local api_endpoint="${pulse_url%/}/api/health"
    print_info "Waiting for Pulse API at ${api_endpoint}..."
    for attempt in $(seq 1 "$retries"); do
        if curl -q -fsS --max-time 2 "$api_endpoint" >/dev/null 2>&1; then
            print_info "Pulse API is reachable"
            return 0
        fi
        sleep "$delay"
    done

    print_warn "Pulse API did not respond after ${retries}s; continuing anyway"
    return 1
}

print_error() {
    echo -e "${RED}[ERROR] $1${NC}" >&2
}

print_success() {
    echo -e "${GREEN}[SUCCESS] $1${NC}"
}

print_info() {
    echo -e "${YELLOW}[INFO] $1${NC}"
}

print_warn() {
    echo -e "${YELLOW}[WARN] $1${NC}"
}

ensure_debian_template() {
    if [[ -n "$DEBIAN_TEMPLATE" ]]; then
        return
    fi

    local candidate=""
    if command -v pveam >/dev/null 2>&1; then
        local available_output=""
        available_output=$(pveam available --section system 2>/dev/null || true)
        candidate=$(echo "$available_output" | awk '$2 ~ /^debian-12-standard_[0-9]+\.[0-9]+-[0-9]+_amd64\.tar\.zst$/ {print $2}' | sort -V | tail -1)

        if [[ -z "$candidate" ]]; then
            available_output=$(pveam available 2>/dev/null || true)
            candidate=$(echo "$available_output" | awk '$2 ~ /^debian-12-standard_[0-9]+\.[0-9]+-[0-9]+_amd64\.tar\.zst$/ {print $2}' | sort -V | tail -1)
        fi
    fi

    if [[ -z "$candidate" ]]; then
        candidate="$DEBIAN_TEMPLATE_FALLBACK"
        print_info "Using fallback Debian template version: $candidate"
    else
        print_info "Detected latest Debian template version: $candidate"
    fi

    DEBIAN_TEMPLATE="$candidate"
}

check_root() {
    if [[ $EUID -ne 0 ]]; then
        print_error "This script must be run as root"
        exit 1
    fi
}

# V3 is deprecated - no longer checking for it

detect_os() {
    if [[ -f /etc/os-release ]]; then
        . /etc/os-release
        OS=$ID
        VER=$VERSION_ID
    else
        print_error "Cannot detect OS"
        exit 1
    fi
}

# Restore SELinux contexts for installed binaries
# On SELinux-enforcing systems (Fedora, RHEL, CentOS), binaries in non-standard
# locations need proper security contexts for systemd to execute them.
restore_selinux_contexts() {
    # Check if SELinux is available and enforcing
    if ! command -v getenforce >/dev/null 2>&1; then
        return 0  # SELinux not installed
    fi

    if [[ "$(getenforce 2>/dev/null)" != "Enforcing" ]]; then
        return 0  # SELinux not enforcing
    fi

    # restorecon is the proper way to fix SELinux contexts
    if command -v restorecon >/dev/null 2>&1; then
        print_info "Restoring SELinux contexts for installed binaries..."
        restorecon -Rv "$INSTALL_DIR/bin/" >/dev/null 2>&1 || true
        restorecon -v "$BINARY_LINK_PATH" >/dev/null 2>&1 || true
        print_success "SELinux contexts restored"
    else
        # Fallback to chcon if restorecon isn't available
        if command -v chcon >/dev/null 2>&1; then
            print_info "Setting SELinux contexts for installed binaries..."
            find "$INSTALL_DIR/bin/" -type f -executable -exec chcon -t bin_t {} \; 2>/dev/null || true
            chcon -h -t bin_t "$BINARY_LINK_PATH" 2>/dev/null || true
        fi
    fi
}

check_proxmox_host() {
    # Check if this is a Proxmox VE host
    if command -v pvesh &> /dev/null && [[ -d /etc/pve ]]; then
        return 0
    fi
    return 1
}

check_docker_environment() {
    # Detect if we're running inside Docker (multiple detection methods)
    if [[ -f /.dockerenv ]] || \
       grep -q docker /proc/1/cgroup 2>/dev/null || \
       grep -q docker /proc/self/cgroup 2>/dev/null || \
       [[ -f /run/.containerenv ]] || \
       [[ "${container:-}" == "docker" ]]; then
        # The install.sh smoke gate exercises the documented systemd install
        # path inside a privileged systemd-in-Docker test container. That is
        # the one legitimate case for bypassing this guard.
        if [[ "${PULSE_INSTALL_ALLOW_DOCKER:-}" == "1" ]]; then
            print_warn "Docker environment detected but PULSE_INSTALL_ALLOW_DOCKER=1 set; continuing."
            return 0
        fi
        print_error "Docker environment detected"
        echo "Please use the Docker image directly: docker run -d -p 7655:7655 $(repo_docker_image_ref latest)"
        echo "See: $(repo_docker_docs_url)"
        exit 1
    fi
}

repo_web_url() {
    printf 'https://github.com/%s\n' "$GITHUB_REPO"
}

resolve_latest_release_tag_for_channel() {
    local channel="${1:-stable}"
    local releases_json=""
    local release=""

    [[ "$channel" == stable || "$channel" == rc ]] || return 1
    if command -v timeout >/dev/null 2>&1; then
        if ! releases_json=$(timeout 15 curl -q -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases" 2>/dev/null); then
            releases_json=""
        fi
    else
        if ! releases_json=$(curl -q -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases" 2>/dev/null); then
            releases_json=""
        fi
    fi

    release=$(latest_pulse_release_tag_from_json "$releases_json" "$channel" 2>/dev/null || true)
    if [[ -n "$release" ]]; then
        printf '%s\n' "$release"
        return 0
    fi

    release=$(get_latest_release_from_redirect 2>/dev/null || true)
    if [[ "$channel" == rc ]] && is_pulse_release_tag "$release"; then
        printf '%s\n' "$release"
        return 0
    elif [[ "$channel" == stable ]] && is_stable_release_tag "$release"; then
        printf '%s\n' "$release"
        return 0
    fi

    return 1
}

repo_release_docs_ref() {
    if [[ -n "${FORCE_VERSION:-}" ]]; then
        printf '%s\n' "$FORCE_VERSION"
        return 0
    fi

    if [[ -n "${LATEST_RELEASE:-}" ]]; then
        printf '%s\n' "$LATEST_RELEASE"
        return 0
    fi

    local channel="${FORCE_CHANNEL:-${UPDATE_CHANNEL:-stable}}"
    resolve_latest_release_tag_for_channel "$channel"
}

repo_docs_url_for_path() {
    local docs_path="$1"
    local ref=""
    ref=$(repo_release_docs_ref 2>/dev/null || true)
    if [[ -n "$ref" ]]; then
        printf '%s/blob/%s/%s\n' "$(repo_web_url)" "$ref" "$docs_path"
        return 0
    fi

    printf '%s/releases/latest\n' "$(repo_web_url)"
}

repo_docker_docs_url() {
    repo_docs_url_for_path "docs/DOCKER.md"
}

repo_docker_image_ref() {
    local tag="${1:-latest}"
    printf '%s:%s\n' "$DOCKER_IMAGE_REPO" "$tag"
}

# Discover host bridge interfaces, including Open vSwitch bridges.
detect_network_bridges() {
    local preferred=()
    local fallback=()
    local ip_output=""
    ip_output=$(ip -o link show type bridge 2>/dev/null || true)
    if [[ -n "$ip_output" ]]; then
        while IFS= read -r line; do
            [[ -z "$line" ]] && continue
            local iface=${line#*: }
            iface=${iface%%:*}
            iface=${iface%%@*}
            case "$iface" in
                docker*|br-*|virbr*|cni*|cilium*|flannel*|kube-*|veth*|tap*|ovs-system)
                    continue
                    ;;
                vmbr*|vnet*|ovs*)
                    preferred+=("$iface")
                    ;;
                *)
                    fallback+=("$iface")
                    ;;
            esac
        done <<< "$ip_output"
    fi
    
    if command -v ovs-vsctl >/dev/null 2>&1; then
        local ovs_output=""
        ovs_output=$(ovs-vsctl list-br 2>/dev/null || true)
        if [[ -n "$ovs_output" ]]; then
            while IFS= read -r iface; do
                [[ -z "$iface" ]] && continue
                case "$iface" in
                    docker*|br-*|virbr*|cni*|cilium*|flannel*|kube-*|veth*|tap*|ovs-system)
                        continue
                        ;;
                    vmbr*|vnet*|ovs*)
                        preferred+=("$iface")
                        ;;
                    *)
                        fallback+=("$iface")
                        ;;
                esac
            done <<< "$ovs_output"
        fi
    fi
    
    local combined=()
    if [[ ${#preferred[@]} -gt 0 ]]; then
        combined=("${preferred[@]}")
    fi
    if [[ ${#fallback[@]} -gt 0 ]]; then
        combined+=("${fallback[@]}")
    fi
    
    if [[ ${#combined[@]} -gt 0 ]]; then
        printf '%s\n' "${combined[@]}" | awk '!seen[$0]++' | paste -sd' ' -
    fi
}

is_bridge_interface() {
    local iface="$1"
    [[ -z "$iface" ]] && return 1
    
    local bridge_info=""
    bridge_info=$(ip -o link show "$iface" type bridge 2>/dev/null || true)
    if [[ -n "$bridge_info" ]]; then
        return 0
    fi
    
    if command -v ovs-vsctl >/dev/null 2>&1 && ovs-vsctl br-exists "$iface" &>/dev/null; then
        return 0
    fi
    
    return 1
}

create_lxc_container() {
    # Only files created by this invocation belong to its EXIT cleanup. Local
    # --archive inputs and a caller-supplied installer must remain untouched.
    local container_script_temp_dir=""
    local container_archive_source=""
    local container_archive_dest=""
    local container_archive_temp=false
    local archive_requested=false
    trap 'handle_container_install_exit "$?"' EXIT
    CURRENT_INSTALL_CTID=""
    INSTALL_CONTAINER_CREATED=false
    trap 'handle_install_interrupt 130' INT
    trap 'handle_install_interrupt 143' TERM

    print_header
    echo "Proxmox VE detected. Installing Pulse in a container."
    echo

    # Creating Pulse must not stop or rewrite any existing guest. Legacy v4
    # sensor-proxy cleanup is a separate, explicitly invoked maintenance job.
    # Check if we can interact with the user
    # Try to read from /dev/tty to test if we have terminal access
    if test -e /dev/tty && (echo -n "" > /dev/tty) 2>/dev/null; then
        # We have terminal access, show menu
        echo "Installation mode:"
        echo "  1) Quick (recommended)"
        echo "  2) Advanced"  
        echo "  3) Cancel"
        safe_read_with_default "Select [1-3]: " mode "1"
    else
        # No terminal access - truly non-interactive
        echo "Non-interactive mode detected. Using Quick installation."
        mode="1"
    fi
    
    case $mode in
        3)
            print_info "Installation cancelled"
            exit 0
            ;;
        2)
            ADVANCED_MODE=true
            ;;
        *)
            ADVANCED_MODE=false
            ;;
    esac

    # A Proxmox-host LXC install creates a brand-new Pulse instance inside the
    # container. Host-local Pulse config must not silently switch that fresh
    # install onto the prerelease channel unless the operator explicitly asked
    # for it via --rc or --version.
    IGNORE_CONFIGURED_UPDATE_CHANNEL=true
    
    # Get next available container ID from Proxmox
    local CTID=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
    
    # If pvesh failed, fallback to manual search
    if [[ "$CTID" == "100" ]]; then
        while pct status $CTID &>/dev/null 2>&1 || qm status $CTID &>/dev/null 2>&1; do
            ((CTID++))
        done
    fi
    
    if [[ "$ADVANCED_MODE" == "true" ]]; then
        echo
        # Ask for port configuration
        safe_read_with_default "Frontend port [7655]: " frontend_port "7655"
        if [[ ! "$frontend_port" =~ ^[0-9]+$ ]] || [[ "$frontend_port" -lt 1 ]] || [[ "$frontend_port" -gt 65535 ]]; then
            print_error "Invalid port number. Using default port 7655."
            frontend_port=7655
        fi
        
        auto_updates_flag=""
        if [[ "$AUTO_UPDATE_CHOICE_EXPLICIT" == "true" ]]; then
            if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then
                auto_updates_flag="--enable-auto-updates"
            else
                auto_updates_flag="--disable-auto-updates"
            fi
        fi
        if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
            echo
            print_info "Skipping auto-update configuration: source builds don't support automatic release updates."
        elif [[ "$AUTO_UPDATE_CHOICE_EXPLICIT" != "true" ]]; then
            echo
            # Ask about auto-updates
            echo "Enable automatic updates?"
            echo "Pulse can automatically install stable updates daily (between 2-6 AM)"
            safe_read_with_default "Enable auto-updates? [y/N]: " enable_updates "n"
            if [[ "$enable_updates" =~ ^[Yy]$ ]]; then
                auto_updates_flag="--enable-auto-updates"
                ENABLE_AUTO_UPDATES=true  # Set the global variable for host installations
            fi
        fi

        
        echo
        # Try to get cluster-wide IDs, fall back to local
        # Disable error exit for this section as commands may fail on fresh PVE installs without VMs
        set +e
        local USED_IDS=""
        if command -v pvesh &>/dev/null; then
            # Parse JSON output using grep and sed (works without jq)
            USED_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | \
                      grep -o '"vmid":[0-9]*' | \
                      sed 's/"vmid"://' | \
                      sort -n | \
                      paste -sd',' -)
        fi
        
        if [[ -z "$USED_IDS" ]]; then
            # Fallback: get local containers and VMs
            local LOCAL_CTS=$(pct list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n)
            local LOCAL_VMS=$(qm list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n)
            USED_IDS=$(echo -e "$LOCAL_CTS\n$LOCAL_VMS" | grep -v '^$' | sort -n | paste -sd',' -)
        fi
        # Re-enable error exit
        set -e
        
        echo "Container/VM IDs in use: ${USED_IDS:-none}"
        safe_read_with_default "Container ID [$CTID]: " custom_ctid "$CTID"
        if [[ "$custom_ctid" != "$CTID" ]] && [[ "$custom_ctid" =~ ^[0-9]+$ ]]; then
            # Check if ID is in use
            if pct status $custom_ctid &>/dev/null 2>&1 || qm status $custom_ctid &>/dev/null 2>&1; then
                print_error "Container/VM ID $custom_ctid is already in use"
                exit 1
            fi
            # Also check cluster if possible
            if command -v pvesh &>/dev/null; then
                if pvesh get /cluster/resources --type vm 2>/dev/null | jq -e ".[] | select(.vmid == $custom_ctid)" &>/dev/null; then
                    print_error "Container/VM ID $custom_ctid is already in use in the cluster"
                    exit 1
                fi
            fi
            CTID=$custom_ctid
        fi
    fi

    print_info "Using container ID: $CTID"
    CURRENT_INSTALL_CTID="$CTID"
    
    if [[ "$ADVANCED_MODE" == "true" ]]; then
        echo
        echo -e "${BLUE}Advanced Mode - Customize all settings${NC}"
        echo -e "${YELLOW}Defaults shown are suitable for monitoring 10-20 nodes${NC}"
        echo
        
        # Container settings
        safe_read_with_default "Container hostname [pulse]: " hostname "pulse"
        
        safe_read_with_default "Memory (MB) [1024]: " memory "1024"
        
        safe_read_with_default "Disk size (GB) [4]: " disk "4"
        
        safe_read_with_default "CPU cores [2]: " cores "2"
        
        safe_read_with_default "CPU limit (0=unlimited) [2]: " cpulimit "2"
        
        safe_read_with_default "Swap (MB) [256]: " swap "256"
        
        safe_read_with_default "Start on boot? [Y/n]: " onboot "Y" -n 1 -r
        echo
        if [[ "$onboot" =~ ^[Nn]$ ]]; then
            onboot=0
        else
            onboot=1
        fi
        
        safe_read_with_default "Enable firewall? [Y/n]: " firewall "Y" -n 1 -r
        echo
        if [[ "$firewall" =~ ^[Nn]$ ]]; then
            firewall=0
        else
            firewall=1
        fi
        
        safe_read_with_default "Unprivileged container? [Y/n]: " unprivileged "Y" -n 1 -r
        echo
        if [[ "$unprivileged" =~ ^[Nn]$ ]]; then
            unprivileged=0
        else
            unprivileged=1
        fi
    else
        # Quick mode - just use defaults silently
        
        # Use optimized defaults
        hostname="pulse"
        memory=1024
        disk=4
        cores=2
        cpulimit=2
        swap=256
        onboot=1
        firewall=1
        unprivileged=1
        # Ask for port even in quick mode
        echo
        safe_read_with_default "Port [7655]: " frontend_port "7655"
        if [[ ! "$frontend_port" =~ ^[0-9]+$ ]] || [[ "$frontend_port" -lt 1 ]] || [[ "$frontend_port" -gt 65535 ]]; then
            print_info "Using default: 7655"
            frontend_port=7655
        fi
        auto_updates_flag=""
        if [[ "$AUTO_UPDATE_CHOICE_EXPLICIT" == "true" ]]; then
            if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then
                auto_updates_flag="--enable-auto-updates"
            else
                auto_updates_flag="--disable-auto-updates"
            fi
        fi
        if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
            echo
            print_info "Skipping auto-update configuration: source builds don't support automatic release updates."
        elif [[ "$AUTO_UPDATE_CHOICE_EXPLICIT" != "true" ]]; then
            # Quick mode should ask about auto-updates too
            echo
            echo "Enable automatic updates?"
            echo "Pulse can automatically install stable updates daily (between 2-6 AM)"
            safe_read_with_default "Enable auto-updates? [y/N]: " enable_updates "n"
            if [[ "$enable_updates" =~ ^[Yy]$ ]]; then
                auto_updates_flag="--enable-auto-updates"
                ENABLE_AUTO_UPDATES=true  # Set the global variable for host installations
            fi
        fi


        # Optional VLAN configuration - defaults to empty (no VLAN) for regular users
        echo
        safe_read_with_default "VLAN ID (press Enter for no VLAN): " vlan_id ""
        if [[ -n "$vlan_id" ]]; then
            # Validate VLAN ID (1-4094)
            if [[ ! "$vlan_id" =~ ^[0-9]+$ ]] || [[ "$vlan_id" -lt 1 ]] || [[ "$vlan_id" -gt 4094 ]]; then
                print_error "Invalid VLAN ID. Must be between 1 and 4094"
                print_info "Proceeding without VLAN"
                vlan_id=""
            fi
        fi
    fi
    
    # Get available network bridges
    echo
    print_info "Detecting available resources..."
    
    # Get available bridges
    local BRIDGES=$(detect_network_bridges)
    
    # First try to find the default network interface (could be bridge or regular interface)
    local DEFAULT_INTERFACE=$(ip route | grep default | head -1 | grep -oP 'dev \K\S+')
    
    # Check if the default interface is a bridge
    local DEFAULT_BRIDGE=""
    if [[ -n "$DEFAULT_INTERFACE" ]]; then
        if is_bridge_interface "$DEFAULT_INTERFACE"; then
            # Default interface is a bridge, use it
            DEFAULT_BRIDGE="$DEFAULT_INTERFACE"
        fi
    fi
    
    # If no default bridge found, try to use the first available bridge
    if [[ -z "$DEFAULT_BRIDGE" && -n "$BRIDGES" ]]; then
        DEFAULT_BRIDGE=$(echo "$BRIDGES" | cut -d' ' -f1)
    fi
    
    # If still no bridge found, we'll need to ask the user
    if [[ -z "$DEFAULT_BRIDGE" ]]; then
        if [[ -n "$DEFAULT_INTERFACE" ]]; then
            # We have a default interface but it's not a bridge
            print_info "Default network interface is $DEFAULT_INTERFACE (not a bridge)"
        fi
        DEFAULT_BRIDGE="vmbr0"  # Fallback suggestion only
    fi
    
    # Get available storage with usage info
    local STORAGE_INFO=$(pvesm status -content rootdir 2>/dev/null | tail -n +2)
    local DEFAULT_STORAGE=$(echo "$STORAGE_INFO" | awk '{print $1}' | head -1)
    DEFAULT_STORAGE=${DEFAULT_STORAGE:-local-lvm}
    
    if [[ "$ADVANCED_MODE" == "true" ]]; then
        # Show available bridges
        echo
        if [[ -n "$BRIDGES" ]]; then
            echo "Available network bridges:"
            local bridge_array=($BRIDGES)
            local default_idx=0
            for i in "${!bridge_array[@]}"; do
                local idx=$((i+1))
                echo "  $idx) ${bridge_array[$i]}"
                if [[ "${bridge_array[$i]}" == "$DEFAULT_BRIDGE" ]]; then
                    default_idx=$idx
                fi
            done
            
            set +e
            safe_read "Select network bridge [${default_idx}]: " bridge_choice
            local read_result=$?
            set -e
            
            if [[ $read_result -eq 0 ]]; then
                bridge_choice=${bridge_choice:-$default_idx}
                if [[ "$bridge_choice" =~ ^[0-9]+$ ]] && [[ "$bridge_choice" -ge 1 ]] && [[ "$bridge_choice" -le ${#bridge_array[@]} ]]; then
                    bridge="${bridge_array[$((bridge_choice-1))]}"
                elif [[ -n "$bridge_choice" ]]; then
                    # User typed a bridge name directly
                    bridge="$bridge_choice"
                else
                    bridge="$DEFAULT_BRIDGE"
                fi
            else
                # Non-interactive, use default
                bridge="$DEFAULT_BRIDGE"
            fi
        else
            echo "No network bridges detected"
            echo "You may need to create a Linux bridge or Open vSwitch bridge first (e.g., vmbr0)"
            set +e
            safe_read "Enter network bridge name: " bridge
            set -e
        fi
        
        # Show available storage with usage details
        echo
        if [[ -n "$STORAGE_INFO" ]]; then
            echo "Available storage pools:"
            local storage_names=()
            local default_idx=0
            local idx=1
            
            while IFS= read -r line; do
                local storage_name storage_type avail_gb total_gb used_pct parsed_line
                parsed_line=$(LC_NUMERIC=C awk '{printf "%s,%s,%.1f,%.1f,%s", $1, $2, $6/1048576, $4/1048576, $7}' <<< "$line")
                [[ -z "$parsed_line" ]] && continue
                IFS=',' read -r storage_name storage_type avail_gb total_gb used_pct <<< "$parsed_line" || continue

                storage_names+=("$storage_name")
                LC_ALL=C printf "  %d) %-15s %-8s %6.1f GB free of %6.1f GB (%s used)\n" \
                    "$idx" "$storage_name" "$storage_type" "$avail_gb" "$total_gb" "$used_pct"
                
                if [[ "$storage_name" == "$DEFAULT_STORAGE" ]]; then
                    default_idx=$idx
                fi
                ((idx++))
            done <<< "$STORAGE_INFO"
            
            set +e
            safe_read "Select storage pool [${default_idx}]: " storage_choice
            local read_result=$?
            set -e
            
            if [[ $read_result -eq 0 ]]; then
                storage_choice=${storage_choice:-$default_idx}
                if [[ "$storage_choice" =~ ^[0-9]+$ ]] && [[ "$storage_choice" -ge 1 ]] && [[ "$storage_choice" -le ${#storage_names[@]} ]]; then
                    storage="${storage_names[$((storage_choice-1))]}"
                elif [[ -n "$storage_choice" ]]; then
                    # User typed a storage name directly
                    storage="$storage_choice"
                else
                    storage="$DEFAULT_STORAGE"
                fi
            else
                # Non-interactive, use default
                storage="$DEFAULT_STORAGE"
            fi
        else
            echo "  No storage pools found"
            set +e
            safe_read "Enter storage pool name [$DEFAULT_STORAGE]: " storage
            set -e
            storage=${storage:-$DEFAULT_STORAGE}
        fi
        
        safe_read_with_default "Static IP with CIDR (e.g. 198.51.100.100/24, leave empty for DHCP): " static_ip ""
        
        # If static IP is provided, we need gateway
        if [[ -n "$static_ip" ]]; then
            # Validate IP format
            if [[ ! "$static_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
                print_error "Invalid IP format. Please use CIDR notation (e.g., 198.51.100.100/24)"
                print_info "Using DHCP instead"
                static_ip=""
            else
                safe_read_with_default "Gateway IP address: " gateway_ip ""
                if [[ -z "$gateway_ip" ]]; then
                    # Try to guess gateway from the IP (use .1 of the subnet)
                    local ip_base="${static_ip%.*}"
                    gateway_ip="${ip_base}.1"
                    print_info "No gateway specified, using $gateway_ip"
                elif [[ ! "$gateway_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
                    print_error "Invalid gateway format. Using DHCP instead"
                    static_ip=""
                    gateway_ip=""
                fi
            fi
        fi
        
        safe_read_with_default "DNS servers (space-separated, empty for host settings): " nameserver ""
        
        # VLAN configuration
        safe_read_with_default "VLAN ID (leave empty for no VLAN): " vlan_id ""
        if [[ -n "$vlan_id" ]]; then
            # Validate VLAN ID (1-4094)
            if [[ ! "$vlan_id" =~ ^[0-9]+$ ]] || [[ "$vlan_id" -lt 1 ]] || [[ "$vlan_id" -gt 4094 ]]; then
                print_error "Invalid VLAN ID. Must be between 1 and 4094"
                print_info "Proceeding without VLAN"
                vlan_id=""
            fi
        fi
        
        safe_read_with_default "Startup order [99]: " startup "99"
    else
        # Quick mode - but still need to verify critical settings
        
        # Network bridge selection
        echo
        if [[ -n "$BRIDGES" ]]; then
            echo "Available network bridges:"
            local bridge_array=($BRIDGES)
            local default_idx=0
            for i in "${!bridge_array[@]}"; do
                local idx=$((i+1))
                echo "  $idx) ${bridge_array[$i]}"
                if [[ "${bridge_array[$i]}" == "$DEFAULT_BRIDGE" ]]; then
                    default_idx=$idx
                fi
            done
            
            if [[ ${#bridge_array[@]} -eq 1 ]]; then
                # Only one bridge available, use it
                bridge="${bridge_array[0]}"
                print_info "Using network bridge: $bridge"
            else
                set +e
                safe_read "Select network bridge [${default_idx}]: " bridge_choice
                local read_result=$?
                set -e
                
                if [[ $read_result -eq 0 ]]; then
                    bridge_choice=${bridge_choice:-$default_idx}
                    if [[ "$bridge_choice" =~ ^[0-9]+$ ]] && [[ "$bridge_choice" -ge 1 ]] && [[ "$bridge_choice" -le ${#bridge_array[@]} ]]; then
                        bridge="${bridge_array[$((bridge_choice-1))]}"
                    else
                        bridge="$DEFAULT_BRIDGE"
                        print_info "Invalid selection, using default: $bridge"
                    fi
                else
                    # Non-interactive, use default
                    bridge="$DEFAULT_BRIDGE"
                fi
            fi
        else
            print_error "No network bridges detected on this system"
            print_info "You may need to create a Linux bridge or Open vSwitch bridge first (e.g., vmbr0)"
            set +e
            safe_read "Enter network bridge name to use: " bridge
            set -e
        fi
        
        # Storage selection
        echo
        if [[ -n "$STORAGE_INFO" ]]; then
            echo "Available storage pools:"
            # Create arrays for storage info
            local storage_names=()
            local storage_info_lines=()
            local default_idx=0
            local idx=1
            
            while IFS= read -r line; do
                local storage_name storage_type avail_gb total_gb used_pct parsed_line
                parsed_line=$(LC_NUMERIC=C awk '{printf "%s,%s,%.1f,%.1f,%s", $1, $2, $6/1048576, $4/1048576, $7}' <<< "$line")
                [[ -z "$parsed_line" ]] && continue
                IFS=',' read -r storage_name storage_type avail_gb total_gb used_pct <<< "$parsed_line" || continue

                storage_names+=("$storage_name")
                LC_ALL=C printf "  %d) %-15s %-8s %6.1f GB free of %6.1f GB (%s used)\n" \
                    "$idx" "$storage_name" "$storage_type" "$avail_gb" "$total_gb" "$used_pct"
                
                if [[ "$storage_name" == "$DEFAULT_STORAGE" ]]; then
                    default_idx=$idx
                fi
                ((idx++))
            done <<< "$STORAGE_INFO"
            
            if [[ ${#storage_names[@]} -eq 1 ]]; then
                # Only one storage available, use it
                storage="${storage_names[0]}"
                print_info "Using storage pool: $storage"
            else
                set +e
                safe_read "Select storage pool [${default_idx}]: " storage_choice
                local read_result=$?
                set -e
                
                if [[ $read_result -eq 0 ]]; then
                    storage_choice=${storage_choice:-$default_idx}
                    if [[ "$storage_choice" =~ ^[0-9]+$ ]] && [[ "$storage_choice" -ge 1 ]] && [[ "$storage_choice" -le ${#storage_names[@]} ]]; then
                        storage="${storage_names[$((storage_choice-1))]}"
                    else
                        storage="$DEFAULT_STORAGE"
                        print_info "Invalid selection, using default: $storage"
                    fi
                else
                    # Non-interactive, use default
                    storage="$DEFAULT_STORAGE"
                fi
            fi
        else
            print_error "No storage pools detected"
            set +e
            safe_read "Enter storage pool name to use: " storage
            set -e
        fi
        
        static_ip=""
        gateway_ip=""
        nameserver=""
        startup=99
    fi
    
    # Handle OS template selection
    echo
    if [[ "$ADVANCED_MODE" == "true" ]]; then
        # Get ALL storages that can contain templates
        local TEMPLATE_STORAGES=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | awk '{print $1}' | paste -sd' ' -)
        
        # Collect templates from ALL template-capable storages
        local ALL_TEMPLATES=""
        for tmpl_storage in $TEMPLATE_STORAGES; do
            # pveam list output already includes the storage prefix in the path
            local STORAGE_TEMPLATES=$(pveam list "$tmpl_storage" 2>/dev/null | tail -n +2 | awk '{print $1}' || true)
            if [[ -n "$STORAGE_TEMPLATES" ]]; then
                if [[ -n "$ALL_TEMPLATES" ]]; then
                    ALL_TEMPLATES="${ALL_TEMPLATES}\n${STORAGE_TEMPLATES}"
                else
                    ALL_TEMPLATES="$STORAGE_TEMPLATES"
                fi
            fi
        done
        
        echo "Available OS templates across all storages:"
        # Format templates with numbers
        local TEMPLATES=$(echo -e "$ALL_TEMPLATES" | nl -w2 -s') ')
        if [[ -n "$TEMPLATES" ]]; then
            echo "$TEMPLATES"
            echo
            echo "Or download a new template:"
            echo "  d) Download Debian 12 (recommended)"
            echo "  u) Download Ubuntu 22.04 LTS"
            echo
            echo "Note: Pulse requires a Debian-based template (apt/systemd)."
            echo
            safe_read_with_default "Select template number or option [Enter for Debian 12]: " template_choice ""
            if [[ -n "$template_choice" ]]; then
                case "$template_choice" in
                    d|D)
                        # Find best storage for templates (prefer one with most free space)
                        local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
                        BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
                        ensure_debian_template
                        print_info "Downloading Debian 12 to storage '$BEST_TEMPLATE_STORAGE'..."
                        pveam download "$BEST_TEMPLATE_STORAGE" "$DEBIAN_TEMPLATE"
                        TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/${DEBIAN_TEMPLATE}"
                        ;;
                    u|U)
                        # Find best storage for templates (prefer one with most free space)
                        local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
                        BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
                        print_info "Downloading Ubuntu 22.04 to storage '$BEST_TEMPLATE_STORAGE'..."
                        pveam download "$BEST_TEMPLATE_STORAGE" ubuntu-22.04-standard_22.04-1_amd64.tar.zst
                        TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
                        ;;
                    [0-9]*)
                        # Extract the full template path from numbered list
                        TEMPLATE=$(echo -e "$ALL_TEMPLATES" | sed -n "${template_choice}p")
                        if [[ -n "$TEMPLATE" ]]; then
                            # Check if selected template is Alpine or other non-Debian based
                            if [[ "$TEMPLATE" =~ alpine|gentoo|arch|void ]]; then
                                print_warn "Selected template ($TEMPLATE) is not Debian-based."
                                print_warn "Pulse LXC installation requires apt and systemd."
                                print_info "Falling back to Debian 12..."
                                local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
                                BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
                                ensure_debian_template
                                TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/${DEBIAN_TEMPLATE}"
                            else
                                print_info "Using template: $TEMPLATE"
                            fi
                        else
                            # Find best storage for templates (prefer one with most free space)
                            local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
                            BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
                            ensure_debian_template
                            TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/${DEBIAN_TEMPLATE}"
                            print_info "Invalid selection, using Debian 12"
                        fi
                        ;;
                    *)
                        # Find best storage for templates (prefer one with most free space)
                        local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
                        BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
                        ensure_debian_template
                        TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/${DEBIAN_TEMPLATE}"
                        ;;
                esac
            else
                # Find best storage for templates (prefer one with most free space)
                local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
                BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
                ensure_debian_template
                TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/${DEBIAN_TEMPLATE}"
            fi
        else
            # Find best storage for templates (prefer one with most free space)
            local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
            BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
            ensure_debian_template
            TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/${DEBIAN_TEMPLATE}"
        fi
    else
        # Quick mode - find best storage for templates
        local BEST_TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | tail -n +2 | sort -k6 -rn | head -1 | awk '{print $1}')
        BEST_TEMPLATE_STORAGE=${BEST_TEMPLATE_STORAGE:-$storage}
        ensure_debian_template
        TEMPLATE="${BEST_TEMPLATE_STORAGE}:vztmpl/${DEBIAN_TEMPLATE}"
    fi
    
    # Download template if it doesn't exist
    # Check if template exists - pveam list shows full paths like storage:vztmpl/file.tar.zst
    local TEMPLATE_EXISTS=false
    if [[ "$TEMPLATE" =~ ^([^:]+):vztmpl/(.+)$ ]]; then
        local STORAGE_NAME="${BASH_REMATCH[1]}"
        local TEMPLATE_FILE="${BASH_REMATCH[2]}"
        # Check if this exact template exists in pveam list
        if pveam list "$STORAGE_NAME" 2>/dev/null | grep -q "$TEMPLATE_FILE"; then
            TEMPLATE_EXISTS=true
        fi
    fi
    
    if [[ "$TEMPLATE_EXISTS" == "false" ]]; then
        # Extract storage name from template path
        local TEMPLATE_STORAGE="${TEMPLATE%%:*}"
        print_info "Template not found, downloading Debian 12 to storage '$TEMPLATE_STORAGE'..."
        ensure_debian_template
        if ! pveam download "$TEMPLATE_STORAGE" "$DEBIAN_TEMPLATE"; then
            print_error "Failed to download template. Please check your internet connection and try again."
            print_info "You can manually download with: pveam download $TEMPLATE_STORAGE $DEBIAN_TEMPLATE"
            exit 1
        fi
        # Verify it was downloaded
        if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$DEBIAN_TEMPLATE"; then
            print_error "Template download succeeded but file not found in storage"
            exit 1
        fi
    fi

    if [[ -n "$ARCHIVE_OVERRIDE" ]]; then
        ARCHIVE_OVERRIDE=$(resolve_archive_override "$ARCHIVE_OVERRIDE") || exit 1
    fi
    
    print_info "Creating container..."
    
    # Build network configuration
    if [[ -n "$static_ip" ]]; then
        # Include gateway in network config for static IP
        if [[ -n "$gateway_ip" ]]; then
            NET_CONFIG="name=eth0,bridge=${bridge},ip=${static_ip},gw=${gateway_ip},firewall=${firewall}"
        else
            # This shouldn't happen but handle it gracefully
            NET_CONFIG="name=eth0,bridge=${bridge},ip=${static_ip},firewall=${firewall}"
        fi
    else
        NET_CONFIG="name=eth0,bridge=${bridge},ip=dhcp,firewall=${firewall}"
    fi
    
    # Add VLAN tag if specified
    if [[ -n "$vlan_id" ]]; then
        NET_CONFIG="${NET_CONFIG},tag=${vlan_id}"
    fi
    
    # Build container create command using array to avoid eval issues
    local CREATE_ARGS=(pct create "$CTID" "$TEMPLATE")
    CREATE_ARGS+=("--hostname" "$hostname")
    CREATE_ARGS+=("--memory" "$memory")
    CREATE_ARGS+=("--cores" "$cores")

    if [[ "$cpulimit" != "0" ]]; then
        CREATE_ARGS+=("--cpulimit" "$cpulimit")
    fi

    CREATE_ARGS+=("--rootfs" "${storage}:${disk}")
    CREATE_ARGS+=("--net0" "$NET_CONFIG")
    CREATE_ARGS+=("--unprivileged" "$unprivileged")
    CREATE_ARGS+=("--features" "nesting=1")
    CREATE_ARGS+=("--onboot" "$onboot")
    CREATE_ARGS+=("--startup" "order=$startup")
    CREATE_ARGS+=("--protection" "0")
    CREATE_ARGS+=("--swap" "$swap")

    if [[ -n "$nameserver" ]]; then
        CREATE_ARGS+=("--nameserver" "$nameserver")
    fi


    # Keep command errors and signal-handler recovery guidance visible.
    if ! "${CREATE_ARGS[@]}"; then
        print_error "Failed to create container"
        exit 1
    fi
    INSTALL_CONTAINER_CREATED=true

    # Start container
    print_info "Starting container..."
    if ! pct start "$CTID"; then
        print_error "Failed to start container"
        exit 1
    fi
    sleep 3
    
    # Wait for network to provide an IP address
    print_info "Waiting for network..."
    local network_ready=false
    for i in {1..60}; do
        local container_ip=""
        container_ip=$(pct exec $CTID -- hostname -I 2>/dev/null | awk '{print $1}') || true
        if [[ -n "$container_ip" ]]; then
            network_ready=true
            break
        fi
        sleep 1
    done

    if [[ "$network_ready" != "true" ]]; then
        print_error "Container network failed to obtain an IP address after 60 seconds"
        exit 1
    fi
    
    # Install dependencies and optimize container
    print_info "Installing dependencies..."
    local dependency_status=0
    pct exec "$CTID" -- bash -e -c "
        apt-get update -qq
        apt-get install -y -qq curl wget ca-certificates
        # Set timezone to UTC for consistent logging
        ln -sf /usr/share/zoneinfo/UTC /etc/localtime 2>/dev/null || true
        # Optimize sysctl for monitoring workload
        echo 'net.core.somaxconn=1024' >> /etc/sysctl.conf
        echo 'net.ipv4.tcp_keepalive_time=60' >> /etc/sysctl.conf
        sysctl -p >/dev/null 2>&1 || true
        
        # Note: We don't create /usr/local/bin/update to avoid conflicts with Community Scripts
        # Native installations should update using: curl -fsSL ... | bash
        
        # Ensure /usr/local/bin is in PATH for all users
        if ! grep -q '/usr/local/bin' /etc/profile 2>/dev/null; then
            echo 'export PATH="/usr/local/bin:$PATH"' >> /etc/profile
        fi
        
        # Also add to bash profile if it exists
        if [[ -f /etc/bash.bashrc ]] && ! grep -q '/usr/local/bin' /etc/bash.bashrc 2>/dev/null; then
            echo 'export PATH="/usr/local/bin:$PATH"' >> /etc/bash.bashrc
        fi
    " || dependency_status=$?
    if [[ "$dependency_status" -ne 0 ]]; then
        print_error "Failed to install dependencies in container (exit $dependency_status)"
        exit "$dependency_status"
    fi

    if [[ -n "$ARCHIVE_OVERRIDE" ]]; then
        archive_requested=true
        container_archive_source="$ARCHIVE_OVERRIDE"
        container_archive_dest="/tmp/$(basename "$container_archive_source")"
    elif [[ "$BUILD_FROM_SOURCE" != "true" ]]; then
        if prefetch_pulse_archive_for_container container_archive_source; then
            container_archive_temp=true
            container_archive_dest="/tmp/$(basename "$container_archive_source")"
        else
            print_warn "Host-side Pulse archive prefetch failed; falling back to in-container download."
        fi
    fi
    
    # Install Pulse inside container
    print_info "Installing Pulse..."
    
    # When piped through curl, $0 is "bash" not the script. Download fresh copy.
    local script_source="$0"
    if [[ "$0" == "bash" ]] || [[ ! -f "$0" ]]; then
        if ! download_container_installer script_source container_script_temp_dir; then
            exit 1
        fi
    fi
    
    # Copy this script to container and run it
    if ! pct push "$CTID" "$script_source" /tmp/install.sh; then
        print_error "Failed to copy install script to container"
        exit 1
    fi
    
    cleanup_container_install_inputs "$container_script_temp_dir" "" false
    container_script_temp_dir=""

    if [[ -n "$container_archive_source" ]]; then
        print_info "Copying Pulse release archive to container..."
        if ! pct push "$CTID" "$container_archive_source" "$container_archive_dest"; then
            if [[ "$container_archive_temp" == "true" ]]; then
                cleanup_temp_archive_path "$container_archive_source"
                container_archive_temp=false
            fi
            if [[ "$archive_requested" == "true" ]]; then
                print_error "Failed to copy Pulse release archive to container"
                exit 1
            fi
            print_warn "Could not copy prefetched archive into container; falling back to in-container download."
            container_archive_source=""
            container_archive_dest=""
            container_archive_temp=false
        else
            # Signature sidecar must travel with the archive so that
            # install_pulse_archive inside the container can verify it.
            if [[ -f "${container_archive_source}.sshsig" ]]; then
                if ! pct push "$CTID" "${container_archive_source}.sshsig" "${container_archive_dest}.sshsig"; then
                    if [[ "$container_archive_temp" == "true" ]]; then
                        cleanup_temp_archive_path "$container_archive_source"
                        container_archive_temp=false
                    fi
                    print_error "Failed to copy Pulse release archive signature to container"
                    exit 1
                fi
            else
                if [[ "$container_archive_temp" == "true" ]]; then
                    cleanup_temp_archive_path "$container_archive_source"
                    container_archive_temp=false
                fi
                print_error "Pulse release archive signature missing alongside ${container_archive_source}"
                exit 1
            fi
        fi
    fi

    if [[ "$container_archive_temp" == "true" ]]; then
        cleanup_temp_archive_path "$container_archive_source"
        container_archive_temp=false
    fi
    
    # Run installation with visible progress
    local install_cmd=""
    install_cmd=$(build_container_install_command)
    
    # Run installation showing output in real-time so users can see progress/errors
    # Use timeout wrapper if available
    local install_status=0
    local timeout_duration=300

    if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
        timeout_duration=1200
    fi

    if [[ -n "${PULSE_CONTAINER_TIMEOUT:-}" ]]; then
        if [[ "${PULSE_CONTAINER_TIMEOUT}" =~ ^[0-9]+$ ]]; then
            timeout_duration=${PULSE_CONTAINER_TIMEOUT}
        else
            print_warn "Ignoring invalid PULSE_CONTAINER_TIMEOUT value '${PULSE_CONTAINER_TIMEOUT}'"
        fi
    fi

    if command -v timeout >/dev/null 2>&1 && [[ "$timeout_duration" -ne 0 ]]; then
        # Show output in real-time with timeout
        timeout "$timeout_duration" pct exec "$CTID" -- bash -c "$install_cmd" || install_status=$?
        if [[ $install_status -eq 124 ]]; then
            print_error "Installation timed out after ${timeout_duration}s"
            if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
                print_info "Building from source can take more than 15 minutes in containers, especially on first run."
            else
                print_info "The timeout does not establish whether installation completed. Check the container's state and logs."
            fi
            print_info "You can increase or disable the timeout by setting PULSE_CONTAINER_TIMEOUT (set to 0 to disable)."
            print_info "Only after confirming no installer is still running, recover inside the retained container:"
            print_info "  pct enter $CTID"
            print_container_recovery_command
            exit "$install_status"
        fi
    else
        # Show output in real-time without timeout
        pct exec "$CTID" -- bash -c "$install_cmd" || install_status=$?
    fi
    
    if [[ $install_status -ne 0 ]]; then
        print_error "Failed to install Pulse inside container"
        print_info "You can enter the container to investigate:"
        print_info "  pct enter $CTID"
        print_container_recovery_command
        exit "$install_status"
    fi
    
    # Get container IP
    local IP=$(pct exec $CTID -- hostname -I | awk '{print $1}')

    local PULSE_BASE_URL="http://${IP}:${frontend_port}"

    # Automatically register the Proxmox host with Pulse to speed setup
    auto_register_pve_node "$CTID" "$IP" "$frontend_port"

    wait_for_pulse_ready "$PULSE_BASE_URL" 120 1

    # Clean final output
    echo
    print_success "Pulse installation complete!"
    echo
    echo "  Web UI:     http://${IP}:${frontend_port}"
    echo "  Container:  $CTID"
    if [[ "$AUTO_NODE_REGISTERED" == true ]]; then
        echo "  Node:       Registered ${AUTO_NODE_REGISTERED_NAME} in Pulse"
    elif [[ -n "$AUTO_NODE_REGISTER_ERROR" ]]; then
        echo "  Node:       Registration pending (${AUTO_NODE_REGISTER_ERROR})"
    fi
    echo
    echo "  First-time setup:"
    local QUOTED_CONFIG_DIR
    printf -v QUOTED_CONFIG_DIR '%q' "$CONFIG_DIR"
    # pct exec runs with PATH=/sbin:/bin:/usr/sbin:/usr/bin, which excludes
    # /usr/local/bin where the binary is linked, so these must be absolute.
    echo "    pct exec $CTID -- env PULSE_DATA_DIR=$QUOTED_CONFIG_DIR $BINARY_LINK_PATH bootstrap-token"
    echo
    echo "  Common commands:"
    echo "    pct enter $CTID              # Enter container"
    echo "    pct exec $CTID -- $UPDATE_HELPER_PATH     # Update Pulse"
    echo
    
    INSTALL_CONTAINER_CREATED=false
    CURRENT_INSTALL_CTID=""
    trap - INT TERM
    
    exit 0
}

# Extract the PVE API token secret from `pveum user token add` output. Prefers
# the deterministic JSON form (`pveum ... --output-format json`) and falls back
# to the legacy box-drawing table layout, mirroring the hardened web-setup
# render path (internal/api/setup_script_render.go) so token capture does not
# silently fail or mis-parse when pveum's table formatting drifts across
# versions/locales (#1312).
extract_pve_token_value() {
    local token_output="$1"
    local token_value=""

    token_value=$(printf '%s\n' "$token_output" | sed -n 's/.*"value"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
    if [[ -n "$token_value" ]]; then
        printf '%s\n' "$token_value"
        return 0
    fi

    # Legacy table form: normalize the box-drawing column separator to a plain
    # pipe (byte-wise via sed, so it is locale-independent) before splitting, so
    # extraction works regardless of the host locale.
    printf '%s\n' "$token_output" | sed 's/│/|/g' | awk -F'|' '
        function trim(value) {
            gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
            return value
        }
        {
            key = trim($2)
            value = trim($3)
            if (key == "value" && value != "") {
                print value
                exit
            }
        }
    '
}

pve_token_already_exists_error() {
    local token_output="$1"
    printf '%s\n' "$token_output" | grep -Eqi 'already exists|already defined|duplicate|exists already'
}

create_pve_auto_register_token() {
    local token_name="$1"
    local token_output_var="$2"
    local token_status_var="$3"
    local token_output=""
    local token_status=0

    set +e
    token_output=$(pveum user token add pulse-monitor@pve "$token_name" --privsep 1 --output-format json 2>&1)
    token_status=$?
    set -e

    # Older pveum builds reject --output-format; fall back to the legacy table form.
    if [[ $token_status -ne 0 ]] && printf '%s\n' "$token_output" | grep -Eqi 'unknown option|unknown command|no such option|unable to parse option|output-format'; then
        set +e
        token_output=$(pveum user token add pulse-monitor@pve "$token_name" --privsep 1 2>&1)
        token_status=$?
        set -e
    fi

    printf -v "$token_output_var" '%s' "$token_output"
    printf -v "$token_status_var" '%s' "$token_status"
}

smoke_test_pve_auto_register_token() {
    local host_url="$1"
    local token_id="$2"
    local token_value="$3"
    local smoke_output=""
    local smoke_status=0

    set +e
    smoke_output=$(printf 'Authorization: PVEAPIToken=%s=%s\n' "$token_id" "$token_value" | curl -q --retry 2 --retry-delay 1 -kfsS -H @- "${host_url%/}/api2/json/nodes" 2>&1)
    smoke_status=$?
    set -e

    if [[ $smoke_status -ne 0 ]]; then
        AUTO_NODE_REGISTER_ERROR="token smoke check failed"
        print_warn "Created Proxmox monitoring token, but a local API smoke check failed; skipping automatic node registration"
        print_warn "Smoke check error: ${smoke_output}"
        return 1
    fi

    return 0
}

auto_register_pve_node() {
    local ctid="$1"
    local pulse_ip="$2"
    local pulse_port="${3:-7655}"

    if [[ "$IN_CONTAINER" == "true" ]]; then
        return
    fi

    if [[ -z "$ctid" || -z "$pulse_ip" ]]; then
        return
    fi

    local skip_auto="${PULSE_SKIP_AUTO_NODE:-}"
    if [[ "$skip_auto" =~ ^([Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|1|on|ON)$ ]]; then
        print_info "Skipping automatic node registration (PULSE_SKIP_AUTO_NODE set)"
        return
    fi

    if ! command -v pveum >/dev/null 2>&1; then
        AUTO_NODE_REGISTER_ERROR="pveum unavailable"
        print_warn "pveum command not available; skipping automatic node registration"
        return
    fi

    if ! command -v curl >/dev/null 2>&1; then
        AUTO_NODE_REGISTER_ERROR="curl unavailable"
        print_warn "curl command not available; skipping automatic node registration"
        return
    fi

    local default_port="${PULSE_PVE_API_PORT:-8006}"
    local host_input="${PULSE_PVE_HOST_URL:-}"
    if [[ -z "$host_input" ]]; then
        host_input="$(hostname -f 2>/dev/null || hostname)"
    fi

    local normalized_host_url
    normalized_host_url=$(python3 - <<'PY' "$host_input" "$default_port"
import sys, urllib.parse
raw = sys.argv[1].strip()
default_port = sys.argv[2]
if not raw:
    print("")
    sys.exit(0)
if "://" not in raw:
    raw = f"https://{raw}"
parsed = urllib.parse.urlparse(raw)
scheme = parsed.scheme or "https"
netloc = parsed.netloc or parsed.path
path = parsed.path if parsed.netloc else ""
if not netloc:
    print("")
    sys.exit(0)
host = netloc.split('@', 1)[-1]
if ':' not in host:
    netloc = f"{netloc}:{default_port}"
print(urllib.parse.urlunparse((scheme, netloc, path, "", "", "")))
PY
) || normalized_host_url=""

    if [[ -z "$normalized_host_url" ]]; then
        AUTO_NODE_REGISTER_ERROR="invalid host URL"
        print_warn "Unable to determine Proxmox API URL; skipping automatic node registration"
        return
    fi

    local server_name
    server_name="$(hostname -s 2>/dev/null || hostname)"
    if [[ -z "$server_name" ]]; then
        server_name="pulse-proxmox-host"
    fi

    local backup_flag="${PULSE_AUTO_BACKUP_PERMS:-true}"
    local backup_perms="false"
    if [[ "$backup_flag" =~ ^([Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|1|on|ON)$ ]]; then
        backup_perms="true"
    fi

    local setup_payload
    setup_payload=$(python3 - <<'PY' "$normalized_host_url" "$backup_perms"
import json, sys
host = sys.argv[1]
backup = sys.argv[2].lower() == "true"
print(json.dumps({"type": "pve", "host": host, "backupPerms": backup}))
PY
)

    # Bootstrap request/response are not retained in shared /tmp diagnostics.

    local pulse_url="http://${pulse_ip}:${pulse_port}"

    set +xv

    local setup_response
    if ! setup_response=$(curl -q --retry 3 --retry-delay 2 -fsS -X POST "$pulse_url/api/setup-script-url" -H "Content-Type: application/json" -d "$setup_payload"); then
        AUTO_NODE_REGISTER_ERROR="setup token request failed"
        print_warn "Unable to request setup token from Pulse (${pulse_url}); skipping automatic node registration"
        return
    fi

    # The response contains a one-time credential; do not persist it as a diagnostic.

    local setup_token
    local setup_type
    local setup_host
    local setup_url
    local setup_download_url
    local setup_script_name
    local setup_command
    local setup_command_with_env
    local setup_command_without_env
    local setup_token_hint
    local setup_expires
    local setup_expiry_state
    IFS=$'\t' read -r setup_token setup_type setup_host setup_url setup_download_url setup_script_name setup_command setup_command_with_env setup_command_without_env setup_token_hint setup_expires setup_expiry_state <<<"$(python3 - "$pulse_url" "$normalized_host_url" "$backup_perms" 3<<<"$setup_response" <<'PY'
import json, sys
import time
from urllib.parse import quote
try:
    data = json.load(__import__("os").fdopen(3))
except Exception:
    print("\t\t\t\t\t\t\t\t\t\t")
    sys.exit(0)
pulse_url = sys.argv[1]
host = sys.argv[2]
backup_query = "backup_perms=true&" if sys.argv[3] == "true" else ""
expires_raw = data.get("expires", "")
expiry_state = ""
try:
    expires_int = int(expires_raw)
except Exception:
    expires_int = 0
if expires_int > int(time.time()):
    expiry_state = "live"
setup_token = str(data.get("setupToken", ""))
token_hint = str(data.get("tokenHint", ""))
setup_url = str(data.get("url", ""))
setup_download_url = str(data.get("downloadURL", ""))
setup_script_name = str(data.get("scriptFileName", ""))
setup_command = str(data.get("command", ""))
setup_command_with_env = str(data.get("commandWithEnv", ""))
setup_command_without_env = str(data.get("commandWithoutEnv", ""))
expected_setup_url = f"{pulse_url}/api/setup-script?{backup_query}host={quote(host, safe='')}&pulse_url={quote(pulse_url, safe='')}&type=pve"
expected_download_url = expected_setup_url
legacy_download_url = f"{pulse_url}/api/setup-script?{backup_query}host={quote(host, safe='')}&pulse_url={quote(pulse_url, safe='')}&setup_token={quote(setup_token, safe='')}&type=pve" if setup_token else ""
modern = setup_download_url == expected_download_url
expected_script_name = "pulse-setup-pve.sh"
if setup_url != expected_setup_url:
    setup_url = ""
if setup_download_url not in (expected_download_url, legacy_download_url):
    setup_download_url = ""
if setup_script_name != expected_script_name:
    setup_script_name = ""
if not setup_command:
    setup_command = ""
if not setup_command_with_env:
    setup_command_with_env = ""
if not setup_command_without_env:
    setup_command_without_env = ""
command_fields = (
    ("command", setup_command, True),
    ("commandWithEnv", setup_command_with_env, True),
    ("commandWithoutEnv", setup_command_without_env, False),
)
for _field_name, _value, _requires_token in command_fields:
    if not _value or expected_setup_url not in _value:
        if _field_name == "command":
            setup_command = ""
        elif _field_name == "commandWithEnv":
            setup_command_with_env = ""
        else:
            setup_command_without_env = ""
        continue
    if 'if [ "$(id -u)" -eq 0 ]; then' not in _value or 'elif command -v sudo >/dev/null 2>&1; then' not in _value:
        if _field_name == "command":
            setup_command = ""
        elif _field_name == "commandWithEnv":
            setup_command_with_env = ""
        else:
            setup_command_without_env = ""
        continue
    if modern:
        valid = "PULSE_SETUP_TOKEN_FILE=" in _value and "PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value
    else:
        valid = "PULSE_SETUP_TOKEN_FILE=" not in _value and (("PULSE_SETUP_TOKEN=" in _value and setup_token in _value) if _requires_token else ("PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value))
    if not valid:
        if _field_name == "command":
            setup_command = ""
        elif _field_name == "commandWithEnv":
            setup_command_with_env = ""
        else:
            setup_command_without_env = ""
if not token_hint or token_hint == setup_token:
    token_hint = ""
print("\t".join([
    setup_token,
    str(data.get("type", "")),
    str(data.get("host", "")),
    setup_url,
    setup_download_url,
    setup_script_name,
    setup_command,
    setup_command_with_env,
    setup_command_without_env,
    token_hint,
    str(expires_raw),
    expiry_state,
]))
PY
)" || setup_token=""

    local backup_query=""
    [[ "$backup_perms" == "true" ]] && backup_query="backup_perms=true&"
    local expected_setup_url="${pulse_url}/api/setup-script?${backup_query}host=$(python3 - <<'PY' "$normalized_host_url"
from urllib.parse import quote
import sys
print(quote(sys.argv[1], safe=''))
PY
)&pulse_url=$(python3 - <<'PY' "$pulse_url"
from urllib.parse import quote
import sys
print(quote(sys.argv[1], safe=''))
PY
)&type=pve"
    local expected_download_url="$(python3 - <<'PY' "$normalized_host_url" "$pulse_url" "$backup_perms" 3<<<"$setup_token"
from urllib.parse import quote
import sys
host, pulse_url, backup_perms = sys.argv[1:]
backup_query = "backup_perms=true&" if backup_perms == "true" else ""
setup_token = __import__("os").fdopen(3).read().strip()
print(f"{pulse_url}/api/setup-script?{backup_query}host={quote(host, safe='')}&pulse_url={quote(pulse_url, safe='')}&setup_token={quote(setup_token, safe='')}&type=pve")
PY
)"
    local expected_script_name="pulse-setup-pve.sh"

    if [[ -z "$setup_token" ]] || [[ "$setup_type" != "pve" ]] || [[ "$setup_host" != "$normalized_host_url" ]] || [[ "$setup_url" != "$expected_setup_url" ]] || { [[ "$setup_download_url" != "$expected_download_url" ]] && [[ "$setup_download_url" != "$expected_setup_url" ]]; } || [[ "$setup_script_name" != "$expected_script_name" ]] || [[ -z "$setup_command" ]] || [[ -z "$setup_command_with_env" ]] || [[ -z "$setup_command_without_env" ]] || [[ -z "$setup_token_hint" ]] || [[ -z "$setup_expires" ]] || [[ "$setup_expiry_state" != "live" ]]; then
        AUTO_NODE_REGISTER_ERROR="missing setup token"
        print_warn "Pulse did not return a setup token; skipping automatic node registration"
        return
    fi

    pveum user add pulse-monitor@pve --comment "Pulse monitoring service" >/dev/null 2>&1 || true
    pveum aclmod / -user pulse-monitor@pve -role PVEAuditor >/dev/null 2>&1 || true
    if [[ "$backup_perms" == "true" ]]; then
        pveum aclmod /storage -user pulse-monitor@pve -role PVEDatastoreAdmin >/dev/null 2>&1 || true
    fi

    local pulse_monitor_role_ready=false
    local extra_privs=()
    if pveum role list 2>/dev/null | grep -q "Sys.Audit"; then
        extra_privs+=("Sys.Audit")
    elif pveum role add PulseSysAuditProbe -privs Sys.Audit >/dev/null 2>&1; then
        extra_privs+=("Sys.Audit")
        pveum role delete PulseSysAuditProbe >/dev/null 2>&1 || true
    fi

    local has_guest_audit=false
    if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.Audit"; then
        has_guest_audit=true
    elif pveum role add PulseGuestAuditProbe -privs VM.GuestAgent.Audit >/dev/null 2>&1; then
        has_guest_audit=true
        pveum role delete PulseGuestAuditProbe >/dev/null 2>&1 || true
    fi

    if [[ "$has_guest_audit" == true ]]; then
        extra_privs+=("VM.GuestAgent.Audit")
        if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.FileRead"; then
            extra_privs+=("VM.GuestAgent.FileRead")
        elif pveum role add PulseGuestFileReadProbe -privs VM.GuestAgent.FileRead >/dev/null 2>&1; then
            extra_privs+=("VM.GuestAgent.FileRead")
            pveum role delete PulseGuestFileReadProbe >/dev/null 2>&1 || true
        fi
    else
        local has_vm_monitor=false
        if pveum role list 2>/dev/null | grep -q "VM.Monitor"; then
            has_vm_monitor=true
        elif pveum role add PulseVmMonitorProbe -privs VM.Monitor >/dev/null 2>&1; then
            has_vm_monitor=true
            pveum role delete PulseVmMonitorProbe >/dev/null 2>&1 || true
        fi
        if [[ "$has_vm_monitor" == true ]]; then
            extra_privs+=("VM.Monitor")
        fi
    fi

    if [[ ${#extra_privs[@]} -gt 0 ]]; then
        local priv_string
        priv_string="$(IFS=,; echo "${extra_privs[*]}")"
        if pveum role modify PulseMonitor -privs "$priv_string" >/dev/null 2>&1 || pveum role add PulseMonitor -privs "$priv_string" >/dev/null 2>&1; then
            pulse_monitor_role_ready=true
            pveum aclmod / -user pulse-monitor@pve -role PulseMonitor >/dev/null 2>&1 || true
        fi
    fi

    local token_name
    token_name=$(python3 - <<'PY' "$pulse_url"
import re, sys, urllib.parse

raw = sys.argv[1].strip()
host = ""
if raw:
    parsed = urllib.parse.urlparse(raw)
    host = (parsed.hostname or "").strip().lower().strip(".")

slug = re.sub(r"[^a-z0-9]+", "-", host)
slug = slug.strip("-")
if len(slug) > 48:
    slug = slug[:48].strip("-")
if not slug:
    slug = "server"

print(f"pulse-{slug}")
PY
)

    local token_output=""
    local token_status=0
    create_pve_auto_register_token "$token_name" token_output token_status
    if [[ $token_status -ne 0 ]] && pve_token_already_exists_error "$token_output"; then
        print_info "Existing Proxmox monitoring token '${token_name}' found; rotating it so Pulse receives a fresh secret"
        if ! pveum user token remove pulse-monitor@pve "$token_name" >/dev/null 2>&1; then
            AUTO_NODE_REGISTER_ERROR="failed to rotate existing token"
            print_warn "Unable to remove existing monitoring API token; skipping automatic node registration"
            return
        fi
        create_pve_auto_register_token "$token_name" token_output token_status
    fi
    if [[ $token_status -ne 0 ]]; then
        AUTO_NODE_REGISTER_ERROR="failed to create token"
        print_warn "Unable to create monitoring API token; skipping automatic node registration"
        return
    fi

    local token_value
    token_value=$(extract_pve_token_value "$token_output" | tail -n1 | tr -d '\r')
    if [[ -z "$token_value" ]]; then
        AUTO_NODE_REGISTER_ERROR="token value unavailable"
        print_warn "Failed to extract token value from pveum output; skipping automatic node registration"
        return
    fi
    local token_id="pulse-monitor@pve!${token_name}"
    pveum aclmod / -token "$token_id" -role PVEAuditor >/dev/null 2>&1 || true
    if [[ "$backup_perms" == "true" ]]; then
        pveum aclmod /storage -token "$token_id" -role PVEDatastoreAdmin >/dev/null 2>&1 || true
    fi
    if [[ "$pulse_monitor_role_ready" == "true" ]]; then
        pveum aclmod / -token "$token_id" -role PulseMonitor >/dev/null 2>&1 || true
    fi

    if ! smoke_test_pve_auto_register_token "$normalized_host_url" "$token_id" "$token_value"; then
        return
    fi

    local register_payload
    register_payload=$(python3 - "$normalized_host_url" "$token_id" "$server_name" 3<<<"$(printf '%s\n%s\n' "$token_value" "$setup_token")" <<'PY'
import json, sys
host, token_id, server_name = sys.argv[1:]
token_value, setup_token = __import__("os").fdopen(3).read().splitlines()
print(json.dumps({
    "type": "pve",
    "host": host,
    "tokenId": token_id,
    "tokenValue": token_value,
    "serverName": server_name,
    "authToken": setup_token,
    "source": "script"
}))
PY
)

    local register_response
    if ! register_response=$(printf %s "$register_payload" | curl -q --retry 3 --retry-delay 2 -fsS -X POST "$pulse_url/api/auto-register" -H "Content-Type: application/json" -d @-); then
        AUTO_NODE_REGISTER_ERROR="auto-register request failed"
        print_warn "Pulse auto-registration request failed; skipping automatic node registration"
        return
    fi

    local register_status
    local register_action
    local register_type
    local register_source
    local register_host
    local register_token_id
    local register_token_value
    local register_node_id
    local register_node_name
    IFS=$'\t' read -r register_status register_action register_type register_source register_host register_token_id register_token_value register_node_id register_node_name <<<"$(python3 - "$register_response" <<'PY'
import json, sys
try:
    data = json.loads(sys.argv[1])
except Exception:
    print("\t\t\t\t\t\t\t\t")
    sys.exit(0)
print("\t".join([
    str(data.get("status", "")),
    str(data.get("action", "")),
    str(data.get("type", "")),
    str(data.get("source", "")),
    str(data.get("host", "")),
    str(data.get("tokenId", "")),
    str(data.get("tokenValue", "")),
    str(data.get("nodeId", "")),
    str(data.get("nodeName", "")),
]))
PY
)" || register_status=""

    if [[ "$register_status" != "success" ]] || [[ "$register_action" != "use_token" ]] || [[ "$register_type" != "pve" ]] || [[ "$register_source" != "script" ]] || [[ "$register_host" != "$normalized_host_url" ]] || [[ "$register_token_id" != "$token_id" ]] || [[ "$register_token_value" != "$token_value" ]] || [[ -z "$register_node_id" ]] || [[ -z "$register_node_name" ]] || [[ "$register_node_id" != "$register_node_name" ]]; then
        AUTO_NODE_REGISTER_ERROR="auto-register unsuccessful"
        print_warn "Pulse auto-registration reported an error: $register_response"
        return
    fi

    AUTO_NODE_REGISTERED=true
    AUTO_NODE_REGISTERED_NAME="$register_node_name"
    AUTO_NODE_REGISTER_ERROR=""
    print_success "Registered ${register_node_name} with Pulse automatically"
}

# Compare two version strings
# Returns: 0 if equal, 1 if first > second, 2 if first < second
compare_versions() {
    local v1="${1#v}"  # Remove 'v' prefix
    local v2="${2#v}"
    
    # Strip any pre-release suffix (e.g., -rc.1, -beta, etc.)
    local base_v1="${v1%%-*}"
    local base_v2="${v2%%-*}"
    local suffix_v1="${v1#*-}"
    local suffix_v2="${v2#*-}"
    
    # If no suffix, suffix equals the full version
    [[ "$suffix_v1" == "$v1" ]] && suffix_v1=""
    [[ "$suffix_v2" == "$v2" ]] && suffix_v2=""
    
    # Split base versions into parts
    IFS='.' read -ra V1_PARTS <<< "$base_v1"
    IFS='.' read -ra V2_PARTS <<< "$base_v2"
    
    # Compare major.minor.patch
    for i in 0 1 2; do
        local p1="${V1_PARTS[$i]:-0}"
        local p2="${V2_PARTS[$i]:-0}"
        if [[ "$p1" -gt "$p2" ]]; then
            return 1
        elif [[ "$p1" -lt "$p2" ]]; then
            return 2
        fi
    done
    
    # Base versions are equal, now compare suffixes
    # No suffix (stable) > rc suffix
    if [[ -z "$suffix_v1" ]] && [[ -n "$suffix_v2" ]]; then
        return 1  # v1 (stable) > v2 (rc)
    elif [[ -n "$suffix_v1" ]] && [[ -z "$suffix_v2" ]]; then
        return 2  # v1 (rc) < v2 (stable)
    elif [[ -n "$suffix_v1" ]] && [[ -n "$suffix_v2" ]]; then
        # Published preview revisions are numeric: rc.10 follows rc.9, not
        # vice versa. Keep the existing lexical fallback for other suffixes.
        if [[ "$suffix_v1" =~ ^(beta|rc)[.][0-9]+$ && "$suffix_v2" =~ ^(beta|rc)[.][0-9]+$ && "${suffix_v1%.*}" == "${suffix_v2%.*}" ]]; then
            local revision_v1="${suffix_v1##*.}" revision_v2="${suffix_v2##*.}"
            if (( 10#$revision_v1 > 10#$revision_v2 )); then
                return 1
            elif (( 10#$revision_v1 < 10#$revision_v2 )); then
                return 2
            fi
            return 0
        fi
        # Both have suffixes, compare them lexicographically
        if [[ "$suffix_v1" > "$suffix_v2" ]]; then
            return 1
        elif [[ "$suffix_v1" < "$suffix_v2" ]]; then
            return 2
        fi
    fi
    
    return 0  # versions are equal
}

version_major() {
    local version="${1:-}"
    version="${version#v}"
    version="${version%%-*}"

    local major="${version%%.*}"
    if [[ ! "$major" =~ ^[0-9]+$ ]]; then
        return 1
    fi

    printf '%s\n' "$major"
}

is_pre_v6_to_v6_upgrade() {
    local current_version="${1:-}"
    local target_version="${2:-}"
    local current_major=""
    local target_major=""

    current_major=$(version_major "$current_version" 2>/dev/null || true)
    target_major=$(version_major "$target_version" 2>/dev/null || true)

    if [[ -z "$current_major" || -z "$target_major" ]]; then
        return 1
    fi

    [[ "$current_major" -lt 6 && "$target_major" -ge 6 ]]
}

inspect_api_tokens_for_upgrade() {
    local token_file="$1"

    if ! command -v python3 >/dev/null 2>&1; then
        return 1
    fi

    python3 - "$token_file" <<'PY'
import datetime
import json
import sys

path = sys.argv[1]
try:
    with open(path, "r", encoding="utf-8") as handle:
        tokens = json.load(handle)
except Exception:
    print("invalid\t0\t0\t0\t0")
    sys.exit(0)

if not isinstance(tokens, list):
    print("invalid\t0\t0\t0\t0")
    sys.exit(0)

now = datetime.datetime.now(datetime.timezone.utc)
agent_scoped = 0
expired = 0
expiring = 0
for token in tokens:
    if not isinstance(token, dict):
        continue
    scopes = token.get("scopes")
    if not scopes:
        scopes = ["*"]
    if not isinstance(scopes, list):
        scopes = [str(scopes)]
    normalized = {str(scope).strip() for scope in scopes}
    if not ({"*", "agent:report", "host-agent:report"} & normalized):
        continue
    agent_scoped += 1
    expires_at = token.get("expiresAt") or token.get("expires_at")
    if not expires_at:
        continue
    try:
        raw = str(expires_at).strip()
        parsed = datetime.datetime.fromisoformat(raw.replace("Z", "+00:00"))
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=datetime.timezone.utc)
    except Exception:
        continue
    if parsed <= now:
        expired += 1
    elif parsed - now <= datetime.timedelta(days=14):
        expiring += 1

print(f"ok\t{len(tokens)}\t{agent_scoped}\t{expired}\t{expiring}")
PY
}

run_upgrade_readiness_preflight() {
    local current_version="${1:-}"
    local target_version="${2:-}"
    local token_file="$CONFIG_DIR/api_tokens.json"

    if ! is_pre_v6_to_v6_upgrade "$current_version" "$target_version"; then
        return 0
    fi

    if [[ "$UPGRADE_PREFLIGHT_RAN" == "true" ]]; then
        return 0
    fi
    UPGRADE_PREFLIGHT_RAN=true

    if [[ "$SKIP_UPGRADE_PREFLIGHT" == "true" ]]; then
        print_warn "Skipping Pulse v5 to v6 upgrade preflight because PULSE_SKIP_UPGRADE_PREFLIGHT=true or --skip-upgrade-preflight was set"
        return 0
    fi

    echo
    print_info "Running Pulse v5 to v6 upgrade preflight..."

    if [[ -e "$token_file" && ! -r "$token_file" ]]; then
        print_error "Cannot read API token inventory at $token_file"
        print_info "Fix file permissions before upgrading so Pulse can preserve agent credentials."
        return 1
    fi

    if [[ ! -f "$token_file" ]]; then
        print_warn "No API token inventory found at $token_file"
        print_info "If this instance has v5 host agents, refresh or reinstall their agent token after the server starts on v6."
        return 0
    fi

    local report=""
    local status=""
    local total_tokens=0
    local agent_tokens=0
    local expired_tokens=0
    local expiring_tokens=0

    report=$(inspect_api_tokens_for_upgrade "$token_file" 2>/dev/null || true)
    IFS=$'\t' read -r status total_tokens agent_tokens expired_tokens expiring_tokens <<<"$report"

    if [[ "$status" == "ok" ]]; then
        if [[ "$agent_tokens" -gt 0 && "$expired_tokens" -lt "$agent_tokens" ]]; then
            print_success "Agent reporting token scope is present for v6 compatibility"
            if [[ "$expired_tokens" -gt 0 || "$expiring_tokens" -gt 0 ]]; then
                print_warn "$expired_tokens agent reporting token(s) are expired and $expiring_tokens expire within 14 days"
            fi
        elif [[ "$agent_tokens" -gt 0 ]]; then
            print_warn "All agent reporting tokens in api_tokens.json are expired"
            print_info "Existing agents may need a refreshed install token after the upgrade."
        else
            print_warn "No agent reporting token scope was found in api_tokens.json"
            print_info "Existing v5 host agents need a token with agent:report or host-agent:report to reconnect through v6 compatibility routes."
        fi
    elif grep -Eq '"(agent:report|host-agent:report|\\*)"' "$token_file" 2>/dev/null; then
        print_warn "API token inventory could not be parsed, but an agent reporting scope string is present"
    else
        print_warn "API token inventory could not be parsed and no agent reporting scope string was found"
    fi

    print_info "The in-app updater performs the live registered-agent readiness check before applying v6 updates."
    return 0
}


check_existing_installation() {
    CURRENT_VERSION=""  # Make it global so we can use it later
    local BINARY_PATH=""
    local detected_service="$SERVICE_NAME"
    local service_available=false
    local service_running=false
    local active_state=""

    # Check for the binary in expected locations
    if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
        BINARY_PATH="$INSTALL_DIR/bin/pulse"
    elif [[ -f "$INSTALL_DIR/pulse" ]]; then
        BINARY_PATH="$INSTALL_DIR/pulse"
    fi

    # Detect actual service name if systemd is available
    if command -v systemctl >/dev/null 2>&1; then
        if [[ "$SERVICE_NAME_EXPLICIT" != "true" ]]; then
            # Status 1 below means no installation. Keep an unavailable
            # inventory distinct so main cannot fall through to a fresh install.
            detected_service=$(detect_service_name) || return 2
            SERVICE_NAME="$detected_service"
        fi
        service_available=true
    fi

    # A failed or stuck manager read is not evidence that Pulse is stopped or
    # absent. Refuse before main can select fresh installation or replacement.
    # Keep this observation separate from the unit-file inventory above: a
    # successful inventory does not establish that the manager is reachable.
    if [[ "$service_available" == true ]]; then
        if ! active_state=$(LC_ALL=C timeout -k 1 5 systemctl show "$detected_service" --property=ActiveState --value 2>/dev/null); then
            print_error "Cannot determine the Pulse service state. No installation changes were made; inspect systemctl status $detected_service before retrying." >&2
            return 2
        fi
        case "$active_state" in
            active|reloading|refreshing) service_running=true ;;
            inactive|failed) ;;
            *)
                print_error "Cannot determine the Pulse service state: it is unknown or still changing. No installation changes were made; inspect systemctl status $detected_service before retrying." >&2
                return 2 ;;
        esac
    fi

    # Try to get version if binary exists
    if [[ -n "$BINARY_PATH" ]]; then
        CURRENT_VERSION=$("$BINARY_PATH" --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?' | head -1 || echo "unknown")
    fi
    
    if [[ "$service_running" == true ]]; then
        if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then
            print_info "Pulse $CURRENT_VERSION is currently running"
        else
            print_info "Pulse is currently running"
        fi
        return 0
    elif [[ -n "$BINARY_PATH" ]]; then
        if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then
            print_info "Pulse $CURRENT_VERSION is installed but not running"
        else
            print_info "Pulse is installed but not running"
        fi
        return 0
    else
        return 1
    fi
}

install_dependencies() {
    print_info "Installing dependencies..."
    
    apt-get update -qq >/dev/null 2>&1
    # Install essential dependencies plus jq for reliable JSON handling.
    # openssh-client provides ssh-keygen, which signed release verification
    # requires before installing downloaded or local release archives.
    apt-get install -y -qq curl wget ca-certificates openssh-client jq >/dev/null 2>&1 || {
        # If jq fails to install, keep the installer-critical dependencies.
        apt-get install -y -qq curl wget ca-certificates openssh-client >/dev/null 2>&1
    }
}

create_user() {
    if ! id -u pulse &>/dev/null; then
        print_info "Creating pulse user..."
        useradd --system --home-dir $INSTALL_DIR --shell /bin/false pulse
    fi
}

config_backup_receipt_matches() {
    local backup_dir="$1" receipt="$1.pulse-snapshot"
    local size identity version source recorded_path recorded_identity remainder=""
    # A name is not evidence that this installer created the directory. Bind
    # the private sidecar to the exact source, destination and directory inode.
    # Replaced directories, copied receipts, links and malformed records stay put.
    [[ -d "$backup_dir" && ! -L "$backup_dir" && -f "$receipt" && ! -L "$receipt" ]] || return 1
    size=$(stat -c '%s' -- "$receipt") || return 1
    [[ "$size" =~ ^[0-9]+$ ]] && (( size <= 16384 )) || return 1
    identity=$(stat -c '%d:%i' -- "$backup_dir") || return 1
    {
        IFS= read -r -d '' version &&
            IFS= read -r -d '' source &&
            IFS= read -r -d '' recorded_path &&
            IFS= read -r -d '' recorded_identity || return 1
        if IFS= read -r -d '' remainder || [[ -n "$remainder" ]]; then
            return 1
        fi
        [[ "$version" == pulse-config-snapshot-v1 && "$source" == "$CONFIG_DIR" &&
           "$recorded_path" == "$backup_dir" && "$recorded_identity" == "$identity" ]]
    } < "$receipt"
}

prune_config_backups() {
    # Unattended updates create one snapshot per run; without rotation they
    # fill small root filesystems (issue #1646). Keep the newest snapshots
    # and delete the rest. Snapshot names embed a sortable timestamp. Positive
    # retention keeps this invocation's completed snapshot, even after a clock step
    # backwards or when random suffixes share the same timestamp.
    local backup_parent="$1"
    local snapshot_prefix="$2"
    local keep="${CONFIG_BACKUP_KEEP_COUNT:-5}"
    local current_backup="${3:-}" current_count=0
    local LC_ALL=C
    local backups=()
    local entry unrecognised=0

    # Expand only the final wildcard, never the instance's literal prefix.
    # A find -name pattern borrows []?* from a custom config name; line-based
    # output also turns an embedded newline into a different deletion target.
    # Bash's ordered pathname expansion keeps each complete path as one word.
    # Match find's non-following directory scope, including an empty inventory.
    for entry in "$backup_parent/$snapshot_prefix"*; do
        [[ -d "$entry" && ! -L "$entry" ]] || continue
        if ! config_backup_receipt_matches "$entry"; then
            (( unrecognised += 1 ))
            continue
        fi
        if [[ "$entry" == "$current_backup" ]] && (( keep > 0 )); then
            current_count=1
            continue
        fi
        backups+=("$entry")
    done
    if (( unrecognised > 0 )); then
        print_warn "Left $unrecognised unrecognised configuration backup directories unchanged; retention applies only to verified installer snapshots"
    fi

    local excess=$(( ${#backups[@]} + current_count - keep ))
    if (( excess <= 0 )); then
        return 0
    fi

    local i
    for (( i = 0; i < excess; i++ )); do
        if ! rm -rf -- "${backups[$i]}"; then
            print_warn "Could not remove old configuration backup ${backups[$i]}"
        elif ! rm -f -- "${backups[$i]}.pulse-snapshot"; then
            print_warn "Could not remove configuration backup receipt ${backups[$i]}.pulse-snapshot"
        fi
    done
}

backup_existing() {
    LAST_CONFIG_BACKUP_DIR=""
    if [[ -d "$CONFIG_DIR" ]]; then
        print_info "Backing up existing configuration..."
        local backup_parent backup_dir backup_stage backup_payload stage_name config_name backup_identity
        # Command substitution strips every trailing newline, including bytes
        # in the configured name or its parent. Keep a sentinel until after
        # capture, then remove only the utility's one output newline and marker.
        # The same literal name must select both publication and retention.
        if ! backup_parent=$(dirname -- "$CONFIG_DIR" && printf '.'); then
            print_error "Could not resolve configuration backup parent; existing snapshots are unchanged"
            return 1
        fi
        backup_parent="${backup_parent%$'\n.'}"
        if ! config_name=$(basename -- "$CONFIG_DIR" && printf '.'); then
            print_error "Could not resolve configuration backup name; existing snapshots are unchanged"
            return 1
        fi
        config_name="${config_name%$'\n.'}"
        if [[ ! -w "$backup_parent" ]]; then
            # The hardened pulse-update.service unit mounts /etc read-only
            # (ProtectSystem=strict; only /opt/pulse, /etc/pulse and /tmp are
            # writable), so keep the safety copy under the install dir there.
            backup_parent="$INSTALL_DIR/config-backups"
            mkdir -p "$backup_parent" || return 1
        fi
        ensure_config_backup_headroom "$CONFIG_DIR" "$backup_parent" || return 1
        # Reserve private work before copying. A second-resolution destination
        # can already be another run's recovery copy: cp would nest into it,
        # and failed-copy cleanup would then erase that earlier snapshot.
        # Hidden, incomplete copies are not candidates for snapshot rotation.
        # Keep a private outer enclosure: cp -a also adopts the source root's
        # ownership/permissions, so copying directly into the reserved directory
        # can expose a legacy broadly readable config before publication.
        if ! backup_stage=$(mktemp -d -- "$backup_parent/.$config_name.backup.$(date +%Y%m%d-%H%M%S).XXXXXX"); then
            print_error "Could not create configuration backup; existing snapshots are unchanged"
            return 1
        fi
        stage_name="${backup_stage##*/}"
        backup_dir="$backup_parent/${stage_name#.}"
        backup_payload="$backup_stage/snapshot"
        if ! mkdir -- "$backup_payload" || ! cp -a -- "$CONFIG_DIR/." "$backup_payload/"; then
            rm -rf -- "$backup_stage"
            print_error "Configuration backup failed; existing snapshots are unchanged"
            return 1
        fi
        # Retain the source ownership for direct recovery, including the root
        # directory. Only its mode is tightened before leaving the private outer
        # enclosure; nested file modes, hidden entries and links stay unchanged.
        if ! chmod 700 -- "$backup_payload"; then
            rm -rf -- "$backup_stage"
            print_error "Could not protect configuration backup; existing snapshots are unchanged"
            return 1
        fi
        # The sidecar is outside the copied tree: never overwrite a source
        # file or add metadata to the recovery payload itself. NUL delimiters
        # retain every pathname byte, including embedded/trailing newlines.
        if ! backup_identity=$(stat -c '%d:%i' -- "$backup_payload") ||
           ! (umask 077; printf 'pulse-config-snapshot-v1\0%s\0%s\0%s\0' \
               "$CONFIG_DIR" "$backup_dir" "$backup_identity" > "$backup_stage/receipt") ||
           [[ -e "$backup_dir.pulse-snapshot" || -L "$backup_dir.pulse-snapshot" ]]; then
            rm -rf -- "$backup_stage"
            print_error "Could not prepare configuration backup ownership receipt; existing snapshots are unchanged"
            return 1
        fi
        # GNU mv -n can return zero without moving anything. Admit publication
        # only when our stage actually disappeared; never merge or overwrite a
        # file, directory or link that appeared at the final path.
        if ! mv -T -n -- "$backup_payload" "$backup_dir" || [[ -e "$backup_payload" || -L "$backup_payload" ]]; then
            rm -rf -- "$backup_stage"
            print_error "Could not publish configuration backup; existing snapshots are unchanged"
            return 1
        fi
        if ! mv -T -n -- "$backup_stage/receipt" "$backup_dir.pulse-snapshot" ||
           [[ -e "$backup_stage/receipt" || -L "$backup_stage/receipt" ]]; then
            rm -rf -- "$backup_stage"
            print_error "Configuration copy retained at $backup_dir, but its ownership receipt could not be published; no snapshots were rotated"
            return 1
        fi
        rmdir -- "$backup_stage" || print_warn "Could not remove empty configuration backup staging directory"
        LAST_CONFIG_BACKUP_DIR="$backup_dir"
        prune_config_backups "$backup_parent" "$config_name.backup." "$backup_dir"
    fi
}

# Drop the configuration snapshot most recently taken by backup_existing when
# the update aborts before it stages or replaces anything. That snapshot was a
# safety copy for an install that never began; leaving it behind makes the
# low-space condition that caused the failure progressively worse on every
# automatic retry until the filesystem fills (#2127).
discard_last_config_backup() {
    if [[ -n "${LAST_CONFIG_BACKUP_DIR:-}" && -e "$LAST_CONFIG_BACKUP_DIR" ]]; then
        if ! config_backup_receipt_matches "$LAST_CONFIG_BACKUP_DIR"; then
            print_warn "Configuration backup ownership is unverified; leaving $LAST_CONFIG_BACKUP_DIR unchanged"
        elif ! rm -rf -- "$LAST_CONFIG_BACKUP_DIR"; then
            print_warn "Could not remove unneeded configuration backup ${LAST_CONFIG_BACKUP_DIR}"
        elif ! rm -f -- "$LAST_CONFIG_BACKUP_DIR.pulse-snapshot"; then
            print_warn "Could not remove configuration backup receipt $LAST_CONFIG_BACKUP_DIR.pulse-snapshot"
        fi
    fi
    LAST_CONFIG_BACKUP_DIR=""
}

resolve_archive_override() {
    local archive_path="$1"

    if [[ -z "$archive_path" ]]; then
        print_error "Archive path is required"
        return 1
    fi
    if [[ ! -f "$archive_path" ]]; then
        print_error "Archive not found: $archive_path"
        return 1
    fi
    if [[ ! -r "$archive_path" ]]; then
        print_error "Archive is not readable: $archive_path"
        return 1
    fi

    if command -v realpath >/dev/null 2>&1; then
        realpath "$archive_path"
    else
        printf '%s\n' "$archive_path"
    fi
}

infer_release_from_archive_name() {
    local archive_name
    archive_name=$(basename "$1")

    if [[ "$archive_name" =~ ^pulse(-pro)?-(v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)*)-linux-(amd64|arm64|armv7|armv6|386)\.tar\.gz$ ]]; then
        printf '%s\n' "${BASH_REMATCH[2]}"
        return 0
    fi

    return 1
}

detect_pulse_architecture() {
    local raw_arch="${1:-$(uname -m)}"

    case $raw_arch in
        x86_64|amd64)
            printf 'amd64\n'
            ;;
        aarch64|arm64)
            printf 'arm64\n'
            ;;
        armv7l|armv7)
            printf 'armv7\n'
            ;;
        *)
            return 1
            ;;
    esac
}

find_pulse_binary_in_dir() {
    local dir="$1"

    # Reject symlinks: a malicious archive could place a symlink at the
    # expected binary path pointing at an arbitrary file outside the
    # extract directory, which would then be cp'd to $INSTALL_DIR/bin/pulse.
    if [[ -f "$dir/bin/pulse" && ! -L "$dir/bin/pulse" ]]; then
        printf '%s\n' "$dir/bin/pulse"
        return 0
    fi
    if [[ -f "$dir/pulse" && ! -L "$dir/pulse" ]]; then
        printf '%s\n' "$dir/pulse"
        return 0
    fi

    return 1
}

detect_pulse_binary_architecture() {
    local binary_path="$1"
    local file_info=""
    local machine=""
    local machine_id=""

    if [[ ! -f "$binary_path" ]]; then
        return 1
    fi

    if command -v readelf >/dev/null 2>&1; then
        machine=$(LC_ALL=C readelf -h "$binary_path" 2>/dev/null | awk -F: '/Machine:/ {gsub(/^[[:space:]]+/, "", $2); print $2; exit}')
        case "$machine" in
            "Advanced Micro Devices X86-64")
                printf 'amd64\n'
                return 0
                ;;
            "AArch64")
                printf 'arm64\n'
                return 0
                ;;
            "ARM")
                printf 'armv7\n'
                return 0
                ;;
        esac
    fi

    if command -v file >/dev/null 2>&1; then
        file_info=$(LC_ALL=C file -b "$binary_path" 2>/dev/null || true)
        case "$file_info" in
            *"x86-64"*)
                printf 'amd64\n'
                return 0
                ;;
            *"aarch64"*|*"ARM64"*)
                printf 'arm64\n'
                return 0
                ;;
            *" ARM "*|ARM,*|*" EABI5"*)
                printf 'armv7\n'
                return 0
                ;;
        esac
    fi

    machine_id=$(dd if="$binary_path" bs=1 skip=18 count=2 2>/dev/null | od -An -tu2 2>/dev/null | tr -d '[:space:]')
    case "$machine_id" in
        62)
            printf 'amd64\n'
            return 0
            ;;
        183)
            printf 'arm64\n'
            return 0
            ;;
        40)
            printf 'armv7\n'
            return 0
            ;;
    esac

    return 1
}

validate_pulse_binary_architecture() {
    local binary_path="$1"
    local target_arch="$2"
    local archive_label="${3:-archive}"
    local binary_arch=""

    if [[ -z "$target_arch" ]]; then
        print_error "Target architecture is required for archive validation"
        return 1
    fi

    binary_arch=$(detect_pulse_binary_architecture "$binary_path" 2>/dev/null || true)
    if [[ -z "$binary_arch" ]]; then
        print_error "Could not determine Pulse binary architecture from $archive_label"
        print_info "Use an official Pulse Linux release tarball for this machine."
        return 1
    fi

    if [[ "$binary_arch" != "$target_arch" ]]; then
        print_error "Archive architecture mismatch: $archive_label contains $binary_arch but this target requires $target_arch"
        print_info "Download the official Pulse release tarball for the target architecture."
        return 1
    fi

    return 0
}

create_temp_archive_path() {
    local prefix="$1"
    local temp_dir=""

    # Reserving then unlinking a mktemp file does not reserve the .tar.gz or
    # sidecar name. Keep both downloads inside one owner-only directory.
    # A hidden staging prefix still yields the canonical archive file name.
    local archive_name="${prefix##*/}"
    archive_name="${archive_name#.}"
    temp_dir=$(mktemp -d "${prefix}-XXXXXX") || return 1
    printf '%s/%s.tar.gz\n' "$temp_dir" "$archive_name"
}

# Server archives are staged and extracted on the install filesystem. /tmp is
# tmpfs on Debian 13 and many LXC templates, where staged bytes count against
# the container's memory limit rather than its disk (#2785).
prepare_server_staging_parent() {
    if ! mkdir -p -- "$INSTALL_DIR"; then
        print_error "Could not prepare $INSTALL_DIR for staging the Pulse update; Pulse has not been stopped"
        return 1
    fi
    sweep_stale_server_staging
    printf '%s\n' "$INSTALL_DIR"
}

# Owner-only staging names created under $INSTALL_DIR by create_temp_archive_path
# and install_pulse_archive (mktemp's six-character suffix).
is_server_staging_name() {
    case "$1" in
        .pulse-extract-??????|.pulse-v*-linux-*-??????) return 0 ;;
    esac
    return 1
}

# Unlike /tmp, the install filesystem keeps an interrupted run's staging
# (~1 GB) across reboots. No installer lock exists, so remove only real
# directories (never symlinks or files) unchanged for longer than any live run
# takes. ctime, unlike mtime, cannot be carried back by extraction.
sweep_stale_server_staging() {
    local entry=""
    for entry in "$INSTALL_DIR"/.pulse-extract-* "$INSTALL_DIR"/.pulse-v*-linux-*; do
        [[ -d "$entry" && ! -L "$entry" ]] || continue
        is_server_staging_name "${entry##*/}" || continue
        if [[ -n "$(find "$entry" -maxdepth 0 -cmin "+${SERVER_STAGING_STALE_MINUTES}" -print 2>/dev/null)" ]]; then
            rm -rf -- "$entry" 2>/dev/null || print_warn "Could not remove stale update staging at $entry"
        fi
    done
    return 0
}

# Recursive pulse ownership for the install directory, excluding root-consumed
# update staging so the extracted payload and archive are never handed to the
# service account while the installer still reads them.
chown_install_dir_for_pulse() {
    local entry="" rc=0
    chown pulse:pulse "$INSTALL_DIR" || rc=1
    for entry in "$INSTALL_DIR"/* "$INSTALL_DIR"/.[!.]* "$INSTALL_DIR"/..?*; do
        [[ -e "$entry" || -L "$entry" ]] || continue
        is_server_staging_name "${entry##*/}" && continue
        chown -R pulse:pulse "$entry" || rc=1
    done
    return "$rc"
}

cleanup_temp_archive_path() {
    local archive_path="$1"
    local cleanup_status=0
    [[ -n "$archive_path" ]] || return 0
    rm -f -- "$archive_path" "${archive_path}.sshsig" || cleanup_status=1
    # Delete only the two owned leaves and an empty parent, never a tree.
    rmdir -- "$(dirname "$archive_path")" 2>/dev/null || true
    return "$cleanup_status"
}

cleanup_container_install_inputs() {
    local script_dir="${1:-}" archive_path="${2:-}" archive_owned="${3:-false}"
    local cleanup_status=0
    if [[ -n "$script_dir" ]]; then
        rm -f -- "$script_dir/install.sh" "$script_dir/install.sh.sshsig" "$script_dir/curl-error" || cleanup_status=1
        rmdir -- "$script_dir" 2>/dev/null || true
    fi
    if [[ "$archive_owned" == true ]]; then
        cleanup_temp_archive_path "$archive_path" || cleanup_status=1
    fi
    return "$cleanup_status"
}

download_container_installer() {
    local source_output="$1" directory_output="$2"
    local download_url="" staging_dir="" staged_installer="" staged_signature=""
    local download_success=false download_error="" attempt
    local max_retries=3

    if ! download_url=$(resolve_install_script_download_url); then
        print_error "Failed to determine installer download URL for the selected release channel"
        return 1
    fi
    if ! require_release_signature_verifier; then
        print_error "Cannot verify the signed container installer"
        return 1
    fi
    if ! staging_dir=$(mktemp -d /tmp/pulse-lxc-installer-XXXXXX); then
        print_error "Could not prepare private container installer staging"
        return 1
    fi
    # Return the owned directory before transport so the caller's EXIT trap
    # can clean an interrupted download too. No path is returned for execution
    # until both the complete transfer and the pinned signature are admitted.
    printf -v "$directory_output" '%s' "$staging_dir"
    staged_installer="$staging_dir/install.sh"
    staged_signature="$staging_dir/install.sh.sshsig"
    for attempt in $(seq 1 "$max_retries"); do
        if [[ "$attempt" -gt 1 ]]; then
            print_info "Retrying download (attempt $attempt/$max_retries)..."
            sleep 2
        fi
        if command -v timeout >/dev/null 2>&1; then
            if timeout 30 curl -q -fsSL --connect-timeout 10 --max-time 30 "$download_url" > "$staged_installer" 2>"$staging_dir/curl-error"; then
                download_success=true
                break
            fi
        elif curl -q -fsSL --connect-timeout 10 --max-time 30 "$download_url" > "$staged_installer" 2>"$staging_dir/curl-error"; then
            download_success=true
            break
        fi
        download_error=$(cat "$staging_dir/curl-error" 2>/dev/null || true)
    done
    if [[ "$download_success" != true ]]; then
        print_error "Failed to download install script after $max_retries attempts"
        [[ -z "$download_error" ]] || print_error "Error: $download_error"
        return 1
    fi
    if ! curl -q -fsSL --connect-timeout 10 --max-time 30 "${download_url}.sshsig" > "$staged_signature" 2>"$staging_dir/curl-error"; then
        print_error "Failed to download container installer signature; refusing to copy or run the installer"
        return 1
    fi
    if ! verify_release_signature "$staged_installer" "$staged_signature" "downloaded container installer"; then
        return 1
    fi
    printf -v "$source_output" '%s' "$staged_installer"
}

resolve_target_release() {
    if [[ -n "${LATEST_RELEASE:-}" ]]; then
        return 0
    fi

    if [[ -n "${FORCE_VERSION}" ]]; then
        LATEST_RELEASE="${FORCE_VERSION}"
        print_info "Installing specific version: $LATEST_RELEASE"

        if command -v timeout >/dev/null 2>&1; then
            if ! timeout 15 curl -q -fsS --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$LATEST_RELEASE" > /dev/null 2>&1; then
                print_warn "Could not verify version $LATEST_RELEASE, proceeding anyway..."
            fi
        else
            if ! curl -q -fsS --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$LATEST_RELEASE" > /dev/null 2>&1; then
                print_warn "Could not verify version $LATEST_RELEASE, proceeding anyway..."
            fi
        fi
        return 0
    fi

    if [[ -z "${UPDATE_CHANNEL:-}" ]]; then
        UPDATE_CHANNEL="stable"

        if [[ -n "${FORCE_CHANNEL}" ]]; then
            UPDATE_CHANNEL="${FORCE_CHANNEL}"
            print_info "Using $UPDATE_CHANNEL channel from command line"
        else
            CONFIGURED_CHANNEL=$(read_configured_update_channel 2>/dev/null || true)
            if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then
                UPDATE_CHANNEL="rc"
                print_info "Prerelease channel detected in configuration"
            fi
        fi
    fi

    # Installer-script URLs and server archives must select the same release.
    LATEST_RELEASE=$(resolve_latest_release_tag_for_channel "$UPDATE_CHANNEL" 2>/dev/null || true)

    if [[ -z "$LATEST_RELEASE" ]]; then
        print_error "Could not determine a published Pulse server release for the $UPDATE_CHANNEL channel"
        print_info "Check GitHub connectivity and that jq is installed, or pin the release explicitly:"
        print_info "  bash install.sh --version vX.Y.Z"
        exit 1
    fi

    # Never move an existing install backwards from auto-resolution. Explicit
    # downgrades stay available via --version.
    if [[ -n "${CURRENT_VERSION:-}" && "$CURRENT_VERSION" != "unknown" ]]; then
        local resolve_cmp=0
        compare_versions "$LATEST_RELEASE" "$CURRENT_VERSION" || resolve_cmp=$?
        if [[ "$resolve_cmp" -eq 2 ]]; then
            print_error "Resolved release $LATEST_RELEASE is older than the installed $CURRENT_VERSION; refusing to downgrade automatically"
            print_info "To downgrade on purpose, run: bash install.sh --version $LATEST_RELEASE"
            exit 1
        fi
    fi

    print_info "Latest version: $LATEST_RELEASE"
}

download_release_archive() {
    local release="$1"
    local pulse_arch="$2"
    local archive_path="$3"

    local archive_name="pulse-${release}-linux-${pulse_arch}.tar.gz"
    local download_url="https://github.com/$GITHUB_REPO/releases/download/$release/${archive_name}"
    local signature_url="${download_url}.sshsig"
    local signature_file="${archive_path}.sshsig"

    DOWNLOAD_URL="$download_url"
    print_info "Downloading from: $DOWNLOAD_URL"

    if ! require_release_signature_verifier; then
        print_error "Cannot verify signed Pulse release downloads"
        return 1
    fi

    if ! wget -q --timeout=60 --tries=2 -O "$archive_path" "$download_url"; then
        print_error "Failed to download Pulse release"
        print_info "This can happen due to network issues or GitHub rate limiting"
        print_info "You can try downloading manually from: $download_url"
        return 1
    fi

    if ! wget -q --timeout=60 --tries=2 -O "$signature_file" "$signature_url"; then
        rm -f "$signature_file" "$archive_path"
        print_error "Failed to download signature for Pulse release"
        print_info "Refusing to install without signature verification"
        return 1
    fi

    # The signature stays alongside the archive (not in a mktemp temp file)
    # so that install_pulse_archive can re-verify before extraction. This
    # also lets the Proxmox LXC bootstrap path propagate the signature
    # into the container alongside the archive.
    if ! verify_release_signature "$archive_path" "$signature_file" "downloaded Pulse release"; then
        rm -f "$signature_file" "$archive_path"
        return 1
    fi
    print_info "Pulse release signature verified"

    return 0
}

# Create (or refresh) the convenience symlink to the installed binary.
# Never fatal and idempotent: the link path can be unwritable — the generated
# pulse-update.service runs the unattended updater with ProtectSystem=strict
# (only the install dir, config dir and /tmp are writable), and transient
# read-only filesystems hit the same error (#1630). The symlink is a
# nice-to-have; an existing correct link is kept and a failure only warns.
install_binary_symlink() {
    local target="$1"
    local link_path="$2"

    if [[ "$(readlink "$link_path" 2>/dev/null)" == "$target" ]]; then
        print_success "Symlink already in place at $link_path"
        return 0
    fi

    mkdir -p "$(dirname "$link_path")" 2>/dev/null || true
    if ln -sf "$target" "$link_path" 2>/dev/null; then
        print_success "Symlink created at $link_path"
    else
        print_warn "Could not create symlink at $link_path (read-only filesystem?); pulse remains available at $target"
    fi
    return 0
}

install_pulse_archive() {
    local archive_path="$1"
    local expected_release="${2:-}"
    local signature_path="${archive_path}.sshsig"
    local temp_extract=""
    local binary_stage=""
    local version_output=""
    local service_name=""
    local installed_version=""
    local pulse_binary_path=""
    local target_arch=""

    if [[ ! -f "$archive_path" ]]; then
        print_error "Archive not found: $archive_path"
        return 1
    fi

    # Signature verification is mandatory at install time, regardless of
    # how the archive got here. download_release_archive already verifies
    # before this is called, but the --archive PATH path (and the LXC
    # bootstrap that propagates --archive into the container) reach this
    # function without going through download. Re-verifying here closes
    # the local-tarball-swap window between download/propagation and
    # extraction.
    if [[ ! -f "$signature_path" ]]; then
        print_error "Required signature sidecar not found: $signature_path"
        print_info "Pulse archives must ship with a matching .sshsig file from the canonical release pipeline."
        return 1
    fi
    if ! verify_release_signature "$archive_path" "$signature_path" "Pulse release archive at $archive_path"; then
        return 1
    fi

    if [[ -z "$expected_release" ]]; then
        expected_release=$(infer_release_from_archive_name "$archive_path" 2>/dev/null || true)
    fi

    local staging_parent=""
    if ! staging_parent=$(prepare_server_staging_parent) ||
       ! temp_extract=$(mktemp -d "$staging_parent/.pulse-extract-XXXXXX"); then
        print_error "Could not prepare archive extraction; Pulse has not been stopped"
        return 1
    fi
    # --no-same-owner: do not honor uid/gid stored in the archive (extract as root, owned by root)
    # --no-overwrite-dir: refuse to replace existing directory metadata with archive entries
    if ! tar --no-same-owner --no-overwrite-dir -xzf "$archive_path" -C "$temp_extract"; then
        print_error "Failed to extract Pulse release archive"
        rm -rf "$temp_extract"
        return 1
    fi

    pulse_binary_path=$(find_pulse_binary_in_dir "$temp_extract" 2>/dev/null || true)
    if [[ -z "$pulse_binary_path" ]]; then
        print_error "Pulse binary not found in archive"
        rm -rf "$temp_extract"
        return 1
    fi

    target_arch=$(detect_pulse_architecture 2>/dev/null || true)
    if [[ -z "$target_arch" ]]; then
        print_error "Unsupported architecture for archive install: $(uname -m)"
        rm -rf "$temp_extract"
        return 1
    fi

    if ! validate_pulse_binary_architecture "$pulse_binary_path" "$target_arch" "$(basename "$archive_path")"; then
        rm -rf "$temp_extract"
        return 1
    fi

    # Prepare the final executable on the destination filesystem while the old
    # service keeps running. Copy/permissions/version failures must not create
    # an outage, and rename must never expose a partially copied live binary.
    if ! mkdir -p "$INSTALL_DIR/bin" || ! binary_stage=$(mktemp -d "$INSTALL_DIR/bin/.pulse-stage-XXXXXX"); then
        print_error "Could not stage the Pulse binary; Pulse has not been stopped"
        rm -rf "$temp_extract"
        return 1
    fi
    if ! cp "$pulse_binary_path" "$binary_stage/pulse" || ! chmod 755 "$binary_stage/pulse" || ! chown pulse:pulse "$binary_stage/pulse"; then
        print_error "Could not prepare the Pulse binary; Pulse has not been stopped"
        rm -rf "$binary_stage" "$temp_extract"
        return 1
    fi
    if ! version_output=$(timeout 5 "$binary_stage/pulse" --version 2>/dev/null); then
        print_error "Staged Pulse binary could not report its version; Pulse has not been stopped"
        rm -rf "$binary_stage" "$temp_extract"
        return 1
    fi
    installed_version=$(printf '%s\n' "$version_output" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?' | head -1 || true)
    if [[ -z "$installed_version" || ( -n "$expected_release" && "$installed_version" != "$expected_release" ) ]]; then
        print_error "Staged Pulse version ${installed_version:-unknown} does not match ${expected_release:-a release version}; Pulse has not been stopped"
        rm -rf "$binary_stage" "$temp_extract"
        return 1
    fi

    if ! service_name=$(detect_service_name); then
        rm -rf "$binary_stage" "$temp_extract"
        return 1
    fi
    PULSE_WAS_ACTIVE="false"
    if ! stop_pulse_for_replacement "$service_name"; then
        rm -rf "$binary_stage" "$temp_extract"
        return 1
    fi

    # Both paths are on the same filesystem. If rename fails the old binary is
    # untouched; do not move it aside, delete it or re-extract an admitted file.
    if ! mv -fT "$binary_stage/pulse" "$INSTALL_DIR/bin/pulse"; then
        print_error "Failed to replace Pulse; the previous binary is unchanged"
        rm -rf "$binary_stage" "$temp_extract"
        recover_pulse_after_failed_replacement "$service_name"
        return 1
    fi
    rm -rf "$binary_stage"

    install_additional_agent_binaries "$expected_release" "$temp_extract"
    local script_status=0
    deploy_agent_scripts "$temp_extract" || script_status=$?
    if [[ "$script_status" -ne 0 ]]; then
        print_error "Pulse binary was installed, but agent script deployment is incomplete; installation has not completed"
        rm -rf "$temp_extract"
        if [[ "$script_status" -eq 1 ]]; then
            recover_pulse_after_failed_replacement "$service_name"
        else
            print_error "Agent script destination needs manual reconciliation; Pulse has not been restarted"
        fi
        return 1
    fi
    chown_install_dir_for_pulse

    print_success "Pulse binary installed to $INSTALL_DIR/bin/pulse"
    install_binary_symlink "$INSTALL_DIR/bin/pulse" "$BINARY_LINK_PATH"

    if [[ -f "$temp_extract/VERSION" ]]; then
        cp "$temp_extract/VERSION" "$INSTALL_DIR/VERSION"
        chown pulse:pulse "$INSTALL_DIR/VERSION"
    fi
    print_success "Version verified: $installed_version"

    restore_selinux_contexts
    rm -rf "$temp_extract"
    return 0
}

download_pulse() {
    # Check if we should build from source - do this FIRST to avoid confusing version messages
    if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
        if ! build_from_source "$SOURCE_BRANCH"; then
            print_error "Source build failed"
            exit 1
        fi
        return 0
    fi

    if [[ "$BUILD_FROM_SOURCE" == "true" && "$SKIP_DOWNLOAD" != "true" ]]; then
        print_error "Source build requested but download path was reached (internal error)"
        exit 1
    fi

    # Only do download if not building from source
    if [[ "$SKIP_DOWNLOAD" != "true" ]]; then
        local archive_path=""
        local expected_release=""
        local raw_arch=""
        local pulse_arch=""
        local archive_from_temp=false
        local inferred_release=""
        local staging_parent=""

        # The archive download and its extraction live on the install
        # filesystem, never /tmp. Debian 13 and many LXC templates mount /tmp
        # as tmpfs, which the kernel charges to the container's memory limit:
        # a ~280 MB archive plus its ~750 MB extracted tree OOM-killed a 1 GiB
        # LXC's init mid-update and Proxmox stopped the container (#2785).
        if ! staging_parent=$(prepare_server_staging_parent) ||
           ! ensure_update_disk_headroom "$staging_parent" "$INSTALL_DIR"; then
            # The configuration snapshot taken earlier is not needed: the update
            # never reached staging. Remove it so a retry loop on a low-space
            # filesystem does not accumulate backups and worsen the shortage
            # (#2127).
            discard_last_config_backup
            exit 1
        fi

        raw_arch=$(uname -m)
        pulse_arch=$(detect_pulse_architecture "$raw_arch") || {
            print_error "Unsupported architecture: $raw_arch"
            exit 1
        }
        print_info "Detected architecture: $raw_arch ($pulse_arch)"

        if [[ -n "$ARCHIVE_OVERRIDE" ]]; then
            archive_path=$(resolve_archive_override "$ARCHIVE_OVERRIDE") || exit 1
            inferred_release=$(infer_release_from_archive_name "$archive_path" 2>/dev/null || true)

            if [[ -n "$FORCE_VERSION" && -n "$inferred_release" && "$FORCE_VERSION" != "$inferred_release" ]]; then
                print_error "Archive version $inferred_release does not match requested version $FORCE_VERSION"
                exit 1
            fi

            if [[ -n "$FORCE_VERSION" ]]; then
                expected_release="$FORCE_VERSION"
            else
                expected_release="$inferred_release"
            fi

            if [[ -n "$expected_release" ]]; then
                LATEST_RELEASE="$expected_release"
                print_info "Installing Pulse from local archive: $archive_path"
                print_info "Archive version: $LATEST_RELEASE"
            else
                print_info "Installing Pulse from local archive: $archive_path"
            fi
        else
            print_info "Downloading Pulse..."
            resolve_target_release

            archive_path=$(create_temp_archive_path "$staging_parent/.pulse-${LATEST_RELEASE}-linux-${pulse_arch}") || {
                print_error "Failed to create temporary archive path"
                exit 1
            }
            archive_from_temp=true
            if ! download_release_archive "$LATEST_RELEASE" "$pulse_arch" "$archive_path"; then
                cleanup_temp_archive_path "$archive_path"
                exit 1
            fi
            expected_release="$LATEST_RELEASE"
        fi

        if ! run_upgrade_readiness_preflight "$CURRENT_VERSION" "$expected_release"; then
            if [[ "$archive_from_temp" == true ]]; then
                cleanup_temp_archive_path "$archive_path"
            fi
            exit 1
        fi

        # Archive admission and destination staging own the confirmed stop:
        # local signature/content/architecture/version failures leave Pulse up.
        if ! install_pulse_archive "$archive_path" "$expected_release"; then
            if [[ "$archive_from_temp" == "true" ]]; then
                cleanup_temp_archive_path "$archive_path"
            fi
            exit 1
        fi

        rm -f "$BUILD_FROM_SOURCE_MARKER"
        if [[ "$archive_from_temp" == "true" ]]; then
            cleanup_temp_archive_path "$archive_path"
        fi
    fi  # End of SKIP_DOWNLOAD check
}

prefetch_pulse_archive_for_container() {
    local output_var="$1"
    local raw_arch=""
    local pulse_arch=""
    local archive_path=""

    resolve_target_release

    raw_arch=$(uname -m)
    pulse_arch=$(detect_pulse_architecture "$raw_arch") || {
        print_error "Unsupported architecture for container install: $raw_arch"
        return 1
    }

    print_info "Prefetching Pulse release archive on Proxmox host..."
    print_info "Detected architecture: $raw_arch ($pulse_arch)"

    archive_path=$(create_temp_archive_path "/tmp/pulse-${LATEST_RELEASE}-linux-${pulse_arch}-lxc") || {
        print_error "Failed to create temporary archive path"
        return 1
    }
    if ! download_release_archive "$LATEST_RELEASE" "$pulse_arch" "$archive_path"; then
        cleanup_temp_archive_path "$archive_path"
        return 1
    fi

    printf -v "$output_var" '%s' "$archive_path"
    return 0
}

copy_unified_agent_binaries_from_dir() {
    local source_dir="$1"

    if [[ -z "$source_dir" ]] || [[ ! -d "$source_dir/bin" ]]; then
        return 1
    fi

    local copied=0
    shopt -s nullglob
    for agent_file in "$source_dir"/bin/pulse-agent-*; do
        [[ -e "$agent_file" ]] || continue

        local base
        base=$(basename "$agent_file")
        # Skip the wrapper script (pulse-agent without arch suffix)
        if [[ "$base" == "pulse-agent" ]]; then
            continue
        fi

        cp -a "$agent_file" "$INSTALL_DIR/bin/$base"
        if [[ ! -L "$INSTALL_DIR/bin/$base" ]]; then
            chmod +x "$INSTALL_DIR/bin/$base"
        fi
        chown -h pulse:pulse "$INSTALL_DIR/bin/$base" || true
        copied=1
    done
    shopt -u nullglob

    if [[ $copied -eq 0 ]]; then
        return 1
    fi
    return 0
}

install_additional_agent_binaries() {
    local version="$1"
    local source_dir="${2:-}"
    local local_unified_installed=0

    if [[ -z "$version" ]]; then
        return
    fi

    local unified_targets=("linux-amd64" "linux-arm64" "linux-armv7" "linux-armv6" "linux-386" "darwin-amd64" "darwin-arm64" "windows-amd64" "windows-arm64" "windows-386")

    # Prefer locally available agents from the extracted archive to avoid network reliance
    if copy_unified_agent_binaries_from_dir "$source_dir"; then
        local_unified_installed=1
    fi

    local unified_missing_targets=()
    for target in "${unified_targets[@]}"; do
        if [[ "$target" == windows-* ]]; then
            if [[ ! -e "$INSTALL_DIR/bin/pulse-agent-$target" && ! -e "$INSTALL_DIR/bin/pulse-agent-$target.exe" ]]; then
                unified_missing_targets+=("$target")
            fi
        else
            if [[ ! -e "$INSTALL_DIR/bin/pulse-agent-$target" ]]; then
                unified_missing_targets+=("$target")
            fi
        fi
    done

    if [[ ${#unified_missing_targets[@]} -eq 0 ]]; then
        if [[ $local_unified_installed -eq 1 ]]; then
            print_success "Unified agent binaries installed"
        fi
        return
    fi

    if [[ $local_unified_installed -eq 1 ]]; then
        print_success "Unified agent binaries installed"
    fi
    print_info "Skipping eager cross-architecture agent bundle download during install"
    print_info "Missing unified agent binaries will be fetched on demand when requested"
    return 0
}

deploy_agent_scripts() {
    local extract_dir="$1"

    if [[ -z "$extract_dir" ]]; then
        print_warn "No extraction directory provided for script deployment"
        return
    fi

    local scripts=(
        "install-container-agent.sh"
        "install-docker.sh"
        "install.sh"
        "install.ps1"
    )
    local deployed=0 script sidecar stage="" live="$INSTALL_DIR/scripts"
    for script in "${scripts[@]}"; do
        [[ ! -f "$extract_dir/scripts/$script" ]] || deployed=$((deployed + 1))
    done
    if [[ $deployed -eq 0 ]]; then
        print_warn "No agent installation scripts found in archive"
        return 0
    fi

    # A script and its detached signatures are one served bundle. Prepare all
    # bytes/permissions in a private enclosure before touching the old bundle;
    # cp failure must never truncate the currently served installer. Preserve
    # unrelated local scripts, but never follow an overridden file's symlink.
    if [[ -L "$live" || ( -e "$live" && ! -d "$live" ) ]]; then
        print_error "Agent script destination is not a regular directory; existing path is unchanged"
        return 2
    fi
    if ! stage=$(mktemp -d -- "$INSTALL_DIR/.pulse-scripts-XXXXXX"); then
        print_error "Could not stage agent scripts; existing scripts are unchanged"
        return 1
    fi
    if ! mkdir -- "$stage/next" ||
       { [[ -d "$live" ]] && ! cp -a -- "$live/." "$stage/next/"; }; then
        rm -rf -- "$stage"
        print_error "Could not prepare agent scripts; existing scripts are unchanged"
        return 1
    fi
    for script in "${scripts[@]}"; do
        [[ -f "$extract_dir/scripts/$script" ]] || continue
        if ! cp --remove-destination -- "$extract_dir/scripts/$script" "$stage/next/$script" ||
           ! chmod 755 -- "$stage/next/$script" || ! chown pulse:pulse "$stage/next/$script"; then
            rm -rf -- "$stage"
            print_error "Could not prepare agent script $script; existing scripts are unchanged"
            return 1
        fi
        # Published archives carry both sidecars for the local /install.sh and
        # /install.ps1 routes. Source/legacy scripts may have neither: do not
        # attach a previous release's signature to newly copied unsigned bytes.
        for sidecar in "${script}.sig" "${script}.sshsig"; do
            if [[ -f "$extract_dir/scripts/$sidecar" ]]; then
                if ! cp --remove-destination -- "$extract_dir/scripts/$sidecar" "$stage/next/$sidecar" ||
                   ! chmod 644 -- "$stage/next/$sidecar" || ! chown pulse:pulse "$stage/next/$sidecar"; then
                    rm -rf -- "$stage"
                    print_error "Could not prepare agent script signature $sidecar; existing scripts are unchanged"
                    return 1
                fi
            elif ! rm -f -- "$stage/next/$sidecar"; then
                rm -rf -- "$stage"
                print_error "Could not remove a stale agent script signature; existing scripts are unchanged"
                return 1
            fi
        done
    done
    if ! chown pulse:pulse "$stage/next"; then
        rm -rf -- "$stage"
        print_error "Could not prepare agent script directory ownership; existing scripts are unchanged"
        return 1
    fi

    # Consumers have already confirmed the server stopped. These are same-
    # filesystem renames, not a recursive copy onto live files. Never overwrite
    # an unexpected destination, including mv -n's successful no-op result.
    if [[ -d "$live" ]]; then
        if ! mv -T -n -- "$live" "$stage/previous" || [[ -e "$live" || -L "$live" ]]; then
            if [[ -e "$stage/previous" || -L "$stage/previous" ]]; then
                print_error "Agent script reservation is incomplete; previous scripts retained at $stage/previous. Reconcile the destination before recovery."
                return 2
            fi
            rm -rf -- "$stage"
            print_error "Could not reserve the previous agent scripts; publication is incomplete"
            return 1
        fi
    fi
    if ! mv -T -n -- "$stage/next" "$live" || [[ -e "$stage/next" || -L "$stage/next" ]]; then
        if [[ ! -e "$stage/previous" && ! -L "$stage/previous" && ( -e "$live" || -L "$live" ) ]]; then
            rm -rf -- "$stage"
            print_error "Unexpected agent script destination; publication is incomplete. Reconcile the destination before recovery."
            return 2
        fi
        if [[ -d "$stage/previous" ]] &&
           { ! mv -T -n -- "$stage/previous" "$live" || [[ -e "$stage/previous" || -L "$stage/previous" ]]; }; then
            print_error "Agent script publication and restore are incomplete; previous scripts retained at $stage/previous. Reconcile the destination before recovery."
            return 2
        fi
        rm -rf -- "$stage"
        print_error "Could not publish agent scripts; previous scripts were not replaced"
        return 1
    fi
    if ! rm -rf -- "$stage"; then
        print_error "Agent scripts published, but previous-bundle cleanup is incomplete at $stage"
        return 1
    fi
    print_success "Deployed $deployed agent installation script(s)"
    return 0
}

build_from_source() {
    local branch="${1:-main}"
    local original_dir
    original_dir=$(pwd)
    local temp_build=""
    # Keep this aligned with go.mod's toolchain directive for consistent builds and security posture.
    local GO_MIN_VERSION="1.25.9"
    local GO_INSTALLED=false
    local arch=""
    local go_arch=""
    local service_name=""
    local binary_stage=""
    local version_output=""
    local source_revision=""

    print_info "Building Pulse from source (branch: $branch)..."

    print_info "Installing build dependencies..."
    if ! (apt-get update >/dev/null 2>&1 && apt-get install -y git make nodejs npm wget >/dev/null 2>&1); then
        print_error "Failed to install build dependencies"
        return 1
    fi

    if command -v go >/dev/null 2>&1; then
        local GO_VERSION
        GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
        if [[ "$(printf '%s\n' "$GO_MIN_VERSION" "$GO_VERSION" | sort -V | head -n1)" == "$GO_MIN_VERSION" ]]; then
            GO_INSTALLED=true
            print_info "Go $GO_VERSION is installed (meets minimum $GO_MIN_VERSION)"
        else
            print_info "Go $GO_VERSION is too old. Installing Go $GO_MIN_VERSION..."
        fi
    fi

    if [[ "$GO_INSTALLED" != "true" ]]; then
        arch=$(uname -m)
        case "$arch" in
            x86_64)
                go_arch="amd64"
                ;;
            aarch64)
                go_arch="arm64"
                ;;
            armv7l)
                go_arch="armv6l"
                ;;
            *)
                print_error "Unsupported architecture for Go: $arch"
                return 1
                ;;
        esac

        if ! cd /tmp; then
            print_error "Failed to prepare Go installation directory"
            cd "$original_dir" >/dev/null 2>&1 || true
            return 1
        fi

        if ! wget -q "https://go.dev/dl/go${GO_MIN_VERSION}.linux-${go_arch}.tar.gz"; then
            print_error "Failed to download Go toolchain"
            cd "$original_dir" >/dev/null 2>&1 || true
            return 1
        fi

        rm -rf /usr/local/go
        if ! tar -C /usr/local -xzf "go${GO_MIN_VERSION}.linux-${go_arch}.tar.gz"; then
            print_error "Failed to extract Go toolchain"
            rm -f "go${GO_MIN_VERSION}.linux-${go_arch}.tar.gz"
            cd "$original_dir" >/dev/null 2>&1 || true
            return 1
        fi
        rm -f "go${GO_MIN_VERSION}.linux-${go_arch}.tar.gz"
        cd "$original_dir" >/dev/null 2>&1 || true
    fi

    export PATH=/usr/local/go/bin:$PATH

    temp_build=$(mktemp -d /tmp/pulse-build-XXXXXX)
    if [[ ! -d "$temp_build" ]]; then
        print_error "Failed to create temporary build directory"
        return 1
    fi

    if ! git clone --depth 1 --branch "$branch" "https://github.com/$GITHUB_REPO.git" "$temp_build/Pulse" >/dev/null 2>&1; then
        print_error "Failed to clone repository (branch: $branch)"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$temp_build"
        return 1
    fi

    if ! cd "$temp_build/Pulse"; then
        print_error "Failed to enter source checkout"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$temp_build"
        return 1
    fi

    if ! cd frontend-modern; then
        print_error "Frontend directory missing in repository"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$temp_build"
        return 1
    fi

    if ! npm ci >/dev/null 2>&1; then
        print_warn "npm ci failed, falling back to npm install..."
        if ! npm install >/dev/null 2>&1; then
            print_error "Failed to install frontend dependencies"
            cd "$original_dir" >/dev/null 2>&1 || true
            rm -rf "$temp_build"
            return 1
        fi
    fi

    if ! npm run build >/dev/null 2>&1; then
        print_error "Failed to build frontend"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$temp_build"
        return 1
    fi

    cd ..

    if ! make build >/dev/null 2>&1; then
        print_error "Failed to build backend"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$temp_build"
        return 1
    fi

    # A successful build command alone does not admit its executable. Prepare
    # it on the destination filesystem before stopping the running service,
    # just as for signed archives. Source versions may be development builds.
    if ! mkdir -p "$INSTALL_DIR/bin" || ! binary_stage=$(mktemp -d "$INSTALL_DIR/bin/.pulse-stage-XXXXXX"); then
        print_error "Could not stage the built Pulse binary; Pulse has not been stopped"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$temp_build"
        return 1
    fi
    if ! cp pulse "$binary_stage/pulse" || ! chmod 755 "$binary_stage/pulse" || ! chown pulse:pulse "$binary_stage/pulse"; then
        print_error "Could not prepare the built Pulse binary; Pulse has not been stopped"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$binary_stage" "$temp_build"
        return 1
    fi
    if ! version_output=$(timeout 5 "$binary_stage/pulse" --version 2>/dev/null) || [[ -z "${version_output//[[:space:]]/}" ]]; then
        print_error "Built Pulse binary could not report its version; Pulse has not been stopped"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$binary_stage" "$temp_build"
        return 1
    fi
    if ! source_revision=$(git rev-parse --short HEAD) || [[ -z "$source_revision" ]]; then
        print_error "Could not identify the source build; Pulse has not been stopped"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$binary_stage" "$temp_build"
        return 1
    fi

    if ! service_name=$(detect_service_name); then
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$binary_stage" "$temp_build"
        return 1
    fi
    PULSE_WAS_ACTIVE="false"
    if ! stop_pulse_for_replacement "$service_name"; then
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$binary_stage" "$temp_build"
        return 1
    fi

    if ! mv -fT "$binary_stage/pulse" "$INSTALL_DIR/bin/pulse"; then
        print_error "Failed to replace built Pulse; the previous binary is unchanged"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$binary_stage" "$temp_build"
        recover_pulse_after_failed_replacement "$service_name"
        return 1
    fi
    rm -rf "$binary_stage"

    local script_status=0
    deploy_agent_scripts "$temp_build/Pulse" || script_status=$?
    if [[ "$script_status" -ne 0 ]]; then
        print_error "Built Pulse binary was installed, but agent script deployment is incomplete; installation has not completed"
        cd "$original_dir" >/dev/null 2>&1 || true
        rm -rf "$temp_build"
        if [[ "$script_status" -eq 1 ]]; then
            recover_pulse_after_failed_replacement "$service_name"
        else
            print_error "Agent script destination needs manual reconciliation; Pulse has not been restarted"
        fi
        return 1
    fi

    install_binary_symlink "$INSTALL_DIR/bin/pulse" "$BINARY_LINK_PATH"

    echo "$branch-$source_revision" > "$INSTALL_DIR/VERSION"
    echo "$branch" > "$BUILD_FROM_SOURCE_MARKER"

    chown_install_dir_for_pulse 2>/dev/null || true
    chown pulse:pulse "$BUILD_FROM_SOURCE_MARKER" 2>/dev/null || true

    cd "$original_dir" >/dev/null 2>&1 || true
    rm -rf "$temp_build"

    SKIP_DOWNLOAD=true
    print_success "Successfully built and installed Pulse from source (branch: $branch)"
    return 0
}

setup_directories() {
    print_info "Setting up directories..."

    # Create directories (only if they don't exist)
    if ! mkdir -p -- "$CONFIG_DIR" || ! mkdir -p -- "$INSTALL_DIR"; then
        print_error "Could not create Pulse directories; directory setup is incomplete"
        return 1
    fi

    # Set permissions (preserve existing files)
    # Use chown without -R on CONFIG_DIR to avoid changing existing file permissions
    # main can itself be called in an OR-list, where Bash disables errexit
    # throughout its callees. Every required operation must propagate failure
    # explicitly before service setup/start or a completion message is allowed.
    if ! chown pulse:pulse "$CONFIG_DIR" || ! chown_install_dir_for_pulse || ! chmod 700 "$CONFIG_DIR"; then
        print_error "Could not set Pulse directory ownership or permissions; directory setup is incomplete"
        return 1
    fi

    # Ensure critical config files retain proper permissions if they exist
    for config_file in "$CONFIG_DIR"/alerts.json "$CONFIG_DIR"/system.json "$CONFIG_DIR"/*.enc "$CONFIG_DIR"/*.json; do
        if [[ -f "$config_file" ]]; then
            if ! chown pulse:pulse "$config_file"; then
                print_error "Could not set ownership of $config_file; directory setup is incomplete"
                return 1
            fi
        fi
    done

    for config_dir in "$CONFIG_DIR"/alerts "$CONFIG_DIR"/notifications "$CONFIG_DIR"/audit; do
        if [[ -d "$config_dir" ]]; then
            if ! chown -R pulse:pulse "$config_dir"; then
                print_error "Could not set ownership of $config_dir; directory setup is incomplete"
                return 1
            fi
        fi
    done

    # Create .env file with mock mode explicitly disabled (unless it already exists)
    if [[ ! -f "$CONFIG_DIR/.env" ]]; then
        if ! cat > "$CONFIG_DIR/.env" << 'EOF'
# Pulse Environment Configuration
# This file is loaded by systemd when starting Pulse

# Mock mode - set to "true" to enable mock/demo data for testing
# WARNING: Only enable this on demo/test servers, never in production!
PULSE_MOCK_MODE=false

# Mock configuration (only used when PULSE_MOCK_MODE=true)
#PULSE_MOCK_NODES=3
#PULSE_MOCK_VMS_PER_NODE=3
#PULSE_MOCK_LXCS_PER_NODE=3
#PULSE_MOCK_RANDOM_METRICS=true
#PULSE_MOCK_STOPPED_PERCENT=6
EOF
        then
            print_error "Could not write Pulse environment configuration; directory setup is incomplete"
            return 1
        fi
    fi
    if [[ -f "$CONFIG_DIR/.env" ]]; then
        if ! chown pulse:pulse "$CONFIG_DIR/.env" || ! chmod 600 "$CONFIG_DIR/.env"; then
            print_error "Could not protect Pulse environment configuration; directory setup is incomplete"
            return 1
        fi
    fi

    if [[ -f "$CONFIG_DIR/.encryption.key" ]]; then
        if ! chown pulse:pulse "$CONFIG_DIR/.encryption.key" || ! chmod 600 "$CONFIG_DIR/.encryption.key"; then
            print_error "Could not protect the Pulse encryption key; directory setup is incomplete"
            return 1
        fi
    fi
    return 0
}

setup_update_command() {
    # Create update command at /bin/update for ProxmoxVE LXC detection
    # This allows the backend to detect ProxmoxVE installations
    local service_name="${SERVICE_NAME:-${PULSE_SERVICE_NAME:-pulse}}"
    local install_dir="${INSTALL_DIR:-${PULSE_INSTALL_DIR:-/opt/pulse}}"
    local config_dir="${CONFIG_DIR:-${PULSE_CONFIG_DIR:-/etc/pulse}}"
    local binary_link_path="${BINARY_LINK_PATH:-${PULSE_BINARY_LINK_PATH:-/usr/local/bin/pulse}}"
    local update_helper_path="${UPDATE_HELPER_PATH:-${PULSE_UPDATE_HELPER_PATH:-/bin/update}}"
    local auto_update_dest="${AUTO_UPDATE_DEST:-${PULSE_AUTO_UPDATE_DEST:-/usr/local/bin/pulse-auto-update.sh}}"
    local update_service_path="${UPDATE_SERVICE_PATH:-${PULSE_UPDATE_SERVICE_PATH:-/etc/systemd/system/${service_name}-update.service}}"
    local update_timer_path="${UPDATE_TIMER_PATH:-${PULSE_UPDATE_TIMER_PATH:-/etc/systemd/system/${service_name}-update.timer}}"
    local profile_path="${PULSE_PROFILE_PATH:-/etc/profile}"
    local bashrc_path="${PULSE_BASHRC_PATH:-/etc/bash.bashrc}"

    # Writing the helper must never abort the installer: with errexit active a
    # failed redirect here used to kill the run after the new binary was
    # already installed and the service stopped (#1630). The path can
    # legitimately be unwritable — the unattended updater runs under
    # ProtectSystem=strict, and transient read-only filesystems hit this too.
    # The old helper may still be executing this installer. Never truncate its
    # inode: Bash can otherwise resume at the old offset in the new script and
    # report a syntax error after an otherwise successful update.
    local staged_helper
    if ! mkdir -p "$(dirname "$update_helper_path")" 2>/dev/null || \
       ! staged_helper=$(mktemp "${update_helper_path}.staged.XXXXXX" 2>/dev/null); then
        print_warn "Could not write the 'update' helper to $update_helper_path (read-only filesystem?); skipping optional update command setup"
        return 0
    fi
    if ! cat 2>/dev/null > "$staged_helper" <<EOF
#!/usr/bin/env bash
# Pulse update command
# This script re-runs the Pulse installer using the configured manual channel

set -euo pipefail

INSTALL_ROOT=$(printf '%q' "$install_dir")
MARKER_FILE="\${INSTALL_ROOT}/BUILD_FROM_SOURCE"
CONFIG_DIR=$(printf '%q' "$config_dir")
INSTALLER_URL="https://github.com/${GITHUB_REPO}/releases/latest/download/install.sh"
PULSE_SERVICE_NAME=$(printf '%q' "$service_name")
PULSE_INSTALL_DIR=$(printf '%q' "$install_dir")
PULSE_CONFIG_DIR=$(printf '%q' "$config_dir")
PULSE_BINARY_LINK_PATH=$(printf '%q' "$binary_link_path")
PULSE_UPDATE_HELPER_PATH=$(printf '%q' "$update_helper_path")
PULSE_AUTO_UPDATE_DEST=$(printf '%q' "$auto_update_dest")
PULSE_UPDATE_SERVICE_PATH=$(printf '%q' "$update_service_path")
PULSE_UPDATE_TIMER_PATH=$(printf '%q' "$update_timer_path")
INSTALLER_SIG_URL="\${INSTALLER_URL}.sshsig"
INSTALL_SIGNATURE_IDENTITY=$(printf '%q' "$INSTALL_SIGNATURE_IDENTITY")
INSTALL_SIGNATURE_NAMESPACE=$(printf '%q' "$INSTALL_SIGNATURE_NAMESPACE")
PINNED_RELEASE_SSH_PUBLIC_KEY=$(printf '%q' "$PINNED_RELEASE_SSH_PUBLIC_KEY")
$(declare -f release_signature_key_available)
$(declare -f require_release_signature_verifier)
$(declare -f verify_release_signature)

extra_args=()
installer_env=(
    "PULSE_SERVICE_NAME=\$PULSE_SERVICE_NAME"
    "PULSE_INSTALL_DIR=\$PULSE_INSTALL_DIR"
    "PULSE_CONFIG_DIR=\$PULSE_CONFIG_DIR"
    "PULSE_BINARY_LINK_PATH=\$PULSE_BINARY_LINK_PATH"
    "PULSE_UPDATE_HELPER_PATH=\$PULSE_UPDATE_HELPER_PATH"
    "PULSE_AUTO_UPDATE_DEST=\$PULSE_AUTO_UPDATE_DEST"
    "PULSE_UPDATE_SERVICE_PATH=\$PULSE_UPDATE_SERVICE_PATH"
    "PULSE_UPDATE_TIMER_PATH=\$PULSE_UPDATE_TIMER_PATH"
)
helper_args=()
if [[ \$# -gt 0 ]]; then
    helper_args=("\$@")
fi
auto_selector_allowed=true
if [[ \${#helper_args[@]} -gt 0 ]]; then
    for helper_arg in "\${helper_args[@]}"; do
        case "\$helper_arg" in
            -h|--help|--uninstall|--version|--rc|--pre|--prerelease|--stable|--source|--from-source|--branch|--archive|--archive=*|--skip-upgrade-preflight)
                auto_selector_allowed=false
                break
                ;;
        esac
    done
fi
if [[ "\$auto_selector_allowed" == "true" ]]; then
    if [[ -f "\$MARKER_FILE" ]]; then
        branch=\$(tr -d '\r\n' <"\$MARKER_FILE" 2>/dev/null || true)
        if [[ -n "\$branch" ]]; then
            extra_args+=(--source "\$branch")
        fi
    elif [[ -e "\${CONFIG_DIR}/system.json" || -L "\${CONFIG_DIR}/system.json" ]]; then
        # Never infer a release channel from nested, quoted or partial JSON.
        # Missing/null channels retain the stable default; malformed or unknown
        # preferences stop before download. An explicit selector above wins.
        if [[ ! -f "\${CONFIG_DIR}/system.json" ]] || ! command -v jq >/dev/null 2>&1; then
            echo "Cannot read the saved update channel; a regular system.json and jq are required. Select --stable or --rc explicitly, or restore the configuration." >&2
            exit 1
        fi
        if ! configured_channel=\$(jq -er -s '
            if length != 1 or (.[0] | type) != "object" then
                error("expected one configuration object")
            else .[0] end
            | .updateChannel
            | if . == null then "stable" else . end
            | if type != "string" then error("invalid update channel") else . end
            | gsub("^\\\\s+|\\\\s+\$"; "") | ascii_downcase
            | if . == "" then "stable" else . end
            | if . == "stable" or . == "rc" then . else error("unknown update channel") end
        ' "\${CONFIG_DIR}/system.json" 2>/dev/null); then
            echo "Cannot parse the saved update channel; no installer downloaded. Select --stable or --rc explicitly, or restore the configuration." >&2
            exit 1
        fi
        if [[ "\$configured_channel" == "rc" ]]; then
            extra_args+=(--rc)
        else
            # Bind the parsed default too: the installer has its own saved
            # preference reader, which must not re-interpret the same file.
            extra_args+=(--stable)
        fi
    fi
fi
if [[ \${#helper_args[@]} -gt 0 ]]; then
    extra_args+=("\${helper_args[@]}")
fi

echo "Updating Pulse..."
tmp_installer=""
tmp_signature=""
trap 'rm -f "\$tmp_installer" "\$tmp_signature"' EXIT
tmp_installer=\$(mktemp /tmp/pulse-update-installer.XXXXXX)
tmp_signature=\$(mktemp /tmp/pulse-update-installer.sig.XXXXXX)
# Do not let ambient curl retries or a stalled bootstrap hold update forever.
# No installer/service change is allowed until both bounded downloads verify.
# Keep both the initial request and GitHub's asset redirects on authenticated
# HTTPS. A signature check is not permission to downgrade the transport.
if ! curl -q -fsSL --connect-timeout 10 --max-time 60 --proto '=https' --proto-redir '=https' "\$INSTALLER_URL" -o "\$tmp_installer"; then
    echo "Failed to download Pulse installer from \$INSTALLER_URL" >&2
    exit 1
fi
if ! curl -q -fsSL --connect-timeout 10 --max-time 60 --proto '=https' --proto-redir '=https' "\$INSTALLER_SIG_URL" -o "\$tmp_signature"; then
    echo "Failed to download Pulse installer signature from \$INSTALLER_SIG_URL" >&2
    exit 1
fi
if ! verify_release_signature "\$tmp_installer" "\$tmp_signature" "downloaded Pulse installer"; then
    exit 1
fi
# The installer owns its outcome and exit status. Help, cancellation and
# removal can also succeed; zero does not mean an update or restart occurred.
if [[ \${#extra_args[@]} -gt 0 ]]; then
    env "\${installer_env[@]}" bash "\$tmp_installer" "\${extra_args[@]}"
else
    env "\${installer_env[@]}" bash "\$tmp_installer"
fi
EOF
    then
        rm -f -- "$staged_helper" 2>/dev/null || true
        print_warn "Could not write the 'update' helper to $update_helper_path (read-only filesystem?); skipping optional update command setup"
        return 0
    fi

    # mktemp starts at 0600 even with a permissive umask. Publish a complete,
    # parseable executable on the same filesystem; -T must not write into a
    # directory accidentally occupying the helper path. Failure keeps the old
    # helper and remains non-fatal for the surrounding installer (#1630).
    if ! chmod 0755 "$staged_helper" 2>/dev/null || \
       ! bash -n "$staged_helper" 2>/dev/null || \
       ! mv -fT -- "$staged_helper" "$update_helper_path" 2>/dev/null; then
        rm -f -- "$staged_helper" 2>/dev/null || true
        print_warn "Could not replace the 'update' helper at $update_helper_path; keeping the previous helper"
        return 0
    fi

    # Ensure /usr/local/bin is in PATH for all users
    if ! grep -q '/usr/local/bin' "$profile_path" 2>/dev/null; then
        echo 'export PATH="/usr/local/bin:$PATH"' 2>/dev/null >> "$profile_path" || \
            print_warn "Could not update PATH in $profile_path"
    fi

    # Also add to bash profile if it exists
    if [[ -f "$bashrc_path" ]] && ! grep -q '/usr/local/bin' "$bashrc_path" 2>/dev/null; then
        echo 'export PATH="/usr/local/bin:$PATH"' 2>/dev/null >> "$bashrc_path" || \
            print_warn "Could not update PATH in $bashrc_path"
    fi
}

download_auto_update_script() {
    local asset_base_url=""
    asset_base_url=$(resolve_release_asset_base_url)
    local url="${asset_base_url}/pulse-auto-update.sh"
    local signature_url="${url}.sshsig"
    local dest="${AUTO_UPDATE_DEST:-${PULSE_AUTO_UPDATE_DEST:-/usr/local/bin/pulse-auto-update.sh}}"
    local attempts=0
    local max_attempts=3
    local connect_timeout=15
    local max_time=60

    if ! require_release_signature_verifier; then
        print_warn "Cannot verify signed auto-update helper downloads"
        return 1
    fi

    while (( attempts < max_attempts )); do
        ((attempts++))
        local curl_status=0
        local signature_file=""

        if command -v timeout >/dev/null 2>&1; then
            if timeout $((max_time + 10)) curl -q -fsSL --connect-timeout "$connect_timeout" --max-time "$max_time" -o "$dest" "$url"; then
                :
            else
                curl_status=$?
            fi
        else
            if curl -q -fsSL --connect-timeout "$connect_timeout" --max-time "$max_time" -o "$dest" "$url"; then
                :
            else
                curl_status=$?
            fi
        fi

        if [[ $curl_status -eq 0 ]]; then
            signature_file=$(mktemp /tmp/pulse-auto-update-signature.XXXXXX)
            if command -v timeout >/dev/null 2>&1; then
                timeout $((max_time + 10)) curl -q -fsSL --connect-timeout "$connect_timeout" --max-time "$max_time" -o "$signature_file" "$signature_url" || curl_status=$?
            else
                curl -q -fsSL --connect-timeout "$connect_timeout" --max-time "$max_time" -o "$signature_file" "$signature_url" || curl_status=$?
            fi

            if [[ $curl_status -ne 0 ]]; then
                print_warn "Failed to download signature for pulse-auto-update.sh"
                rm -f "$signature_file" "$dest"
            elif ! verify_release_signature "$dest" "$signature_file" "downloaded pulse-auto-update.sh"; then
                rm -f "$signature_file" "$dest"
                curl_status=1
            else
                rm -f "$signature_file"
                chmod +x "$dest"
                print_info "pulse-auto-update.sh signature verified"
                return 0
            fi
        fi

        print_warn "Auto-update download attempt $attempts/$max_attempts failed (curl exit code $curl_status)"
        if (( attempts < max_attempts )); then
            local wait_time=$((attempts * 3))
            print_info "Retrying in ${wait_time}s..."
            sleep "$wait_time"
        fi
    done

    return 1
}

configure_auto_update_script_repo() {
    local dest="${1:-${AUTO_UPDATE_DEST:-${PULSE_AUTO_UPDATE_DEST:-/usr/local/bin/pulse-auto-update.sh}}}"
    if [[ ! -f "$dest" ]]; then
        return 1
    fi

    # Both callers suppress errexit with `if !`: every preparation and
    # publication failure must be returned explicitly. Keep the rewrite beside
    # the supplied staging file so its final rename cannot become a cross-
    # filesystem copy, and do not chmod or replace the input before it is ready.
    local tmp
    if ! tmp=$(mktemp "${dest}.repo.XXXXXX"); then
        return 1
    fi
    if ! awk -v repo="$GITHUB_REPO" '
        BEGIN { inserted = 0 }
        /^#!/ {
            print
            if (!inserted) {
                print "GITHUB_REPO=\"" repo "\""
                inserted = 1
            }
            next
        }
        /^GITHUB_REPO=/ {
            if (!inserted) {
                print "GITHUB_REPO=\"" repo "\""
                inserted = 1
            }
            next
        }
        { print }
        END {
            if (!inserted) {
                print "GITHUB_REPO=\"" repo "\""
            }
        }
    ' "$dest" > "$tmp"; then
        rm -f "$tmp"
        return 1
    fi

    if ! bash -n "$tmp" || ! chmod 0755 "$tmp" || ! mv -Tf -- "$tmp" "$dest"; then
        rm -f -- "$tmp"
        return 1
    fi
    return 0
}

# Probes whether new entries can be created in a directory. This is exactly
# the capability the staged-rename swaps below need (rename creates and
# unlinks entries in the target directory), and unlike `test -w` it gives an
# unambiguous answer for root under a ProtectSystem= sandbox.
auto_update_dir_writable() {
    local dir="$1"
    local probe=""
    [[ -d "$dir" ]] || return 1
    probe=$(mktemp "${dir}/.pulse-write-probe.XXXXXX" 2>/dev/null) || return 1
    rm -f "$probe"
    return 0
}

# Guards the helper swap: a truncated or shebang-less helper would replace a
# working one with a file systemd cannot execute as the update unit's
# ExecStart. This matters because configure_auto_update_script_repo's awk
# happily emits a lone GITHUB_REPO= line from empty input, so a silently
# failed copy would otherwise produce a plausible-looking one-line "helper"
# whose only effect is to return success and never update anything.
auto_update_helper_is_sane() {
    local path="$1"
    [[ -s "$path" ]] || return 1
    local first_line=""
    IFS= read -r first_line < "$path" || true
    [[ "$first_line" == '#!'* ]]
}

# Migration for boxes installed before the update sandbox was widened.
#
# Unattended updates run this installer from pulse-update.service, and on a box
# whose unit predates the widened ReadWritePaths that sandbox covers only the
# install dir, config dir and /tmp. The corrected unit therefore cannot be
# written by the very run that would install it (EROFS), so without an escape
# no deployed box ever receives the single-schedule timer or the widened
# sandbox through auto-update — updater fixes reach only manually reinstalled
# boxes. The in-app Go update pipeline cannot carry the migration either:
# pulse.service runs as User=pulse under its own ProtectSystem=strict with
# ReadWritePaths limited to the install and config dirs, so it cannot write
# /etc/systemd/system or /usr/local/bin.
#
# systemd-run asks PID 1 to fork the repair, so the transient unit starts in
# the host mount namespace rather than inheriting this unit's sandbox. The
# system bus stays reachable from inside the sandbox because ProtectSystem=
# only remounts the hierarchy read-only and a read-only mount does not block
# connect() on an AF_UNIX socket (the same reason the unit's own
# ExecCondition=systemctl call works). The installer is copied into the
# install dir rather than run from its current path because the helper stages
# it under /tmp and PrivateTmp=yes hides that from PID 1; re-execing this
# already-signature-verified file also avoids a second unverified download.
migrate_auto_update_assets_outside_sandbox() {
    local blocked_dir="$1"

    # The escaped run writes with no sandbox; if it still cannot write, a
    # second escape would loop forever.
    if [[ "${PULSE_AUTO_UPDATE_ASSET_REPAIR:-}" == "1" ]]; then
        return 1
    fi
    if [[ "$(id -u)" -ne 0 ]]; then
        return 1
    fi
    # Absent on hosts without systemd (and inside Docker), where there is no
    # timer driving this path in the first place.
    if ! command -v systemd-run >/dev/null 2>&1; then
        return 1
    fi

    local installer_src="${BASH_SOURCE[0]:-}"
    if [[ -z "$installer_src" || ! -r "$installer_src" ]]; then
        # curl | bash leaves no file to re-exec.
        return 1
    fi

    local install_dir="${INSTALL_DIR:-${PULSE_INSTALL_DIR:-/opt/pulse}}"
    local repair_copy="${install_dir}/.pulse-update-asset-repair.sh"
    if ! cp "$installer_src" "$repair_copy" 2>/dev/null; then
        return 1
    fi
    chmod 0700 "$repair_copy" 2>/dev/null || true

    print_info "The auto-update sandbox blocks ${blocked_dir}; refreshing the update assets through a transient systemd unit."

    local status=0
    systemd-run --quiet --collect --wait \
        --unit="pulse-update-asset-repair-$$" \
        --description="Refresh Pulse auto-update helper and units outside the update sandbox" \
        --property=Type=oneshot \
        --setenv=PULSE_AUTO_UPDATE_ASSET_REPAIR=1 \
        --setenv="PULSE_SERVICE_NAME=${SERVICE_NAME:-${PULSE_SERVICE_NAME:-pulse}}" \
        --setenv="PULSE_INSTALL_DIR=${install_dir}" \
        --setenv="PULSE_CONFIG_DIR=${CONFIG_DIR:-${PULSE_CONFIG_DIR:-/etc/pulse}}" \
        --setenv="PULSE_AUTO_UPDATE_DEST=${AUTO_UPDATE_DEST:-${PULSE_AUTO_UPDATE_DEST:-/usr/local/bin/pulse-auto-update.sh}}" \
        --setenv="PULSE_UPDATE_SERVICE_PATH=${UPDATE_SERVICE_PATH:-${PULSE_UPDATE_SERVICE_PATH:-/etc/systemd/system/pulse-update.service}}" \
        --setenv="PULSE_UPDATE_TIMER_PATH=${UPDATE_TIMER_PATH:-${PULSE_UPDATE_TIMER_PATH:-/etc/systemd/system/pulse-update.timer}}" \
        /usr/bin/env bash "$repair_copy" --repair-auto-update-units || status=$?

    rm -f "$repair_copy"

    if [[ "$status" -ne 0 ]]; then
        print_warn "Could not refresh the auto-update assets outside the sandbox (systemd-run exited ${status})."
        return 1
    fi

    print_success "Auto-update helper and units refreshed outside the update sandbox."
    return 0
}

# A runtime mask may live in /run while we write /etc, and a persistent mask
# may live outside a configured destination. Checking only those destination
# symlinks can therefore override an effective operator stop. Inspect both
# exact unit names before even staging assets, including sandbox repair/setup.
auto_update_units_refreshable() {
    local timer_path="$1" service_path="$2" asset=""
    for asset in "$timer_path" "$service_path"; do
        if [[ -L "$asset" && "$asset" -ef /dev/null ]]; then
            print_info "Auto-update unit is deliberately masked ($asset); preserving the existing helper and units."
            return 1
        fi
    done

    local timer_unit service_unit unit_files=""
    timer_unit="$(basename "$timer_path")"
    service_unit="$(basename "$service_path")"
    if ! command -v systemctl >/dev/null 2>&1 ||
       ! unit_files=$(unit_file_inventory "$timer_unit" "$service_unit"); then
        print_warn "Cannot confirm auto-update unit masks; preserving the existing helper and units."
        return 1
    fi

    local unit="" state="" remainder="" seen_timer=false seen_service=false
    while read -r unit state remainder; do
        [[ -n "$unit" ]] || continue
        if [[ "$unit" == "$timer_unit" && "$seen_timer" == false ]]; then
            seen_timer=true
        elif [[ "$unit" == "$service_unit" && "$seen_service" == false ]]; then
            seen_service=true
        else
            print_warn "Unexpected auto-update unit inventory; preserving the existing helper and units."
            return 1
        fi
        case "$state" in
            masked|masked-runtime)
                print_info "Auto-update unit is deliberately $state ($unit); preserving the existing helper and units."
                return 1 ;;
            enabled|enabled-runtime|disabled|static|indirect|linked|linked-runtime|alias|generated|transient) ;;
            *)
                print_warn "Cannot confirm auto-update unit state ($unit); preserving the existing helper and units."
                return 1 ;;
        esac
    done <<< "$unit_files"
    # A successful empty inventory is valid for first-time setup; failed or
    # malformed stdout above is never treated as absence or consent.
    return 0
}

# A linked/masked or non-regular destination belongs to operator reconciliation,
# not an automatic conversion into a Pulse-owned file. Check the complete set
# before staging and publication; each rename also rechecks its own destination.
auto_update_asset_destinations_regular() {
    local asset
    for asset in "$@"; do
        if [[ -L "$asset" || ( -e "$asset" && ! -f "$asset" ) ]]; then
            print_warn "Cannot replace redirected or non-regular auto-update asset ($asset); inspect the destination before retrying."
            return 1
        fi
    done
    return 0
}

# Only this invocation's exclusively allocated stages are supplied here. Never
# adopt, truncate or clean up the old fixed .tmp names or another run's files.
cleanup_auto_update_asset_stages() {
    local stage
    for stage in "$@"; do
        [[ -n "$stage" ]] || continue
        if ! rm -f -- "$stage"; then
            print_warn "Could not remove staged auto-update asset ($stage); inspect the retained staging file before retrying."
        fi
    done
    return 0
}

# Installs the auto-update helper script and rewrites the systemd
# service/timer units. Shared by setup_auto_updates (first-time enable) and
# refresh_auto_updates (updates/reinstalls where the timer already exists).
# Does not touch system.json or the timer's enabled/started state.
install_auto_update_assets() {
    local service_name="${SERVICE_NAME:-${PULSE_SERVICE_NAME:-pulse}}"
    local install_dir="${INSTALL_DIR:-${PULSE_INSTALL_DIR:-/opt/pulse}}"
    local config_dir="${CONFIG_DIR:-${PULSE_CONFIG_DIR:-/etc/pulse}}"
    local auto_update_dest="${AUTO_UPDATE_DEST:-${PULSE_AUTO_UPDATE_DEST:-/usr/local/bin/pulse-auto-update.sh}}"
    local update_service_path="${UPDATE_SERVICE_PATH:-${PULSE_UPDATE_SERVICE_PATH:-/etc/systemd/system/${service_name}-update.service}}"
    local update_timer_path="${UPDATE_TIMER_PATH:-${PULSE_UPDATE_TIMER_PATH:-/etc/systemd/system/${service_name}-update.timer}}"
    local update_timer_unit
    update_timer_unit="$(basename "$update_timer_path")"
    auto_update_units_refreshable "$update_timer_path" "$update_service_path" || return 1
    auto_update_asset_destinations_regular "$auto_update_dest" "$update_service_path" "$update_timer_path" || return 1
    local auto_update_bin_dir update_unit_dir update_timer_dir
    auto_update_bin_dir="$(dirname "$auto_update_dest")"
    update_unit_dir="$(dirname "$update_service_path")"
    update_timer_dir="$(dirname "$update_timer_path")"
    local unit_write_dirs="$update_unit_dir"
    if [[ "$update_timer_dir" != "$update_unit_dir" ]]; then
        unit_write_dirs="$unit_write_dirs $update_timer_dir"
    fi

    # Everything below stages into these directories and commits with a
    # rename, so check up front that entries can be created in each of them.
    # A box whose update unit predates the widened sandbox fails here, and the
    # migration escapes that sandbox to write the corrected assets instead.
    local asset_dir
    for asset_dir in "$auto_update_bin_dir" $unit_write_dirs; do
        if auto_update_dir_writable "$asset_dir"; then
            continue
        fi
        if migrate_auto_update_assets_outside_sandbox "$asset_dir"; then
            return 0
        fi
        print_warn "Cannot write to ${asset_dir} to refresh the auto-update assets."
        return 1
    done

    # Stage the helper next to its destination and only swap it in once it is
    # fully configured. During unattended updates install.sh runs under the
    # pulse-update.service sandbox, where the file being replaced is the very
    # script bash is executing — the same-directory mv keeps the swap an
    # atomic rename — and a failed download or configure must leave the
    # previously working helper in place rather than deleting it out from
    # under the timer's ExecStart.
    local staged_helper
    if ! staged_helper=$(mktemp "${auto_update_dest}.staged.XXXXXX"); then
        print_warn "Cannot write to ${auto_update_bin_dir} to stage the auto-update helper."
        return 1
    fi
    if [[ -f "$install_dir/scripts/pulse-auto-update.sh" ]]; then
        # Copy auto-update script if it exists in the release. Both callers
        # invoke this function under `if !`, which suppresses errexit for the
        # whole body, so an unchecked cp (ENOSPC, EIO) would fall through and
        # swap a zero-byte "helper" over the working one.
        if ! cp "$install_dir/scripts/pulse-auto-update.sh" "$staged_helper"; then
            print_warn "Could not copy the bundled auto-update helper into ${auto_update_bin_dir}."
            cleanup_auto_update_asset_stages "$staged_helper"
            return 1
        fi
    else
        print_info "Downloading auto-update script..."
        if ! AUTO_UPDATE_DEST="$staged_helper" download_auto_update_script; then
            print_warn "Could not download the auto-update helper after multiple attempts."
            cleanup_auto_update_asset_stages "$staged_helper"
            return 1
        fi
    fi

    if ! configure_auto_update_script_repo "$staged_helper"; then
        print_warn "Could not configure the auto-update helper for the selected release repo."
        cleanup_auto_update_asset_stages "$staged_helper"
        return 1
    fi

    # Render both units beside their destinations before committing anything.
    # A bare truncating `cat > "$unit"` with no status check could leave a
    # half-written unit behind and still report success, because the only
    # thing left in this function is safe_systemctl daemon-reload, which
    # deliberately returns 0 even when it fails.
    local staged_service="" staged_timer=""
    if ! staged_service=$(mktemp "${update_service_path}.staged.XXXXXX"); then
        print_warn "Could not stage ${update_service_path}; existing auto-update assets were not changed."
        cleanup_auto_update_asset_stages "$staged_helper"
        return 1
    fi
    if ! staged_timer=$(mktemp "${update_timer_path}.staged.XXXXXX"); then
        print_warn "Could not stage ${update_timer_path}; existing auto-update assets were not changed."
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service"
        return 1
    fi

    # The heredoc is unquoted so the installer substitutes paths/names; the
    # rendered unit must contain no unexpanded $ (in particular never $$,
    # which bash expands to the installer's PID and which broke ExecCondition
    # before v6.0.0).
    if ! cat > "$staged_service" <<EOF
[Unit]
Description=Automatic Pulse update check and install
Documentation=$(repo_web_url)
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=root
Group=root
# Skip auto-update run unless the Pulse service it updates is active
ExecCondition=/bin/sh -c 'systemctl is-active --quiet ${service_name}'
ExecStart=${auto_update_dest}
Restart=no
TimeoutStartSec=600
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${service_name}-update
Environment="PULSE_SERVICE_NAME=$service_name"
Environment="PULSE_INSTALL_DIR=$install_dir"
Environment="PULSE_CONFIG_DIR=$config_dir"
Environment="PULSE_UPDATE_TIMER_UNIT=$update_timer_unit"
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
# The helper and unit directories are writable on purpose: the installer this
# service runs refreshes the auto-update helper and rewrites these units, and
# without write access updater fixes only ever reach boxes via manual installs.
# These are directory grants rather than the four individual file paths
# because every one of those writes commits with a rename from a sibling
# staging file (so the previously working helper survives a failed refresh),
# and rename needs write access on the containing directory. The tradeoff is
# that this unit can write any unit file: it already runs the installer as
# root, so the grant does not widen what a compromised installer could do,
# but it does mean the sandbox no longer contains a buggy installer's writes
# to /etc/systemd/system.
ReadWritePaths=$install_dir $config_dir /tmp $auto_update_bin_dir $unit_write_dirs
PrivateNetwork=no
Nice=10

[Install]
WantedBy=multi-user.target
EOF
    then
        print_warn "Could not write ${update_service_path}."
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service" "$staged_timer"
        return 1
    fi

    if ! cat > "$staged_timer" <<EOF
[Unit]
Description=Daily check for Pulse updates
Documentation=$(repo_web_url)
After=network-online.target
Wants=network-online.target

[Timer]
# One update attempt per day, landing in the 02:00-06:00 window. A second
# OnCalendar=daily line here used to double the schedule with an extra
# midnight trigger (issue #1643).
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=4h
Persistent=true
AccuracySec=1h

[Install]
WantedBy=timers.target
EOF
    then
        print_warn "Could not write ${update_timer_path}."
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service" "$staged_timer"
        return 1
    fi

    if ! auto_update_helper_is_sane "$staged_helper"; then
        print_warn "The staged auto-update helper is empty or has no interpreter line; keeping the previously installed helper."
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service" "$staged_timer"
        return 1
    fi

    # Explicit mode, not chmod +x: mktemp creates the staging file 0600, and
    # the installed helper should keep the 0755 a plain copy would have had.
    if ! chmod 0755 "$staged_helper"; then
        print_warn "Could not make the staged auto-update helper executable."
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service" "$staged_timer"
        return 1
    fi

    # mktemp creates private files; preserve the installed unit modes even
    # when the installer itself runs under a private umask.
    if ! chmod 0644 "$staged_service" || ! chmod 0644 "$staged_timer"; then
        print_warn "Could not set auto-update unit permissions; existing assets were not changed."
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service" "$staged_timer"
        return 1
    fi

    # Rendering is not a lock. Reconfirm effective masks and the complete set
    # before the first publication, then recheck each destination at its swap.
    if ! auto_update_units_refreshable "$update_timer_path" "$update_service_path" ||
       ! auto_update_asset_destinations_regular "$auto_update_dest" "$update_service_path" "$update_timer_path"; then
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service" "$staged_timer"
        return 1
    fi

    # Each same-directory rename is atomic, not the three-file set. Keep the
    # helper -> service -> timer dependency order and never move a stage INTO
    # an unexpected directory. A later failure reports the partial publication;
    # it does not claim rollback or change preferences, enablement or starts.
    if ! auto_update_asset_destinations_regular "$auto_update_dest" ||
       ! mv -fT -- "$staged_helper" "$auto_update_dest"; then
        print_warn "Could not install the auto-update helper at ${auto_update_dest}."
        cleanup_auto_update_asset_stages "$staged_helper" "$staged_service" "$staged_timer"
        return 1
    fi
    if ! auto_update_asset_destinations_regular "$update_service_path" ||
       ! mv -fT -- "$staged_service" "$update_service_path"; then
        print_warn "Auto-update helper was installed, but service publication is unconfirmed (${update_service_path}); inspect the assets before retrying."
        cleanup_auto_update_asset_stages "$staged_service" "$staged_timer"
        return 1
    fi
    if ! auto_update_asset_destinations_regular "$update_timer_path" ||
       ! mv -fT -- "$staged_timer" "$update_timer_path"; then
        print_warn "Auto-update helper and service were installed, but timer publication is unconfirmed (${update_timer_path}); inspect the assets before retrying."
        cleanup_auto_update_asset_stages "$staged_timer"
        # The complete new service is already on disk. Reconcile its loaded
        # definition even on partial publication, without enabling/starting it.
        safe_systemctl daemon-reload
        return 1
    fi

    # Reload systemd daemon
    safe_systemctl daemon-reload
}

setup_auto_updates() {
    print_info "Setting up automatic updates..."
    local desired_channel
    desired_channel=$(selected_update_channel)
    local service_name="${SERVICE_NAME:-${PULSE_SERVICE_NAME:-pulse}}"
    local config_dir="${CONFIG_DIR:-${PULSE_CONFIG_DIR:-/etc/pulse}}"
    local update_timer_path="${UPDATE_TIMER_PATH:-${PULSE_UPDATE_TIMER_PATH:-/etc/systemd/system/${service_name}-update.timer}}"
    local update_timer_unit
    update_timer_unit="$(basename "$update_timer_path")"

    # A reinstall over a removed /etc/pulse reaches setup_auto_updates before
    # setup_directories has recreated the config directory, and the
    # system.json write below would fail with "No such file or directory"
    # while the run still reported that auto-updates were enabled. Do this
    # before installing assets or enabling the timer so a failure here leaves
    # nothing half-configured.
    if ! mkdir -p "$config_dir"; then
        print_warn "Could not create $config_dir. Continuing without automatic updates."
        ENABLE_AUTO_UPDATES=false
        return 0
    fi

    if ! install_auto_update_assets; then
        print_warn "Continuing without automatic updates. Review the reported unit state or asset error before retrying."
        ENABLE_AUTO_UPDATES=false
        return 0
    fi

    # Updater setup is optional: a failure must not prevent the main flow from
    # restarting monitoring. But a failed command is not successful enablement.
    if ! safe_systemctl enable "$update_timer_unit"; then
        print_warn "Automatic update setup is incomplete: could not enable $update_timer_unit. Saved update preferences were not changed."
        print_info "Inspect the timer before retrying: systemctl status $update_timer_unit"
        ENABLE_AUTO_UPDATES=false
        return 0
    fi

    # Stage the entire preference change beside the destination. A failed jq,
    # sed or rename must preserve the old configuration, not start the timer
    # against partially written preferences or report setup as complete.
    local staged_config="" config_saved=false
    if staged_config=$(mktemp "$config_dir/.system.json.XXXXXX"); then
        if [[ -f "$config_dir/system.json" ]]; then
            if command -v jq &> /dev/null; then
                if jq --arg channel "$desired_channel" '.autoUpdateEnabled = true | .updateChannel = $channel' "$config_dir/system.json" > "$staged_config"; then
                    config_saved=true
                fi
            elif cp "$config_dir/system.json" "$staged_config"; then
                # Keep the existing no-jq fallback, but apply both edits only
                # to the staged copy and require each edit to succeed.
                local enable_edit='s/^{/{"autoUpdateEnabled":true,/'
                local channel_edit="s/^{/{\"updateChannel\":\"$desired_channel\",/"
                if grep -q '"autoUpdateEnabled"' "$staged_config"; then
                    enable_edit='s/"autoUpdateEnabled":[^,}]*/"autoUpdateEnabled":true/'
                fi
                if grep -q '"updateChannel"' "$staged_config"; then
                    channel_edit="s/\"updateChannel\":[^,}]*/\"updateChannel\":\"$desired_channel\"/"
                fi
                if sed -i "$enable_edit" "$staged_config" && sed -i "$channel_edit" "$staged_config"; then
                    config_saved=true
                fi
            fi
        elif printf '{"autoUpdateEnabled":true,"updateChannel":"%s","pollingInterval":5}\n' "$desired_channel" > "$staged_config"; then
            config_saved=true
        fi
    fi
    if [[ "$config_saved" != true ]] || ! mv "$staged_config" "$config_dir/system.json"; then
        if [[ -n "$staged_config" ]] && ! rm -f "$staged_config"; then
            print_warn "Could not remove the staged update preferences in $config_dir; inspect retained staging files before retrying."
        fi
        print_warn "Automatic update setup is incomplete: could not save update preferences. The previous configuration was preserved; the timer may already be enabled, but no start was requested."
        print_info "Inspect the timer before retrying: systemctl status $update_timer_unit"
        ENABLE_AUTO_UPDATES=false
        return 0
    fi

    chown pulse:pulse "$config_dir/system.json" 2>/dev/null || true

    if ! safe_systemctl start "$update_timer_unit"; then
        print_warn "Automatic update setup is incomplete: preferences were saved, but $update_timer_unit could not be started."
        print_info "Inspect the timer before retrying: systemctl status $update_timer_unit"
        ENABLE_AUTO_UPDATES=false
        return 0
    fi

    # Command success is not an observed running timer. The completion section
    # separately reads its actual boot setting and active state.
    print_success "Automatic update preferences saved; timer enable/start commands completed. Check the timer state in the completion summary."
}

# Updates and reinstalls only run setup_auto_updates when the user opts in,
# but a box that already has the timer keeps whatever helper script and units
# a previous install wrote — e.g. a v5.1-pinned helper that never selects a
# v6 release, so unattended updates silently never resume after upgrading.
# Refresh the installed assets unconditionally instead, without changing the
# user's choices: system.json and the timer's enabled state stay untouched.
refresh_auto_updates() {
    print_info "Refreshing the installed auto-update helper..."
    if ! install_auto_update_assets; then
        print_warn "Could not refresh the auto-update helper; the previously installed one may be stale."
        print_warn "Review the reported unit state or asset error before retrying; enabling updates is not a mask repair."
    fi
    return 0
}

# Use the writer's exact service selection and destination admission before
# callers stop Pulse or change its files. Ensure-only updates keep a sound
# existing unit (including a linked unit); they do not need to rewrite it.
preflight_systemd_service_installation() {
    if [[ "${1:-replace}" == ensure ]]; then
        command -v systemctl >/dev/null 2>&1 || return 0
        [[ ! -f "/etc/systemd/system/${SERVICE_NAME}.service" ]] || return 0
    fi

    # Use existing service name if found, otherwise use default
    local existing_service="$SERVICE_NAME"
    if [[ "$SERVICE_NAME_EXPLICIT" != "true" ]]; then
        existing_service=$(detect_service_name) || return 1
    fi
    if [[ "$existing_service" == "pulse-backend" ]] && [[ -f "/etc/systemd/system/pulse-backend.service" ]]; then
        # Keep using pulse-backend for compatibility (ProxmoxVE)
        SERVICE_NAME="pulse-backend"
        print_info "Using existing service name: pulse-backend"
    fi
    
    local service_path="/etc/systemd/system/$SERVICE_NAME.service"
    # Atomic publication must not replace an operator mask or redirected unit.
    # Keep non-regular destinations for deliberate manual reconciliation.
    if [[ -L "$service_path" || ( -e "$service_path" && ! -f "$service_path" ) ]]; then
        print_error "Cannot replace a redirected or non-regular service definition ($service_path); installation is incomplete. Inspect the unit before retrying."
        return 1
    fi
    return 0
}

install_systemd_service() {
    print_info "Installing systemd service..."
    # Recheck at the writer as well: early caller admission is not a lock.
    preflight_systemd_service_installation || return 1

    local service_path="/etc/systemd/system/$SERVICE_NAME.service"
    local staged_service="" render_failed=false
    if ! staged_service=$(mktemp "${service_path}.staged.XXXXXX"); then
        print_error "Cannot stage the systemd service ($service_path); installation is incomplete. The existing service definition was not changed."
        return 1
    fi

    # The main callers check this function with || return 1, which suppresses
    # errexit throughout it. Check every write, including both append phases;
    # best-effort daemon-reload must never hide a truncated unit publication.
    if ! cat > "$staged_service" << EOF
[Unit]
Description=Pulse Monitoring Server
After=network.target

[Service]
Type=simple
User=pulse
Group=pulse
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/bin/pulse
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
SyslogLevelPrefix=true
Environment="HOME=$INSTALL_DIR"
Environment="PATH=$INSTALL_DIR/.local/bin:$INSTALL_DIR/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Environment="PULSE_DATA_DIR=$CONFIG_DIR"
Environment="PULSE_DEPLOYMENT_METHOD=systemd"
# Pulse prefixes each JSON line with its zerolog priority. systemd consumes
# the prefix into PRIORITY and retains the original JSON as MESSAGE.
Environment="PULSE_LOG_JOURNAL_LEVEL_PREFIX=true"
EnvironmentFile=-$CONFIG_DIR/.env
EOF
    then
        render_failed=true
    fi

    # Add port configuration if not default
    if [[ "$render_failed" == false && "${FRONTEND_PORT:-7655}" != "7655" ]]; then
        if ! cat >> "$staged_service" << EOF
Environment="FRONTEND_PORT=$FRONTEND_PORT"
EOF
        then
            render_failed=true
        fi
    fi

    if [[ "$render_failed" == false ]] && ! cat >> "$staged_service" << EOF

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$INSTALL_DIR $CONFIG_DIR
# ICMP availability probes exec the system ping binary; NoNewPrivileges strips
# its setuid/file capabilities, so grant CAP_NET_RAW ambiently instead (#1554).
AmbientCapabilities=CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_RAW

[Install]
WantedBy=multi-user.target
EOF
    then
        render_failed=true
    fi

    # Recheck the destination before the one same-directory atomic rename.
    # A failed staging, mode or publication operation stops the real caller
    # before startup/completion; no rollback or service recovery is implied.
    if [[ "$render_failed" == true || -L "$service_path" || ( -e "$service_path" && ! -f "$service_path" ) ]] ||
       ! chmod 0644 "$staged_service" || ! mv -fT -- "$staged_service" "$service_path"; then
        if ! rm -f -- "$staged_service"; then
            print_warn "Could not remove staged service definition ($staged_service); inspect the retained staging file before retrying."
        fi
        print_error "Could not publish the complete systemd service ($service_path); installation is incomplete. Inspect the service definition and state before retrying; no automatic restart was attempted."
        return 1
    fi

    # Reload systemd daemon
    safe_systemctl daemon-reload
}

# Updates keep an existing unit file untouched (it may carry user
# customizations such as FRONTEND_PORT), but a half-removed installation —
# binary still present, unit deleted by hand (#1663) — must get a fresh unit
# or the enable/start below fails with "Unit ... could not be found".
ensure_systemd_service_installed() {
    command -v systemctl >/dev/null 2>&1 || return 0
    if [[ ! -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
        print_warn "systemd unit ${SERVICE_NAME}.service is missing; recreating it"
        install_systemd_service || return 1
    fi
}

# Tracks whether Pulse was running before an update-time stop, so start_pulse can
# guarantee it comes back up afterward instead of silently leaving it stopped
# (#1323: on unprivileged LXC the installer's restart can silently fail).
PULSE_WAS_ACTIVE="false"

# Poll within one elapsed-time budget (default 20 seconds), including manager
# reads and sleeps. Each client gets at most five seconds, capped by the budget,
# plus a one-second forced-stop grace. A late active reply cannot pass admission.
wait_for_service_active() {
    local service_name=$1
    local timeout_seconds="${2:-20}"
    [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || return 1
    local started_at=$SECONDS elapsed=0 probe_seconds
    while (( elapsed < timeout_seconds )); do
        probe_seconds=$((timeout_seconds - elapsed))
        (( probe_seconds <= 5 )) || probe_seconds=5
        if timeout -k 1 "$probe_seconds" systemctl is-active --quiet "$service_name" 2>/dev/null; then
            (( SECONDS - started_at < timeout_seconds )) && return 0
            return 1
        fi
        elapsed=$((SECONDS - started_at))
        (( elapsed < timeout_seconds )) || break
        sleep 1
        elapsed=$((SECONDS - started_at))
    done
    return 1
}

# Stop Pulse for an update, remembering whether it was running so start_pulse can
# guarantee it comes back up afterward (#1323).
stop_pulse_for_update() {
    PULSE_WAS_ACTIVE="false"
    stop_pulse_for_replacement "$SERVICE_NAME"
}

# After an update, if Pulse was running beforehand, make sure it is running again:
# retry one explicit start, and surface a clear error if it still will not come up
# — rather than printing a success completion over a stopped service (#1323).
ensure_pulse_running_after_update() {
    [[ "$PULSE_WAS_ACTIVE" == "true" ]] || return 0
    PULSE_WAS_ACTIVE="false"

    if wait_for_service_active "$SERVICE_NAME" 20; then
        return 0
    fi

    print_warn "Pulse did not come back up after the update; retrying start..."
    safe_systemctl start "$SERVICE_NAME" || true
    if wait_for_service_active "$SERVICE_NAME" 20; then
        print_success "Pulse is running again."
        return 0
    fi

    print_error "Pulse was running before the update but did not come back up (#1323)."
    print_info "The new binary is installed; investigate why the service will not start:"
    print_info "  systemctl status $SERVICE_NAME"
    print_info "  journalctl -u $SERVICE_NAME -n 50"
    return 1
}

start_pulse() {
    print_info "Starting Pulse..."

    # A missing unit is a broken installation, not an unprivileged-container
    # quirk: "Unit ... could not be found" must never be softened into the
    # container notes below and then reported as success (#1663). The
    # installer always writes /etc/systemd/system/<name>.service; systemctl
    # cat covers units living elsewhere.
    if command -v systemctl >/dev/null 2>&1 \
        && [[ ! -f "/etc/systemd/system/${SERVICE_NAME}.service" ]] \
        && ! timeout -k 1 5 systemctl cat "${SERVICE_NAME}.service" >/dev/null 2>&1; then
        print_error "systemd unit ${SERVICE_NAME}.service does not exist; Pulse was not started"
        print_info "Re-run the installer to recreate the service unit"
        return 1
    fi

    # Enabling boot-time startup and observing a running service are separate.
    # A command error does not establish its cause or a future successful boot.
    if ! safe_systemctl enable "$SERVICE_NAME"; then
        print_warn "Could not enable Pulse at boot; check the service's enabled state"
    fi

    if ! safe_systemctl start "$SERVICE_NAME"; then
        print_warn "Pulse start did not complete successfully; checking actual service state"
    fi

    # Wait for service to start
    sleep 3

    if [[ "$PULSE_WAS_ACTIVE" == "true" ]]; then
        # Preserve the existing bounded retry only for an update that stopped
        # a running Pulse. Fresh/prior-stopped installs still require readback.
        ensure_pulse_running_after_update || return 1
    elif ! wait_for_service_active "$SERVICE_NAME" 20; then
        print_error "Could not confirm Pulse is active; installation is incomplete"
        print_info "Installed files and configuration have been retained; check the service before retrying:"
        print_info "  systemctl status $SERVICE_NAME"
        print_info "  journalctl -u $SERVICE_NAME -n 50"
        timeout -k 1 5 journalctl -u "$SERVICE_NAME" -n 20 2>/dev/null || true
        return 1
    fi

    print_success "Pulse started successfully"
    return 0
}

create_marker_file() {
    # Create marker file for version tracking (helps with Community Scripts compatibility)
    touch ~/.pulse 2>/dev/null || true
}

print_completion() {
    local IP=$(hostname -I | awk '{print $1}')
    
    # Get the port from the service file or use default
    local PORT
    PORT=$(current_frontend_port)
    
    echo
    print_header
    print_success "Pulse installation completed!"
    echo
    local PULSE_URL="http://${IP}:${PORT}"

    echo -e "${GREEN}Access Pulse at:${NC} ${PULSE_URL}"
    echo
    echo -e "${YELLOW}Quick commands:${NC}"
    echo "  systemctl status $SERVICE_NAME    - Check status"
    echo "  systemctl restart $SERVICE_NAME   - Restart"
    echo "  journalctl -u $SERVICE_NAME -f    - View logs"
    echo
    echo -e "${YELLOW}Management:${NC}"
    echo "  Update:     $(build_printed_management_command update)"
    echo "  Reset:      $(build_printed_management_command reset)"
    echo "  Uninstall:  $(build_printed_management_command uninstall)"

    print_auto_update_status

    # Show bootstrap token on fresh install
    local TOKEN_DATA_DIR="${CONFIG_DIR:-/etc/pulse}"
    local TOKEN_FILE="$TOKEN_DATA_DIR/.bootstrap_token"
    if [[ -f "$TOKEN_FILE" ]]; then
        local -a BOOTSTRAP_TOKEN_COMMAND=()
        if [[ -x "$BINARY_LINK_PATH" ]]; then
            BOOTSTRAP_TOKEN_COMMAND=("$BINARY_LINK_PATH" "bootstrap-token")
        elif [[ -x "$INSTALL_DIR/bin/pulse" ]]; then
            BOOTSTRAP_TOKEN_COMMAND=("$INSTALL_DIR/bin/pulse" "bootstrap-token")
        elif command -v pulse >/dev/null 2>&1; then
            BOOTSTRAP_TOKEN_COMMAND=("$(command -v pulse)" "bootstrap-token")
        fi

        echo
        if [[ ${#BOOTSTRAP_TOKEN_COMMAND[@]} -gt 0 ]]; then
            if ! PULSE_DATA_DIR="$TOKEN_DATA_DIR" "${BOOTSTRAP_TOKEN_COMMAND[@]}"; then
                local QUOTED_TOKEN_DATA_DIR
                local TOKEN_COMMAND_DISPLAY
                printf -v QUOTED_TOKEN_DATA_DIR '%q' "$TOKEN_DATA_DIR"
                printf -v TOKEN_COMMAND_DISPLAY '%q ' "${BOOTSTRAP_TOKEN_COMMAND[@]}"
                TOKEN_COMMAND_DISPLAY="${TOKEN_COMMAND_DISPLAY% }"
                print_warn "Bootstrap token exists but could not be displayed automatically."
                print_info "Run: PULSE_DATA_DIR=$QUOTED_TOKEN_DATA_DIR $TOKEN_COMMAND_DISPLAY"
            fi
        else
            echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════════════╗${NC}"
            echo -e "${YELLOW}║          BOOTSTRAP TOKEN REQUIRED FOR FIRST-TIME SETUP                ║${NC}"
            echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════════════╣${NC}"
            echo -e "${YELLOW}║${NC}  Run this command on the Pulse host to reveal the setup token:      ${YELLOW}║${NC}"
            printf "${YELLOW}║${NC}  PULSE_DATA_DIR=%-47s${YELLOW}║${NC}\n" "$TOKEN_DATA_DIR"
            printf "${YELLOW}║${NC}  %s/bin/pulse bootstrap-token%-37s${YELLOW}║${NC}\n" "$INSTALL_DIR" ""
            echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════════════╣${NC}"
            echo -e "${YELLOW}║${NC}  Paste the token into the unlock screen in your browser.            ${YELLOW}║${NC}"
            echo -e "${YELLOW}║${NC}  This token will be automatically deleted after successful setup.   ${YELLOW}║${NC}"
            echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════════════╝${NC}"
        fi
    fi

    echo
}

build_printed_management_command() {
    local action=$1
    local update_helper_path="${UPDATE_HELPER_PATH:-${PULSE_UPDATE_HELPER_PATH:-/bin/update}}"
    local -a args=()

    case "$action" in
        update)
            ;;
        reset)
            args+=(--reset)
            ;;
        uninstall)
            args+=(--uninstall)
            ;;
        *)
            return 1
            ;;
    esac

    if [[ -n "${FORCE_VERSION:-}" ]] && [[ "$action" != "uninstall" ]]; then
        args=(--version "$FORCE_VERSION" "${args[@]}")
    elif [[ "${FORCE_CHANNEL:-${UPDATE_CHANNEL:-stable}}" == "rc" ]] && [[ "$action" != "uninstall" ]]; then
        args=(--rc "${args[@]}")
    fi

    printf '%q' "$update_helper_path"

    if [[ ${#args[@]} -eq 0 ]]; then
        printf '\n'
        return 0
    fi

    local arg
    for arg in "${args[@]}"; do
        printf ' %q' "$arg"
    done
    printf '\n'
}

# Main installation flow
main() {
    # Skip Proxmox host check if we're already inside a container
    if [[ "$IN_CONTAINER" != "true" ]] && check_proxmox_host; then
        create_lxc_container
        exit 0
    fi
    
    print_header
    check_root
    detect_os
    check_docker_environment
    
    # Check for existing installation FIRST before asking for configuration
    local existing_install_status=0
    check_existing_installation || existing_install_status=$?
    if [[ "$existing_install_status" -gt 1 ]]; then
        return "$existing_install_status"
    fi
    if [[ "$existing_install_status" -eq 0 ]]; then
        # If building from source was requested, skip the update prompt
        if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
            preflight_systemd_service_installation || return 1
            create_user
            setup_directories || return 1

            if ! build_from_source "$SOURCE_BRANCH"; then
                exit 1
            fi

            setup_update_command
            install_systemd_service || return 1

            if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then
                setup_auto_updates
            elif update_timer_exists; then
                refresh_auto_updates
            fi

            start_pulse || return 1
            create_marker_file
            print_completion
            exit 0
        fi
        # If a specific version was requested, just update to it
        if [[ -n "${FORCE_VERSION}" ]]; then
            # Determine if this is an upgrade, downgrade, or reinstall
            local action_word="Installing"
            if [[ -n "$CURRENT_VERSION" ]] && [[ "$CURRENT_VERSION" != "unknown" ]]; then
                local compare_result
                compare_versions "$FORCE_VERSION" "$CURRENT_VERSION" && compare_result=$? || compare_result=$?
                case $compare_result in
                    0) action_word="Reinstalling" ;;
                    1) action_word="Updating to" ;;
                    2) action_word="Downgrading to" ;;
                esac
            fi
            print_info "${action_word} version ${FORCE_VERSION}..."
            LATEST_RELEASE="${FORCE_VERSION}"
            
            offer_existing_auto_updates

            # Detect the actual service name before trying to stop it
            SERVICE_NAME=$(detect_service_name) || return 1

            if ! run_upgrade_readiness_preflight "$CURRENT_VERSION" "$LATEST_RELEASE"; then
                exit 1
            fi
            
            preflight_systemd_service_installation ensure || return 1
            backup_existing || return 1
            create_user
            download_pulse
            # A half-removed installation (binary present, /etc/pulse or the
            # unit file deleted, #1663) reaches this update path; recreate
            # the environment the steps below assume instead of crashing on
            # a missing config dir or reporting success over a missing unit.
            setup_directories || return 1
            setup_update_command
            ensure_systemd_service_installed || return 1

            # Setup auto-updates if requested
            if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then
                setup_auto_updates
            elif update_timer_exists; then
                refresh_auto_updates
            fi

            start_pulse || return 1
            create_marker_file
            print_completion
            return 0
        fi
        
        # Use the same complete, server-only metadata admission as downloads.
        # A chart tag or a draft must never become an existing-install action.
        local STABLE_VERSION=""
        STABLE_VERSION=$(resolve_latest_release_tag_for_channel stable 2>/dev/null || true)
        local RC_VERSION=""
        RC_VERSION=$(resolve_latest_release_tag_for_channel rc 2>/dev/null || true)

        # Determine default update channel
        UPDATE_CHANNEL="stable"
        
        # Allow override via command line
        if [[ -n "${FORCE_CHANNEL}" ]]; then
            UPDATE_CHANNEL="${FORCE_CHANNEL}"
        else
            CONFIGURED_CHANNEL=$(read_configured_update_channel 2>/dev/null || true)
            if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then
                UPDATE_CHANNEL="rc"
            fi
        fi
        
        echo
        echo "What would you like to do?"
        
        # Show update options based on available versions
        local menu_option=1
        local stable_choice="" rc_choice=""
        if [[ -n "$STABLE_VERSION" ]] && [[ "$STABLE_VERSION" != "$CURRENT_VERSION" ]]; then
            stable_choice=$menu_option
            echo "${menu_option}) Update to $STABLE_VERSION (stable)"
            ((menu_option++))
        fi
        
        if [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]] && [[ "$RC_VERSION" != "$CURRENT_VERSION" ]]; then
            rc_choice=$menu_option
            echo "${menu_option}) Update to $RC_VERSION (prerelease preview)"
            ((menu_option++))
        fi
        
        echo "${menu_option}) Reinstall current version"
        ((menu_option++))
        echo "${menu_option}) Remove Pulse"
        ((menu_option++))
        echo "${menu_option}) Cancel"
        local max_option=$menu_option
        
        # Try to read user choice interactively
        # safe_read handles both normal and piped input (via /dev/tty)
        local automatic_choice=false
        local requested_channel="$UPDATE_CHANNEL"
        if [[ "$IN_DOCKER" == "true" ]]; then
            # In Docker, always auto-select
            print_info "Docker environment detected. Auto-selecting update option."
            automatic_choice=true
        elif safe_read "Select option [1-${max_option}]: " choice; then
            # Successfully read user choice (either from stdin or /dev/tty)
            : # Do nothing, choice was set
        else
            # safe_read failed - truly non-interactive
            print_info "Non-interactive mode detected. Auto-selecting update option."
            automatic_choice=true
        fi
        
        if [[ "$automatic_choice" == true ]]; then
            # Rows disappear when a release is unavailable or already installed.
            # Fixed option numbers could then select preview, reinstall or REMOVE.
            # Only a newer release in the requested channel is an automatic action.
            local automatic_release="$STABLE_VERSION"
            choice="$stable_choice"
            if [[ "$requested_channel" == rc ]]; then
                automatic_release="$RC_VERSION"
                if [[ "$RC_VERSION" == "$STABLE_VERSION" ]]; then
                    choice="$stable_choice"
                else
                    choice="$rc_choice"
                fi
            fi
            if [[ -z "$automatic_release" ]]; then
                print_error "Cannot determine a published Pulse server release for the $requested_channel channel. Existing installation is unchanged; check connectivity and jq, or deliberately choose an exact version with --version."
                return 1
            fi
            if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != unknown ]]; then
                local automatic_compare=0
                compare_versions "$automatic_release" "$CURRENT_VERSION" || automatic_compare=$?
                case "$automatic_compare" in
                    0)
                        print_info "$CURRENT_VERSION is already the selected $requested_channel release. Existing installation is unchanged."
                        return 0
                        ;;
                    2)
                        print_error "Selected release $automatic_release is older than $CURRENT_VERSION; refusing to downgrade automatically. Existing installation is unchanged; use --version only for a deliberate rollback."
                        return 1
                        ;;
                esac
            fi
            if [[ -z "$choice" ]]; then
                print_error "No update option matches the selected release. Existing installation is unchanged."
                return 1
            fi
        fi

        # Debug: Check if choice was read correctly
        if [[ -z "$choice" ]]; then
            print_error "No option selected. Exiting."
            exit 1
        fi
        
        # Debug output to see what's happening
        # print_info "DEBUG: You selected option $choice"
        
        # Determine what action to take based on the dynamic menu
        local action=""
        local target_version=""
        local current_choice=1
        
        # Check if user selected stable update
        if [[ -n "$STABLE_VERSION" ]] && [[ "$STABLE_VERSION" != "$CURRENT_VERSION" ]]; then
            if [[ "$choice" == "$current_choice" ]]; then
                action="update"
                target_version="$STABLE_VERSION"
                UPDATE_CHANNEL="stable"
            fi
            ((current_choice++))
        fi
        
        # Check if user selected RC update
        if [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]] && [[ "$RC_VERSION" != "$CURRENT_VERSION" ]]; then
            if [[ "$choice" == "$current_choice" ]]; then
                action="update"
                target_version="$RC_VERSION"
                UPDATE_CHANNEL="rc"
            fi
            ((current_choice++))
        fi
        
        # Check if user selected reinstall
        if [[ "$choice" == "$current_choice" ]]; then
            action="reinstall"
        fi
        ((current_choice++))
        
        # Check if user selected remove
        if [[ "$choice" == "$current_choice" ]]; then
            action="remove"
        fi
        ((current_choice++))
        
        # Check if user selected cancel
        if [[ "$choice" == "$current_choice" ]]; then
            action="cancel"
        fi
        
        # Preview can legitimately select the newest stable release. Do not
        # silently turn that requested preview channel into a stable preference.
        if [[ "$automatic_choice" == true && "$action" == update ]]; then
            UPDATE_CHANNEL="$requested_channel"
        fi

        # Debug: Show what action was determined
        # print_info "DEBUG: Action determined: ${action:-'none'}"
        
        case $action in
            update)
                # Determine if this is an upgrade or downgrade
                local action_word="Installing"
                if [[ -n "$CURRENT_VERSION" ]] && [[ "$CURRENT_VERSION" != "unknown" ]]; then
                    local cmp_result=0
                    compare_versions "$target_version" "$CURRENT_VERSION" || cmp_result=$?
                    case $cmp_result in
                        0) action_word="Reinstalling" ;;
                        1) action_word="Updating to" ;;
                        2) action_word="Downgrading to" ;;
                    esac
                fi
                print_info "${action_word} $target_version..."
                LATEST_RELEASE="$target_version"
                
                offer_existing_auto_updates

                if ! run_upgrade_readiness_preflight "$CURRENT_VERSION" "$LATEST_RELEASE"; then
                    exit 1
                fi
                
                preflight_systemd_service_installation ensure || return 1
                backup_existing || return 1
                create_user
                download_pulse
                # Same repair as the --version path: a half-removed
                # installation (#1663) must get its config dir and unit
                # file back before auto-update setup and start.
                setup_directories || return 1
                setup_update_command
                ensure_systemd_service_installed || return 1

                # Setup auto-updates if requested during update; otherwise
                # refresh assets a previous install already put in place so a
                # stale helper (e.g. v5-pinned) doesn't survive the upgrade
                if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then
                    setup_auto_updates
                elif update_timer_exists; then
                    refresh_auto_updates
                fi
                
                start_pulse || return 1
                create_marker_file
                print_completion
                exit 0
                ;;
            reinstall)
                if ! is_pulse_release_tag "$CURRENT_VERSION"; then
                    print_error "Cannot reinstall an unknown current server version. Existing installation is unchanged; deliberately choose an exact version with --version."
                    return 1
                fi
                LATEST_RELEASE="$CURRENT_VERSION"
                offer_existing_auto_updates

                preflight_systemd_service_installation || return 1
                backup_existing || return 1
                create_user
                download_pulse
                setup_directories || return 1
                setup_update_command
                install_systemd_service || return 1
                
                # Setup auto-updates if requested during reinstall; otherwise
                # refresh assets a previous install already put in place
                if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then
                    setup_auto_updates
                elif update_timer_exists; then
                    refresh_auto_updates
                fi
                
                start_pulse || return 1
                create_marker_file
                print_completion
                exit 0
                ;;
            remove)
                if ! quiesce_pulse_for_removal "$SERVICE_NAME"; then
                    print_error "Pulse removal is incomplete; no files have been removed. Previously stopped units are not automatically restarted."
                    return 1
                fi
                
                remove_pulse_runtime_files "$SERVICE_NAME" || return 1
                reload_pulse_units_after_removal || return 1
                
                # Ask about config/data removal
                echo
                print_info "Config and data files exist in $CONFIG_DIR"
                safe_read_with_default "Remove all configuration and data? (y/N): " remove_config "n"
                # Ask about user removal
                if id "pulse" &>/dev/null; then
                    safe_read_with_default "Remove pulse user account? (y/N): " remove_user "n"
                    if [[ "$remove_user" =~ ^[Yy]$ ]]; then
                        remove_pulse_user_for_removal || return 1
                        print_success "User account removed"
                    else
                        print_info "User account preserved"
                    fi
                fi
                
                # Remove any log files
                local log_path
                for log_path in /var/log/pulse*.log; do
                    remove_pulse_path_for_removal "$log_path" || return 1
                done
                if [[ "$SERVICE_NAME_EXPLICIT" != "true" ]]; then
                    remove_pulse_path_for_removal /opt/pulse.log || return 1
                fi

                # Data deletion is last: any earlier incomplete removal leaves
                # recovery state intact. A deletion error can itself be partial.
                if [[ "$remove_config" =~ ^[Yy]$ ]]; then
                    remove_pulse_path_for_removal "$CONFIG_DIR" directory || return 1
                    print_success "Configuration and data removed"
                else
                    print_info "Configuration preserved in $CONFIG_DIR"
                fi
                
                print_success "Pulse removed successfully"
                ;;
            cancel)
                print_info "Installation cancelled"
                exit 0
                ;;
            *)
                print_error "Invalid option"
                exit 1
                ;;
        esac
    else
        # Check if this is truly a fresh installation or an update
        # Check for existing installation BEFORE we create directories
        # If binary exists OR system.json exists OR --version was specified, it's likely an update
        if [[ -f "$INSTALL_DIR/bin/pulse" ]] || [[ -f "$INSTALL_DIR/pulse" ]] || [[ -f "$CONFIG_DIR/system.json" ]] || [[ -n "${FORCE_VERSION}" ]]; then
            # This is an update/reinstall, don't prompt for port
            FRONTEND_PORT=${FRONTEND_PORT:-7655}
        else
            # Fresh installation - ask for port configuration and auto-updates
            FRONTEND_PORT=${FRONTEND_PORT:-}
            if [[ -z "$FRONTEND_PORT" ]]; then
                if [[ "$IN_DOCKER" == "true" ]] || [[ "$IN_CONTAINER" == "true" ]]; then
                    # In Docker/container mode, use default port without prompting
                    FRONTEND_PORT=7655
                else
                    echo
                    safe_read_with_default "Frontend port [7655]: " FRONTEND_PORT "7655"
                    if [[ ! "$FRONTEND_PORT" =~ ^[0-9]+$ ]] || [[ "$FRONTEND_PORT" -lt 1 ]] || [[ "$FRONTEND_PORT" -gt 65535 ]]; then
                        print_error "Invalid port number. Using default port 7655."
                        FRONTEND_PORT=7655
                    fi
                fi
            fi
            
            # Ask about auto-updates for fresh installation
            # Skip if: already set by flag, in Docker, or being installed from host (IN_CONTAINER=true)
            if [[ "$AUTO_UPDATE_CHOICE_EXPLICIT" != "true" ]] && [[ "$ENABLE_AUTO_UPDATES" != "true" ]] && [[ "$IN_DOCKER" != "true" ]] && [[ "$IN_CONTAINER" != "true" ]]; then
                echo
                echo "Enable automatic updates?"
                echo "Pulse can automatically install stable updates daily (between 2-6 AM)"
                safe_read_with_default "Enable auto-updates? [y/N]: " enable_updates "n"
                if [[ "$enable_updates" =~ ^[Yy]$ ]]; then
                    ENABLE_AUTO_UPDATES=true
                fi
            fi
        fi
        
        preflight_systemd_service_installation || return 1
        install_dependencies
        create_user
        setup_directories || return 1
        download_pulse
        setup_update_command
        install_systemd_service || return 1
        
        # Setup auto-updates if requested; a leftover timer from a previous
        # install still gets its helper script and units refreshed
        if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then
            setup_auto_updates
        elif update_timer_exists; then
            refresh_auto_updates
        fi
        
        start_pulse || return 1
        create_marker_file
        print_completion
    fi
}

# Legacy pulse-sensor-proxy footprint (v4/v5). install.sh never installs the
# sensor-proxy in v6 — the unified agent replaced it — but a Proxmox host that
# was upgraded from v5 may still carry it. These paths mirror the LOCAL subset
# of scripts/uninstall-sensor-proxy.sh so a full `--uninstall` on such a host
# leaves nothing behind: binary, units, runtime/state, service user, and —
# security-relevant — the managed SSH keys in root's authorized_keys. The
# cluster-wide key removal and Proxmox API-user deletion stay behind the
# explicit standalone script (we print a pointer to it).
SENSOR_PROXY_BINARY_PATH="${PULSE_SENSOR_PROXY_BINARY_PATH:-/usr/local/bin/pulse-sensor-proxy}"
SENSOR_PROXY_INSTALL_ROOT="${PULSE_SENSOR_PROXY_INSTALL_ROOT:-/opt/pulse/sensor-proxy}"
SENSOR_PROXY_RUNTIME_DIR="${PULSE_SENSOR_PROXY_RUNTIME_DIR:-/run/pulse-sensor-proxy}"
SENSOR_PROXY_WORK_DIR="${PULSE_SENSOR_PROXY_WORK_DIR:-/var/lib/pulse-sensor-proxy}"
SENSOR_PROXY_CONFIG_DIR="${PULSE_SENSOR_PROXY_CONFIG_DIR:-/etc/pulse-sensor-proxy}"
SENSOR_PROXY_LOG_DIR="${PULSE_SENSOR_PROXY_LOG_DIR:-/var/log/pulse/sensor-proxy}"
SENSOR_PROXY_SERVICE_USER="${PULSE_SENSOR_PROXY_SERVICE_USER:-pulse-sensor-proxy}"
SENSOR_PROXY_AUTHORIZED_KEYS_PATH="${PULSE_SENSOR_PROXY_AUTHORIZED_KEYS_PATH:-/root/.ssh/authorized_keys}"
SENSOR_PROXY_SYSTEMD_DIR="${PULSE_SENSOR_PROXY_SYSTEMD_DIR:-/etc/systemd/system}"

local_sensor_proxy_present() {
    [[ -e "$SENSOR_PROXY_BINARY_PATH" ]] && return 0
    [[ -e "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy.service" ]] && return 0
    [[ -d "$SENSOR_PROXY_INSTALL_ROOT" ]] && return 0
    [[ -d "$SENSOR_PROXY_WORK_DIR" ]] && return 0
    [[ -d "$SENSOR_PROXY_CONFIG_DIR" ]] && return 0
    return 1
}

remove_local_sensor_proxy_managed_keys() {
    local auth_file="$SENSOR_PROXY_AUTHORIZED_KEYS_PATH"
    local tmp_file=""
    local status=0
    if [[ ! -e "$auth_file" && ! -L "$auth_file" ]]; then
        return 0
    fi
    # Proxmox can link this file into the shared cluster filesystem. Resolve
    # before replacing it; broken/non-file targets are not confirmed absence.
    if ! auth_file=$(readlink -f -- "$auth_file") || [[ ! -f "$auth_file" || -L "$auth_file" ]]; then
        return 1
    fi
    # Keep traps/descriptors private and replacement on the same filesystem.
    # A failed output open must never masquerade as grep's valid no-match (1).
    if ! (
        tmp_file=$(mktemp "${auth_file}.pulse-cleanup.XXXXXX") || exit 1
        trap 'status=$?; rm -f -- "$tmp_file" || status=1; exit "$status"' EXIT
        trap 'exit 1' HUP INT TERM
        exec 3>"$tmp_file" || exit 1
        grep -v -E '# pulse-(managed|proxy)-key$' "$auth_file" >&3 2>/dev/null || status=$?
        exec 3>&- || exit 1
        (( status <= 1 )) || exit 1
        status=0
        cmp -s "$tmp_file" "$auth_file" || status=$?
        case "$status" in
            0) ;;
            1)
                chmod --reference="$auth_file" "$tmp_file" &&
                    chown --reference="$auth_file" "$tmp_file" &&
                    mv -- "$tmp_file" "$auth_file" || exit 1
                ;;
            *) exit 1 ;;
        esac
    ); then
        return 1
    fi
    echo "Removed legacy pulse-sensor-proxy SSH key entries from $auth_file"
}

cleanup_local_sensor_proxy() {
    local_sensor_proxy_present || return 0

    echo "Removing legacy pulse-sensor-proxy footprint from this host..."

    local unit=""
    for unit in \
        pulse-sensor-proxy.service \
        pulse-sensor-proxy-selfheal.timer \
        pulse-sensor-proxy-selfheal.service \
        pulse-sensor-cleanup.path \
        pulse-sensor-cleanup.service; do
        stop_pulse_unit_for_removal "$unit" || return 1
    done

    rm -f "$SENSOR_PROXY_BINARY_PATH" || return 1
    rm -f /usr/local/bin/pulse-sensor-cleanup.sh || return 1
    rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy.service" || return 1
    rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy-selfheal.service" || return 1
    rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy-selfheal.timer" || return 1
    rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-cleanup.service" || return 1
    rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-cleanup.path" || return 1
    rm -rf "$SENSOR_PROXY_RUNTIME_DIR" || return 1
    rm -rf "$SENSOR_PROXY_INSTALL_ROOT" || return 1
    rm -rf "$SENSOR_PROXY_WORK_DIR" || return 1
    rm -rf "$SENSOR_PROXY_CONFIG_DIR" || return 1
    rm -rf "$SENSOR_PROXY_LOG_DIR" || return 1

    remove_local_sensor_proxy_managed_keys || return 1

    if id -u "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1; then
        userdel --remove "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1 || userdel "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1 || true
    fi
    if getent group "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1; then
        groupdel "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1 || true
    fi

    echo "Removed legacy pulse-sensor-proxy footprint from this host."
    echo "If this host belongs to a Proxmox cluster, run the full cleanup on the"
    echo "other nodes (and to remove the pulse-monitor@pam API user):"
    echo "  curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | bash -s -- --purge --remove-proxmox-access --local-only"
}


# Uninstall function
uninstall_pulse() {
    check_root
    print_header
    echo -e "\033[0;33mUninstalling Pulse...\033[0m"
    echo

    # Detect service name
    local service_name
    service_name=$(detect_service_name) || return 1
    
    if ! quiesce_pulse_for_removal "$service_name"; then
        print_error "Pulse removal is incomplete; no files have been removed. Previously stopped units are not automatically restarted."
        return 1
    fi

    # Remove files
    echo "Removing Pulse files..."
    remove_pulse_runtime_files "$service_name" || return 1

    # Remove any leftover legacy pulse-sensor-proxy footprint on this host so a
    # full uninstall on a v5-upgraded Proxmox host leaves nothing behind.
    cleanup_local_sensor_proxy || return 1

    # Reload systemd
    reload_pulse_units_after_removal || return 1

    # Keep configuration/credentials until runtime, legacy cleanup, systemd and
    # account removal have all completed. Never turn a partial deletion into a
    # successful uninstall when this entry is called conditionally.
    remove_pulse_user_for_removal || return 1
    remove_pulse_path_for_removal "$CONFIG_DIR" directory || return 1
    
    echo
    echo -e "\033[0;32m✓ Pulse has been completely uninstalled\033[0m"
    exit 0
}

# Reset retains the install and unit files. Observe every possible writer before
# mutation; only timers lose enablement temporarily. Never replay an interrupted
# updater, or restart services after an uncertain stop/deletion.
read_pulse_reset_unit_state() {
    local unit="$1"
    PULSE_RESET_LOAD="" PULSE_RESET_ACTIVE="" PULSE_RESET_ENABLED=""
    if ! PULSE_RESET_LOAD=$(timeout -k 1 5 systemctl show "$unit" --property=LoadState --value 2>/dev/null) ||
       ! PULSE_RESET_ACTIVE=$(timeout -k 1 5 systemctl show "$unit" --property=ActiveState --value 2>/dev/null); then
        print_error "Cannot observe unit ($unit); reset incomplete. No automatic recovery will be attempted; reconcile service state before retrying."
        return 1
    fi
    if [[ "$PULSE_RESET_LOAD" == "not-found" && "$PULSE_RESET_ACTIVE" == "inactive" ]]; then
        return 0
    fi
    case "$PULSE_RESET_LOAD" in
        loaded|masked) ;;
        *) print_error "Unknown unit load state ($unit); reset incomplete"; return 1 ;;
    esac
    case "$PULSE_RESET_ACTIVE" in
        active|inactive|failed) ;;
        *) print_error "Unsettled unit state ($unit); reset incomplete"; return 1 ;;
    esac
    if ! PULSE_RESET_ENABLED=$(timeout -k 1 5 systemctl show "$unit" --property=UnitFileState --value 2>/dev/null); then
        print_error "Cannot observe unit enablement ($unit); reset incomplete"
        return 1
    fi
    case "$PULSE_RESET_ENABLED" in
        enabled|enabled-runtime|disabled|static|indirect|masked|masked-runtime|linked|linked-runtime) ;;
        *) print_error "Unknown unit enablement ($unit); reset incomplete"; return 1 ;;
    esac
}

confirm_pulse_reset_unit_stopped() {
    local unit="$1" expected_load="$2" expected_enabled="$3"
    read_pulse_reset_unit_state "$unit" || return 1
    if [[ "$PULSE_RESET_ACTIVE" != "inactive" || "$PULSE_RESET_LOAD" != "$expected_load" || "$PULSE_RESET_ENABLED" != "$expected_enabled" ]]; then
        print_error "Unit quiescence changed or is unconfirmed ($unit); reset incomplete. Files are preserved; no services will be restarted. Reconcile service state before retrying."
        return 1
    fi
}

# Reset function: the caller's existing --reset intent is unchanged.
reset_pulse() {
    check_root
    print_header
    echo -e "\033[0;33mResetting Pulse configuration...\033[0m"
    echo

    local service_name unit i prior_enabled
    service_name=$(detect_service_name) || return 1
    service_name="${service_name%.service}.service"
    local units=("$UPDATE_TIMER_UNIT") roles=(timer)
    # Historical default aliases share the same configuration. Custom instances
    # must not stop, enable or restart another installation's units.
    if [[ "$SERVICE_NAME_EXPLICIT" != "true" && "$UPDATE_TIMER_UNIT" != "pulse-backend-update.timer" ]]; then
        units+=(pulse-backend-update.timer); roles+=(timer)
    fi
    units+=("$UPDATE_SERVICE_UNIT"); roles+=(updater)
    if [[ "$SERVICE_NAME_EXPLICIT" != "true" && "$UPDATE_SERVICE_UNIT" != "pulse-backend-update.service" ]]; then
        units+=(pulse-backend-update.service); roles+=(updater)
    fi
    units+=("$service_name"); roles+=(server)
    if [[ "$SERVICE_NAME_EXPLICIT" != "true" ]]; then
        if [[ "$service_name" != "pulse.service" ]]; then units+=(pulse.service); roles+=(server); fi
        if [[ "$service_name" != "pulse-backend.service" ]]; then units+=(pulse-backend.service); roles+=(server); fi
    fi
    local loads=() active_states=() enabled_states=() stopped_enabled_states=()
    # A failed observation anywhere must precede every stop and deletion.
    for unit in "${units[@]}"; do
        read_pulse_reset_unit_state "$unit" || return 1
        if [[ "$unit" == "$service_name" && "$PULSE_RESET_LOAD" == "not-found" ]]; then
            print_error "Pulse service is missing ($unit); refusing configuration reset"
            return 1
        fi
        loads+=("$PULSE_RESET_LOAD")
        active_states+=("$PULSE_RESET_ACTIVE")
        enabled_states+=("$PULSE_RESET_ENABLED")
        stopped_enabled_states+=("$PULSE_RESET_ENABLED")
    done
    for i in "${!units[@]}"; do
        unit="${units[$i]}"
        if [[ "${active_states[$i]}" != "inactive" ]]; then
            if ! timeout -k 1 5 systemctl stop "$unit"; then
                print_error "Cannot stop unit ($unit); reset incomplete. Configuration is preserved; no services will be restarted. Reconcile service state before retrying."
                return 1
            fi
        fi
        prior_enabled="${enabled_states[$i]}"
        if [[ "${roles[$i]}" == timer ]]; then
            case "$prior_enabled" in
                enabled)
                    if ! timeout -k 1 5 systemctl disable "$unit"; then
                        print_error "Cannot disable timer ($unit); reset incomplete; configuration is preserved"
                        return 1
                    fi
                    stopped_enabled_states[$i]=disabled
                    ;;
                enabled-runtime)
                    if ! timeout -k 1 5 systemctl disable --runtime "$unit"; then
                        print_error "Cannot disable runtime timer ($unit); reset incomplete; configuration is preserved"
                        return 1
                    fi
                    stopped_enabled_states[$i]=disabled
                    ;;
            esac
        fi
        confirm_pulse_reset_unit_stopped "$unit" "${loads[$i]}" "${stopped_enabled_states[$i]}" || return 1
    done
    # Recheck ALL writers immediately before deletion, including an earlier timer
    # that reactivated or an updater that appeared while the server was stopping.
    for i in "${!units[@]}"; do
        confirm_pulse_reset_unit_stopped "${units[$i]}" "${loads[$i]}" "${stopped_enabled_states[$i]}" || return 1
    done

    echo "Removing configuration and data..."
    if ! rm -rf -- "$CONFIG_DIR"/*; then
        print_error "Configuration deletion failed; reset incomplete. Services and timers remain stopped; inspect retained data before recovery."
        return 1
    fi

    # Restore only previously active servers. A prior inactive/failed service is
    # not consent to start it. Interrupted updater jobs are deliberately not replayed.
    for i in "${!units[@]}"; do
        unit="${units[$i]}"
        [[ "${roles[$i]}" == server ]] || continue
        if [[ "${active_states[$i]}" == "active" ]]; then
            if ! timeout -k 1 5 systemctl start "$unit"; then
                print_error "Cannot restart prior-active Pulse ($unit); reset incomplete. Timers remain stopped; reconcile service state before recovery."
                return 1
            fi
            read_pulse_reset_unit_state "$unit" || return 1
            if [[ "$PULSE_RESET_ACTIVE" != "active" || "$PULSE_RESET_LOAD" != "${loads[$i]}" || "$PULSE_RESET_ENABLED" != "${enabled_states[$i]}" ]]; then
                print_error "Prior-active Pulse restoration is unconfirmed ($unit); reset incomplete. Timers remain stopped."
                return 1
            fi
        fi
    done
    for i in "${!units[@]}"; do
        unit="${units[$i]}"
        [[ "${roles[$i]}" == timer ]] || continue
        case "${enabled_states[$i]}" in
            enabled) timeout -k 1 5 systemctl enable "$unit" || { print_error "Timer enablement restoration failed ($unit); reset incomplete"; return 1; } ;;
            enabled-runtime) timeout -k 1 5 systemctl enable --runtime "$unit" || { print_error "Runtime timer enablement restoration failed ($unit); reset incomplete"; return 1; } ;;
        esac
        confirm_pulse_reset_unit_stopped "$unit" "${loads[$i]}" "${enabled_states[$i]}" || return 1
        if [[ "${active_states[$i]}" == "active" ]]; then
            timeout -k 1 5 systemctl start "$unit" || { print_error "Timer restart failed ($unit); reset incomplete"; return 1; }
        fi
    done
    # Completion requires actual readback, not successful start/enable commands.
    for i in "${!units[@]}"; do
        unit="${units[$i]}"
        read_pulse_reset_unit_state "$unit" || return 1
        local expected_active="${active_states[$i]}"
        [[ "${roles[$i]}" != updater && "$expected_active" != "failed" ]] || expected_active=inactive
        if [[ "$PULSE_RESET_ACTIVE" != "$expected_active" || "$PULSE_RESET_LOAD" != "${loads[$i]}" || "$PULSE_RESET_ENABLED" != "${enabled_states[$i]}" ]]; then
            print_error "Final unit restoration is unconfirmed ($unit); reset incomplete. Reconcile service state before recovery."
            return 1
        fi
        if [[ "${roles[$i]}" == updater && "${active_states[$i]}" == "active" ]]; then
            print_info "Interrupted updater ($unit) remains stopped and was not replayed."
        fi
    done
    echo
    echo -e "\033[0;32m✓ Pulse configuration has been reset; prior-active server and timer states are restored\033[0m"
    exit 0
}

# When sourced (e.g. by tests) rather than executed, define the functions above
# but do not run the installer. BASH_SOURCE is empty when the script is piped to
# bash (curl ... | bash), which is an execution and not a source, so guard the
# lookup with a default to avoid an "unbound variable" abort under `set -u`.
if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "$0" ]]; then
    return 0
fi

# Parse command line arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        --uninstall)
            uninstall_pulse
            ;;
        --reset)
            reset_pulse
            ;;
        --rc|--pre|--prerelease)
            FORCE_CHANNEL="rc"
            shift
            ;;
        --stable)
            FORCE_CHANNEL="stable"
            shift
            ;;
        --version)
            if [[ $# -lt 2 ]] || [[ -z "${2:-}" ]] || [[ "$2" =~ ^-- ]]; then
                print_error "Missing value for --version"
                echo "Use --help for usage information"
                exit 1
            fi
            FORCE_VERSION="$2"
            shift 2
            ;;
        --archive)
            if [[ $# -lt 2 ]] || [[ -z "${2:-}" ]] || [[ "$2" =~ ^-- ]]; then
                print_error "--archive requires a local .tar.gz path"
                echo "Use --help for usage information"
                exit 1
            fi
            ARCHIVE_OVERRIDE="$2"
            shift 2
            ;;
        --archive=*)
            ARCHIVE_OVERRIDE="${1#*=}"
            if [[ -z "$ARCHIVE_OVERRIDE" ]]; then
                print_error "--archive requires a local .tar.gz path"
                echo "Use --help for usage information"
                exit 1
            fi
            shift
            ;;
        --in-container)
            IN_CONTAINER=true
            # Check if this is a Docker container (multiple detection methods)
            if [[ -f /.dockerenv ]] || \
               grep -q docker /proc/1/cgroup 2>/dev/null || \
               grep -q docker /proc/self/cgroup 2>/dev/null || \
               [[ -f /run/.containerenv ]] || \
               [[ "${container:-}" == "docker" ]]; then
                IN_DOCKER=true
            fi
            shift
            ;;
        --enable-auto-updates)
            ENABLE_AUTO_UPDATES=true
            AUTO_UPDATE_CHOICE_EXPLICIT=true
            shift
            ;;
        --disable-auto-updates)
            ENABLE_AUTO_UPDATES=false
            AUTO_UPDATE_CHOICE_EXPLICIT=true
            shift
            ;;
        --skip-upgrade-preflight)
            SKIP_UPGRADE_PREFLIGHT=true
            shift
            ;;
        --repair-auto-update-units)
            # Internal re-entry point for migrate_auto_update_assets_outside_sandbox:
            # rewrite the auto-update helper and units, nothing else. Deliberately
            # undocumented in --help; it exists so the transient systemd unit can
            # run this installer outside the update sandbox that blocks those writes.
            if install_auto_update_assets; then
                exit 0
            fi
            exit 1
            ;;
        --source|--from-source|--branch)
            BUILD_FROM_SOURCE=true
            # Optional: specify branch
            if [[ $# -gt 1 ]] && [[ -n "$2" ]] && [[ ! "$2" =~ ^-- ]]; then
                SOURCE_BRANCH="$2"
                shift 2
            else
                SOURCE_BRANCH="main"
                shift
            fi
            ;;
        -h|--help)
            echo "Usage: $0 [OPTIONS]"
            echo ""
            echo "Installation options:"
            echo "  --rc, --pre        Install latest prerelease preview version"
            echo "  --stable           Install latest stable version (default)"
            echo "  --version VERSION  Install specific version (e.g., v4.4.0-rc.1)"
            echo "  --archive PATH     Install from a local Pulse release tarball"
            echo "                     Requires PATH.sshsig sidecar from the canonical release pipeline."
            echo "  --source [BRANCH]  Build and install from source (default: main)"
            echo "  --enable-auto-updates   Enable automatic stable updates (via systemd timer)"
            echo "  --disable-auto-updates  Explicitly keep automatic updates disabled"
            echo "  --skip-upgrade-preflight Skip v5 to v6 compatibility preflight"
            echo ""
            echo "Management options:"
            echo "  --reset            Reset Pulse to fresh configuration"
            echo "  --uninstall        Completely remove Pulse from system"
            echo ""
            echo "  -h, --help         Show this help message"
            exit 0
            ;;
        *)
            print_error "Unknown option: $1"
            echo "Use --help for usage information"
            exit 1
            ;;
    esac
done

if [[ "$BUILD_FROM_SOURCE" == "true" ]] && [[ -n "$ARCHIVE_OVERRIDE" ]]; then
    print_error "--archive cannot be used with --source"
    exit 1
fi

auto_detect_container_environment

# Export for use in download_pulse function
export FORCE_VERSION FORCE_CHANNEL

# Run main function
main