#!/bin/bash

# ==============================================================================
# jaspernode_install.sh
#
# Installs, updates, or uninstalls the jaspernode binary as a systemd service.
#
# Usage (Install/Update):
#   curl -sL https://dl2.jasperx.io/jn/install.sh | sudo -E bash -
#
# Usage (Install/Update with first-boot JasperX enrolment):
#   curl -sL https://dl2.jasperx.io/jn/install.sh | JX_CLAIM=<claim> sudo -E bash -
#
# Usage (Install latest pre-release, beta channel):
#   curl -sL https://dl2.jasperx.io/jn/install.sh | JN_CHANNEL=beta sudo -E bash -
#
# Usage (Install/Update from local file):
#   sudo bash linux-install.sh --local /path/to/jaspernode-binary
#
# Usage (Uninstall):
#   curl -sL https://dl2.jasperx.io/jn/install.sh | sudo -E bash -s -- uninstall
#
# ==============================================================================

# --- Configuration ---
INSTALL_SCRIPT_VERSION="2.0.0"
SERVICE_NAME="jaspernode"
INSTALL_USER="jaspernode"
# APP_DIR also used to temporary store the downloaded update file
APP_DIR="/var/lib/jaspernode"
# Node state (engine DB, credentials, backups) — the binary's built-in Linux
# default. Shared by prod and dev builds so a dev↔prod swap reopens the same DB.
DATA_DIR="${APP_DIR}/data"
BINARY_PATH="${APP_DIR}/jaspernode"
SYMLINK_PATH="/usr/local/bin/jaspernode"
VERSION_FILE="${APP_DIR}/.version"
# How many previous-binary backups (${BINARY_PATH}.bak-<version>) to keep after
# an install/update. Each is the full ~300MB binary, so they pile up fast on the
# small root partitions of edge devices. Override with JN_BACKUP_RETENTION.
BACKUP_RETENTION="${JN_BACKUP_RETENTION:-2}"
BASE_URL="https://dl2.jasperx.io/jn"

# Update channel: stable (default) follows latest.txt; JN_CHANNEL=beta follows
# latest-beta.txt (pre-releases included). Mirrors the node's own updateChannel.
CHANNEL="${JN_CHANNEL:-stable}"
POINTER_FILE="latest.txt"
if [ "$CHANNEL" = "beta" ]; then
    POINTER_FILE="latest-beta.txt"
fi

# Public key that signs JasperNode release binaries.
# Regenerate with:  openssl ec -in ~/.jaspernode/signing.key -pubout
SIGNING_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjSHEV0F5u++HqGxY4+LG27kcX+2J
crjf96FyqB71rqKN7MFIuEQNwbm7dmqyqYmXc/wh03onwYQpBtGg32EOvw==
-----END PUBLIC KEY-----"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"

# --- Color Definitions ---
C_RESET='\033[0m'
C_GREEN='\033[0;32m'
C_RED='\033[0;31m'
C_YELLOW='\033[0;33m'
C_CYAN='\033[0;36m'

# --- Helper Functions ---
info() {
    echo -e "${C_GREEN}[INFO]${C_RESET} $1"
}

warn() {
    echo -e "${C_YELLOW}[WARN]${C_RESET} $1"
}

error() {
    echo -e "${C_RED}[ERROR]${C_RESET} $1"
}

get_arch() {
    case $(uname -m) in
        x86_64) echo "linux64";;
        aarch64) echo "linuxA64";;
        *) error "Unsupported architecture: $(uname -m)." >&2; exit 1;;
    esac
}

get_latest_version_info() {
    local arch=$1
    info "Checking for the latest version (${CHANNEL} channel)..." >&2

    # -f (--fail): a GCS 404 returns an XML NoSuchKey body with HTTP 200-shaped
    # output otherwise, which would poison latest_version with XML garbage
    # instead of tripping the empty-string fallback below.
    local latest_version
    latest_version=$(curl -sfL "${BASE_URL}/${POINTER_FILE}" | tr -d '[:space:]')
    # Deliberately looser than the node's own updater (UpdateService only falls
    # back to stable on a 404/empty beta pointer, and surfaces other fetch
    # errors instead of silently switching channels). This is an install-time
    # UX choice: an operator is present and sees the warning below, and
    # aborting the install over a transient beta-pointer hiccup helps nobody.
    # The node-side updater stays strict on purpose, to protect unattended
    # channel fidelity when nobody is watching.
    if [ -z "$latest_version" ] && [ "$POINTER_FILE" != "latest.txt" ]; then
        warn "Channel pointer ${POINTER_FILE} unavailable — falling back to stable latest.txt." >&2
        latest_version=$(curl -sfL "${BASE_URL}/latest.txt" | tr -d '[:space:]')
    fi
    if [ -z "$latest_version" ]; then
        error "Could not determine the latest version." >&2
        exit 1
    fi
    # Sanity check: a version string is [A-Za-z0-9.+-]+. Anything else means a
    # fetch returned something other than a pointer file (error page, XML, HTML).
    case "$latest_version" in
        *[!A-Za-z0-9.+-]*|"")
            error "Fetched version pointer looks malformed: '${latest_version}' (from ${BASE_URL}/${POINTER_FILE})." >&2
            exit 1 ;;
    esac

    # v2 hosting is a flat, immutable GCS bucket: jn/jaspernode_<v>_<arch>.
    local download_url="${BASE_URL}/jaspernode_${latest_version}_${arch}"

    echo "${latest_version}|${download_url}"
}

# Verify a downloaded binary against the embedded signing public key. Any
# failure (no key configured, missing signature, bad signature) aborts.
verify_signature() {
    local binary="$1" url="$2"
    if printf '%s' "$SIGNING_PUBLIC_KEY" | grep -q "REPLACE_WITH_REAL_PUBLIC_KEY"; then
        error "No signing public key is configured in this installer. Refusing to install."
        rm -f "${binary}"; exit 1
    fi
    if ! command -v openssl >/dev/null 2>&1; then
        info "Installing openssl (required to verify the download)..."
        (apt-get update -qq && apt-get install -y -qq openssl) >/dev/null 2>&1 || {
            error "openssl is required to verify the download."; rm -f "${binary}"; exit 1; }
    fi
    local pub sig
    pub="$(mktemp)"; sig="$(mktemp)"
    printf '%s\n' "$SIGNING_PUBLIC_KEY" > "$pub"
    if ! curl -sL --fail "${url}.sig" -o "${sig}"; then
        error "Could not download signature ${url}.sig — refusing to install."
        rm -f "$pub" "$sig" "$binary"; exit 1
    fi
    if ! openssl dgst -sha256 -verify "$pub" -signature "$sig" "$binary" >/dev/null 2>&1; then
        error "Signature verification FAILED — binary is not signed by the JasperNode key. Refusing to install."
        rm -f "$pub" "$sig" "$binary"; exit 1
    fi
    rm -f "$pub" "$sig"
    info "Signature verified."
}

get_installed_version() {
    if [ -f "${VERSION_FILE}" ]; then
        cat "${VERSION_FILE}" | tr -d '[:space:]'
    else
        echo "not-installed"
    fi
}


uninstall_app() {
    info "Starting JasperNode uninstallation..."

    if systemctl list-units --full -all | grep -Fq "${SERVICE_NAME}.service"; then
        info "Stopping and disabling ${SERVICE_NAME} service..."
        systemctl stop "${SERVICE_NAME}"
        systemctl disable "${SERVICE_NAME}"
    else
        info "Service ${SERVICE_NAME} not found, skipping."
    fi

    if [ -f "${SERVICE_FILE}" ]; then
        info "Removing systemd service file..."
        rm -f "${SERVICE_FILE}"
    fi

    if [ -d "/etc/systemd/system/${SERVICE_NAME}.service.d" ]; then
        info "Removing systemd drop-in directory..."
        rm -rf "/etc/systemd/system/${SERVICE_NAME}.service.d"
    fi

    # Kept across reinstalls (so dev→prod→dev needs no re-enrolment), but a
    # full uninstall sweeps the enrolment env file too.
    if [ -f "/etc/${SERVICE_NAME}.env" ]; then
        info "Removing /etc/${SERVICE_NAME}.env..."
        rm -f "/etc/${SERVICE_NAME}.env"
    fi

    info "Reloading systemd daemon..."
    systemctl daemon-reload

    if [ -L "${SYMLINK_PATH}" ]; then
        info "Removing symlink ${SYMLINK_PATH}..."
        rm -f "${SYMLINK_PATH}"
    fi

    if [ -f "/usr/local/bin/jnsm" ]; then
        info "Removing jnsm management script..."
        rm -f "/usr/local/bin/jnsm"
    fi

    if [ -f "/etc/sudoers.d/jaspernode" ]; then
        info "Removing sudoers configuration..."
        rm -f "/etc/sudoers.d/jaspernode"
    fi

    if [ -d "${APP_DIR}" ]; then
        warn "The application directory exists at ${APP_DIR}."
        warn "This directory may contain your database and configuration."
        read -p "Do you want to DELETE this directory and all data? (y/N) " -n 1 -r REPLY < /dev/tty
        echo
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            info "Removing application directory ${APP_DIR}..."
            rm -rf "${APP_DIR}"
        else
            info "Application directory ${APP_DIR} was preserved."
        fi
    fi

    if id "${INSTALL_USER}" &>/dev/null; then
        warn "The user '${INSTALL_USER}' exists."
        read -p "Do you want to remove this user? (y/N) " -n 1 -r REPLY < /dev/tty
        echo
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            info "Removing user '${INSTALL_USER}'..."
            # Use -r to remove home directory if it exists, suppress harmless warnings if it doesn't
            userdel -r "${INSTALL_USER}" 2>/dev/null
        else
            info "User '${INSTALL_USER}' was not removed."
        fi
    fi

    echo
    echo -e "${C_GREEN}✔ JasperNode has been uninstalled successfully.${C_RESET}"
    exit 0
}


# --- Main Logic ---

# Print install script version
info "JasperNode Install Script Version: ${C_CYAN}${INSTALL_SCRIPT_VERSION}${C_RESET}"

# 1. Check for root privileges
if [ "$(id -u)" -ne 0 ]; then
    error "This installer must be run as root or with sudo."
    exit 1
fi

# 2. Parse Arguments
LOCAL_INSTALL_FILE=""
while [[ $# -gt 0 ]]; do
    case $1 in
        uninstall)
            uninstall_app
            ;;
        --local)
            LOCAL_INSTALL_FILE="$2"
            shift # past argument
            shift # past value
            ;;
        *)
            shift # unknown option
            ;;
    esac
done

# 3. Prepare Environment
info "Ensuring application directory exists at ${APP_DIR}..."
mkdir -p "${APP_DIR}" "${DATA_DIR}" # -p is safe, it won't delete existing data
# If the service user already exists (repair/re-run), hand the data dir over
# NOW — several early paths exit before the full chown further down, and a
# root-owned ${DATA_DIR} would stop the service writing its DB.
if id "${INSTALL_USER}" &>/dev/null; then
    chown "${INSTALL_USER}:${INSTALL_USER}" "${DATA_DIR}"
fi

ARCH=$(get_arch)
INSTALLED_VERSION=$(get_installed_version)
TARGET_VERSION=""
TMP_FILE=$(mktemp)

# 4. Acquire Binary (Local or Network)
if [ -n "$LOCAL_INSTALL_FILE" ]; then
    info "Mode: Local Install"
    if [ ! -f "$LOCAL_INSTALL_FILE" ]; then
        error "Local file not found: $LOCAL_INSTALL_FILE"
        rm -f "${TMP_FILE}"
        exit 1
    fi
    info "Using local binary: ${LOCAL_INSTALL_FILE}"
    cp "$LOCAL_INSTALL_FILE" "$TMP_FILE"
    TARGET_VERSION="local-$(date +%Y%m%d)"
else
    info "Mode: Network Install/Update"
    VERSION_INFO=$(get_latest_version_info "$ARCH")
    IFS='|' read -r LATEST_VERSION DOWNLOAD_URL <<< "$VERSION_INFO"
    TARGET_VERSION="$LATEST_VERSION"

    if [ "$INSTALLED_VERSION" != "not-installed" ]; then
        info "JasperNode is already installed. Version: ${C_CYAN}${INSTALLED_VERSION}${C_RESET}"
        if [ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]; then
            info "You are already running the latest version."
            # Allow reinstall to repair service file/permissions
            if [ -t 0 ]; then
                 read -p "Reinstall/Repair existing version? (y/N) " -n 1 -r REPLY < /dev/tty
                 echo
                 if [[ ! $REPLY =~ ^[Yy]$ ]]; then
                     info "Exiting."
                     rm -f "${TMP_FILE}"
                     exit 0
                 fi
            else
                 info "Version matches. Exiting."
                 rm -f "${TMP_FILE}"
                 exit 0
            fi
        else
            warn "A new version is available: ${C_CYAN}${LATEST_VERSION}${C_RESET}"
             if [ -t 0 ]; then
                read -p "Do you want to upgrade? (y/N) " -n 1 -r REPLY < /dev/tty
                echo
                if [[ ! $REPLY =~ ^[Yy]$ ]]; then
                    info "Update cancelled."
                    rm -f "${TMP_FILE}"
                    exit 0
                fi
            fi
        fi
    else
        info "Installing version: ${C_CYAN}${LATEST_VERSION}${C_RESET}"
    fi

    info "Downloading from ${DOWNLOAD_URL}..."
    if ! curl -sL --fail "${DOWNLOAD_URL}" -o "${TMP_FILE}"; then
        error "Failed to download binary."
        rm -f "${TMP_FILE}"
        exit 1
    fi
    verify_signature "${TMP_FILE}" "${DOWNLOAD_URL}"
fi

# Verify Download/Copy
if [ ! -s "${TMP_FILE}" ]; then
    error "Install file is empty. Aborting."
    rm -f "${TMP_FILE}"
    exit 1
fi

# 5. Install Binary
info "Installing binary to ${BINARY_PATH}..."

# Backup existing if it exists
if [ -f "${BINARY_PATH}" ]; then
    BACKUP_NAME="${BINARY_PATH}.bak-${INSTALLED_VERSION}"
    info "Backing up existing binary to ${BACKUP_NAME}..."
    mv "${BINARY_PATH}" "${BACKUP_NAME}"

    # Prune older backups, keeping only the most recent ${BACKUP_RETENTION}
    # (newest by mtime). Without this they accumulate one ~300MB file per
    # deploy and silently fill the root partition on edge devices.
    ls -1t "${BINARY_PATH}".bak-* 2>/dev/null | tail -n +$((BACKUP_RETENTION + 1)) | while read -r old_backup; do
        info "Pruning old backup ${old_backup}..."
        rm -f "${old_backup}"
    done
fi

mv "${TMP_FILE}" "${BINARY_PATH}"
chmod +x "${BINARY_PATH}"
echo "${TARGET_VERSION}" > "${VERSION_FILE}"

# 6. Configuration (Always runs, ensuring Service File updates)

info "Creating symlink at ${SYMLINK_PATH}..."
ln -sf "${BINARY_PATH}" "${SYMLINK_PATH}"

if id "${INSTALL_USER}" &>/dev/null; then
    info "User '${INSTALL_USER}' already exists."
else
    info "Creating system user '${INSTALL_USER}'..."
    useradd -r -s /bin/false -d "${APP_DIR}" "${INSTALL_USER}"
fi

info "Setting ownership of ${APP_DIR} to ${INSTALL_USER}..."
chown -R "${INSTALL_USER}:${INSTALL_USER}" "${APP_DIR}"
# Ensure binary is owned by user
chown "${INSTALL_USER}:${INSTALL_USER}" "${BINARY_PATH}" "${VERSION_FILE}"


# First-boot JasperX enrolment. When JX_CLAIM is supplied in the environment
# (curl ... | JX_CLAIM=<claim> sudo -E bash -), pin it into the unit so the node
# runs `POST /enrol {claim}` on its next boot and stores the device JWT. The
# unit is rewritten every run, so a claim is present only when explicitly passed
# — a consumed claim drops off on the next update that doesn't re-supply it.
JX_CLAIM_ENV_LINE=""
if [ -n "${JX_CLAIM:-}" ]; then
    # The claim is written verbatim into the unit file: restrict it to token
    # characters so whitespace/quotes/newlines can neither break Environment=
    # parsing nor inject unit directives.
    case "${JX_CLAIM}" in
        *[!A-Za-z0-9._+/=:-]*)
            error "JX_CLAIM contains characters outside [A-Za-z0-9._+/=:-] — refusing to write it into the service unit."
            exit 1 ;;
    esac
    info "JX_CLAIM provided — pinning first-boot enrolment claim into the service unit."
    JX_CLAIM_ENV_LINE="Environment=\"JX_CLAIM=${JX_CLAIM}\""
fi

info "Updating systemd service file..."
# Important: We rewrite this file every time to ensure updates to capabilities/config are applied.
# The leading "-" on the guard ExecStartPre tells systemd to ignore a nonzero
# exit / missing file — jn-guard.sh is written later in this same script, so a
# streamed install that dies mid-way (or a very first boot before this run
# completes) must not block the service from starting.
cat << EOF > "${SERVICE_FILE}"
[Unit]
Description=JasperNode Service
After=network.target

[Service]
${JX_CLAIM_ENV_LINE}
ExecStartPre=-/bin/bash ${APP_DIR}/jn-guard.sh
ExecStartPre=/bin/bash -c 'echo "Via JasperNode Install Script Version: ${INSTALL_SCRIPT_VERSION}"'
ExecStart=${BINARY_PATH}
User=${INSTALL_USER}
Group=${INSTALL_USER}
WorkingDirectory=${APP_DIR}
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

# Basic security
NoNewPrivileges=yes

# Grant specific capabilities for priority and resource management
AmbientCapabilities=CAP_SYS_NICE CAP_SYS_RESOURCE CAP_DAC_OVERRIDE CAP_FOWNER CAP_NET_RAW CAP_NET_ADMIN

# High priority settings for industrial control application
Nice=-7
IOSchedulingClass=1
IOSchedulingPriority=4
LimitNOFILE=65536
MemoryMax=2G

[Install]
WantedBy=multi-user.target
EOF

# Create JasperNode Service Manager (jnsm) script
info "Updating JasperNode Service Manager (jnsm)..."
cat << 'EOF' > "/usr/local/bin/jnsm"
#!/bin/bash

# ==============================================================================
# JasperNode Service Manager (jnsm)
# Management tool for JasperNode systemd service
# ==============================================================================

SERVICE_NAME="jaspernode"
SCRIPT_NAME="jnsm"

# Color definitions
C_RESET='\033[0m'
C_GREEN='\033[0;32m'
C_RED='\033[0;31m'
C_YELLOW='\033[0;33m'
C_CYAN='\033[0;36m'

info() {
    echo -e "${C_GREEN}[INFO]${C_RESET} $1"
}

warn() {
    echo -e "${C_YELLOW}[WARN]${C_RESET} $1"
}

error() {
    echo -e "${C_RED}[ERROR]${C_RESET} $1"
}

show_help() {
    echo "JasperNode Service Manager (jnsm)"
    echo
    echo "Usage: jnsm <command>"
    echo
    echo "Commands:"
    echo "  status     - Show service status"
    echo "  start      - Start the service"
    echo "  stop       - Stop the service"
    echo "  restart    - Restart the service"
    echo "  enable     - Enable service to start on boot"
    echo "  disable    - Disable service from starting on boot"
    echo "  logs       - Show recent logs (last 100 lines)"
    echo "  logs-follow - Follow logs in real-time"
    echo "  check      - Check installation type and permissions"
    echo "  version    - Show installed version"
    echo "  help       - Show this help message"
    echo
}

check_installation() {
    info "Checking JasperNode installation..."
    
    # Check if service exists
    if systemctl list-units --full -all | grep -Fq "${SERVICE_NAME}.service"; then
        info "✓ Service found: ${SERVICE_NAME}.service"
        
        # Check service status
        local status=$(systemctl is-active "${SERVICE_NAME}" 2>/dev/null)
        if [ "$status" = "active" ]; then
            info "✓ Service is running"
        else
            warn "Service is not running (status: $status)"
        fi
        
        # Check if enabled
        if systemctl is-enabled "${SERVICE_NAME}" >/dev/null 2>&1; then
            info "✓ Service is enabled (starts on boot)"
        else
            warn "Service is not enabled (won't start on boot)"
        fi
        
        # Check version
        if [ -f "/var/lib/jaspernode/.version" ]; then
            local version=$(cat "/var/lib/jaspernode/.version")
            info "✓ Installed version: $version"
        else
            warn "Version file not found"
        fi
                
        # Check permissions
        info "Checking permissions..."
        if [ -f "/var/lib/jaspernode/jaspernode" ]; then
            local perms=$(ls -l "/var/lib/jaspernode/jaspernode" | awk '{print $1}')
            local owner=$(ls -l "/var/lib/jaspernode/jaspernode" | awk '{print $3":"$4}')
            info "✓ Binary permissions: $perms, owner: $owner"
        else
            error "Binary not found at /var/lib/jaspernode/jaspernode"
        fi
        
    else
        error "Service ${SERVICE_NAME}.service not found"
        return 1
    fi
}

case "$1" in
    status)
        sudo systemctl status "${SERVICE_NAME}"
        ;;
    start)
        info "Starting ${SERVICE_NAME} service..."
        sudo systemctl start "${SERVICE_NAME}"
        ;;
    stop)
        info "Stopping ${SERVICE_NAME} service..."
        sudo systemctl stop "${SERVICE_NAME}"
        ;;
    restart)
        info "Restarting ${SERVICE_NAME} service..."
        sudo systemctl restart "${SERVICE_NAME}"
        ;;
    enable)
        info "Enabling ${SERVICE_NAME} service..."
        sudo systemctl enable "${SERVICE_NAME}"
        ;;
    disable)
        info "Disabling ${SERVICE_NAME} service..."
        sudo systemctl disable "${SERVICE_NAME}"
        ;;
    logs)
        sudo journalctl -n 100 -u "${SERVICE_NAME}" --no-pager
        ;;
    logs-follow)
        sudo journalctl -u "${SERVICE_NAME}" -f
        ;;
    check)
        check_installation
        ;;
    version)
        if [ -f "/var/lib/jaspernode/.version" ]; then
            cat "/var/lib/jaspernode/.version"
        else
            error "Version file not found"
            exit 1
        fi
        ;;
    help|--help|-h)
        show_help
        ;;
    *)
        error "Unknown command: $1"
        echo
        show_help
        exit 1
        ;;
esac
EOF

chmod +x "/usr/local/bin/jnsm"
info "✓ jnsm script created and made executable"

# Create the boot guard (ExecStartPre). Rolls back a crash-looping update:
# the node's updater writes data/update_pending.json before swapping the
# binary and deletes it once a boot reaches healthy. While the marker exists,
# each start increments a counter; after 3 crashed starts the guard restores
# the .old binary (or, if there is none to restore, just records the failure)
# and writes data/rolled_back.json for the node to surface.
info "Updating JasperNode boot guard (jn-guard.sh)..."
cat << 'EOF' > "${APP_DIR}/jn-guard.sh"
#!/bin/bash
# --- jn-guard begin ---
# JasperNode boot guard — see install script + update.service.ts marker contract.
# JN_GUARD_* env overrides exist for the test harness only.
# Hardcodes the standard data dir. A node started with a DATA_PATH override
# writes its markers elsewhere and this guard sees no marker there — it is
# deliberately inert in that case (fail-safe: no spurious rollback) rather
# than trying to discover the override.
GUARD_APP_DIR="${JN_GUARD_APP_DIR:-/var/lib/jaspernode}"
GUARD_DATA_DIR="${JN_GUARD_DATA_DIR:-${GUARD_APP_DIR}/data}"
BINARY="${GUARD_APP_DIR}/jaspernode"
VERSION_RECORD="${GUARD_APP_DIR}/.version"
MARKER="${GUARD_DATA_DIR}/update_pending.json"
COUNTER="${GUARD_DATA_DIR}/.update_boot_attempts"
ROLLED_BACK="${GUARD_DATA_DIR}/rolled_back.json"

if [ ! -f "$MARKER" ]; then
    rm -f "$COUNTER"
    exit 0
fi

ATTEMPTS=$(cat "$COUNTER" 2>/dev/null || echo 0)
# Corrupted/non-numeric counter content resets the budget instead of letting
# the arithmetic below error out into (effectively) the rollback branch.
case "$ATTEMPTS" in
    ''|*[!0-9]*) ATTEMPTS=0 ;;
esac
ATTEMPTS=$((ATTEMPTS + 1))
echo "$ATTEMPTS" > "$COUNTER"

if [ "$ATTEMPTS" -le 3 ]; then
    exit 0
fi

if [ -f "${BINARY}.old" ]; then
    echo "jn-guard: pending update crash-looped (${ATTEMPTS} starts) — restoring previous binary"
    mv -f "${BINARY}.old" "$BINARY"
    chmod +x "$BINARY"
    # The version record moves back with the binary it describes.
    [ -f "${VERSION_RECORD}.old" ] && mv -f "${VERSION_RECORD}.old" "$VERSION_RECORD"
    # The pending marker is single-line JSON — append the attempts count and
    # hand it to the node as the rolled-back record (surfaced on next boot).
    sed 's/}$/,"attempts":'"$ATTEMPTS"'}/' "$MARKER" > "$ROLLED_BACK" 2>/dev/null \
        || cp "$MARKER" "$ROLLED_BACK"
    rm -f "$MARKER" "$COUNTER"
else
    echo "jn-guard: pending update crash-looped (${ATTEMPTS} starts) but no ${BINARY}.old to restore — recording failure, not restoring"
    sed 's/}$/,"attempts":'"$ATTEMPTS"',"reason":"no_old_binary"}/' "$MARKER" > "$ROLLED_BACK" 2>/dev/null \
        || cp "$MARKER" "$ROLLED_BACK"
    rm -f "$MARKER" "$COUNTER"
fi
exit 0
# --- jn-guard end ---
EOF
chmod +x "${APP_DIR}/jn-guard.sh"
info "✓ jn-guard.sh created"

# Configure sudoers to allow jaspernode user to restart the service without password
info "Configuring sudoers to allow service restart without password..."
cat << EOF > "/etc/sudoers.d/jaspernode"
# Allow jaspernode user to restart jaspernode service without password
${INSTALL_USER} ALL=(root) NOPASSWD: /bin/systemctl restart ${SERVICE_NAME}
${INSTALL_USER} ALL=(root) NOPASSWD: /bin/systemctl stop ${SERVICE_NAME}
${INSTALL_USER} ALL=(root) NOPASSWD: /bin/systemctl start ${SERVICE_NAME}
${INSTALL_USER} ALL=(root) NOPASSWD: /bin/systemctl status ${SERVICE_NAME}
EOF

# Set proper permissions on sudoers file
chmod 440 "/etc/sudoers.d/jaspernode"

# Every canonical install sheds the dev-build wiring (systemd drop-in loading
# /etc/jaspernode.env with the backdoor enrol cert). install-dev.sh layers it
# back after calling this script, so a prod install over a dev install comes
# up clean while the enrolment itself (/etc/jaspernode.env) is kept for the
# next dev cycle. The data dir is untouched — the node reopens the same DB.
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service.d/override.conf" ]; then
    info "Removing dev-build systemd drop-in (prod installs run without it)..."
    rm -f "/etc/systemd/system/${SERVICE_NAME}.service.d/override.conf"
    rmdir "/etc/systemd/system/${SERVICE_NAME}.service.d" 2>/dev/null || true
fi

info "Reloading systemd, enabling and starting service..."
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
systemctl restart "${SERVICE_NAME}"

echo
echo -e "${C_GREEN}✔ JasperNode was installed and started successfully!${C_RESET}"
echo
info "Installation completed:"
echo -e "  - Service runs as user: ${C_CYAN}${INSTALL_USER}${C_RESET} (not root)"
echo -e "  - Security: ${C_CYAN}NoNewPrivileges=yes${C_RESET} (prevents privilege escalation)"
echo -e "  - Capabilities: ${C_CYAN}SYS_NICE, SYS_RESOURCE, DAC_OVERRIDE, FOWNER${C_RESET}"
echo -e "  - Priority: ${C_CYAN}Nice=-7, IOSchedulingClass=1${C_RESET} (high priority for industrial control)"
echo -e "  - Self-updates: ${C_CYAN}Automatic - no manual intervention required${C_RESET}"
echo -e "  - Service restart: ${C_CYAN}No sudo required - configured via sudoers${C_RESET}"
echo
info "You can manage the service with these commands:"
echo -e "  - Check status: ${C_YELLOW}systemctl status ${SERVICE_NAME}${C_RESET}"
echo -e "  - View logs:    ${C_YELLOW}sudo journalctl -u ${SERVICE_NAME} -f${C_RESET}"
echo -e "  - Stop service:   ${C_YELLOW}sudo systemctl stop ${SERVICE_NAME}${C_RESET}"
echo -e "  - Restart service:   ${C_YELLOW}sudo systemctl restart ${SERVICE_NAME}${C_RESET}"
echo
info "Or use the JasperNode Service Manager (jnsm):"
echo -e "  - Service status: ${C_YELLOW}jnsm status${C_RESET}"
echo -e "  - View logs:   ${C_YELLOW}jnsm logs${C_RESET}"
echo -e "  - Follow logs: ${C_YELLOW}jnsm logs-follow${C_RESET}"
echo -e "  - Check install: ${C_YELLOW}jnsm check${C_RESET}"
echo -e "  - Show version: ${C_YELLOW}jnsm version${C_RESET}"
echo -e "  - Get help:    ${C_YELLOW}jnsm help${C_RESET}"
echo
