#!/bin/sh
# PassWall2 Smart Updater / Installer for OpenWrt
# Version 2.0.1
# - Supports apk and opkg
# - Updates an existing PassWall2 installation
# - Can install PassWall2 from scratch when it is not installed
# - Detects the latest stable PassWall2 GitHub release dynamically
# - Can configure the PassWall binary feeds for dependencies/cores
# - Creates automatic backups only under /tmp (RAM); they disappear after reboot
# - Detects/fixes known legacy tcp/udp_no_redir_ports='disable' migration issue
# - Detects/installs/updates the official Persian PassWall2 language pack
# - Offers Xray, Sing-box, Hysteria and GeoView install/update individually
# - Offers remaining OpenWrt package upgrades only after explicit confirmation

PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH

SCRIPT_VERSION="2.0.1"
PW2_REPO="Openwrt-Passwall/openwrt-passwall2"
PW2_API="https://api.github.com/repos/${PW2_REPO}/releases/latest"
PASSWALL_BUILD_BASE="https://master.dl.sourceforge.net/project/openwrt-passwall-build"
TMP_ROOT="/tmp/passwall2-smart-updater"
BACKUP_DIR="${TMP_ROOT}/backups/passwall2"
SYSTEM_BACKUP_DIR="${TMP_ROOT}/backups/openwrt"
FEED_BACKUP_DIR="${TMP_ROOT}/backups/feeds"
WORK_DIR="${TMP_ROOT}/work.$$"
PM=""
OPENWRT_VERSION="unknown"
OPENWRT_ARCH="unknown"
OPENWRT_RELEASE_BASE="unknown"
OPENWRT_IS_SNAPSHOT=0
LATEST_TAG=""
LATEST_VERSION=""
LATEST_ASSET_URL=""
LATEST_FA_ASSET_URL=""
LATEST_FA_VERSION=""
FRESH_INSTALL=0

say()  { printf '%s\n' "$*"; }
info() { printf '[INFO] %s\n' "$*"; }
ok()   { printf '[ OK ] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
err()  { printf '[ERR ] %s\n' "$*" >&2; }
hr()   { printf '%s\n' '============================================================'; }

cleanup() {
    rm -rf "$WORK_DIR" 2>/dev/null
}
trap cleanup EXIT INT TERM

need_root() {
    if [ "$(id -u 2>/dev/null)" != "0" ]; then
        err "Run this script as root."
        exit 1
    fi
}

ask_yn() {
    prompt="$1"
    def="${2:-n}"
    while :; do
        if [ "$def" = "y" ]; then
            printf '%s [Y/n]: ' "$prompt"
        else
            printf '%s [y/N]: ' "$prompt"
        fi
        IFS= read -r ans
        case "$ans" in
            y|Y|yes|YES|Yes) return 0 ;;
            n|N|no|NO|No) return 1 ;;
            "") [ "$def" = "y" ] && return 0 || return 1 ;;
            *) warn "Please answer y or n." ;;
        esac
    done
}

detect_platform() {
    if command -v apk >/dev/null 2>&1; then
        PM="apk"
    elif command -v opkg >/dev/null 2>&1; then
        PM="opkg"
    else
        err "Neither apk nor opkg was found."
        exit 1
    fi

    if [ -r /etc/openwrt_release ]; then
        . /etc/openwrt_release
        OPENWRT_VERSION="${DISTRIB_RELEASE:-unknown}"
        OPENWRT_ARCH="${DISTRIB_ARCH:-unknown}"
    fi

    case "$OPENWRT_VERSION" in
        *SNAPSHOT*|*snapshot*)
            OPENWRT_IS_SNAPSHOT=1
            OPENWRT_RELEASE_BASE="snapshot"
            ;;
        *)
            OPENWRT_RELEASE_BASE="${OPENWRT_VERSION%.*}"
            [ -n "$OPENWRT_RELEASE_BASE" ] || OPENWRT_RELEASE_BASE="unknown"
            ;;
    esac

    # The PassWall build feed path uses DISTRIB_ARCH. Only fall back when it is absent.
    if [ "$OPENWRT_ARCH" = "unknown" ] || [ -z "$OPENWRT_ARCH" ]; then
        if [ "$PM" = "apk" ]; then
            OPENWRT_ARCH="$(apk --print-arch 2>/dev/null | head -n1)"
        elif command -v opkg >/dev/null 2>&1; then
            OPENWRT_ARCH="$(opkg print-architecture 2>/dev/null | awk 'END{print $2}')"
        fi
        [ -n "$OPENWRT_ARCH" ] || OPENWRT_ARCH="unknown"
    fi
}

fetch_text() {
    url="$1"
    if command -v wget >/dev/null 2>&1; then
        wget -qO- "$url"
    elif command -v uclient-fetch >/dev/null 2>&1; then
        uclient-fetch -q -O - "$url"
    elif command -v curl >/dev/null 2>&1; then
        curl -fsSL "$url"
    else
        err "No downloader found (wget/uclient-fetch/curl)."
        return 127
    fi
}

download_file() {
    url="$1"
    out="$2"
    rm -f "$out"
    if command -v wget >/dev/null 2>&1; then
        wget -O "$out" "$url"
    elif command -v uclient-fetch >/dev/null 2>&1; then
        uclient-fetch -O "$out" "$url"
    elif command -v curl >/dev/null 2>&1; then
        curl -fL "$url" -o "$out"
    else
        err "No downloader found (wget/uclient-fetch/curl)."
        return 127
    fi
}

refresh_repositories() {
    hr
    info "Refreshing package repositories..."
    if [ "$PM" = "apk" ]; then
        apk update
    else
        opkg update
    fi
}

installed_version() {
    pkg="$1"
    if [ "$PM" = "apk" ]; then
        apk list --installed "$pkg" 2>/dev/null \
            | awk -v p="$pkg" '$1 ~ ("^" p "-") {x=$1; sub("^" p "-", "", x); print x; exit}'
    else
        opkg list-installed "$pkg" 2>/dev/null | awk -v p="$pkg" '$1==p {print $3; exit}'
    fi
}

repo_version() {
    pkg="$1"
    if [ "$PM" = "apk" ]; then
        apk list "$pkg" 2>/dev/null \
            | awk -v p="$pkg" '$1 ~ ("^" p "-") {x=$1; sub("^" p "-", "", x); print x; exit}'
    else
        opkg list "$pkg" 2>/dev/null | awk -v p="$pkg" '$1==p {print $3; exit}'
    fi
}

upgrade_candidate_version() {
    pkg="$1"
    if [ "$PM" = "apk" ]; then
        apk list --upgradeable "$pkg" 2>/dev/null \
            | awk -v p="$pkg" '$1 ~ ("^" p "-") {x=$1; sub("^" p "-", "", x); print x; exit}'
    else
        opkg list-upgradable 2>/dev/null | awk -v p="$pkg" '$1==p {print $5; exit}'
    fi
}

pkg_exists_in_repo() {
    pkg="$1"
    [ -n "$(repo_version "$pkg" 2>/dev/null)" ]
}

version_ge_base() {
    # Numeric compare of dotted base version only; ignores suffix such as -r1.
    # Returns success if $1 >= $2.
    awk -v A="$1" -v B="$2" '
    BEGIN {
        sub(/[^0-9.].*$/, "", A)
        sub(/[^0-9.].*$/, "", B)
        na=split(A,a,"."); nb=split(B,b,".")
        n=(na>nb?na:nb)
        for(i=1;i<=n;i++){
            av=(i<=na?a[i]+0:0); bv=(i<=nb?b[i]+0:0)
            if(av>bv) exit 0
            if(av<bv) exit 1
        }
        exit 0
    }'
}

show_storage_hint() {
    avail_kb="$(df -k /overlay 2>/dev/null | awk 'NR==2 {print $4}')"
    [ -n "$avail_kb" ] || avail_kb="$(df -k / 2>/dev/null | awk 'NR==2 {print $4}')"
    if [ -n "$avail_kb" ]; then
        avail_mb=$((avail_kb / 1024))
        say "Free persistent storage : ${avail_mb} MB"
        if [ "$avail_mb" -lt 32 ]; then
            warn "Free persistent storage is low. Core installation may fail due to insufficient space."
        fi
    fi
}

create_passwall_backup() {
    mkdir -p "$BACKUP_DIR" || return 1
    stamp="$(date '+%Y-%m-%d_%H%M%S')"
    archive="${BACKUP_DIR}/passwall2-${stamp}.tar.gz"
    meta="${archive}.meta.txt"

    if [ ! -f /etc/config/passwall2 ]; then
        info "PassWall2 config does not exist yet; no PassWall2 config backup is needed."
        return 0
    fi

    if [ -f /etc/config/passwall2_server ]; then
        tar -czf "$archive" -C / etc/config/passwall2 etc/config/passwall2_server || return 1
    else
        tar -czf "$archive" -C / etc/config/passwall2 || return 1
    fi

    {
        echo "Backup time: $(date)"
        echo "Storage: RAM (/tmp) - automatically deleted after reboot"
        echo "OpenWrt: $OPENWRT_VERSION"
        echo "Architecture: $OPENWRT_ARCH"
        echo "Package manager: $PM"
        echo "PassWall2: $(installed_version luci-app-passwall2 2>/dev/null || echo not-installed)"
        for p in xray-core sing-box hysteria geoview; do
            v="$(installed_version "$p" 2>/dev/null)"
            [ -n "$v" ] || v="not-installed"
            echo "$p: $v"
        done
        echo
        echo "---- UCI passwall2 ----"
        uci show passwall2 2>/dev/null || true
    } > "$meta"

    ok "PassWall2 backup created: $archive"
    info "This backup is in /tmp and will disappear automatically after reboot."
}

backup_feed_config() {
    mkdir -p "$FEED_BACKUP_DIR" || return 1
    stamp="$(date '+%Y-%m-%d_%H%M%S')"
    if [ "$PM" = "apk" ]; then
        src="/etc/apk/repositories.d/customfeeds.list"
        if [ -f "$src" ]; then
            cp "$src" "${FEED_BACKUP_DIR}/customfeeds.list.${stamp}" || return 1
        else
            : > "${FEED_BACKUP_DIR}/customfeeds.list.${stamp}.was-missing"
        fi
    else
        src="/etc/opkg/customfeeds.conf"
        if [ -f "$src" ]; then
            cp "$src" "${FEED_BACKUP_DIR}/customfeeds.conf.${stamp}" || return 1
        else
            : > "${FEED_BACKUP_DIR}/customfeeds.conf.${stamp}.was-missing"
        fi
    fi
    ok "Package-feed configuration backed up in RAM (/tmp)."
}

create_system_backup() {
    mkdir -p "$SYSTEM_BACKUP_DIR" || return 1
    stamp="$(date '+%Y-%m-%d_%H%M%S')"
    archive="${SYSTEM_BACKUP_DIR}/openwrt-config-${stamp}.tar.gz"

    if command -v sysupgrade >/dev/null 2>&1; then
        if sysupgrade -b "$archive"; then
            ok "OpenWrt config backup created: $archive"
            info "This backup is in /tmp and will disappear automatically after reboot."
            return 0
        fi
    fi

    warn "sysupgrade -b failed; creating a minimal /etc/config backup instead."
    tar -czf "$archive" -C / etc/config 2>/dev/null || return 1
    ok "Minimal OpenWrt config backup created: $archive"
    info "This backup is in /tmp and will disappear automatically after reboot."
}

append_unique_line() {
    file="$1"
    line="$2"
    mkdir -p "$(dirname "$file")" || return 1
    touch "$file" || return 1
    if grep -Fqx "$line" "$file" 2>/dev/null; then
        info "Feed already present: $line"
    else
        printf '%s\n' "$line" >> "$file" || return 1
        ok "Added feed: $line"
    fi
}

configure_passwall_feeds() {
    hr
    say "PassWall binary repository setup"
    hr
    say "This adds the OpenWrt-PassWall build repository and signing key."
    say "It is needed for PassWall2 dependencies and optional cores on a clean router."
    say "Feed changes are persistent; a RAM backup of the previous feed file is made first."

    if ! ask_yn "Configure/verify the PassWall package feeds now?" y; then
        warn "Feed setup skipped. A clean PassWall2 installation may fail on missing dependencies."
        return 1
    fi

    backup_feed_config || {
        err "Could not back up the feed configuration."
        return 1
    }

    if [ "$OPENWRT_ARCH" = "unknown" ]; then
        err "Could not determine DISTRIB_ARCH; cannot build a safe feed URL."
        return 1
    fi

    if [ "$PM" = "apk" ]; then
        mkdir -p /etc/apk/keys /etc/apk/repositories.d || return 1
        key_tmp="$WORK_DIR/passwall-apk.pub"
        info "Downloading PassWall APK signing key..."
        download_file "${PASSWALL_BUILD_BASE}/apk.pub" "$key_tmp" || {
            err "Failed to download PassWall APK signing key."
            return 1
        }
        [ -s "$key_tmp" ] || {
            err "Downloaded APK signing key is empty."
            return 1
        }
        cp "$key_tmp" /etc/apk/keys/openwrt-passwall-build.pem || return 1
        ok "PassWall APK signing key installed."

        repo_file="/etc/apk/repositories.d/customfeeds.list"
        if [ "$OPENWRT_IS_SNAPSHOT" -eq 1 ]; then
            feed_root="${PASSWALL_BUILD_BASE}/snapshots/packages/${OPENWRT_ARCH}"
        else
            if [ "$OPENWRT_RELEASE_BASE" = "unknown" ]; then
                err "Could not determine OpenWrt release base for the feed URL."
                return 1
            fi
            feed_root="${PASSWALL_BUILD_BASE}/releases/packages-${OPENWRT_RELEASE_BASE}/${OPENWRT_ARCH}"
        fi

        for feed in passwall_luci passwall_packages passwall2; do
            append_unique_line "$repo_file" "${feed_root}/${feed}/packages.adb" || return 1
        done
    else
        if ! command -v opkg-key >/dev/null 2>&1; then
            err "opkg-key is not available; cannot safely add the signed PassWall feed."
            return 1
        fi
        key_tmp="$WORK_DIR/passwall-ipk.pub"
        info "Downloading PassWall OPKG signing key..."
        download_file "${PASSWALL_BUILD_BASE}/ipk.pub" "$key_tmp" || {
            err "Failed to download PassWall OPKG signing key."
            return 1
        }
        [ -s "$key_tmp" ] || {
            err "Downloaded OPKG signing key is empty."
            return 1
        }
        opkg-key add "$key_tmp" || {
            err "Could not install the PassWall OPKG signing key."
            return 1
        }
        ok "PassWall OPKG signing key installed."

        repo_file="/etc/opkg/customfeeds.conf"
        if [ "$OPENWRT_IS_SNAPSHOT" -eq 1 ]; then
            feed_root="${PASSWALL_BUILD_BASE}/snapshots/packages/${OPENWRT_ARCH}"
        else
            if [ "$OPENWRT_RELEASE_BASE" = "unknown" ]; then
                err "Could not determine OpenWrt release base for the feed URL."
                return 1
            fi
            feed_root="${PASSWALL_BUILD_BASE}/releases/packages-${OPENWRT_RELEASE_BASE}/${OPENWRT_ARCH}"
        fi

        for feed in passwall_luci passwall_packages passwall2; do
            append_unique_line "$repo_file" "src/gz ${feed} ${feed_root}/${feed}" || return 1
        done
    fi

    refresh_repositories || {
        err "Package repository refresh failed after adding PassWall feeds."
        return 1
    }

    if pkg_exists_in_repo geoview && pkg_exists_in_repo tcping; then
        ok "PassWall dependency feed is reachable for this OpenWrt release/architecture."
    else
        err "PassWall feed was added, but expected packages (geoview/tcping) are not visible."
        err "The feed may not support this OpenWrt release or architecture."
        return 1
    fi
    return 0
}

legacy_disable_keys() {
    uci show passwall2 2>/dev/null \
        | grep -E "\.(tcp_no_redir_ports|udp_no_redir_ports)='disable'$" \
        | sed 's/=.*$//'
}

fix_legacy_disable_if_needed() {
    target="$1"

    if ! version_ge_base "$target" "26.8.27"; then
        return 0
    fi

    keys="$(legacy_disable_keys)"
    if [ -z "$keys" ]; then
        ok "No incompatible tcp/udp_no_redir_ports='disable' values found."
        return 0
    fi

    hr
    warn "Legacy PassWall2 settings detected:"
    printf '%s\n' "$keys" | while IFS= read -r k; do
        [ -n "$k" ] && printf '  %s='\''disable'\''\n' "$k"
    done
    warn "These can generate invalid nftables rules such as dport {disable}."

    if ask_yn "Fix only these known legacy values before updating?" y; then
        printf '%s\n' "$keys" | while IFS= read -r k; do
            [ -n "$k" ] || continue
            info "Removing legacy option: $k"
            uci -q delete "$k"
        done
        uci commit passwall2

        left="$(legacy_disable_keys)"
        if [ -n "$left" ]; then
            err "Migration failed; incompatible values remain."
            printf '%s\n' "$left"
            return 1
        fi
        ok "Legacy disable migration completed."
        return 0
    fi

    warn "Migration declined. Updating across the 26.8.27 config change may fail."
    ask_yn "Continue the PassWall2 update anyway?" n
}

discover_latest_passwall() {
    mkdir -p "$WORK_DIR" || return 1
    json="$WORK_DIR/release.json"

    info "Checking the latest stable official PassWall2 GitHub release..."
    if ! fetch_text "$PW2_API" > "$json"; then
        err "Could not query the GitHub release API."
        return 1
    fi

    if command -v jsonfilter >/dev/null 2>&1; then
        LATEST_TAG="$(jsonfilter -i "$json" -e '@.tag_name' 2>/dev/null | head -n1)"
    else
        LATEST_TAG="$(sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' "$json" | head -n1)"
    fi

    if [ "$PM" = "apk" ]; then
        LATEST_ASSET_URL="$(
            grep '"browser_download_url":' "$json" \
            | sed 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/' \
            | grep '/luci-app-passwall2-[^/]*\.apk$' \
            | head -n1
        )"
        if [ -n "$LATEST_ASSET_URL" ]; then
            name="${LATEST_ASSET_URL##*/}"
            LATEST_VERSION="${name#luci-app-passwall2-}"
            LATEST_VERSION="${LATEST_VERSION%.apk}"
        fi
    else
        LATEST_ASSET_URL="$(
            grep '"browser_download_url":' "$json" \
            | sed 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/' \
            | grep '/luci-app-passwall2_[^/]*_all\.ipk$' \
            | head -n1
        )"
        if [ -n "$LATEST_ASSET_URL" ]; then
            name="${LATEST_ASSET_URL##*/}"
            LATEST_VERSION="${name#luci-app-passwall2_}"
            LATEST_VERSION="${LATEST_VERSION%_all.ipk}"
        fi
    fi

    # Fallback to the project's release naming convention, e.g. 26.9.16-1 -> 26.9.16-r1.
    if [ -z "$LATEST_ASSET_URL" ] || [ -z "$LATEST_VERSION" ]; then
        [ -n "$LATEST_TAG" ] || return 1
        base="${LATEST_TAG%-*}"
        rel="${LATEST_TAG##*-}"
        LATEST_VERSION="${base}-r${rel}"
        if [ "$PM" = "apk" ]; then
            asset="luci-app-passwall2-${LATEST_VERSION}.apk"
        else
            asset="luci-app-passwall2_${LATEST_VERSION}_all.ipk"
        fi
        LATEST_ASSET_URL="https://github.com/${PW2_REPO}/releases/download/${LATEST_TAG}/${asset}"
    fi

    # Discover the Persian language package from the same official release.
    LATEST_FA_ASSET_URL=""
    LATEST_FA_VERSION=""
    if [ "$PM" = "apk" ]; then
        LATEST_FA_ASSET_URL="$(
            grep '"browser_download_url":' "$json" \
            | sed 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/' \
            | grep '/luci-i18n-passwall2-fa-[^/]*\.apk$' \
            | head -n1
        )"
        if [ -n "$LATEST_FA_ASSET_URL" ]; then
            fa_name="${LATEST_FA_ASSET_URL##*/}"
            LATEST_FA_VERSION="${fa_name#luci-i18n-passwall2-fa-}"
            LATEST_FA_VERSION="${LATEST_FA_VERSION%.apk}"
        fi
    else
        LATEST_FA_ASSET_URL="$(
            grep '"browser_download_url":' "$json" \
            | sed 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/' \
            | grep '/luci-i18n-passwall2-fa_[^/]*_all\.ipk$' \
            | head -n1
        )"
        if [ -n "$LATEST_FA_ASSET_URL" ]; then
            fa_name="${LATEST_FA_ASSET_URL##*/}"
            LATEST_FA_VERSION="${fa_name#luci-i18n-passwall2-fa_}"
            LATEST_FA_VERSION="${LATEST_FA_VERSION%_all.ipk}"
        fi
    fi

    # Fallback to the release naming convention if the API asset list
    # could not be parsed for the Persian package.
    if [ -z "$LATEST_FA_ASSET_URL" ] && [ -n "$LATEST_TAG" ] && [ -n "$LATEST_VERSION" ]; then
        fa_base="${LATEST_VERSION%-r*}"
        LATEST_FA_VERSION="$fa_base"
        if [ "$PM" = "apk" ]; then
            fa_asset="luci-i18n-passwall2-fa-${LATEST_FA_VERSION}.apk"
        else
            fa_asset="luci-i18n-passwall2-fa_${LATEST_FA_VERSION}_all.ipk"
        fi
        LATEST_FA_ASSET_URL="https://github.com/${PW2_REPO}/releases/download/${LATEST_TAG}/${fa_asset}"
    fi

    [ -n "$LATEST_VERSION" ] && [ -n "$LATEST_ASSET_URL" ]
}

install_latest_passwall() {
    file="$WORK_DIR/${LATEST_ASSET_URL##*/}"

    info "Downloading: $LATEST_ASSET_URL"
    download_file "$LATEST_ASSET_URL" "$file" || {
        err "Download failed."
        return 1
    }
    [ -s "$file" ] || {
        err "Downloaded PassWall2 file is empty."
        return 1
    }

    /etc/init.d/passwall2 stop 2>/dev/null || true
    sleep 2

    if [ "$PM" = "apk" ]; then
        apk add --allow-untrusted "$file" || return 1
    else
        opkg install "$file" || return 1
    fi

    rm -rf /tmp/etc/passwall2 /tmp/etc/passwall2_tmp /tmp/luci-* 2>/dev/null
    /etc/init.d/rpcd restart 2>/dev/null || true

    if [ -x /etc/init.d/passwall2 ]; then
        start_log="$WORK_DIR/passwall2-start.log"
        : > "$start_log"
        /etc/init.d/passwall2 start >"$start_log" 2>&1 || true
        cat "$start_log"

        if grep -Eqi 'dport.*disable|Could not resolve service.*disable' "$start_log"; then
            err "PassWall2 started with a legacy disable/nftables error."
            return 1
        fi
    else
        warn "PassWall2 init script is not present after package installation."
    fi

    ok "PassWall2 package installation stage finished."
    return 0
}

passwall_health_check() {
    hr
    say "PassWall2 health check"
    hr

    v="$(installed_version luci-app-passwall2 2>/dev/null)"
    [ -n "$v" ] && ok "Installed PassWall2: $v" || warn "luci-app-passwall2 package not found."

    if [ -z "$(legacy_disable_keys)" ]; then
        ok "Legacy disable values: clean"
    else
        warn "Legacy disable values still exist:"
        legacy_disable_keys
    fi

    if command -v nft >/dev/null 2>&1 && nft list table inet passwall2 >/dev/null 2>&1; then
        ok "nftables table inet passwall2: present"
    else
        warn "nftables table inet passwall2: not present (PassWall2 may be disabled/not configured yet)."
    fi

    if ps w 2>/dev/null | grep -E '/tmp/etc/passwall2/bin/(xray|sing-box|hysteria)' | grep -v grep >/dev/null 2>&1; then
        ok "PassWall2 proxy core process: running"
    else
        warn "No active xray/sing-box/hysteria process detected under /tmp/etc/passwall2/bin."
        info "On a fresh install this is normal until a node is configured and PassWall2 is enabled."
    fi

    if ps w 2>/dev/null | grep '/tmp/etc/passwall2/bin/dnsmasq' | grep -v grep >/dev/null 2>&1; then
        ok "PassWall2 dedicated dnsmasq: running"
    else
        info "PassWall2 dedicated dnsmasq process not detected. This can be normal before first configuration."
    fi

    if ip rule show 2>/dev/null | grep -q 'lookup 999'; then
        ok "IPv4 policy routing table 999: active"
    else
        info "IPv4 routing table 999 rule not detected; this can be normal when PassWall2 is not active yet."
    fi
}

fresh_install_preflight() {
    hr
    say "PassWall2 clean-install preparation"
    hr
    show_storage_hint
    say "OpenWrt release base : $OPENWRT_RELEASE_BASE"
    say "Feed architecture    : $OPENWRT_ARCH"

    if ! configure_passwall_feeds; then
        warn "PassWall feed setup was not completed."
        if ! ask_yn "Try installing the GitHub PassWall2 package anyway?" n; then
            return 1
        fi
    fi

    return 0
}

passwall_update_stage() {
    hr
    say "PassWall2 Install / Update"
    hr

    current="$(installed_version luci-app-passwall2 2>/dev/null)"
    if [ -n "$current" ]; then
        FRESH_INSTALL=0
    else
        current="not-installed"
        FRESH_INSTALL=1
    fi

    if ! discover_latest_passwall; then
        warn "Could not discover the latest PassWall2 release; skipping PassWall2 stage."
        return 0
    fi

    say "Current PassWall2 : $current"
    say "Latest PassWall2  : $LATEST_VERSION"
    [ -n "$LATEST_TAG" ] && say "GitHub release tag : $LATEST_TAG"

    if [ "$FRESH_INSTALL" -eq 1 ]; then
        say
        warn "PassWall2 is not installed on this router."
        if ! ask_yn "Install the latest PassWall2 ($LATEST_VERSION) from scratch?" n; then
            info "PassWall2 installation skipped."
            return 0
        fi

        if ! fresh_install_preflight; then
            warn "Clean-install preparation was cancelled or failed."
            return 1
        fi

        if install_latest_passwall; then
            after="$(installed_version luci-app-passwall2 2>/dev/null)"
            if [ -n "$after" ]; then
                ok "PassWall2 installed successfully: $after"
            else
                err "Package command completed but luci-app-passwall2 is still not detected."
                return 1
            fi
        else
            err "PassWall2 clean installation failed."
            warn "Do not use force-depends. Review the missing/conflicting dependency shown above."
            return 1
        fi

        # A new config normally does not need migration, but verify anyway.
        if [ -n "$(legacy_disable_keys)" ]; then
            create_passwall_backup || true
            fix_legacy_disable_if_needed "$LATEST_VERSION" || return 1
        fi

        passwall_health_check
        return 0
    fi

    if [ "$current" = "$LATEST_VERSION" ]; then
        ok "PassWall2 is already up to date."
        if [ -n "$(legacy_disable_keys)" ]; then
            create_passwall_backup || return 1
            fix_legacy_disable_if_needed "$LATEST_VERSION" || return 1
            rm -rf /tmp/etc/passwall2 /tmp/etc/passwall2_tmp /tmp/luci-* 2>/dev/null
            /etc/init.d/passwall2 restart 2>/dev/null || true
        fi
        passwall_health_check
        return 0
    fi

    if ! ask_yn "Update PassWall2 from $current to $LATEST_VERSION?" n; then
        info "PassWall2 update skipped."
        return 0
    fi

    create_passwall_backup || {
        err "Backup failed. Update cancelled."
        return 1
    }

    if ! fix_legacy_disable_if_needed "$LATEST_VERSION"; then
        err "Legacy configuration handling failed or was cancelled. Update stopped."
        return 1
    fi

    if install_latest_passwall; then
        after="$(installed_version luci-app-passwall2 2>/dev/null)"
        ok "PassWall2 now installed: ${after:-unknown}"
    else
        err "PassWall2 update failed."
        warn "The RAM backup remains available until reboot under: $BACKUP_DIR"
        return 1
    fi

    passwall_health_check
}

persian_language_stage() {
    hr
    say "PassWall2 Persian Language Pack"
    hr

    if [ -z "$(installed_version luci-app-passwall2 2>/dev/null)" ]; then
        info "PassWall2 is not installed; Persian language-pack stage skipped."
        return 0
    fi

    # Reuse the same latest official GitHub release already checked above.
    if [ -z "$LATEST_FA_ASSET_URL" ] || [ -z "$LATEST_FA_VERSION" ]; then
        if ! discover_latest_passwall; then
            warn "Could not discover the latest official Persian language package."
            return 0
        fi
    fi

    pkg="luci-i18n-passwall2-fa"
    current="$(installed_version "$pkg" 2>/dev/null)"

    if [ -z "$current" ]; then
        say "Persian language pack : not installed"
        say "Latest official       : ${LATEST_FA_VERSION:-unknown}"
        if ! ask_yn "Install Persian translation for PassWall2?" y; then
            info "Persian language-pack installation skipped."
            return 0
        fi
    else
        say "Persian language pack : $current"
        say "Latest official       : ${LATEST_FA_VERSION:-unknown}"

        if [ -n "$LATEST_FA_VERSION" ] && [ "$current" = "$LATEST_FA_VERSION" ]; then
            ok "Persian language pack is already up to date."
            return 0
        fi

        if ! ask_yn "Update the Persian PassWall2 language pack?" y; then
            info "Persian language-pack update skipped."
            return 0
        fi
    fi

    if [ -n "$LATEST_FA_ASSET_URL" ]; then
        fa_file="$WORK_DIR/${LATEST_FA_ASSET_URL##*/}"
        info "Downloading official Persian language package..."
        if ! download_file "$LATEST_FA_ASSET_URL" "$fa_file"; then
            err "Persian language-package download failed."
            return 1
        fi
        if [ ! -s "$fa_file" ]; then
            err "Downloaded Persian language package is empty."
            return 1
        fi

        if [ "$PM" = "apk" ]; then
            apk add --allow-untrusted "$fa_file"
        else
            opkg install "$fa_file"
        fi
        rc=$?
    else
        warn "Official release asset URL was unavailable; trying configured package feeds."
        if [ "$PM" = "apk" ]; then
            apk add "$pkg"
        else
            opkg install "$pkg"
        fi
        rc=$?
    fi

    if [ "$rc" -ne 0 ]; then
        err "Persian language-pack installation/update failed."
        return "$rc"
    fi

    rm -rf /tmp/luci-* 2>/dev/null
    /etc/init.d/rpcd restart 2>/dev/null || true

    newv="$(installed_version "$pkg" 2>/dev/null)"
    if [ -n "$newv" ]; then
        ok "Persian PassWall2 language pack installed: $newv"
        info "If LuCI is already open, refresh the browser page."
        return 0
    fi

    err "Package command completed, but $pkg is not detected as installed."
    return 1
}

install_or_update_core() {
    pkg="$1"
    label="$2"
    default_install="${3:-n}"

    current="$(installed_version "$pkg" 2>/dev/null)"
    if [ -z "$current" ]; then
        available="$(repo_version "$pkg" 2>/dev/null)"
        if [ -z "$available" ]; then
            say "$label ($pkg)"
            say "  Installed : no"
            say "  Available : not found in configured feeds"
            return 0
        fi

        say "$label ($pkg)"
        say "  Installed : no"
        say "  Available : $available"

        if ! ask_yn "Install $label $available?" "$default_install"; then
            info "$label installation skipped."
            return 0
        fi

        if [ -f /etc/config/passwall2 ]; then
            create_passwall_backup || {
                err "Backup failed; $label installation cancelled."
                return 1
            }
        fi

        if [ "$PM" = "apk" ]; then
            apk add "$pkg"
        else
            opkg install "$pkg"
        fi
        rc=$?
        if [ "$rc" -eq 0 ]; then
            newv="$(installed_version "$pkg" 2>/dev/null)"
            ok "$label installed: ${newv:-unknown}"
        else
            err "$label installation failed."
        fi
        return "$rc"
    fi

    candidate="$(upgrade_candidate_version "$pkg" 2>/dev/null)"
    if [ -z "$candidate" ]; then
        say "$label ($pkg)"
        say "  Installed : $current"
        say "  Available : no newer version in configured feeds"
        return 0
    fi

    say "$label ($pkg)"
    say "  Installed : $current"
    say "  Available : $candidate"

    if ! ask_yn "Update $label to $candidate?" n; then
        info "$label update skipped."
        return 0
    fi

    if [ -f /etc/config/passwall2 ]; then
        create_passwall_backup || {
            err "Backup failed; $label update cancelled."
            return 1
        }
    fi

    if [ "$PM" = "apk" ]; then
        apk upgrade "$pkg"
    else
        opkg upgrade "$pkg"
    fi
    rc=$?

    if [ "$rc" -eq 0 ]; then
        newv="$(installed_version "$pkg" 2>/dev/null)"
        ok "$label updated: ${newv:-unknown}"
    else
        err "$label update failed."
    fi
    return "$rc"
}

ensure_core_feeds_if_needed() {
    # If core packages cannot be seen, offer feed setup once.
    if pkg_exists_in_repo xray-core || pkg_exists_in_repo sing-box; then
        return 0
    fi

    warn "PassWall core packages are not visible in the currently configured repositories."
    configure_passwall_feeds
}

core_update_stage() {
    hr
    say "PassWall2 Core Packages"
    hr

    ensure_core_feeds_if_needed || warn "Core feed verification failed; unavailable packages will be skipped."

    if [ "$FRESH_INSTALL" -eq 1 ]; then
        say "Fresh install: at least one main proxy core (Xray or Sing-box) is recommended."
        say
        install_or_update_core "xray-core" "Xray Core" y
        install_or_update_core "sing-box" "Sing-box" n
    else
        install_or_update_core "xray-core" "Xray Core" n
        install_or_update_core "sing-box" "Sing-box" n
    fi

    install_or_update_core "hysteria" "Hysteria" n
    install_or_update_core "geoview" "GeoView" n

    if [ -n "$(installed_version luci-app-passwall2 2>/dev/null)" ]; then
        if ask_yn "Restart PassWall2 now to apply core changes?" y; then
            rm -rf /tmp/etc/passwall2 /tmp/etc/passwall2_tmp 2>/dev/null
            /etc/init.d/passwall2 restart 2>/dev/null || true
            passwall_health_check
        fi
    fi
}

show_upgradeable_packages() {
    if [ "$PM" = "apk" ]; then
        apk list --upgradeable 2>/dev/null
    else
        opkg list-upgradable 2>/dev/null
    fi
}

bulk_openwrt_stage() {
    hr
    say "OpenWrt Package Updates"
    hr

    list_file="$WORK_DIR/openwrt-upgrades.txt"
    show_upgradeable_packages > "$list_file"
    count="$(grep -c . "$list_file" 2>/dev/null)"
    [ -n "$count" ] || count=0

    if [ "$count" -eq 0 ]; then
        ok "No package upgrades are currently listed by $PM."
        return 0
    fi

    say "$count package(s) are listed as upgradeable."
    if ask_yn "Show the complete list?" y; then
        cat "$list_file"
    fi

    say
    warn "Blind bulk OpenWrt package upgrades can break ABI, kernel modules, Wi-Fi or LuCI."
    warn "A firmware/sysupgrade path is generally safer for system-wide upgrades."
    say

    if ! ask_yn "Do you still want to bulk-upgrade ALL listed packages?" n; then
        info "Bulk OpenWrt package upgrade skipped."
        return 0
    fi

    printf 'Type UPGRADE-ALL to confirm: '
    IFS= read -r phrase
    if [ "$phrase" != "UPGRADE-ALL" ]; then
        warn "Confirmation text did not match; bulk upgrade cancelled."
        return 0
    fi

    create_system_backup || {
        err "System configuration backup failed. Bulk upgrade cancelled."
        return 1
    }

    if [ "$PM" = "apk" ]; then
        apk upgrade
    else
        opkg upgrade
    fi
    rc=$?

    if [ "$rc" -eq 0 ]; then
        ok "Bulk package upgrade command completed."
    else
        err "Bulk package upgrade returned exit code $rc."
    fi
    return "$rc"
}

restore_passwall_backup() {
    detect_platform
    hr
    say "PassWall2 RAM Backup Restore"
    hr

    if [ ! -d "$BACKUP_DIR" ]; then
        warn "No PassWall2 RAM backups exist. They are automatically removed after reboot."
        return 1
    fi

    files="$(find "$BACKUP_DIR" -maxdepth 1 -type f -name 'passwall2-*.tar.gz' 2>/dev/null | sort -r)"
    if [ -z "$files" ]; then
        warn "No PassWall2 RAM backups found."
        return 1
    fi

    i=1
    printf '%s\n' "$files" | while IFS= read -r f; do
        [ -n "$f" ] || continue
        printf '%d) %s\n' "$i" "$f"
        i=$((i + 1))
    done

    printf 'Enter backup number to restore, or 0 to cancel: '
    IFS= read -r sel
    case "$sel" in
        ''|*[!0-9]*) warn "Invalid selection."; return 1 ;;
        0) return 0 ;;
    esac

    chosen="$(printf '%s\n' "$files" | sed -n "${sel}p")"
    [ -n "$chosen" ] || {
        warn "Invalid selection."
        return 1
    }

    say "Selected: $chosen"
    if ! ask_yn "Restore this PassWall2 configuration?" n; then
        return 0
    fi

    create_passwall_backup || true
    /etc/init.d/passwall2 stop 2>/dev/null || true
    tar -xzf "$chosen" -C / || {
        err "Restore extraction failed."
        return 1
    }
    uci commit passwall2 2>/dev/null || true
    rm -rf /tmp/etc/passwall2 /tmp/etc/passwall2_tmp /tmp/luci-* 2>/dev/null
    /etc/init.d/passwall2 start 2>/dev/null || true
    ok "PassWall2 configuration restored from RAM backup."
    passwall_health_check
}

main() {
    need_root
    detect_platform
    mkdir -p "$WORK_DIR" "$BACKUP_DIR" "$SYSTEM_BACKUP_DIR" "$FEED_BACKUP_DIR" || exit 1

    if [ "${1:-}" = "--restore" ]; then
        restore_passwall_backup
        exit $?
    fi

    hr
    say "PassWall2 Smart Updater / Installer v${SCRIPT_VERSION}"
    hr
    say "OpenWrt version : $OPENWRT_VERSION"
    say "Architecture    : $OPENWRT_ARCH"
    say "Package manager : $PM"
    say "RAM backups     : $TMP_ROOT"
    say "                  (automatically deleted after reboot)"
    hr

    refresh_repositories || warn "Repository refresh returned an error; continuing where possible."

    passwall_update_stage

    persian_language_stage

    if ask_yn "Check/install/update PassWall2 core packages now?" y; then
        core_update_stage
    else
        info "Core package stage skipped."
    fi

    if ask_yn "Check the remaining OpenWrt package upgrades now?" y; then
        bulk_openwrt_stage
    else
        info "OpenWrt package stage skipped."
    fi

    hr
    say "Finished."
    say "All automatic backups created by this script are under /tmp."
    say "They will be removed automatically on the next reboot."
    say "Restore a PassWall2 config backup before reboot with: $0 --restore"
    hr
}

main "$@"
