#!/bin/bash
#
# install-spols-ve.sh - Installer for the Spols-VE stack
# Compatible with RPM-based distributions (RHEL, Oracle Linux, Rocky,
# AlmaLinux, Fedora) and Debian-based distributions (Debian, Ubuntu).
#
# What it does:
#   - Detects the distribution family and installs system dependencies
#   - Creates /VMSTORE and its subdirectories (mandatory: shared cluster
#     storage, used both for VM disks and for spols-guard's JSON state files)
#   - Installs the binaries into /usr/bin
#   - Installs example configuration files into /etc/spols (without
#     overwriting configuration that is already present)
#   - Creates the required log directories
#   - Installs the Cockpit plugin (web UI) if the files are present
#   - Installs the systemd units for the three daemons
#   - Installs the logrotate configuration
#   - Can uninstall everything (--uninstall) and check the consistency of
#     the security material (CA/tokens) across cluster nodes (--check-consistency)
#
# Usage:
#   sudo ./install-spols-ve.sh [options]
#
# Install options:
#   --bin-dir <path>      Directory containing the compiled binaries
#                          (default: same directory as this script)
#   --ui-dir <path>       Directory containing the Cockpit plugin files
#                          (default: same directory as this script)
#   --skip-deps           Do not install system dependencies (useful on
#                          environments that are already provisioned/hardened,
#                          where package installation is handled elsewhere)
#   --open-firewall       Opens ports 8080/8081/8082 via firewalld/ufw if
#                          present (in addition to whatever spolsnet already
#                          manages)
#   --start                Enables and starts the systemd services right away
#                          (not recommended until PKI/tokens/config are ready -
#                          see the final summary)
#
# Uninstall options:
#   --uninstall            Stops/disables the services, removes binaries,
#                           systemd units, the Cockpit plugin and logrotate
#                           configs. Does NOT touch /etc/spols (config/PKI/
#                           tokens) nor /VMSTORE (VM disks) unless combined with:
#   --purge-config          (with --uninstall) Also removes /etc/spols
#   --purge-data             (with --uninstall) Also wipes /VMSTORE - asks for
#                            explicit confirmation by typing "DELETE", because
#                            it holds the disks of ALL VMs in the cluster
#
# Consistency check options:
#   --check-consistency    Locally checks presence/permissions of the shared
#                           CA and tokens (under /etc/spols). Installs nothing.
#   --nodes <ip1,ip2,...>  (with --check-consistency) Compares the hash of the
#                           CA and tokens against the other listed nodes over
#                           SSH, to catch a mismatched token/CA ahead of time
#                           (the kind of thing that otherwise shows up as a
#                           wave of 401 errors). Requires working key-based
#                           SSH access.
#   --ssh-user <user>      SSH user for --nodes (default: root)
#
#   -h, --help             Show this message
#
set -euo pipefail

# ==========================================
# CONFIGURATION
# ==========================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_DIR="$SCRIPT_DIR"
UI_DIR="$SCRIPT_DIR"
SKIP_DEPS=0
OPEN_FIREWALL=0
START_SERVICES=0
UNINSTALL=0
PURGE_CONFIG=0
PURGE_DATA=0
CHECK_CONSISTENCY=0
CHECK_NODES=""
SSH_USER="root"

BINARIES=(spols-guard spols-boot spols-vncproxy spols-io spolsnet spols-pki)
UI_FILES=(index.html app.js manifest.json operations.html operations.js spols-ve.css storage.html storage.js)

VMSTORE_DIR="/VMSTORE"
VMSTORE_SUBDIRS=(vms-xml scores novnc imports exports)

ETC_DIR="/etc/spols"
PKI_DIR="$ETC_DIR/pki"
SHARED_PKI_DIR="$VMSTORE_DIR/.pki"

LOG_DIR_GUARD="/var/log/spols-ve"
COCKPIT_APP_DIR="/usr/share/cockpit/spols-ve"

# ==========================================
# HELPERS
# ==========================================

log()  { echo "[INSTALL] $*"; }
warn() { echo "[INSTALL-WARNING] $*" >&2; }
die()  { echo "[INSTALL-FATAL] $*" >&2; exit 1; }

require_root() {
    if [ "$(id -u)" -ne 0 ]; then
        die "This script must be run as root (use sudo)."
    fi
}

usage() {
    sed -n '2,/^set -euo pipefail/p' "$0" | grep '^#' | sed -e 's/^# \{0,1\}//'
    exit 0
}

# ==========================================
# ARGUMENT PARSING
# ==========================================

while [ $# -gt 0 ]; do
    case "$1" in
        --bin-dir)          BIN_DIR="$2"; shift 2 ;;
        --ui-dir)           UI_DIR="$2"; shift 2 ;;
        --skip-deps)        SKIP_DEPS=1; shift ;;
        --open-firewall)    OPEN_FIREWALL=1; shift ;;
        --start)            START_SERVICES=1; shift ;;
        --uninstall)        UNINSTALL=1; shift ;;
        --purge-config)     PURGE_CONFIG=1; shift ;;
        --purge-data)       PURGE_DATA=1; shift ;;
        --check-consistency) CHECK_CONSISTENCY=1; shift ;;
        --nodes)            CHECK_NODES="$2"; shift 2 ;;
        --ssh-user)         SSH_USER="$2"; shift 2 ;;
        -h|--help)          usage ;;
        *) die "Unknown option: $1 (use --help)" ;;
    esac
done

# ==========================================
# DISTRIBUTION DETECTION
# ==========================================

DISTRO_FAMILY=""   # "rpm" or "deb"
PKG_INSTALL=""

detect_distro() {
    if [ ! -f /etc/os-release ]; then
        die "Unable to detect the distribution: /etc/os-release not found."
    fi
    # shellcheck disable=SC1091
    . /etc/os-release

    local id="${ID:-}"
    local id_like="${ID_LIKE:-}"

    case "$id $id_like" in
        *rhel*|*fedora*|*ol*|*centos*|*rocky*|*almalinux*)
            DISTRO_FAMILY="rpm"
            ;;
        *debian*|*ubuntu*)
            DISTRO_FAMILY="deb"
            ;;
        *)
            # Fallback: some derivatives only report ID_LIKE
            if echo "$id_like" | grep -qi "rhel\|fedora"; then
                DISTRO_FAMILY="rpm"
            elif echo "$id_like" | grep -qi "debian"; then
                DISTRO_FAMILY="deb"
            else
                die "Unrecognized distribution (ID=$id, ID_LIKE=$id_like). Supported: RHEL-based, Debian-based."
            fi
            ;;
    esac

    log "Detected distribution: ${PRETTY_NAME:-$id} (family: $DISTRO_FAMILY)"

    if [ "$DISTRO_FAMILY" = "rpm" ]; then
        if command -v dnf >/dev/null 2>&1; then
            PKG_INSTALL="dnf install -y"
        else
            PKG_INSTALL="yum install -y"
        fi
    else
        PKG_INSTALL="apt-get install -y"
    fi
}

# ==========================================
# SYSTEM DEPENDENCIES
# ==========================================

install_dependencies() {
    if [ "$SKIP_DEPS" -eq 1 ]; then
        log "Dependency installation skipped (--skip-deps)."
        return
    fi

    log "Installing system dependencies (family: $DISTRO_FAMILY)..."

    if [ "$DISTRO_FAMILY" = "rpm" ]; then
        local pkgs=(
            qemu-kvm libvirt libvirt-daemon-kvm libvirt-client
            bridge-utils
            glusterfs-fuse
            cockpit
            openssl
            iptables
            NetworkManager
            logrotate
        )
        # shellcheck disable=SC2086
        $PKG_INSTALL "${pkgs[@]}" || warn "Some packages may not be available under these names on this release: please check manually."
        systemctl enable --now libvirtd 2>/dev/null || true
        systemctl enable --now cockpit.socket 2>/dev/null || true
    else
        export DEBIAN_FRONTEND=noninteractive
        apt-get update -y
        local pkgs=(
            qemu-kvm libvirt-daemon-system libvirt-clients
            bridge-utils
            glusterfs-client
            cockpit
            openssl
            iptables
            network-manager
            logrotate
        )
        # shellcheck disable=SC2086
        $PKG_INSTALL "${pkgs[@]}" || warn "Some packages may not be available under these names on this release: please check manually."
        systemctl enable --now libvirtd 2>/dev/null || systemctl enable --now libvirt-daemon 2>/dev/null || true
        systemctl enable --now cockpit.socket 2>/dev/null || true
    fi
}

# ==========================================
# DIRECTORIES (VMSTORE, /etc/spols, logs)
# ==========================================

create_directories() {
    log "Creating directories..."

    if [ -d "$VMSTORE_DIR" ]; then
        log "$VMSTORE_DIR already present."
    else
        mkdir -p "$VMSTORE_DIR"
        log "$VMSTORE_DIR created."
    fi

    if ! mountpoint -q "$VMSTORE_DIR" 2>/dev/null; then
        warn "$VMSTORE_DIR does not appear to be a shared-storage mountpoint (e.g. GlusterFS)."
        warn "For a multi-node cluster, $VMSTORE_DIR MUST be storage shared across all nodes,"
        warn "otherwise the cluster state (spols-state.json) and VM disks will not be visible"
        warn "to the other nodes. If this is a single test node, you can ignore this warning."
    fi

    for d in "${VMSTORE_SUBDIRS[@]}"; do
        mkdir -p "$VMSTORE_DIR/$d"
    done
    chmod 755 "$VMSTORE_DIR"

    # Shared CA directory: holds ca.crt AND ca.key, so any node can sign its
    # own certificate locally without copying the private key around. Kept
    # separate from VMSTORE_SUBDIRS above because it needs 700, not 755.
    mkdir -p "$SHARED_PKI_DIR"
    chmod 700 "$SHARED_PKI_DIR"

    mkdir -p "$ETC_DIR"
    mkdir -p "$PKI_DIR"
    chmod 750 "$ETC_DIR"
    chmod 700 "$PKI_DIR"

    mkdir -p "$LOG_DIR_GUARD"
    chmod 755 "$LOG_DIR_GUARD"

    # spols-boot writes directly to a file, not a directory: make sure it
    # exists with the right permissions before the first start.
    touch /var/log/spols-boot.log
    chmod 640 /var/log/spols-boot.log

    log "Directories ready: $VMSTORE_DIR, $SHARED_PKI_DIR, $ETC_DIR, $PKI_DIR, $LOG_DIR_GUARD"
}

# ==========================================
# BINARIES
# ==========================================

install_binaries() {
    log "Installing binaries from $BIN_DIR into /usr/bin..."
    local missing=()

    for bin in "${BINARIES[@]}"; do
        if [ -f "$BIN_DIR/$bin" ]; then
            install -m 0755 -o root -g root "$BIN_DIR/$bin" "/usr/bin/$bin"
            log "  $bin installed."
        else
            missing+=("$bin")
        fi
    done

    if [ "${#missing[@]}" -gt 0 ]; then
        warn "Binaries not found in $BIN_DIR (skipped): ${missing[*]}"
        warn "Use --bin-dir to point at the right directory, or copy them into /usr/bin manually."
    fi
}

# ==========================================
# CONFIGURATION (without overwriting existing files)
# ==========================================

install_config_example() {
    local example_file="$1"
    local target_name="$2"

    if [ ! -f "$SCRIPT_DIR/$example_file" ]; then
        warn "$example_file not found next to the script: $target_name configuration not installed."
        return
    fi

    # Always copy the .example file for reference/updating
    install -m 0644 -o root -g root "$SCRIPT_DIR/$example_file" "$ETC_DIR/$example_file"

    if [ -f "$ETC_DIR/$target_name" ]; then
        log "  $ETC_DIR/$target_name already present: not overwritten (see $ETC_DIR/$example_file for the available parameters)."
    else
        log "  $ETC_DIR/$target_name not present: NOT created automatically (it requires manual choices, e.g. management_ip)."
        log "  Copy $ETC_DIR/$example_file to $ETC_DIR/$target_name and adjust it before starting the services."
    fi
}

install_configs() {
    log "Installing configuration into $ETC_DIR..."
    install_config_example "guard.json.example"     "guard.json"
    install_config_example "boot.json.example"       "boot.json"
    install_config_example "vncproxy.json.example"   "vncproxy.json"
    log "  spols-pki (CA/certificate/token generation for TLS and inter-daemon authentication)"
    log "  is installed as a regular binary in /usr/bin, alongside the other Spols-VE tools."
}

# ==========================================
# LOGROTATE
# ==========================================

install_logrotate_config() {
    log "Installing logrotate configuration..."

    if [ -f "$SCRIPT_DIR/spols-guard.logrotate" ]; then
        install -m 0644 -o root -g root "$SCRIPT_DIR/spols-guard.logrotate" "/etc/logrotate.d/spols-guard"
    else
        cat > /etc/logrotate.d/spols-guard <<'EOF'
/var/log/spols-ve/spols-guard.log {
    weekly
    rotate 8
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}
EOF
    fi

    cat > /etc/logrotate.d/spols-boot <<'EOF'
/var/log/spols-boot.log {
    weekly
    rotate 8
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}
EOF

    log "  Logrotate configuration installed for spols-guard and spols-boot."
}

# ==========================================
# COCKPIT PLUGIN (WEB UI)
# ==========================================

install_ui() {
    local found=0
    for f in "${UI_FILES[@]}"; do
        [ -f "$UI_DIR/$f" ] && found=1
    done

    if [ "$found" -eq 0 ]; then
        warn "No UI files found in $UI_DIR: Cockpit plugin not installed (use --ui-dir if needed)."
        return
    fi

    log "Installing Cockpit plugin into $COCKPIT_APP_DIR..."
    mkdir -p "$COCKPIT_APP_DIR"

    for f in "${UI_FILES[@]}"; do
        if [ -f "$UI_DIR/$f" ]; then
            install -m 0644 -o root -g root "$UI_DIR/$f" "$COCKPIT_APP_DIR/$f"
        else
            warn "  Missing UI file (skipped): $f"
        fi
    done

    log "  Cockpit plugin installed. You may need to refresh the Cockpit session to see it in the menu."
}

# ==========================================
# SYSTEMD UNITS
# ==========================================

install_systemd_units() {
    log "Installing systemd units..."

    cat > /etc/systemd/system/spols-boot.service <<'EOF'
[Unit]
Description=Spols-VE Boot Resilience & Eco-Manager Agent
After=network-online.target libvirtd.service
Wants=network-online.target
Documentation=https://localhost:9090
# Limite esplicito ai riavvii ravvicinati: se il demone continua a fallire,
# si ferma dopo 5 tentativi in 30s (stato "failed") invece di ritentare
# all'infinito. Dopo aver risolto la causa: systemctl reset-failed <unit>.
StartLimitIntervalSec=30
StartLimitBurst=5

[Service]
Type=simple
ExecStart=/usr/bin/spols-boot
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target
EOF

    cat > /etc/systemd/system/spols-vncproxy.service <<'EOF'
[Unit]
Description=Spols-VE Lightweight VNC WebSocket Proxy
After=network-online.target libvirtd.service
Wants=network-online.target
StartLimitIntervalSec=30
StartLimitBurst=5

[Service]
Type=simple
ExecStart=/usr/bin/spols-vncproxy
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target
EOF

    cat > /etc/systemd/system/spols-guard.service <<'EOF'
[Unit]
Description=Spols-VE High Availability Guard Daemon
Documentation=https://localhost:9090
After=network.target libvirtd.service
Requires=libvirtd.service
# LA DIRETTIVA CHIAVE:
# systemd controllera' /etc/fstab e i mount correnti. Attendera' automaticamente
# la risoluzione di iSCSI, FC, NFS o GlusterFS prima di lanciare il demone.
RequiresMountsFor=/VMSTORE
# Limite esplicito ai riavvii ravvicinati: se il demone continua a fallire
# (es. licenza non valida, config errata) si ferma dopo 5 tentativi in 30s
# (stato "failed") invece di ritentare all'infinito. Dopo aver risolto la
# causa: systemctl reset-failed spols-guard && systemctl start spols-guard.
StartLimitIntervalSec=30
StartLimitBurst=5

[Service]
Type=simple
ExecStart=/usr/bin/spols-guard
User=root
Group=root
# Un arresto RICHIESTO dall'amministratore (systemctl stop) non attiva mai
# il riavvio: systemd lo riconosce come intenzionale.
Restart=always
RestartSec=5
# Aumentiamo il limite dei file aperti per supportare carichi elevati
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
    log "  Systemd units installed: spols-boot.service, spols-vncproxy.service, spols-guard.service"
}

# ==========================================
# FIREWALL (optional)
# ==========================================

open_firewall_ports() {
    if [ "$OPEN_FIREWALL" -eq 0 ]; then
        return
    fi

    log "Opening application ports (8080, 8081, 8082)..."

    if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld 2>/dev/null; then
        for p in 8080 8081 8082; do
            firewall-cmd --permanent --add-port="${p}/tcp" || true
        done
        firewall-cmd --reload || true
        log "  firewalld rules applied."
    elif command -v ufw >/dev/null 2>&1; then
        for p in 8080 8081 8082; do
            ufw allow "${p}/tcp" || true
        done
        log "  ufw rules applied."
    else
        warn "No known firewall manager active (firewalld/ufw). Remember that spolsnet still manages its own dedicated iptables rules: consider 'spolsnet fw allow <port>/tcp' for access from other cluster nodes."
    fi
}

# ==========================================
# START SERVICES (optional)
# ==========================================

maybe_start_services() {
    if [ "$START_SERVICES" -eq 0 ]; then
        log "Services NOT started automatically (use --start to do so, after completing PKI/config setup)."
        return
    fi

    log "Enabling and starting services..."
    systemctl enable --now spols-boot.service
    systemctl enable --now spols-vncproxy.service
    systemctl enable --now spols-guard.service
}

# ==========================================
# FINAL SUMMARY
# ==========================================

print_summary() {
    cat <<EOF

===================================================================
 Spols-VE installation complete
===================================================================

Next steps BEFORE starting the services (if not already done):

  1. Verify that $VMSTORE_DIR is mounted on the actual shared cluster
     storage (GlusterFS or equivalent) on ALL nodes.

  2. Generate the CA ONCE, from any single node (it is stored on shared
     storage, at $SHARED_PKI_DIR - every node can already reach it):
       spols-pki ca

  3. On EVERY node (including the one from step 2), run locally - each node
     signs its own certificate using the shared CA, nothing is copied
     between nodes for this part:
       spols-pki node \$(hostname) <management-IP>
       spols-pki cockpit \$(hostname) <management-IP>

  4. Generate the two shared tokens ONCE, then copy them IDENTICAL to every
     node (see --to, or copy manually):
       spols-pki token
       spols-pki api-token

  5. Copy/adapt the configuration files under $ETC_DIR:
       $ETC_DIR/guard.json      (from guard.json.example - management_ip required)
       $ETC_DIR/boot.json       (from boot.json.example)
       $ETC_DIR/vncproxy.json   (from vncproxy.json.example)
     For a lab environment with no Common Criteria/FIPS constraints, set
     "require_tls": false and "require_auth": false in the respective
     files to fall back to no TLS/authentication.

  6. Start the services:
       systemctl enable --now spols-boot spols-vncproxy spols-guard

Logs:
  - spols-guard : $LOG_DIR_GUARD/spols-guard.log (in addition to journalctl -u spols-guard)
  - spols-boot  : /var/log/spols-boot.log (in addition to journalctl -u spols-boot)
  - spols-vncproxy : journalctl -u spols-vncproxy (stdout/journald)

===================================================================
EOF
}

# ==========================================
# UNINSTALL
# ==========================================

uninstall() {
    log "Uninstalling Spols-VE..."

    for svc in spols-guard spols-vncproxy spols-boot; do
        systemctl stop "$svc" 2>/dev/null || true
        systemctl disable "$svc" 2>/dev/null || true
    done

    rm -f /etc/systemd/system/spols-guard.service
    rm -f /etc/systemd/system/spols-vncproxy.service
    rm -f /etc/systemd/system/spols-boot.service
    systemctl daemon-reload

    for bin in "${BINARIES[@]}"; do
        rm -f "/usr/bin/$bin"
    done

    rm -rf "$COCKPIT_APP_DIR"
    rm -f /etc/logrotate.d/spols-guard /etc/logrotate.d/spols-boot

    log "Services, binaries (including spols-pki), Cockpit plugin, and logrotate configs removed."

    if [ "$PURGE_CONFIG" -eq 1 ]; then
        warn "Removing $ETC_DIR (config, PKI, tokens) as requested by --purge-config..."
        rm -rf "${ETC_DIR:?}"
        log "  $ETC_DIR removed."
    else
        log "$ETC_DIR (configuration, PKI, tokens) NOT removed. Use --purge-config to delete it."
    fi

    if [ "$PURGE_DATA" -eq 1 ]; then
        echo
        echo "WARNING: you are about to wipe $VMSTORE_DIR, which may contain the disks"
        echo "of ALL virtual machines in the cluster and the shared state."
        read -r -p "Type DELETE (all caps) to confirm, anything else to cancel: " confirm
        if [ "$confirm" = "DELETE" ]; then
            rm -rf "${VMSTORE_DIR:?}"/*
            warn "$VMSTORE_DIR wiped."
        else
            log "Confirmation not received: $VMSTORE_DIR NOT touched."
        fi
    else
        log "$VMSTORE_DIR NOT touched (it holds the VM disks). Use --purge-data to explicitly wipe it (with confirmation)."
    fi

    log "Uninstall complete."
}

# ==========================================
# CONSISTENCY CHECK (CA freshness + tokens across nodes)
# ==========================================

# Tokens are genuine copies that must be byte-for-byte identical across all
# nodes (they are shared secrets, generated once and distributed) - this
# still requires an SSH-based comparison. The CA is no longer copied node to
# node (it lives once on shared storage), so it needs a different check: is
# this node's LOCAL ca.crt (installed by "spols-pki node/cockpit") still in
# sync with the shared master copy, or stale because a CA rotation happened
# and this node hasn't re-run spols-pki since? That comparison needs no SSH
# at all, since shared storage is already mounted locally.
CONSISTENCY_FILES=(
    "$ETC_DIR/boot.token"
    "$ETC_DIR/guard-api.token"
)

check_ca_freshness() {
    local shared_ca="$SHARED_PKI_DIR/ca.crt"
    local local_ca="$PKI_DIR/ca.crt"

    if [ ! -f "$shared_ca" ]; then
        warn "  $shared_ca: MISSING (has 'spols-pki ca' been run yet? is shared storage mounted?)"
        return 1
    fi
    if [ ! -f "$local_ca" ]; then
        warn "  $local_ca: MISSING (run 'spols-pki node ...' / 'spols-pki cockpit ...' on this node)"
        return 1
    fi

    local shared_hash local_hash
    shared_hash=$(sha256sum "$shared_ca" | awk '{print $1}')
    local_hash=$(sha256sum "$local_ca" | awk '{print $1}')

    if [ "$shared_hash" = "$local_hash" ]; then
        log "  $local_ca: OK (matches shared CA at $shared_ca)"
        return 0
    else
        warn "  $local_ca is STALE: does not match the current shared CA at $shared_ca."
        warn "    Likely cause: the CA was rotated (spols-pki ca --force) after this node's certificate"
        warn "    was last issued. Re-run 'spols-pki node ...' / 'spols-pki cockpit ...' on this node."
        return 1
    fi
}

check_local_file() {
    local f="$1"
    if [ ! -f "$f" ]; then
        warn "  $f: MISSING"
        return 1
    fi

    local perm
    perm=$(stat -c '%a' "$f" 2>/dev/null || stat -f '%Lp' "$f" 2>/dev/null || echo "??")
    local hash
    hash=$(sha256sum "$f" 2>/dev/null | awk '{print $1}')

    log "  $f: present, permissions=$perm, sha256=${hash:0:16}..."

    case "$f" in
        *token*)
            if [ "$perm" != "600" ]; then
                warn "    Unexpected permissions $perm for a secret: should be 600 (root only)."
            fi
            ;;
    esac
    return 0
}

check_consistency() {
    log "Checking CA freshness (local copy vs. shared master copy)..."
    check_ca_freshness || true

    log "Checking local consistency of shared tokens..."
    for f in "${CONSISTENCY_FILES[@]}"; do
        check_local_file "$f" || true
    done

    if [ -z "$CHECK_NODES" ]; then
        log "No remote nodes specified (--nodes ip1,ip2,...): check limited to this machine."
        return
    fi

    if ! command -v ssh >/dev/null 2>&1; then
        warn "SSH client not found: cannot compare against remote nodes."
        return
    fi

    log "Comparing against remote nodes (SSH user: $SSH_USER)..."
    local mismatch_found=0

    IFS=',' read -ra NODES <<< "$CHECK_NODES"
    for node in "${NODES[@]}"; do
        node="$(echo "$node" | xargs)"
        [ -z "$node" ] && continue
        log "  Node $node:"

        if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "${SSH_USER}@${node}" true 2>/dev/null; then
            warn "    Unable to connect over SSH (check passwordless key-based access for ${SSH_USER}@${node})."
            continue
        fi

        for f in "${CONSISTENCY_FILES[@]}"; do
            local local_hash remote_hash
            local_hash=$(sha256sum "$f" 2>/dev/null | awk '{print $1}')
            remote_hash=$(ssh -o BatchMode=yes -o ConnectTimeout=5 "${SSH_USER}@${node}" \
                "sha256sum '$f' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)

            if [ -z "$local_hash" ] && [ -z "$remote_hash" ]; then
                warn "    $f: missing both here and on $node"
            elif [ -z "$local_hash" ]; then
                warn "    $f: missing on this node but present on $node"
            elif [ -z "$remote_hash" ]; then
                warn "    $f: present here but missing on $node"
            elif [ "$local_hash" = "$remote_hash" ]; then
                log "    $f: OK (matches $node)"
            else
                warn "    $f: DIFFERS between this node and $node! Authenticated communication between the two will fail with 401."
                mismatch_found=1
            fi
        done
    done

    if [ "$mismatch_found" -eq 1 ]; then
        warn "Inconsistencies found: fix them by copying the correct (identical) file to all nodes before starting the services."
    else
        log "No inconsistencies found across the checked nodes."
    fi
}

# ==========================================
# MAIN
# ==========================================

require_root
detect_distro

if [ "$UNINSTALL" -eq 1 ]; then
    uninstall
    exit 0
fi

if [ "$CHECK_CONSISTENCY" -eq 1 ]; then
    check_consistency
    exit 0
fi

if [ "$PURGE_CONFIG" -eq 1 ] || [ "$PURGE_DATA" -eq 1 ]; then
    die "--purge-config and --purge-data only take effect together with --uninstall."
fi

install_dependencies
create_directories
install_binaries
install_configs
install_logrotate_config
install_ui
install_systemd_units
open_firewall_ports
maybe_start_services
print_summary