#!/bin/bash
# ==============================================================================================
# ================================= Unraid Platform Adapter ====================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Platform abstraction layer for Unraid. Sourced by load_config.sh when
# PLATFORM=unraid. Scripts call platform_*() functions instead of OS-specific
# commands — the adapter isolates all OS-dependent logic in one place.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# API CONTRACT
#   Every function returns 0 on success / 1 on failure unless noted.
#   Functions that produce output write to stdout; callers capture with $().
#   No function calls exit — callers decide what failure means for their flow.
#
# ADDING A PLATFORM
#   Create Plugin/truenas/adapter.sh (or ubuntu/adapter.sh) implementing the
#   same function names. load_config.sh sources Plugin/$PLATFORM/adapter.sh —
#   no other changes needed anywhere in the codebase.
#
# FUNCTIONS
#   platform_require_cmd           — verify a platform command exists and is executable
#   platform_storage_healthy       — array mounted and shfs active on /mnt/user
#   platform_disk_states_path      — path to the platform disk state file
#   platform_get_disk_states       — raw disk state content from platform
#   platform_get_temp_thresholds   — disk warn/crit °C from platform config
#   platform_is_maintenance_running — parity check/sync in progress
#   platform_is_service_enabled    — docker or libvirt enabled in boot config
#   platform_restart_service       — restart a named service via rc.d
#   platform_stop_service          — stop a named service via rc.d
#   platform_is_service_running    — check if a named service process is alive
#   platform_is_mover_running      — unRAID mover process check
#   platform_stop_user_scripts     — kill all user.scripts background processes
#   platform_send_os_notification  — native unRAID notify (dynamix)
#   platform_storage_path          — root path for user shares/storage (e.g. /mnt/user)
#   platform_webui_install_path    — where the platform serves the WebGUI plugin files from
#   platform_scripts_dir_probe_cmd — shell command to run on a remote to discover its SCRIPTS_DIR
#   platform_get_templates_dir     — path to Unraid CA docker templates-user directory
#   platform_setup_db_path         — path to the persistent Varaverk setup/wizard state database
#   platform_get_os_version        — local OS version string (e.g. "7.2.3")
#   platform_os_version_probe_cmd  — shell command to run on a remote to retrieve its OS version
#   platform_rebuild_container     — rebuild a container from its stored XML template
#   platform_push_conf             — push master.conf to all listed hosts via WebGUI PHP
#   platform_push_setup_state      — push wizard setup state to WebGUI PHP
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# One Place Knows the OS
#   Every Unraid-specific path, rc.d script and config location lives here. Scripts never
#   branch on OS and never hardcode /etc/rc.d, /boot/config or dynamix paths. That is what
#   makes a second platform a single new file rather than a hundred edits.
#
# Never Exit, Always Return
#   No function here calls exit. A platform capability being absent is information the
#   caller needs, not a decision the adapter gets to make — a watchdog may want to skip a
#   check where an installer wants to abort, and only they know which.
#
# Report, Do Not Remediate
#   The adapter answers questions and performs the single action asked of it. It does not
#   retry, escalate, notify or heal. Every one of those policies belongs to the caller, and
#   burying them here would make identical calls behave differently per platform.
#
# Stdout Is the Return Channel
#   Value-producing functions write to stdout and are captured with $(). Status is carried
#   by the exit code. Keeping those separate is what lets callers use them in conditionals
#   without parsing output.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# No Root, No Lock, No detect_hosts — Deliberate
#   This is a sourced library, loaded by load_config.sh whenever PLATFORM=unraid. It runs
#   inside the caller's process and holds no state of its own. A root check here would fire
#   for every script in the ecosystem including read-only ones, and a lock would be taken
#   on every source. The executable scripts own those gates. Do not "fix" this to match them.
#
# Executable Validation Before Use
#   platform_require_cmd() and the rc.d helpers verify a target exists and is executable
#   before invoking it, so a missing platform binary returns a clean failure rather than a
#   command-not-found in the middle of a caller's flow.
#
# Notification Is Best-Effort
#   platform_send_os_notification() returns 1 if the dynamix notify script is absent rather
#   than failing the caller. A missing notifier must never turn a successful operation into
#   a reported failure.
#
# Graceful Degradation on Unknown Services
#   _platform_rc_script() falls through to /etc/rc.d/rc.<name> for any service it does not
#   explicitly map, so a new service works without an adapter change — and still fails
#   cleanly if that path does not exist.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# None of its own. The adapter is selected by PLATFORM in varaverk.cfg, which load_config.sh
# uses to source Plugin/$PLATFORM/adapter.sh.
#
# It reads platform-owned files rather than Varaverk config:
#
#   /boot/config/plugins/dynamix/dynamix.cfg   disk temperature thresholds
#   /boot/config/plugins/varaverk/varaverk.cfg SCRIPTS_DIR probe
#   /boot/config/plugins/dockerMan/templates-user  container rebuild templates
#   /etc/rc.d/rc.*                             service control
#
# Those paths are Unraid's, not Varaverk's — which is exactly why they are confined to this
# file. STATE_DIR, DATA_DIR and friends belong to master.conf and are not the adapter's
# concern.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# None — sourced, never executed:
#
#   source "$LOAD_CONFIG_DIR/Plugin/$PLATFORM/adapter.sh"
#
# No argument parsing, no --dry-run, no --status. Callers that need a dry run implement it
# around the platform_*() call, since only they know which of their actions are destructive.
#
# ==============================================================================================

# ──────────────────────────────────────────────────────────────────────────────────────────────
# Internal: map a logical service name → its rc.d script path
# ──────────────────────────────────────────────────────────────────────────────────────────────
_platform_rc_script() {
    case "$1" in
        docker)  echo "/etc/rc.d/rc.docker" ;;
        sshd)    echo "/etc/rc.d/rc.sshd" ;;
        libvirt) echo "/etc/rc.d/rc.libvirt" ;;
        rsyslog) echo "/etc/rc.d/rc.rsyslogd" ;;
        nginx)   echo "/etc/rc.d/rc.nginx" ;;
        php-fpm) echo "/etc/rc.d/rc.php-fpm" ;;
        *)       echo "/etc/rc.d/rc.$1" ;;
    esac
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_require_cmd <path> [test_arg] [expected_output] [label]
#   Replaces platform_require_cmd. Returns 0 if the command exists and (optionally) its output
#   matches expected_output. Returns 1 and prints a warning if not.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_require_cmd() {
    local cmd="$1"
    local test_arg="${2:-}"
    local expected="${3:-}"
    local label="${4:-$cmd}"

    if [[ ! -x "$cmd" ]]; then
        return 1
    fi
    if [[ -n "$test_arg" && -n "$expected" ]]; then
        "$cmd" $test_arg 2>&1 | grep -q "$expected" || return 1
    fi
    return 0
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_storage_healthy
#   Returns 0 if /mnt/user is mounted as shfs (array is up and healthy).
#   Returns 1 if the mount is absent or is not shfs (array stopped / degraded).
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_storage_healthy() {
    df --output=fstype /mnt/user 2>/dev/null | grep -q shfs
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_get_disk_states
#   Writes raw /var/local/emhttp/disks.ini to stdout.
#   Returns 1 if the file is absent (array not started or emhttp not running).
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_disk_states_path() {
    echo "/var/local/emhttp/disks.ini"
}

platform_get_disk_states() {
    local disks_ini
    disks_ini=$(platform_disk_states_path)
    [[ -f "$disks_ini" ]] || return 1
    cat "$disks_ini"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_get_temp_thresholds
#   Writes four space-separated values to stdout: HDD_HOT HDD_MAX SSD_HOT SSD_MAX (°C integers).
#   Falls back to 45/55/60/70 if dynamix.cfg is absent or keys are missing.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_get_temp_thresholds() {
    local cfg="/boot/config/plugins/dynamix/dynamix.cfg"
    local hdd_hot hdd_max ssd_hot ssd_max
    hdd_hot=$(grep -m1 '^hot='    "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"')
    hdd_max=$(grep -m1 '^max='    "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"')
    ssd_hot=$(grep -m1 '^hotssd=' "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"')
    ssd_max=$(grep -m1 '^maxssd=' "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"')
    echo "${hdd_hot:-45} ${hdd_max:-55} ${ssd_hot:-60} ${ssd_max:-70}"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_is_maintenance_running
#   Returns 0 if a parity check or sync is currently in progress.
#   Checks var.ini (Unraid 7.3+) then falls back to parity-date.txt (older).
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_is_maintenance_running() {
    local resync
    resync=$(awk -F'"' '/^mdResync=/{print $2}' /var/local/emhttp/var.ini 2>/dev/null)
    if [[ -n "$resync" && "$resync" != "0" ]]; then
        return 0
    fi
    grep -q "progress" /var/local/emhttp/parity-date.txt 2>/dev/null
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_is_service_enabled <service>
#   service: docker | libvirt
#   Returns 0 if the service is enabled in the Unraid boot config.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_is_service_enabled() {
    case "$1" in
        docker)  grep -qE '^DOCKER_ENABLED="?yes"?' /boot/config/docker.cfg  2>/dev/null ;;
        libvirt) grep -q  '^SERVICE="enable"'      /boot/config/domain.cfg  2>/dev/null ;;
        *)       return 1 ;;
    esac
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_restart_service <service>
#   Restarts the named service via its rc.d script.
#   Returns 1 if the rc.d script does not exist or is not executable.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_restart_service() {
    local rc
    rc=$(_platform_rc_script "$1")
    [[ -x "$rc" ]] || return 1
    "$rc" restart >/dev/null 2>&1
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_stop_service <service>
#   Stops the named service via its rc.d script.
#   Returns 1 if the rc.d script does not exist or is not executable.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_stop_service() {
    local rc
    rc=$(_platform_rc_script "$1")
    [[ -x "$rc" ]] || return 1
    "$rc" stop >/dev/null 2>&1
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_is_service_running <service>
#   Returns 0 if the service's main process is alive.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_is_service_running() {
    case "$1" in
        docker)  pgrep -x dockerd   >/dev/null 2>&1 ;;
        emhttp)  pgrep -x emhttpd   >/dev/null 2>&1 ;;
        nginx)   pgrep -x nginx     >/dev/null 2>&1 ;;
        php-fpm) pgrep -x php-fpm   >/dev/null 2>&1 ;;
        sshd)    pgrep -x sshd      >/dev/null 2>&1 ;;
        rsyslog) pgrep -x rsyslogd  >/dev/null 2>&1 ;;
        libvirt) pgrep -x libvirtd  >/dev/null 2>&1 ;;
        *)       pgrep -x "$1"      >/dev/null 2>&1 ;;
    esac
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_is_mover_running
#   Returns 0 if the Unraid mover is currently active.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_is_mover_running() {
    pgrep -f "emhttp.*Mover" >/dev/null 2>&1
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_get_mover_pid
#   Writes the mover's PID to stdout. Returns 1 if the mover is not running.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_get_mover_pid() {
    local pid
    pid=$(pgrep -f "emhttp.*Mover" | head -1)
    [[ -n "$pid" ]] || return 1
    echo "$pid"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_stop_user_scripts
#   Kills all Unraid user.scripts background processes.
#   Returns 0 whether or not any processes were found (pkill exits 1 on no match).
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_stop_user_scripts() {
    pkill -f "/tmp/user.scripts" 2>/dev/null || true
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_send_os_notification <message> [subject] [severity]
#   Sends a native Unraid notification via the dynamix notify script.
#   severity: normal | warning | alert  (default: normal)
#   Returns 1 if the notify script is absent.
# ──────────────────────────────────────────────────────────────────────────────────────────────
# ── Platform path defaults ─────────────────────────────────────────────────────────────────────
# Set here so master.conf and core scripts never contain OS-specific path literals.
# host*.conf may override any of these after the adapter is sourced.
DOCKER_APPDATA_BASE="${DOCKER_APPDATA_BASE:-/mnt/user/appdata}"

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_storage_path
#   Writes the root path where user shares and storage are accessible to stdout.
#   Used by array health checks and disk verification.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_storage_path() {
    echo "/mnt/user"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_webui_install_path
#   Writes the path where the platform serves WebGUI plugin files from.
#   On Unraid the PHP WebGUI is served from /boot/, separate from SCRIPTS_DIR.
#   Returns 1 (empty) if the platform serves directly from SCRIPTS_DIR (no sync needed).
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_webui_install_path() {
    echo "/boot/config/plugins/varaverk"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_scripts_dir_probe_cmd
#   Writes a shell command suitable for running on a remote via SSH to discover
#   that remote's SCRIPTS_DIR. Each platform exposes its install path differently.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_scripts_dir_probe_cmd() {
    echo "grep -m1 'SCRIPTS_DIR' /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null | cut -d= -f2 | tr -d '\"'"
}

platform_send_os_notification() {
    local message="$1"
    local subject="${2:-Varaverk}"
    local severity="${3:-normal}"
    local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify"

    [[ -x "$notify_script" ]] || return 1
    "$notify_script" -e "Varaverk" -s "$subject" -d "$message" -i "$severity" 2>/dev/null
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_get_templates_dir
#   Writes the path to the Unraid CA docker templates-user directory to stdout.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_get_templates_dir() {
    echo "/boot/config/plugins/dockerMan/templates-user"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_setup_db_path
#   Writes the path to the persistent Varaverk setup/wizard state database.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_setup_db_path() {
    echo "${STATE_DIR}/varaverk_setup.db"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_get_os_version
#   Writes the platform OS version string to stdout (e.g. "7.2.3").
#   Returns 1 if the version file is absent or unparseable.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_get_os_version() {
    local ver
    ver=$(grep -oP '(?<=version=")[^"]+' /etc/unraid-version 2>/dev/null)
    [[ -n "$ver" ]] || return 1
    echo "$ver"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_os_version_probe_cmd
#   Writes a shell command suitable for running on a remote via SSH to retrieve
#   that remote's OS version string. Output format matches platform_get_os_version.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_os_version_probe_cmd() {
    echo "grep -oP '(?<=version=\")[^\"]+' /etc/unraid-version 2>/dev/null"
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_rebuild_container <container_name>
#   Rebuilds a container from its stored Unraid CA XML template (stops old, recreates on new
#   image digest, prunes old image). Returns 1 if the rebuild script is absent or fails.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_rebuild_container() {
    local container="$1"
    local rebuild_script="/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container"
    [[ -x "$rebuild_script" ]] || return 1
    "$rebuild_script" "$container" >/dev/null 2>&1
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_push_conf
#   Pushes master.conf to all listed hosts via the WebGUI PHP API.
#   Writes per-host push results to stdout. Returns 1 if any host failed.
#   Returns 0 silently if php is unavailable (caller should warn manually).
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_push_conf() {
    command -v php &>/dev/null || return 0
    local rc=0
    php -r "
require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
\$results = vv_push_master_conf();
if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
\$failed = 0;
foreach (\$results as \$r) {
    echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
    if (!\$r['ok']) \$failed++;
}
exit(\$failed > 0 ? 1 : 0);
" 2>/dev/null
    rc=$?
    return $rc
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_push_setup_state
#   Pushes the Varaverk wizard setup state to all partners via the WebGUI PHP API.
#   No-op if php is unavailable. Always returns 0.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_push_setup_state() {
    command -v php &>/dev/null || return 0
    php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_setup_state();" 2>/dev/null
    return 0
}
