#!/bin/sh
# flash.sh — Flash custom Kahf Router firmware onto an OpenWrt router
#
# Downloads the latest sysupgrade image from S3 and flashes it via sysupgrade.
# Resets all config (-n flag) so 99-family-safe runs cleanly on first boot.
# After reboot, streams bootstrap logs so you can watch OAF install live.
#
# Usage:
#   lab/flash.sh [options]
#   lab/flash.sh --ip 192.168.1.1 --password mypass --channel dev
#
# Options:
#   -i, --ip <IP>           Router IP (default: auto-detect via kahf-router.lan, then openwrt.lan)
#   -u, --user <user>       SSH user (default: root)
#   -p, --password <pass>   SSH password
#   -c, --channel <ch>      Channel: dev or prod (default: prod)
#   -h, --help              Show this help

set -eu

# Package/firmware CDN host. Matches KAHF_CDN_DOMAIN in config/lab.env.
KAHF_HOST="${KAHF_HOST:-router.kahf.co}"

# Do NOT use 'set -u' beyond this point — many variables are conditionally
# set inside blocks and referenced later.
set +u

# ── Colours ──────────────────────────────────────────────────────────────────
_R='\033[1;31m' _G='\033[1;32m' _Y='\033[1;33m' _B='\033[1;34m' _N='\033[0m'
info() { printf "${_B}[*]${_N} %s\n" "$*"; }
ok() { printf "${_G}[+]${_N} %s\n" "$*"; }
warn() { printf "${_Y}[!]${_N} %s\n" "$*"; }
die() {
    printf "${_R}[-]${_N} %s\n" "$*" >&2
    exit 1
}
ask() { printf "${_B}  ➜${_N} %s " "$*"; }

# ── Defaults ─────────────────────────────────────────────────────────────────
ROUTER_IP=""
SSH_USER="root"
SSH_PASS=""
CHANNEL="dev"
LOCAL_FILE=""
FW_OWNED=0  # 1 if we downloaded FW_TMP and must delete it on exit
AUTO_YES=0  # 1 to skip interactive confirmations (--yes flag)

EXPECTED_MODEL="Tenbay WR3000K"
EXPECTED_VERSION="25.12.4"

# ── Argument parsing ──────────────────────────────────────────────────────────
usage() {
    cat <<EOF
Usage: $(basename "$0") [options]

Options:
  -i, --ip <IP>           Router IP (default: auto-detect via kahf-router.lan, then openwrt.lan)
  -u, --user <user>       SSH user (default: root)
  -p, --password <pass>   SSH password
  -c, --channel <ch>      Channel: dev or prod (default: prod)
  -f, --local-file <path>  Flash a locally-built firmware file (skips S3 download)
  -y, --yes               Skip interactive confirmations (for non-interactive use)
  -h, --help              Show this help
EOF
}

while [ $# -gt 0 ]; do
    case "$1" in
    -i | --ip)
        ROUTER_IP="$2"
        shift 2
        ;;
    -u | --user)
        SSH_USER="$2"
        shift 2
        ;;
    -p | --password)
        SSH_PASS="$2"
        shift 2
        ;;
    -c | --channel)
        CHANNEL="$2"
        shift 2
        ;;
    -f | --local-file | --file)
        LOCAL_FILE="$2"
        shift 2
        ;;
    -y | --yes)
        AUTO_YES=1
        shift
        ;;
    -h | --help)
        usage
        exit 0
        ;;
    *) die "Unknown option: $1" ;;
    esac
done

if [ -z "${LOCAL_FILE}" ]; then
    case "${CHANNEL}" in
    dev | prod) ;;
    *) die "Invalid channel: '${CHANNEL}' (must be dev or prod)" ;;
    esac
fi
S3_URL="https://${KAHF_HOST}/router-${CHANNEL}/firmware/openwrt-sysupgrade.bin"
FW_TMP="/tmp/kahf-firmware-$$.bin"

# ── SSH helpers ───────────────────────────────────────────────────────────────
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR"

_ssh() {
    # -n: redirect SSH stdin from /dev/null so interactive read prompts in this
    # script are not consumed by SSH connections
    # shellcheck disable=SC2086
    if [ -n "${SSH_PASS}" ] && command -v sshpass >/dev/null 2>&1; then
        sshpass -p "${SSH_PASS}" ssh -n ${SSH_OPTS} "${SSH_USER}@${ROUTER_IP}" "$@"
    else
        # shellcheck disable=SC2086
        ssh -n ${SSH_OPTS} "${SSH_USER}@${ROUTER_IP}" "$@"
    fi
}

_ssh_pipe_in() {
    # Pipe stdin to a remote file path ($1)
    local remote="$1"
    # shellcheck disable=SC2086,SC2029
    if [ -n "${SSH_PASS}" ] && command -v sshpass >/dev/null 2>&1; then
        sshpass -p "${SSH_PASS}" ssh ${SSH_OPTS} "${SSH_USER}@${ROUTER_IP}" "cat > '${remote}'"
    else
        # shellcheck disable=SC2086,SC2029
        ssh ${SSH_OPTS} "${SSH_USER}@${ROUTER_IP}" "cat > '${remote}'"
    fi
}

# ── IP auto-detection ─────────────────────────────────────────────────────────
detect_ip() {
    local ip=""  # shellcheck disable=SC2034

    # 1. Try kahf-router.lan first (Kahf custom firmware hostname), then fall
    #    back to openwrt.lan (stock OpenWrt). Use hostnames not resolved IPs so
    #    macOS mDNS routes via the correct interface (avoids VPN routing issues)
    if ping -c1 -W2 kahf-router.lan >/dev/null 2>&1; then
        echo "kahf-router.lan"
        return
    fi
    if ping -c1 -W2 openwrt.lan >/dev/null 2>&1; then
        echo "openwrt.lan"
        return
    fi

    # 2. Common OpenWrt default
    if ping -c1 -W2 192.168.1.1 >/dev/null 2>&1; then
        echo "192.168.1.1"
        return
    fi

    # 3. Default gateway
    local gw=""
    gw=$(ip route show default 2>/dev/null | awk '/default via/{print $3}' | head -1 || true)
    if [ -z "$gw" ]; then
        # macOS fallback
        gw=$(route -n get default 2>/dev/null | awk '/gateway:/{print $2}' || true)
    fi
    if [ -n "$gw" ] && ping -c1 -W2 "$gw" >/dev/null 2>&1; then
        echo "$gw"
        return
    fi

    echo ""
}

# ── Cleanup ───────────────────────────────────────────────────────────────────
cleanup() {
    [ "${FW_OWNED}" = "1" ] && rm -f "${FW_TMP}"
}
trap cleanup EXIT

# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
echo "║   Kahf Router Firmware Flash                            ║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""

# ── Step 1: Router IP ────────────────────────────────────────────────────────
if [ -z "${ROUTER_IP:-}" ]; then
    info "Auto-detecting router IP..."
    ROUTER_IP=$(detect_ip)
    if [ -n "${ROUTER_IP}" ]; then
        ok "Detected: ${ROUTER_IP}"
    else
        warn "Could not auto-detect router IP."
        ask "Router IP address:"
        read -r ROUTER_IP
        [ -z "${ROUTER_IP}" ] && die "No IP provided."
    fi
fi

# ── Step 2: SSH password ──────────────────────────────────────────────────────
if [ -z "${SSH_PASS}" ] && [ -t 0 ]; then
    ask "SSH password for ${SSH_USER}@${ROUTER_IP} (leave blank if key auth):"
    # Read without echo
    stty -echo 2>/dev/null || true
    read -r SSH_PASS
    stty echo 2>/dev/null || true
    echo ""
fi

if [ -n "${SSH_PASS}" ] && ! command -v sshpass >/dev/null 2>&1; then
    warn "sshpass not found — SSH will prompt for password interactively."
    warn "Install with: brew install sshpass  OR  apt install sshpass"
    SSH_PASS=""
fi

# ── Step 3: Test SSH connectivity ─────────────────────────────────────────────
info "Testing SSH connection to ${SSH_USER}@${ROUTER_IP}..."
if ! _ssh "echo ok" >/dev/null 2>&1; then
    die "Cannot SSH to ${ROUTER_IP}. Check IP, user, and password."
fi
ok "SSH connection successful"

# ── Step 4: Verify router model ───────────────────────────────────────────────
info "Checking router model..."
ACTUAL_MODEL=$(_ssh "cat /tmp/sysinfo/model 2>/dev/null || echo unknown" || echo "unknown")
if [ "${ACTUAL_MODEL}" = "${EXPECTED_MODEL}" ]; then
    ok "Model: ${ACTUAL_MODEL}"
else
    warn "Model mismatch!"
    warn "  Expected: ${EXPECTED_MODEL}"
    warn "  Actual:   ${ACTUAL_MODEL}"
    warn ""
    warn "Flashing the wrong firmware can brick your router."
    ask "Flash anyway? [y/N]: "
    read -r CONFIRM
    case "${CONFIRM}" in
    y | Y | yes | YES) warn "Proceeding with override..." ;;
    *) die "Aborted." ;;
    esac
fi

# ── Step 5: Check current firmware version ───────────────────────────────────
info "Checking current firmware version..."
CURRENT_VERSION=$(_ssh ". /etc/openwrt_release 2>/dev/null && echo \"\${DISTRIB_RELEASE}\"" || echo "unknown")
if [ "${CURRENT_VERSION}" = "${EXPECTED_VERSION}" ]; then
    ok "Router is already running OpenWrt ${EXPECTED_VERSION}"
    if [ "${AUTO_YES}" = "1" ] || [ ! -t 0 ]; then
        warn "Re-flashing (non-interactive / --yes mode)..."
    else
        ask "Already on target version. Re-flash anyway? [y/N]: "
        read -r CONFIRM
        case "${CONFIRM}" in
        y | Y | yes | YES) warn "Re-flashing..." ;;
        *)
            ok "Nothing to do."
            exit 0
            ;;
        esac
    fi
else
    if [ "${CURRENT_VERSION}" = "unknown" ]; then
        warn "Could not read firmware version — proceeding with flash"
    else
        info "Current: ${CURRENT_VERSION} → upgrading to ${EXPECTED_VERSION}"
    fi
fi

# ── Step 6: Acquire firmware ──────────────────────────────────────────────────
if [ -n "${LOCAL_FILE}" ]; then
    [ -f "${LOCAL_FILE}" ] || die "Local firmware file not found: ${LOCAL_FILE}"
    FW_SIZE=$(wc -c < "${LOCAL_FILE}" 2>/dev/null || echo "0")
    [ "${FW_SIZE}" -eq 0 ] && die "Local firmware file is empty: ${LOCAL_FILE}"
    ok "Using local firmware: ${LOCAL_FILE}"
    ok "  Size: $(( FW_SIZE / 1024 / 1024 )) MB"
    FW_TMP="${LOCAL_FILE}"
else
    info "Downloading firmware from S3..."
    info "  Channel: ${CHANNEL}"
    info "  URL:     ${S3_URL}"
    echo ""

    # Cache-bust: S3/CDN can serve a stale openwrt-sysupgrade.bin that lags the
    # freshly-published sha256sums, which would (correctly) fail the integrity
    # check below. A unique query string forces the current object.
    _CB="$(date +%s)$$"
    DL_RC=1
    if command -v curl >/dev/null 2>&1; then
        curl -fSL --progress-bar -o "${FW_TMP}" "${S3_URL}?cb=${_CB}" && DL_RC=0 || DL_RC=$?
    elif command -v wget >/dev/null 2>&1; then
        wget -q --show-progress -O "${FW_TMP}" "${S3_URL}?cb=${_CB}" && DL_RC=0 || DL_RC=$?
    else
        die "Neither curl nor wget found."
    fi
    [ "${DL_RC}" -ne 0 ] && die "Download failed (exit=${DL_RC}). Check channel '${CHANNEL}' has a published firmware."

    FW_SIZE=$(wc -c < "${FW_TMP}" 2>/dev/null || echo "0")
    [ "${FW_SIZE}" -eq 0 ] && die "Downloaded firmware is empty (0 bytes)."
    ok "Downloaded: $(( FW_SIZE / 1024 / 1024 )) MB (${FW_SIZE} bytes)"
    FW_OWNED=1

    # ── Verify image integrity against the published checksum ────────────────
    # Detects corruption/truncation/tampering in transit before we flash. The
    # checksum is published by CI alongside the image; if absent (older build),
    # warn rather than block.
    SUMS_URL="https://${KAHF_HOST}/router-${CHANNEL}/firmware/sha256sums?cb=${_CB}"
    EXPECTED=""
    if command -v curl >/dev/null 2>&1; then
        EXPECTED=$(curl -fsSL --max-time 15 "${SUMS_URL}" 2>/dev/null | awk 'NR==1{print $1}')
    elif command -v wget >/dev/null 2>&1; then
        EXPECTED=$(wget -qO- "${SUMS_URL}" 2>/dev/null | awk 'NR==1{print $1}')
    fi
    if [ -n "${EXPECTED}" ]; then
        if command -v sha256sum >/dev/null 2>&1; then
            ACTUAL=$(sha256sum "${FW_TMP}" | awk '{print $1}')
        else
            ACTUAL=$(shasum -a 256 "${FW_TMP}" | awk '{print $1}')
        fi
        if [ "${ACTUAL}" = "${EXPECTED}" ]; then
            ok "Image checksum verified (sha256 matches published sha256sums)"
        else
            die "Image checksum MISMATCH — refusing to flash.\n  expected: ${EXPECTED}\n  actual:   ${ACTUAL}\nThe download may be corrupt or tampered. Retry, or check the CDN."
        fi
    else
        warn "No published sha256sums for channel '${CHANNEL}' — skipping integrity check (older build?)."
    fi
fi

# ── Step 7: Upload firmware to router ────────────────────────────────────────
info "Uploading firmware to router..."
if ! _ssh_pipe_in "/tmp/firmware.bin" <"${FW_TMP}"; then
    die "Firmware upload failed — SSH connection may have dropped."
fi
ok "Firmware uploaded to /tmp/firmware.bin"

# ── Step 8: Validate image on router ─────────────────────────────────────────
info "Validating image on router..."
if ! _ssh "sysupgrade --test /tmp/firmware.bin" 2>&1; then
    die "sysupgrade --test failed — image may be incompatible with this router."
fi
ok "Image validation passed"

# ── Step 9: Confirm flash ────────────────────────────────────────────────────
echo ""
warn "About to flash the router. This will:"
warn "  • Erase all current configuration (clean first boot)"
warn "  • Reset WiFi to SSID: Kahf-Router"
warn "  • Reboot the router (~30 seconds)"
warn ""
if [ "${AUTO_YES}" = "1" ] || [ ! -t 0 ]; then
    warn "Proceeding (non-interactive / --yes mode)..."
else
    ask "Proceed with flashing? [y/N]: "
    read -r CONFIRM
    case "${CONFIRM}" in
    y | Y | yes | YES) ;;
    *) die "Aborted." ;;
    esac
fi

# ── Step 10: Flash ────────────────────────────────────────────────────────────
info "Flashing firmware (router will reboot)..."
_ssh "sysupgrade -n /tmp/firmware.bin" 2>/dev/null || true # connection drops on reboot
# Brief pause to let sysupgrade begin writing before we start polling
sleep 3

# ── Step 11: Wait for reboot ─────────────────────────────────────────────────
echo ""
info "Waiting for router to go offline..."
TIMEOUT=30
i=0
while [ $i -lt $TIMEOUT ]; do
    if ! ping -c1 -W1 "${ROUTER_IP}" >/dev/null 2>&1; then
        ok "Router offline (${i}s)"
        break
    fi
    sleep 1
    i=$((i + 1))
done
if [ $i -ge $TIMEOUT ]; then
    warn "Router still responding after ${TIMEOUT}s — it may have rebooted very quickly"
fi

info "Waiting for router to come back online (up to 120s)..."
TIMEOUT=120
i=0
while [ $i -lt $TIMEOUT ]; do
    if ping -c1 -W1 "${ROUTER_IP}" >/dev/null 2>&1; then
        ok "Router online after ${i}s"
        break
    fi
    sleep 1
    i=$((i + 1))
done
[ $i -ge $TIMEOUT ] && die "Router did not come back online within ${TIMEOUT}s."

# Give SSH daemon a moment to start
sleep 5

info "Waiting for SSH to be ready..."
i=0
while [ $i -lt 30 ]; do
    if _ssh "echo ok" >/dev/null 2>&1; then
        ok "SSH ready"
        break
    fi
    sleep 2
    i=$((i + 2))
done
[ $i -ge 30 ] && die "SSH not ready after reboot."

# mDNS hostnames (.lan) can flicker during the router's avahi-daemon restart
# after a clean flash.  Resolve once to a numeric IP now while we know SSH
# works, so all subsequent _ssh polling calls never hit name resolution races.
case "${ROUTER_IP}" in
*.lan)
    _ip4=""
    # Try getent first (Linux); fall back to ping on macOS
    _ip4=$(getent hosts "${ROUTER_IP}" 2>/dev/null | awk '{print $1; exit}' || true)
    if [ -z "${_ip4}" ]; then
        _ip4=$(ping -c1 -W2 "${ROUTER_IP}" 2>/dev/null \
            | awk -F'[()]' '/^PING/{print $2; exit}' || true)
    fi
    if [ -n "${_ip4}" ]; then
        ok "Pinned ${ROUTER_IP} → ${_ip4} (avoids mDNS flicker during polling)"
        ROUTER_IP="${_ip4}"
    fi
    ;;
esac

# ── Step 12: Check internet, configure WAN if needed ─────────────────────────
echo ""
info "Checking internet connectivity..."
INET_OK=0
if _ssh "ping -c1 -W5 1.1.1.1 >/dev/null 2>&1"; then
    ok "Internet is reachable"
    INET_OK=1
fi

# DHCP boot-race recovery: after a clean flash the modem needs ~30-60s to
# re-establish upstream.  udhcpc's initial 3-discover burst misses this window
# and enters ~11-minute exponential backoff.  Kicking 'ifup wan' resets the
# DHCP state machine so it sends a fresh DISCOVER immediately.
if [ $INET_OK -eq 0 ]; then
    _wan_proto=$(_ssh "uci -q get network.wan.proto 2>/dev/null || echo unknown")
    if [ "${_wan_proto}" = "dhcp" ]; then
        info "DHCP WAN has no lease yet — kicking DHCP client and waiting up to 90s..."
        _ssh "ifup wan" >/dev/null 2>&1 || true
        _i=0
        while [ $_i -lt 90 ]; do
            sleep 5
            _i=$((_i + 5))
            if _ssh "ping -c1 -W3 1.1.1.1 >/dev/null 2>&1"; then
                ok "Internet is reachable (DHCP lease obtained after ${_i}s retry)"
                INET_OK=1
                break
            fi
            [ $((_i % 15)) -eq 0 ] && info "  Still waiting for DHCP lease... (${_i}s / 90s)"
        done
        [ $INET_OK -eq 0 ] && warn "DHCP lease not obtained within 90s after kick."
    fi
fi

if [ $INET_OK -eq 0 ]; then
    warn "No internet — WAN needs setup after clean flash."
    info "Current WAN proto: $(_ssh "uci -q get network.wan.proto 2>/dev/null || echo unset")"
    echo ""
    if [ "${AUTO_YES}" = "1" ] || [ ! -t 0 ]; then
        warn "Skipping WAN setup (non-interactive / --yes mode)."
        warn "Configure WAN via the Kahf app — family-safe installs automatically once internet is available."
        exit 0
    else
        ask "Configure PPPoE WAN? [Y/n]:"
        read -r SETUP_PPP
    fi
    case "${SETUP_PPP}" in
    n | N | no | NO)
        warn "Skipping WAN setup."
        echo ""
        ok "Flash complete. Configure WAN via the Kahf app — family-safe installs automatically once internet is available."
        exit 0
        ;;
    *)
        ask "PPPoE username:"
        read -r PPP_USER
        [ -z "${PPP_USER}" ] && die "PPPoE username required."
        ask "PPPoE password:"
        stty -echo 2>/dev/null || true
        read -r PPP_PASS
        stty echo 2>/dev/null || true
        echo ""
        [ -z "${PPP_PASS}" ] && die "PPPoE password required."

        info "Configuring PPPoE..."
        # Write a script to the router to avoid shell injection with special chars
        {
            echo "uci set network.wan.proto='pppoe'"
            printf "uci set network.wan.username='%s'\n" \
                "$(printf '%s' "${PPP_USER}" | sed "s/'/'\\\\''/g")"
            printf "uci set network.wan.password='%s'\n" \
                "$(printf '%s' "${PPP_PASS}" | sed "s/'/'\\\\''/g")"
            echo "uci set network.wan.peerdns='0'"
            echo "uci -q delete network.wan.dns"
            echo "uci add_list network.wan.dns='1.1.1.3'"
            echo "uci add_list network.wan.dns='1.0.0.3'"
            echo "uci commit network"
            echo "ifup wan"
        } | _ssh_pipe_in "/tmp/setup-wan.sh"
        _ssh "sh /tmp/setup-wan.sh; rm -f /tmp/setup-wan.sh"

        info "Waiting for PPPoE to connect (up to 30s)..."
        i=0
        while [ $i -lt 30 ]; do
            if _ssh "ping -c1 -W3 1.1.1.1 >/dev/null 2>&1"; then
                ok "Internet is reachable"
                INET_OK=1
                break
            fi
            sleep 2
            i=$((i + 2))
        done
        [ $INET_OK -eq 0 ] && warn "PPPoE still offline — bootstrap may fail without internet."
        ;;
    esac
fi

# ── Step 13: Stream bootstrap logs ────────────────────────────────────────────
echo ""
ok "Router is up. Streaming bootstrap logs (waiting for OAF install)..."
echo "────────────────────────────────────────────────────────────"

# Stream logs in background, writing to a temp fifo
LOG_FIFO="/tmp/kahf-bootstrap-log-$$"
mkfifo "${LOG_FIFO}" || die "Failed to create FIFO at ${LOG_FIFO}"
trap '[ "${FW_OWNED}" = "1" ] && rm -f "${FW_TMP}"; rm -f "${LOG_FIFO}"; kill "${LOG_BG_PID:-}" 2>/dev/null || true; wait "${LOG_BG_PID:-}" 2>/dev/null || true' EXIT

_ssh "logread -f 2>/dev/null" >"${LOG_FIFO}" &
LOG_BG_PID=$!

# Give logread a moment to establish the stream
sleep 1

# Read from fifo, print relevant lines, exit on completion event.
# Show all router syslog lines that are relevant to the bootstrap process
# so the user sees progress instead of a blank screen.
DONE=0
TIMEOUT=300 # 5 minutes max
START=$(date +%s)
LAST_STATUS=$(date +%s)
LINE_COUNT=0

while IFS= read -r line; do
    LINE_COUNT=$((LINE_COUNT + 1))
    case "${line}" in
    *oaf-bootstrap* | *oaf-update* | *family-safe* | *cron*)
        echo "  ${line}"
        case "${line}" in
        *event=cron_removed*)
            DONE=1
            break
            ;;
        esac
        LAST_STATUS=$(date +%s)
        ;;
    esac
    # Print a periodic status message so the user knows we're alive
    NOW=$(date +%s)
    ELAPSED=$((NOW - START))
    SILENCE=$((NOW - LAST_STATUS))
    if [ $SILENCE -ge 15 ]; then
        printf "  %s  Waiting for bootstrap cron tick... (%ds)\n" "$(date +%H:%M:%S)" "$ELAPSED"
        LAST_STATUS=$NOW
    fi
    [ $ELAPSED -gt $TIMEOUT ] && break
done <"${LOG_FIFO}"

kill "${LOG_BG_PID}" 2>/dev/null || true
wait "${LOG_BG_PID}" 2>/dev/null || true

echo "────────────────────────────────────────────────────────────"

if [ $DONE -eq 1 ]; then
    echo ""
    ok "Bootstrap complete! OAF and family-safe are installed."
    echo ""

    # ── Step 14: Post-bootstrap validation ────────────────────────────────
    FAIL=0
    UPDATE_OK=1
    DNSMASQ_OK=0
    GW_REACHABLE=0
    DNS_OK=0

    # 14a: Verify oaf-update.sh completed successfully.
    #
    # We already know it finished because cron_removed only fires after
    # oaf-bootstrap.sh's "sh ${TMP}" returns 0.  But verify by checking
    # syslog for the event=finish line — no process detection needed.
    info "Verifying oaf-update result..."
    _finish=$(_ssh "logread | grep 'oaf-update.*event=finish' | tail -1" 2>/dev/null || echo "")
    if [ -n "${_finish}" ]; then
        ok "oaf-update.sh completed successfully"
    else
        # event=finish not in syslog yet — unlikely but poll for it
        info "  Waiting for event=finish in syslog..."
        UPDATE_TIMEOUT=120
        _i=0
        while [ $_i -lt $UPDATE_TIMEOUT ]; do
            _finish=$(_ssh "logread | grep 'oaf-update.*event=finish' | tail -1" 2>/dev/null || echo "")
            if [ -n "${_finish}" ]; then
                ok "oaf-update.sh completed successfully (${_i}s)"
                break
            fi
            if [ $((_i % 15)) -eq 0 ]; then
                info "  oaf-update.sh still running... (${_i}s / ${UPDATE_TIMEOUT}s)"
            fi
            sleep 5
            _i=$((_i + 5))
        done
        if [ $_i -ge $UPDATE_TIMEOUT ]; then
            warn "oaf-update.sh event=finish not found in syslog within ${UPDATE_TIMEOUT}s"
            UPDATE_OK=0; FAIL=1
        fi
    fi

    # 14b: Wait for dnsmasq to be running and listening
    info "Checking dnsmasq..."
    DNS_TIMEOUT=60
    _i=0
    while [ $_i -lt $DNS_TIMEOUT ]; do
        _dnsmasq=$(_ssh 'pgrep dnsmasq >/dev/null 2>&1 && netstat -tuln 2>/dev/null | grep -q ":53 " && echo 1 || echo 0')
        if [ "${_dnsmasq}" = "1" ]; then
            ok "dnsmasq is running and listening on port 53"
            DNSMASQ_OK=1
            break
        fi
        if [ $((_i % 10)) -eq 0 ] && [ $_i -gt 0 ]; then
            info "  dnsmasq not ready yet... (${_i}s / ${DNS_TIMEOUT}s)"
        fi
        sleep 3
        _i=$((_i + 3))
    done
    if [ $_i -ge $DNS_TIMEOUT ]; then
        warn "dnsmasq not ready within ${DNS_TIMEOUT}s"
        FAIL=1
    fi

    # 14c: Test WAN gateway reachability
    info "Testing WAN gateway reachability..."
    _gw=$(_ssh 'ip route show default 2>/dev/null | awk "/default/{print \$3}"' 2>/dev/null || echo "")
    if [ -n "${_gw}" ]; then
        if _ssh "ping -c1 -W5 ${_gw} >/dev/null 2>&1"; then
            ok "WAN gateway reachable (${_gw})"
            GW_REACHABLE=1
        else
            warn "WAN gateway not reachable (${_gw})"
            FAIL=1
        fi
    else
        warn "No default gateway found"
        FAIL=1
    fi

    # 14d: Test DNS resolution
    info "Testing DNS resolution..."

    # Test via dnsmasq (router's own resolver)
    _dns_result=$(_ssh 'nslookup google.com 127.0.0.1 >/dev/null 2>&1 && echo ok || echo fail' 2>/dev/null || echo "fail")
    if [ "${_dns_result}" = "ok" ]; then
        ok "DNS resolution working (via dnsmasq)"
        DNS_OK=1
    else
        warn "DNS resolution failed (via dnsmasq)"
        FAIL=1
    fi

    # Also test direct upstream DNS if gateway is reachable
    if [ $GW_REACHABLE -eq 1 ] && [ $DNS_OK -eq 0 ]; then
        info "  Testing direct upstream DNS..."
        _dns_direct=$(_ssh 'nslookup google.com 1.1.1.3 >/dev/null 2>&1 && echo ok || echo fail' 2>/dev/null || echo "fail")
        if [ "${_dns_direct}" = "ok" ]; then
            ok "Direct upstream DNS works (1.1.1.3) — dnsmasq may need a moment"
            DNS_OK=1
        else
            warn "Direct upstream DNS also failed"
        fi
    fi

    # 14e: Report which DNS transport the router selected (DoT/DoH/plaintext).
    # This is set by apply_encrypted_upstream after its latency-gated probing.
    info "Checking DNS upstream protocol..."
    DNS_MODE=$(_ssh 'uci -q get family-safe.resolver.upstream_mode 2>/dev/null' 2>/dev/null || echo "")
    DNS_LAT=$(_ssh 'uci -q get family-safe.resolver.upstream_latency_ms 2>/dev/null' 2>/dev/null || echo "")
    case "${DNS_MODE}" in
        dot)
            ok "DNS protocol: DoT (stubby)${DNS_LAT:+ — avg ${DNS_LAT}ms}" ;;
        doh)
            ok "DNS protocol: DoH (https-dns-proxy)${DNS_LAT:+ — avg ${DNS_LAT}ms}" ;;
        plaintext)
            # DNS still works (filtered family resolvers) — acceptable fallback,
            # not a failure. The on-router guard retries encrypted every 15 min.
            warn "DNS protocol: plaintext (filtered family DNS) — encrypted DoT/DoH unavailable; guard will retry" ;;
        *)
            warn "DNS protocol: unknown (family-safe.resolver.upstream_mode unset) — package may still be settling" ;;
    esac

    # ── Validation summary ──────────────────────────────────────────────
    echo ""
    echo "════════════════════════════════════════════════════════════"
    if [ $FAIL -eq 0 ]; then
        ok "All checks passed — router is fully operational"
    else
        warn "Some checks failed — router may need manual attention"
    fi
    printf "  oaf-update.sh : %s\n" "$([ $UPDATE_OK -eq 0 ] && printf "${_Y}TIMEOUT${_N}" || printf "${_G}OK${_N}")"
    printf "  dnsmasq        : %s\n" "$([ $DNSMASQ_OK -eq 0 ] && printf "${_Y}FAIL${_N}" || printf "${_G}OK${_N}")"
    printf "  WAN gateway    : %s\n" "$([ $GW_REACHABLE -eq 0 ] && printf "${_Y}FAIL${_N}" || printf "${_G}OK${_N}")"
    printf "  DNS resolution : %s\n" "$([ $DNS_OK -eq 0 ] && printf "${_Y}FAIL${_N}" || printf "${_G}OK${_N}")"
    case "${DNS_MODE}" in
        dot|doh)   printf "  DNS protocol   : ${_G}%s${_N}%s\n" "${DNS_MODE}" "${DNS_LAT:+ (${DNS_LAT}ms)}" ;;
        plaintext) printf "  DNS protocol   : ${_Y}plaintext${_N} (encrypted unavailable)\n" ;;
        *)         printf "  DNS protocol   : ${_Y}unknown${_N}\n" ;;
    esac
    echo "════════════════════════════════════════════════════════════"
    echo ""

    # ── Router summary ────────────────────────────────────────────────
    info "Router summary:"
    # Use individual _ssh calls (not heredoc) since _ssh uses -n (no stdin)
    printf "  Model   : %s\n" "$(_ssh 'cat /tmp/sysinfo/model 2>/dev/null')"
    printf "  Firmware: %s\n" "$(_ssh 'grep DISTRIB_DESCRIPTION /etc/openwrt_release | cut -d= -f2 | tr -d "\\x27"')"
    printf "  OAF     : %s\n" "$(_ssh 'apk info appfilter 2>/dev/null | head -1 || opkg list-installed appfilter 2>/dev/null | head -1 || echo not installed')"
    printf "  DNS     : %s\n" "${DNS_MODE:-unknown}${DNS_LAT:+ (${DNS_LAT}ms avg)}"
    printf "  WiFi    : %s / %s\n" \
        "$(_ssh 'uci get wireless.default_radio0.ssid 2>/dev/null')" \
        "$(_ssh 'uci get wireless.default_radio0.encryption 2>/dev/null')"
    echo ""
    ok "Flash complete. Connect to WiFi: Kahf-Router"
else
    warn "Bootstrap did not complete within ${TIMEOUT}s."
    warn "The OAF install may still be running on the router."
    warn "Check logs on the router:"
    warn "  ssh root@${ROUTER_IP} \"logread | grep oaf-bootstrap\""
    exit 1
fi
