#!/bin/bash
# verify-requirements.sh — Pre-flight check for WEDA Node Edge deployment.
# Verifies all system requirements before installing.
#
# Usage:
#   sudo ./verify-requirements.sh
#
# Exit codes:
#   0 — all checks passed (warnings allowed)
#   1 — one or more checks failed

set -uo pipefail

# ─── Configuration ────────────────────────────────────────────────────────────

DOCKER_MIN_VERSION="27.5"
COMPOSE_MIN_VERSION="2.32.4"
MIN_FREE_RAM_MB=512
MIN_FREE_DISK_GB=1
REQUIRED_SUSI_MAJOR=4
REQUIRED_SUSI_MINOR=0

REPORT_FILE="weda-requirements-$(date '+%Y%m%d-%H%M%S').txt"

# ─── Output helpers ───────────────────────────────────────────────────────────

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'

PASS=0; FAIL=0; WARN=0
CURRENT_SECTION=""
REPORT_LINES=()   # plain-text lines for the report file

section() {
    CURRENT_SECTION="$*"
    echo -e "\n${BOLD}── $* ──────────────────────────────────────────${RESET}"
    REPORT_LINES+=("" "### $*")
}

subsection() {
    echo -e "\n  ${BOLD}$*${RESET}"
    REPORT_LINES+=("" "  [$*]")
}

# check_mount_group <title> <required|optional> <path> [<path> ...]
# Prints a subsection header then checks each path, failing or warning based on mode.
check_mount_group() {
    local title="$1" mode="$2"; shift 2
    [[ $# -eq 0 ]] && return
    subsection "$title"
    for p in "$@"; do
        if [[ -e "$p" ]]; then
            pass "$p"
        elif [[ "$mode" == "required" ]]; then
            fail "$p — not found"
        else
            warn "$p — not found (feature disabled if absent)"
        fi
    done
}

pass() {
    echo -e "  ${GREEN}[PASS]${RESET} $*"
    REPORT_LINES+=("  [PASS] $*")
    (( PASS++ ))
}

fail() {
    echo -e "  ${RED}[FAIL]${RESET} $*"
    REPORT_LINES+=("  [FAIL] $*")
    (( FAIL++ ))
}

warn() {
    echo -e "  ${YELLOW}[WARN]${RESET} $*"
    REPORT_LINES+=("  [WARN] $*")
    (( WARN++ ))
}

info() {
    echo -e "  ${CYAN}[INFO]${RESET} $*"
    REPORT_LINES+=("  [INFO] $*")
}

# ─── Hardware model detection ─────────────────────────────────────────────────
# Reads the board/hw-model string in priority order, mirroring Go ReadDeviceModel:
#   1. /proc/board                        (kernel-provided, most authoritative)
#   2. /etc/board                         (user-configured persistent value)
#   3. /proc/device-tree/board/board-type (device-tree firmware value)
# Echoes "<source>:<model>" on success, or "" when none found.
read_board_model() {
    for src in /proc/board /etc/board /proc/device-tree/board/board-type; do
        if [[ -f "$src" ]]; then
            # Strip NUL bytes (device-tree files are NUL-terminated) and all
            # whitespace anywhere (spaces, tabs, line endings). Example:
            # "ADV ECU-579\n\x00" -> "ADVECU-579".
            local val
            val=$(tr -d '\0\t\n\r\v\f ' < "$src")
            if [[ -n "$val" && "${#val}" -le 100 ]]; then
                echo "${src}:${val}"
                return 0
            fi
        fi
    done
    return 1
}

# ─── Version comparison ───────────────────────────────────────────────────────
# Returns 0 if $1 >= $2 (both are semver strings like "27.5.0")

version_gte() {
    local actual="$1" required="$2"
    # Strip Debian/Ubuntu suffixes (e.g. "2.40.3+ds1-0ubuntu1" → "2.40.3")
    actual="${actual%%[+-]*}"
    required="${required%%[+-]*}"
    # Split on '.' and take only the first three numeric fields
    local a1 a2 a3 r1 r2 r3 _rest
    IFS='.' read -r a1 a2 a3 _rest <<< "$actual"
    IFS='.' read -r r1 r2 r3 _rest <<< "$required"
    # Default missing components to 0
    a1=$(( 10#${a1:-0} )); a2=$(( 10#${a2:-0} )); a3=$(( 10#${a3:-0} ))
    r1=$(( 10#${r1:-0} )); r2=$(( 10#${r2:-0} )); r3=$(( 10#${r3:-0} ))
    if (( a1 > r1 )); then return 0; fi
    if (( a1 < r1 )); then return 1; fi
    if (( a2 > r2 )); then return 0; fi
    if (( a2 < r2 )); then return 1; fi
    if (( a3 >= r3 )); then return 0; fi
    return 1
}

# ─── 1. Operating System ──────────────────────────────────────────────────────

section "Operating System"

OS_NAME="$(uname -s)"
if [[ "$OS_NAME" != "Linux" ]]; then
    fail "Not Linux (detected: $OS_NAME)"
else
    DISTRO="unknown"
    if [[ -f /etc/os-release ]]; then
        DISTRO="$(. /etc/os-release && echo "${PRETTY_NAME:-$NAME}")"
    elif [[ -f /etc/lsb-release ]]; then
        DISTRO="$(. /etc/lsb-release && echo "${DISTRIB_DESCRIPTION:-$DISTRIB_ID}")"
    fi

    # Jetson / L4T
    if [[ -f /etc/nv_tegra_release ]] || grep -qi "tegra\|jetson\|l4t" /etc/os-release 2>/dev/null; then
        pass "Linux — Jetson/L4T detected ($DISTRO)"
    # Yocto
    elif grep -qi "yocto\|poky" /etc/os-release 2>/dev/null || [[ -f /etc/build ]]; then
        pass "Linux — Yocto detected ($DISTRO)"
    # Ubuntu
    elif grep -qi "ubuntu" /etc/os-release 2>/dev/null; then
        pass "Linux — Ubuntu detected ($DISTRO)"
    else
        warn "Linux detected ($DISTRO) — not Ubuntu, Jetson/L4T, or Yocto; may work but untested"
    fi
fi

KERNEL="$(uname -r)"
info "Kernel: $KERNEL  Arch: $(uname -m)"

# ─── 2. User Permissions ──────────────────────────────────────────────────────

section "User Permissions"

if [[ "$(id -u)" -eq 0 ]]; then
    pass "Running as root"
else
    if sudo -n true 2>/dev/null; then
        pass "sudo access available (non-root user: $(id -un))"
    else
        fail "Not root and sudo access unavailable — root or sudo is required"
    fi
fi

# ─── 3. Docker Engine ─────────────────────────────────────────────────────────

section "Docker Engine"

if ! command -v docker &>/dev/null; then
    fail "docker not found in PATH"
else
    DOCKER_VERSION_RAW="$(docker version --format '{{.Server.Version}}' 2>/dev/null || true)"
    if [[ -z "$DOCKER_VERSION_RAW" ]]; then
        fail "Docker found but cannot reach daemon (is it running?)"
    else
        if version_gte "$DOCKER_VERSION_RAW" "$DOCKER_MIN_VERSION"; then
            pass "Docker Engine v${DOCKER_VERSION_RAW} (>= ${DOCKER_MIN_VERSION})"
        else
            fail "Docker Engine v${DOCKER_VERSION_RAW} is below minimum v${DOCKER_MIN_VERSION}"
        fi

        if docker info --format '{{.ServerVersion}}' &>/dev/null; then
            pass "Docker daemon is running"
        else
            fail "Docker daemon is not running"
        fi
    fi
fi

# ─── 4. Docker Compose ────────────────────────────────────────────────────────

section "Docker Compose"

COMPOSE_CMD=""
COMPOSE_VERSION_RAW=""

# Prefer the plugin form
if docker compose version &>/dev/null 2>&1; then
    COMPOSE_CMD="docker compose"
    COMPOSE_VERSION_RAW="$(docker compose version --short 2>/dev/null | tr -d 'v')"
elif command -v docker-compose &>/dev/null; then
    COMPOSE_CMD="docker-compose"
    COMPOSE_VERSION_RAW="$(docker-compose version --short 2>/dev/null | tr -d 'v')"
fi

if [[ -z "$COMPOSE_CMD" ]]; then
    fail "Neither 'docker compose' plugin nor 'docker-compose' standalone found"
else
    info "Compose command: $COMPOSE_CMD"
    if version_gte "$COMPOSE_VERSION_RAW" "$COMPOSE_MIN_VERSION"; then
        pass "Docker Compose v${COMPOSE_VERSION_RAW} (>= ${COMPOSE_MIN_VERSION})"
    else
        fail "Docker Compose v${COMPOSE_VERSION_RAW} is below minimum v${COMPOSE_MIN_VERSION}"
    fi
fi

# ─── 5. Filesystem Access ─────────────────────────────────────────────────────

section "Filesystem Access"

OPT_ADV="/opt/Advantech"
if mkdir -p "${OPT_ADV}/.weda-verify-test" 2>/dev/null; then
    rmdir "${OPT_ADV}/.weda-verify-test"
    pass "Can create directories in /opt/Advantech"
else
    fail "Cannot write to /opt/Advantech — check permissions (requires root/sudo)"
fi

# ─── 6. Storage ───────────────────────────────────────────────────────────────

section "Storage"

# df -BG rounds up; use -k for KB precision
FREE_KB="$(df -k /opt 2>/dev/null | awk 'NR==2 {print $4}')"
if [[ -z "$FREE_KB" ]]; then
    warn "Could not determine free disk space on /opt"
else
    FREE_GB=$(( FREE_KB / 1024 / 1024 ))
    FREE_MB=$(( FREE_KB / 1024 ))
    if (( FREE_KB >= MIN_FREE_DISK_GB * 1024 * 1024 )); then
        pass "Free disk space on /opt: ${FREE_GB} GB (>= ${MIN_FREE_DISK_GB} GB)"
    else
        fail "Free disk space on /opt: ${FREE_MB} MB — minimum ${MIN_FREE_DISK_GB} GB required"
    fi
fi

# ─── 7. Memory ────────────────────────────────────────────────────────────────

section "Memory"

FREE_RAM_KB="$(grep MemAvailable /proc/meminfo 2>/dev/null | awk '{print $2}')"
if [[ -z "$FREE_RAM_KB" ]]; then
    warn "Cannot read /proc/meminfo — memory check skipped"
else
    FREE_RAM_MB=$(( FREE_RAM_KB / 1024 ))
    if (( FREE_RAM_MB >= MIN_FREE_RAM_MB )); then
        pass "Free RAM: ${FREE_RAM_MB} MB (>= ${MIN_FREE_RAM_MB} MB)"
    else
        fail "Free RAM: ${FREE_RAM_MB} MB — minimum ${MIN_FREE_RAM_MB} MB required"
    fi
fi

# ─── 8. Network Interface ─────────────────────────────────────────────────────

section "Network Interface"

PHYSICAL_IFACES=()

# /sys/class/net excludes loopback; filter out well-known virtual types
for iface_path in /sys/class/net/*/; do
    iface="$(basename "$iface_path")"

    # Skip loopback and known virtual prefixes
    [[ "$iface" == "lo" ]] && continue
    [[ "$iface" =~ ^(docker|veth|br-|virbr|tun|tap|dummy|bond|team|wlan|wlp|wlx) ]] && continue

    # Must be Ethernet (type 1)
    nic_type="$(cat "${iface_path}/type" 2>/dev/null || true)"
    [[ "$nic_type" != "1" ]] && continue

    # Physical NICs have a 'device' symlink pointing to a bus (PCI/USB/platform).
    # Virtual interfaces (bridges, macvlan slaves, etc.) lack this link.
    [[ -L "${iface_path}/device" ]] || continue

    PHYSICAL_IFACES+=("$iface")
done

if [[ ${#PHYSICAL_IFACES[@]} -gt 0 ]]; then
    pass "Physical Ethernet interface(s) found: ${PHYSICAL_IFACES[*]}"
else
    fail "No physical Ethernet interface detected — required for Device ID generation"
fi

# ─── 9. Advantech SUSI Library ────────────────────────────────────────────────

section "Advantech SUSI Library"

# Detect any libSUSI-<major>.<minor>.so on common library search paths
SUSI_FOUND=false
SUSI_VERSION=""
LIB_SEARCH_PATHS=(/lib /lib64 /usr/lib /usr/lib64 /usr/local/lib /usr/lib/Advantech)

for lib_dir in "${LIB_SEARCH_PATHS[@]}"; do
    [[ -d "$lib_dir" ]] || continue
    # Match filenames like libSUSI-4.02.so or libSUSI-4.00.so
    while IFS= read -r -d '' lib_file; do
        fname="$(basename "$lib_file")"
        # Extract major.minor from libSUSI-M.mm.so*
        if [[ "$fname" =~ libSUSI-([0-9]+)\.([0-9]+)\. ]]; then
            major="${BASH_REMATCH[1]}"
            minor="${BASH_REMATCH[2]}"
            # Prefer the highest version found
            if [[ -z "$SUSI_VERSION" ]]; then
                SUSI_VERSION="${major}.${minor}"
                SUSI_FOUND=true
            elif (( major > REQUIRED_SUSI_MAJOR )) || \
                 (( major == REQUIRED_SUSI_MAJOR && minor > ${SUSI_VERSION##*.} )); then
                SUSI_VERSION="${major}.${minor}"
            fi
        fi
    done < <(find "$lib_dir" -maxdepth 2 -name "libSUSI-*.so*" -print0 2>/dev/null)
done

if ! $SUSI_FOUND; then
    warn "Advantech SUSI library not found — required for hardware I/O features (SUSI >= ${REQUIRED_SUSI_MAJOR}.${REQUIRED_SUSI_MINOR})"
else
    SUSI_MAJOR="${SUSI_VERSION%%.*}"
    SUSI_MINOR="${SUSI_VERSION##*.}"
    if (( SUSI_MAJOR > REQUIRED_SUSI_MAJOR )) || \
       (( SUSI_MAJOR == REQUIRED_SUSI_MAJOR && SUSI_MINOR >= REQUIRED_SUSI_MINOR )); then
        pass "SUSI library v${SUSI_VERSION} found (>= ${REQUIRED_SUSI_MAJOR}.${REQUIRED_SUSI_MINOR})"
    else
        fail "SUSI library v${SUSI_VERSION} is below minimum v${REQUIRED_SUSI_MAJOR}.${REQUIRED_SUSI_MINOR}"
    fi

    # Board info is required when SUSI is present
    board_result=""
    if board_result=$(read_board_model); then
        board_src="${board_result%%:*}"
        board_val="${board_result#*:}"
        pass "Advantech board info found at ${board_src} (model: ${board_val})"
    else
        fail "Advantech board info not found — checked /proc/board, /etc/board, /proc/device-tree/board/board-type (required when SUSI is present)"
    fi
fi

# ─── 10. WEDA Node Resources ──────────────────────────────────────────────────
# Paths are derived from internal/templates/docker-compose.template.yml.
# Installer-managed paths (/opt/Advantech/*) and always-present paths (/, /dev)
# are intentionally excluded — they are not expected to pre-exist.

section "WEDA Node Resources"

# ── Docker Infrastructure ─────────────────────────────────────────────────────
# Required by all services. FAIL if missing.
check_mount_group "Docker Infrastructure" required \
    /var/run/docker.sock \
    /var/lib/docker/volumes

# ── Board Info ────────────────────────────────────────────────────────────────
# Used by dmagent and system-agent. Checked in priority order:
#   1. /proc/board  2. /etc/board  3. /proc/device-tree/board/board-type
subsection "Board Info"
board_result=""
if board_result=$(read_board_model); then
    board_src="${board_result%%:*}"
    board_val="${board_result#*:}"
    pass "${board_src} (model: ${board_val})"
else
    fail "Board info not found — checked /proc/board, /etc/board, /proc/device-tree/board/board-type"
fi

# ── EAPI Dependencies ───────────────────────────────────────────────
# Advantech EAPI hardware abstraction libs. WARN if missing.
check_mount_group "EAPI Dependencies" optional \
    /usr/src/advantech \
    /lib/libEAPI.so \
    /lib/libEAPI-IPS.so \
    /lib/libATAuxIO.so \
    /lib/libATGPIO.so \
    /lib/libATSMBUS.so \
    /lib/libBoardResource.so \
    /lib/libECGPIO.so

# ── SUSI Dependencies ─────────────────────────────────────────────────
# Advantech SUSI driver libs and jansson dependency. WARN if missing.
check_mount_group "SUSI Dependencies" optional \
    /usr/lib/Advantech/ \
    /lib/libSUSI-4.00.so \
    /lib/libSUSI-4.00.so.1 \
    /lib/libSUSI-4.00.so.1.0.0 \
    /lib/libSusiIoT.so \
    /lib/libSusiIoT.so.1.0.0 \
    /lib/libjansson.a \
    /lib/libjansson.so \
    /lib/libjansson.so.4 \
    /lib/libjansson.so.4.11.0

# ─── Summary ──────────────────────────────────────────────────────────────────

echo -e "\n${BOLD}══════════════════════════════════════════════════════${RESET}"
echo -e "${BOLD}  Verification Summary${RESET}"
echo -e "${BOLD}══════════════════════════════════════════════════════${RESET}"
echo -e "  ${GREEN}PASS${RESET}  $PASS    ${YELLOW}WARN${RESET}  $WARN    ${RED}FAIL${RESET}  $FAIL"
echo -e "${BOLD}══════════════════════════════════════════════════════${RESET}"

if (( FAIL > 0 )); then
    RESULT_LINE="Result: FAILED — $FAIL requirement(s) not met. Resolve above issues before deploying."
    echo -e "\n  ${RED}${RESULT_LINE}${RESET}\n"
elif (( WARN > 0 )); then
    RESULT_LINE="Result: PASSED with warnings — review warnings before deploying."
    echo -e "\n  ${YELLOW}${RESULT_LINE}${RESET}\n"
else
    RESULT_LINE="Result: ALL CHECKS PASSED — system is ready for WEDA Node deployment."
    echo -e "\n  ${GREEN}${RESULT_LINE}${RESET}\n"
fi

# ─── Write report ─────────────────────────────────────────────────────────────

{
    echo "WEDA Node Edge — Requirements Verification Report"
    echo "=================================================="
    echo "Date      : $(date '+%Y-%m-%d %H:%M:%S %Z')"
    echo "Host      : $(hostname)"
    echo "OS        : $(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-unknown}" || echo unknown)"
    echo "Kernel    : $(uname -r)"
    echo "Arch      : $(uname -m)"
    echo ""
    for line in "${REPORT_LINES[@]}"; do
        echo "$line"
    done
    echo ""
    echo "=================================================="
    echo "  PASS: $PASS   WARN: $WARN   FAIL: $FAIL"
    echo "  $RESULT_LINE"
    echo "=================================================="
} > "$REPORT_FILE"

echo -e "  Report saved to: ${BOLD}${REPORT_FILE}${RESET}\n"

(( FAIL > 0 )) && exit 1 || exit 0
