#!/usr/bin/env bash
###############################################################################
# XGenStack Agent Manager — Unified Install / Update / Remove
#
# One command for everything:
#
#   Fresh install:
#     curl -sSL https://xgenstack.com/agent | bash -s -- install \
#         --api-url https://xgenstack.com \
#         --enroll-token TOKEN --server-id ID
#
#   Update (auto-detects existing install):
#     curl -sSL https://xgenstack.com/agent | bash -s -- update
#
#   Remove:
#     curl -sSL https://xgenstack.com/agent | bash -s -- remove
#     curl -sSL https://xgenstack.com/agent | bash -s -- remove --purge
#
#   Status:
#     curl -sSL https://xgenstack.com/agent | bash -s -- status
#
# Or just:
#     curl -sSL https://xgenstack.com/agent | bash
#   (auto-detects: updates if installed, shows help if not)
#
###############################################################################
set -euo pipefail

SCRIPT_VERSION="1.0.2"
_BIN="/usr/local/bin/xgs-agent"
_UPDATE_BIN="/usr/local/bin/xgs-update"
_CONF_DIR="/etc/xgenstack"
_DATA="/var/lib/xgs"
_LOGS="/var/log/xgenstack"
_APPS="/opt/xgs/apps"
_SVC="xgs-agent"
_SVC_FILE="/etc/systemd/system/xgs-agent.service"
_ENV="/etc/xgenstack/agent.env"
_VER_FILE="/etc/xgenstack/version"
_LOCK="/var/run/xgs-update.lock"
_BAK="/tmp/xgs-backup-$$"
_PLAT="${XGS_PLATFORM_URL:-https://CONTROL-PLANE-HOST}"
_UPD_SVC="/etc/systemd/system/xgs-update.service"
_UPD_TMR="/etc/systemd/system/xgs-update.timer"
_WD_BIN="/usr/local/bin/xgs-watchdog"
_WD_SVC="xgs-watchdog"
_WD_SVC_FILE="/etc/systemd/system/xgs-watchdog.service"

# ---------------------------------------------------------------------------
# Colors & Output
# ---------------------------------------------------------------------------
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'
DIM='\033[2m'; NC='\033[0m'

info()    { echo -e "${BLUE}  [INFO]${NC}  $*"; }
ok()      { echo -e "${GREEN}  [ OK ]${NC}  $*"; }
warn()    { echo -e "${YELLOW}  [WARN]${NC}  $*"; }
err()     { echo -e "${RED}  [FAIL]${NC}  $*"; }
step()    { echo -e "\n${BOLD}${CYAN}  ▸ $*${NC}"; }
banner()  {
    echo ""
    echo -e "${BOLD}${GREEN}  ╔══════════════════════════════════════════╗${NC}"
    echo -e "${BOLD}${GREEN}  ║     XGenStack Agent Manager v${SCRIPT_VERSION}       ║${NC}"
    echo -e "${BOLD}${GREEN}  ╚══════════════════════════════════════════╝${NC}"
    echo ""
}

die() { err "$*"; exit 1; }

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
require_root() {
    [ "$(id -u)" -eq 0 ] || die "Must be run as root (use sudo)"
}

is_installed() {
    [ -f "${_BIN}" ] && [ -f "${_ENV}" ]
}

is_running() {
    systemctl is-active --quiet "${_SVC}" 2>/dev/null
}

get_current_version() {
    if [ -f "${_VER_FILE}" ]; then
        tr -d '[:space:]' < "${_VER_FILE}" 2>/dev/null
    elif [ -x "${_BIN}" ]; then
        timeout 3 "${_BIN}" --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown"
    else
        echo "not installed"
    fi
}

get_arch_label() {
    local arch
    arch=$(uname -m)
    case "${arch}" in
        x86_64)  echo "amd64" ;;
        aarch64) echo "arm64" ;;
        armv7l)  echo "armv7" ;;
        *)       echo "${arch}" ;;
    esac
}

get_download_url() {
    local api_url="${1}"
    local sys arch
    sys=$(uname -s | tr '[:upper:]' '[:lower:]')
    arch=$(get_arch_label)
    echo "${api_url}/downloads/agent/${sys}-${arch}"
}

# Load specific vars from agent.env without polluting script namespace
load_env() {
    if [ ! -f "${_ENV}" ]; then
        return 1
    fi
    # Only extract the vars we need
    _E_API_URL=$(grep -E '^API_URL=' "${_ENV}" | head -1 | cut -d= -f2-)
    _E_AGENT_KEY=$(grep -E '^AGENT_KEY=' "${_ENV}" | head -1 | cut -d= -f2-)
    _E_NODE_ID=$(grep -E '^NODE_ID=' "${_ENV}" | head -1 | cut -d= -f2-)
    _E_SERVER_ID=$(grep -E '^SERVER_ID=' "${_ENV}" | head -1 | cut -d= -f2-)
    _E_SIGNING_PUBKEY=$(grep -E '^AGENT_SIGNING_PUBKEY=' "${_ENV}" | head -1 | cut -d= -f2-)
    return 0
}

# Verify the downloaded agent against the enrollment-pinned Ed25519 key. The
# manifest binds OS, architecture, version and SHA-256; a compromised download
# endpoint cannot replace the binary without the signing identity.
verify_agent_release() {
    local api_url="$1" binary="$2" expected_version="$3" expected_sha="$4"
    local pubkey="${_E_SIGNING_PUBKEY:-}"
    if [ -z "${pubkey}" ]; then
        die "Enrollment-pinned signing key is missing; refusing checksum-only agent update"
    fi
    command -v openssl >/dev/null || die "openssl is required for signed agent updates"
    command -v base64 >/dev/null || die "base64 is required for signed agent updates"
    local os_name arch manifest version manifest_sha manifest_size manifest_os manifest_arch signed_message signature key_id
    os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
    arch=$(get_arch_label)
    manifest=$(curl -fsS --connect-timeout 10 --max-time 30 "${api_url}/api/v1/agents/manifest?os=${os_name}&arch=${arch}") || die "Cannot fetch signed agent manifest"
    version=$(printf '%s' "${manifest}" | jq -er '.data.version') || die "Signed manifest has no version"
    manifest_sha=$(printf '%s' "${manifest}" | jq -er '.data.sha256') || die "Signed manifest has no SHA-256"
    manifest_size=$(printf '%s' "${manifest}" | jq -er '.data.size') || die "Signed manifest has no size"
    manifest_os=$(printf '%s' "${manifest}" | jq -er '.data.os') || die "Signed manifest has no OS"
    manifest_arch=$(printf '%s' "${manifest}" | jq -er '.data.arch') || die "Signed manifest has no architecture"
    signed_message=$(printf '%s' "${manifest}" | jq -er '.data.signed_message') || die "Signed manifest has no message"
    signature=$(printf '%s' "${manifest}" | jq -er '.data.signature') || die "Signed manifest has no signature"
    key_id=$(printf '%s' "${manifest}" | jq -er '.data.key_id') || die "Signed manifest has no key ID"
    [ "${version}" = "${expected_version}" ] || die "Signed manifest version mismatch"
    [ "${manifest_sha}" = "${expected_sha}" ] || die "Signed manifest checksum mismatch"
    [ "${manifest_size}" -eq "$(wc -c < "${binary}")" ] || die "Signed manifest binary size mismatch"
    [ "${manifest_os}" = "${os_name}" ] && [ "${manifest_arch}" = "${arch}" ] || die "Signed manifest platform mismatch"
    local canonical
    canonical=$(printf 'xgs-agent-manifest\nv1\n%s-%s\n%s\n%s' "${os_name}" "${arch}" "${version}" "${manifest_sha}")
    [ "${signed_message}" = "${canonical}" ] || die "Signed manifest canonical message mismatch"

    local verify_dir raw_key pub_der sig_file msg_file derived_key_id
    verify_dir=$(mktemp -d)
    raw_key="${verify_dir}/key.raw"; pub_der="${verify_dir}/key.der"
    sig_file="${verify_dir}/signature"; msg_file="${verify_dir}/message"
    printf '%s' "${pubkey}" | base64 -d > "${raw_key}" 2>/dev/null || { rm -rf "${verify_dir}"; die "Pinned signing key is malformed"; }
    [ "$(wc -c < "${raw_key}")" -eq 32 ] || { rm -rf "${verify_dir}"; die "Pinned signing key has invalid length"; }
    derived_key_id=$(sha256sum "${raw_key}" | cut -c1-16)
    [ "${derived_key_id}" = "${key_id}" ] || { rm -rf "${verify_dir}"; die "Signed manifest key ID does not match enrolled key"; }
    printf '\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00' > "${pub_der}"
    cat "${raw_key}" >> "${pub_der}"
    printf '%s' "${signature}" | base64 -d > "${sig_file}" 2>/dev/null || { rm -rf "${verify_dir}"; die "Manifest signature is malformed"; }
    printf '%s' "${canonical}" > "${msg_file}"
    openssl pkeyutl -verify -pubin -inkey "${pub_der}" -keyform DER -rawin -in "${msg_file}" -sigfile "${sig_file}" >/dev/null 2>&1 || {
        rm -rf "${verify_dir}"; die "Agent binary signature verification failed"
    }
    rm -rf "${verify_dir}"
    ok "Ed25519 release signature verified"
}

# Validate binary is ELF without requiring 'file' command
is_elf_binary() {
    local path="$1"
    # Check ELF magic bytes: 0x7f 'E' 'L' 'F'
    local magic
    magic=$(xxd -l 4 -p "${path}" 2>/dev/null || od -A n -t x1 -N 4 "${path}" 2>/dev/null | tr -d ' ')
    [ "${magic}" = "7f454c46" ]
}

acquire_lock() {
    if [ -f "${_LOCK}" ]; then
        local pid
        pid=$(cat "${_LOCK}" 2>/dev/null || echo "")
        if [ -n "${pid}" ] && kill -0 "${pid}" 2>/dev/null; then
            die "Another operation is running (PID ${pid}). Wait or remove ${_LOCK}"
        fi
        rm -f "${_LOCK}"
    fi
    echo $$ > "${_LOCK}"
}

release_lock() {
    rm -f "${_LOCK}" 2>/dev/null || true
}

cleanup() {
    release_lock
    rm -rf "${_BAK}" 2>/dev/null || true
}
trap cleanup EXIT

# ---------------------------------------------------------------------------
# COMMAND: status
# ---------------------------------------------------------------------------
cmd_status() {
    banner
    if ! is_installed; then
        info "Agent is ${RED}not installed${NC}"
        echo ""
        echo "  Install with:"
        echo "    curl -sSL ${_PLAT}/agent | bash -s -- install \\"
        echo "        --api-url ${_PLAT} \\"
        echo "        --enroll-token TOKEN --server-id ID"
        echo ""
        return 0
    fi

    local ver
    ver=$(get_current_version)

    echo -e "  ${CYAN}Version:${NC}    ${ver}"
    echo -e "  ${CYAN}Binary:${NC}     ${_BIN}"
    echo -e "  ${CYAN}Config:${NC}     ${_ENV}"

    if is_running; then
        echo -e "  ${CYAN}Status:${NC}     ${GREEN}running${NC}"
        local pid
        pid=$(systemctl show -p MainPID --value "${_SVC}" 2>/dev/null || echo "?")
        echo -e "  ${CYAN}PID:${NC}        ${pid}"
        local uptime_val
        uptime_val=$(systemctl show -p ActiveEnterTimestamp --value "${_SVC}" 2>/dev/null || echo "?")
        echo -e "  ${CYAN}Since:${NC}      ${uptime_val}"
    else
        echo -e "  ${CYAN}Status:${NC}     ${RED}stopped${NC}"
    fi

    if load_env 2>/dev/null; then
        echo -e "  ${CYAN}API URL:${NC}    ${_E_API_URL:-unknown}"
        echo -e "  ${CYAN}Node ID:${NC}    ${_E_NODE_ID:-unknown}"
        echo -e "  ${CYAN}Server ID:${NC}  ${_E_SERVER_ID:-unknown}"
    fi

    # Check watchdog
    if systemctl is-active --quiet xgs-watchdog 2>/dev/null; then
        echo -e "  ${CYAN}Watchdog:${NC}    ${GREEN}active${NC}"
    else
        echo -e "  ${CYAN}Watchdog:${NC}    ${YELLOW}inactive${NC}"
    fi

    # Check update timer
    if systemctl is-active --quiet xgs-update.timer 2>/dev/null; then
        local next
        next=$(systemctl show -p NextElapseUSecRealtime --value xgs-update.timer 2>/dev/null || echo "?")
        echo -e "  ${CYAN}Auto-update:${NC} active (next: ${next})"
    else
        echo -e "  ${CYAN}Auto-update:${NC} ${YELLOW}inactive${NC}"
    fi

    echo ""
    echo -e "  ${DIM}Logs: journalctl -u ${_SVC} -f${NC}"
    echo ""
}

# ---------------------------------------------------------------------------
# COMMAND: update
# ---------------------------------------------------------------------------
cmd_update() {
    require_root
    banner

    if ! is_installed; then
        die "Agent is not installed. Use 'install' command first."
    fi

    acquire_lock
    load_env || die "Cannot load ${_ENV}"

    local api_url="${_E_API_URL:-}"
    [ -n "${api_url}" ] || die "API_URL not set in ${_ENV}"

    local current_ver
    current_ver=$(get_current_version)
    step "Current version: ${current_ver}"

    # --- Check for new version ---
    step "Checking for updates..."
    local agent_key="${_E_AGENT_KEY:-}"
    local node_id="${_E_NODE_ID:-}"
	api_url="${api_url%/}"
	# From this point every instruction and generated artifact must name the
	# control plane that enrolled this node, never a vendor installation.
	_PLAT="${api_url}"

    local latest_ver=""
    local download_url=""
    local checksum=""

    # Try the version endpoint
    local ver_resp=""
    ver_resp=$(curl -sS --connect-timeout 10 --max-time 30 \
        -H "X-Agent-Key: ${agent_key}" \
        -H "X-Node-ID: ${node_id}" \
        "${api_url}/api/v1/agents/version" 2>/dev/null) || true

    if [ -n "${ver_resp}" ]; then
        latest_ver=$(echo "${ver_resp}" | jq -r '.data.version // .version // empty' 2>/dev/null || true)
        download_url=$(echo "${ver_resp}" | jq -r '.data.download_url // .download_url // empty' 2>/dev/null || true)
        checksum=$(echo "${ver_resp}" | jq -r '.data.checksum // .checksum // empty' 2>/dev/null || true)
    fi

    if [ -z "${latest_ver}" ]; then
        die "Could not determine the signed release version from API"
    fi

    local force="${FORCE:-false}"
    if [ "${force}" = false ] && [ "${latest_ver}" != "latest" ] && [ "${latest_ver}" = "${current_ver}" ]; then
        ok "Already up to date (${current_ver}). Use --force to re-download."
        return 0
    fi

    info "Updating: ${current_ver} → ${latest_ver}"

    # --- Download new binary ---
    if [ -z "${download_url}" ]; then
        download_url=$(get_download_url "${api_url}")
    fi

    step "Downloading new binary..."
    local tmp_bin="/tmp/xgs-agent-new-$$"

    # Default curl progress meter shows percent, total size, downloaded
    # bytes, average + current speed, and ETA — written to stderr so it
    # stays visible even when stdout is captured. After the download
    # finishes we print our own summary line with size + avg speed + time
    # taken from -w, formatted in MB / MB/s for readability.
    local http_code size_bytes speed_bps time_total
    if [ -t 2 ]; then
        # TTY: let curl's progress meter render to stderr.
        local stats
        stats=$(curl -fS \
            -o "${tmp_bin}" \
            --connect-timeout 10 --max-time 120 \
            -H "X-Agent-Key: ${agent_key}" \
            -H "X-Node-ID: ${node_id}" \
            -w '%{http_code} %{size_download} %{speed_download} %{time_total}' \
            "${download_url}" 2>&2 || echo "000 0 0 0")
        read -r http_code size_bytes speed_bps time_total <<< "${stats}"
    else
        # Non-TTY (piped/systemd): silent, single capture.
        local stats
        stats=$(curl -fsS \
            -o "${tmp_bin}" \
            --connect-timeout 10 --max-time 120 \
            -H "X-Agent-Key: ${agent_key}" \
            -H "X-Node-ID: ${node_id}" \
            -w '%{http_code} %{size_download} %{speed_download} %{time_total}' \
            "${download_url}" || echo "000 0 0 0")
        read -r http_code size_bytes speed_bps time_total <<< "${stats}"
    fi

    # Print formatted summary regardless of TTY (helpful in logs too).
    if [ -n "${size_bytes}" ] && [ "${size_bytes}" != "0" ]; then
        local sz_mb spd_mbs
        sz_mb=$(awk "BEGIN{printf \"%.2f\", ${size_bytes}/1048576}")
        spd_mbs=$(awk "BEGIN{printf \"%.2f\", ${speed_bps}/1048576}")
        echo -e "    ${DIM}${sz_mb} MB at ${spd_mbs} MB/s in ${time_total}s${NC}"
    fi

    if [ "${http_code}" != "200" ] || [ ! -s "${tmp_bin}" ]; then
        rm -f "${tmp_bin}"
        die "Download failed (HTTP ${http_code}) from ${download_url}"
    fi

    local size size_human
    size=$(wc -c < "${tmp_bin}")
    if [ "${size}" -ge 1048576 ]; then
        size_human="$((size / 1048576)) MB ($(printf "%'d" "${size}") bytes)"
    else
        size_human="$(printf "%'d" "${size}") bytes"
    fi
    ok "Downloaded ${size_human}"

    # --- Verify checksum ---
    if [ -n "${checksum}" ]; then
        step "Verifying checksum..."
        local actual
        actual=$(sha256sum "${tmp_bin}" | awk '{print $1}')
        if [ "${actual}" != "${checksum}" ]; then
            rm -f "${tmp_bin}"
            die "Checksum mismatch! Expected: ${checksum} Got: ${actual}"
        fi
        ok "Checksum verified"
    else
        rm -f "${tmp_bin}"
        die "Update response omitted the required binary checksum"
    fi

    # --- Verify it's a valid binary ---
    chmod +x "${tmp_bin}"
    if ! is_elf_binary "${tmp_bin}"; then
        rm -f "${tmp_bin}"
        die "Downloaded file is not a valid ELF binary"
    fi
    ok "Binary validated (ELF)"

    verify_agent_release "${api_url}" "${tmp_bin}" "${latest_ver}" "${checksum}"

    # --- Backup current binary ---
    step "Backing up current binary..."
    mkdir -p "${_BAK}"
    if [ -f "${_BIN}" ]; then
        cp "${_BIN}" "${_BAK}/xgs-agent.bak"
        ok "Backed up to ${_BAK}/xgs-agent.bak"
    fi
    if [ -f "${_VER_FILE}" ]; then
        cp "${_VER_FILE}" "${_BAK}/version.bak"
    fi

    # --- Stop service ---
    step "Stopping agent..."
    systemctl stop "${_SVC}" 2>/dev/null || true
    sleep 1

    # Double-check it's actually stopped (kill lingering process)
    local old_pid=""
    old_pid=$(systemctl show -p MainPID --value "${_SVC}" 2>/dev/null || echo "0")
    if [ "${old_pid}" != "0" ] && [ -n "${old_pid}" ] && kill -0 "${old_pid}" 2>/dev/null; then
        warn "Service still running (PID ${old_pid}), sending SIGKILL..."
        kill -9 "${old_pid}" 2>/dev/null || true
        sleep 1
    fi
    ok "Agent stopped"

    # --- Replace binary ---
    step "Installing new binary..."
    mv "${tmp_bin}" "${_BIN}"
    chmod +x "${_BIN}"
    ok "Binary replaced"

    # --- Update version file ---
    if [ "${latest_ver}" != "latest" ]; then
        echo "${latest_ver}" > "${_VER_FILE}"
    else
        echo "0.0.0" > "${_VER_FILE}"
    fi

    # --- Reinstall auto-update timer (keeps it current) ---
    step "Updating auto-update timer..."
    install_update_timer "${api_url}"

    # --- Update watchdog ---
    step "Updating watchdog..."
    install_watchdog "${api_url}"

    # --- Install Nixpacks (v2.5 Tier 1 detector) — managed only ---
    # Read the posture from the existing agent.env. Missing ⇒ managed (matches
    # the agent binary), so existing managed boxes keep nixpacks on update;
    # observe boxes never get it.
    local _mode
    _mode=$(grep -E '^XGS_AGENT_PROFILE=' "${_ENV}" 2>/dev/null | head -1 | cut -d= -f2- || true)
    [ -n "${_mode}" ] || _mode=$(grep -E '^XGS_AGENT_MODE=' "${_ENV}" 2>/dev/null | head -1 | cut -d= -f2- || true)
    if [ "${_mode}" != "observe" ]; then
        step "Ensuring nixpacks is installed..."
        install_nixpacks

		# Managed agents are host control planes: provisioning legitimately writes
		# package databases, service users, /usr, nginx, systemd and application
		# trees. A previous installer revision combined ProtectSystem=strict with a
		# partial allowlist; that made useradd see /etc/passwd as read-only and broke
		# migrations only after artifact transfer. Reconcile old units on every
		# update so the fix reaches already-enrolled servers without reinstalling.
		if grep -q '^ProtectSystem=strict$' "${_SVC_FILE}" 2>/dev/null; then
			sed -i 's/^ProtectSystem=strict$/ProtectSystem=false/; s/^NoNewPrivileges=true$/NoNewPrivileges=false/; /^ReadWritePaths=/d' "${_SVC_FILE}"
			systemctl daemon-reload
			ok "Managed-agent host authority restored in systemd sandbox"
		fi
    fi

    # Ollama/Phi-3 RCA model install removed — too heavy for typical VPS.
    # RCA uses the agent's built-in rule-based engine.

    # --- Start service ---
    step "Starting agent..."
    systemctl start "${_SVC}"

    # --- Health check ---
    step "Health check..."
    local retries=5
    local healthy=false
    for i in $(seq 1 ${retries}); do
        sleep 2
        if systemctl is-active --quiet "${_SVC}"; then
            healthy=true
            break
        fi
        warn "Attempt ${i}/${retries}: not running yet..."
    done

    if [ "${healthy}" = true ]; then
        ok "Agent is running!"
        local new_ver
        new_ver=$(get_current_version)
        echo ""
        echo -e "  ${GREEN}${BOLD}Update successful: ${current_ver} → ${new_ver}${NC}"
        echo -e "  ${DIM}Logs: journalctl -u ${_SVC} -f${NC}"
        echo ""

        # Clean up backup
        rm -rf "${_BAK}"
    else
        # --- ROLLBACK ---
        err "Agent failed to start after update!"
        echo ""

        if [ -f "${_BAK}/xgs-agent.bak" ]; then
            step "Rolling back to previous version..."
            mv "${_BAK}/xgs-agent.bak" "${_BIN}"
            chmod +x "${_BIN}"
            if [ -f "${_BAK}/version.bak" ]; then
                cp "${_BAK}/version.bak" "${_VER_FILE}"
            fi

            systemctl start "${_SVC}" 2>/dev/null || true
            sleep 2

            if systemctl is-active --quiet "${_SVC}"; then
                ok "Rollback successful. Running previous version."
            else
                err "Rollback failed! Agent is not running."
                err "Manual fix needed: journalctl -u ${_SVC} --no-pager -n 50"
            fi
        else
            err "No backup available for rollback."
            err "Manual fix needed: journalctl -u ${_SVC} --no-pager -n 50"
        fi
        exit 1
    fi
}

# ---------------------------------------------------------------------------
# COMMAND: install
# ---------------------------------------------------------------------------
cmd_install() {
    require_root
    banner

    # Parse install-specific arguments.
    # --mode: observe (default) = pure sensor, minimal footprint, no host
    # mutation; managed = full provisioning (build deps, nixpacks, startup
    # self-heal/firewall/vhosts). See docs/observe-mode-plan.
    local api_url="" enroll_token="" server_id="" binary_sha256="" force=false mode="observe"

    while [ $# -gt 0 ]; do
        case "$1" in
            --api-url=*)       api_url="${1#*=}"; shift ;;
            --api-url)         api_url="${2:-}"; shift 2 ;;
            --enroll-token=*)  enroll_token="${1#*=}"; shift ;;
            --enroll-token)    enroll_token="${2:-}"; shift 2 ;;
            --token=*)         enroll_token="${1#*=}"; shift ;;
            --token)           enroll_token="${2:-}"; shift 2 ;;
            --server-id=*)     server_id="${1#*=}"; shift ;;
            --server-id)       server_id="${2:-}"; shift 2 ;;
            --binary-sha256=*) binary_sha256="${1#*=}"; shift ;;
            --binary-sha256)   binary_sha256="${2:-}"; shift 2 ;;
            --profile=*)       mode="${1#*=}"; shift ;;
            --profile)         mode="${2:-}"; shift 2 ;;
            --mode=*)          mode="${1#*=}"; shift ;;
            --mode)            mode="${2:-}"; shift 2 ;;
            --managed)         mode="managed"; shift ;;
            --observe)         mode="observe"; shift ;;
            --force)           force=true; shift ;;
            *)                 shift ;;
        esac
    done

    # Normalise mode — anything other than the literal "managed" is observe.
    [ "${mode}" = "managed" ] || mode="observe"

    # Validate
    [ -n "${api_url}" ]       || die "Missing --api-url"
    [ -n "${enroll_token}" ]  || die "Missing --enroll-token"
    [ -n "${server_id}" ]     || die "Missing --server-id"
	if [ -n "${binary_sha256}" ] && ! printf '%s' "${binary_sha256}" | grep -Eq '^[a-f0-9]{64}$'; then
		die "--binary-sha256 must be exactly 64 lowercase hexadecimal characters"
	fi

    api_url="${api_url%/}"

    # Fetch the platform's "latest agent version" pointer so we can
    # write the correct value into /etc/xgenstack/version AND report
    # it during enrollment. Without this the script hard-coded 0.3.0
    # and every fresh install reported as ancient.
    LATEST_AGENT_VER=$(curl -sS --connect-timeout 5 --max-time 10 \
        -H "Accept: application/json" \
        "${api_url}/api/v1/agents/version" 2>/dev/null \
        | { if command -v jq >/dev/null 2>&1; then
                jq -r '.data.version // .version // empty' 2>/dev/null
            else
                # Version lookup happens BEFORE prerequisite installation. A
                # fresh host therefore cannot depend on jq yet; extract the
                # tightly constrained semver value with POSIX tools and let the
                # authenticated heartbeat remain authoritative after startup.
                sed -n 's/.*"version":"\([0-9][0-9.]*\)".*/\1/p' | head -1
            fi; } || echo "")
    if [ -z "${LATEST_AGENT_VER}" ]; then
        warn "Could not fetch latest version from platform — defaulting to 0.0.0 (heartbeat will trigger an upgrade)"
        LATEST_AGENT_VER="0.0.0"
    fi

    # Existing install? An `install` ALWAYS carries a fresh one-time --enroll-token
    # (required above), so reaching here means the operator is (re-)enrolling this
    # box — e.g. re-adding a server whose previous agent wasn't fully removed. Do
    # NOT bail with "already installed"; re-enroll cleanly (the "Clean any previous
    # installation" block below stops + wipes the old agent first). This fixes
    # "re-add says already installed even though I removed it".
    if is_installed && [ "${force}" = false ] && is_running; then
        warn "Agent already present on this host — re-enrolling with the new token (old install will be replaced)."
    fi

    # Clean any previous install
    if is_installed || [ -f "${_BIN}" ]; then
        step "Cleaning previous installation..."
        systemctl stop "${_SVC}" 2>/dev/null || true
        systemctl disable "${_SVC}" 2>/dev/null || true
        rm -f "${_BIN}" "${_UPDATE_BIN}"
        rm -f "${_SVC_FILE}" "${_UPD_SVC}" "${_UPD_TMR}"
        rm -f "${_LOCK}"
        systemctl daemon-reload 2>/dev/null || true
        ok "Previous install cleaned"
    fi

    acquire_lock

    # --- Detect OS ---
    step "Detecting system..."
    local os="" os_ver="" arch=""
    if [ -f /etc/os-release ]; then
        # shellcheck source=/dev/null
        . /etc/os-release
        os="${ID}"
        os_ver="${VERSION_ID}"
    else
        die "Cannot detect OS"
    fi
    arch=$(uname -m)
    ok "${os} ${os_ver} (${arch})"

    # --- Install prerequisites ---
    # observe mode: install ONLY the tiny tools the agent itself needs
    # (curl/jq/ca-certificates). No compiler toolchain, no image libs —
    # nothing that mutates a production box beyond the agent.
    # managed mode: ALSO install the native dev libraries that popular Node
    # packages need at deploy time (sharp/canvas/better-sqlite3/node-postgres
    # native), removing the #2 deploy-failure class (W1.1). ~150 MB one-time.
    if [ "${mode}" = "managed" ]; then
        step "Installing prerequisites + native build deps (managed)..."
        if command -v apt-get &>/dev/null; then
            DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>/dev/null
            DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
                curl jq ca-certificates git \
                build-essential pkg-config python3 \
                libvips-dev libcairo2-dev libpango1.0-dev \
                libjpeg-dev libgif-dev librsvg2-dev \
                libsqlite3-dev libpq-dev \
                2>/dev/null || warn "some native deps may not have installed (non-fatal; deploys will retry)"
        elif command -v dnf &>/dev/null; then
            dnf install -y -q \
                curl jq ca-certificates git \
                gcc gcc-c++ make pkgconf-pkg-config python3 \
                vips-devel cairo-devel pango-devel \
                libjpeg-turbo-devel giflib-devel librsvg2-devel \
                sqlite-devel libpq-devel \
                2>/dev/null || warn "some native deps may not have installed"
        elif command -v yum &>/dev/null; then
            yum install -y -q curl jq ca-certificates 2>/dev/null
        fi
        ok "Prerequisites + native build deps ready"
    else
        step "Installing minimal prerequisites (observe — curl, jq, ca-certificates)..."
        if command -v apt-get &>/dev/null; then
            DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl jq ca-certificates 2>/dev/null || warn "prereq install had warnings (non-fatal)"
        elif command -v dnf &>/dev/null; then
            dnf install -y -q curl jq ca-certificates 2>/dev/null || true
        elif command -v yum &>/dev/null; then
            yum install -y -q curl jq ca-certificates 2>/dev/null || true
        fi
        ok "Minimal prerequisites ready (observe mode — no build toolchain installed)"
    fi

    # --- Create directories ---
    step "Creating directories..."
    for dir in "${_CONF_DIR}" "${_DATA}" "${_LOGS}" "${_APPS}"; do
        mkdir -p "${dir}"
    done
    chmod 750 "${_CONF_DIR}" "${_DATA}" "${_LOGS}"
    ok "Directories ready"

    # --- Download binary ---
    step "Downloading agent binary..."
    local download_url
    download_url=$(get_download_url "${api_url}")

    local tmp_bin="/tmp/xgs-agent-download-$$"
    local http_code
    http_code=$(curl -sS -w '%{http_code}' -o "${tmp_bin}" \
        --connect-timeout 15 --max-time 120 \
        "${download_url}" 2>/dev/null || echo "000")

    if [ "${http_code}" != "200" ] || [ ! -s "${tmp_bin}" ]; then
        rm -f "${tmp_bin}"
        die "Download failed (HTTP ${http_code}) from ${download_url}"
    fi
	if [ -n "${binary_sha256}" ]; then
		local downloaded_sha256
		downloaded_sha256=$(sha256sum "${tmp_bin}" | awk '{print $1}')
		if [ "${downloaded_sha256}" != "${binary_sha256}" ]; then
			rm -f "${tmp_bin}"
			die "Bootstrap binary checksum does not match the independently supplied pin"
		fi
		ok "Bootstrap binary matched independent SHA-256 pin"
	else
		warn "Bootstrap binary is protected only by HTTPS. For high-assurance enrollment, supply --binary-sha256 from an independent channel."
	fi

    chmod +x "${tmp_bin}"
    if ! is_elf_binary "${tmp_bin}"; then
        rm -f "${tmp_bin}"
        die "Downloaded file is not a valid ELF binary"
    fi

    mv "${tmp_bin}" "${_BIN}"
    chmod +x "${_BIN}"
    local size
    size=$(wc -c < "${_BIN}")
    ok "Installed ${_BIN} (${size} bytes)"

    # --- Enroll with platform ---
    step "Enrolling with platform..."
    local hostname_val ip_val machine_id_val
    hostname_val="$(hostname)"
    ip_val="$(curl -s --connect-timeout 5 ifconfig.me 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo 'unknown')"

    # A stable identity for THIS machine, so the platform can refuse to bind one
    # box to two records. /etc/machine-id is set once at system install and
    # survives reboots, hostname changes and IP changes. Where it is missing we
    # persist our own UUID rather than sending nothing, because an absent
    # identity is exactly what allowed the duplicate this prevents.
    machine_id_val=""
    if [ -r /etc/machine-id ]; then
        machine_id_val=$(tr -d ' \n' < /etc/machine-id 2>/dev/null || true)
    fi
    if [ -z "${machine_id_val}" ] && [ -r /var/lib/dbus/machine-id ]; then
        machine_id_val=$(tr -d ' \n' < /var/lib/dbus/machine-id 2>/dev/null || true)
    fi
    if [ -z "${machine_id_val}" ]; then
        mkdir -p /var/lib/xgs 2>/dev/null || true
        if [ -r /var/lib/xgs/machine-id ]; then
            machine_id_val=$(tr -d ' \n' < /var/lib/xgs/machine-id 2>/dev/null || true)
        else
            machine_id_val=$( (cat /proc/sys/kernel/random/uuid 2>/dev/null || date +%s%N) | tr -d ' \n')
            printf '%s\n' "${machine_id_val}" > /var/lib/xgs/machine-id 2>/dev/null || true
        fi
    fi

    local enroll_resp
    enroll_resp=$(curl -sS --connect-timeout 10 --max-time 30 \
        -X POST "${api_url}/api/v1/agents/enroll" \
        -H "Content-Type: application/json" \
        -d "{
            \"server_id\": \"${server_id}\",
            \"token\": \"${enroll_token}\",
            \"hostname\": \"${hostname_val}\",
            \"ip_address\": \"${ip_val}\",
            \"os\": \"${os}\",
            \"os_version\": \"${os_ver}\",
            \"arch\": \"${arch}\",
            \"agent_version\": \"${LATEST_AGENT_VER}\",
            \"machine_id\": \"${machine_id_val}\"
        }") || die "Failed to connect to ${api_url}"

    local node_id agent_key hmac_secret signing_pubkey
    node_id=$(echo "${enroll_resp}" | jq -r '.data.node_id // empty')
    agent_key=$(echo "${enroll_resp}" | jq -r '.data.agent_key // empty')
    hmac_secret=$(echo "${enroll_resp}" | jq -r '.data.hmac_secret // empty')
    signing_pubkey=$(echo "${enroll_resp}" | jq -r '.data.signing_pubkey // empty')

    if [ -z "${node_id}" ] || [ -z "${agent_key}" ]; then
        err "Enrollment failed:"
        echo "${enroll_resp}" | jq . 2>/dev/null || echo "${enroll_resp}"
        exit 1
    fi
    [ -n "${signing_pubkey}" ] || die "Enrollment response omitted the mandatory agent-signing public key"
    local signing_key_probe
    signing_key_probe=$(mktemp)
    if ! printf '%s' "${signing_pubkey}" | base64 -d > "${signing_key_probe}" 2>/dev/null || [ "$(wc -c < "${signing_key_probe}")" -ne 32 ]; then
        rm -f "${signing_key_probe}"
        die "Enrollment response contained an invalid Ed25519 signing public key"
    fi
    rm -f "${signing_key_probe}"
    command -v openssl >/dev/null 2>&1 || die "OpenSSL is required for signed agent releases"
    local signing_pin signing_pin_tmp
    signing_pin="${_CONF_DIR}/agent-signing.pub.pem"
    signing_pin_tmp=$(mktemp "${_CONF_DIR}/.agent-signing.pub.XXXXXX")
    {
        printf -- '-----BEGIN PUBLIC KEY-----\n'
        printf 'MCowBQYDK2VwAyEA%s\n' "${signing_pubkey}"
        printf -- '-----END PUBLIC KEY-----\n'
    } > "${signing_pin_tmp}"
    chmod 0644 "${signing_pin_tmp}"
    openssl pkey -pubin -in "${signing_pin_tmp}" -noout >/dev/null 2>&1 \
        || { rm -f "${signing_pin_tmp}"; die "Enrollment signing key failed OpenSSL validation"; }
    mv -f "${signing_pin_tmp}" "${signing_pin}"
    ok "Enrolled (Node: ${node_id})"

    # Save certs if provided
    local cert_pem key_pem
    cert_pem=$(echo "${enroll_resp}" | jq -r '.data.cert_pem // empty')
    key_pem=$(echo "${enroll_resp}" | jq -r '.data.key_pem // empty')
    if [ -n "${cert_pem}" ] && [ -n "${key_pem}" ]; then
        echo "${cert_pem}" > "${_DATA}/node.crt"
        echo "${key_pem}" > "${_DATA}/node.key"
        chmod 600 "${_DATA}/node.key"
    fi

    # --- Write config ---
    step "Writing configuration..."
    cat > "${_ENV}" <<ENVEOF
# XGenStack Agent Configuration
# Generated on $(date -u +"%Y-%m-%dT%H:%M:%SZ")
API_URL=${api_url}
SERVER_ID=${server_id}
NODE_ID=${node_id}
AGENT_KEY=${agent_key}
HMAC_SECRET=${hmac_secret}
AGENT_SIGNING_PUBKEY=${signing_pubkey}
AGENT_SIGNING_REQUIRED=true
# Install/runtime profile: observe = pure sensor (no host mutation); managed =
# full provisioning. The agent gates startup mutations on this. Promote with
# --profile managed at install, or change here + restart xgs-agent.
XGS_AGENT_PROFILE=${mode}
GUARDIAN_MODE=observer
DEFENDER_MODE=observer
LOG_LEVEL=info
ENVEOF
    chmod 600 "${_ENV}"
    echo "${LATEST_AGENT_VER}" > "${_VER_FILE}"
    ok "Config written to ${_ENV}"

    # --- Install systemd service ---
    step "Installing systemd service..."
    cat > "${_SVC_FILE}" <<'SVCEOF'
[Unit]
Description=XGenStack Agent
After=network-online.target
Wants=network-online.target

[Service]
# Type=notify enables the systemd watchdog protocol. The agent's
# StartSystemdWatchdog goroutine sends sd_notify(READY=1) once the WS connect
# loop is up, then pings WATCHDOG=1 every WATCHDOG_USEC/2. If a ping is
# missed (deadlock, infinite loop, exhausted goroutine pool), systemd
# kills + respawns us.
Type=notify
NotifyAccess=main
WatchdogSec=30s

EnvironmentFile=/etc/xgenstack/agent.env
ExecStart=/usr/local/bin/xgs-agent

# Standard restart policy. On crash-loop (>5 fails in 60s) systemd holds
# off and the separate xgs-watchdog binary kicks in (see scripts/xgs-watchdog.sh).
Restart=always
RestartSec=5
StartLimitInterval=60
StartLimitBurst=5

# Resource isolation. The agent itself is small at steady state (~30MB),
# but during a build it streams turbo/pnpm/next output through bufio
# scanners + WS-send buffers and the in-process line counter — easily
# 200-400MB transient. 512M MemoryMax killed the agent mid-build on
# OpenStatus (2026-05-14), which in turn killed the systemd-run scope
# hosting the build → "exit 1 with no error". 2GB is generous headroom.
# Build children run in their OWN systemd-run scope under xgs-build.slice
# (sibling, not child) so their memory does NOT count against this cap.
MemoryHigh=1G
MemoryMax=2G
CPUQuota=200%
TasksMax=infinity
LimitNOFILE=65536
LimitNPROC=4096

# Sandbox hardening. The agent runs as root by design (apt, systemctl,
# nginx vhosts, certbot, docker, iptables). NoNewPrivileges=true prevents
# child processes gaining extra capabilities via setuid binaries.
# ProtectSystem=strict makes /usr /boot /efi read-only; explicit
# ReadWritePaths re-opens only what the agent actually writes to.
# ProtectHome=false: the file manager lets users manage /root and /home.
# ProtectControlGroups=false: docker and systemctl need cgroup access.
#
# Each ReadWritePaths entry is prefixed with `-` (tolerate-if-absent). On a
# minimal fresh VPS the heavy dirs (/etc/nginx, /etc/letsencrypt, /etc/fail2ban,
# /etc/cron.*) don't exist until those components are lazy-installed on first
# use — and systemd fails the ENTIRE namespace setup (exit 226/NAMESPACE,
# agent never starts) if any listed path is missing. The `-` makes systemd
# silently skip absent paths and re-include them after they're created.
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=-/var/lib/xgs -/usr/local/bin -/tmp -/etc/nginx -/etc/letsencrypt -/etc/systemd -/etc/xgenstack -/etc/fail2ban -/etc/ssh -/etc/cron.d -/etc/cron.daily -/run/nginx.pid -/var/log/nginx
RuntimeDirectory=xgs-agent
RuntimeDirectoryMode=0750
PrivateTmp=true
RestrictSUIDSGID=true
ProtectHome=false
ProtectKernelModules=true
ProtectControlGroups=false

StandardOutput=journal
StandardError=journal
SyslogIdentifier=xgs-agent

[Install]
WantedBy=multi-user.target
SVCEOF
	# The strict filesystem allowlist above is appropriate for observe mode, but
	# a managed agent is explicitly authorized to provision the host. Keeping it
	# strict makes /etc/passwd, distro packages and runtime directories read-only.
	if [ "${mode}" != "observe" ]; then
		sed -i 's/^ProtectSystem=strict$/ProtectSystem=false/; s/^NoNewPrivileges=true$/NoNewPrivileges=false/; /^ReadWritePaths=/d' "${_SVC_FILE}"
	fi
    systemctl daemon-reload
    systemctl enable "${_SVC}" 2>/dev/null
    ok "Service installed"

    # --- Install auto-update timer ---
    step "Installing auto-update timer..."
    install_update_timer "${api_url}"

    # --- Install watchdog ---
    step "Installing watchdog service..."
    install_watchdog "${api_url}"

    # --- Install dux (disk-health engine) — required for the Disk tab ---
    step "Installing dux disk-health engine..."
    install_dux "${api_url}" "${mode}"

    # --- Install Nixpacks (v2.5 Tier 1 detector) — managed only ---
    if [ "${mode}" = "managed" ]; then
        step "Installing Nixpacks build detector..."
        install_nixpacks
    fi

    # Ollama/Phi-3 RCA model install removed — too heavy for typical VPS.
    # RCA uses the agent's built-in rule-based engine.

    # --- Start agent ---
    step "Starting agent..."
    systemctl start "${_SVC}"

    # Health check
    local retries=5 healthy=false
    for i in $(seq 1 ${retries}); do
        sleep 2
        if is_running; then
            healthy=true
            break
        fi
        warn "Waiting... (${i}/${retries})"
    done

    if [ "${healthy}" = true ]; then
        ok "Agent is running!"
    else
        err "Agent failed to start. Check: journalctl -u ${_SVC} --no-pager -n 30"
        exit 1
    fi

    # --- Summary ---
    echo ""
    echo -e "  ${BOLD}${GREEN}╔══════════════════════════════════════════╗${NC}"
    echo -e "  ${BOLD}${GREEN}║      Installation Complete!              ║${NC}"
    echo -e "  ${BOLD}${GREEN}╚══════════════════════════════════════════╝${NC}"
    echo ""
    echo -e "  ${CYAN}Node ID:${NC}     ${node_id}"
    echo -e "  ${CYAN}Server:${NC}      ${server_id}"
    echo -e "  ${CYAN}IP:${NC}          ${ip_val}"
    echo -e "  ${CYAN}OS:${NC}          ${os} ${os_ver} (${arch})"
    echo -e "  ${CYAN}Version:${NC}     ${LATEST_AGENT_VER}"
    echo ""
    echo -e "  ${DIM}Commands:${NC}"
    echo -e "    ${DIM}systemctl status ${_SVC}${NC}"
    echo -e "    ${DIM}journalctl -u ${_SVC} -f${NC}"
    echo -e "    ${DIM}curl -sSL ${_PLAT}/agent | bash -s -- update${NC}"
    echo ""
}

# ---------------------------------------------------------------------------
# COMMAND: remove
# ---------------------------------------------------------------------------
# ── doctor ───────────────────────────────────────────────────────────────────
#
# An agent that stops reporting is the failure this product cannot afford: the
# server goes dark and nobody can fix it remotely, which is the one promise the
# whole platform rests on. Every time that happened, diagnosing it meant an
# operator pasting logs to someone else and waiting.
#
# So the checks live on the machine that has the answers. Read-only by
# construction — it inspects, resolves and connects, and changes nothing — so it
# is always safe to run on a production box that is already misbehaving.
cmd_doctor() {
    banner
    local problems=0 warnings=0

    _dr_fail() { echo -e "  ${RED}  ✗${NC} $*"; problems=$((problems+1)); }
    _dr_warn() { echo -e "  ${YELLOW}  !${NC} $*"; warnings=$((warnings+1)); }
    _dr_ok()   { echo -e "  ${GREEN}  ✓${NC} $*"; }

    step "Installation"
    if is_installed; then
        _dr_ok "binary present: ${_BIN} ($(get_current_version))"
    else
        _dr_fail "agent is not installed"
        echo ""
        echo "  Install it with the command from your server's page in the interface."
        return 1
    fi

    step "Service"
    if is_running; then
        local since
        since=$(systemctl show -p ActiveEnterTimestamp --value "${_SVC}" 2>/dev/null || echo "?")
        _dr_ok "running since ${since}"
    else
        _dr_fail "service is NOT running — this is why the platform shows it offline"
        echo -e "      ${CYAN}systemctl start xgs-agent${NC}"
    fi
    if systemctl is-enabled --quiet "${_SVC}" 2>/dev/null; then
        _dr_ok "enabled at boot"
    else
        _dr_warn "not enabled at boot — it will not come back after a reboot"
        echo -e "      ${CYAN}systemctl enable xgs-agent${NC}"
    fi
    # A service that restarts repeatedly looks "running" at any instant while
    # never staying up long enough to be useful, so the count matters more than
    # the state.
    local nrestarts
    nrestarts=$(systemctl show -p NRestarts --value "${_SVC}" 2>/dev/null || echo 0)
    if [ "${nrestarts:-0}" -gt 5 ]; then
        _dr_fail "restarted ${nrestarts} times — it is crash-looping, not running"
    elif [ "${nrestarts:-0}" -gt 0 ]; then
        _dr_warn "restarted ${nrestarts} time(s)"
    fi

    step "Configuration"
    if [ -r "${_ENV}" ]; then
        local _api _srv _node _prof
        _api=$(grep -E '^API_URL=' "${_ENV}" 2>/dev/null | head -1 | cut -d= -f2- || true)
        _srv=$(grep -E '^SERVER_ID=' "${_ENV}" 2>/dev/null | head -1 | cut -d= -f2- || true)
        _node=$(grep -E '^NODE_ID=' "${_ENV}" 2>/dev/null | head -1 | cut -d= -f2- || true)
        _prof=$(grep -E '^XGS_AGENT_PROFILE=' "${_ENV}" 2>/dev/null | head -1 | cut -d= -f2- || true)
        [ -n "${_api}" ] && _dr_ok "platform: ${_api}" || _dr_fail "no API_URL in ${_ENV}"
        [ -n "${_srv}" ] && _dr_ok "server id: ${_srv}" || _dr_fail "no SERVER_ID in ${_ENV}"
        [ -n "${_node}" ] && _dr_ok "node id: ${_node}" || _dr_warn "no node id — enrollment may not have completed"
        echo -e "  ${GREEN}  ✓${NC} mode: ${_prof:-managed (default)}"
        # The agent key is a credential; its presence is the useful fact, never
        # its value.
        if grep -qE '^AGENT_KEY=.+' "${_ENV}" 2>/dev/null; then
            _dr_ok "agent key present"
        else
            _dr_fail "no agent key — the platform will reject every request; re-enroll"
        fi
    else
        _dr_fail "config missing: ${_ENV}"
    fi

    step "Machine identity"
    local _mid=""
    [ -r /etc/machine-id ] && _mid=$(tr -d ' \n' < /etc/machine-id 2>/dev/null || true)
    [ -z "${_mid}" ] && [ -r /var/lib/xgs/machine-id ] && _mid=$(tr -d ' \n' < /var/lib/xgs/machine-id 2>/dev/null || true)
    if [ -n "${_mid}" ]; then
        _dr_ok "machine id: ${_mid}"
        echo "      one machine holds one server record; this is how the platform knows"
    else
        _dr_warn "no machine id — the platform cannot tell this box from another"
    fi

    step "Reaching the platform"
    local api_host code
    api_host=$(printf '%s' "${_api:-${_PLAT}}" | sed -e 's|^https\?://||' -e 's|/.*$||')
    if getent hosts "${api_host}" >/dev/null 2>&1; then
        _dr_ok "DNS resolves ${api_host}"
    else
        _dr_fail "cannot resolve ${api_host} — DNS is broken on this host"
    fi
    code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 15 \
            "${_api:-${_PLAT}}/api/v1/agents/version" 2>/dev/null || echo "000")
    case "${code}" in
        200) _dr_ok "outbound HTTPS works (the platform answered 200)" ;;
        000) _dr_fail "cannot reach ${_api:-${_PLAT}} at all — outbound HTTPS is blocked, or there is no route"
             echo "      the agent only ever dials OUT; nothing needs to reach this box" ;;
        *)   _dr_warn "platform answered HTTP ${code}" ;;
    esac

    step "Things that quietly kill an agent"
    # A full disk stops it writing its journal and it dies with an error that
    # looks like anything but "no space".
    local diskpct
    diskpct=$(df -P /var/lib 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')
    if [ -n "${diskpct}" ] && [ "${diskpct}" -ge 95 ]; then
        _dr_fail "disk is ${diskpct}% full — this alone will stop the agent"
    elif [ -n "${diskpct}" ] && [ "${diskpct}" -ge 85 ]; then
        _dr_warn "disk is ${diskpct}% full"
    else
        _dr_ok "disk has room (${diskpct:-?}% used)"
    fi
    # Clock skew breaks TLS and token validation, and the resulting errors never
    # mention time.
    if command -v timedatectl >/dev/null 2>&1; then
        if timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -q yes; then
            _dr_ok "clock is synchronised"
        else
            _dr_warn "clock is NOT synchronised — TLS and tokens fail in ways that never mention time"
        fi
    fi
    if systemctl is-active --quiet xgs-watchdog 2>/dev/null; then
        _dr_ok "watchdog active (restarts the agent if it dies)"
    else
        _dr_warn "watchdog is not active"
    fi
    # The timer unit has carried more than one name across versions; a
    # diagnostic must not report a working timer as missing.
    if systemctl list-timers --all 2>/dev/null | grep -qiE 'xgs.*(update|upgrade)'; then
        _dr_ok "auto-update timer active"
    else
        _dr_warn "auto-update timer is not active"
    fi

    step "Why it stopped (last errors)"
    if command -v journalctl >/dev/null 2>&1; then
        local errs
        errs=$(journalctl -u "${_SVC}" -n 300 --no-pager 2>/dev/null \
               | grep -iE "error|fatal|panic|refused|denied|timeout" | tail -8 || true)
        if [ -n "${errs}" ]; then
            printf '%s\n' "${errs}" | sed 's/^/      /'
        else
            _dr_ok "no errors in the recent log"
        fi
    else
        _dr_warn "journalctl unavailable; cannot read the service log"
    fi

    echo ""
    if [ "${problems}" -gt 0 ]; then
        echo -e "  ${RED}${BOLD}${problems} problem(s) found${NC}${warnings:+, ${warnings} warning(s)}"
        echo ""
        echo "  Reinstalling repairs the agent WITHOUT touching your apps or data —"
        echo "  it replaces only the binary and its services:"
        echo ""
        echo "    curl -sSL ${_api:-${_PLAT}}/agent | bash -s -- install \\"
        echo "        --api-url ${_api:-${_PLAT}} \\"
        echo "        --enroll-token <new token> --server-id ${_srv:-<server id>}"
        echo ""
        echo "  Use that exact server id — a machine may only hold one record."
    elif [ "${warnings}" -gt 0 ]; then
        echo -e "  ${YELLOW}${BOLD}Healthy, with ${warnings} warning(s)${NC}"
    else
        echo -e "  ${GREEN}${BOLD}Everything checks out${NC}"
        echo "  If the platform still shows this server offline, the agent is"
        echo "  reaching it and the problem is on the platform side — say so."
    fi
    echo ""
    [ "${problems}" -eq 0 ]
}

cmd_remove() {
    require_root
    banner

    local purge=false yes=false
    while [ $# -gt 0 ]; do
        case "$1" in
            --purge)  purge=true; shift ;;
            --yes|-y) yes=true; shift ;;
            *)        shift ;;
        esac
    done

    if ! is_installed && [ ! -f "${_BIN}" ] && [ ! -f "${_SVC_FILE}" ]; then
        info "Agent is not installed. Nothing to remove."
        return 0
    fi

    if [ "${yes}" = false ]; then
        echo -e "  ${BOLD}This will remove the XGenStack agent.${NC}"
        if [ "${purge}" = true ]; then
            echo -e "  ${YELLOW}--purge: ALL config, data, and logs will be deleted.${NC}"
        fi
        echo ""
        echo -n "  Continue? [y/N] "
        read -r confirm
        case "${confirm}" in [yY]|[yY][eE][sS]) ;; *) info "Cancelled."; exit 0 ;; esac
    fi

    # Stop services
    step "Stopping services..."
    systemctl stop "${_WD_SVC}" 2>/dev/null || true
    systemctl disable "${_WD_SVC}" 2>/dev/null || true
    systemctl stop xgs-update.timer 2>/dev/null || true
    systemctl disable xgs-update.timer 2>/dev/null || true
    systemctl stop "${_SVC}" 2>/dev/null || true
    systemctl disable "${_SVC}" 2>/dev/null || true

    # Kill any lingering process
    local pid=""
    pid=$(pgrep -f "xgs-agent" 2>/dev/null || true)
    if [ -n "${pid}" ]; then
        kill -9 ${pid} 2>/dev/null || true
        sleep 1
    fi
    ok "Services stopped"

    # Remove unit files
    step "Removing systemd units..."
    rm -f "${_SVC_FILE}" "${_UPD_SVC}" "${_UPD_TMR}" "${_WD_SVC_FILE}"
    systemctl daemon-reload
    ok "Units removed"

    # Remove binaries
    step "Removing binaries..."
    rm -f "${_BIN}" "${_UPDATE_BIN}" "${_WD_BIN}"
    ok "Binaries removed"

    # Remove logs
    step "Removing logs..."
    rm -rf "${_LOGS}"
    ok "Logs removed"

    # Remove lock
    rm -f "${_LOCK}"

    if [ "${purge}" = true ]; then
        step "Purging config and data..."
        rm -rf "${_CONF_DIR}" "${_DATA}"
        if [ -d "/opt/xgs" ]; then
            rm -rf "/opt/xgs"
        fi
        ok "Config and data purged"
    else
        info "Config preserved at ${_CONF_DIR} (use --purge to remove)"
    fi

    echo ""
    echo -e "  ${BOLD}${GREEN}Agent removed.${NC}"
    echo ""
}

# ---------------------------------------------------------------------------
# Install watchdog service (independent agent monitor)
# ---------------------------------------------------------------------------
# install_dux — install the dux disk-health engine from the platform mirror.
# Disk analysis (Server > Disk) does not work without it. Best-effort: a dux
# failure must never abort the agent install. Prefers the native package (.deb/
# .rpm — ships a systemd index daemon); falls back to the raw binary.
install_dux() {
    local api_url="${1}"; api_url="${api_url%/}"
    # Profile passed EXPLICITLY rather than read from the caller's scope. Bash
    # would resolve it dynamically and it would work today, but a rename in the
    # caller would silently turn this into the managed path on every observe
    # install — a failure with no error to notice.
    local profile="${2:-managed}"
    if command -v dux >/dev/null 2>&1; then ok "dux already installed ($(dux --version 2>/dev/null | head -1))"; return 0; fi
    local base="${api_url}/downloads/dux" tmp="/tmp/xgs-dux-$$"
    # THE PACKAGE POSTINST BLOCKS ON AN INITIAL FULL DISK SCAN.
    #
    # Measured on a production Ubuntu 24.04 host (16.145.121.175), from
    # /var/log/dpkg.log:
    #
    #   20:03:44  configure dux         postinst begins
    #   20:03:44  half-configured
    #   20:05:59  installed             2m15s later
    #
    # dux.postinst runs `systemctl restart dux.service`, and that does not return
    # until dux has finished indexing / — which scales with the filesystem. With
    # output redirected to /dev/null the installer sits silent for minutes and
    # looks hung. The operator reported it as "stuck forever"; it was not stuck,
    # it was mute.
    #
    # DO NOT "fix" this with `timeout N dpkg -i`. Killing dpkg inside a postinst
    # leaves a half-configured package and can wedge apt on someone's production
    # server — a far worse outcome than a slow optional install. An earlier
    # revision of this function did exactly that and is the reason this warning
    # exists.
    #
    # OBSERVE MODE TAKES THE RAW BINARY INSTEAD. The daemon exists to keep a live
    # index; observe-profile servers only ever run on-demand scans, so the
    # package buys nothing and costs the whole stall. The binary is one download
    # and no postinst.
    if [ "${profile}" = "observe" ]; then
        if curl -fsSL --connect-timeout 8 --max-time 45 "${base}/linux-amd64" -o "${tmp}.bin" 2>/dev/null &&
           install -m755 "${tmp}.bin" /usr/local/bin/dux; then
            rm -f "${tmp}.bin"; ok "dux binary installed (observe — no daemon, no initial scan)"; return 0
        fi
    fi
    if command -v dpkg >/dev/null 2>&1; then
        if curl -fsSL --connect-timeout 8 --max-time 45 "${base}/dux_amd64.deb" -o "${tmp}.deb" 2>/dev/null; then
            # Say so BEFORE blocking. Silence for minutes is indistinguishable
            # from a hang, and an operator who cannot tell the difference will
            # kill the installer part-way — which is how this became a report.
            printf '        (dux indexes the filesystem on first start — this can take a few minutes)\n'
            if dpkg -i "${tmp}.deb" >/dev/null 2>&1 ||
               { apt-get -y -f install >/dev/null 2>&1 && dpkg -i "${tmp}.deb" >/dev/null 2>&1; }; then
                rm -f "${tmp}.deb"; ok "dux installed ($(dux --version 2>/dev/null | head -1))"; return 0
            fi
        fi
    elif command -v rpm >/dev/null 2>&1; then
        if curl -fsSL --connect-timeout 8 --max-time 45 "${base}/dux-x86_64.rpm" -o "${tmp}.rpm" 2>/dev/null &&
           rpm -i "${tmp}.rpm" >/dev/null 2>&1; then
            rm -f "${tmp}.rpm"; ok "dux installed ($(dux --version 2>/dev/null | head -1))"; return 0
        fi
    fi
    # Fallback: raw binary (no systemd daemon, but `dux` works for on-demand scans)
    if curl -fsSL --connect-timeout 8 --max-time 45 "${base}/linux-amd64" -o "${tmp}.bin" 2>/dev/null; then
        install -m755 "${tmp}.bin" /usr/local/bin/dux && { rm -f "${tmp}.bin"; ok "dux binary installed (fallback)"; return 0; }
    fi
    rm -f "${tmp}".* 2>/dev/null || true
    warn "dux install failed — Disk analysis will lazy-install on first use"
    return 0
}

install_watchdog() {
    local api_url="${1}"
    api_url="${api_url%/}"

    # Download watchdog script from platform
    local tmp_wd="/tmp/xgs-watchdog-$$"
    local http_code
    # Versioned query bypasses any CDN negative-cache entry left from before
    # the artifact route existed. The origin itself is no-store.
    http_code=$(curl -sS -w '%{http_code}' -o "${tmp_wd}" \
        --connect-timeout 10 --max-time 30 \
        "${api_url}/downloads/xgs-watchdog?v=1" 2>/dev/null || echo "000")

    if [ "${http_code}" != "200" ] || [ ! -s "${tmp_wd}" ]; then
        rm -f "${tmp_wd}"
        warn "Watchdog download failed (HTTP ${http_code}), skipping"
        return 0
    fi

    mv "${tmp_wd}" "${_WD_BIN}"
    chmod +x "${_WD_BIN}"

    # Install systemd service
    cat > "${_WD_SVC_FILE}" <<'WDEOF'
[Unit]
Description=XGenStack Agent Watchdog
After=network-online.target
Wants=xgs-agent.service

[Service]
Type=simple
ExecStart=/usr/local/bin/xgs-watchdog
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=xgs-watchdog

[Install]
WantedBy=multi-user.target
WDEOF

    systemctl daemon-reload
    systemctl enable "${_WD_SVC}" 2>/dev/null
    systemctl restart "${_WD_SVC}" 2>/dev/null
    ok "Watchdog installed"
}

# ---------------------------------------------------------------------------
# Install auto-update timer + self-updating update script
# ---------------------------------------------------------------------------
install_nixpacks() {
    # Nixpacks is the v2.5 Tier 1 detector — Railway's open-source build
    # planner that handles ~70% of Node/Python/Rust/Go apps automatically.
    # We only call `nixpacks plan` (NOT `nixpacks build`), so no Nix
    # runtime needed; we just steal the install/build/start commands.
    #
    # SECURITY: hardcoded version + SHA-256. Do NOT read NIXPACKS_VERSION
    # from the environment — any env-var-based injection would let a
    # compromised agent.env override the pinned binary.
    local NIX_BIN="/usr/local/bin/nixpacks"
    local NIX_VER="1.41.0"

    # Pinned SHAs — must be updated in tandem with NIX_VER above AND with
    # backend/agent/nixpacks/nixpacks.go's PinnedSHA256.
    local NIX_SHA_X86_64="0f55de7874507b9cf7502113120bd96f2ab6979f78d10eaf2eb2ade9207b3af6"
    local NIX_SHA_AARCH64="912bd02dd2bb6f9c3a9ed965fe8a68b4aa318dc7a2546e2eca6f2806a894ba39"

    # Skip if already at expected version. Saves ~50 MB of redundant
    # downloads on every agent upgrade.
    if [ -x "${NIX_BIN}" ]; then
        local CUR_VER
        CUR_VER=$("${NIX_BIN}" --version 2>/dev/null | awk '{print $NF}' || echo "")
        if [ "${CUR_VER}" = "${NIX_VER}" ]; then
            ok "Nixpacks ${NIX_VER} already installed"
            return 0
        fi
    fi

    local arch_tag expected_sha
    case "$(uname -m)" in
        x86_64|amd64)   arch_tag="x86_64";  expected_sha="${NIX_SHA_X86_64}"  ;;
        aarch64|arm64)  arch_tag="aarch64"; expected_sha="${NIX_SHA_AARCH64}" ;;
        *) warn "Unsupported arch for nixpacks: $(uname -m); skipping"; return 0 ;;
    esac

    local url="https://github.com/railwayapp/nixpacks/releases/download/v${NIX_VER}/nixpacks-v${NIX_VER}-${arch_tag}-unknown-linux-musl.tar.gz"
    local tmp="/tmp/nixpacks-$$.tar.gz"
    local http_code
    http_code=$(curl -fsSL -o "${tmp}" -w '%{http_code}' \
        --connect-timeout 10 --max-time 90 \
        "${url}" 2>/dev/null || echo "000")

    if [ "${http_code}" != "200" ] || [ ! -s "${tmp}" ]; then
        rm -f "${tmp}"
        warn "Nixpacks download failed (HTTP ${http_code}); detector will be skipped"
        return 0
    fi

    # SECURITY: verify SHA-256 before extracting / installing.
    # A compromised CDN or MITM bypass cannot deliver a trojan binary.
    local got_sha
    got_sha=$(sha256sum "${tmp}" | awk '{print $1}')
    if [ "${got_sha}" != "${expected_sha}" ]; then
        rm -f "${tmp}"
        warn "Nixpacks SHA-256 mismatch (got=${got_sha} expected=${expected_sha}) — refusing to install possibly tampered binary"
        return 0
    fi

    # Extract to a per-PID dir so concurrent installs don't collide.
    local extract_dir="/tmp/nixpacks-extract-$$"
    rm -rf "${extract_dir}"
    mkdir -p "${extract_dir}"
    if ! tar -xzf "${tmp}" -C "${extract_dir}" 2>/dev/null; then
        rm -rf "${extract_dir}" "${tmp}"
        warn "Nixpacks extraction failed; skipping"
        return 0
    fi

    local found
    found=$(find "${extract_dir}" -name nixpacks -type f | head -1)
    if [ -z "${found}" ]; then
        rm -rf "${extract_dir}" "${tmp}"
        warn "Nixpacks binary not found in tarball; skipping"
        return 0
    fi

    # Atomic install: write to .tmp then rename — avoids the partial-write
    # window where the file exists but is truncated.
    local stage="${NIX_BIN}.tmp.$$"
    if ! install -m 0755 "${found}" "${stage}" 2>/dev/null; then
        rm -rf "${extract_dir}" "${tmp}" "${stage}"
        warn "Nixpacks install (stage) failed; skipping"
        return 0
    fi
    if ! mv -f "${stage}" "${NIX_BIN}" 2>/dev/null; then
        rm -rf "${extract_dir}" "${tmp}" "${stage}"
        warn "Nixpacks install (rename) failed; skipping"
        return 0
    fi
    rm -rf "${extract_dir}" "${tmp}"

    if "${NIX_BIN}" --version >/dev/null 2>&1; then
        ok "Nixpacks ${NIX_VER} installed at ${NIX_BIN}"
    else
        warn "Nixpacks installed but --version failed; detector may not work"
    fi
}

install_ollama_rca() {
    # DISABLED — the local LLM (Ollama + Phi-3-mini, ~2.4 GB) was far too heavy
    # for a typical VPS. RCA now relies solely on the agent's built-in
    # rule-based engine. Kept as a no-op so any stray caller is harmless.
    return 0
}

install_update_timer() {
    local api_url="${1}"

    # Write the update script inline (so it's always current)
    cat > "${_UPDATE_BIN}" <<'UPDATEEOF'
#!/usr/bin/env bash
# XGenStack Agent Auto-Update — invoked by systemd timer
set -euo pipefail
exec 2>&1

ULOG="/var/log/xgenstack/updates.log"
mkdir -p /var/log/xgenstack

ulog() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [$1] $2" | tee -a "$ULOG"; }

[ "$(id -u)" -eq 0 ] || { ulog ERROR "Must be root"; exit 1; }
[ -f /etc/xgenstack/agent.env ] || { ulog ERROR "No agent.env"; exit 1; }

# Extract vars safely
U_API_URL=$(grep -E '^API_URL=' /etc/xgenstack/agent.env | head -1 | cut -d= -f2-)
U_AGENT_KEY=$(grep -E '^AGENT_KEY=' /etc/xgenstack/agent.env | head -1 | cut -d= -f2-)
U_NODE_ID=$(grep -E '^NODE_ID=' /etc/xgenstack/agent.env | head -1 | cut -d= -f2-)
U_SIGNING_PUBKEY=$(grep -E '^AGENT_SIGNING_PUBKEY=' /etc/xgenstack/agent.env | head -1 | cut -d= -f2- || true)

[ -n "${U_API_URL:-}" ] || { ulog ERROR "No API_URL"; exit 1; }
[ -n "${U_AGENT_KEY:-}" ] || { ulog ERROR "No AGENT_KEY"; exit 1; }

U_API_URL="${U_API_URL%/}"
UCURRENT=$(tr -d '[:space:]' < /etc/xgenstack/version 2>/dev/null || echo "0.0.0")
ulog INFO "Current version: $UCURRENT"

# Check latest
URESP=$(curl -sS --connect-timeout 10 --max-time 30 \
    -H "X-Agent-Key: ${U_AGENT_KEY}" -H "X-Node-ID: ${U_NODE_ID:-}" \
    "${U_API_URL}/api/v1/agents/version" 2>/dev/null) || { ulog ERROR "Cannot reach API"; exit 1; }

ULATEST=$(echo "$URESP" | jq -r '.data.version // .version // empty' 2>/dev/null)
[ -n "$ULATEST" ] || { ulog INFO "No version info from API"; exit 0; }
ulog INFO "Latest version: $ULATEST"

# Compare
if [ "$ULATEST" = "$UCURRENT" ]; then
    ulog INFO "Already up to date"
    exit 0
fi

ulog INFO "Updating $UCURRENT -> $ULATEST"

# Download
UARCH=$(uname -m); USYS=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$UARCH" in x86_64) UAL=amd64;; aarch64) UAL=arm64;; *) UAL=$UARCH;; esac
UDL_URL=$(echo "$URESP" | jq -r '.data.download_url // empty' 2>/dev/null)
[ -n "$UDL_URL" ] || UDL_URL="${U_API_URL}/downloads/agent/${USYS}-${UAL}"

UTMP="/tmp/xgs-agent-update-$$"
UBAK="/tmp/xgs-agent-backup-$$"
trap 'rm -f "$UTMP"' EXIT

UHTTP=$(curl -sS -w '%{http_code}' -o "$UTMP" --connect-timeout 10 --max-time 120 \
    -H "X-Agent-Key: ${U_AGENT_KEY}" -H "X-Node-ID: ${U_NODE_ID:-}" "$UDL_URL" 2>/dev/null || echo 000)

if [ "$UHTTP" != "200" ] || [ ! -s "$UTMP" ]; then
    ulog ERROR "Download failed (HTTP $UHTTP)"
    exit 1
fi
ulog INFO "Downloaded $(wc -c < "$UTMP") bytes"

# Checksum
UCHK=$(echo "$URESP" | jq -r '.data.checksum // empty' 2>/dev/null)
[ -n "$UCHK" ] || { ulog ERROR "Update response omitted checksum"; exit 1; }
UACT=$(sha256sum "$UTMP" | awk '{print $1}')
[ "$UACT" = "$UCHK" ] || { ulog ERROR "Checksum mismatch"; exit 1; }
ulog INFO "Checksum OK"

# Verify the same signed manifest as the in-process updater. A legacy host must
# be explicitly re-enrolled/backfilled before it can update; silently trusting
# a checksum delivered by the update channel defeats the signing boundary.
if [ -z "${U_SIGNING_PUBKEY:-}" ]; then
    ulog ERROR "Enrollment-pinned signing key missing; refusing checksum-only update"
    exit 1
else
    command -v openssl >/dev/null || { ulog ERROR "openssl required for signed update"; exit 1; }
    UMAN=$(curl -fsS --connect-timeout 10 --max-time 30 "${U_API_URL}/api/v1/agents/manifest?os=${USYS}&arch=${UAL}") || { ulog ERROR "Cannot fetch signed manifest"; exit 1; }
    UMVER=$(echo "$UMAN" | jq -er '.data.version') || exit 1
    UMSHA=$(echo "$UMAN" | jq -er '.data.sha256') || exit 1
    UMSIZE=$(echo "$UMAN" | jq -er '.data.size') || exit 1
    UMOS=$(echo "$UMAN" | jq -er '.data.os') || exit 1
    UMARCH=$(echo "$UMAN" | jq -er '.data.arch') || exit 1
    UMMSG=$(echo "$UMAN" | jq -er '.data.signed_message') || exit 1
    UMSIG=$(echo "$UMAN" | jq -er '.data.signature') || exit 1
    UMKID=$(echo "$UMAN" | jq -er '.data.key_id') || exit 1
    [ "$UMVER" = "$ULATEST" ] && [ "$UMSHA" = "$UACT" ] && [ "$UMSIZE" -eq "$(wc -c < "$UTMP")" ] && [ "$UMOS" = "$USYS" ] && [ "$UMARCH" = "$UAL" ] || { ulog ERROR "Signed manifest binding mismatch"; exit 1; }
    UMCANON=$(printf 'xgs-agent-manifest\nv1\n%s-%s\n%s\n%s' "$USYS" "$UAL" "$UMVER" "$UMSHA")
    [ "$UMMSG" = "$UMCANON" ] || { ulog ERROR "Signed manifest canonical mismatch"; exit 1; }
    UVDIR=$(mktemp -d); trap 'rm -f "$UTMP"; rm -rf "$UVDIR"' EXIT
    printf '%s' "$U_SIGNING_PUBKEY" | base64 -d > "$UVDIR/key.raw" 2>/dev/null || { ulog ERROR "Pinned key malformed"; exit 1; }
    [ "$(wc -c < "$UVDIR/key.raw")" -eq 32 ] || { ulog ERROR "Pinned key length invalid"; exit 1; }
    [ "$(sha256sum "$UVDIR/key.raw" | cut -c1-16)" = "$UMKID" ] || { ulog ERROR "Manifest key ID mismatch"; exit 1; }
    printf '\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00' > "$UVDIR/key.der"
    cat "$UVDIR/key.raw" >> "$UVDIR/key.der"
    printf '%s' "$UMSIG" | base64 -d > "$UVDIR/sig" 2>/dev/null || { ulog ERROR "Signature malformed"; exit 1; }
    printf '%s' "$UMCANON" > "$UVDIR/msg"
    openssl pkeyutl -verify -pubin -inkey "$UVDIR/key.der" -keyform DER -rawin -in "$UVDIR/msg" -sigfile "$UVDIR/sig" >/dev/null 2>&1 || { ulog ERROR "Signature verification failed"; exit 1; }
    rm -rf "$UVDIR"
    ulog INFO "Ed25519 release signature verified"
fi

# Backup + replace
cp /usr/local/bin/xgs-agent "$UBAK" 2>/dev/null || true
systemctl stop xgs-agent 2>/dev/null || true
sleep 1
mv "$UTMP" /usr/local/bin/xgs-agent
chmod +x /usr/local/bin/xgs-agent
echo "$ULATEST" > /etc/xgenstack/version
systemctl start xgs-agent

# Health check
sleep 3
if systemctl is-active --quiet xgs-agent; then
    ulog INFO "Update successful: $UCURRENT -> $ULATEST"
    rm -f "$UBAK"
else
    ulog ERROR "Agent failed to start! Rolling back..."
    if [ -f "$UBAK" ]; then
        mv "$UBAK" /usr/local/bin/xgs-agent
        chmod +x /usr/local/bin/xgs-agent
        echo "$UCURRENT" > /etc/xgenstack/version
        systemctl start xgs-agent 2>/dev/null || true
        sleep 2
        if systemctl is-active --quiet xgs-agent; then
            ulog INFO "Rollback successful"
        else
            ulog ERROR "Rollback failed — manual intervention needed"
        fi
    fi
    exit 1
fi
UPDATEEOF
    chmod +x "${_UPDATE_BIN}"

    # Timer service
    cat > "${_UPD_SVC}" <<TSVEOF
[Unit]
Description=XGenStack Agent Auto-Update
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=${_UPDATE_BIN}
StandardOutput=journal
StandardError=journal
SyslogIdentifier=xgs-update
TSVEOF

    # Timer
    cat > "${_UPD_TMR}" <<TMEOF
[Unit]
Description=XGenStack Agent Auto-Update Timer

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target
TMEOF

    systemctl daemon-reload
    systemctl enable xgs-update.timer 2>/dev/null
    systemctl start xgs-update.timer 2>/dev/null
    ok "Auto-update timer installed (daily 03:00 +/- 30m)"
}

# ---------------------------------------------------------------------------
# MAIN — route to command
# ---------------------------------------------------------------------------
main() {
    local cmd="${1:-auto}"
    shift 2>/dev/null || true

    case "${cmd}" in
        install)  cmd_install "$@" ;;
        update)
            FORCE=false
            for arg in "$@"; do
                [ "${arg}" = "--force" ] && FORCE=true
            done
            cmd_update
            ;;
        remove|uninstall)  cmd_remove "$@" ;;
        status)   cmd_status ;;
        doctor|diagnose|check)  cmd_doctor ;;
        auto)
            # Auto-detect: update if installed, show help if not
            if is_installed; then
                FORCE=false
                cmd_update
            else
                banner
                echo "  Usage:"
                echo ""
                echo -e "  ${BOLD}Install:${NC}"
                echo "    curl -sSL ${_PLAT}/agent | bash -s -- install \\"
                echo "        --api-url ${_PLAT} \\"
                echo "        --enroll-token TOKEN --server-id ID \\"
                echo "        --binary-sha256 SHA256_FROM_INDEPENDENT_CHANNEL"
                echo ""
                echo -e "  ${BOLD}Update:${NC}  (on a server with agent already installed)"
                echo "    curl -sSL ${_PLAT}/agent | bash -s -- update"
                echo ""
                echo -e "  ${BOLD}Remove:${NC}"
                echo "    curl -sSL ${_PLAT}/agent | bash -s -- remove [--purge]"
                echo ""
                echo -e "  ${BOLD}Status:${NC}"
                echo "    curl -sSL ${_PLAT}/agent | bash -s -- status"
                echo ""
                echo -e "  ${BOLD}Doctor:${NC}  (agent offline or misbehaving — read-only)"
                echo "    curl -sSL ${_PLAT}/agent | bash -s -- doctor"
                echo ""
            fi
            ;;
        -h|--help|help)
            banner
            echo "  Commands:"
            echo "    install   Install agent (requires --api-url, --enroll-token, --server-id)"
            echo "    update    Update existing agent to latest version"
            echo "    remove    Remove agent (add --purge to delete config/data)"
            echo "    status    Show agent status and version"
            echo "    doctor    Diagnose an agent that is offline or misbehaving (read-only)"
            echo ""
            ;;
        *)
            die "Unknown command: ${cmd}. Use: install, update, remove, status, doctor"
            ;;
    esac
}

main "$@"
