#!/usr/bin/env bash
# ePulz.io LAN Agent installer - cross-distro (apt/dnf/apk/pacman), cross-arch (x86_64, ARM).
#
# Usage:
#   curl -fsSL https://epulz.io/install-agent.sh | sudo bash -s epulzio_YOUR_TOKEN
#
# Or step-by-step (recommended for first-time users):
#   curl -fsSL https://epulz.io/install-agent.sh -o install-agent.sh
#   less install-agent.sh                     # read what it does (1 min)
#   sudo bash install-agent.sh epulzio_YOUR_TOKEN
#
# Supported:
#   - Debian / Ubuntu / Raspberry Pi OS (apt)
#   - Fedora / RHEL / AlmaLinux / Rocky (dnf or yum)
#   - Alpine Linux (apk)
#   - Arch / Manjaro (pacman)
#   - openSUSE (zypper)
#   - x86_64, aarch64 (ARM64), armv7l (RPi 3+), armv6l (RPi Zero)
#
# Tested on:
#   - Raspberry Pi OS 12 (Bookworm) ARM64
#   - Ubuntu 22.04 / 24.04 x86_64
#   - Debian 12 x86_64 and ARM
#   - Alpine 3.19 x86_64

set -euo pipefail

# ─────────── Configuration ───────────
TOKEN="${1:-${EPULZIO_AGENT_TOKEN:-}}"
API="${EPULZIO_API:-https://epulz.io}"
INSTALL_DIR="/opt/epulzio-agent"
SERVICE_FILE="/etc/systemd/system/epulzio-agent.service"

# ─────────── Pretty output ───────────
RED=$'\033[0;31m'
YELLOW=$'\033[0;33m'
GREEN=$'\033[0;32m'
BLUE=$'\033[0;34m'
RESET=$'\033[0m'

step() { echo "${BLUE}==>${RESET} $1"; }
ok()   { echo "    ${GREEN}✓${RESET} $1"; }
warn() { echo "    ${YELLOW}!${RESET} $1" >&2; }
die()  { echo "${RED}ERROR:${RESET} $1" >&2; exit 1; }

# ─────────── Pre-flight checks ───────────

step "ePulz.io LAN Agent installer (v2)"
echo

if [[ -z "$TOKEN" ]]; then
    die "Missing agent token.

Usage:
  curl -fsSL ${API}/install-agent.sh | sudo bash -s epulzio_YOUR_TOKEN

Get your token at: ${API}/dashboard/agents (click 'Create agent', copy the token immediately)"
fi

if [[ ! "$TOKEN" =~ ^epulzio_[A-Za-z0-9_-]+$ ]]; then
    die "Invalid token format. Token must start with 'epulzio_'. Generate a new token at ${API}/dashboard/agents"
fi

if [[ "$EUID" -ne 0 ]]; then
    die "This installer must run as root (use 'sudo bash ...')."
fi

step "Detecting system"

# Architecture
ARCH="$(uname -m)"
case "$ARCH" in
    x86_64|amd64)    ARCH_LABEL="x86_64 (Intel/AMD 64-bit)" ;;
    aarch64|arm64)   ARCH_LABEL="aarch64 (ARM 64-bit, e.g. Raspberry Pi 4/5)" ;;
    armv7l)          ARCH_LABEL="armv7l (ARM 32-bit, e.g. Raspberry Pi 3)" ;;
    armv6l)          ARCH_LABEL="armv6l (ARM 32-bit, e.g. Raspberry Pi Zero)" ;;
    *)               ARCH_LABEL="$ARCH (untested - agent is pure Python so it should work)" ;;
esac
ok "Architecture: $ARCH_LABEL"

# Distro
if [[ -f /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    DISTRO_LABEL="${PRETTY_NAME:-${NAME:-unknown}}"
else
    DISTRO_LABEL="unknown (no /etc/os-release)"
fi
ok "Distro: $DISTRO_LABEL"

# Package manager
PKG_MGR=""
PKG_INSTALL=""
for cand in apt-get dnf yum apk pacman zypper; do
    if command -v "$cand" >/dev/null 2>&1; then
        PKG_MGR="$cand"
        break
    fi
done

case "$PKG_MGR" in
    apt-get) PKG_INSTALL="apt-get install -y" ;;
    dnf)     PKG_INSTALL="dnf install -y" ;;
    yum)     PKG_INSTALL="yum install -y" ;;
    apk)     PKG_INSTALL="apk add --no-cache" ;;
    pacman)
        # In LXC/containers, pacman v7+ sandbox (Landlock) fails. Auto-detect and add --disable-sandbox.
        PKG_INSTALL="pacman -S --noconfirm --needed"
        # Test if we're in container (LXC)
        if [[ -f /run/.containerenv ]] || grep -q lxc /proc/1/cgroup 2>/dev/null || [[ -f /run/systemd/container ]]; then
            PKG_INSTALL="pacman -S --noconfirm --needed --disable-sandbox"
        fi
        ;;
    zypper)  PKG_INSTALL="zypper install -y" ;;
    *)       PKG_INSTALL="" ;;
esac

if [[ -n "$PKG_MGR" ]]; then
    ok "Package manager: $PKG_MGR"
else
    warn "No known package manager (apt/dnf/apk/pacman/zypper). Will skip auto-install of dependencies."
    warn "Make sure Python 3 and 'ping' are installed manually."
fi

# Required commands
need_python=0
need_ping=0
if command -v python3 >/dev/null 2>&1; then
    # Python exists - check if version >= 3.8
    if ! python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,8) else 1)' 2>/dev/null; then
        # Old python (e.g. 3.6 on openSUSE Leap), check for newer python3.X
        FOUND_NEW=0
        for try in python3.13 python3.12 python3.11 python3.10 python3.9 python3.8; do
            if command -v "$try" >/dev/null 2>&1 && \
               "$try" -c 'import sys; sys.exit(0 if sys.version_info >= (3,8) else 1)' 2>/dev/null; then
                FOUND_NEW=1
                break
            fi
        done
        [[ $FOUND_NEW -eq 0 ]] && need_python=1
    fi
else
    need_python=1
fi
command -v ping >/dev/null 2>&1 || need_ping=1

# Disk space pre-check (need ~5MB free)
# Use parent of INSTALL_DIR which may not exist yet
DISK_CHECK_PATH="/opt"
[[ -d "$DISK_CHECK_PATH" ]] || DISK_CHECK_PATH="/"
# df -kP forces POSIX format (single line per filesystem, BusyBox compatible)
FREE_KB="$(df -kP "$DISK_CHECK_PATH" 2>/dev/null | awk 'NR==2{print $4}')"
FREE_KB="${FREE_KB:-0}"
if [[ "$FREE_KB" -lt 5120 ]] 2>/dev/null; then
    die "Not enough disk space in $DISK_CHECK_PATH (need 5MB, have ${FREE_KB}KB). Free up space first."
fi
ok "Disk space: $((FREE_KB / 1024)) MB free in $DISK_CHECK_PATH"

# Update package index if needed for apt/dnf/yum (Alpine apk doesn't need it for online install)
if [[ -n "$PKG_INSTALL" && ( $need_python -eq 1 || $need_ping -eq 1 ) ]]; then
    step "Installing missing dependencies"
    case "$PKG_MGR" in
        apt-get) apt-get update -qq ;;
    esac
    pkgs=()
    if [[ $need_python -eq 1 ]]; then
        case "$PKG_MGR" in
            apt-get|dnf|yum) pkgs+=(python3) ;;
            apk|pacman)      pkgs+=(python3) ;;
            zypper)          pkgs+=(python311) ;;  # openSUSE Leap defaults to py 3.6, use 3.11
        esac
    fi
    if [[ $need_ping -eq 1 ]]; then
        case "$PKG_MGR" in
            apt-get) pkgs+=(iputils-ping) ;;
            dnf|yum) pkgs+=(iputils) ;;
            apk)     pkgs+=(iputils-ping) ;;
            pacman)  pkgs+=(iputils) ;;
            zypper)  pkgs+=(iputils) ;;
        esac
    fi
    if [[ ${#pkgs[@]} -gt 0 ]]; then
        echo "    Running: $PKG_INSTALL ${pkgs[*]}"
        # shellcheck disable=SC2086
        $PKG_INSTALL "${pkgs[@]}" || die "Package install failed. Install manually: ${pkgs[*]}"
        ok "Dependencies installed"
    fi
elif [[ $need_python -eq 1 || $need_ping -eq 1 ]]; then
    [[ $need_python -eq 1 ]] && die "Python 3 is missing and we cannot install automatically. Install python3 manually and re-run."
    [[ $need_ping -eq 1 ]]   && warn "'ping' command missing - ICMP monitors will not work."
fi

# Python version - prefer newest available if default is too old
PY_BIN="python3"
PY_VER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "?")"
if ! python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,8) else 1)' 2>/dev/null; then
    # Look for newer python3.X (e.g. on openSUSE python311 -> /usr/bin/python3.11)
    for try in python3.13 python3.12 python3.11 python3.10 python3.9 python3.8; do
        if command -v "$try" >/dev/null 2>&1; then
            if "$try" -c 'import sys; sys.exit(0 if sys.version_info >= (3,8) else 1)' 2>/dev/null; then
                PY_BIN="$(command -v "$try")"
                PY_VER="$($PY_BIN -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
                warn "Default python3 was too old ($PY_VER), using $PY_BIN instead"
                break
            fi
        fi
    done
    if ! "$PY_BIN" -c 'import sys; sys.exit(0 if sys.version_info >= (3,8) else 1)' 2>/dev/null; then
        die "Python 3.8+ required, found $PY_VER. Install python3.8+ (e.g. zypper install python311) and re-run."
    fi
fi
# Normalize to absolute path for systemd ExecStart
PY_BIN="$(command -v "$PY_BIN")"
ok "Python: $PY_VER (binary: $PY_BIN)"

# Init system detection: systemd OR openrc (Alpine)
INIT_SYSTEM=""
if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then
    INIT_SYSTEM="systemd"
    ok "Init system: systemd"
elif command -v rc-update >/dev/null 2>&1; then
    INIT_SYSTEM="openrc"
    ok "Init system: OpenRC (Alpine-style)"
else
    die "Neither systemd nor OpenRC detected.
For other init systems (runit, s6, etc.) or containerized setups, use the Docker variant:
  ${API}/dashboard/help/lan-agent/install-agent#docker"
fi

# Connectivity check
step "Connectivity check"
if ! curl -fsS -o /dev/null --max-time 10 "${API}/health"; then
    warn "Cannot reach ${API}/health - check firewall, DNS, or HTTPS outbound."
    warn "Agent will still install but will not work until network is fixed."
else
    ok "Reach ${API}: OK"
fi

# ─────────── Install ───────────

step "Installing agent to $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"

# Retry logic for download - 3 attempts with exponential backoff (1s, 2s, 4s)
DL_OK=0
for attempt in 1 2 3; do
    if curl -fsSL --max-time 30 "${API}/static/agent/epulzio-agent.py" -o "$INSTALL_DIR/agent.py.new"; then
        DL_OK=1
        break
    fi
    warn "Download attempt $attempt/3 failed, retrying in $((attempt * attempt))s..."
    sleep $((attempt * attempt))
done
if [[ $DL_OK -ne 1 ]]; then
    die "Failed to download agent.py from ${API}/static/agent/epulzio-agent.py after 3 attempts.
Test connectivity manually: curl -v ${API}/static/agent/epulzio-agent.py
Common causes: corporate firewall, DNS issue, SSL inspection without CA bundle."
fi
mv "$INSTALL_DIR/agent.py.new" "$INSTALL_DIR/agent.py"
chmod +x "$INSTALL_DIR/agent.py"
ok "Downloaded $(wc -l < "$INSTALL_DIR/agent.py") LOC agent.py"

cat > "$INSTALL_DIR/agent.env" <<EOF
EPULZIO_API=${API}
EPULZIO_AGENT_TOKEN=${TOKEN}
EPULZIO_POLL_SECONDS=30
EOF
chmod 600 "$INSTALL_DIR/agent.env"
ok "Token saved to $INSTALL_DIR/agent.env (mode 0600)"

if [[ "$INIT_SYSTEM" == "systemd" ]]; then
    step "Creating systemd service"
    # Agent runs as an UNPRIVILEGED dynamic user with full systemd sandboxing.
    # CAP_NET_RAW (ambient) is granted only for ICMP ping checks.
    write_unit_hardened() {
        cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=ePulz.io LAN Agent
Documentation=${API}/dashboard/help/lan-agent/install-agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=${INSTALL_DIR}/agent.env
ExecStart=${PY_BIN} ${INSTALL_DIR}/agent.py
Restart=always
RestartSec=10
DynamicUser=yes
StateDirectory=epulzio-agent
AmbientCapabilities=CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_RAW
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK

[Install]
WantedBy=multi-user.target
EOF
    }
    # Fallback for old systemd / exotic containers where DynamicUser is unavailable
    write_unit_legacy() {
        cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=ePulz.io LAN Agent
Documentation=${API}/dashboard/help/lan-agent/install-agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=${INSTALL_DIR}/agent.env
ExecStart=${PY_BIN} ${INSTALL_DIR}/agent.py
Restart=always
RestartSec=10
User=root
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF
    }
    write_unit_hardened
    ok "Service file: $SERVICE_FILE (unprivileged DynamicUser + sandboxing)"

    systemctl daemon-reload
    systemctl enable epulzio-agent >/dev/null 2>&1
    systemctl restart epulzio-agent
    ok "Service enabled and (re)started"

    sleep 2
    step "Service status"
    if systemctl --no-pager --quiet is-active epulzio-agent; then
        echo
        systemctl --no-pager status epulzio-agent | head -10
        echo
        ok "Agent is running (unprivileged user, sandboxed)"
    else
        warn "Hardened service failed to start - retrying with legacy configuration (root, reduced sandboxing)"
        write_unit_legacy
        systemctl daemon-reload
        systemctl restart epulzio-agent >/dev/null 2>&1 || true
        sleep 2
        if systemctl --no-pager --quiet is-active epulzio-agent; then
            echo
            systemctl --no-pager status epulzio-agent | head -10
            echo
            ok "Agent is running (legacy mode). Consider upgrading systemd for sandboxed mode."
        else
            echo
            systemctl --no-pager status epulzio-agent | head -20
            echo
            die "Agent failed to start. Check logs: journalctl -u epulzio-agent -n 50"
        fi
    fi
    LOG_CMD="journalctl -u epulzio-agent -f"
    STATUS_CMD="systemctl status epulzio-agent"
    RESTART_CMD="systemctl restart epulzio-agent"
    STOP_CMD="systemctl stop epulzio-agent"
    UNINSTALL_CMD="systemctl stop epulzio-agent && systemctl disable epulzio-agent && rm -rf ${INSTALL_DIR} ${SERVICE_FILE} && systemctl daemon-reload"
elif [[ "$INIT_SYSTEM" == "openrc" ]]; then
    step "Creating OpenRC service"
    OPENRC_FILE="/etc/init.d/epulzio-agent"
    cat > "$OPENRC_FILE" <<EOF
#!/sbin/openrc-run
# ePulz.io LAN Agent

name="ePulz.io LAN Agent"
description="ePulz.io LAN monitoring agent"
command="${PY_BIN}"
command_args="${INSTALL_DIR}/agent.py"
command_user="root"
pidfile="/var/run/\${RC_SVCNAME}.pid"
command_background=true
output_log="/var/log/epulzio-agent.log"
error_log="/var/log/epulzio-agent.log"
respawn_max=0  # always respawn
respawn_period=30

depend() {
    need net
    after network
}

start_pre() {
    if [ -f "${INSTALL_DIR}/agent.env" ]; then
        set -a; . "${INSTALL_DIR}/agent.env"; set +a
        export EPULZIO_API EPULZIO_AGENT_TOKEN EPULZIO_POLL_SECONDS
    fi
}
EOF
    chmod +x "$OPENRC_FILE"
    ok "OpenRC init script: $OPENRC_FILE"

    rc-update add epulzio-agent default >/dev/null 2>&1 || true
    rc-service epulzio-agent start
    ok "Service started"

    sleep 2
    step "Service status"
    if rc-service epulzio-agent status | grep -q started; then
        ok "Agent is running"
    else
        rc-service epulzio-agent status
        die "Agent failed to start. Check logs: tail -50 /var/log/epulzio-agent.log"
    fi
    LOG_CMD="tail -f /var/log/epulzio-agent.log"
    STATUS_CMD="rc-service epulzio-agent status"
    RESTART_CMD="rc-service epulzio-agent restart"
    STOP_CMD="rc-service epulzio-agent stop"
    UNINSTALL_CMD="rc-service epulzio-agent stop && rc-update del epulzio-agent && rm -rf ${INSTALL_DIR} ${OPENRC_FILE}"
fi

echo
echo "${GREEN}══════════════════════════════════════════════════${RESET}"
echo "${GREEN}  Installation complete${RESET}"
echo "${GREEN}══════════════════════════════════════════════════${RESET}"
echo
echo "  Live logs:    ${LOG_CMD}"
echo "  Status:       sudo ${STATUS_CMD}"
echo "  Restart:      sudo ${RESTART_CMD}"
echo "  Stop:         sudo ${STOP_CMD}"
echo "  Update:       sudo curl -fsSL ${API}/static/agent/epulzio-agent.py -o ${INSTALL_DIR}/agent.py && sudo ${RESTART_CMD}"
echo "  Uninstall:    sudo sh -c \"${UNINSTALL_CMD}\""
echo "  Health:       curl -s http://127.0.0.1:9090/health | python3 -m json.tool"
echo
echo "  Dashboard:    ${API}/dashboard/agents (your agent should appear as 'online' within 30 seconds)"
echo "  First LAN monitor: ${API}/dashboard/help/lan-agent/first-lan-monitor"
echo
