#!/bin/bash

# This file is part of Gluesync.
#
# Gluesync is dual-licensed under the following licenses:
#
# 1. GNU General Public License (GPL) Version 3
#    You may use, modify, and distribute this software under the terms of the GPL v3.
#    See the LICENSE-GPL file or <http://www.gnu.org/licenses/gpl-3.0.html> for details.
#    This option is available at no cost, but any derivative works must also be licensed under GPL v3.
#
# 2. MOLO17 Commercial License
#    Alternatively, you may use this software under the MOLO17 Commercial License,
#    which includes a warranty and permits proprietary use. Contact MOLO17 at info@molo17.com
#    for licensing terms and conditions.
#
# You must choose one of these licenses to use this software. Using this software implies
# acceptance of one of these licenses. See the accompanying LICENSE files or contact
# MOLO17 for more information.
#
# Copyright (C) 2026 MOLO17. All rights reserved.

# ---------------------------------------------------------------------------------------------------------------------
# Unattended / non-interactive use (cloud-init, CI, ssh without a tty, curl | bash). Every prompt has an env answer:
#   GLUESYNC_KIT_ID=<12 hex>   kit ID from the download email (skips the kit ID prompt)
#   GLUESYNC_SETUP=y|n         "proceed with Gluesync setup?"; without a terminal: y when GLUESYNC_KIT_ID is set, else n
#   GLUESYNC_START=y|n         "start Gluesync now?"; without a terminal: n
#   GLUESYNC_NONINTERACTIVE=1  never prompt, even when a terminal is available
# Prompts read from stdin when it is a terminal, else from /dev/tty when the process has one (curl | sudo bash in a
# terminal), else the defaults above apply and are logged. With sudo, pass the variables explicitly:
#   sudo GLUESYNC_KIT_ID=a1b2c3d4e5f6 GLUESYNC_START=y bash docker-installer.sh
# ---------------------------------------------------------------------------------------------------------------------
GS_TTY=""
case "$(printf '%s' "${GLUESYNC_NONINTERACTIVE:-}" | tr '[:upper:]' '[:lower:]')" in
    1|true|yes|on) ;;
    *)
        if [ -t 0 ]; then GS_TTY="/dev/stdin"
        elif (exec < /dev/tty) 2>/dev/null; then GS_TTY="/dev/tty"
        fi ;;
esac

# gs_ask <prompt> <env answer> <default when there is no terminal> [single-char]: sets REPLY. Never fails (safe under set -e).
gs_ask() {
    local prompt="$1" answer="${2:-}" default="${3:-}" single="${4:-}"
    REPLY=""
    if [ -n "$answer" ]; then
        REPLY="$answer"
        echo "${prompt}${REPLY} (from environment)"
    elif [ -n "$GS_TTY" ]; then
        if [ -n "$single" ]; then read -r -n 1 -p "$prompt" REPLY < "$GS_TTY" || REPLY=""; echo ""
        else read -r -p "$prompt" REPLY < "$GS_TTY" || REPLY=""
        fi
    else
        REPLY="$default"
        echo "${prompt}${REPLY:-<empty>} (no terminal: default; see GLUESYNC_* variables at the top of this script)"
    fi
    return 0
}

# Clearing the screen is cosmetic: never fail without a usable TERM (clear exits 1 under set -e in cloud-init/CI).
clear 2>/dev/null || true

echo "╔══════════════════════════════════════════════════════════════╗"
echo "║                                                              ║"
echo "║      Welcome to the Gluesync by MOLO17 Container Linux       ║"
echo "║                    installer v1.3.5                          ║"
echo "║                                                              ║"
echo "║  This software is about to run the setup of the required     ║"
echo "║  environment and following it will perform the setup of      ║"
echo "║  Gluesync and run it if the user gives consent.              ║"
echo "║                                                              ║"
echo "║  This script automatically installs Docker or Podman on      ║"
echo "║  Linux systems for use with Gluesync. It supports multiple   ║"
echo "║  Linux distributions and configures the container runtime    ║"
echo "║  with optimal settings.                                      ║"
echo "║                                                              ║"
echo "║  Supported distributions: Fedora, Ubuntu, Debian, CentOS,    ║"
echo "║  Red Hat, AlmaLinux, Rocky Linux, Oracle Linux, Amazon Linux,║"
echo "║  SUSE Linux Enterprise Server, openSUSE                      ║"
echo "║                                                              ║"
echo "║  The script will:                                            ║"
echo "║    • Check for sufficient permissions (root/sudo required)   ║"
echo "║    • Detect your Linux distribution                          ║"
echo "║    • Install your chosen container runtime                   ║"
echo "║    • Configure logging to prevent disk space issues          ║"
echo "║    • Verify the installation                                 ║"
echo "║                                                              ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""

if [ -n "$GS_TTY" ]; then
    read -r -p "Press Enter to continue with the installation..." < "$GS_TTY" || true
else
    echo "Non-interactive run: continuing without the Enter prompt."
fi

echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║                                                              ║"
echo "║              Choose Your Container Runtime                   ║"
echo "║                                                              ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "Please select which container runtime you would like to use:"
echo ""
echo "  1) Docker - The industry standard container platform"
echo "     • Most widely used and supported"
echo "     • Extensive documentation and community"
echo "     • Best for general use and compatibility"
echo ""
# TEMPORARILY DISABLED - Podman option hidden
# echo "  2) Podman - A modern, secure alternative"
# echo "     • Runs containers without root privileges (rootless mode)"
# echo "     • Enhanced security with user-level isolation"
# echo "     • Compatible with Docker commands"
# echo "     • NOTE: Requires running installer with 'sudo' from a regular user account"
# echo ""

# Automatically select Docker (Podman option temporarily disabled)
CONTAINER_RUNTIME="docker"
log_info "Selected: Docker"

# Original selection code (temporarily disabled)
# while true; do
#     read -p "Enter your choice (1 for Docker, 2 for Podman): " RUNTIME_CHOICE
#     case $RUNTIME_CHOICE in
#         1)
#             CONTAINER_RUNTIME="docker"
#             log_info "Selected: Docker"
#             break
#             ;;
#         2)
#             CONTAINER_RUNTIME="podman"
#             log_info "Selected: Podman"
#             break
#             ;;
#         *)
#             log_error "Invalid choice. Please enter 1 or 2."
#             ;;
#     esac
# done

echo ""

# Container Runtime Installer Script for Linux Environments
# Based on Gluesync Installation Guide
# This script automates Docker or Podman installation and configuration on various Linux distributions
# Supports user choice between Docker (industry standard) and Podman (rootless, secure alternative)

set -e  # Exit on any error

# Installer release (reported by the install confirmation). Not "VERSION": detect_distribution() sets VERSION from /etc/os-release.
INSTALLER_VERSION="1.3.5"

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

# Logging functions
log_info() {
    echo -e "${BLUE}[INFO]${NC} $1"
}

configure_ubuntu_network_tuning() {
    if [[ ! -r /etc/os-release ]]; then
        return
    fi

    # shellcheck disable=SC1091
    . /etc/os-release

    local distro_id="${ID:-}"
    local distro_like="${ID_LIKE:-}"

    if [[ "$distro_id" != "ubuntu" && "$distro_id" != "debian" && "$distro_like" != *"ubuntu"* && "$distro_like" != *"debian"* ]]; then
        return
    fi

    local sysctl_file="/etc/sysctl.d/99-gluesync.conf"
    log_info "Applying Gluesync sysctl tuning at $sysctl_file..."

    cat << EOF | $SUDO tee "$sysctl_file" > /dev/null
net.netfilter.nf_conntrack_max = 131072
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
EOF

    local sysctl_cmd=""
    if command -v sysctl >/dev/null 2>&1; then
        sysctl_cmd=$(command -v sysctl)
    elif [[ -x /sbin/sysctl ]]; then
        sysctl_cmd="/sbin/sysctl"
    elif [[ -x /usr/sbin/sysctl ]]; then
        sysctl_cmd="/usr/sbin/sysctl"
    fi

    if [[ -n "$sysctl_cmd" ]]; then
        log_info "Reloading sysctl parameters via $sysctl_cmd --system"
        if $SUDO "$sysctl_cmd" --system; then
            log_success "Sysctl parameters applied successfully."
        else
            log_warning "Failed to reload sysctl settings automatically. Please run 'sudo sysctl --system'."
        fi
    else
        log_warning "sysctl command not found; please reload kernel parameters manually."
    fi
}

# Install Docker on SUSE Linux Enterprise Server (INCUBATING)
install_suse() {
    log_warning "[INCUBATING] SLES support is preliminary and not recommended for production use."
    log_info "Installing Docker on SUSE Linux Enterprise Server..."

    local ARCH=$(uname -m)
    local SLES_MAJOR="${VERSION%%.*}"
    [[ -z "$SLES_MAJOR" ]] && SLES_MAJOR="15"

    if [[ "$ARCH" == "s390x" ]]; then
        # Docker CE is available for s390x from official repo
        log_info "Detected s390x. Using official Docker CE repository."
        local DOCKER_REPO_ALIAS="docker-ce-stable"
        local DOCKER_REPO_URL="https://download.docker.com/linux/sles/${SLES_MAJOR}/s390x/stable"

        # Install prerequisites
        log_info "Installing prerequisites (ca-certificates-mozilla, curl, zip)..."
        if ! $SUDO zypper --non-interactive install -y ca-certificates-mozilla curl zip; then
            log_error "Failed to install prerequisites via zypper."
            exit 1
        fi

        # Add Docker repository
        log_info "Adding Docker CE repository (${DOCKER_REPO_URL})..."
        $SUDO zypper --non-interactive rr $DOCKER_REPO_ALIAS >/dev/null 2>&1 || true
        if ! $SUDO zypper --non-interactive addrepo "$DOCKER_REPO_URL" $DOCKER_REPO_ALIAS; then
            log_error "Unable to add Docker repository automatically."
            exit 1
        fi

        log_info "Refreshing Docker repository metadata..."
        if ! $SUDO zypper --non-interactive --gpg-auto-import-keys refresh $DOCKER_REPO_ALIAS; then
            log_error "Failed to refresh Docker repository."
            exit 1
        fi

        # Install Docker packages
        log_info "Installing Docker CE packages..."
        if ! $SUDO zypper --non-interactive install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then
            log_error "Docker CE package installation failed."
            exit 1
        fi
    else
        # No Docker CE repo for x86_64 SLES; use SUSE's built-in docker from Containers module
        log_info "No official Docker CE repository for SLES on ${ARCH}. Using SUSE Containers module."

        # Check registration status
        if command -v SUSEConnect > /dev/null 2>&1; then
            if ! $SUDO SUSEConnect --status 2>/dev/null | grep -q "Registered"; then
                log_error "System is not registered with SUSE. Please register first using:"
                log_info "  sudo SUSEConnect -r <REGISTRATION_CODE>"
                log_info "Then rerun this installer."
                exit 1
            fi
        fi

        # Enable Containers module
        if command -v SUSEConnect > /dev/null 2>&1 && [[ -n "$VERSION" ]]; then
            local SUSE_MODULE="sle-module-containers/${VERSION}/${ARCH}"
            log_info "Enabling SUSE Containers module (${SUSE_MODULE})..."
            if $SUDO SUSEConnect -p "$SUSE_MODULE" 2>/dev/null; then
                log_success "SUSE Containers module enabled"
            else
                log_error "Unable to enable Containers module. Please enable it manually:"
                log_info "  sudo SUSEConnect -p ${SUSE_MODULE}"
                log_info "Then rerun this installer."
                exit 1
            fi
        else
            log_error "SUSEConnect not found or VERSION not detected. Cannot enable Containers module."
            exit 1
        fi

        # Refresh repositories
        log_info "Refreshing repositories..."
        if ! $SUDO zypper --non-interactive refresh; then
            log_error "Repository refresh failed. Ensure the system is registered and has network access."
            exit 1
        fi

        # Install docker from SUSE repos
        log_info "Installing docker package from SUSE repositories..."
        if ! $SUDO zypper --non-interactive install -y docker zip jq; then
            log_error "Docker installation failed. Ensure the Containers module is enabled and the system is registered."
            exit 1
        fi
    fi

    # Enable and start Docker
    log_info "Enabling and starting Docker service..."
    $SUDO systemctl enable --now docker

    # Add user to docker group (optional)
    if id -u "$USER" >/dev/null 2>&1; then
        log_info "Adding $USER to docker group..."
        $SUDO usermod -aG docker "$USER" || log_warning "Unable to add $USER to docker group automatically."
        log_info "Run 'newgrp docker' or log out/in to use Docker without sudo."
    fi

    log_success "Docker installed successfully on SUSE Linux Enterprise Server"
}

# Install Docker on openSUSE
install_opensuse() {
    log_info "Installing Docker on openSUSE..."

    # Refresh repositories
    log_info "Refreshing repositories..."
    if ! $SUDO zypper --non-interactive refresh; then
        log_error "Repository refresh failed."
        exit 1
    fi

    # Install docker from openSUSE repos
    log_info "Installing docker package..."
    if ! $SUDO zypper --non-interactive install -y docker zip jq; then
        log_error "Docker installation failed."
        exit 1
    fi

    # Install docker-compose standalone (works with podman compatibility layer); skipped when Compose is already there
    # (the pre-flight does not probe github.com in that case)
    if gs_pre_compose_present; then
        log_info "Docker Compose is already installed; skipping its download."
    else
        log_info "Installing docker-compose standalone..."
        local ARCH=$(uname -m)
        if ! $SUDO curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-${ARCH}" -o /usr/local/bin/docker-compose; then
            log_error "Failed to download docker-compose."
            exit 1
        fi
        $SUDO chmod +x /usr/local/bin/docker-compose
        log_success "docker-compose installed"
    fi

    # Enable and start Docker
    log_info "Enabling and starting Docker service..."
    $SUDO systemctl enable --now docker

    # Add user to docker group
    if id -u "$USER" >/dev/null 2>&1; then
        log_info "Adding $USER to docker group..."
        $SUDO usermod -G docker -a "$USER" || log_warning "Unable to add $USER to docker group automatically."
        log_info "Run 'newgrp docker' or log out/in to use Docker without sudo."
    fi

    log_success "Docker installed successfully on openSUSE"
}

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

log_warning() {
    echo -e "${YELLOW}[WARNING]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# >>> gs-telemetry (install confirmation: anonymous install-success telemetry, contract v1.1)
# Sent once after "Setup completed". Non-blocking: never fails the install or changes the exit code; up to 3 attempts
# (2 s then 5 s backoff), each 3 s connect / 5 s total, hard 20 s budget. Retries only on network errors, 5xx and 429
# (retryAfter <= 10 s). No hostnames, usernames, IPs, MACs, machine IDs, paths or licence contents are sent.
# Opt-out: GLUESYNC_TELEMETRY=0. Lab runs: GLUESYNC_TELEMETRY_TEST=1 adds "test": true to the body.
# (with sudo, pass the variables through: sudo GLUESYNC_TELEMETRY_TEST=1 bash docker-installer.sh)
GS_TELEMETRY_ENDPOINT="https://api.backoffice.molo17.com/install-events"

gs_tel_lower() { printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]'; }
gs_tel_flag_off() { case "$(gs_tel_lower "${1:-}")" in 0|false|no|off) return 0 ;; esac; return 1; }
gs_tel_flag_on() { case "$(gs_tel_lower "${1:-}")" in 1|true|yes|on) return 0 ;; esac; return 1; }

# GLUESYNC_TELEMETRY_URL overrides the endpoint for lab/staging only: https, or plain http to localhost (fake servers).
gs_tel_uri() {
    case "${GLUESYNC_TELEMETRY_URL:-}" in
        https://*|http://127.0.0.1/*|http://127.0.0.1:*|http://localhost/*|http://localhost:*) printf '%s' "$GLUESYNC_TELEMETRY_URL" ;;
        *) printf '%s' "$GS_TELEMETRY_ENDPOINT" ;;
    esac
}

# Clean value: keep only the allowed tr set ($3), truncate to $2 chars.
gs_tel_clean() {
    local v
    v=$(printf '%s' "${1:-}" | tr -cd "${3:-[:print:]}" | tr -d '\134' | cut -c1-"${2:-64}")
    v="${v#"${v%%[![:space:]]*}"}"; v="${v%"${v##*[![:space:]]}"}"
    printf '%s' "$v"
}
# JSON string (quoted, escaped) or null.
gs_tel_json_str() {
    local v
    v=$(gs_tel_clean "${1:-}" "${2:-64}" "${3:-[:print:]}")
    if [ -z "$v" ]; then printf 'null'; return 0; fi
    v=${v//\\/\\\\}; v=${v//\"/\\\"}
    printf '"%s"' "$v"
}
gs_tel_json_ver() { gs_tel_json_str "${1:-}" 32 '0-9A-Za-z.+-'; }
# JSON integer within [$2, $3] or null.
gs_tel_json_int() {
    local v="${1:-}"
    case "$v" in ''|*[!0-9]*) printf 'null'; return 0 ;; esac
    v=$((10#$v))
    [ "$v" -lt "$2" ] && v=$2
    [ "$v" -gt "$3" ] && v=$3
    printf '%s' "$v"
}
# JSON value from an allowed list, or null.
gs_tel_json_enum() {
    local v="${1:-}" a
    shift
    for a in "$@"; do if [ "$v" = "$a" ]; then printf '"%s"' "$v"; return 0; fi; done
    printf 'null'
}

# Image tag -> version (strips -win-nanoserver-ltsc2022, -windows, -debian, ...).
gs_tel_version_from_tag() {
    printf '%s' "${1:-}" | sed -E 's/-([Ww][Ii][Nn]|[Ww]indows|nanoserver|servercore|ltsc[0-9]*|debian|alpine)(-.*)?$//'
}

# Exact bucket labels from ENV_MAPPING.md (hardware.diskFreeGbBucket); $1 = free GB (integer).
gs_tel_disk_bucket() {
    local g="${1:-}"
    case "$g" in ''|*[!0-9]*) return 0 ;; esac
    if [ "$g" -lt 50 ]; then printf '<50'
    elif [ "$g" -lt 100 ]; then printf '50-100'
    elif [ "$g" -lt 250 ]; then printf '100-250'
    elif [ "$g" -lt 500 ]; then printf '250-500'
    elif [ "$g" -lt 1000 ]; then printf '500-1000'
    else printf '>1000'
    fi
}

gs_tel_new_install_id() {
    local u=""
    if [ -r /proc/sys/kernel/random/uuid ]; then u=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || true)
    elif command -v uuidgen > /dev/null 2>&1; then u=$(uuidgen -r 2>/dev/null || uuidgen 2>/dev/null || true)
    fi
    u=$(gs_tel_lower "$u")
    if ! [[ "$u" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ ]]; then
        # Fallback: 16 random bytes from /dev/urandom formatted as a v4 UUID.
        local h
        h=$(od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')
        [ ${#h} -eq 32 ] || return 1
        u="${h:0:8}-${h:8:4}-4${h:13:3}-$(printf '%x' $(( (16#${h:16:1} & 3) | 8 )))${h:17:3}-${h:20:12}"
    fi
    printf '%s' "$u"
}

# Kit image tags: sets GS_TEL_CORE_HUB and GS_TEL_AGENTS_JSON (<= 10 agents) from the compose file(s) under $1.
gs_tel_kit_info() {
    local dir="${1:-}" f line img last repo tag name ver n=0 seen=" " agents=""
    GS_TEL_CORE_HUB=""
    GS_TEL_AGENTS_JSON="[]"
    [ -d "$dir" ] || return 0
    while IFS= read -r f; do
        while IFS= read -r line || [ -n "$line" ]; do
            img=$(printf '%s' "$line" | sed -nE "s/^[[:space:]]*image:[[:space:]]*[\"']?([^[:space:]\"']+).*/\\1/p")
            [ -n "$img" ] || continue
            repo="$img"; tag=""; last="${img##*/}"
            if [[ "$last" == *:* ]]; then tag="${last##*:}"; repo="${img%:*}"; fi
            name=$(gs_tel_lower "${repo##*/}")
            case "$name" in gluesync-*) ;; *) continue ;; esac
            ver=$(gs_tel_version_from_tag "$tag")
            case "$name" in
                gluesync-core-hub*) [ -n "$GS_TEL_CORE_HUB" ] || GS_TEL_CORE_HUB="$ver"; continue ;;
                gluesync-chronos|gluesync-conductor) continue ;;
            esac
            [[ "$name" =~ ^[a-z0-9-]{1,64}$ ]] || continue
            case "$seen" in *" $name "*) continue ;; esac
            [ "$n" -lt 10 ] || continue
            seen="$seen$name "; n=$((n + 1))
            agents="$agents${agents:+,}{\"name\":\"$name\",\"version\":$(gs_tel_json_ver "$ver")}"
        done < "$f"
    done < <(find "$dir" -maxdepth 3 -type f \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' \) 2>/dev/null | sort)
    GS_TEL_AGENTS_JSON="[$agents]"
}

# Host facts (best effort; unknown = empty). $1 = docker|podman, $2 = install dir.
gs_tel_collect_facts() {
    local runtime="${1:-docker}" dir="${2:-/opt/gluesync}" kb dev rota avail
    local -a pre=()
    [ -n "${SUDO:-}" ] && pre+=("$SUDO")
    command -v timeout > /dev/null 2>&1 && pre+=(timeout 5)
    GS_TEL_OS_NAME=""; GS_TEL_OS_VERSION=""; GS_TEL_OS_DISTRO=""; GS_TEL_OS_KERNEL=""; GS_TEL_ARCH=""
    GS_TEL_CPU=""; GS_TEL_RAM=""; GS_TEL_DISK_BUCKET=""; GS_TEL_CPU_KIND=""; GS_TEL_DISK_KIND=""
    GS_TEL_ENGINE="$runtime"; GS_TEL_ENGINE_VERSION=""; GS_TEL_COMPOSE_VERSION=""; GS_TEL_CONTAINERS=""
    if [ -r /etc/os-release ]; then
        # shellcheck disable=SC1091
        GS_TEL_OS_NAME=$(. /etc/os-release 2>/dev/null; printf '%s' "${NAME:-}")
        # shellcheck disable=SC1091
        GS_TEL_OS_VERSION=$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_ID:-}")
        # shellcheck disable=SC1091
        GS_TEL_OS_DISTRO=$(. /etc/os-release 2>/dev/null; gs_tel_lower "${ID:-}")
    fi
    GS_TEL_OS_KERNEL=$(uname -r 2>/dev/null || true)
    case "$(uname -m 2>/dev/null || true)" in
        x86_64|amd64) GS_TEL_ARCH="x86_64" ;;
        aarch64|arm64) GS_TEL_ARCH="arm64" ;;
    esac
    GS_TEL_CPU=$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || true)
    kb=$(awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null || true)
    if [ -n "$kb" ]; then GS_TEL_RAM=$(( (kb + 524288) / 1048576 )); [ "$GS_TEL_RAM" -ge 1 ] || GS_TEL_RAM=1; fi
    [ -d "$dir" ] || dir="/"
    avail=$(df -Pk "$dir" 2>/dev/null | awk 'NR==2 {print $4}' || true)
    case "$avail" in ''|*[!0-9]*) ;; *) GS_TEL_DISK_BUCKET=$(gs_tel_disk_bucket $(( avail / 1048576 ))) ;; esac
    dev=$(df -P "$dir" 2>/dev/null | awk 'NR==2 {print $1}' || true)
    if [ -n "$dev" ] && command -v lsblk > /dev/null 2>&1; then
        rota=$(lsblk -ndo ROTA "$dev" 2>/dev/null | head -n1 | tr -d ' ' || true)
        case "$rota" in 0) GS_TEL_DISK_KIND="SSD" ;; 1) GS_TEL_DISK_KIND="HDD" ;; esac
    fi
    if command -v systemd-detect-virt > /dev/null 2>&1; then
        if systemd-detect-virt -q 2>/dev/null; then GS_TEL_CPU_KIND="virtual"; else GS_TEL_CPU_KIND="physical"; fi
    fi
    if [ "$runtime" = "podman" ]; then
        GS_TEL_ENGINE_VERSION=$("${pre[@]}" podman version --format '{{.Client.Version}}' 2>/dev/null || true)
        GS_TEL_COMPOSE_VERSION=$(podman-compose --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | tail -n1 || true)
        if [ -n "${SUDO_USER:-}" ] && [ "${EUID:-$(id -u)}" -eq 0 ]; then
            GS_TEL_CONTAINERS=$(sudo -u "$SUDO_USER" podman ps -q 2>/dev/null | grep -c . || true)
        else
            GS_TEL_CONTAINERS=$("${pre[@]}" podman ps -q 2>/dev/null | grep -c . || true)
        fi
    else
        GS_TEL_ENGINE_VERSION=$("${pre[@]}" docker version --format '{{.Server.Version}}' 2>/dev/null || true)
        GS_TEL_COMPOSE_VERSION=$("${pre[@]}" docker compose version --short 2>/dev/null || true)
        if [ -z "$GS_TEL_COMPOSE_VERSION" ] && command -v docker-compose > /dev/null 2>&1; then
            GS_TEL_COMPOSE_VERSION=$(docker-compose version --short 2>/dev/null || true)
        fi
        GS_TEL_CONTAINERS=$("${pre[@]}" docker ps -q --filter status=running 2>/dev/null | grep -c . || true)
    fi
    GS_TEL_COMPOSE_VERSION="${GS_TEL_COMPOSE_VERSION#v}"
}

# Prints the contract v1.1 JSON body from the GS_TEL_* facts. $1 kitId, $2 installId, $3 platformStarted (true|false|""),
# $4 event (install_succeeded|install_failed), $5 errorCode (install_failed only; ^[A-Z0-9_]{1,64}$, else UNKNOWN_ERROR).
gs_tel_build_payload() {
    local kit_id="$1" install_id="$2" started="${3:-}" event="${4:-install_succeeded}" err="null" deployment="docker-linux" isolation="linux" test_field="" ts
    [ "${GS_TEL_ENGINE:-docker}" = "podman" ] && deployment="podman"
    case "$started" in true|false) ;; *) started="null" ;; esac
    if [ "$event" = "install_failed" ]; then
        err=$(printf '%s' "${5:-}" | tr '[:lower:]' '[:upper:]' | tr -c 'A-Z0-9_' '_' | cut -c1-64)
        [ -n "$err" ] || err="UNKNOWN_ERROR"
        err="\"$err\""
    else
        event="install_succeeded"
    fi
    if gs_tel_flag_on "${GLUESYNC_TELEMETRY_TEST:-}"; then test_field=',"test":true'; fi
    ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    printf '{"schemaVersion":"1.1","kitId":"%s","installId":"%s","event":"%s","errorCode":%s,"installer":"sh","installerVersion":%s,"gluesyncVersion":%s,' \
        "$kit_id" "$install_id" "$event" "$err" "$(gs_tel_json_ver "${INSTALLER_VERSION:-unknown}")" "$(gs_tel_json_ver "${GS_TEL_CORE_HUB:-}")"
    printf '"os":{"name":%s,"version":%s,"build":null,"distroId":%s,"kernel":%s},' \
        "$(v=$(gs_tel_json_str "${GS_TEL_OS_NAME:-}" 64); [ "$v" = null ] && v='"Linux"'; printf '%s' "$v")" \
        "$(gs_tel_json_str "${GS_TEL_OS_VERSION:-}" 32)" \
        "$(gs_tel_json_str "$(gs_tel_lower "${GS_TEL_OS_DISTRO:-}")" 32 'a-z0-9._-')" \
        "$(gs_tel_json_str "${GS_TEL_OS_KERNEL:-}" 64 '0-9A-Za-z._+~-')"
    printf '"engine":{"name":%s,"version":%s,"composeVersion":%s},"isolation":"%s","containerCount":%s,"platformStarted":%s,"deployment":"%s","arch":%s,' \
        "$(e=$(gs_tel_json_enum "${GS_TEL_ENGINE:-}" docker podman docker-desktop other); [ "$e" = null ] && e='"other"'; printf '%s' "$e")" \
        "$(gs_tel_json_ver "${GS_TEL_ENGINE_VERSION:-}")" "$(gs_tel_json_ver "${GS_TEL_COMPOSE_VERSION:-}")" \
        "$isolation" "$(gs_tel_json_int "${GS_TEL_CONTAINERS:-}" 0 100)" "$started" "$deployment" \
        "$(gs_tel_json_enum "${GS_TEL_ARCH:-}" x86_64 arm64)"
    printf '"hardware":{"cpuCores":%s,"ramGb":%s,"diskFreeGbBucket":%s,"cpuKind":%s,"diskKind":%s},' \
        "$(gs_tel_json_int "${GS_TEL_CPU:-}" 1 1024)" "$(gs_tel_json_int "${GS_TEL_RAM:-}" 1 4096)" \
        "$(gs_tel_json_enum "${GS_TEL_DISK_BUCKET:-}" '<50' '50-100' '100-250' '250-500' '500-1000' '>1000')" \
        "$(gs_tel_json_enum "${GS_TEL_CPU_KIND:-}" physical virtual)" "$(gs_tel_json_enum "${GS_TEL_DISK_KIND:-}" SSD HDD)"
    printf '"kit":{"coreHubVersion":%s,"agents":%s},"timestamp":"%s"%s}' \
        "$(gs_tel_json_ver "${GS_TEL_CORE_HUB:-}")" "${GS_TEL_AGENTS_JSON:-[]}" "$ts" "$test_field"
}

# One POST attempt. Prints "<http code> <retryAfter or ->"; code 000 = network error/timeout. $1 json, $2 max seconds.
gs_tel_post_once() {
    local json="$1" max="$2" uri body code ra="" hdr
    uri=$(gs_tel_uri)
    body=$(mktemp 2>/dev/null || echo "/tmp/gs-tel-body.$$")
    code="000"
    if command -v curl > /dev/null 2>&1; then
        # curl honours https_proxy/HTTPS_PROXY (and no_proxy) from the environment.
        code=$(printf '%s' "$json" | curl -sS -o "$body" -w '%{http_code}' --connect-timeout 3 --max-time "$max" \
            -X POST -H 'Content-Type: application/json; charset=utf-8' -H 'User-Agent: gluesync-installer' \
            --data-binary @- "$uri" 2>/dev/null) || true
    elif command -v wget > /dev/null 2>&1; then
        # wget honours https_proxy; -T bounds each network step, timeout(1) (when present) bounds the whole call.
        local -a tw=()
        command -v timeout > /dev/null 2>&1 && tw=(timeout "$((max + 1))")
        local -a wx=(-t 1)
        wget --help 2>&1 | grep -q -- '--content-on-error' && wx+=(--content-on-error)
        hdr=$("${tw[@]}" wget -q -S -T "$max" "${wx[@]}" -O "$body" --header='Content-Type: application/json; charset=utf-8' \
            --header='User-Agent: gluesync-installer' --post-data="$json" "$uri" 2>&1 || true)
        code=$(printf '%s\n' "$hdr" | sed -nE 's/^[[:space:]]*HTTP\/[0-9.]+[[:space:]]+([0-9]{3}).*/\1/p' | tail -n1)
        ra=$(printf '%s\n' "$hdr" | sed -nE 's/^[[:space:]]*[Rr]etry-[Aa]fter:[[:space:]]*([0-9]+).*/\1/p' | tail -n1)
    else
        rm -f "$body"
        printf 'none -'
        return 0
    fi
    case "$code" in [0-9][0-9][0-9]) ;; *) code="000" ;; esac
    local ra_body
    ra_body=$(sed -nE 's/.*"retryAfter"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$body" 2>/dev/null | head -n1)
    [ -n "$ra_body" ] && ra="$ra_body"
    rm -f "$body"
    printf '%s %s' "$code" "${ra:--}"
}

# Retry policy. Prints "sent|failed <detail>". $1 json.
gs_tel_send() {
    local json="$1" a=1 start now remaining max out code ra wait detail="" result="failed"
    start=$(date +%s)
    while [ "$a" -le 3 ]; do
        now=$(date +%s); remaining=$(( 20 - (now - start) ))
        if [ "$remaining" -lt 1 ]; then detail="$detail; time budget used up"; break; fi
        max=5; [ "$remaining" -lt 5 ] && max=$remaining
        out=$(gs_tel_post_once "$json" "$max")
        code="${out%% *}"; ra="${out##* }"; wait=""
        case "$code" in
            none) detail="no curl or wget"; result="skipped"; break ;;
            2[0-9][0-9]) result="sent"; detail="HTTP $code on attempt $a"; break ;;
            429)
                if [ "$ra" != "-" ] && [ "$ra" -gt 10 ]; then detail="HTTP 429, retryAfter ${ra}s > 10s, not retried"; break; fi
                if [ "$ra" != "-" ]; then wait=$ra; fi
                detail="HTTP 429 on attempt $a" ;;
            5[0-9][0-9]) detail="HTTP $code on attempt $a" ;;
            000) detail="network error or timeout on attempt $a" ;;
            *) detail="HTTP $code (final)"; break ;;
        esac
        [ "$a" -ge 3 ] && break
        if [ -z "$wait" ]; then if [ "$a" -eq 1 ]; then wait=2; else wait=5; fi; fi
        now=$(date +%s)
        if [ $(( now - start + wait + 1 )) -gt 20 ]; then detail="$detail; no time left for a retry"; break; fi
        sleep "$wait"
        a=$((a + 1))
    done
    printf '%s %s' "$result" "$detail"
}

# >>> gs-normalize-kit-id (copied byte for byte into gluesync-pre-flight-checker/gluesync-preflight.sh; keep identical)
# Kit ID as typed or from GLUESYNC_KIT_ID -> canonical form: whitespace removed, lowercase. The kit download path on
# S3/CloudFront is case-sensitive (an uppercase kit ID 404s); validate (^[a-f0-9]{12}$) after this, never before.
gs_normalize_kit_id() {
    printf '%s' "${1:-}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]'
}
# <<< gs-normalize-kit-id

# Full flow. $1 kitId, $2 platformStarted (true|false), $3 runtime (docker|podman), $4 install dir,
# $5 event (default install_succeeded), $6 errorCode (install_failed). One installId per run (GS_TEL_INSTALL_ID).
# Always returns 0.
send_install_telemetry() {
    local kit_id started="${2:-}" runtime="${3:-docker}" dir="${4:-/opt/gluesync}" event="${5:-install_succeeded}" err="${6:-}"
    kit_id=$(gs_normalize_kit_id "${1:-}")
    local result="skipped" detail="" install_id="${GS_TEL_INSTALL_ID:-}" json out label="Install confirmation"
    [ "$event" = "install_failed" ] && label="Install failure report"
    if gs_tel_flag_off "${GLUESYNC_TELEMETRY:-}"; then
        detail="turned off (GLUESYNC_TELEMETRY=0)"
    elif ! [[ "$kit_id" =~ ^[a-f0-9]{12}$ ]]; then
        detail="no valid kit ID"
    elif ! [[ "$install_id" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ ]] && ! install_id=$(gs_tel_new_install_id); then
        detail="could not generate an install ID"
    else
        GS_TEL_INSTALL_ID="$install_id"
        gs_tel_collect_facts "$runtime" "$dir" || true
        gs_tel_kit_info "$dir" || true
        json=$(gs_tel_build_payload "$kit_id" "$install_id" "$started" "$event" "$err")
        out=$(gs_tel_send "$json")
        result="${out%% *}"; detail="${out#* }"
    fi
    if [ -n "${GS_TEL_LOG:-}" ]; then printf 'telemetry=%s (%s)\n%s: %s\n' "$result" "$detail" "$label" "$result" >> "$GS_TEL_LOG" 2>/dev/null || true; fi
    log_info "$label: $result"
    return 0
}
# <<< gs-telemetry

# Detect whether the CPU supports the x86-64-v3 microarchitecture level.
# The default Core Hub image requires x86-64-v3; older CPUs (x86-64-v2 or lower)
# must use the "-debian" image variant instead.
cpu_supports_x86_64_v3() {
    local arch
    arch=$(uname -m 2>/dev/null || echo "")

    # Non-x86 architectures (e.g. arm64) are unaffected
    if [[ "$arch" != "x86_64" && "$arch" != "amd64" ]]; then
        return 0
    fi

    # Preferred: ask the glibc dynamic loader (glibc >= 2.33 reports microarch levels)
    local ld_so ld_out
    for ld_so in /lib64/ld-linux-x86-64.so.2 /lib/ld-linux-x86-64.so.2 /usr/lib64/ld-linux-x86-64.so.2; do
        if [[ -x "$ld_so" ]]; then
            ld_out=$("$ld_so" --help 2>/dev/null || true)
            if [[ "$ld_out" == *"x86-64-v3"* ]]; then
                if echo "$ld_out" | grep -q "x86-64-v3 (supported"; then
                    return 0
                fi
                return 1
            fi
            break
        fi
    done

    # Fallback: check the CPU flags required by x86-64-v3
    local flags flag
    flags=$(grep -m1 '^flags' /proc/cpuinfo 2>/dev/null | cut -d: -f2 || true)
    if [[ -z "$flags" ]]; then
        # Cannot determine: assume supported (default image)
        return 0
    fi
    for flag in avx avx2 bmi1 bmi2 f16c fma movbe xsave abm; do
        if ! grep -qw "$flag" <<< "$flags"; then
            return 1
        fi
    done
    return 0
}

# Switch the Core Hub image to the "-debian" variant in the installed
# docker-compose file when the CPU cannot run x86-64-v3 binaries.
apply_core_hub_cpu_compatibility() {
    if cpu_supports_x86_64_v3; then
        log_info "CPU supports x86-64-v3: using the default Gluesync Core Hub image."
        return 0
    fi

    log_warning "CPU without x86-64-v3 support detected (x86-64-v2 or older)."
    log_warning "Switching Gluesync Core Hub to the \"-debian\" image variant."

    local compose_file patched=0
    while IFS= read -r -d '' compose_file; do
        if $SUDO sed -i.bak -E 's#(molo17/gluesync-core-hub[A-Za-z0-9._-]*:[A-Za-z0-9._-]+)#\1-debian#g; s#-debian-debian#-debian#g' "$compose_file"; then
            $SUDO rm -f "${compose_file}.bak"
            log_success "Updated Core Hub image to -debian variant in $(basename "$compose_file")"
            patched=$((patched + 1))
        else
            log_warning "Failed to update Core Hub image reference in $compose_file"
        fi
    done < <(find /opt/gluesync -maxdepth 3 -type f \( -name "docker-compose.yaml" -o -name "docker-compose.yml" \) -print0 2>/dev/null)

    if (( patched == 0 )); then
        log_warning "No docker-compose file found to apply the Core Hub -debian variant."
    fi
}

# Allocate a 10 GB swap file if the system doesn't already have enough swap.
# Idempotent: skips creation when total swap already meets the target.
setup_swap() {
    local TARGET_SWAP_GB=10
    local TARGET_SWAP_KB=$(( TARGET_SWAP_GB * 1024 * 1024 ))
    local SWAP_FILE="/swapfile"

    log_info "Checking swap configuration (target: ${TARGET_SWAP_GB}GB)..."

    # Sum all existing swap areas from /proc/swaps (column 3 = size in KiB)
    local current_swap_kb=0
    if [[ -r /proc/swaps ]]; then
        current_swap_kb=$(awk 'NR>1 {sum+=$3} END {print sum+0}' /proc/swaps)
    fi

    local current_swap_gb
    current_swap_gb=$(awk -v kb="$current_swap_kb" 'BEGIN { printf "%.1f", kb/1048576 }')

    if (( current_swap_kb >= TARGET_SWAP_KB )); then
        log_success "Swap already configured (${current_swap_gb}GB). Skipping swap allocation."
        return 0
    fi

    log_info "Current swap: ${current_swap_gb}GB — allocating ${TARGET_SWAP_GB}GB swap file at ${SWAP_FILE}..."

    # If a swap file already exists but is too small, turn it off and remove it
    if [[ -f "$SWAP_FILE" ]]; then
        log_info "Removing existing (undersized) swap file..."
        $SUDO swapoff "$SWAP_FILE" 2>/dev/null || true
        $SUDO rm -f "$SWAP_FILE"
    fi

    # Allocate the file — prefer fallocate (fast), fall back to dd
    if command -v fallocate >/dev/null 2>&1; then
        log_info "Allocating ${TARGET_SWAP_GB}GB with fallocate..."
        if ! $SUDO fallocate -l "${TARGET_SWAP_GB}G" "$SWAP_FILE"; then
            log_warning "fallocate failed, falling back to dd..."
            $SUDO dd if=/dev/zero of="$SWAP_FILE" bs=1M count=$(( TARGET_SWAP_GB * 1024 )) status=progress
        fi
    else
        log_info "Allocating ${TARGET_SWAP_GB}GB with dd (this may take a moment)..."
        $SUDO dd if=/dev/zero of="$SWAP_FILE" bs=1M count=$(( TARGET_SWAP_GB * 1024 )) status=progress
    fi

    # Secure the file: only root can read/write
    $SUDO chmod 600 "$SWAP_FILE"

    # Format and activate
    $SUDO mkswap "$SWAP_FILE"
    $SUDO swapon "$SWAP_FILE"

    # Persist across reboots — add only if not already present
    if ! grep -q "^${SWAP_FILE}" /etc/fstab 2>/dev/null; then
        log_info "Persisting swap in /etc/fstab..."
        echo "${SWAP_FILE} none swap sw 0 0" | $SUDO tee -a /etc/fstab > /dev/null
    fi

    local new_swap_kb
    new_swap_kb=$(awk 'NR>1 {sum+=$3} END {print sum+0}' /proc/swaps 2>/dev/null || echo 0)
    local new_swap_gb
    new_swap_gb=$(awk -v kb="$new_swap_kb" 'BEGIN { printf "%.1f", kb/1048576 }')
    log_success "Swap configured successfully (${new_swap_gb}GB total active swap)."
}

# >>> gs-check-disk (copied byte for byte into gluesync-pre-flight-checker/gluesync-preflight.sh; keep identical)
# GS-PRE-050 LOW_DISK (installer 1.3.3), called by check_system_requirements. Returns 1 when the install must stop.
gs_check_disk() {
    # GS-PRE-050 LOW_DISK (1.3.3): block below 100 GB free (decimal GB, same rule as the Windows MSI/EXE), on / and on the
    # Docker data-root (docker info, else /var/lib/docker or its nearest existing parent). No warning tier.
    # Always a block: no GLUESYNC_ACCEPT override or other switch (the same rule on Windows).
    local min_disk_kb=$(( 100 * 1000 * 1000 * 1000 / 1024 ))
    local disk_paths=("/") data_root=""
    if command -v docker >/dev/null 2>&1; then
        data_root=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || true)
    fi
    [ -n "$data_root" ] || data_root="/var/lib/docker"
    while [ -n "$data_root" ] && [ ! -d "$data_root" ]; do data_root=$(dirname "$data_root"); done
    [ -n "$data_root" ] && [ "$data_root" != "/" ] && disk_paths+=("$data_root")
    local disk_path avail_space_kb avail_space_gb low_path="" low_kb="" disk_checked=0
    for disk_path in "${disk_paths[@]}"; do
        avail_space_kb=$(df -Pk "$disk_path" 2>/dev/null | awk 'NR==2 {print $4}')
        [[ "$avail_space_kb" =~ ^[0-9]+$ ]] || continue
        disk_checked=1
        avail_space_gb=$(awk -v kb="$avail_space_kb" 'BEGIN { printf "%.1f", kb*1024/1e9 }')
        log_info "Disk free on $disk_path: ${avail_space_gb}GB"
        if [ -z "$low_kb" ] || (( avail_space_kb < low_kb )); then low_kb="$avail_space_kb"; low_path="$disk_path"; fi
    done
    if (( disk_checked == 0 )); then
        log_warning "Unable to determine available disk space. Continuing without enforcing the disk requirement."
    elif (( low_kb < min_disk_kb )); then
        avail_space_gb=$(awk -v kb="$low_kb" 'BEGIN { printf "%.1f", kb*1024/1e9 }')
        log_error "GS-PRE-050 LOW_DISK: only ${avail_space_gb}GB free on ${low_path}. Gluesync needs at least 100GB free for container images and data."
        log_error "    Fix: free space on ${low_path}, or move Docker's data-root (/etc/docker/daemon.json \"data-root\") to a disk with 100GB free."
        return 1
    else
        log_success "Disk space requirement satisfied (at least 100GB free)."
    fi
    return 0
}
# <<< gs-check-disk

# >>> gs-pre-shared (GS-PRE codes; same codes, names and wording as installer/gluesync-preflight-lib.ps1 on Windows)
# This block is copied byte for byte into docker-installer-linux-offline.sh (tests/preflight/test-sh-preflight.sh checks it).
# GS-PRE-040 PORTS_IN_USE, 050 LOW_DISK (gs_check_disk), 051/052 RAM, 053 LOW_CPU, 061/062/063 network, 030/031 kit ID.
# Soft blocks (040, 061, 062, 063) can be accepted with GLUESYNC_ACCEPT=CODE[,CODE] or --accept CODE[,CODE]: they become
# warnings. 030, 031, 050, 051 and 053 are never acceptable. Every finding prints "CODE NAME: message" then "    Fix: hint".
GS_PRE_PORTS="80 443 9443"
GS_PRE_SOFT_CODES=" GS-PRE-040 GS-PRE-061 GS-PRE-062 GS-PRE-063 "

# --accept CODE[,CODE] (repeatable, also --accept=CODE) appends to GLUESYNC_ACCEPT. Other arguments are ignored.
gs_pre_parse_args() {
    while [ $# -gt 0 ]; do
        case "$1" in
            --accept)
                if [ $# -ge 2 ]; then GLUESYNC_ACCEPT="${GLUESYNC_ACCEPT:+$GLUESYNC_ACCEPT,}$2"; shift; fi ;;
            --accept=*) GLUESYNC_ACCEPT="${GLUESYNC_ACCEPT:+$GLUESYNC_ACCEPT,}${1#--accept=}" ;;
        esac
        shift
    done
    return 0
}

# $1 code. Returns 0 when it is a soft block listed in GLUESYNC_ACCEPT (separators: comma, semicolon, space; any case).
gs_pre_accepted() {
    local code="$1" item
    local -a items=()
    case "$GS_PRE_SOFT_CODES" in *" $code "*) ;; *) return 1 ;; esac
    read -r -a items <<< "$(printf '%s' "${GLUESYNC_ACCEPT:-}" | tr 'a-z;,' 'A-Z  ')"
    for item in "${items[@]}"; do
        if [ "$item" = "$code" ]; then return 0; fi
    done
    return 1
}

# Findings. $1 code, $2 name, $3 message, $4 fix. gs_pre_block returns 1 unless the code is accepted (then it warns).
gs_pre_block() {
    if gs_pre_accepted "$1"; then
        log_warning "$1 $2: $3 (accepted with GLUESYNC_ACCEPT)"
        log_warning "    Fix: $4"
        return 0
    fi
    log_error "$1 $2: $3"
    log_error "    Fix: $4"
    return 1
}
gs_pre_warn() {
    log_warning "$1 $2: $3"
    log_warning "    Fix: $4"
    return 0
}

# ---- Ports (GS-PRE-040) ---------------------------------------------------------------------------------------------
# The kit publishes host ports 80 and 443 (reverse proxy) and 9443 (Portainer).

# Prints TCP listeners as "port|process names (comma separated, may be empty)". ss, then netstat, then /proc/net/tcp*.
# Returns 1 when none of them is usable. $SUDO lets ss/netstat see the owners of other users' sockets.
gs_pre_listeners() {
    local -a pre=()
    local out f line st addr
    [ -z "${SUDO:-}" ] || pre=("$SUDO")
    if command -v ss >/dev/null 2>&1 && out=$("${pre[@]}" ss -ltnp 2>/dev/null); then
        # -H (no header) is missing on old iproute2: keep only the LISTEN lines. Field 4 = local address, field 6 = users:(("name",pid=..)).
        printf '%s\n' "$out" | awk '$1 == "LISTEN" {
            a = $4; sub(/.*:/, "", a); p = ""
            s = $0
            while (match(s, /\(\("[^"]*"|,\("[^"]*"/)) {
                n = substr(s, RSTART + 3, RLENGTH - 4); p = p (p == "" ? "" : ",") n; s = substr(s, RSTART + RLENGTH)
            }
            print a "|" p }'
        return 0
    fi
    if command -v netstat >/dev/null 2>&1 && out=$("${pre[@]}" netstat -ltnp 2>/dev/null); then
        # "tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1234/nginx: master" (owner "-" without privileges)
        printf '%s\n' "$out" | awk '$6 == "LISTEN" {
            a = $4; sub(/.*:/, "", a); n = $7; sub(/^[0-9]+\//, "", n); sub(/:$/, "", n); if (n == "-") n = ""
            print a "|" n }'
        return 0
    fi
    st=0
    for f in ${GS_PRE_PROC_TCP:-/proc/net/tcp /proc/net/tcp6}; do
        [ -r "$f" ] || continue
        st=1
        while read -r _ addr _ line _; do
            [ "$line" = "0A" ] || continue
            printf '%s|\n' "$((16#${addr##*:}))"
        done < <(tail -n +2 "$f")
    done
    [ "$st" = 1 ]
}

# $1 process name. 0 = a port forwarder of a container runtime: not the real owner of the port.
gs_pre_forwarder() {
    case "$1" in docker-proxy*|rootlessport*|conmon*|podman*|pasta*|slirp4netns*) return 0 ;; esac
    return 1
}

# Containers "name|ports|compose project|image" (docker ps / podman ps) -> "gs|name|port" for containers of a Gluesync
# install (image molo17/*, or any container of a compose project that has one) and "other|name|port" for the rest,
# one line per published TCP host port in GS_PRE_PORTS.
gs_pre_published_ports() {
    awk -F'|' -v want=" $GS_PRE_PORTS " '
        function gsimg(i) { return i ~ /(^|\/)molo17\// }
        { n++; nm[n] = $1; pt[n] = $2; pj[n] = $3; im[n] = $4; if (gsimg($4) && $3 != "") gsp[$3] = 1 }
        END {
            for (i = 1; i <= n; i++) {
                kind = (gsimg(im[i]) || (pj[i] != "" && (pj[i] in gsp))) ? "gs" : "other"
                m = split(pt[i], e, /, */)
                for (k = 1; k <= m; k++) {
                    if (e[k] ~ /\/udp$/) continue
                    q = index(e[k], "->"); if (q == 0) continue
                    l = substr(e[k], 1, q - 1); sub(/.*:/, "", l)
                    lo = l; hi = l
                    if (l ~ /^[0-9]+-[0-9]+$/) { split(l, r, "-"); lo = r[1]; hi = r[2] }
                    if (lo !~ /^[0-9]+$/) continue
                    for (x = lo + 0; x <= hi + 0; x++) if (index(want, " " x " ")) print kind "|" nm[i] "|" x
                }
            }
        }'
}

# Reads "name|ports|project|image" lines of the running containers of docker and podman (whichever answers).
gs_pre_containers() {
    local rt out
    local -a pre=()
    command -v timeout >/dev/null 2>&1 && pre=(timeout 15)
    [ -z "${SUDO:-}" ] || pre+=("$SUDO")
    for rt in docker podman; do
        command -v "$rt" >/dev/null 2>&1 || continue
        out=$("${pre[@]}" "$rt" ps --format '{{.Names}}|{{.Ports}}|{{.Label "com.docker.compose.project"}}|{{.Image}}' 2>/dev/null) \
            || out=$("${pre[@]}" "$rt" ps --format '{{.Names}}|{{.Ports}}||{{.Image}}' 2>/dev/null) || continue
        [ -z "$out" ] || printf '%s\n' "$out"
    done
    return 0
}

# $1 listeners ("port|procs" lines), $2 published ports ("gs|name|port" / "other|name|port" lines).
# Prints "port|owner" for each port of GS_PRE_PORTS that a re-run of the install would not own. Pure.
#  - a port published by a running Gluesync container is not a conflict (a re-run on a working server);
#  - a listener whose owners are all container port forwarders is not a conflict (docker-proxy, rootlessport...) unless a
#    container that is not Gluesync publishes the port;
#  - any other listener is a conflict, owner = its process names ("" when unknown).
gs_pre_port_conflicts() {
    local port listening procs gs foreign p owner nonfwd
    for port in $GS_PRE_PORTS; do
        gs=$(printf '%s\n' "$2" | awk -F'|' -v p="$port" '$1 == "gs" && $3 == p { f = 1 } END { print f + 0 }')
        [ "$gs" = 0 ] || continue
        foreign=$(printf '%s\n' "$2" | awk -F'|' -v p="$port" '$1 == "other" && $3 == p && !($2 in s) { s[$2] = 1; o = o (o == "" ? "" : ", ") $2 } END { print o }')
        listening=$(printf '%s\n' "$1" | awk -F'|' -v p="$port" '$1 == p { f = 1 } END { print f + 0 }')
        procs=$(printf '%s\n' "$1" | awk -F'|' -v p="$port" '$1 == p { n = split($2, a, ","); for (i = 1; i <= n; i++) if (a[i] != "" && !(a[i] in s)) { s[a[i]] = 1; o = o (o == "" ? "" : ",") a[i] } } END { print o }')
        nonfwd=""
        local -a names=()
        IFS=',' read -r -a names <<< "$procs"
        for p in "${names[@]}"; do
            gs_pre_forwarder "$p" || nonfwd="${nonfwd:+$nonfwd, }$p"
        done
        owner=""
        if [ -n "$foreign" ]; then owner="container $foreign"
        elif [ "$listening" = 1 ] && [ -n "$nonfwd" ]; then owner="$nonfwd"
        elif [ "$listening" = 1 ] && [ -z "$procs" ]; then owner=""
        else continue
        fi
        printf '%s|%s\n' "$port" "$owner"
    done
}

# $1 "port|owner" lines. One GS-PRE-040 finding naming every port. Returns 1 when blocking.
gs_pre_ports_finding() {
    local port owner list="" ports="" n=0 msg last_owner=""
    while IFS='|' read -r port owner; do
        [ -n "$port" ] || continue
        n=$((n + 1)); ports="${ports:+$ports, }$port"
        list="${list:+$list, }$port${owner:+ ($owner)}"
        last_owner="$owner"
    done <<< "$1"
    [ "$n" -gt 0 ] || return 0
    if [ "$n" = 1 ]; then msg="Port $ports is in use${last_owner:+ ($last_owner)}."; else msg="Ports $list are in use."; fi
    gs_pre_block GS-PRE-040 PORTS_IN_USE "$msg Gluesync publishes ports 80, 443 and 9443 on this server, so its containers cannot start." \
        "Stop or reconfigure the service that uses the port, then run the installer again."
}

# Returns 1 when a finding blocks.
gs_pre_ports() {
    local listeners published conflicts
    if ! listeners=$(gs_pre_listeners); then
        log_info "Unable to list listening ports (no ss, netstat or /proc/net/tcp); skipping the port check."
        return 0
    fi
    published=$(gs_pre_containers | gs_pre_published_ports)
    conflicts=$(gs_pre_port_conflicts "$listeners" "$published")
    if [ -z "$conflicts" ]; then
        log_success "Ports 80, 443 and 9443 are free (or already used by this Gluesync install)."
        return 0
    fi
    gs_pre_ports_finding "$conflicts"
}
# <<< gs-pre-shared

# >>> gs-preflight (installer-only checks: CPU, RAM, network, kit ID; the shared pieces are in gs-pre-shared above)
# $1 = cores. GS-PRE-053 LOW_CPU blocks below 4; exactly 4 warns without a code. Returns 1 on a block.
gs_pre_cpu() {
    local cores="$1"
    log_info "Detected CPU cores: $cores"
    if (( cores < 4 )); then
        gs_pre_block GS-PRE-053 LOW_CPU "System has $cores CPU core(s). At least 4 cores are required." "Use a server with at least 4 CPU cores." || return 1
    elif (( cores == 4 )); then
        log_warning "System has 4 CPU cores. This is the bare minimum and may limit performance."
    else
        log_success "CPU requirement satisfied ($cores cores available)."
    fi
    return 0
}

# $1 = total RAM in KB. Decimal GB (a VM sold as "8 GB" shows about 7.6 GiB). GS-PRE-051 LOW_RAM blocks below 4 GB,
# GS-PRE-052 RAM_BELOW_RECOMMENDED warns below 8 GB. Returns 1 on a block.
gs_pre_ram() {
    local total_ram_kb="$1" total_ram_gb
    local min_ram_kb=$(( (4 * 1000 * 1000 * 1000) / 1024 ))
    local recommended_ram_kb=$(( (8 * 1000 * 1000 * 1000) / 1024 ))
    total_ram_gb=$(awk -v kb="$total_ram_kb" 'BEGIN { printf "%.1f", kb/1048576 }')
    if (( total_ram_kb < min_ram_kb )); then
        gs_pre_block GS-PRE-051 LOW_RAM "Only ${total_ram_gb}GB of RAM detected. At least 4GB is required (8GB recommended)." "Add memory (at least 4GB, 8GB recommended)." || return 1
    elif (( total_ram_kb < recommended_ram_kb )); then
        gs_pre_warn GS-PRE-052 RAM_BELOW_RECOMMENDED "Only ${total_ram_gb}GB of RAM detected. 8GB or more is recommended; Gluesync may run slowly or fail under load." "Add memory if Gluesync runs slowly."
    else
        log_success "RAM requirement satisfied (${total_ram_gb}GB available)."
    fi
    return 0
}

# ---- Which hosts this run will actually use ---------------------------------------------------------------------------
# detect_distribution has already run (DISTRO, set from /etc/os-release). The case lists mirror the install dispatch in main.
gs_pre_has_cmd() { command -v "$1" >/dev/null 2>&1; }

# 0 = Docker Compose is already installed (standalone binary or the docker compose plugin): the install steps skip its download.
gs_pre_compose_present() {
    local -a pre=()
    [ -z "${SUDO:-}" ] || pre=("$SUDO")
    gs_pre_has_cmd docker-compose && return 0
    gs_pre_has_cmd docker && "${pre[@]}" docker compose version >/dev/null 2>&1
}

# 0 = this run installs Docker from Docker's own repositories (download.docker.com): docker-ce on Fedora, Ubuntu,
# RHEL family, Oracle Linux, Debian, CentOS and SLES s390x. Not Amazon Linux, openSUSE or SLES x86_64 (distro packages).
# The install steps do not skip Docker when it is already present, so there is no "already installed" exception.
gs_pre_needs_docker_repo() {
    [ "${CONTAINER_RUNTIME:-docker}" = docker ] || return 1
    case "${DISTRO:-}" in
        fedora|ubuntu|rhel|redhat|almalinux|rocky|ol|debian|centos) return 0 ;;
        sles|suse) [ "$(uname -m)" = s390x ] ;;
        *) return 1 ;;
    esac
}

# 0 = this run downloads Docker Compose from github.com: Amazon Linux 2023 and openSUSE, unless Compose is already there.
gs_pre_needs_github() {
    [ "${CONTAINER_RUNTIME:-docker}" = docker ] || return 1
    case "${DISTRO:-}" in amzn|opensuse*) ;; *) return 1 ;; esac
    ! gs_pre_compose_present
}

# ---- Network (GS-PRE-061 / 062 / 063) ------------------------------------------------------------------------------
# code|kind|target|label|condition (a function; empty = always probed). A host is reachable when the URL answers 2xx (no redirect following, as before) or, for tcp,
# when the TCP connection opens. One finding per code, listing every unreachable host.
GS_PRE_ENDPOINTS=(
    "GS-PRE-061|http|https://hub.docker.com/layers/molo17/gluesync-core-hub/latest/images/|hub.docker.com|"
    "GS-PRE-061|http|https://www.docker.com|www.docker.com|"
    "GS-PRE-061|http|https://download.docker.com/linux/|download.docker.com|gs_pre_needs_docker_repo"
    "GS-PRE-061|tcp|registry-1.docker.io:443|registry-1.docker.io:443|"
    "GS-PRE-062|http|https://api.backoffice.molo17.com/agent/core-hub/version|api.backoffice.molo17.com|"
    "GS-PRE-062|http|https://molo17.com/|molo17.com|"
    "GS-PRE-063|http|https://github.com/docker/compose/releases|github.com|gs_pre_needs_github"
)

# $1 url. Prints the HTTP status, 000 on a network error or timeout. curl honours https_proxy / no_proxy.
gs_pre_http_status() {
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$1" 2>/dev/null) || true
    case "$code" in [0-9][0-9][0-9]) ;; *) code="000" ;; esac
    printf '%s' "$code"
}

# $1 host:port. 0 = connected, 1 = failed, 2 = no tool to test with.
gs_pre_tcp() {
    local host="${1%%:*}" port="${1##*:}"
    command -v timeout >/dev/null 2>&1 || return 2
    if command -v nc >/dev/null 2>&1; then
        timeout 10 nc -z "$host" "$port" >/dev/null 2>&1 && return 0
        return 1
    fi
    timeout 10 bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null && return 0
    return 1
}

# Proxy note for messages, without credentials: " via proxy host:port" or "".
gs_pre_proxy_note() {
    local p="${https_proxy:-${HTTPS_PROXY:-${http_proxy:-${HTTP_PROXY:-}}}}"
    [ -n "$p" ] || return 0
    p="${p#*://}"; p="${p##*@}"; p="${p%%/*}"
    printf ' via proxy %s' "$p"
}

# $1 code, $2 failure list ("label [reason]" joined by ", "). Builds the one finding of that code. Returns 1 when blocking.
gs_pre_network_finding() {
    local code="$1" list="$2" via
    via=$(gs_pre_proxy_note)
    case "$code" in
        GS-PRE-061)
            gs_pre_block GS-PRE-061 DOCKER_ENDPOINTS_UNREACHABLE "Cannot reach Docker Hub / download.docker.com (${list}${via})." \
                "Allow HTTPS (443) to download.docker.com, hub.docker.com, www.docker.com, registry-1.docker.io, auth.docker.io and production.cloudflare.docker.com (the Gluesync images are molo17/* on Docker Hub), or configure a registry mirror." ;;
        GS-PRE-062)
            gs_pre_block GS-PRE-062 MOLO17_ENDPOINTS_UNREACHABLE "Cannot reach MOLO17 services (${list}${via})." \
                "Allow HTTPS (443) to molo17.com and api.backoffice.molo17.com, directly or on your proxy." ;;
        *)
            gs_pre_block GS-PRE-063 GITHUB_UNREACHABLE "Cannot reach GitHub (${list}${via}, Docker Compose download)." \
                "Allow HTTPS (443) to github.com and objects.githubusercontent.com." ;;
    esac
}

# Probes GS_PRE_ENDPOINTS. Returns 1 when a finding blocks. Sets GS_PRE_KIT_HOST_DOWN=1 when molo17.com is unreachable.
gs_pre_network() {
    local entry code kind target label cond detail rc=0 c f61="" f62="" f63=""
    GS_PRE_KIT_HOST_DOWN=0
    for entry in "${GS_PRE_ENDPOINTS[@]}"; do
        IFS='|' read -r code kind target label cond <<< "$entry"
        if [ -n "$cond" ] && ! "$cond"; then log_info "Not needed on this install path, not checked: $label"; continue; fi
        detail=""
        if [ "$kind" = "tcp" ]; then
            c=0; gs_pre_tcp "$target" || c=$?
            if [ "$c" = 2 ]; then log_warning "nc or timeout not available; cannot verify TCP connection to $label"; continue; fi
            if [ "$c" = 0 ]; then log_success "TCP connection OK: $label"; continue; fi
            detail="no TCP connection"
        else
            c=$(gs_pre_http_status "$target")
            if [[ "$c" =~ ^2[0-9]{2}$ ]]; then log_success "Reachability OK: $label ($target) [HTTP $c]"; continue; fi
            detail="HTTP $c"
        fi
        log_info "Unreachable: $label ($detail)"
        if [ "$label" = "molo17.com" ]; then GS_PRE_KIT_HOST_DOWN=1; fi
        f="$label [$detail]"
        case "$code" in
            GS-PRE-061) f61="${f61:+$f61, }$f" ;;
            GS-PRE-062) f62="${f62:+$f62, }$f" ;;
            *) f63="${f63:+$f63, }$f" ;;
        esac
    done
    [ -z "$f61" ] || gs_pre_network_finding GS-PRE-061 "$f61" || rc=1
    [ -z "$f62" ] || gs_pre_network_finding GS-PRE-062 "$f62" || rc=1
    [ -z "$f63" ] || gs_pre_network_finding GS-PRE-063 "$f63" || rc=1
    return "$rc"
}

# ---- Kit ID (GS-PRE-030 / GS-PRE-031) ------------------------------------------------------------------------------
# $1 url. Prints the HTTP status of a HEAD request (no body, the zip is never downloaded); one retry on a curl
# network error; 000 when it still fails. curl honours https_proxy / no_proxy.
gs_pre_head_status() {
    local code a
    for a in 1 2; do
        if code=$(curl -sS -I -o /dev/null -w '%{http_code}' --max-time 10 "$1" 2>/dev/null); then break; fi
        code="000"
    done
    case "$code" in [0-9][0-9][0-9]) ;; *) code="000" ;; esac
    printf '%s' "$code"
}

# True when this run will go on to the Gluesync setup with a kit ID that is already known (GLUESYNC_KIT_ID), i.e. what
# gs_ask would answer for the setup question.
gs_pre_kit_expected() {
    [ -n "${GLUESYNC_KIT_ID:-}" ] || return 1
    case "${GLUESYNC_SETUP:-y}" in [Yy]*) return 0 ;; esac
    return 1
}

# GS-PRE-030 KIT_ID_INVALID for a kit ID that is known before anything is installed. Exits 26 (as the setup step does).
gs_pre_check_kit_format() {
    local kit
    gs_pre_kit_expected || return 0
    kit=$(gs_normalize_kit_id "$GLUESYNC_KIT_ID")
    [ -n "$kit" ] || return 0   # blank: the setup step skips the install without a kit ID
    if [[ ! $kit =~ ^[a-f0-9]{12}$ ]]; then
        log_error "Invalid kit ID format. Expected 12 hexadecimal characters (0-9, a-f)."
        log_error "GS-PRE-030 KIT_ID_INVALID: GLUESYNC_KIT_ID must be 12 hexadecimal characters (0-9, a-f) from your download e-mail."
        exit 26
    fi
    return 0
}

# GS-PRE-031 KIT_NOT_FOUND. $1 = normalized, valid kit ID. HTTP 404 blocks; 200 passes; anything else is no finding
# (reachability is GS-PRE-062). The kit ID is not printed. Returns 1 on a block.
gs_pre_check_kit_exists() {
    local code
    command -v curl >/dev/null 2>&1 || { log_info "curl is not available; skipping the kit ID check."; return 0; }
    code=$(gs_pre_head_status "https://molo17.com/gluesync-trials/${1}-trial-kit.zip")
    case "$code" in
        200) log_success "Kit ID found on the MOLO17 download server." ;;
        404)
            gs_pre_block GS-PRE-031 KIT_NOT_FOUND "No Gluesync kit with this kit ID exists on the MOLO17 download server (HTTP 404)." \
                "Check the kit ID in your trial email (12 hexadecimal characters), or request a new trial kit."
            return 1 ;;
        *) log_info "Could not confirm the kit ID on the MOLO17 download server (HTTP $code); continuing." ;;
    esac
    return 0
}
# <<< gs-preflight

check_system_requirements() {
    log_info "Running system pre-flight checks..."
    local fatal=0

    # GS-PRE-030: a kit ID given up front (GLUESYNC_KIT_ID) is validated before anything is installed (exit 26).
    gs_pre_check_kit_format

    local cores=""
    if command -v nproc >/dev/null 2>&1; then
        cores=$(nproc 2>/dev/null)
    elif command -v sysctl >/dev/null 2>&1; then
        cores=$(sysctl -n hw.ncpu 2>/dev/null)
    fi

    if [[ "$cores" =~ ^[0-9]+$ ]]; then
        gs_pre_cpu "$cores" || fatal=1
    else
        log_warning "Unable to determine CPU core count. Skipping CPU requirement enforcement."
    fi

    local total_ram_kb=""
    if [[ -r /proc/meminfo ]]; then
        total_ram_kb=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null)
    elif command -v sysctl >/dev/null 2>&1; then
        total_ram_kb=$(sysctl -n hw.memsize 2>/dev/null)
        if [[ "$total_ram_kb" =~ ^[0-9]+$ ]]; then
            total_ram_kb=$(( total_ram_kb / 1024 )) # convert bytes to KB
        fi
    fi

    if [[ "$total_ram_kb" =~ ^[0-9]+$ ]]; then
        # Block below 4 GB, warn between 4 and 8 GB (same thresholds as docker-installer-windows.ps1).
        gs_pre_ram "$total_ram_kb" || fatal=1
    else
        log_warning "Unable to determine total RAM. Continuing without enforcing RAM requirement."
    fi

    gs_check_disk || fatal=1

    gs_pre_ports || fatal=1

    GS_PRE_KIT_HOST_DOWN=0
    if command -v curl >/dev/null 2>&1; then
        log_info "Checking network reachability for required endpoints..."
        gs_pre_network || fatal=1
    else
        log_error "curl is not available; cannot verify network reachability."
        fatal=1
    fi

    # GS-PRE-031: the kit ID given up front must exist on the download server (HEAD request, nothing is downloaded).
    if gs_pre_kit_expected; then
        GS_PRE_KIT_CHECKED=$(gs_normalize_kit_id "$GLUESYNC_KIT_ID")
        if [ -z "$GS_PRE_KIT_CHECKED" ]; then
            :
        elif [ "$GS_PRE_KIT_HOST_DOWN" = 1 ]; then
            log_info "molo17.com is unreachable (see above); skipping the kit ID check."
        else
            gs_pre_check_kit_exists "$GS_PRE_KIT_CHECKED" || fatal=1
        fi
    fi

    if (( fatal )); then
        log_error "System pre-flight checks failed. Resolve the errors above and run the installer again."
        exit 1
    else
        log_success "System pre-flight checks passed."
    fi
}

# Check if running with sufficient privileges
check_permissions() {
    if [[ $EUID -eq 0 ]]; then
        log_success "Running as root"
        SUDO=""
    elif sudo -n true 2>/dev/null; then
        log_success "Running with sudo privileges"
        SUDO="sudo"
    else
        log_error "This script requires root privileges or sudo access"
        log_info "Please run as root or ensure you have sudo privileges"
        exit 1
    fi
}

# >>> gs-detect-distribution (copied byte for byte into gluesync-pre-flight-checker/gluesync-preflight.sh; keep identical)
# Detect Linux distribution
detect_distribution() {
    if [[ -f /etc/os-release ]]; then
        . /etc/os-release
        DISTRO=$ID
        VERSION=$VERSION_ID
        log_info "Detected distribution: $PRETTY_NAME"
    elif [[ -f /etc/redhat-release ]]; then
        DISTRO="rhel"
        log_info "Detected distribution: Red Hat Enterprise Linux"
    elif [[ -f /etc/debian_version ]]; then
        DISTRO="debian"
        log_info "Detected distribution: Debian"
    else
        log_error "Unable to detect Linux distribution"
        exit 1
    fi
}
# <<< gs-detect-distribution

# Install Docker on Fedora
install_fedora() {
    log_info "Installing Docker on Fedora..."

    # Install dnf plugins and add Docker repository
    log_info "Installing prerequisites and adding Docker repository..."
    $SUDO dnf -y install dnf-plugins-core zip jq
    $SUDO curl -fsSL https://download.docker.com/linux/fedora/docker-ce.repo -o /etc/yum.repos.d/docker-ce.repo

    # Install Docker
    log_info "Installing Docker packages..."
    $SUDO dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    # Start and enable Docker
    log_info "Starting and enabling Docker service..."
    $SUDO systemctl start docker
    $SUDO systemctl enable docker

    log_success "Docker installed successfully on Fedora"
}

# Install Docker on Ubuntu
install_ubuntu() {
    log_info "Installing Docker on Ubuntu..."

    # Update package index and install required packages
    log_info "Updating package index and installing prerequisites..."
    $SUDO apt-get update
    $SUDO apt-get install -y apt-transport-https ca-certificates curl software-properties-common zip jq

    # Add Docker's official GPG key
    log_info "Adding Docker GPG key..."
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | $SUDO gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

    # Set up the repository
    log_info "Setting up Docker repository..."
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | $SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null

    # Install Docker
    log_info "Installing Docker packages..."
    $SUDO apt-get update
    $SUDO apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    log_success "Docker installed successfully on Ubuntu"
}

# Install Docker on Red Hat
install_redhat() {
    log_info "Installing Docker on Red Hat..."

    # Install required packages
    log_info "Installing prerequisites..."
    $SUDO yum install -y yum-utils zip jq

    # Add Docker repository
    log_info "Adding Docker repository..."
    $SUDO yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

    # Install Docker
    log_info "Installing Docker packages..."
    $SUDO yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    # Start and enable Docker
    log_info "Starting and enabling Docker service..."
    $SUDO systemctl start docker
    $SUDO systemctl enable docker

    log_success "Docker installed successfully on Red Hat"
}

# Install Docker on Oracle Linux
install_oracle_linux() {
    log_info "Installing Docker on Oracle Linux..."

    # Install required packages
    log_info "Installing prerequisites..."
    $SUDO yum install -y yum-utils zip jq

    # Add Docker repository
    log_info "Adding Docker repository..."
    $SUDO yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

    # Install Docker
    log_info "Installing Docker packages..."
    $SUDO yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    # Start and enable Docker
    log_info "Starting and enabling Docker service..."
    $SUDO systemctl start docker
    $SUDO systemctl enable docker

    log_success "Docker installed successfully on Oracle Linux"
}

# Install Docker on Debian
install_debian() {
    log_info "Installing Docker on Debian..."

    # Update package index
    log_info "Updating package index..."
    $SUDO apt-get update

    # Install required packages
    log_info "Installing prerequisites..."
    $SUDO apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release zip jq

    # Add Docker's official GPG key
    log_info "Adding Docker GPG key..."
    curl -fsSL https://download.docker.com/linux/debian/gpg | $SUDO gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

    # Set up the repository
    log_info "Setting up Docker repository..."
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | $SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null

    # Install Docker
    log_info "Installing Docker packages..."
    $SUDO apt-get update
    $SUDO apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    log_success "Docker installed successfully on Debian"
}

# Install Docker on CentOS
install_centos() {
    log_info "Installing Docker on CentOS..."

    # Install required packages
    log_info "Installing prerequisites..."
    $SUDO yum install -y yum-utils zip jq

    # Add Docker repository
    log_info "Adding Docker repository..."
    $SUDO yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

    # Install Docker
    log_info "Installing Docker packages..."
    $SUDO yum install -y docker-ce docker-ce-cli containerd.io

    # Start and enable Docker
    log_info "Starting and enabling Docker service..."
    $SUDO systemctl start docker
    $SUDO systemctl enable docker

    log_success "Docker installed successfully on CentOS"
}

# Install Docker on Amazon Linux 2023
install_amazon_linux() {
    log_info "Installing Docker on Amazon Linux 2023..."

    # Update system packages
    log_info "Updating system packages..."
    $SUDO dnf update -y

    # Install Docker
    log_info "Installing Docker and utilities..."
    $SUDO dnf install -y docker zip jq

    # Enable and start the Docker service
    log_info "Enabling and starting Docker service..."
    $SUDO systemctl enable docker
    $SUDO systemctl start docker

    # Add user to Docker group
    log_info "Adding current user to Docker group..."
    $SUDO usermod -a -G docker $USER

    # Install Docker Compose (skipped when it is already there: the pre-flight does not probe github.com in that case)
    if gs_pre_compose_present; then
        log_info "Docker Compose is already installed; skipping its download."
    else
        log_info "Installing Docker Compose..."

        # Create directory for Docker CLI plugins
        $SUDO mkdir -p /usr/libexec/docker/cli-plugins/

        # Download Docker Compose
        log_info "Downloading Docker Compose..."
        $SUDO curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m) -o /usr/libexec/docker/cli-plugins/docker-compose

        # Make executable
        $SUDO chmod +x /usr/libexec/docker/cli-plugins/docker-compose
    fi

    log_warning "Please reboot the system to apply Docker group changes: sudo reboot"
    log_success "Docker installed successfully on Amazon Linux 2023"
}

# Enable and verify Podman socket with Docker compatibility
# Configures rootless Podman for the actual user (not root)
enable_podman_socket() {
    # Determine the actual user (the one who invoked sudo, or current user if not using sudo)
    local ACTUAL_USER="${SUDO_USER:-$USER}"
    local ACTUAL_UID=$(id -u "$ACTUAL_USER" 2>/dev/null || echo "")
    
    if [[ -z "$ACTUAL_UID" ]]; then
        log_error "Unable to determine actual user for Podman configuration"
        return 1
    fi
    
    log_info "Configuring rootless Podman for user: $ACTUAL_USER"
    
    # Enable user lingering (allows user services to run without login)
    log_info "Enabling lingering for $ACTUAL_USER..."
    loginctl enable-linger "$ACTUAL_USER" 2>/dev/null || true
    
    # Configure subuid and subgid for rootless containers
    if ! grep -q "^${ACTUAL_USER}:" /etc/subuid 2>/dev/null; then
        log_info "Configuring subuid for $ACTUAL_USER..."
        echo "${ACTUAL_USER}:100000:65536" | $SUDO tee -a /etc/subuid > /dev/null
    fi
    
    if ! grep -q "^${ACTUAL_USER}:" /etc/subgid 2>/dev/null; then
        log_info "Configuring subgid for $ACTUAL_USER..."
        echo "${ACTUAL_USER}:100000:65536" | $SUDO tee -a /etc/subgid > /dev/null
    fi
    
    # Enable Podman socket for the actual user (rootless mode)
    log_info "Enabling Podman socket for $ACTUAL_USER (rootless mode)..."
    sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR=/run/user/$ACTUAL_UID systemctl --user enable --now podman.socket 2>/dev/null || true
    
    # Verify socket is running
    if sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR=/run/user/$ACTUAL_UID systemctl --user is-active --quiet podman.socket 2>/dev/null; then
        log_success "Podman socket is active for $ACTUAL_USER"
    else
        log_warning "Podman socket may not be running properly for $ACTUAL_USER"
        log_info "The socket will be started when $ACTUAL_USER runs Gluesync"
    fi
    
    # Configure unprivileged port access for HTTPS (port 443)
    log_info "Configuring unprivileged port access for rootless containers..."
    if ! grep -q "net.ipv4.ip_unprivileged_port_start" /etc/sysctl.conf 2>/dev/null; then
        echo "net.ipv4.ip_unprivileged_port_start=443" | $SUDO tee -a /etc/sysctl.conf > /dev/null
        $SUDO sysctl -p > /dev/null 2>&1 || true
        log_success "Unprivileged port access configured (ports >= 443)"
    else
        log_info "Unprivileged port access already configured"
    fi
    
    log_success "Rootless Podman configured for $ACTUAL_USER"
}

# Install Podman on Fedora
install_podman_fedora() {
    log_info "Installing Podman on Fedora..."

    # Install Podman, podman-compose, and CNI plugins
    log_info "Installing Podman packages..."
    $SUDO dnf -y install podman podman-compose containernetworking-plugins zip

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on Fedora"
}

# Install Podman on Ubuntu
install_podman_ubuntu() {
    log_info "Installing Podman on Ubuntu..."

    # Update package index
    log_info "Updating package index..."
    $SUDO apt-get update

    # Install Podman and CNI plugins
    log_info "Installing Podman packages..."
    $SUDO apt-get install -y podman containernetworking-plugins zip

    # Install podman-compose via pip (system-wide)
    log_info "Installing podman-compose..."
    if ! command -v pip3 > /dev/null 2>&1; then
        $SUDO apt-get install -y python3-pip
    fi
    $SUDO pip3 install podman-compose --break-system-packages 2>/dev/null || $SUDO pip3 install podman-compose

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on Ubuntu"
}

# Install Podman on Debian
install_podman_debian() {
    log_info "Installing Podman on Debian..."

    # Update package index
    log_info "Updating package index..."
    $SUDO apt-get update

    # Install Podman and CNI plugins
    log_info "Installing Podman packages..."
    $SUDO apt-get install -y podman containernetworking-plugins zip

    # Install podman-compose via pip (system-wide)
    log_info "Installing podman-compose..."
    if ! command -v pip3 > /dev/null 2>&1; then
        $SUDO apt-get install -y python3-pip
    fi
    $SUDO pip3 install podman-compose --break-system-packages 2>/dev/null || $SUDO pip3 install podman-compose

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on Debian"
}

# Install Podman on Red Hat
install_podman_redhat() {
    log_info "Installing Podman on Red Hat..."

    # Install Podman, podman-compose, and CNI plugins
    log_info "Installing Podman packages..."
    $SUDO dnf -y install podman podman-compose containernetworking-plugins zip

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on Red Hat"
}

# Install Podman on Oracle Linux
install_podman_oracle_linux() {
    log_info "Installing Podman on Oracle Linux..."

    # Install Podman, podman-compose, and CNI plugins
    log_info "Installing Podman packages..."
    $SUDO dnf -y install podman podman-compose containernetworking-plugins zip

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on Oracle Linux"
}

# Install Podman on CentOS
install_podman_centos() {
    log_info "Installing Podman on CentOS..."

    # Install Podman and CNI plugins
    log_info "Installing Podman packages..."
    $SUDO yum install -y podman containernetworking-plugins zip

    # Install podman-compose via pip (system-wide)
    log_info "Installing podman-compose..."
    if ! command -v pip3 > /dev/null 2>&1; then
        $SUDO yum install -y python3-pip
    fi
    $SUDO pip3 install podman-compose --break-system-packages 2>/dev/null || $SUDO pip3 install podman-compose

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on CentOS"
}

# Install Podman on Amazon Linux
install_podman_amazon_linux() {
    log_info "Installing Podman on Amazon Linux 2023..."

    # Update system packages
    log_info "Updating system packages..."
    $SUDO dnf update -y

    # Install Podman and CNI plugins
    log_info "Installing Podman..."
    $SUDO dnf install -y podman containernetworking-plugins zip

    # Install podman-compose via pip (system-wide)
    log_info "Installing podman-compose..."
    if ! command -v pip3 > /dev/null 2>&1; then
        $SUDO dnf install -y python3-pip
    fi
    $SUDO pip3 install podman-compose --break-system-packages 2>/dev/null || $SUDO pip3 install podman-compose

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on Amazon Linux 2023"
}

# Install Podman on SUSE Linux Enterprise Server
install_podman_suse() {
    log_info "Installing Podman on SUSE Linux Enterprise Server..."

    # Refresh repositories
    log_info "Refreshing repositories..."
    if ! $SUDO zypper --non-interactive refresh; then
        log_error "Repository refresh failed."
        exit 1
    fi

    # Install Podman and CNI plugins
    log_info "Installing Podman package..."
    if ! $SUDO zypper --non-interactive install -y podman containernetworking-plugins zip; then
        log_error "Podman installation failed."
        exit 1
    fi

    # Install podman-compose via pip (system-wide)
    log_info "Installing podman-compose..."
    if ! command -v pip3 > /dev/null 2>&1; then
        $SUDO zypper --non-interactive install -y python3-pip
    fi
    $SUDO pip3 install podman-compose --break-system-packages 2>/dev/null || $SUDO pip3 install podman-compose

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on SUSE Linux Enterprise Server"
}

# Install Podman on openSUSE
install_podman_opensuse() {
    log_info "Installing Podman on openSUSE..."

    # Refresh repositories
    log_info "Refreshing repositories..."
    if ! $SUDO zypper --non-interactive refresh; then
        log_error "Repository refresh failed."
        exit 1
    fi

    # Install Podman and CNI plugins
    log_info "Installing Podman package..."
    if ! $SUDO zypper --non-interactive install -y podman containernetworking-plugins zip; then
        log_error "Podman installation failed."
        exit 1
    fi

    # Install podman-compose via pip (system-wide)
    log_info "Installing podman-compose..."
    if ! command -v pip3 > /dev/null 2>&1; then
        $SUDO zypper --non-interactive install -y python3-pip
    fi
    $SUDO pip3 install podman-compose --break-system-packages 2>/dev/null || $SUDO pip3 install podman-compose

    # Enable and verify Podman socket
    enable_podman_socket

    log_success "Podman installed successfully on openSUSE"
}

# Configure Docker logging driver
configure_logging() {
    log_info "Configuring Docker logging driver..."

    # Create or modify daemon.json
    $SUDO mkdir -p /etc/docker

    # Check if daemon.json exists and backup if necessary
    if [[ -f /etc/docker/daemon.json ]]; then
        log_warning "Existing daemon.json found, backing up to daemon.json.backup"
        $SUDO cp /etc/docker/daemon.json /etc/docker/daemon.json.backup
    fi

    # Create daemon.json with logging configuration
    cat << EOF | $SUDO tee /etc/docker/daemon.json > /dev/null
{
    "log-driver": "json-file",
    "log-opts": {
        "mode": "non-blocking",
        "max-buffer-size": "4m",
        "max-size": "100m",
        "max-file": "10"
    }
}
EOF

    # Restart Docker to apply changes
    log_info "Restarting Docker to apply logging configuration..."
    $SUDO systemctl restart docker

    log_success "Docker logging configured successfully"
}

# Fix CNI version in existing network config files
fix_cni_version() {
    log_info "Checking CNI network configuration files..."
    
    local CNI_DIR="/etc/cni/net.d"
    if [[ -d "$CNI_DIR" ]]; then
        # Find all .conflist files and downgrade CNI version to 0.4.0 if needed
        for conffile in "$CNI_DIR"/*.conflist; do
            if [[ -f "$conffile" ]]; then
                if grep -q '"cniVersion".*"1\.0\.0"' "$conffile" 2>/dev/null; then
                    log_info "Downgrading CNI version in $(basename "$conffile") to 0.4.0..."
                    $SUDO sed -i.bak 's/"cniVersion"[[:space:]]*:[[:space:]]*"1\.0\.0"/"cniVersion": "0.4.0"/g' "$conffile"
                fi
            fi
        done
        log_success "CNI configuration files updated"
    fi
}

# Configure Podman logging
configure_podman_logging() {
    log_info "Configuring Podman logging and registries..."

    # Determine the actual user (the one who invoked sudo, or current user if not using sudo)
    local ACTUAL_USER="${SUDO_USER:-$USER}"
    local ACTUAL_HOME=$(eval echo ~$ACTUAL_USER)
    
    # Create or modify containers.conf for logging
    local CONTAINERS_CONF_DIR="$ACTUAL_HOME/.config/containers"
    $SUDO mkdir -p "$CONTAINERS_CONF_DIR"

    # Check if containers.conf exists and backup if necessary
    if [[ -f "$CONTAINERS_CONF_DIR/containers.conf" ]]; then
        log_warning "Existing containers.conf found, backing up to containers.conf.backup"
        $SUDO cp "$CONTAINERS_CONF_DIR/containers.conf" "$CONTAINERS_CONF_DIR/containers.conf.backup"
    fi

    # Create containers.conf with logging configuration
    # Note: Using k8s-file driver for rootless Podman (json-file is not supported in rootless mode)
    cat << EOF | $SUDO tee "$CONTAINERS_CONF_DIR/containers.conf" > /dev/null
[containers]
log_driver = "k8s-file"
log_size_max = 104857600
EOF

    # Configure registries to include docker.io
    if [[ -f "$CONTAINERS_CONF_DIR/registries.conf" ]]; then
        log_warning "Existing registries.conf found, backing up to registries.conf.backup"
        $SUDO cp "$CONTAINERS_CONF_DIR/registries.conf" "$CONTAINERS_CONF_DIR/registries.conf.backup"
    fi

    # Create registries.conf with docker.io as default registry
    cat << EOF | $SUDO tee "$CONTAINERS_CONF_DIR/registries.conf" > /dev/null
# Default registries for image search and pull
[registries.search]
registries = ["docker.io"]

# Allow insecure registries if needed (uncomment if required)
# [registries.insecure]
# registries = []
EOF

    # Set proper ownership for the actual user
    $SUDO chown -R "$ACTUAL_USER:$ACTUAL_USER" "$CONTAINERS_CONF_DIR"

    # Fix CNI version in existing network configs
    fix_cni_version

    log_success "Podman logging and registries configured successfully"
}

# Verify Docker installation
verify_installation() {
    log_info "Verifying Docker installation..."

    # Check if Docker is running
    if ! $SUDO systemctl is-active --quiet docker; then
        log_error "Docker service is not running"
        return 1
    fi

    # Test Docker with hello-world
    log_info "Testing Docker installation with hello-world container..."
    if $SUDO docker run --rm hello-world > /dev/null 2>&1; then
        log_success "Docker hello-world test passed"
    else
        log_warning "Docker hello-world test failed - this may be due to network restrictions"
        log_info "Docker is installed but may require network access to Docker Hub"
    fi

    # Check Docker version
    DOCKER_VERSION=$($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo "unknown")
    log_info "Docker version: $DOCKER_VERSION"

    # Check Docker Compose version (if available)
    if command -v docker-compose > /dev/null 2>&1; then
        COMPOSE_VERSION=$(docker-compose version --short 2>/dev/null || echo "unknown")
        log_info "Docker Compose version: $COMPOSE_VERSION"
    elif $SUDO docker compose version > /dev/null 2>&1; then
        COMPOSE_VERSION=$($SUDO docker compose version --short 2>/dev/null || echo "unknown")
        log_info "Docker Compose (plugin) version: $COMPOSE_VERSION"
    fi

    log_success "Docker installation verification completed"
}

# Verify Podman installation
verify_podman_installation() {
    log_info "Verifying Podman installation..."

    # Test Podman with hello-world
    log_info "Testing Podman installation with hello-world container..."
    if podman run --rm hello-world > /dev/null 2>&1; then
        log_success "Podman hello-world test passed"
    else
        log_warning "Podman hello-world test failed - this may be due to network restrictions"
        log_info "Podman is installed but may require network access to container registries"
    fi

    # Check Podman version
    PODMAN_VERSION=$(podman version --format '{{.Version}}' 2>/dev/null || echo "unknown")
    log_info "Podman version: $PODMAN_VERSION"

    # Check podman-compose version
    if command -v podman-compose > /dev/null 2>&1; then
        COMPOSE_VERSION=$(podman-compose version 2>/dev/null | head -n1 || echo "unknown")
        log_info "podman-compose version: $COMPOSE_VERSION"
    fi

    log_success "Podman installation verification completed"
}

# Main function
main() {
    log_info "Starting Container Runtime Installation Script"
    log_info "=============================================="

    # Check permissions
    check_permissions

    # Allocate 10 GB swap (idempotent — skipped if already sufficient)
    setup_swap

    # For Podman, ensure we're running with sudo from a regular user (not as root directly)
    # This is required for proper rootless configuration
    if [[ "$CONTAINER_RUNTIME" == "podman" ]]; then
        # Check if running as root directly (without sudo)
        if [[ $EUID -eq 0 ]] && [[ -z "$SUDO_USER" ]]; then
            log_error "Podman installation must be run with 'sudo' from a regular user account."
            log_error "Running as root directly is not supported for rootless Podman setup."
            echo ""
            log_info "Please exit the root shell and run as a regular user with sudo:"
            log_info "  exit"
            log_info "  sudo $0"
            echo ""
            log_info "Why? Podman will be configured to run in rootless mode for your user account,"
            log_info "which provides better security by running containers without root privileges."
            log_info "The installer needs to know which user account to configure."
            exit 1
        fi
        
        # Check if running without root privileges at all
        if [[ $EUID -ne 0 ]]; then
            log_error "Podman installation requires root privileges to install system packages."
            log_info "Please run this installer with sudo:"
            log_info "  sudo $0"
            echo ""
            log_info "Note: Podman will be configured to run in rootless mode for your user,"
            log_info "but the installer needs root access to install the required packages."
            exit 1
        fi
        
        # At this point, we're running with sudo from a regular user
        local ACTUAL_USER="$SUDO_USER"
        log_info "Podman will be configured for rootless operation for user: $ACTUAL_USER"
    fi

    # Detect distribution (the pre-flight probes only the hosts this distribution's install path uses)
    detect_distribution

    # Ensure system meets baseline requirements
    check_system_requirements

    # Install container runtime based on user choice and distribution
    if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
        case $DISTRO in
            fedora)
                install_fedora
                ;;
            ubuntu)
                install_ubuntu
                ;;
            rhel|redhat|almalinux|rocky)
                install_redhat
                ;;
            ol)
                install_oracle_linux
                ;;
            debian)
                install_debian
                ;;
            centos)
                install_centos
                ;;
            amzn)
                install_amazon_linux
                ;;
            sles|suse)
                install_suse
                ;;
            opensuse* )
                install_opensuse
                ;;
            *)
                log_error "Unsupported distribution: $DISTRO"
                log_info "Supported distributions: Fedora, Ubuntu, Red Hat, AlmaLinux, Rocky Linux, Oracle Linux, Debian, CentOS, Amazon Linux, SUSE Linux Enterprise Server, openSUSE"
                exit 1
                ;;
        esac

        # Configure logging and Ubuntu-specific sysctl tuning (if applicable)
        configure_logging
        configure_ubuntu_network_tuning

        # Verify installation
        verify_installation

        clear 2>/dev/null || true

        log_success "Docker installation completed successfully!"
        log_info "You may need to reboot your system or log out/in for Docker group changes to take effect."
        log_info "To test Docker without sudo, run: docker ps"
    else
        # Podman installation
        case $DISTRO in
            fedora)
                install_podman_fedora
                ;;
            ubuntu)
                install_podman_ubuntu
                ;;
            rhel|redhat|almalinux|rocky)
                install_podman_redhat
                ;;
            ol)
                install_podman_oracle_linux
                ;;
            debian)
                install_podman_debian
                ;;
            centos)
                install_podman_centos
                ;;
            amzn)
                install_podman_amazon_linux
                ;;
            sles|suse)
                install_podman_suse
                ;;
            opensuse* )
                install_podman_opensuse
                ;;
            *)
                log_error "Unsupported distribution: $DISTRO"
                log_info "Supported distributions: Fedora, Ubuntu, Red Hat, AlmaLinux, Rocky Linux, Oracle Linux, Debian, CentOS, Amazon Linux, SUSE Linux Enterprise Server, openSUSE"
                exit 1
                ;;
        esac

        # Configure logging and Ubuntu-specific sysctl tuning (if applicable)
        configure_podman_logging
        configure_ubuntu_network_tuning

        # Verify installation
        verify_podman_installation

        clear 2>/dev/null || true

        log_success "Podman installation completed successfully!"
        log_info "Podman runs in rootless mode by default for enhanced security."
        log_info "To test Podman, run: podman ps"
    fi

    # Optional Gluesync setup
    echo ""
    if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
        log_info "🐳 Container runtime installation completed!"
    else
        log_info "📦 Container runtime installation completed!"
    fi
    echo ""
    local setup_default="n"
    if [ -n "${GLUESYNC_KIT_ID:-}" ]; then setup_default="y"; fi
    gs_ask "Would you like to proceed with Gluesync setup? (y/N): " "${GLUESYNC_SETUP:-}" "$setup_default" single
    if [[ $REPLY =~ ^[Yy] ]]; then
        setup_gluesync
    else
        log_info "Gluesync setup skipped. You can run this installer again later or set up Gluesync manually."
    fi
}

# Setup Gluesync after Docker installation
setup_gluesync() {
    log_info "🔧 Starting Gluesync setup..."

    # Determine kit folder name based on container runtime
    if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
        KIT_FOLDER="gluesync-docker"
    else
        KIT_FOLDER="gluesync-podman"
    fi

    # Ask for kit identifier
    echo ""
    log_info "📦 Gluesync Kit Setup"
    echo "   Please enter your Gluesync kit's unique ID from the download email."
    echo "   Format: a1b2c3d4e5f6 (12 hexadecimal characters)"
    echo ""

    while true; do
        gs_ask "Enter your kit ID (or press Enter to cancel): " "${GLUESYNC_KIT_ID:-}" ""
        KIT_ID=$(gs_normalize_kit_id "$REPLY")
        echo ""

        # Allow cancel
        if [[ -z "$KIT_ID" ]]; then
            if [ -z "$GS_TTY" ]; then
                log_info "No kit ID (set GLUESYNC_KIT_ID for an unattended install). Gluesync setup skipped."
            else
                log_info "Gluesync setup cancelled."
            fi
            return 1
        fi

        # Validate kit ID format (12 hex characters)
        if [[ ! $KIT_ID =~ ^[a-f0-9]{12}$ ]]; then
            log_error "Invalid kit ID format. Expected 12 hexadecimal characters (0-9, a-f)."
            log_info "Please check your download email for the correct kit ID."
            # An env or non-interactive answer can't be corrected by asking again.
            if [ -n "${GLUESYNC_KIT_ID:-}" ] || [ -z "$GS_TTY" ]; then
                log_error "GS-PRE-030 KIT_ID_INVALID: GLUESYNC_KIT_ID must be 12 hexadecimal characters (0-9, a-f) from your download e-mail."
                exit 26
            fi
            continue
        fi

        # GS-PRE-031: a kit ID typed at the prompt is checked here (the one from GLUESYNC_KIT_ID was checked in the pre-flight).
        if [ "${GS_PRE_KIT_CHECKED:-}" != "$KIT_ID" ] && ! gs_pre_check_kit_exists "$KIT_ID"; then
            if [ -n "${GLUESYNC_KIT_ID:-}" ] || [ -z "$GS_TTY" ]; then exit 1; fi
            continue
        fi

        # Construct full kit identifier
        KIT_IDENTIFIER="${KIT_ID}-trial-kit.zip"
        break
    done
    # One installId per run, shared by install_failed and install_succeeded.
    GS_TEL_INSTALL_ID=""

    # Create installation directory
    log_info "Creating installation directory..."
    $SUDO mkdir -p /opt/gluesync
    $SUDO chown $USER:$USER /opt/gluesync 2>/dev/null || true

    # Download kit
    log_info "Downloading Gluesync kit..."
    KIT_URL="https://molo17.com/gluesync-trials/${KIT_IDENTIFIER}"

    if wget -O "/tmp/gluesync-kit.zip" "$KIT_URL"; then
        log_success "Kit downloaded successfully"
    else
        gs_install_failed KIT_DOWNLOAD_FAILED "Failed to download Gluesync kit. Please check your kit identifier and internet connection."
    fi

    # Extract kit
    log_info "Extracting Gluesync kit..."
    if ! command -v unzip > /dev/null 2>&1; then
        log_info "Installing unzip..."
        # Try apt-get first (works for Debian, Ubuntu, and many others)
        if command -v apt-get > /dev/null 2>&1; then
            $SUDO apt-get update && $SUDO apt-get install -y unzip
        else
            case $DISTRO in
                fedora|amzn)
                    $SUDO dnf install -y unzip
                    ;;
                rhel|redhat|centos|ol)
                    $SUDO yum install -y unzip
                    ;;
                sles|suse|opensuse*)
                    $SUDO zypper --non-interactive install -y unzip
                    ;;
                *)
                    log_error "Unable to install unzip on this distribution. Please install unzip manually."
                    rm -f "/tmp/gluesync-kit.zip"
                    return 1
                    ;;
            esac
        fi
    fi
    if unzip -q "/tmp/gluesync-kit.zip" -d "/tmp/gluesync-extract"; then
        log_success "Kit extracted successfully"
    else
        rm -f "/tmp/gluesync-kit.zip"
        gs_install_failed KIT_EXTRACT_FAILED "Failed to extract Gluesync kit"
    fi

    # Check extracted contents
    cd "/tmp/gluesync-extract"
    if [[ ! -d "$KIT_FOLDER" ]]; then
        cd /
        rm -rf "/tmp/gluesync-extract" "/tmp/gluesync-kit.zip"
        gs_install_failed KIT_INVALID "$KIT_FOLDER folder not found in the kit"
    fi

    # Copy kit contents to /opt/gluesync
    log_info "Installing Gluesync $CONTAINER_RUNTIME components..."
    if cp -r "$KIT_FOLDER"/* /opt/gluesync/ 2>/dev/null; then
        log_success "Gluesync $CONTAINER_RUNTIME components installed"
    else
        cd /
        rm -rf "/tmp/gluesync-extract" "/tmp/gluesync-kit.zip"
        gs_install_failed KIT_COPY_FAILED "Failed to install Gluesync $CONTAINER_RUNTIME components"
    fi

    # Copy root level files (excluding the platform-specific folders)
    log_info "Installing Gluesync configuration files..."
    for item in *; do
        if [[ "$item" != "gluesync-docker" && "$item" != "gluesync-podman" && "$item" != "gluesync-docker-windows" && "$item" != "gluesync-docker-windows-2019" && "$item" != "gluesync-kubernetes" ]]; then
            if [[ -f "$item" ]] || [[ -d "$item" ]]; then
                if cp -r "$item" /opt/gluesync/ 2>/dev/null; then
                    log_info "Installed: $item"
                fi
            fi
        fi
    done

    # Cleanup
    cd /
    rm -rf "/tmp/gluesync-extract" "/tmp/gluesync-kit.zip"

    # Set proper permissions
    log_info "Setting permissions..."
    
    # Determine the actual user for ownership
    local ACTUAL_USER="${SUDO_USER:-$USER}"
    
    if [[ "$CONTAINER_RUNTIME" == "podman" ]]; then
        # For Podman, set ownership to the actual user (rootless mode)
        log_info "Setting ownership to $ACTUAL_USER for rootless Podman..."
        $SUDO chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/gluesync
    else
        # For Docker, use current user
        $SUDO chown -R $USER:$USER /opt/gluesync 2>/dev/null || true
    fi

    # Ensure the Core Hub image variant matches the CPU capabilities
    apply_core_hub_cpu_compatibility

    log_success "🎉 Gluesync setup completed successfully!"
    log_info "Installation location: /opt/gluesync"
    log_info "You can now proceed with Gluesync configuration and startup."
    log_info "Check the Gluesync documentation for next steps."

    echo ""
    gs_ask "Would you like to start Gluesync now? (y/N): " "${GLUESYNC_START:-}" "n" single
    START_SUCCESS=false
    if [[ $REPLY =~ ^[Yy] ]]; then
        log_info "Starting Gluesync..."
        
        # run.sh gets the terminal when there is one, else /dev/null (with curl | bash, stdin is this script's text).
        local run_stdin="${GS_TTY:-/dev/null}"
        # Run Gluesync as the actual user for Podman (rootless), or normally for Docker
        if [[ "$CONTAINER_RUNTIME" == "podman" ]]; then
            log_info "Starting Gluesync as $ACTUAL_USER (rootless mode)..."
            # shellcheck disable=SC2024  # the redirect is meant to be opened by this (root) shell
            if sudo -u "$ACTUAL_USER" bash -c "cd /opt/gluesync && ./run.sh" < "$run_stdin"; then
                START_SUCCESS=true
            else
                START_SUCCESS=false
            fi
        else
            if (cd /opt/gluesync && ./run.sh) < "$run_stdin"; then
                START_SUCCESS=true
            else
                START_SUCCESS=false
            fi
        fi
        
        if [[ "$START_SUCCESS" != "true" ]]; then
            gs_install_failed START_FAILED "Failed to start Gluesync: run.sh exited with an error. Check the output above and run /opt/gluesync/run.sh manually."
        fi
        # run.sh returning 0 is not enough: only claim success once the containers are actually running.
        log_info "Verifying that the Gluesync containers are running (up to 3 minutes)..."
        if ! gs_wait_containers "$CONTAINER_RUNTIME" /opt/gluesync 180 "$ACTUAL_USER"; then
            START_SUCCESS=false
            gs_install_failed CONTAINERS_NOT_RUNNING "run.sh finished but the Gluesync containers are not running (${GS_CONTAINER_DETAIL}). Check '$CONTAINER_RUNTIME ps -a' and the container logs."
        fi
        log_info "Container check: ${GS_CONTAINER_DETAIL}"
        if [[ "$START_SUCCESS" == "true" ]]; then
            log_success "Gluesync started successfully!"
            HOST_IP=$(hostname -I | awk '{print $1}')
            echo ""
            echo "╔══════════════════════════════════════════════════════════════╗"
            echo "║                                                              ║"
            echo "║                 🎉 GLUESYNC IS NOW RUNNING!                  ║"
            echo "║                                                              ║"
            echo "║  Access URL: https://$HOST_IP                           ║"
            echo "║                                                              ║"
            echo "║  Default username: admin                                     ║"
            echo "║  Default password: admin                                     ║"
            echo "║                                                              ║"
            echo "║  Keep this URL safe and share only with authorized users.    ║"
            echo "║                                                              ║"
            echo "╚══════════════════════════════════════════════════════════════╝"
            echo ""
        fi
    else
        log_info "You can start Gluesync later by running /opt/gluesync/run.sh"
        if [[ "$CONTAINER_RUNTIME" == "podman" ]]; then
            log_info "Note: After first start, run the following to fix CNI warnings:"
            echo "  sudo sed -i.bak 's/\"cniVersion\"[[:space:]]*:[[:space:]]*\"1\\.0\\.0\"/\"cniVersion\": \"0.4.0\"/g' /etc/cni/net.d/*.conflist"
        fi
    fi

    # Install confirmation (non-blocking); platformStarted reflects the y/N answer and the run.sh result.
    local TELEMETRY_STARTED="false"
    if [[ "${START_SUCCESS:-false}" == "true" ]]; then TELEMETRY_STARTED="true"; fi
    send_install_telemetry "$KIT_ID" "$TELEMETRY_STARTED" "$CONTAINER_RUNTIME" /opt/gluesync || true
}

# Named fatal error after the kit ID is known: sends install_failed (errorCode $1; same non-blocking policy as the
# success confirmation) and exits with the matching code. Console/log name GS-RUN-<CODE>.
gs_install_failed() {
    local code="$1" msg="${2:-}" rc=1
    case "$code" in
        KIT_DOWNLOAD_FAILED) rc=22 ;; KIT_EXTRACT_FAILED) rc=23 ;; KIT_INVALID) rc=24 ;; KIT_COPY_FAILED) rc=25 ;;
        START_FAILED) rc=31 ;; CONTAINERS_NOT_RUNNING) rc=32 ;;
    esac
    log_error "GS-RUN-${code}: ${msg}"
    send_install_telemetry "${KIT_ID:-}" false "${CONTAINER_RUNTIME:-docker}" /opt/gluesync install_failed "$code" || true
    log_error "Installation failed (GS-RUN-${code}, exit code ${rc})."
    exit "$rc"
}

# Waits until the kit's containers run: the Gluesync Core Hub container when the kit's compose file names one, else at
# least one running container. $1 docker|podman, $2 install dir, $3 timeout s, $4 user for rootless podman.
# Sets GS_CONTAINER_DETAIL. Returns 0 when up.
gs_wait_containers() {
    local runtime="${1:-docker}" dir="${2:-/opt/gluesync}" max="${3:-180}" user="${4:-}" expect_hub=0 services=0 start images running hub ok
    local -a cmd=()
    if [ "$runtime" = "podman" ] && [ -n "$user" ] && [ "${EUID:-$(id -u)}" -eq 0 ]; then cmd=(sudo -u "$user" podman)
    elif [ "$runtime" = "podman" ]; then cmd=(podman)
    else
        if [ -n "${SUDO:-}" ]; then cmd=("$SUDO" docker); else cmd=(docker); fi
    fi
    if [ -d "$dir" ]; then
        services=$(find "$dir" -maxdepth 3 -type f \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' \) -exec grep -hE '^[[:space:]]*image:' {} + 2>/dev/null | grep -c . || true)
        if find "$dir" -maxdepth 3 -type f \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' \) -exec grep -hE '^[[:space:]]*image:.*gluesync-core-hub' {} + 2>/dev/null | grep -q .; then expect_hub=1; fi
    fi
    start=$(date +%s)
    while true; do
        ok=1
        if images=$("${cmd[@]}" ps --filter status=running --format '{{.Image}}' 2>/dev/null); then
            running=$(printf '%s\n' "$images" | grep -c . || true)
            hub=$(printf '%s\n' "$images" | grep -ci 'gluesync-core-hub' || true)
            GS_CONTAINER_DETAIL="${running} running container(s)"
            [ "${services:-0}" -gt 0 ] && GS_CONTAINER_DETAIL="$GS_CONTAINER_DETAIL, kit defines ${services} service(s)"
            if [ "$expect_hub" -eq 1 ]; then
                if [ "${hub:-0}" -gt 0 ]; then GS_CONTAINER_DETAIL="$GS_CONTAINER_DETAIL, core hub up"; ok=0
                else GS_CONTAINER_DETAIL="$GS_CONTAINER_DETAIL, core hub not running"; fi
            elif [ "${running:-0}" -ge 1 ]; then ok=0
            fi
        else
            GS_CONTAINER_DETAIL="$runtime ps failed"
        fi
        [ "$ok" -eq 0 ] && return 0
        [ $(( $(date +%s) - start )) -ge "$max" ] && return 1
        sleep 5
    done
}

# Run main function
gs_pre_parse_args "$@"
main "$@"
