#!/usr/bin/env bash

# Exit immediately if a command exits with a non-zero status
set -euo pipefail

# Everything below runs inside a function that is called on the very last
# line, which is what makes `curl | bash` safe to interrupt.
#
# Piped into bash, this script is executed as it arrives. Left as top-level
# statements, a connection dropped mid-transfer would run whichever prefix
# had landed — and this installer opens firewall ports, rewrites sshd_config
# and swaps a root service, so a half-run is not a no-op. Wrapped, bash has
# to read the closing brace before it can call anything, so a truncated
# download does nothing at all.
#
# Indentation is left as it was: bash does not care, and reindenting 800
# lines would bury this change in noise.
__rubuz_install_main() {

GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'
BOLD='\033[1m'
DIM='\033[2m'

LOG_FILE=$(mktemp /tmp/rubuz-install.XXXXXX.log)
WORK_DIR=$(mktemp -d /tmp/rubuz-install.XXXXXX)
APT_UPDATE_MARKER="$WORK_DIR/apt-updated"
chmod 600 "$LOG_FILE"

RUBUZ_PORT="${RUBUZ_PORT:-3000}"
RUBUZ_HEALTH_TIMEOUT="${RUBUZ_HEALTH_TIMEOUT:-90}"
SSH_PORT="${SSH_PORT:-}"

# PaperScale by default, not Rubuz: PaperScale is the edition most visitors
# to this script actually want (see internal/brand's own comment on the
# split). get.rubuz.com serves this same file with RUBUZ_EDITION=rubuz
# exported ahead of it (see functions/index.js) so that domain installs the
# other edition. Determined this early, not just before the download further
# down, because it also names the product in every message between here and
# there — the banner, the OS gate, the phase headers.
EDITION="${RUBUZ_EDITION:-paperscale}"
case "$EDITION" in
    rubuz)
        EDITION_DISPLAY_NAME="Rubuz"
        EDITION_DOMAIN="downloads.rubuz.com"
        EDITION_GET_DOMAIN="get.rubuz.com"
        ;;
    paperscale)
        EDITION_DISPLAY_NAME="PaperScale"
        EDITION_DOMAIN="downloads.paperscale.io"
        EDITION_GET_DOMAIN="get.paperscale.io"
        ;;
    *)
        echo -e "${RED}[!] RUBUZ_EDITION must be 'rubuz' or 'paperscale', got: $EDITION${NC}"
        exit 1
        ;;
esac

# This script only ever performs a first install. Rubuz's own binary updates
# itself in place (internal/service/panel_updates.go) — a Go-native download,
# signature check, swap and rollback that already knows the panel's exact
# running state, which a shell script reaching in from outside does not. An
# --update mode lived here once, for a Docker-to-Podman migration that ended
# when Docker support did; nothing has invoked it since.
on_exit() {
    local exit_code=$?
    trap - EXIT
    rm -rf -- "$WORK_DIR"
    if [ "$exit_code" -eq 0 ]; then
        rm -f -- "$LOG_FILE"
    fi
    exit "$exit_code"
}
trap on_exit EXIT

if [[ ! "$RUBUZ_PORT" =~ ^[0-9]+$ ]] || [ "$RUBUZ_PORT" -lt 1 ] || [ "$RUBUZ_PORT" -gt 65535 ]; then
    echo -e "${RED}[!] RUBUZ_PORT must be a number between 1 and 65535.${NC}"
    exit 1
fi
if [[ ! "$RUBUZ_HEALTH_TIMEOUT" =~ ^[0-9]+$ ]] || [ "$RUBUZ_HEALTH_TIMEOUT" -lt 1 ] || [ "$RUBUZ_HEALTH_TIMEOUT" -gt 600 ]; then
    echo -e "${RED}[!] RUBUZ_HEALTH_TIMEOUT must be a number between 1 and 600 seconds.${NC}"
    exit 1
fi

# Letter-art only ever spelled out "RUBUZ" — fine for one product name, not
# a design that survives a second one of a different length. A plain rule
# scales to both without maintaining two hand-aligned banners.
show_banner() {
    clear 2>/dev/null || true
    echo -e "${BLUE}${BOLD}"
    echo "════════════════════════════════════════"
    echo "   ${EDITION_DISPLAY_NAME} Installer"
    echo "════════════════════════════════════════"
    echo -e "${NC}\n"
}

show_banner

# 1. Check for root
if [ "$EUID" -ne 0 ]; then
  echo -e "${RED}[!] Please run as root (use: sudo curl -fsSL https://${EDITION_GET_DOMAIN} | sudo bash)${NC}"
  exit 1
fi

# 2. Check for Systemd
if ! command -v systemctl &> /dev/null; then
    echo -e "${RED}[!] Systemd is required but was not found.${NC}"
    exit 1
fi

# Rubuz targets Debian: the apt/systemd platform it ships and tests against,
# failing early on systems where the package, firewall and service setup below
# would only half-complete rather than midway through as root.
MIN_DEBIAN_MAJOR=13
OS_ID=""
OS_VERSION_ID=""
OS_PRETTY_NAME="Linux"
if [ -r /etc/os-release ]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    OS_ID="${ID:-}"
    OS_VERSION_ID="${VERSION_ID:-}"
    OS_PRETTY_NAME="${PRETTY_NAME:-Linux}"
fi
case "$OS_ID" in
    debian) ;;
    *)
        echo -e "${RED}[!] Unsupported operating system: ${OS_PRETTY_NAME}.${NC}"
        echo -e "${RED}    ${EDITION_DISPLAY_NAME} supports Debian ${MIN_DEBIAN_MAJOR} or newer.${NC}"
        exit 1
        ;;
esac

# The release matters as much as the distribution, because what actually runs
# every app on this server is podman, and its version is whatever the release
# froze: trixie ships 5.4, bookworm 4.3, bullseye 3.0. Only trixie's is tested
# here and in tests/install-smoke.sh.
#
# Refusing is the kind option. An older podman does not fail at install time —
# it installs, reports success, and then diverges later on the parts that moved
# between those versions: restart policies that decide whether a customer's
# apps come back after a reboot, and the socket API Traefik reads to discover
# them. A server that is fine until its first reboot is worse than one that
# said no on day one.
OS_MAJOR="${OS_VERSION_ID%%.*}"
case "$OS_MAJOR" in
    '' | *[!0-9]*)
        # Testing and unstable carry no VERSION_ID at all.
        echo -e "${RED}[!] Could not determine the Debian release (${OS_PRETTY_NAME}).${NC}"
        echo -e "${RED}    ${EDITION_DISPLAY_NAME} supports Debian ${MIN_DEBIAN_MAJOR} or newer — a stable release, not testing or unstable.${NC}"
        exit 1
        ;;
esac
if [ "$OS_MAJOR" -lt "$MIN_DEBIAN_MAJOR" ]; then
    echo -e "${RED}[!] Debian ${OS_MAJOR} is too old: ${OS_PRETTY_NAME}.${NC}"
    echo -e "${RED}    ${EDITION_DISPLAY_NAME} supports Debian ${MIN_DEBIAN_MAJOR} or newer, for the podman version it ships.${NC}"
    echo -e "${DIM}    Reinstall this server with Debian ${MIN_DEBIAN_MAJOR} and run this installer again.${NC}"
    exit 1
fi
if ! command -v apt-get >/dev/null 2>&1; then
    echo -e "${RED}[!] apt-get is required on Debian.${NC}"
    exit 1
fi

if [ -z "$SSH_PORT" ] && command -v sshd >/dev/null 2>&1; then
    SSH_PORT=$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }' || true)
fi
SSH_PORT="${SSH_PORT:-22}"
if [[ ! "$SSH_PORT" =~ ^[0-9]+$ ]] || [ "$SSH_PORT" -lt 1 ] || [ "$SSH_PORT" -gt 65535 ]; then
    echo -e "${RED}[!] SSH_PORT must be a number between 1 and 65535.${NC}"
    exit 1
fi

get_diagnostics() {
    CPU_ARCH=$(uname -m)
    if command -v free >/dev/null 2>&1; then
        TOTAL_MEM=$(free -h | awk '/^Mem:/ {print $2}' 2>/dev/null || echo "Unknown")
    elif [ -r /proc/meminfo ]; then
        TOTAL_MEM=$(awk '/^MemTotal:/ { printf "%.1f GiB\n", $2 / 1024 / 1024 }' /proc/meminfo)
    else
        TOTAL_MEM="Unknown"
    fi
    PUBLIC_IP=$(curl -s --max-time 2 https://ipinfo.io/ip || echo "Unknown")
    
    FIREWALL_STATUS="Inactive"
    if command -v ufw >/dev/null 2>&1; then
        if ufw status | grep -qE '^Status:[[:space:]]+active$'; then
            FIREWALL_STATUS="Active (UFW)"
        fi
    fi

    echo -e "${BOLD}🔍 System Diagnostics:${NC}"
    echo -e "   ${DIM}•${NC} OS:          $OS_PRETTY_NAME"
    echo -e "   ${DIM}•${NC} CPU Arch:    $CPU_ARCH"
    echo -e "   ${DIM}•${NC} Memory:      $TOTAL_MEM"
    echo -e "   ${DIM}•${NC} Public IP:   $PUBLIC_IP"
    echo -e "   ${DIM}•${NC} Firewall:    $FIREWALL_STATUS"
    echo ""
}

get_diagnostics

countdown() {
    echo -e "${BLUE}${BOLD}ℹ  Notice:${NC} ${EDITION_DISPLAY_NAME} will configure UFW firewall rules and secure SSH access."
    echo -n -e "   Starting setup in 5 seconds (Press Ctrl+C to cancel)... "
    for i in {5..1}; do
        echo -n -e "${BOLD}$i... ${NC}"
        sleep 1
    done
    echo -e "\n"
}

countdown

# Helper: Wait for apt/dpkg lock to be released
wait_for_apt_lock() {
    local max_wait=120
    local waited=0
    if ! command -v fuser >/dev/null 2>&1; then
        return 0
    fi
    while fuser /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock >/dev/null 2>&1; do
        if [ $waited -ge $max_wait ]; then
            echo -e "${RED}[!] Timed out waiting for apt lock.${NC}" >> "$LOG_FILE"
            return 1
        fi
        sleep 5
        waited=$((waited + 5))
    done
    return 0
}

# Helper: Run apt-get with lock waiting
apt_get_safe() {
    local attempt
    for attempt in 1 2 3; do
        wait_for_apt_lock || return 1
        if DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 "$@"; then
            return 0
        fi
        sleep $((attempt * 2))
    done
    return 1
}

apt_update_once() {
    if [ -f "$APT_UPDATE_MARKER" ]; then
        return 0
    fi
    apt_get_safe update
    touch "$APT_UPDATE_MARKER"
}

# fetch_with_retry downloads a small file — the version marker, a checksum, a
# signature — with the same tolerance for a dropped connection that
# download_binary already has for the ~30MB binary itself. These are far
# smaller, but they cross the same network on the same freshly-booted VPS,
# and a blip does not check the size of the request before it happens; before
# this, only the binary got a second try.
#
# Every attempt writes to one scratch file via -o, win or lose, so a partial
# response from a failed attempt is fully overwritten by the next rather than
# concatenated onto it — which matters for the no-$out form below, where the
# file's content becomes the caller's captured stdout.
fetch_with_retry() {
    local url="$1"
    local out="${2:-}"
    local max_retries=5
    local retry_count=0
    local tmp
    tmp=$(mktemp "$WORK_DIR/fetch.XXXXXX")
    while [ "$retry_count" -lt "$max_retries" ]; do
        if curl -fsSL --connect-timeout 10 --max-time 15 "$url" -o "$tmp" 2>>"$LOG_FILE"; then
            if [ -n "$out" ]; then
                mv "$tmp" "$out"
            else
                cat "$tmp"
                rm -f "$tmp"
            fi
            return 0
        fi
        retry_count=$((retry_count + 1))
        sleep 2
    done
    rm -f "$tmp"
    return 1
}

run_silent() {
    local task_name="$1"
    shift
    
    # Run the task command in the background
    "$@" >> "$LOG_FILE" 2>&1 &
    local pid=$!
    
    local spin_chars=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
    
    # Hide cursor
    tput civis 2>/dev/null || true
    
    while kill -0 "$pid" 2>/dev/null; do
        for char in "${spin_chars[@]}"; do
            if ! kill -0 "$pid" 2>/dev/null; then
                break
            fi
            printf "\r   ${DIM}•${NC} %s... %s" "$task_name" "$char"
            sleep 0.1
        done
    done
    
    # Restore cursor
    tput cnorm 2>/dev/null || true
    
    # Wait for background task and check its exit status
    if wait "$pid"; then
        printf "\r   ${DIM}•${NC} %s... ${GREEN}${BOLD}✔ Done${NC}\e[K\n" "$task_name"
    else
        printf "\r   ${DIM}•${NC} %s... ${RED}${BOLD}✖ Failed${NC}\e[K\n" "$task_name"
        echo -e "      ${RED}Error details written to $LOG_FILE${NC}"
        exit 1
    fi
}

# ----------------------------------------------------
# Phase 1: Dependencies
# ----------------------------------------------------
echo -e "${BOLD}⚡ Phase 1: Installing Core Dependencies${NC}"

# curl check/install
install_curl() {
    if ! command -v curl &> /dev/null || [ ! -s /etc/ssl/certs/ca-certificates.crt ]; then
        apt_update_once && apt_get_safe install -y ca-certificates curl
    fi
}
run_silent "Checking curl utility" install_curl

# restic install
install_restic() {
    if ! command -v restic &> /dev/null; then
        apt_update_once && apt_get_safe install -y restic
    fi
}
run_silent "Installing Restic Backup Engine" install_restic

# podman install
#
# Two things this needs told explicitly that a container runtime doesn't
# always give for free:
#
# 1. A bare "postgres:16-alpine" has no registry host; Podman refuses to
#    resolve that short form outright unless a search registry is
#    configured. Every image in the catalog is written the short way, so
#    this is not optional.
# 2. Traefik discovers containers by mounting the runtime's Docker-compatible
#    API socket. Podman's is socket-activated and off until `podman.socket`
#    is enabled — Traefik would otherwise start against a socket that does
#    not exist yet.
install_podman() {
    if ! command -v podman &> /dev/null; then
        apt_update_once && apt_get_safe install -y podman
    fi

    mkdir -p /etc/containers/registries.conf.d
    echo 'unqualified-search-registries = ["docker.io"]' > /etc/containers/registries.conf.d/rubuz.conf

    systemctl enable --now podman.socket

    # Podman has no daemon of its own to enforce each container's --restart
    # policy the way dockerd did. podman-restart.service is the systemd unit
    # that re-applies those policies after a reboot; without it, a host restart
    # leaves Traefik and every app dead until someone starts them by hand.
    #
    # It ships filtering on restart-policy=always, but every container Rubuz
    # creates uses unless-stopped — the policy that does *not* resurrect
    # something an operator deliberately stopped, which is what the panel's own
    # stopped-app state depends on. That filter is an exact string match, so
    # without this override the unit runs at boot, matches nothing, and enabling
    # it accomplishes exactly nothing.
    mkdir -p /etc/systemd/system/podman-restart.service.d
    cat > /etc/systemd/system/podman-restart.service.d/rubuz-unless-stopped.conf <<'DROPIN'
[Service]
ExecStart=
ExecStart=/usr/bin/podman $LOGGING start --all --filter restart-policy=unless-stopped
DROPIN
    systemctl daemon-reload
    systemctl enable --now podman-restart.service
}

run_silent "Configuring Podman" install_podman

# swap configuration
#
# Best-effort: some hosts — a container, certain restricted or already-
# swap-managed VPS tiers — refuse swapon outright (EPERM), not because
# anything is wrong but because they do not permit a guest to manage swap.
# Rubuz runs fine without the added swap; failing the entire install over a
# performance cushion it does not strictly need would not be proportionate,
# and nothing before this line depends on it existing.
configure_swap() {
    local swap_total swap_ok=1
    swap_total=$(free -m | awk '/^Swap:/ {print $2}' 2>/dev/null || echo "0")

    if [ "$swap_total" -eq 0 ]; then
        if [ ! -f /swapfile ]; then
            # Attempt fallocate, fallback to dd if unsupported by filesystem
            if ! { fallocate -l 2G /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count=2048 status=none 2>/dev/null; }; then
                swap_ok=0
            elif ! { chmod 600 /swapfile && mkswap /swapfile >/dev/null 2>&1; }; then
                swap_ok=0
            fi
        fi
        if [ "$swap_ok" -eq 1 ] && ! swapon /swapfile >/dev/null 2>&1; then
            swap_ok=0
        fi
    fi

    if [ "$swap_ok" -eq 0 ]; then
        echo "Could not configure swap (no permission to manage it here); continuing without it." >> "$LOG_FILE"
        rm -f /swapfile
        return 0
    fi

    if [ -f /swapfile ] && ! grep -qE '^/swapfile[[:space:]]' /etc/fstab; then
        echo '/swapfile none swap sw 0 0' >> /etc/fstab
    fi

    # Apply immediately, then persist through systemd-sysctl. Debian 13 no
    # longer reads /etc/sysctl.conf; /etc/sysctl.d is the supported location.
    sysctl vm.swappiness=10 >/dev/null 2>&1 || true
    sysctl vm.vfs_cache_pressure=50 >/dev/null 2>&1 || true
    mkdir -p /etc/sysctl.d
    cat > /etc/sysctl.d/99-rubuz.conf <<'SYSCTL'
vm.swappiness=10
vm.vfs_cache_pressure=50
SYSCTL
    sysctl --system >/dev/null 2>&1 || true
}
run_silent "Configuring 2GB Virtual Swap Space" configure_swap

echo ""

# ----------------------------------------------------
# Phase 2: Security Shield
# ----------------------------------------------------
echo -e "${BOLD}🔒 Phase 2: Running Security Shield${NC}"

# firewall setup
configure_firewall() {
    if command -v ufw > /dev/null 2>&1 || { apt_update_once && apt_get_safe install -y ufw; }; then
        ufw default deny incoming
        ufw default allow outgoing
        
        # Podman requires FORWARD policy to be ACCEPT, otherwise mapped ports 
        # (like 80, 443 for Traefik) will be dropped when routed to containers.
        if [ -f /etc/default/ufw ]; then
            sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
        fi

        # Podman (unlike Docker) does not bypass UFW. Container DNS (aardvark-dns)
        # and inter-container communication go through the host's bridge interfaces.
        # Without these rules, container DNS resolution fails and containers on the
        # same network cannot reach each other.
        if [ -f /etc/ufw/before.rules ]; then
            if ! grep -q "podman bridge" /etc/ufw/before.rules; then
                sed -i '/-A ufw-before-input -i lo -j ACCEPT/a \
# Allow all traffic on podman bridge interfaces (container DNS + networking)\
-A ufw-before-input -i podman+ -j ACCEPT\
-A ufw-before-forward -i podman+ -j ACCEPT\
-A ufw-before-forward -o podman+ -j ACCEPT' /etc/ufw/before.rules
            fi
        fi

        ufw allow 80/tcp
        ufw allow 443/tcp
        ufw allow "${RUBUZ_PORT}/tcp"
        if [ "${SSH_PORT}" != "22" ]; then
            ufw allow "${SSH_PORT}/tcp"
        else
            ufw allow 22/tcp
        fi
        ufw deny 3306/tcp
        ufw deny 5432/tcp
        ufw deny 6379/tcp
        ufw deny 27017/tcp
        ufw --force enable

        # Apply Podman bridge rules immediately (ufw enable loads before.rules)
        iptables -C INPUT -i podman+ -j ACCEPT 2>/dev/null || iptables -I INPUT -i podman+ -j ACCEPT

        # UFW Reload Edge Case: When UFW reloads, it flushes iptables. This wipes
        # Podman's dynamic NAT and port-forwarding rules (so ports 80/443 break).
        # We add a UFW hook to automatically restore Podman networks on reload.
        if command -v podman > /dev/null 2>&1; then
            cat << 'EOF' > /etc/ufw/after.init
#!/bin/sh
# This script is executed by UFW after it finishes loading rules.
# We use it to restore Podman's iptables NAT rules which UFW flushes.
case "$1" in
    start|restart|reload|force-reload)
        if command -v podman >/dev/null 2>&1; then
            # Supress output to prevent UFW status warnings
            podman network reload --all >/dev/null 2>&1 || true
        fi
        ;;
esac
EOF
            chmod +x /etc/ufw/after.init
        fi
    else
        return 1
    fi
}
run_silent "Tightening UFW Firewall (Blocking database ports)" configure_firewall

# SSH hardening
harden_ssh() {
    if [ -f /etc/ssh/sshd_config ]; then
        local dropin_dir="/etc/ssh/sshd_config.d"
        local dropin="$dropin_dir/00-rubuz.conf"
        local main_backup="$WORK_DIR/sshd_config.before-rubuz"
        local dropin_backup="$WORK_DIR/00-rubuz.conf.before-rubuz"
        local dropin_existed=0

        cp /etc/ssh/sshd_config "$main_backup"
        if [ ! -f /etc/ssh/sshd_config.rubuz.bak ]; then
            cp /etc/ssh/sshd_config /etc/ssh/sshd_config.rubuz.bak
        fi

        mkdir -p "$dropin_dir"
        if [ -f "$dropin" ]; then
            cp "$dropin" "$dropin_backup"
            dropin_existed=1
        fi
        if ! grep -qE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' /etc/ssh/sshd_config; then
            sed -i '1iInclude /etc/ssh/sshd_config.d/*.conf' /etc/ssh/sshd_config
        fi

        {
            printf 'Port %s\n' "$SSH_PORT"
            printf 'PermitEmptyPasswords no\n'
            # Only disable password authentication if SSH keys are configured.
            # A 00-prefixed drop-in is evaluated before cloud-init defaults;
            # OpenSSH uses the first value it finds for these options.
            if [ -s /root/.ssh/authorized_keys ]; then
                printf 'PasswordAuthentication no\n'
                printf 'KbdInteractiveAuthentication no\n'
                printf 'PermitRootLogin prohibit-password\n'
            else
                printf 'PasswordAuthentication yes\n'
                printf 'PermitRootLogin yes\n'
            fi
        } > "$dropin"

        if sshd -t; then
            systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
            return 0
        fi

        cp "$main_backup" /etc/ssh/sshd_config
        if [ "$dropin_existed" -eq 1 ]; then
            cp "$dropin_backup" "$dropin"
        else
            rm -f "$dropin"
        fi
        systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
        return 1
    fi
}
run_silent "Hardening SSH configuration" harden_ssh

# OS update policy. Package lists and downloads may refresh automatically, but
# installation is owned by Rubuz so an update can be logged, health-checked and
# eventually gated by the canary approval service. A second unattended writer
# running apt behind the panel's back would bypass all of those guarantees.
configure_upgrades() {
    if command -v apt-get > /dev/null 2>&1; then
        apt_update_once && apt_get_safe install -y unattended-upgrades apt-listchanges
        mkdir -p /etc/apt/apt.conf.d

        # ${distro_id} and ${distro_codename} below are literal: unattended-
        # upgrades resolves them itself against the running system, they are
        # not shell variables (the heredoc delimiter is quoted).
        {
            cat <<'APT'
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
APT
        } > /etc/apt/apt.conf.d/50rubuz-unattended
        cat > /etc/apt/apt.conf.d/20rubuz-periodic <<'APT'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "0";
APT
    fi
}
if command -v apt-get > /dev/null 2>&1; then
    run_silent "Configuring controlled OS update checks" configure_upgrades
fi
echo ""

# ----------------------------------------------------
# Phase 3: Downloading & Installing Rubuz
# ----------------------------------------------------
echo -e "${BOLD}🚀 Phase 3: Deploying ${EDITION_DISPLAY_NAME} Engine${NC}"

# EDITION and EDITION_DOMAIN were already resolved at the top of the script
# (see the comment there) — both R2 folders live behind their own custom
# domain on the same bucket.
VERSION_URL="${RUBUZ_VERSION_URL:-https://${EDITION_DOMAIN}/${EDITION}/latest/version}"
DOWNLOAD_ROOT="${RUBUZ_DOWNLOAD_ROOT:-https://${EDITION_DOMAIN}/${EDITION}/latest}"
VERSION="${RUBUZ_VERSION:-}"
if [ -z "$VERSION" ]; then
    if ! VERSION=$(fetch_with_retry "$VERSION_URL"); then
        echo -e "${RED}[!] Could not determine the ${EDITION_DISPLAY_NAME} release version.${NC}"
        exit 1
    fi
fi
VERSION=$(printf '%s' "$VERSION" | tr -d '\r\n')
if [ -z "$VERSION" ]; then
    echo -e "${RED}[!] The ${EDITION_DISPLAY_NAME} release version is empty.${NC}"
    exit 1
fi
if [[ ! "$VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$ ]]; then
    echo -e "${RED}[!] The ${EDITION_DISPLAY_NAME} release version has an invalid format.${NC}"
    exit 1
fi
echo -e "   ${DIM}•${NC} ${EDITION_DISPLAY_NAME} Version: $VERSION"

# Detect Architecture
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
    ARCH="amd64"
elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
    ARCH="arm64"
else
    echo -e "${RED}[!] Unsupported architecture: $ARCH${NC}"
    exit 1
fi

mkdir -p /opt/rubuz/bin

# The remote filename is the edition, matching how the release workflow
# publishes it — but the local temp file, the installed path, and the
# service name all stay "rubuz": that convention is brand-agnostic
# infrastructure, unrelated to which edition's bytes end up there (see
# deploy.sh's equivalent choice for the same reasoning).
DOWNLOAD_URL="${DOWNLOAD_ROOT}/linux-${ARCH}/${EDITION}"
TMP_BIN="$WORK_DIR/rubuz"
TMP_CHECKSUM="$WORK_DIR/rubuz.sha256"
BIN_DEST="/opt/rubuz/bin/rubuz"
SERVICE_NAME="rubuz"

download_binary() {
    local max_retries=5
    local retry_count=0
    rm -f "$TMP_BIN"
    while [ "$retry_count" -lt "$max_retries" ]; do
        # Start each retry from a clean file. A server or CDN that ignores
        # range requests must never leave a mixed binary behind.
        rm -f "$TMP_BIN"
        if curl -fsSL --connect-timeout 10 --max-time 300 --speed-time 30 --speed-limit 1024 "$DOWNLOAD_URL" -o "$TMP_BIN"; then
            if [ -s "$TMP_BIN" ]; then
                return 0
            fi
        fi
        retry_count=$((retry_count + 1))
        sleep 2
    done
    return 1
}
run_silent "Downloading ${EDITION_DISPLAY_NAME} Binary" download_binary

verify_checksum() {
    local checksum_url="${DOWNLOAD_URL}.sha256"
    local expected_hash
    local actual_hash

    # Verification is fail-closed. Installing an unverified root service when
    # the checksum endpoint is unavailable is worse than leaving the current
    # version running and asking the operator to retry.
    if ! fetch_with_retry "$checksum_url" "$TMP_CHECKSUM"; then
        echo "Checksum download failed: $checksum_url" >> "$LOG_FILE"
        return 1
    fi

    expected_hash=$(awk 'NR == 1 { print tolower($1) }' "$TMP_CHECKSUM")
    if [[ ! "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then
        echo "Checksum file is empty or malformed." >> "$LOG_FILE"
        return 1
    fi

    actual_hash=$(sha256sum "$TMP_BIN" | awk '{print tolower($1)}')
    if [ "$expected_hash" != "$actual_hash" ]; then
        echo "Checksum mismatch: expected $expected_hash, got $actual_hash" >> "$LOG_FILE"
        return 1
    fi
}
run_silent "Verifying file checksum integrity" verify_checksum

# Ed25519 public key for release signatures. Pinned here on purpose.
#
# The checksum above only proves the download was not corrupted in transit: it
# comes from the same bucket as the binary, so anyone able to replace one can
# replace the other. This key does not — it ships inside this script, served
# from a different origin — so forging a release needs both to be compromised,
# not either.
#
# The panel's own updater already verifies this signature before swapping its
# binary (internal/service/panel_updates.go). Until now the very first install
# — the one that runs as root on a machine with nothing on it yet — was the
# only step that did not, even though the signature was already published
# alongside every release.
#
# Rotating the key means updating this literal and the repository variable
# RUBUZ_UPDATE_SIGNING_PUBLIC_KEY together.
RUBUZ_SIGNING_PUBLIC_KEY="${RUBUZ_SIGNING_PUBLIC_KEY:-tKWo4328sE1dLTG0/jfEzy+ZP9yDO2LJTMq7iOOqD6E=}"

verify_signature() {
    local signature_url="${DOWNLOAD_URL}.sha256.sig"
    local sig_b64 expected_hash
    local pem="$WORK_DIR/rubuz-signing-key.pem"
    local sig_bin="$WORK_DIR/rubuz.sig.bin"
    local payload="$WORK_DIR/rubuz.payload.bin"

    if ! command -v openssl >/dev/null 2>&1; then
        if apt_update_once; then
            apt_get_safe install -y openssl >>"$LOG_FILE" 2>&1 || true
        fi
    fi
    if ! command -v openssl >/dev/null 2>&1; then
        echo "openssl is required to verify the release signature but could not be installed." >> "$LOG_FILE"
        return 1
    fi

    if ! fetch_with_retry "$signature_url" "$WORK_DIR/rubuz.sig"; then
        echo "Signature download failed: $signature_url" >> "$LOG_FILE"
        return 1
    fi

    sig_b64=$(tr -d ' \t\r\n' < "$WORK_DIR/rubuz.sig")
    if [ -z "$sig_b64" ]; then
        echo "Signature file is empty." >> "$LOG_FILE"
        return 1
    fi
    if ! printf '%s' "$sig_b64" | base64 -d > "$sig_bin" 2>>"$LOG_FILE"; then
        echo "Signature is not valid base64." >> "$LOG_FILE"
        return 1
    fi
    # Ed25519 signatures are always 64 bytes; anything else is not one.
    if [ "$(wc -c < "$sig_bin")" -ne 64 ]; then
        echo "Signature is not 64 bytes." >> "$LOG_FILE"
        return 1
    fi

    # A raw 32-byte Ed25519 key encodes to exactly 43 base64 characters plus
    # one '=' of padding. Checking the shape here means the PEM built below
    # cannot be silently malformed.
    if [[ ! "$RUBUZ_SIGNING_PUBLIC_KEY" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then
        echo "Signing public key is not a base64-encoded 32-byte Ed25519 key." >> "$LOG_FILE"
        return 1
    fi

    # OpenSSL wants a SubjectPublicKeyInfo, not the bare key. Its DER header
    # for Ed25519 is a fixed 12 bytes — and 12 bytes is exactly four base64
    # groups, so the header's own encoding ("MCowBQYDK2VwAyEA") can simply be
    # prefixed to the key's. That keeps this to string handling: assembling
    # the DER instead would need a hex-to-binary tool, and xxd is not present
    # on a minimal Debian install.
    {
        echo "-----BEGIN PUBLIC KEY-----"
        echo "MCowBQYDK2VwAyEA${RUBUZ_SIGNING_PUBLIC_KEY}"
        echo "-----END PUBLIC KEY-----"
    } > "$pem"

    # Exactly the bytes the panel signs and verifies — see
    # panelReleaseSignaturePayload in internal/service/panel_updates.go. The
    # version and architecture are covered as well as the checksum, so a
    # genuine signature cannot be lifted from one release onto another.
    expected_hash=$(awk 'NR == 1 { print tolower($1) }' "$TMP_CHECKSUM")
    printf 'rubuz-update-v1\n%s\n%s\n%s\n' "$VERSION" "$ARCH" "$expected_hash" > "$payload"

    if ! openssl pkeyutl -verify -pubin -inkey "$pem" -rawin -in "$payload" -sigfile "$sig_bin" >>"$LOG_FILE" 2>&1; then
        echo "Release signature is INVALID for $VERSION/$ARCH ($expected_hash)." >> "$LOG_FILE"
        return 1
    fi
}
run_silent "Verifying release signature" verify_signature

wait_for_rubuz_health() {
    local expected_version="${1:-}"
    local max_wait="${2:-90}"
    local waited=0
    local response=""
    local compact=""

    while [ "$waited" -lt "$max_wait" ]; do
        if systemctl is-active --quiet "$SERVICE_NAME"; then
            # -k: the panel serves HTTPS with a certificate it signs itself
            # at first boot, and this is loopback on the same machine.
            response=$(curl -fsSk --max-time 2 "https://127.0.0.1:${RUBUZ_PORT}/healthz" 2>/dev/null || true)
            compact=$(printf '%s' "$response" | tr -d '[:space:]')
            if [[ "$compact" == *'"status":"ok"'* ]]; then
                if [ -z "$expected_version" ] || [[ "$compact" == *"\"version\":\"${expected_version}\""* ]]; then
                    return 0
                fi
            fi
        fi
        sleep 2
        waited=$((waited + 2))
    done

    echo "${EDITION_DISPLAY_NAME} did not become healthy within ${max_wait}s. Last response: ${response:-none}" >> "$LOG_FILE"
    return 1
}

install_binary() {
    mv "$TMP_BIN" "$BIN_DEST"
    chmod +x "$BIN_DEST"
}
run_silent "Installing verified binaries" install_binary

write_systemd_unit() {
    # Podman has no long-running daemon of its own to depend on — what has to
    # be up first is the socket Traefik mounts, which is socket-activated
    # rather than a plain service.
    #
    # KillMode=process is not optional here. Podman is daemonless, so a
    # container's conmon supervisor is a child of whoever ran `podman run` —
    # this service. Under systemd's default KillMode=control-group, stopping
    # or restarting the panel SIGKILLs every process in its cgroup, which
    # means every conmon, which means every container on the box: Traefik and
    # all of a user's apps died on each panel update, with an unretrievable
    # exit code (-1) and empty logs, because they were killed rather than
    # stopped. Docker never had this failure mode — containers lived under
    # dockerd's cgroup, not the panel's.
    cat <<EOF > /etc/systemd/system/${SERVICE_NAME}.service
[Unit]
Description=${EDITION_DISPLAY_NAME}
Wants=network-online.target
After=network-online.target podman.socket
Requires=podman.socket

[Service]
Type=simple
KillMode=process
Environment="PORT=${RUBUZ_PORT}"
Environment="DATABASE_URL=/opt/rubuz/rubuz.db"
ExecStart=${BIN_DEST}
Restart=always
RestartSec=5
WorkingDirectory=/opt/rubuz

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
}

setup_systemd() {
    write_systemd_unit
    systemctl enable --now $SERVICE_NAME
    wait_for_rubuz_health "$VERSION" "$RUBUZ_HEALTH_TIMEOUT"
}
run_silent "Creating and starting systemd service" setup_systemd
echo ""

# ----------------------------------------------------
# Phase 4: Success Message
# ----------------------------------------------------
echo -e "${BLUE}${BOLD}====================================================${NC}"
echo -e "${GREEN}${BOLD}✔ ${EDITION_DISPLAY_NAME} Installed Successfully!${NC}"
echo -e "   ${EDITION_DISPLAY_NAME} is running in the background via systemd."
echo -e "   Check status: ${BOLD}systemctl status rubuz${NC}"
echo -e "   Bootstrapping Traefik and networks silently...\n"

PUBLIC_IP=$(curl -s --max-time 2 https://ipinfo.io/ip || echo "")
LOCAL_IP=$(hostname -I | awk '{print $1}')

# The panel writes this on a boot that finds no accounts, before it starts
# listening — so the health check above having passed means it is there.
# It is what stops whoever scans port 3000 first from claiming this server:
# the setup wizard cannot authenticate its caller, so proving you can read
# a root-only file on the box is what stands in for that.
SETUP_PATH="/setup"
SETUP_TOKEN=""
if [ -r /opt/rubuz/data/setup-token ]; then
    SETUP_TOKEN=$(tr -d '\r\n' < /opt/rubuz/data/setup-token)
fi
if [ -n "$SETUP_TOKEN" ]; then
    SETUP_PATH="/setup?token=${SETUP_TOKEN}"
fi

echo -e "${BLUE}${BOLD}════════════════════════════════════════════════════${NC}"
echo -e "  ${BOLD}✨ Access ${EDITION_DISPLAY_NAME}${NC}"
echo -e "${BLUE}${BOLD}════════════════════════════════════════════════════${NC}\n"
if [ -n "$PUBLIC_IP" ]; then
    echo -e "   ${BLUE}${BOLD}https://${PUBLIC_IP}:3000${SETUP_PATH}${NC}"
    echo -e "     ${DIM}➔ Public IP — for Cloud, VPS, and Bare Metal servers${NC}\n"
fi
if [ "$LOCAL_IP" != "$PUBLIC_IP" ] && [ -n "$LOCAL_IP" ]; then
    echo -e "   ${BLUE}${BOLD}https://${LOCAL_IP}:3000${SETUP_PATH}${NC}"
    echo -e "     ${DIM}➔ Local Network — for Local VMs, WSL, and LAN setups${NC}\n"
fi

if [ -n "$SETUP_TOKEN" ]; then
    echo -e "   ${RED}${BOLD}⚠ Save this link now${NC}${RED} — closing this window loses it.${NC}\n"
else
    echo -e "   ${RED}[!] Could not read the setup token.${NC}"
    echo -e "   ${DIM}Something went wrong — check: ${NC}${BOLD}systemctl status rubuz${NC}\n"
fi

echo -e "${BLUE}${BOLD}────────────────────────────────────────────────────${NC}"
echo -e "   ${BOLD}🔒 Your browser will warn about the certificate.${NC}"
echo -e "   ${DIM}That's expected — no certificate authority issues a cert for a"
echo -e "   bare IP. Continue past the warning; it goes away on its own once"
echo -e "   you set a domain, when ${EDITION_DISPLAY_NAME} gets a real Let's Encrypt certificate.${NC}"
echo -e "${BLUE}${BOLD}════════════════════════════════════════════════════${NC}"

}

__rubuz_install_main "$@"