#!/bin/bash
# Apply configuration changes — restarts the service if running.
# Called by Unraid's /update.php after config file is saved (#command).

PLUGIN="unraid-management-agent"
SCRIPTS_DIR="/usr/local/emhttp/plugins/$PLUGIN/scripts"
CONFIG_FILE="/boot/config/plugins/$PLUGIN/config.cfg"
LOG_FILE="/var/log/unraid-management-agent.log"
DEFAULT_PORT=8043
# Absolute path: when invoked from the webgui (php-fpm), PATH lacks /usr/sbin.
IP_BIN="/usr/sbin/ip"
[ -x "$IP_BIN" ] || IP_BIN="/sbin/ip"

# Ensure config file has restrictive permissions (contains MQTT password)
if [ -f "$CONFIG_FILE" ]; then
    if ! chmod 0600 "$CONFIG_FILE"; then
        echo "Error: chmod 0600 failed on $CONFIG_FILE" >&2
        exit 1
    fi
fi

# Read a single key from the config without sourcing it (prevents code
# injection). Internal use only: callers pass literal known keys (e.g.
# BIND_ADDRESS, PORT), never user-supplied input.
read_cfg() {
    grep -E "^${1}=" "$CONFIG_FILE" 2>/dev/null | head -1 | sed "s/^${1}=//; s/^[\"']//; s/[\"'].*//"
}

# ── Server-side bind address validation ──────────────────────
# The HTML form cannot validate this reliably. Loopback is rejected outright
# (integrations such as Home Assistant must be able to reach the agent) and
# addresses not assigned to a local interface are reset, mirroring the
# daemon's own startup fallback so the UI always shows the effective value.
BIND_ADDRESS=""
if [ -f "$CONFIG_FILE" ]; then
    BIND_ADDRESS="$(read_cfg BIND_ADDRESS)"
    reject_reason=""
    case "${BIND_ADDRESS:-}" in
        "" | 0.0.0.0 | ::) ;; # empty/wildcard = all interfaces
        127.* | ::1)
            reject_reason="loopback addresses are not allowed (integrations must be able to reach the agent)"
            ;;
        *)
            # Case-insensitive match: ip(8) prints IPv6 hex in lowercase but
            # users may enter uppercase (e.g. 2001:DB8::1).
            if ! "$IP_BIN" addr show 2>/dev/null | grep -iFq "inet ${BIND_ADDRESS}/" &&
                ! "$IP_BIN" addr show 2>/dev/null | grep -iFq "inet6 ${BIND_ADDRESS}/"; then
                reject_reason="address is not assigned to any local interface"
            fi
            ;;
    esac
    if [ -n "$reject_reason" ]; then
        echo "Warning: bind address '${BIND_ADDRESS}' rejected: ${reject_reason}; resetting to all interfaces"
        if ! sed -i 's/^BIND_ADDRESS=.*/BIND_ADDRESS=""/' "$CONFIG_FILE"; then
            echo "Error: failed to reset BIND_ADDRESS in $CONFIG_FILE" >&2
            exit 1
        fi
        BIND_ADDRESS=""
    fi
fi

# ── Server-side port validation ───────────────────────────────
# The HTML form enforces 1024-65535 client-side only; an invalid value would
# make the daemon fail to bind and exit silently.
PORT="$(read_cfg PORT)"
if ! echo "${PORT:-}" | grep -Eq '^[0-9]+$' || [ "$PORT" -lt 1024 ] || [ "$PORT" -gt 65535 ]; then
    if [ -n "${PORT:-}" ]; then
        echo "Warning: port '${PORT}' is invalid (must be 1024-65535); resetting to ${DEFAULT_PORT}"
        if ! sed -i "s/^PORT=.*/PORT=\"${DEFAULT_PORT}\"/" "$CONFIG_FILE"; then
            echo "Error: failed to reset PORT in $CONFIG_FILE" >&2
            exit 1
        fi
    fi
    PORT="$DEFAULT_PORT"
fi

# Only restart if the service is currently running
if pidof "$PLUGIN" > /dev/null 2>&1; then
    "$SCRIPTS_DIR/stop"
    stop_rc=$?

    if [ $stop_rc -ne 0 ]; then
        echo "Error: stop script failed (rc=$stop_rc)" >&2
        exit $stop_rc
    fi

    # Poll until stopped or timeout (5 attempts x 500ms = 2.5s max)
    for _ in {1..5}; do
        if ! pidof "$PLUGIN" > /dev/null 2>&1; then
            break
        fi
        usleep 500000 2>/dev/null || sleep 0.5
    done

    # Final check — abort if still running
    if pidof "$PLUGIN" > /dev/null 2>&1; then
        echo "Error: service did not stop within timeout" >&2
        exit 1
    fi

    "$SCRIPTS_DIR/start"
    start_rc=$?

    if [ $start_rc -ne 0 ]; then
        echo "Error: start script failed (rc=$start_rc)" >&2
        exit $start_rc
    fi

    # Verify the daemon actually came back up and answers HTTP. Probe the
    # address it binds to — a hardcoded loopback probe fails when a specific
    # NIC address is configured. Wildcards are reachable via loopback; IPv6
    # literals need brackets in a URL.
    PROBE_HOST="127.0.0.1"
    case "${BIND_ADDRESS:-}" in
        "" | 0.0.0.0 | ::) PROBE_HOST="127.0.0.1" ;;
        *:*) PROBE_HOST="[${BIND_ADDRESS}]" ;;
        *) PROBE_HOST="$BIND_ADDRESS" ;;
    esac

    process_alive=0
    probe_ok=0
    for _ in {1..6}; do
        if ! pidof "$PLUGIN" > /dev/null 2>&1; then
            process_alive=0
            break
        fi
        process_alive=1
        if command -v curl > /dev/null 2>&1 &&
            curl -fsS -m 2 -o /dev/null "http://${PROBE_HOST}:${PORT}/api/v1/health" 2>/dev/null; then
            probe_ok=1
            break
        fi
        # Process alive but HTTP not answering yet — keep waiting.
        sleep 1
    done

    if [ "$process_alive" -ne 1 ]; then
        echo "Error: service failed to restart — check the log:" >&2
        tail -5 "$LOG_FILE" 2>/dev/null >&2
        exit 1
    fi
    if [ "$probe_ok" -eq 1 ]; then
        echo "Service restarted (bind ${BIND_ADDRESS:-all interfaces}, port ${PORT})"
    else
        # Treated as success: the daemon is running, the probe may simply be
        # blocked or still warming up.
        echo "Warning: service restarted but the HTTP health check on http://${PROBE_HOST}:${PORT} did not answer yet"
    fi
fi
