#!/bin/sh
# syno-daemon.sh — Synology bus router WAN balancing daemon.
# Rides on top of SmartWAN (does NOT own routing or health checks).
# Uses the same scoring engine as GL kit-busrouter (lib-score.sh + lib-decide.sh).
#
# Cycle (every CHECK_INTERVAL seconds):
#   1. Read SmartWAN state (interfaces, current weights, gateways)
#   2. Measure WAN1 / WAN2 latency, jitter, packet loss
#   3. Read Eyeride signal strength (if available)
#   4. Read Starlink status (if available)
#   5. Score each WAN (lib-score.sh 6-factor composite)
#   6. Compute target weights (lib-decide.sh)
#   7. Write weights to SmartWAN if change >= 10 (smartwan-adapter.sh)
#   8. Send telemetry to fleet hub (if hub is reachable)
#
# NEVER calls loadbalance. NEVER flushes conntrack.

set -e

# ── Paths ──────────────────────────────────────────────────────────
PKG_DIR="${PKG_DIR:-/var/packages/syno-balance/target}"
LIB_DIR="${LIB_DIR:-${PKG_DIR}/bin}"
CONF="/etc/syno-balance/syno-balance.conf"
STATE_DIR="/tmp/syno-balance"
PID_FILE="$PKG_DIR/var/syno-balance.pid"
LOG_TAG="syno-balance"

# ── Source libs (same scoring engine as GL) ────────────────────────
. "${LIB_DIR}/lib-score.sh"
. "${LIB_DIR}/lib-decide.sh"
. "${LIB_DIR}/smartwan-adapter.sh"

mkdir -p "$STATE_DIR"
echo "$$" > "$PID_FILE"

log()  { logger -t "$LOG_TAG" -p local0.warn "syno-balance[$(cat /etc/busrouter/device-id 2>/dev/null || hostname)]: $*"; }
warn() { logger -t "$LOG_TAG" -p local0.warn "syno-balance[$(cat /etc/busrouter/device-id 2>/dev/null || hostname)]: WARNING: $*"; }

# ── Config defaults ────────────────────────────────────────────────
load_config() {
    [ -f "$CONF" ] && . "$CONF"
    : "${CHECK_INTERVAL:=30}"
    : "${WAN1_IFACE:=eth0}"
    : "${WAN2_IFACE:=eth2}"
    : "${SPEEDTEST_AUTO_ENABLED:=0}"
    : "${EYERIDE_ENABLE:=0}"
    : "${EYERIDE_IP:=192.168.10.1}"
    : "${EYERIDE_PORT:=8080}"
    : "${EYERIDE_USER:=root}"
    : "${STARLINK_ENABLE:=0}"
    : "${STARLINK_IP:=192.168.100.1}"
    : "${STARLINK_PORT:=9200}"
    : "${PING_TARGETS:=1.1.1.2 9.9.9.9 8.8.8.8}"
    : "${PING_COUNT:=5}"
    : "${WEIGHT_MIN:=20}"
    : "${WEIGHT_MAX:=80}"
    : "${ROLLING_SAMPLES:=3}"
    : "${TELEMETRY_HUB:=http://167.172.237.162:8080/api/telemetry}"
    : "${TELEMETRY_BUFFER_DIR:=/tmp/syno-balance/telemetry-buf}"
    DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || hostname 2>/dev/null || echo "unknown")
}

# ── Measure WAN metrics ────────────────────────────────────────────
measure_wan() {
    local iface="$1"
    local gw="$2"
    local outfile="$STATE_DIR/metric_${iface}"

    # Latency + jitter + loss via ping (bound to interface)
    local ping_out=""
    ping_out=$(ping -I "$iface" -c "$PING_COUNT" -q "${PING_TARGETS%% *}" 2>/dev/null) || true

    local lat="" loss="" jitter=""
    if [ -n "$ping_out" ]; then
        # rtt min/avg/max/mdev
        lat=$(echo "$ping_out" | awk -F'[/ ]' '/^rtt/ {printf "%.1f", $8}')
        jitter=$(echo "$ping_out" | awk -F'[/ ]' '/^rtt/ {printf "%.1f", $11}')
        loss=$(echo "$ping_out" | grep -o '[0-9.]*%' | head -1 | tr -d '%')
    fi

    echo "lat=${lat:-0}"     >  "$outfile"
    echo "jitter=${jitter:-0}" >> "$outfile"
    echo "loss=${loss:-0}"   >> "$outfile"
    echo "gw=${gw:-none}"    >> "$outfile"
    echo "ts=$(date +%s)"    >> "$outfile"
}

# ── Read Eyeride signal via ubus (if available) ────────────────────
read_eyeride_signal() {
    [ "$EYERIDE_ENABLE" != "1" ] && return 1
    [ -z "$EYERIDE_PASSWORD" ] && return 1

    # ubus call over SSH to Eyeride (OpenWrt device)
    local sig=""
    sig=$(sshpass -p "$EYERIDE_PASSWORD" ssh -o StrictHostKeyChecking=no \
        -o ConnectTimeout=5 "${EYERIDE_USER}@${EYERIDE_IP}" -p "${EYERIDE_PORT}" \
        "ubus call network.interface.wwan status 2>/dev/null | grep -o '\"signal_strength\":[0-9-]*' | cut -d: -f2" 2>/dev/null) || true

    if [ -n "$sig" ]; then
        echo "$sig" > "$STATE_DIR/signal_eyeride"
        return 0
    fi
    return 1
}

# ── Read Starlink status (gRPC or HTTP fallback) ───────────────────
read_starlink_status() {
    [ "$STARLINK_ENABLE" != "1" ] && return 1

    # HTTP probe to Starlink dish
    local status=""
    status=$(curl -s --connect-timeout 3 "http://${STARLINK_IP}:${STARLINK_PORT}/status" 2>/dev/null) || true
    if [ -n "$status" ]; then
        echo "$status" > "$STATE_DIR/starlink_status"
        return 0
    fi
    return 1
}

# ── Score both WANs ────────────────────────────────────────────────
score_wans() {
    local w1_lat=0 w1_jit=0 w1_loss=0 w1_sig=""
    local w2_lat=0 w2_jit=0 w2_loss=0 w2_sig=""

    # Read WAN1 metrics
    if [ -f "$STATE_DIR/metric_${WAN1_IFACE}" ]; then
        . "$STATE_DIR/metric_${WAN1_IFACE}"
        w1_lat="${lat:-0}"; w1_jit="${jitter:-0}"; w1_loss="${loss:-0}"
    fi

    # Read WAN2 metrics
    if [ -f "$STATE_DIR/metric_${WAN2_IFACE}" ]; then
        . "$STATE_DIR/metric_${WAN2_IFACE}"
        w2_lat="${lat:-0}"; w2_jit="${jitter:-0}"; w2_loss="${loss:-0}"
    fi

    # Read Eyeride signal for WAN1
    if [ -f "$STATE_DIR/signal_eyeride" ]; then
        w1_sig=$(cat "$STATE_DIR/signal_eyeride" 2>/dev/null)
    fi

    # Compute scores (same algorithm as GL)
    local score1=0 score2=0
    # WAN1: latency, fake-dl=10, fake-ul=5, jitter, loss, signal
    score1=$(score_composite "$w1_lat" "10" "5" "$w1_jit" "$w1_loss" "$w1_sig")
    # WAN2: latency, fake-dl=10, fake-ul=5, jitter, loss, no signal
    score2=$(score_composite "$w2_lat" "10" "5" "$w2_jit" "$w2_loss" "")

    # Rolling average for stability
    local avg1="$score1" avg2="$score2"
    if [ "$ROLLING_SAMPLES" -gt 1 ]; then
        # Store last N scores
        echo "$score1" >> "$STATE_DIR/score_history_${WAN1_IFACE}"
        echo "$score2" >> "$STATE_DIR/score_history_${WAN2_IFACE}"
        tail -n "$ROLLING_SAMPLES" "$STATE_DIR/score_history_${WAN1_IFACE}" > "$STATE_DIR/score_roll_1"
        tail -n "$ROLLING_SAMPLES" "$STATE_DIR/score_history_${WAN2_IFACE}" > "$STATE_DIR/score_roll_2"
        # Trim
        if [ "$(wc -l < "$STATE_DIR/score_history_${WAN1_IFACE}")" -gt 10 ]; then
            tail -n 10 "$STATE_DIR/score_history_${WAN1_IFACE}" > "${STATE_DIR}/score_history_${WAN1_IFACE}.tmp"
            mv "${STATE_DIR}/score_history_${WAN1_IFACE}.tmp" "$STATE_DIR/score_history_${WAN1_IFACE}"
        fi
        avg1=$(awk '{s+=$1}END{printf "%d",(s/NR+0.5)}' "$STATE_DIR/score_roll_1" 2>/dev/null || echo "$score1")
        avg2=$(awk '{s+=$1}END{printf "%d",(s/NR+0.5)}' "$STATE_DIR/score_roll_2" 2>/dev/null || echo "$score2")
    fi

    echo "$avg1" > "$STATE_DIR/score_${WAN1_IFACE}"
    echo "$avg2" > "$STATE_DIR/score_${WAN2_IFACE}"

    log "scores: ${WAN1_IFACE}=${avg1} ${WAN2_IFACE}=${avg2}"
}

# ── Apply weights to SmartWAN ──────────────────────────────────────
apply_weights() {
    local s1=$(cat "$STATE_DIR/score_${WAN1_IFACE}" 2>/dev/null || echo "50")
    local s2=$(cat "$STATE_DIR/score_${WAN2_IFACE}" 2>/dev/null || echo "50")

    # WAN1 weight = 100 - WAN2 weight (SmartWAN dw_weight_ratio is WAN1 share)
    # Our wan_weight gives WAN1 share integer 20-80
    local w1=$(wan_weight "$s1" "$s2")

    # Clamp to configured range
    [ "$w1" -lt "$WEIGHT_MIN" ] && w1="$WEIGHT_MIN"
    [ "$w1" -gt "$WEIGHT_MAX" ] && w1="$WEIGHT_MAX"

    sw_write_weights "$w1"
}

# ── Main loop ──────────────────────────────────────────────────────
log "EVENT=DAEMON_START device=$DEVICE_ID version=${SYNO_BALANCE_VERSION:-0.0}"

load_config

# Verify SmartWAN is in load-balance mode
if ! sw_is_lb_mode; then
    warn "SmartWAN is not in load-balance mode. Set Load Balancing in Network Center."
    # Don't exit — router may transition to LB mode later
fi

_cycle=0
while true; do
    _cycle=$((_cycle + 1))

    # Read SmartWAN state
    if ! sw_read_state >/dev/null 2>&1; then
        warn "cannot read SmartWAN state — waiting for interface config"
        sleep "$CHECK_INTERVAL"
        continue
    fi

    # Measure both WANs
    measure_wan "$WAN1_IFACE" "$(ip route show dev "$WAN1_IFACE" 2>/dev/null | grep default | awk '{print $3}' | head -1)"
    measure_wan "$WAN2_IFACE" "$(ip route show dev "$WAN2_IFACE" 2>/dev/null | grep default | awk '{print $3}' | head -1)"

    # Optional: Eyeride signal, Starlink status
    read_eyeride_signal 2>/dev/null || true
    read_starlink_status 2>/dev/null || true

    # Score + apply
    score_wans
    apply_weights

    # Save cycle state for telemetry
    echo "$_cycle" > "$STATE_DIR/cycle"

    sleep "$CHECK_INTERVAL"
done
