#!/bin/sh
# shellcheck shell=dash
# oaf-update.sh — Nightly updater for OAF and family-safe packages
#
# Deployed to the router at /usr/sbin/oaf-update.sh by:
#   - 'make router-install' (from dev machine)
#   - Baked into custom firmware via build-image.sh
#
# Invoked by:
#   - Nightly cron: 0 3 * * * /usr/sbin/oaf-update.sh >> /var/log/oaf-update.log 2>&1
#   - Manually:     /usr/sbin/oaf-update.sh [--force]
#
# POSIX sh — compatible with OpenWrt busybox ash (no bashisms).
# Uses only: wget, awk, grep, sed, uname, logger, opkg (apk fallback)
#
# S3 contract:
#   manifest-oaf.env      (written by OpenAppFilter repo CI)
#   manifest-familysafe.env (written by this repo's CI)
#   packages/             versioned .ipk files
#
# Exit codes:
#   0 — success (or graceful skip due to network/S3 failure)
#   1 — hard error (no package manager found)

# Update channel: "prod" (default) or "dev".
# Override by setting UPDATE_CHANNEL=dev in /etc/oaf-update.conf.
UPDATE_CHANNEL="dev"
if [ -f /etc/oaf-update.conf ]; then
    # shellcheck disable=SC1091
    . /etc/oaf-update.conf
fi
case "${UPDATE_CHANNEL}" in
    dev) S3_BASE="https://s3.kahf.co/router-dev" ;;
    *)   S3_BASE="https://s3.kahf.co/router-prod" ;;
esac
MANIFEST_OAF_URL="${S3_BASE}/manifest-oaf.env"
MANIFEST_FS_URL="${S3_BASE}/manifest-familysafe.env"
TMP_DIR="/tmp/oaf-update"
LOG_TAG="oaf-update"
WGET_TIMEOUT=30
WGET_DOWNLOAD_TIMEOUT=120

FORCE=0
for arg in "$@"; do
    case "$arg" in
        --force|-f) FORCE=1 ;;
    esac
done

# log to both stdout and syslog (cron captures stdout → /var/log/oaf-update.log)
log()  { logger -t "${LOG_TAG}" "$*" 2>/dev/null; echo "$*"; }
warn() { logger -t "${LOG_TAG}" "WARN: $*" 2>/dev/null; echo "[WARN] $*"; }

log "=== OAF+FamilySafe update started: $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="

mkdir -p "${TMP_DIR}"

# ------------------------------------------------------------------
# Detect package manager — opkg preferred (23.05.5/ipk fleet)
# ------------------------------------------------------------------
PKG_MGR=""
if command -v opkg >/dev/null 2>&1; then PKG_MGR="opkg"; fi
if [ -z "${PKG_MGR}" ] && command -v apk >/dev/null 2>&1; then PKG_MGR="apk"; fi

if [ -z "${PKG_MGR}" ]; then
    warn "No package manager found (opkg/apk) — cannot install packages"
    rm -rf "${TMP_DIR}"
    exit 1
fi

log "[pkg] Using package manager: ${PKG_MGR}"

# ------------------------------------------------------------------
# Step 1: Refresh package lists (do NOT full-upgrade — risky on routers)
# ------------------------------------------------------------------
log "[1/3] Refreshing package lists..."

if [ "${PKG_MGR}" = "opkg" ]; then
    if opkg update 2>&1; then
        log "[OK] opkg package lists refreshed"
    else
        warn "opkg update encountered errors (continuing)"
    fi
elif [ "${PKG_MGR}" = "apk" ]; then
    if apk update 2>&1; then
        log "[OK] apk index refreshed"
    else
        warn "apk update encountered errors (continuing)"
    fi
fi

# ------------------------------------------------------------------
# Step 2: Backup /etc/config/appfilter (best-effort, before any changes)
# ------------------------------------------------------------------
if [ -f /etc/config/appfilter ]; then
    cp /etc/config/appfilter "${TMP_DIR}/appfilter.bak" 2>/dev/null \
        && log "[OK] Backed up /etc/config/appfilter" \
        || warn "Failed to back up /etc/config/appfilter (continuing)"
fi

# ------------------------------------------------------------------
# Helper: fetch a manifest from S3.  Returns 1 if unreachable/missing.
# ------------------------------------------------------------------
fetch_manifest() {
    local url="$1"
    local dest="$2"
    if wget -q --timeout="${WGET_TIMEOUT}" -O "${dest}" "${url}" 2>&1; then
        return 0
    fi
    warn "Failed to fetch ${url} (network unavailable or S3 unreachable)"
    return 1
}

# ------------------------------------------------------------------
# Helper: get installed version of a package
# ------------------------------------------------------------------
get_installed_version() {
    local pkg="$1"
    if [ "${PKG_MGR}" = "opkg" ]; then
        opkg list-installed 2>/dev/null | awk -v p="${pkg}" '$1==p{print $3}'
    elif [ "${PKG_MGR}" = "apk" ]; then
        apk info "${pkg}" 2>/dev/null | awk 'NR==1{print $1}' | sed "s/^${pkg}-//"
    fi
}

# ------------------------------------------------------------------
# Helper: download and install a package
#   update_pkg <pkgname> <manifest_ver> <url> [extra_opkg_flags]
# ------------------------------------------------------------------
update_pkg() {
    local pkg="$1"
    local manifest_ver="$2"
    local url="$3"
    local extra_flags="${4:-}"
    local pkg_file
    pkg_file="${TMP_DIR}/$(basename "${url}")"

    installed_ver="$(get_installed_version "${pkg}")"

    if [ "${FORCE}" = "0" ] && [ -n "${installed_ver}" ] && [ "${installed_ver}" = "${manifest_ver}" ]; then
        log "  [=] ${pkg}: ${installed_ver} (up to date)"
        return 0
    fi

    if [ -n "${installed_ver}" ]; then
        log "  [↑] ${pkg}: ${installed_ver} → ${manifest_ver}"
    else
        log "  [+] ${pkg}: installing ${manifest_ver}"
    fi

    if ! wget -q --timeout="${WGET_DOWNLOAD_TIMEOUT}" -O "${pkg_file}" "${url}" 2>&1; then
        warn "Failed to download ${pkg} from ${url}"
        rm -f "${pkg_file}"
        return 1
    fi

    if [ "${PKG_MGR}" = "opkg" ]; then
        # shellcheck disable=SC2086
        if opkg install --force-reinstall ${extra_flags} "${pkg_file}" 2>&1; then
            log "  [OK] ${pkg} installed"
        else
            warn "${pkg} install failed"
        fi
    elif [ "${PKG_MGR}" = "apk" ]; then
        if apk add --allow-untrusted "${pkg_file}" 2>&1; then
            log "  [OK] ${pkg} installed"
        else
            warn "${pkg} install failed"
        fi
    fi

    rm -f "${pkg_file}"
}

# ------------------------------------------------------------------
# Step 3: OAF packages (manifest-oaf.env)
# ------------------------------------------------------------------
log "[2/3] Checking OAF packages from ${MANIFEST_OAF_URL}..."

MANIFEST_OAF="${TMP_DIR}/manifest-oaf.env"
if fetch_manifest "${MANIFEST_OAF_URL}" "${MANIFEST_OAF}"; then
    # Validate manifest has the expected contract key
    if ! grep -q "^APPFILTER_VERSION=" "${MANIFEST_OAF}" 2>/dev/null; then
        warn "manifest-oaf.env appears invalid — skipping OAF update"
    else
        # shellcheck disable=SC1090
        . "${MANIFEST_OAF}"

        log "  OAF manifest: build ${BUILD_COMMIT:-unknown} (${BUILD_DATE:-unknown})"
        log "  kmod:      ${KMOD_VERSION:-?} (kernel ABI: ${KMOD_KERNEL_ABI:-?})"
        log "  appfilter: ${APPFILTER_VERSION:-?}"
        log "  luci:      ${LUCI_VERSION:-?}"

        # kmod guard: compare ABI string from manifest against installed kernel
        # KMOD_KERNEL_ABI from manifest is the exact "kernel (= X)" dep value,
        # e.g. "5.15.167-1-03ba5b5fee..."; the router reports the same via opkg.
        ROUTER_KERNEL_VER="$(opkg list-installed 2>/dev/null | awk '$1=="kernel"{print $3}')"
        if [ -z "${ROUTER_KERNEL_VER}" ] && [ "${PKG_MGR}" = "apk" ]; then
            ROUTER_KERNEL_VER="$(uname -r)"
        fi

        if [ -n "${KMOD_KERNEL_ABI:-}" ] && [ -n "${ROUTER_KERNEL_VER}" ] \
           && [ "${ROUTER_KERNEL_VER}" = "${KMOD_KERNEL_ABI}" ]; then
            update_pkg "kmod-oaf" "${KMOD_VERSION}" "${KMOD_URL}"
        else
            warn "kmod-oaf SKIPPED: kernel ABI mismatch"
            warn "  Router kernel (opkg): ${ROUTER_KERNEL_VER:-unknown}"
            warn "  Manifest KMOD_KERNEL_ABI: ${KMOD_KERNEL_ABI:-not set}"
            warn "  A firmware upgrade is required to install a matching kmod."
        fi

        update_pkg "appfilter"    "${APPFILTER_VERSION}" "${APPFILTER_URL}" "--force-depends"
        update_pkg "luci-app-oaf" "${LUCI_VERSION}"      "${LUCI_URL}"      "--force-depends"
    fi
else
    warn "manifest-oaf.env unavailable — skipping OAF update (exit 0)"
fi

# ------------------------------------------------------------------
# Step 4: family-safe package (manifest-familysafe.env)
# ------------------------------------------------------------------
log "[3/3] Checking family-safe package from ${MANIFEST_FS_URL}..."

MANIFEST_FS="${TMP_DIR}/manifest-familysafe.env"
if fetch_manifest "${MANIFEST_FS_URL}" "${MANIFEST_FS}"; then
    if ! grep -q "^FAMILYSAFE_VERSION=" "${MANIFEST_FS}" 2>/dev/null; then
        warn "manifest-familysafe.env appears invalid — skipping family-safe update"
    else
        # shellcheck disable=SC1090
        . "${MANIFEST_FS}"

        log "  family-safe manifest: build ${BUILD_COMMIT:-unknown} (${BUILD_DATE:-unknown})"
        log "  family-safe: ${FAMILYSAFE_VERSION:-?}"

        # --force-depends: package may depend on dnsmasq-full; avoid blocking
        # if the user has a custom dnsmasq variant that satisfies the function.
        update_pkg "family-safe" "${FAMILYSAFE_VERSION}" "${FAMILYSAFE_URL}" "--force-depends"
    fi
else
    warn "manifest-familysafe.env unavailable — skipping family-safe update (exit 0)"
fi

# ------------------------------------------------------------------
# Cleanup
# ------------------------------------------------------------------
rm -rf "${TMP_DIR}"
log "=== OAF+FamilySafe update finished: $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
exit 0
