#!/bin/ash

check_required_file() {
    local file="$1"

    if [ ! -r "$file" ]; then
        echo "Error: required file '$file' is missing or not readable" >&2
        exit 1
    fi
}

PODKOP_LIB="/usr/lib/podkop-plus"
check_required_file /lib/functions.sh
check_required_file /lib/config/uci.sh
check_required_file /lib/functions/network.sh
check_required_file "$PODKOP_LIB/constants.sh"
check_required_file "$PODKOP_LIB/nft.sh"
check_required_file "$PODKOP_LIB/helpers.sh"
check_required_file "$PODKOP_LIB/json_utils.uc"
check_required_file "$PODKOP_LIB/provider_status.uc"
check_required_file "$PODKOP_LIB/zapret.sh"
check_required_file "$PODKOP_LIB/byedpi.sh"
check_required_file "$PODKOP_LIB/sing_box_config_manager.sh"
check_required_file "$PODKOP_LIB/sing_box_config_manager.uc"
check_required_file "$PODKOP_LIB/sing_box_config_facade.sh"
check_required_file "$PODKOP_LIB/sing_box_config_facade.uc"
check_required_file "$PODKOP_LIB/subscription_cache.uc"
check_required_file "$PODKOP_LIB/subscription_parser.uc"
check_required_file "$PODKOP_LIB/subscription_parser.sh"
check_required_file "$PODKOP_LIB/logging.sh"
check_required_file "$PODKOP_LIB/rulesets.sh"
check_required_file "$PODKOP_LIB/rulesets.uc"
check_required_file "$PODKOP_LIB/updater.sh"
. /lib/functions.sh
. /lib/config/uci.sh
. /lib/functions/network.sh
. "$PODKOP_LIB/constants.sh"
. "$PODKOP_LIB/nft.sh"
. "$PODKOP_LIB/helpers.sh"
. "$PODKOP_LIB/zapret.sh"
. "$PODKOP_LIB/byedpi.sh"
. "$PODKOP_LIB/sing_box_config_manager.sh"
. "$PODKOP_LIB/sing_box_config_facade.sh"
. "$PODKOP_LIB/subscription_parser.sh"
. "$PODKOP_LIB/logging.sh"
. "$PODKOP_LIB/rulesets.sh"
. "$PODKOP_LIB/updater.sh"

PODKOP_INTERNAL_CONFIG_TRIGGER_GUARD="/var/run/podkop-plus.internal-config-change"
PODKOP_RUNTIME_STATE_DIR="/var/run/podkop-plus"
PODKOP_SYSTEM_INFO_CACHE_FILE="$PODKOP_RUNTIME_STATE_DIR/system-info.json"
PODKOP_RELOAD_STATE_FILE="$PODKOP_RUNTIME_STATE_DIR/reload-state"
PODKOP_PENDING_RELOAD_FILE="$PODKOP_RUNTIME_STATE_DIR/reload.pending"
PODKOP_LIST_UPDATE_STATE_FILE="$PODKOP_RUNTIME_STATE_DIR/list-update.timestamp"
PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-update"
PODKOP_SUBSCRIPTION_LINKS_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-links"
PODKOP_SUBSCRIPTION_METADATA_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-metadata"
PODKOP_OUTBOUND_METADATA_DIR="$PODKOP_RUNTIME_STATE_DIR/outbound-metadata"
PODKOP_SECTION_CACHE_DIR="$PODKOP_RUNTIME_STATE_DIR/section-cache"
PODKOP_RUNTIME_CACHE_FORMAT_FILE="$PODKOP_RUNTIME_STATE_DIR/cache-format"
PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE="$PODKOP_RUNTIME_STATE_DIR/subscription-bootstrap-retry.pid"
PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-update.lock"
PODKOP_RELOAD_LOCK_DIR="/var/run/podkop-plus.reload.lock"
PODKOP_LIST_UPDATE_CRON_MARKER="# podkop-plus-list-update"
PODKOP_SUBSCRIPTION_UPDATE_CRON_MARKER="# podkop-plus-subscription-update"
# sing-box falls back to 1d when update_interval is empty, so use a large
# duration to emulate disabled automatic refreshes for remote rule sets.
SING_BOX_DISABLED_UPDATE_INTERVAL="876000h"
SING_BOX_URLTEST_DEFAULT_IDLE_TIMEOUT="30m"
PODKOP_RELOAD_STATE_FORMAT=1
PODKOP_RUNTIME_CACHE_FORMAT=3
PODKOP_RUNTIME_STABLE_MIN_AGE=2
PODKOP_SYSTEM_INFO_CACHE_TTL=3600
PODKOP_NFT_POPULATE_ENABLED=1
PODKOP_ZAPRET_MISSING_WARNING_EMITTED=0
PODKOP_BYEDPI_MISSING_WARNING_EMITTED=0
PODKOP_URLTEST_NEW_ENABLED_SECTIONS=""
PODKOP_URLTEST_SELECTOR_SWITCHES=""
SUBSCRIPTION_DEFERRED_SECTIONS=""
SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS=""

config_load "$PODKOP_CONFIG_NAME"

ensure_runtime_dirs() {
    mkdir -p "$TMP_SING_BOX_FOLDER"
    mkdir -p "$TMP_RULESET_FOLDER"
    mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
    mkdir -p "$PODKOP_RUNTIME_STATE_DIR"
    mkdir -p "$PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR"
    mkdir -p "$PODKOP_SUBSCRIPTION_LINKS_DIR"
    mkdir -p "$PODKOP_SUBSCRIPTION_METADATA_DIR"
    mkdir -p "$PODKOP_OUTBOUND_METADATA_DIR"
    mkdir -p "$PODKOP_SECTION_CACHE_DIR"
}

get_subscription_json_path() {
    local section="$1"

    echo "$TMP_SUBSCRIPTION_FOLDER/${section}.json"
}

get_subscription_url_cache_path() {
    local section="$1"

    echo "$TMP_SUBSCRIPTION_FOLDER/${section}.url"
}

get_subscription_user_agent_cache_path() {
    local section="$1"

    echo "$TMP_SUBSCRIPTION_FOLDER/${section}.user_agent"
}

get_subscription_update_timestamp_path() {
    local section="$1"

    echo "$PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR/${section}.timestamp"
}

get_subscription_links_path() {
    local section="$1"

    echo "$PODKOP_SUBSCRIPTION_LINKS_DIR/${section}.json"
}

get_subscription_metadata_path() {
    local section="$1"

    echo "$PODKOP_SUBSCRIPTION_METADATA_DIR/${section}.json"
}

get_outbound_metadata_path() {
    local section="$1"

    echo "$PODKOP_OUTBOUND_METADATA_DIR/${section}.json"
}

get_section_cache_path() {
    local section="$1"

    echo "$PODKOP_SECTION_CACHE_DIR/${section}.json"
}

clear_subscription_runtime_cache() {
    rm -rf \
        "$TMP_SUBSCRIPTION_FOLDER" \
        "$PODKOP_SUBSCRIPTION_LINKS_DIR" \
        "$PODKOP_SUBSCRIPTION_METADATA_DIR" \
        "$PODKOP_OUTBOUND_METADATA_DIR" \
        "$PODKOP_SECTION_CACHE_DIR"
}

ensure_runtime_cache_format() {
    local current_format

    mkdir -p "$PODKOP_RUNTIME_STATE_DIR"
    current_format="$(sed -n '1p' "$PODKOP_RUNTIME_CACHE_FORMAT_FILE" 2>/dev/null)"

    if [ "$current_format" != "$PODKOP_RUNTIME_CACHE_FORMAT" ]; then
        log "Runtime subscription cache format changed; clearing old subscription cache" "info"
        clear_subscription_runtime_cache
        ensure_runtime_dirs
        printf '%s\n' "$PODKOP_RUNTIME_CACHE_FORMAT" > "$PODKOP_RUNTIME_CACHE_FORMAT_FILE"
    fi
}

remove_legacy_server_country_cache() {
    rm -f "$PODKOP_RUNTIME_STATE_DIR/server-country-cache.json"
}

subscription_cache_ucode() {
    ucode "$PODKOP_LIB/subscription_cache.uc" "$@"
}

subscription_cache_tmpfile() {
    local section="$1"
    local kind="$2"

    mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.${kind}.XXXXXX" 2>/dev/null
}

write_subscription_outbound_link_cache() {
    local section="$1"
    local links_json="$2"
    local link_refs_json="${3:-}"
    local links_tmp link_refs_tmp status

    ensure_runtime_dirs
    [ -n "$links_json" ] || links_json="{}"
    [ -n "$link_refs_json" ] || link_refs_json="{}"

    links_tmp="$(subscription_cache_tmpfile "$section" "links")" || return 1
    link_refs_tmp="$(subscription_cache_tmpfile "$section" "link-refs")" || {
        rm -f "$links_tmp"
        return 1
    }

    printf '%s' "$links_json" > "$links_tmp" || {
        rm -f "$links_tmp" "$link_refs_tmp"
        return 1
    }
    printf '%s' "$link_refs_json" > "$link_refs_tmp" || {
        rm -f "$links_tmp" "$link_refs_tmp"
        return 1
    }

    subscription_cache_ucode write-link-cache \
        "$PODKOP_SECTION_CACHE_DIR" "$PODKOP_RUNTIME_CACHE_FORMAT" "$section" \
        "$links_tmp" "$link_refs_tmp"
    status=$?
    rm -f "$links_tmp" "$link_refs_tmp"
    return "$status"
}

write_outbound_metadata() {
    local section="$1"
    local names_json="$2"
    local countries_json="$3"
    local servers_json="${4:-}"
    local names_tmp countries_tmp servers_tmp status

    ensure_runtime_dirs
    [ -n "$names_json" ] || names_json="{}"
    [ -n "$countries_json" ] || countries_json="{}"
    [ -n "$servers_json" ] || servers_json="{}"

    names_tmp="$(subscription_cache_tmpfile "$section" "names")" || return 1
    countries_tmp="$(subscription_cache_tmpfile "$section" "countries")" || {
        rm -f "$names_tmp"
        return 1
    }
    servers_tmp="$(subscription_cache_tmpfile "$section" "servers")" || {
        rm -f "$names_tmp" "$countries_tmp"
        return 1
    }

    printf '%s' "$names_json" > "$names_tmp" || {
        rm -f "$names_tmp" "$countries_tmp" "$servers_tmp"
        return 1
    }
    printf '%s' "$countries_json" > "$countries_tmp" || {
        rm -f "$names_tmp" "$countries_tmp" "$servers_tmp"
        return 1
    }
    printf '%s' "$servers_json" > "$servers_tmp" || {
        rm -f "$names_tmp" "$countries_tmp" "$servers_tmp"
        return 1
    }

    subscription_cache_ucode write-outbound-metadata \
        "$PODKOP_SECTION_CACHE_DIR" "$PODKOP_RUNTIME_CACHE_FORMAT" "$section" \
        "$names_tmp" "$countries_tmp" "$servers_tmp"
    status=$?
    rm -f "$names_tmp" "$countries_tmp" "$servers_tmp"
    return "$status"
}

subscription_metadata_append_file() {
    local array_path="$1"
    local metadata_path="$2"
    local source_index="${3:-0}"
    local source_section="${4:-}"

    [ -n "$array_path" ] || return 0
    [ -n "$metadata_path" ] || return 0

    subscription_cache_ucode append-metadata-file "$array_path" "$metadata_path" "$source_index" "$source_section" || true
}

subscription_metadata_append_cached_source() {
    local array_path="$1"
    local section="$2"
    local source_index="${3:-0}"
    local source_section="${4:-}"
    local legacy_target_path

    [ -n "$array_path" ] || return 0
    [ -n "$section" ] || return 0

    legacy_target_path="$(get_subscription_metadata_path "$section")"
    subscription_cache_ucode append-cached-metadata \
        "$array_path" "$PODKOP_SECTION_CACHE_DIR" "$section" "$legacy_target_path" \
        "$source_index" "$source_section"
}

write_subscription_metadata_json() {
    local section="$1"
    local metadata_json_path="$2"

    ensure_runtime_dirs
    [ -n "$metadata_json_path" ] || metadata_json_path="/dev/null"

    subscription_cache_ucode write-subscription-metadata \
        "$PODKOP_SECTION_CACHE_DIR" "$PODKOP_RUNTIME_CACHE_FORMAT" "$section" "$metadata_json_path"
}

write_subscription_source_metadata_json() {
    local section="$1"
    local source_index="$2"
    local source_section="$3"
    local metadata_json_path="$4"
    local legacy_target_path

    ensure_runtime_dirs
    [ -n "$metadata_json_path" ] || metadata_json_path="/dev/null"
    legacy_target_path="$(get_subscription_metadata_path "$section")"

    subscription_cache_ucode write-source-metadata \
        "$PODKOP_SECTION_CACHE_DIR" "$PODKOP_RUNTIME_CACHE_FORMAT" "$section" \
        "$source_index" "$source_section" "$metadata_json_path" "$legacy_target_path"
}

trim_string() {
    sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}

subscription_source_id() {
    local section="$1"
    local index="$2"

    printf '%s-subscription-%s\n' "$section" "$index"
}

subscription_mark_unavailable_source() {
    local source_section="$1"

    subscription_source_is_marked_unavailable "$source_section" && return 0
    SUBSCRIPTION_UNAVAILABLE_SOURCES="${SUBSCRIPTION_UNAVAILABLE_SOURCES}
${source_section}"
}

subscription_source_is_marked_unavailable() {
    local source_section="$1"

    case "
${SUBSCRIPTION_UNAVAILABLE_SOURCES}
" in
    *"
${source_section}
"*)
        return 0
        ;;
    esac

    return 1
}

parse_subscription_source_entry() {
    local entry="$1"
    local delimiter=" | "
    local url user_agent

    SUBSCRIPTION_SOURCE_URL=""
    SUBSCRIPTION_SOURCE_USER_AGENT=""
    SUBSCRIPTION_SOURCE_PARSE_ERROR=""

    entry="$(printf '%s' "$entry" | trim_string)"
    if [ -z "$entry" ]; then
        SUBSCRIPTION_SOURCE_PARSE_ERROR="Subscription URL cannot be empty"
        return 1
    fi

    case "$entry" in
    *"$delimiter"*)
        url="${entry%$delimiter*}"
        user_agent="${entry##*$delimiter}"
        url="$(printf '%s' "$url" | trim_string)"
        user_agent="$(printf '%s' "$user_agent" | trim_string)"
        if [ -z "$url" ] || [ -z "$user_agent" ]; then
            SUBSCRIPTION_SOURCE_PARSE_ERROR="Subscription User-Agent separator requires non-empty URL and User-Agent"
            return 1
        fi
        ;;
    *" |"* | *"| "*)
        SUBSCRIPTION_SOURCE_PARSE_ERROR="Use 'URL | User-Agent' with spaces on both sides of the separator"
        return 1
        ;;
    *)
        url="$entry"
        user_agent=""
        ;;
    esac

    case "$url" in
    http://* | https://*) ;;
    *)
        SUBSCRIPTION_SOURCE_PARSE_ERROR="Subscription URL must start with http:// or https://"
        return 1
        ;;
    esac

    SUBSCRIPTION_SOURCE_URL="$url"
    SUBSCRIPTION_SOURCE_USER_AGENT="$user_agent"
    return 0
}

for_each_subscription_source_dispatch() {
    local entry="$1"
    local section="$2"
    local callback="$3"
    shift 3

    SUBSCRIPTION_SOURCE_INDEX=$((SUBSCRIPTION_SOURCE_INDEX + 1))
    "$callback" "$section" "$SUBSCRIPTION_SOURCE_INDEX" "$entry" "$@"
}

for_each_subscription_source() {
    local section="$1"
    local callback="$2"
    local subscription_urls
    shift 2

    SUBSCRIPTION_SOURCE_INDEX=0
    config_get subscription_urls "$section" "subscription_urls"
    [ -n "$subscription_urls" ] || return 0

    config_list_foreach "$section" "subscription_urls" for_each_subscription_source_dispatch "$section" "$callback" "$@"
}

rule_has_subscription_urls() {
    local section="$1"
    local subscription_urls

    config_get subscription_urls "$section" "subscription_urls"

    [ -n "$subscription_urls" ]
}

subscription_config_match_handler() {
    local entry="$1"
    local expected_url="$2"
    local expected_user_agent="$3"

    parse_subscription_source_entry "$entry" >/dev/null 2>&1 || return 0
    if [ "$SUBSCRIPTION_SOURCE_URL" = "$expected_url" ] &&
        [ "$SUBSCRIPTION_SOURCE_USER_AGENT" = "$expected_user_agent" ]; then
        SUBSCRIPTION_CONFIG_MATCH_FOUND=1
    fi
}

subscription_config_is_current() {
    local section="$1"
    local subscription_url="$2"
    local subscription_user_agent="${3:-}"
    local subscription_urls

    config_get subscription_urls "$section" "subscription_urls"
    [ -n "$subscription_urls" ] || return 1

    SUBSCRIPTION_CONFIG_MATCH_FOUND=0
    config_list_foreach "$section" "subscription_urls" subscription_config_match_handler "$subscription_url" "$subscription_user_agent"
    [ "$SUBSCRIPTION_CONFIG_MATCH_FOUND" -eq 1 ]
}

subscription_cache_is_usable() {
    local subscription_json_path="$1"

    [ -s "$subscription_json_path" ] || return 1

    validate_subscription_file "$subscription_json_path"
}

subscription_append_word_once() {
    local list="$1"
    local word="$2"

    [ -n "$word" ] || {
        printf '%s\n' "$list"
        return 0
    }

    list_has_word "$list" "$word" && {
        printf '%s\n' "$list"
        return 0
    }

    printf '%s\n' "${list}${list:+ }${word}"
}

subscription_mark_startup_blocked_section() {
    local section="$1"

    SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS="$(subscription_append_word_once "$SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS" "$section")"
}

subscription_startup_blocked_section_is_marked() {
    local section="$1"

    list_has_word "$SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS" "$section"
}

subscription_mark_deferred_section() {
    local section="$1"

    SUBSCRIPTION_DEFERRED_SECTIONS="$(subscription_append_word_once "$SUBSCRIPTION_DEFERRED_SECTIONS" "$section")"
}

subscription_section_is_deferred() {
    local section="$1"

    list_has_word "$SUBSCRIPTION_DEFERRED_SECTIONS" "$section"
}

subscription_section_usable_cache_handler() {
    local section="$1"
    local index="$2"
    local entry="$3"
    local source_section subscription_json_path subscription_url_cache_path subscription_user_agent_cache_path \
        cached_subscription_url cached_subscription_user_agent

    [ "$SUBSCRIPTION_SECTION_USABLE_CACHE_FOUND" -eq 0 ] || return 0
    parse_subscription_source_entry "$entry" >/dev/null 2>&1 || return 0

    source_section="$(subscription_source_id "$section" "$index")"
    subscription_json_path="$(get_subscription_json_path "$source_section")"
    subscription_url_cache_path="$(get_subscription_url_cache_path "$source_section")"
    subscription_user_agent_cache_path="$(get_subscription_user_agent_cache_path "$source_section")"

    subscription_cache_is_usable "$subscription_json_path" || return 0
    cached_subscription_url="$(cat "$subscription_url_cache_path" 2>/dev/null)"
    cached_subscription_user_agent="$(cat "$subscription_user_agent_cache_path" 2>/dev/null)"

    if [ "$cached_subscription_url" = "$SUBSCRIPTION_SOURCE_URL" ] &&
        [ "$cached_subscription_user_agent" = "$SUBSCRIPTION_SOURCE_USER_AGENT" ]; then
        SUBSCRIPTION_SECTION_USABLE_CACHE_FOUND=1
    fi
}

subscription_section_has_current_usable_cache() {
    local section="$1"

    rule_has_subscription_urls "$section" || return 1

    SUBSCRIPTION_SECTION_USABLE_CACHE_FOUND=0
    for_each_subscription_source "$section" subscription_section_usable_cache_handler
    [ "$SUBSCRIPTION_SECTION_USABLE_CACHE_FOUND" -eq 1 ]
}

subscription_bootstrap_download_section_is_ready() {
    local download_lists_via_proxy download_lists_via_proxy_section action selector_proxy_links outbound_json interface_name

    config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
    [ "$download_lists_via_proxy" -eq 1 ] || {
        log "Subscription startup cannot be deferred because Download lists/updates/subscriptions via Proxy/VPN is disabled" "error"
        return 1
    }

    config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
    [ -n "$download_lists_via_proxy_section" ] || return 1

    if subscription_startup_blocked_section_is_marked "$download_lists_via_proxy_section"; then
        log "Subscription startup cannot be deferred because selected download rule '$download_lists_via_proxy_section' is also waiting for an unavailable subscription cache" "error"
        return 1
    fi

    rule_is_enabled "$download_lists_via_proxy_section" || return 1

    action="$(get_rule_action "$download_lists_via_proxy_section")"
    case "$action" in
    proxy)
        config_get selector_proxy_links "$download_lists_via_proxy_section" "selector_proxy_links"
        if [ -n "$selector_proxy_links" ]; then
            return 0
        fi

        if subscription_section_has_current_usable_cache "$download_lists_via_proxy_section"; then
            return 0
        fi

        log "Subscription startup cannot be deferred because selected proxy rule '$download_lists_via_proxy_section' has no manual proxy links and no usable subscription cache" "error"
        return 1
        ;;
    outbound)
        config_get outbound_json "$download_lists_via_proxy_section" "outbound_json"
        [ -n "$outbound_json" ] || return 1
        return 0
        ;;
    vpn)
        config_get interface_name "$download_lists_via_proxy_section" "interface"
        [ -n "$interface_name" ] || return 1
        return 0
        ;;
    *)
        return 1
        ;;
    esac
}

subscription_defer_startup_blocked_sections() {
    local section

    for section in $SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS; do
        subscription_mark_deferred_section "$section"
    done
}

subscription_log_startup_blocked_sections() {
    local section

    for section in $SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS; do
        log "No usable subscription cache for rule '$section' and no manual proxy links are configured; startup cannot continue" "error"
    done
}

get_subscription_download_proxy_address() {
    local section="$1"
    local had_usable_cache="${2:-0}"
    local phase="${3:-runtime}"
    local service_proxy_address download_lists_via_proxy_section selector_proxy_links

    service_proxy_address="$(get_service_proxy_address)"
    [ -n "$service_proxy_address" ] || return 0

    if ! sing_box_service_is_running; then
        if [ "$phase" = "startup" ]; then
            log "download_lists_via_proxy is enabled, but sing-box is not running yet; bootstrapping subscription for rule '$section' directly" "info"
        else
            log "download_lists_via_proxy is enabled, but sing-box service proxy is not running; downloading subscription for rule '$section' directly" "warn"
        fi
        return 0
    fi

    config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
    if [ -n "$download_lists_via_proxy_section" ] &&
        [ "$section" = "$download_lists_via_proxy_section" ] &&
        [ "$had_usable_cache" -eq 0 ]; then
        config_get selector_proxy_links "$section" "selector_proxy_links"
        if [ -z "$selector_proxy_links" ]; then
            log "Rule '$section' is selected for downloading external resources but has no usable subscription cache; downloading it directly to avoid a bootstrap loop" "warn"
            return 0
        fi
    fi

    log "Downloading subscription for rule '$section' via service proxy $service_proxy_address" "debug"
    printf '%s\n' "$service_proxy_address"
}

download_subscription_into_cache() {
    local section="$1"
    local subscription_url="$2"
    local subscription_json_path="$3"
    local subscription_url_cache_path="$4"
    local service_proxy_address="$5"
    local subscription_user_agent="${6:-}"
    local cache_section="${7:-$section}"
    local metadata_output_path="${8:-}"
    local subscription_user_agent_cache_path raw_tmpfile headers_tmpfile normalized_tmpfile metadata_tmpfile

    mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
    subscription_user_agent_cache_path="$(get_subscription_user_agent_cache_path "$cache_section")"
    raw_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${cache_section}.download.XXXXXX")" || return 1
    headers_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${cache_section}.headers.XXXXXX")" || {
        rm -f "$raw_tmpfile"
        return 1
    }
    normalized_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${cache_section}.normalized.XXXXXX")" || {
        rm -f "$raw_tmpfile" "$headers_tmpfile"
        return 1
    }
    metadata_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${cache_section}.metadata.XXXXXX")" || {
        rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile"
        return 1
    }

    if ! download_subscription "$subscription_url" "$raw_tmpfile" "$service_proxy_address" 3 2 15 "$headers_tmpfile" "$subscription_user_agent"; then
        [ -n "$metadata_output_path" ] && rm -f "$metadata_output_path"
        rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
        return 1
    fi

    subscription_extract_ui_metadata "$headers_tmpfile" "$raw_tmpfile" "$metadata_tmpfile" >/dev/null 2>&1 || rm -f "$metadata_tmpfile"
    if [ -n "$metadata_output_path" ]; then
        if [ -s "$metadata_tmpfile" ] && json_utils_ucode object-has-extra-keys "$metadata_tmpfile" >/dev/null 2>&1; then
            cp "$metadata_tmpfile" "$metadata_output_path" || rm -f "$metadata_output_path"
        else
            rm -f "$metadata_output_path"
        fi
    fi

    if ! normalize_subscription_file "$raw_tmpfile" "$normalized_tmpfile" "$section"; then
        log "Downloaded subscription for rule '$section' is invalid" "error"
        [ -n "$metadata_output_path" ] && rm -f "$metadata_output_path"
        rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
        return 1
    fi

    if ! validate_subscription_file "$normalized_tmpfile"; then
        log "Normalized subscription for rule '$section' is invalid" "error"
        [ -n "$metadata_output_path" ] && rm -f "$metadata_output_path"
        rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
        return 1
    fi

    if ! subscription_config_is_current "$section" "$subscription_url" "$subscription_user_agent"; then
        log "Subscription source settings changed while updating rule '$section'; discarding superseded download" "warn"
        [ -n "$metadata_output_path" ] && rm -f "$metadata_output_path"
        rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
        return 4
    fi

    if [ -f "$subscription_json_path" ] && cmp -s "$normalized_tmpfile" "$subscription_json_path"; then
        rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
        printf '%s' "$subscription_url" > "$subscription_url_cache_path"
        printf '%s' "$subscription_user_agent" > "$subscription_user_agent_cache_path"
        log "Subscription for rule '$section' is unchanged" "info"
        return 2
    fi

    mv "$normalized_tmpfile" "$subscription_json_path" || {
        [ -n "$metadata_output_path" ] && rm -f "$metadata_output_path"
        rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
        return 1
    }

    rm -f "$raw_tmpfile" "$headers_tmpfile" "$metadata_tmpfile"
    printf '%s' "$subscription_url" > "$subscription_url_cache_path"
    printf '%s' "$subscription_user_agent" > "$subscription_user_agent_cache_path"
    return 0
}

rule_is_subscription_proxy() {
    local section="$1"
    local action

    rule_is_enabled "$section" || return 1

    action="$(get_rule_action "$section")"
    [ "$action" = "proxy" ] || return 1

    rule_has_subscription_urls "$section"
}

ensure_subscription_cache_for_source() {
    local section="$1"
    local source_section="$2"
    local subscription_url="$3"
    local subscription_user_agent="$4"
    local fatal_on_error="${5:-0}"
    local source_index="${6:-0}"
    local download_phase="${7:-runtime}"
    local subscription_json_path subscription_url_cache_path subscription_user_agent_cache_path metadata_output_path \
        cached_subscription_url cached_subscription_user_agent
    local had_usable_cache cache_needs_refresh service_proxy_address update_result

    if [ -z "$subscription_url" ]; then
        log "Subscription URL is not set for rule '$section'. Aborted." "fatal"
        exit 1
    fi

    mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
    subscription_json_path="$(get_subscription_json_path "$source_section")"
    subscription_url_cache_path="$(get_subscription_url_cache_path "$source_section")"
    subscription_user_agent_cache_path="$(get_subscription_user_agent_cache_path "$source_section")"
    cached_subscription_url=""
    cached_subscription_user_agent=""
    had_usable_cache=0
    cache_needs_refresh=0

    if subscription_cache_is_usable "$subscription_json_path"; then
        had_usable_cache=1
    else
        rm -f "$subscription_json_path"
    fi

    if [ -f "$subscription_url_cache_path" ]; then
        cached_subscription_url="$(cat "$subscription_url_cache_path" 2> /dev/null)"
    fi

    if [ -f "$subscription_user_agent_cache_path" ]; then
        cached_subscription_user_agent="$(cat "$subscription_user_agent_cache_path" 2> /dev/null)"
    fi

    if [ "$had_usable_cache" -eq 0 ] ||
        [ "$cached_subscription_url" != "$subscription_url" ] ||
        [ "$cached_subscription_user_agent" != "$subscription_user_agent" ]; then
        cache_needs_refresh=1
    fi

    if [ "$cache_needs_refresh" -eq 0 ]; then
        if [ -n "$SUBSCRIPTION_SECTION_METADATA_TMP" ]; then
            if subscription_metadata_append_cached_source "$SUBSCRIPTION_SECTION_METADATA_TMP" "$section" "$source_index" "$source_section"; then
                return 0
            fi

            return 0
        else
            return 0
        fi
    fi

    metadata_output_path=""
    if [ -n "$SUBSCRIPTION_SECTION_METADATA_TMP" ]; then
        metadata_output_path="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${source_section}.metadata-output.XXXXXX" 2>/dev/null || true)"
    fi

    service_proxy_address="$(get_subscription_download_proxy_address "$section" "$had_usable_cache" "$download_phase")"

    download_subscription_into_cache \
        "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" "$subscription_user_agent" "$source_section" "$metadata_output_path"
    update_result="$?"
    case "$update_result" in
    0 | 2)
        subscription_metadata_append_file "$SUBSCRIPTION_SECTION_METADATA_TMP" "$metadata_output_path" "$source_index" "$source_section"
        rm -f "$metadata_output_path"
        return 0
        ;;
    esac

    rm -f "$metadata_output_path"

    if [ "$had_usable_cache" -eq 1 ] &&
        [ "$cached_subscription_url" = "$subscription_url" ] &&
        [ "$cached_subscription_user_agent" = "$subscription_user_agent" ]; then
        log "Keeping cached subscription for rule '$section' until a fresh download succeeds" "warn"
        subscription_metadata_append_cached_source "$SUBSCRIPTION_SECTION_METADATA_TMP" "$section" "$source_index" "$source_section" || true
        return 0
    fi

    if [ "$fatal_on_error" -eq 1 ]; then
        log "Failed to download subscription for rule '$section'. Aborted." "fatal"
        exit 1
    fi

    log "No usable subscription cache for rule '$section'" "warn"
    return 1
}

prepare_subscription_cache_source() {
    local section="$1"
    local index="$2"
    local entry="$3"
    local source_section

    SUBSCRIPTION_SECTION_SOURCE_TOTAL=$((SUBSCRIPTION_SECTION_SOURCE_TOTAL + 1))

    if ! parse_subscription_source_entry "$entry"; then
        log "Invalid subscription source for rule '$section': $SUBSCRIPTION_SOURCE_PARSE_ERROR" "error"
        subscription_startup_blocked=1
        return 0
    fi

    source_section="$(subscription_source_id "$section" "$index")"
    if ensure_subscription_cache_for_source \
        "$section" "$source_section" "$SUBSCRIPTION_SOURCE_URL" "$SUBSCRIPTION_SOURCE_USER_AGENT" 0 "$index" "startup"; then
        SUBSCRIPTION_SECTION_SOURCE_READY=$((SUBSCRIPTION_SECTION_SOURCE_READY + 1))
    else
        SUBSCRIPTION_SECTION_SOURCE_FAILED=$((SUBSCRIPTION_SECTION_SOURCE_FAILED + 1))
        subscription_mark_unavailable_source "$source_section"
    fi
}

prepare_subscription_cache_for_startup() {
    local section="$1"
    local metadata_tmpfile metadata_count selector_proxy_links

    rule_is_subscription_proxy "$section" || return 0

    mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
    metadata_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.metadata-section.XXXXXX" 2>/dev/null || true)"
    if [ -n "$metadata_tmpfile" ]; then
        printf '[]\n' > "$metadata_tmpfile"
        SUBSCRIPTION_SECTION_METADATA_TMP="$metadata_tmpfile"
    else
        SUBSCRIPTION_SECTION_METADATA_TMP=""
    fi

    SUBSCRIPTION_SECTION_SOURCE_TOTAL=0
    SUBSCRIPTION_SECTION_SOURCE_READY=0
    SUBSCRIPTION_SECTION_SOURCE_FAILED=0
    for_each_subscription_source "$section" prepare_subscription_cache_source

    if [ "$SUBSCRIPTION_SECTION_SOURCE_TOTAL" -gt 0 ] &&
        [ "$SUBSCRIPTION_SECTION_SOURCE_READY" -eq 0 ]; then
        config_get selector_proxy_links "$section" "selector_proxy_links"
        if [ -n "$selector_proxy_links" ]; then
            log "All subscription sources for rule '$section' are unavailable; starting with manual proxy links only" "warn"
        else
            log "No usable subscription cache for rule '$section' and no manual proxy links are configured; checking whether startup can be bootstrapped through the selected proxy/VPN rule" "warn"
            subscription_mark_startup_blocked_section "$section"
        fi
    elif [ "$SUBSCRIPTION_SECTION_SOURCE_FAILED" -gt 0 ]; then
        log "Skipping unavailable subscription source(s) for rule '$section'; using available outbounds" "warn"
    fi

    if [ -n "$metadata_tmpfile" ]; then
        metadata_count="$(json_utils_ucode length "$metadata_tmpfile" 2>/dev/null)"
        if [ -n "$metadata_count" ] && [ "$metadata_count" -gt 0 ]; then
            write_subscription_metadata_json "$section" "$metadata_tmpfile"
        else
            write_subscription_metadata_json "$section" ""
        fi
        rm -f "$metadata_tmpfile"
    fi
    SUBSCRIPTION_SECTION_METADATA_TMP=""
}

prepare_subscription_caches_for_startup() {
    subscription_startup_blocked=0
    SUBSCRIPTION_UNAVAILABLE_SOURCES=""
    SUBSCRIPTION_DEFERRED_SECTIONS=""
    SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS=""
    config_foreach prepare_subscription_cache_for_startup "section"

    [ "$subscription_startup_blocked" -eq 0 ] || return 1
    [ -n "$SUBSCRIPTION_STARTUP_BLOCKED_SECTIONS" ] || return 0

    if subscription_bootstrap_download_section_is_ready; then
        subscription_defer_startup_blocked_sections
        log "Starting temporarily without subscription-only rule(s): $SUBSCRIPTION_DEFERRED_SECTIONS. They will be retried through the service proxy after sing-box starts" "warn"
        return 0
    fi

    subscription_log_startup_blocked_sections
    return 1
}

wait_for_subscription_bootstrap_service_proxy() {
    local attempt

    attempt=1
    while [ "$attempt" -le 10 ]; do
        if sing_box_service_is_running; then
            return 0
        fi

        sleep 1
        attempt=$((attempt + 1))
    done

    return 1
}

retry_deferred_subscription_section() {
    local section="$1"
    local result

    if ! rule_is_subscription_proxy "$section"; then
        log "Deferred subscription rule '$section' no longer has subscription proxy sources; removing it from bootstrap retry list" "warn"
        return 0
    fi

    log "Retrying deferred subscription rule '$section' through the service proxy" "info"
    subscription_update_section "$section" 1
    result="$?"

    case "$result" in
    0 | 2)
        return 0
        ;;
    esac

    if subscription_section_has_current_usable_cache "$section"; then
        log "Deferred subscription rule '$section' has a usable cache after retry despite partial update errors" "warn"
        return 0
    fi

    return 1
}

rebuild_runtime_after_deferred_subscription_recovery() {
    local recovered_sections="$1"

    log "Recovered deferred subscription rule(s): $recovered_sections; rebuilding sing-box runtime" "info"
    config_load "$PODKOP_CONFIG_NAME"
    prepare_all_server_defaults
    validate_runtime_settings
    sing_box_configure_service
    rebuild_nft_runtime
    PODKOP_NFT_POPULATE_ENABLED=1
    sing_box_init_config
    reload_sing_box_runtime
    apply_pending_urltest_selector_switches
    capture_reload_state
    write_reload_state
}

deferred_subscription_bootstrap_retry_pass() {
    local section remaining_deferred recovered_sections

    [ -n "$SUBSCRIPTION_DEFERRED_SECTIONS" ] || return 0

    remaining_deferred=""
    recovered_sections=""
    for section in $SUBSCRIPTION_DEFERRED_SECTIONS; do
        if retry_deferred_subscription_section "$section"; then
            recovered_sections="${recovered_sections}${recovered_sections:+ }$section"
        else
            remaining_deferred="${remaining_deferred}${remaining_deferred:+ }$section"
            log "Deferred subscription rule '$section' is still unavailable; keeping it disabled for this startup" "warn"
        fi
    done

    [ -n "$recovered_sections" ] || {
        SUBSCRIPTION_DEFERRED_SECTIONS="$remaining_deferred"
        return 1
    }

    SUBSCRIPTION_DEFERRED_SECTIONS="$remaining_deferred"
    SUBSCRIPTION_UNAVAILABLE_SOURCES=""
    rebuild_runtime_after_deferred_subscription_recovery "$recovered_sections"

    if [ -n "$SUBSCRIPTION_DEFERRED_SECTIONS" ]; then
        log "Some deferred subscription rule(s) are still disabled for this startup: $SUBSCRIPTION_DEFERRED_SECTIONS" "warn"
        return 1
    fi

    return 0
}

subscription_bootstrap_retry_worker_cleanup() {
    local status="${1:-0}"

    trap - EXIT INT TERM
    rm -f "$PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE"
    release_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"
    release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
    exit "$status"
}

deferred_subscription_bootstrap_retry_worker() {
    local remaining_sections="$1"

    trap 'subscription_bootstrap_retry_worker_cleanup "$?"' EXIT INT TERM

    while [ -n "$remaining_sections" ]; do
        sleep 30

        if ! sing_box_service_is_running; then
            log "Stopping subscription bootstrap retry worker because sing-box is not running" "warn"
            break
        fi

        config_load "$PODKOP_CONFIG_NAME"
        ensure_runtime_dirs

        if ! acquire_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"; then
            log "Subscription bootstrap retry skipped because another subscription update is running" "debug"
            continue
        fi

        if ! acquire_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"; then
            release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
            log "Subscription bootstrap retry skipped because Podkop Plus reload is running" "debug"
            continue
        fi

        SUBSCRIPTION_DEFERRED_SECTIONS="$remaining_sections"
        SUBSCRIPTION_UNAVAILABLE_SOURCES=""

        if deferred_subscription_bootstrap_retry_pass; then
            release_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"
            release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
            break
        fi

        remaining_sections="$SUBSCRIPTION_DEFERRED_SECTIONS"
        release_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"
        release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
    done
}

start_deferred_subscription_bootstrap_retry_worker() {
    local existing_pid

    [ -n "$SUBSCRIPTION_DEFERRED_SECTIONS" ] || return 0

    existing_pid="$(sed -n '1p' "$PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE" 2>/dev/null)"
    if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
        log "Subscription bootstrap retry worker is already running with PID $existing_pid" "debug"
        return 0
    fi

    (close_inherited_service_lock_fd; deferred_subscription_bootstrap_retry_worker "$SUBSCRIPTION_DEFERRED_SECTIONS") &
    printf '%s\n' "$!" > "$PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE"
    log "Started subscription bootstrap retry worker for rule(s): $SUBSCRIPTION_DEFERRED_SECTIONS" "info"
}

stop_deferred_subscription_bootstrap_retry_worker() {
    local pid

    pid="$(sed -n '1p' "$PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE" 2>/dev/null)"
    if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
        kill "$pid" 2>/dev/null || true
        log "Stopped subscription bootstrap retry worker"
    fi
    rm -f "$PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE"
}

run_deferred_subscription_bootstrap() {
    [ -n "$SUBSCRIPTION_DEFERRED_SECTIONS" ] || return 0

    log "Waiting for sing-box service proxy before retrying deferred subscription rule(s): $SUBSCRIPTION_DEFERRED_SECTIONS" "info"
    if ! wait_for_subscription_bootstrap_service_proxy; then
        log "sing-box service proxy did not become ready in time; deferred subscription rule(s) will remain disabled until the next successful subscription update" "warn"
        start_deferred_subscription_bootstrap_retry_worker
        return 0
    fi

    deferred_subscription_bootstrap_retry_pass || start_deferred_subscription_bootstrap_retry_worker
}

close_inherited_service_lock_fd() {
    exec 1000>&- 2>/dev/null || true
}

acquire_runtime_dir_lock() {
    local lock_dir="$1"
    local owner_pid

    if mkdir "$lock_dir" 2>/dev/null; then
        printf '%s\n' "$$" > "$lock_dir/pid"
        return 0
    fi

    owner_pid="$(sed -n '1p' "$lock_dir/pid" 2>/dev/null)"
    if [ -n "$owner_pid" ] && kill -0 "$owner_pid" 2>/dev/null; then
        return 1
    fi

    rm -f "$lock_dir/pid" 2>/dev/null
    rmdir "$lock_dir" 2>/dev/null || return 1
    mkdir "$lock_dir" 2>/dev/null || return 1
    printf '%s\n' "$$" > "$lock_dir/pid"
}

acquire_runtime_dir_lock_wait() {
    local lock_dir="$1"
    local timeout="${2:-300}"
    local start now elapsed

    start="$(date +%s 2>/dev/null)"
    case "$start" in
    '' | *[!0-9]*) start=0 ;;
    esac

    while ! acquire_runtime_dir_lock "$lock_dir"; do
        now="$(date +%s 2>/dev/null)"
        case "$now" in
        '' | *[!0-9]*) now="$start" ;;
        esac
        elapsed=$((now - start))
        [ "$elapsed" -lt "$timeout" ] || return 1
        sleep 2
    done

    return 0
}

release_runtime_dir_lock() {
    local lock_dir="$1"

    rm -f "$lock_dir/pid" 2>/dev/null
    rmdir "$lock_dir" 2>/dev/null
}

mark_pending_reload() {
    local reason="${1:-pending}"
    local now

    ensure_runtime_dirs
    now="$(date +%s 2>/dev/null)"
    case "$now" in
    '' | *[!0-9]*) now=0 ;;
    esac

    {
        printf 'reason=%s\n' "$reason"
        printf 'updated_at=%s\n' "$now"
    } > "$PODKOP_PENDING_RELOAD_FILE"
}

consume_pending_reload() {
    [ -f "$PODKOP_PENDING_RELOAD_FILE" ] || return 1
    rm -f "$PODKOP_PENDING_RELOAD_FILE" 2>/dev/null
}

run_pending_reload_if_requested() {
    consume_pending_reload || return 0
    echolog "Applying pending Podkop Plus reload"
    "$PODKOP_SERVICE_INIT" reload pending >/dev/null 2>&1 &
}

sync_time_if_needed() {
    local current_year

    current_year="$(date +%Y 2>/dev/null)"
    case "$current_year" in
    '' | *[!0-9]*)
        return 0
        ;;
    esac

    [ "$current_year" -lt 2024 ] || return 0

    /usr/sbin/ntpd -q -p 194.190.168.1 -p 216.239.35.0 -p 216.239.35.4 -p 162.159.200.1 -p 162.159.200.123 >/dev/null 2>&1 || true
}

process_age_seconds() {
    local pid="$1"
    local stat stat_rest start_ticks uptime_seconds

    [ -r "/proc/$pid/stat" ] || return 1

    stat="$(cat "/proc/$pid/stat" 2>/dev/null)" || return 1
    stat_rest="${stat##*) }"

    set -- $stat_rest
    [ "$#" -ge 20 ] || return 1
    shift 19
    start_ticks="$1"

    case "$start_ticks" in
    '' | *[!0-9]*) return 1 ;;
    esac

    read -r uptime_seconds _ < /proc/uptime || return 1
    uptime_seconds="${uptime_seconds%%.*}"
    case "$uptime_seconds" in
    '' | *[!0-9]*) return 1 ;;
    esac

    echo $((uptime_seconds - (start_ticks / 100)))
}

sing_box_service_pid() {
    local service_json='{"name":"sing-box"}'

    ubus call service list "$service_json" 2>/dev/null |
        json_utils_ucode sing-box-service-pid 2>/dev/null |
        sed -n '1p'
}

pid_is_sing_box() {
    local pid="$1"
    local exe

    case "$pid" in
    '' | *[!0-9]*) return 1 ;;
    esac

    exe="$(readlink "/proc/$pid/exe" 2>/dev/null)" || return 1
    [ "${exe##*/}" = "sing-box" ]
}

sing_box_service_is_running() {
    local pid

    pid="$(sing_box_service_pid)"
    pid_is_sing_box "$pid"
}

sing_box_service_is_stable() {
    local min_age="${1:-$PODKOP_RUNTIME_STABLE_MIN_AGE}"
    local pid age

    pid="$(sing_box_service_pid)"
    pid_is_sing_box "$pid" || return 1

    age="$(process_age_seconds "$pid")" || return 1
    [ "$age" -ge "$min_age" ]
}

podkop_runtime_network_is_configured() {
    nft list table inet "$NFT_TABLE_NAME" >/dev/null 2>&1 &&
        ip rule list | grep -q "lookup $RT_TABLE_NAME" &&
        ip route list table "$RT_TABLE_NAME" 2>/dev/null | grep -q "local default dev lo scope host"
}

podkop_is_running() {
    sing_box_service_is_running &&
        podkop_runtime_network_is_configured
}

podkop_is_stably_running() {
    sing_box_service_is_stable &&
        podkop_runtime_network_is_configured
}

clear_reload_state() {
    rm -f "$PODKOP_RELOAD_STATE_FILE"
}

read_reload_state_value() {
    local key="$1"

    [ -f "$PODKOP_RELOAD_STATE_FILE" ] || return 1

    awk -F= -v key="$key" '$1 == key { print substr($0, index($0, "=") + 1); exit }' "$PODKOP_RELOAD_STATE_FILE"
}

reload_state_has_key() {
    local key="$1"

    [ -f "$PODKOP_RELOAD_STATE_FILE" ] || return 1

    awk -F= -v key="$key" '
        $1 == key { found = 1 }
        END { exit found ? 0 : 1 }
    ' "$PODKOP_RELOAD_STATE_FILE"
}

reload_state_is_compatible() {
    [ "$(read_reload_state_value "format")" = "$PODKOP_RELOAD_STATE_FORMAT" ]
}

signature_begin() {
    RELOAD_SIGNATURE_TMPFILE="$(mktemp)"
}

signature_add() {
    local key="$1"
    local value="$2"

    printf '[%s]\n%s\n' "$key" "$value" >> "$RELOAD_SIGNATURE_TMPFILE"
}

signature_finish() {
    local hash

    hash="$(md5sum "$RELOAD_SIGNATURE_TMPFILE" | awk '{print $1}')"
    rm -f "$RELOAD_SIGNATURE_TMPFILE"
    echo "$hash"
}

build_service_trigger_signature() {
    # These settings are consumed by init.d/procd triggers, not by sing-box/nft
    # runtime. Track them separately so config reloads can refresh procd without
    # forcing an in-process runtime restart.
    local enable_badwan_interface_monitoring badwan_monitored_interfaces badwan_reload_delay

    signature_begin
    config_get_bool enable_badwan_interface_monitoring "settings" "enable_badwan_interface_monitoring" 0
    signature_add "settings.enable_badwan_interface_monitoring" "$enable_badwan_interface_monitoring"

    if [ "$enable_badwan_interface_monitoring" -eq 1 ]; then
        config_get badwan_monitored_interfaces "settings" "badwan_monitored_interfaces"
        config_get badwan_reload_delay "settings" "badwan_reload_delay" "2000"
        signature_add "settings.badwan_monitored_interfaces" "$badwan_monitored_interfaces"
        signature_add "settings.badwan_reload_delay" "$badwan_reload_delay"
    fi

    signature_finish
}

dnsmasq_inherited_default_options() {
    echo "domain local expandhosts localise_queries domainneeded add_local_domain add_local_hostname add_local_fqdn rebind_protection rebind_localhost"
}

dnsmasq_inherited_default_list_options() {
    echo "rebind_domain"
}

dnsmasq_server_is_domain_forward() {
    local server="$1"

    case "$server" in
    /#/* | /./* | //*)
        return 1
        ;;
    /*/*)
        return 0
        ;;
    esac

    return 1
}

dnsmasq_default_domain_forward_servers() {
    local server

    for server in $(uci_get "dhcp" "@dnsmasq[0]" "server"); do
        dnsmasq_server_is_domain_forward "$server" || continue
        printf '%s\n' "$server"
    done
}

dnsmasq_default_domain_forward_servers_signature() {
    dnsmasq_default_domain_forward_servers | tr '\n' ' '
}

append_dnsmasq_inherited_options_signature() {
    local option value

    for option in $(dnsmasq_inherited_default_options); do
        value="$(uci_get "dhcp" "@dnsmasq[0]" "$option")"
        signature_add "dhcp.@dnsmasq[0].$option" "$value"
    done

    for option in $(dnsmasq_inherited_default_list_options); do
        value="$(uci_get "dhcp" "@dnsmasq[0]" "$option")"
        signature_add "dhcp.@dnsmasq[0].$option" "$value"
    done
}

build_dnsmasq_signature() {
    local dont_touch_dhcp source_network_interfaces

    signature_begin
    config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
    config_get source_network_interfaces "settings" "source_network_interfaces" "br-lan"
    signature_add "settings.dont_touch_dhcp" "$dont_touch_dhcp"
    signature_add "settings.source_network_interfaces" "$source_network_interfaces"
    append_dnsmasq_inherited_options_signature
    signature_add "dhcp.@dnsmasq[0].domain_forward_servers" "$(dnsmasq_default_domain_forward_servers_signature)"
    signature_finish
}

append_sing_box_rule_signature() {
    local section="$1"
    local action domain domain_suffix domain_keyword domain_regex ip_cidr source_ip_cidr fully_routed_ips \
        ports community_lists rule_set rule_set_with_subnets domain_ip_lists mixed_proxy_enabled mixed_proxy_port \
        mixed_proxy_auth_enabled mixed_proxy_username mixed_proxy_password resolve_real_ip_for_routing

    rule_is_enabled "$section" || return 0

    action="$(get_rule_action "$section")"
    [ -n "$action" ] || return 0

    signature_add "rule.$section.action" "$action"

    case "$action" in
    proxy)
        local selector_proxy_links subscription_urls udp_over_tcp urltest_enabled \
            urltest_check_interval urltest_tolerance urltest_testing_url subscription_update_enabled \
            subscription_update_interval detect_server_country urltest_filter_mode urltest_exclude_countries urltest_include_countries \
            urltest_exclude_outbounds urltest_exclude_regex urltest_include_outbounds urltest_include_regex
        config_get selector_proxy_links "$section" "selector_proxy_links"
        config_get subscription_urls "$section" "subscription_urls"
        config_get_bool udp_over_tcp "$section" "enable_udp_over_tcp" 0
        config_get_bool urltest_enabled "$section" "urltest_enabled" 0
        config_get_bool detect_server_country "$section" "detect_server_country" 0
        urltest_check_interval="$(get_urltest_check_interval_for_rule "$section")"
        config_get urltest_tolerance "$section" "urltest_tolerance" 50
        config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204"
        config_get_bool subscription_update_enabled "$section" "subscription_update_enabled" 1
        subscription_update_interval="$(get_subscription_update_interval_for_rule "$section")"
        config_get urltest_filter_mode "$section" "urltest_filter_mode" "disabled"
        config_get urltest_exclude_countries "$section" "urltest_exclude_countries"
        config_get urltest_include_countries "$section" "urltest_include_countries"
        config_get urltest_exclude_outbounds "$section" "urltest_exclude_outbounds"
        config_get urltest_exclude_regex "$section" "urltest_exclude_regex"
        config_get urltest_include_outbounds "$section" "urltest_include_outbounds"
        config_get urltest_include_regex "$section" "urltest_include_regex"

        signature_add "rule.$section.selector_proxy_links" "$selector_proxy_links"
        signature_add "rule.$section.subscription_urls" "$subscription_urls"
        signature_add "rule.$section.enable_udp_over_tcp" "$udp_over_tcp"
        signature_add "rule.$section.urltest_enabled" "$urltest_enabled"
        signature_add "rule.$section.detect_server_country" "$detect_server_country"
        signature_add "rule.$section.urltest_check_interval" "$urltest_check_interval"
        signature_add "rule.$section.urltest_tolerance" "$urltest_tolerance"
        signature_add "rule.$section.urltest_testing_url" "$urltest_testing_url"
        signature_add "rule.$section.urltest_filter_mode" "$urltest_filter_mode"
        signature_add "rule.$section.urltest_exclude_countries" "$urltest_exclude_countries"
        signature_add "rule.$section.urltest_include_countries" "$urltest_include_countries"
        signature_add "rule.$section.urltest_exclude_outbounds" "$urltest_exclude_outbounds"
        signature_add "rule.$section.urltest_exclude_regex" "$urltest_exclude_regex"
        signature_add "rule.$section.urltest_include_outbounds" "$urltest_include_outbounds"
        signature_add "rule.$section.urltest_include_regex" "$urltest_include_regex"
        signature_add "rule.$section.subscription_update_enabled" "$subscription_update_enabled"
        signature_add "rule.$section.subscription_update_interval" "$subscription_update_interval"

        config_get_bool mixed_proxy_enabled "$section" "mixed_proxy_enabled" 0
        signature_add "rule.$section.mixed_proxy_enabled" "$mixed_proxy_enabled"
        if [ "$mixed_proxy_enabled" -eq 1 ]; then
            config_get mixed_proxy_port "$section" "mixed_proxy_port"
            signature_add "rule.$section.mixed_proxy_port" "$mixed_proxy_port"
            config_get_bool mixed_proxy_auth_enabled "$section" "mixed_proxy_auth_enabled" 0
            signature_add "rule.$section.mixed_proxy_auth_enabled" "$mixed_proxy_auth_enabled"
            if [ "$mixed_proxy_auth_enabled" -eq 1 ]; then
                config_get mixed_proxy_username "$section" "mixed_proxy_username"
                config_get mixed_proxy_password "$section" "mixed_proxy_password"
                signature_add "rule.$section.mixed_proxy_username" "$mixed_proxy_username"
                signature_add "rule.$section.mixed_proxy_password" "$mixed_proxy_password"
            fi
        fi

        config_get_bool resolve_real_ip_for_routing "$section" "resolve_real_ip_for_routing" 0
        signature_add "rule.$section.resolve_real_ip_for_routing" "$resolve_real_ip_for_routing"
        ;;
    outbound)
        local outbound_json
        config_get outbound_json "$section" "outbound_json"
        signature_add "rule.$section.outbound_json" "$outbound_json"

        config_get_bool mixed_proxy_enabled "$section" "mixed_proxy_enabled" 0
        signature_add "rule.$section.mixed_proxy_enabled" "$mixed_proxy_enabled"
        if [ "$mixed_proxy_enabled" -eq 1 ]; then
            config_get mixed_proxy_port "$section" "mixed_proxy_port"
            signature_add "rule.$section.mixed_proxy_port" "$mixed_proxy_port"
            config_get_bool mixed_proxy_auth_enabled "$section" "mixed_proxy_auth_enabled" 0
            signature_add "rule.$section.mixed_proxy_auth_enabled" "$mixed_proxy_auth_enabled"
            if [ "$mixed_proxy_auth_enabled" -eq 1 ]; then
                config_get mixed_proxy_username "$section" "mixed_proxy_username"
                config_get mixed_proxy_password "$section" "mixed_proxy_password"
                signature_add "rule.$section.mixed_proxy_username" "$mixed_proxy_username"
                signature_add "rule.$section.mixed_proxy_password" "$mixed_proxy_password"
            fi
        fi

        config_get_bool resolve_real_ip_for_routing "$section" "resolve_real_ip_for_routing" 0
        signature_add "rule.$section.resolve_real_ip_for_routing" "$resolve_real_ip_for_routing"
        ;;
    byedpi)
        local byedpi_index
        byedpi_index="$(get_byedpi_rule_index "$section")"
        signature_add "rule.$section.byedpi_index" "$byedpi_index"

        config_get_bool mixed_proxy_enabled "$section" "mixed_proxy_enabled" 0
        signature_add "rule.$section.mixed_proxy_enabled" "$mixed_proxy_enabled"
        if [ "$mixed_proxy_enabled" -eq 1 ]; then
            config_get mixed_proxy_port "$section" "mixed_proxy_port"
            signature_add "rule.$section.mixed_proxy_port" "$mixed_proxy_port"
            config_get_bool mixed_proxy_auth_enabled "$section" "mixed_proxy_auth_enabled" 0
            signature_add "rule.$section.mixed_proxy_auth_enabled" "$mixed_proxy_auth_enabled"
            if [ "$mixed_proxy_auth_enabled" -eq 1 ]; then
                config_get mixed_proxy_username "$section" "mixed_proxy_username"
                config_get mixed_proxy_password "$section" "mixed_proxy_password"
                signature_add "rule.$section.mixed_proxy_username" "$mixed_proxy_username"
                signature_add "rule.$section.mixed_proxy_password" "$mixed_proxy_password"
            fi
        fi

        signature_add "rule.$section.resolve_real_ip_for_routing" "1"
        ;;
    zapret)
        config_get_bool mixed_proxy_enabled "$section" "mixed_proxy_enabled" 0
        signature_add "rule.$section.mixed_proxy_enabled" "$mixed_proxy_enabled"
        if [ "$mixed_proxy_enabled" -eq 1 ]; then
            config_get mixed_proxy_port "$section" "mixed_proxy_port"
            signature_add "rule.$section.mixed_proxy_port" "$mixed_proxy_port"
            config_get_bool mixed_proxy_auth_enabled "$section" "mixed_proxy_auth_enabled" 0
            signature_add "rule.$section.mixed_proxy_auth_enabled" "$mixed_proxy_auth_enabled"
            if [ "$mixed_proxy_auth_enabled" -eq 1 ]; then
                config_get mixed_proxy_username "$section" "mixed_proxy_username"
                config_get mixed_proxy_password "$section" "mixed_proxy_password"
                signature_add "rule.$section.mixed_proxy_username" "$mixed_proxy_username"
                signature_add "rule.$section.mixed_proxy_password" "$mixed_proxy_password"
            fi
        fi
        ;;
    vpn)
        local interface_name domain_resolver_enabled domain_resolver_dns_type domain_resolver_dns_server
        config_get interface_name "$section" "interface"
        config_get_bool domain_resolver_enabled "$section" "domain_resolver_enabled" 0
        signature_add "rule.$section.interface" "$interface_name"
        signature_add "rule.$section.domain_resolver_enabled" "$domain_resolver_enabled"
        if [ "$domain_resolver_enabled" -eq 1 ]; then
            config_get domain_resolver_dns_type "$section" "domain_resolver_dns_type"
            config_get domain_resolver_dns_server "$section" "domain_resolver_dns_server"
            signature_add "rule.$section.domain_resolver_dns_type" "$domain_resolver_dns_type"
            signature_add "rule.$section.domain_resolver_dns_server" "$domain_resolver_dns_server"
        fi

        config_get_bool mixed_proxy_enabled "$section" "mixed_proxy_enabled" 0
        signature_add "rule.$section.mixed_proxy_enabled" "$mixed_proxy_enabled"
        if [ "$mixed_proxy_enabled" -eq 1 ]; then
            config_get mixed_proxy_port "$section" "mixed_proxy_port"
            signature_add "rule.$section.mixed_proxy_port" "$mixed_proxy_port"
            config_get_bool mixed_proxy_auth_enabled "$section" "mixed_proxy_auth_enabled" 0
            signature_add "rule.$section.mixed_proxy_auth_enabled" "$mixed_proxy_auth_enabled"
            if [ "$mixed_proxy_auth_enabled" -eq 1 ]; then
                config_get mixed_proxy_username "$section" "mixed_proxy_username"
                config_get mixed_proxy_password "$section" "mixed_proxy_password"
                signature_add "rule.$section.mixed_proxy_username" "$mixed_proxy_username"
                signature_add "rule.$section.mixed_proxy_password" "$mixed_proxy_password"
            fi
        fi

        config_get_bool resolve_real_ip_for_routing "$section" "resolve_real_ip_for_routing" 0
        signature_add "rule.$section.resolve_real_ip_for_routing" "$resolve_real_ip_for_routing"
        ;;
    esac

    domain="$(get_rule_condition_commas_string "$section" "domain" "domains")"
    domain_suffix="$(get_rule_condition_commas_string "$section" "domain_suffix" "domains")"
    domain_keyword="$(get_rule_condition_commas_string "$section" "domain_keyword" "generic")"
    domain_regex="$(get_rule_condition_commas_string "$section" "domain_regex" "generic")"
    ip_cidr="$(get_rule_condition_commas_string "$section" "ip_cidr" "subnets")"
    source_ip_cidr="$(get_rule_condition_commas_string "$section" "source_ip_cidr" "subnets")"
    ports="$(get_rule_ports_commas_string "$section")"
    config_get fully_routed_ips "$section" "fully_routed_ips"
    config_get community_lists "$section" "community_lists"
    config_get rule_set "$section" "rule_set"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"

    signature_add "rule.$section.domain" "$domain"
    signature_add "rule.$section.domain_suffix" "$domain_suffix"
    signature_add "rule.$section.domain_keyword" "$domain_keyword"
    signature_add "rule.$section.domain_regex" "$domain_regex"
    signature_add "rule.$section.ip_cidr" "$ip_cidr"
    signature_add "rule.$section.source_ip_cidr" "$source_ip_cidr"
    signature_add "rule.$section.ports" "$ports"
    signature_add "rule.$section.fully_routed_ips" "$fully_routed_ips"
    signature_add "rule.$section.community_lists" "$community_lists"
    signature_add "rule.$section.rule_set" "$rule_set"
    signature_add "rule.$section.rule_set_with_subnets" "$rule_set_with_subnets"
    signature_add "rule.$section.domain_ip_lists" "$domain_ip_lists"
}

append_sing_box_server_signature() {
    local section="$1"
    local enabled label protocol listen listen_port public_host routing_mode routing_section security \
        users tls_server_name tls_alpn tls_certificate_path tls_key_path reality_handshake_server \
        reality_handshake_server_port reality_private_key reality_public_key reality_short_id \
        reality_max_time_difference transport transport_path transport_host transport_service_name transport_hosts \
        client_fingerprint server_uuid server_username server_password vless_flow vmess_alter_id \
        shadowsocks_method hysteria2_up_mbps hysteria2_down_mbps hysteria2_obfs_type hysteria2_obfs_password \
        tailscale_auth_key tailscale_control_url tailscale_hostname \
        tailscale_accept_routes tailscale_advertise_routes \
        tailscale_advertise_exit_node

    config_get_bool enabled "$section" "enabled" 0
    signature_add "server.$section.enabled" "$enabled"
    [ "$enabled" -eq 1 ] || return 0

    config_get label "$section" "label" "$section"
    config_get protocol "$section" "protocol" "vless"
    config_get listen "$section" "listen" "0.0.0.0"
    config_get listen_port "$section" "listen_port"
    config_get public_host "$section" "public_host"
    config_get routing_mode "$section" "routing_mode" "rules"
    config_get routing_section "$section" "routing_section"
    config_get security "$section" "security" "reality"
    config_get users "$section" "server_users"
    config_get tls_server_name "$section" "tls_server_name"
    config_get tls_alpn "$section" "tls_alpn"
    config_get tls_certificate_path "$section" "tls_certificate_path"
    config_get tls_key_path "$section" "tls_key_path"
    config_get reality_handshake_server "$section" "reality_handshake_server"
    config_get reality_handshake_server_port "$section" "reality_handshake_server_port"
    config_get reality_private_key "$section" "reality_private_key"
    config_get reality_public_key "$section" "reality_public_key"
    config_get reality_short_id "$section" "reality_short_id"
    config_get reality_max_time_difference "$section" "reality_max_time_difference"
    config_get transport "$section" "transport" "tcp"
    config_get transport_path "$section" "transport_path"
    config_get transport_host "$section" "transport_host"
    config_get transport_service_name "$section" "transport_service_name"
    config_get transport_hosts "$section" "transport_hosts"
    config_get client_fingerprint "$section" "client_fingerprint"
    config_get server_uuid "$section" "server_uuid"
    config_get server_username "$section" "server_username"
    config_get server_password "$section" "server_password"
    config_get vless_flow "$section" "vless_flow"
    config_get vmess_alter_id "$section" "vmess_alter_id"
    config_get shadowsocks_method "$section" "shadowsocks_method"
    config_get hysteria2_up_mbps "$section" "hysteria2_up_mbps"
    config_get hysteria2_down_mbps "$section" "hysteria2_down_mbps"
    config_get hysteria2_obfs_type "$section" "hysteria2_obfs_type"
    config_get hysteria2_obfs_password "$section" "hysteria2_obfs_password"
    config_get tailscale_auth_key "$section" "tailscale_auth_key"
    config_get tailscale_control_url "$section" "tailscale_control_url"
    config_get tailscale_hostname "$section" "tailscale_hostname"
    config_get tailscale_accept_routes "$section" "tailscale_accept_routes"
    config_get tailscale_advertise_routes "$section" "tailscale_advertise_routes"
    config_get tailscale_advertise_exit_node "$section" "tailscale_advertise_exit_node"

    signature_add "server.$section.label" "$label"
    signature_add "server.$section.protocol" "$protocol"
    signature_add "server.$section.listen" "$listen"
    signature_add "server.$section.listen_port" "$listen_port"
    signature_add "server.$section.public_host" "$public_host"
    signature_add "server.$section.routing_mode" "$routing_mode"
    signature_add "server.$section.routing_section" "$routing_section"
    signature_add "server.$section.security" "$security"
    signature_add "server.$section.server_users" "$users"
    signature_add "server.$section.tls_server_name" "$tls_server_name"
    signature_add "server.$section.tls_alpn" "$tls_alpn"
    signature_add "server.$section.tls_certificate_path" "$tls_certificate_path"
    signature_add "server.$section.tls_key_path" "$tls_key_path"
    signature_add "server.$section.reality_handshake_server" "$reality_handshake_server"
    signature_add "server.$section.reality_handshake_server_port" "$reality_handshake_server_port"
    signature_add "server.$section.reality_private_key" "$reality_private_key"
    signature_add "server.$section.reality_public_key" "$reality_public_key"
    signature_add "server.$section.reality_short_id" "$reality_short_id"
    signature_add "server.$section.reality_max_time_difference" "$reality_max_time_difference"
    signature_add "server.$section.transport" "$transport"
    signature_add "server.$section.transport_path" "$transport_path"
    signature_add "server.$section.transport_host" "$transport_host"
    signature_add "server.$section.transport_service_name" "$transport_service_name"
    signature_add "server.$section.transport_hosts" "$transport_hosts"
    signature_add "server.$section.client_fingerprint" "$client_fingerprint"
    signature_add "server.$section.server_uuid" "$server_uuid"
    signature_add "server.$section.server_username" "$server_username"
    signature_add "server.$section.server_password" "$server_password"
    signature_add "server.$section.vless_flow" "$vless_flow"
    signature_add "server.$section.vmess_alter_id" "$vmess_alter_id"
    signature_add "server.$section.shadowsocks_method" "$shadowsocks_method"
    signature_add "server.$section.hysteria2_up_mbps" "$hysteria2_up_mbps"
    signature_add "server.$section.hysteria2_down_mbps" "$hysteria2_down_mbps"
    signature_add "server.$section.hysteria2_obfs_type" "$hysteria2_obfs_type"
    signature_add "server.$section.hysteria2_obfs_password" "$hysteria2_obfs_password"
    signature_add "server.$section.tailscale_auth_key" "$tailscale_auth_key"
    signature_add "server.$section.tailscale_control_url" "$tailscale_control_url"
    signature_add "server.$section.tailscale_hostname" "$tailscale_hostname"
    signature_add "server.$section.tailscale_accept_routes" "$tailscale_accept_routes"
    signature_add "server.$section.tailscale_advertise_routes" "$tailscale_advertise_routes"
    signature_add "server.$section.tailscale_advertise_exit_node" "$tailscale_advertise_exit_node"
}

build_sing_box_signature() {
    local dns_type dns_server bootstrap_dns_server rewrite_ttl output_network_interface disable_quic routing_excluded_ips \
        update_interval cache_path config_path log_level enable_yacd enable_yacd_wan_access yacd_secret_key \
        download_lists_via_proxy download_lists_via_proxy_section service_listen_address mwan3_active

    signature_begin

    config_get dns_type "settings" "dns_type" "doh"
    config_get dns_server "settings" "dns_server" "1.1.1.1"
    config_get bootstrap_dns_server "settings" "bootstrap_dns_server" "77.88.8.8"
    config_get rewrite_ttl "settings" "dns_rewrite_ttl" "60"
    config_get output_network_interface "settings" "output_network_interface"
    config_get_bool disable_quic "settings" "disable_quic" 0
    config_get routing_excluded_ips "settings" "routing_excluded_ips"
    update_interval="$(get_settings_update_interval)"
    config_get cache_path "settings" "cache_path" "/tmp/sing-box/cache.db"
    config_get config_path "settings" "config_path"
    config_get log_level "settings" "log_level" "warn"
    config_get service_listen_address "settings" "service_listen_address"
    config_get_bool enable_yacd "settings" "enable_yacd" 0
    config_get_bool enable_yacd_wan_access "settings" "enable_yacd_wan_access" 0
    config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
    mwan3_active=0
    mwan3_is_active && mwan3_active=1

    signature_add "settings.dns_type" "$dns_type"
    signature_add "settings.dns_server" "$dns_server"
    signature_add "settings.bootstrap_dns_server" "$bootstrap_dns_server"
    signature_add "settings.dns_rewrite_ttl" "$rewrite_ttl"
    signature_add "settings.output_network_interface" "$output_network_interface"
    signature_add "settings.disable_quic" "$disable_quic"
    signature_add "settings.routing_excluded_ips" "$routing_excluded_ips"
    if has_remote_sing_box_ruleset_sources; then
        signature_add "settings.update_interval" "$update_interval"
    fi
    signature_add "settings.cache_path" "$cache_path"
    signature_add "settings.config_path" "$config_path"
    signature_add "settings.log_level" "$log_level"
    signature_add "settings.service_listen_address" "$service_listen_address"
    signature_add "runtime.mwan3_active" "$mwan3_active"
    signature_add "settings.enable_yacd" "$enable_yacd"
    if [ "$enable_yacd" -eq 1 ]; then
        signature_add "settings.enable_yacd_wan_access" "$enable_yacd_wan_access"
        config_get yacd_secret_key "settings" "yacd_secret_key"
        signature_add "settings.yacd_secret_key" "$yacd_secret_key"
    fi

    signature_add "settings.download_lists_via_proxy" "$download_lists_via_proxy"
    if [ "$download_lists_via_proxy" -eq 1 ]; then
        config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
        signature_add "settings.download_lists_via_proxy_section" "$download_lists_via_proxy_section"
    fi

    config_foreach append_sing_box_rule_signature "section"
    config_foreach append_sing_box_server_signature "server"
    signature_finish
}

append_nft_rule_signature() {
    local section="$1"
    local ip_cidr ports community_lists community_subnet_lists remote_subnet_lists rule_set_with_subnets domain_ip_lists

    rule_is_enabled "$section" || return 0

    ip_cidr="$(get_rule_condition_commas_string "$section" "ip_cidr" "subnets")"
    ports="$(get_rule_ports_commas_string "$section")"
    config_get community_lists "$section" "community_lists"
    community_subnet_lists="$(filter_community_subnet_lists "$community_lists")"
    config_get remote_subnet_lists "$section" "remote_subnet_lists"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"

    signature_add "rule.$section.ip_cidr" "$ip_cidr"
    signature_add "rule.$section.ports" "$ports"
    signature_add "rule.$section.community_subnet_lists" "$community_subnet_lists"
    signature_add "rule.$section.remote_subnet_lists" "$remote_subnet_lists"
    signature_add "rule.$section.rule_set_with_subnets" "$rule_set_with_subnets"
    signature_add "rule.$section.domain_ip_lists" "$domain_ip_lists"
}

build_nft_signature() {
    local source_network_interfaces exclude_ntp

    signature_begin
    config_get source_network_interfaces "settings" "source_network_interfaces" "br-lan"
    config_get_bool exclude_ntp "settings" "exclude_ntp" 0
    signature_add "settings.source_network_interfaces" "$source_network_interfaces"
    signature_add "settings.exclude_ntp" "$exclude_ntp"
    config_foreach append_nft_rule_signature "section"
    signature_finish
}

append_zapret_queue_signature() {
    local section="$1"

    rule_is_enabled "$section" || return 0
    [ "$(get_rule_action "$section")" = "zapret" ] || return 0

    signature_add "zapret_queue.section" "$section"
}

build_zapret_queue_signature() {
    signature_begin
    config_foreach append_zapret_queue_signature "section"
    signature_finish
}

append_zapret_runtime_signature() {
    local section="$1"
    local domain domain_suffix community_lists rule_set rule_set_with_subnets domain_ip_lists user_domain_list_type items

    rule_is_enabled "$section" || return 0
    [ "$(get_rule_action "$section")" = "zapret" ] || return 0

    domain="$(get_rule_condition_commas_string "$section" "domain" "domains")"
    domain_suffix="$(get_rule_condition_commas_string "$section" "domain_suffix" "domains")"
    config_get community_lists "$section" "community_lists"
    config_get rule_set "$section" "rule_set"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"
    config_get user_domain_list_type "$section" "user_domain_list_type" "disabled"

    signature_add "zapret.$section.nfqws_opt" "$(get_rule_nfqws_opt "$section")"
    signature_add "zapret.$section.domain" "$domain"
    signature_add "zapret.$section.domain_suffix" "$domain_suffix"
    signature_add "zapret.$section.community_lists" "$community_lists"
    signature_add "zapret.$section.rule_set" "$rule_set"
    signature_add "zapret.$section.rule_set_with_subnets" "$rule_set_with_subnets"
    signature_add "zapret.$section.domain_ip_lists" "$domain_ip_lists"
    signature_add "zapret.$section.user_domain_list_type" "$user_domain_list_type"
    config_get items "$section" "local_domain_lists"
    signature_add "zapret.$section.local_domain_lists" "$items"
    config_get items "$section" "remote_domain_lists"
    signature_add "zapret.$section.remote_domain_lists" "$items"

    case "$user_domain_list_type" in
    dynamic) config_get items "$section" "user_domains" ;;
    text) config_get items "$section" "user_domains_text" ;;
    *) items="" ;;
    esac
    signature_add "zapret.$section.user_domains" "$items"
}

build_zapret_runtime_signature() {
    signature_begin
    config_foreach append_zapret_runtime_signature "section"
    signature_finish
}

append_byedpi_runtime_signature() {
    local section="$1"
    local byedpi_index

    rule_is_enabled "$section" || return 0
    [ "$(get_rule_action "$section")" = "byedpi" ] || return 0

    byedpi_index="$(get_byedpi_rule_index "$section")"
    signature_add "byedpi.$section.index" "$byedpi_index"
    signature_add "byedpi.$section.byedpi_cmd_opts" "$(get_rule_byedpi_cmd_opts "$section")"
}

build_byedpi_runtime_signature() {
    signature_begin
    config_foreach append_byedpi_runtime_signature "section"
    signature_finish
}

append_list_update_signature() {
    local section="$1"
    local ports community_lists community_subnet_lists remote_domain_lists remote_subnet_lists rule_set_with_subnets domain_ip_lists

    rule_is_enabled "$section" || return 0

    ports="$(get_rule_ports_commas_string "$section")"
    config_get community_lists "$section" "community_lists"
    community_subnet_lists="$(filter_community_subnet_lists "$community_lists")"
    config_get remote_domain_lists "$section" "remote_domain_lists"
    config_get remote_subnet_lists "$section" "remote_subnet_lists"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"

    signature_add "lists.$section.ports" "$ports"
    signature_add "lists.$section.community_subnet_lists" "$community_subnet_lists"
    signature_add "lists.$section.remote_domain_lists" "$remote_domain_lists"
    signature_add "lists.$section.remote_subnet_lists" "$remote_subnet_lists"
    signature_add "lists.$section.rule_set_with_subnets" "$rule_set_with_subnets"
    signature_add "lists.$section.domain_ip_lists" "$domain_ip_lists"
}

build_list_update_signature() {
    signature_begin
    config_foreach append_list_update_signature "section"
    signature_finish
}

append_subscription_update_cron_signature() {
    local section="$1"
    local subscription_update_interval subscription_urls subscription_update_enabled

    rule_is_subscription_proxy "$section" || return 0

    config_get subscription_urls "$section" "subscription_urls"
    config_get_bool subscription_update_enabled "$section" "subscription_update_enabled" 1
    subscription_update_interval="$(get_subscription_update_interval_for_rule "$section")"

    signature_add "subscription.$section.subscription_urls" "$subscription_urls"
    signature_add "subscription.$section.subscription_update_enabled" "$subscription_update_enabled"
    signature_add "subscription.$section.subscription_update_interval" "$subscription_update_interval"
}

build_cron_signature() {
    local update_interval

    signature_begin
    update_interval="$(get_settings_update_interval)"
    signature_add "settings.update_interval" "$update_interval"
    config_foreach append_list_update_signature "section"
    config_foreach append_subscription_update_cron_signature "section"
    signature_finish
}

list_has_remote_references() {
    local items="$1"
    local item

    for item in $items; do
        case "$item" in
        http://* | https://*)
            return 0
            ;;
        esac
    done

    return 1
}

community_service_has_subnet_list() {
    case "$1" in
    twitter | meta | telegram | cloudflare | hetzner | ovh | digitalocean | cloudfront | discord | roblox)
        return 0
        ;;
    esac

    return 1
}

filter_community_subnet_lists() {
    local items="$1"
    local item result

    for item in $items; do
        community_service_has_subnet_list "$item" || continue

        if [ -z "$result" ]; then
            result="$item"
        else
            result="$result $item"
        fi
    done

    printf '%s\n' "$result"
}

reference_is_remote_sing_box_ruleset() {
    case "$1" in
    http://* | https://*)
        return 0
        ;;
    esac

    return 1
}

list_has_remote_sing_box_rulesets() {
    local items="$1"
    local item

    for item in $items; do
        reference_is_remote_sing_box_ruleset "$item" && return 0
    done

    return 1
}

enabled_rule_has_remote_sing_box_ruleset_source() {
    local section="$1"
    local community_lists rule_set rule_set_with_subnets

    rule_is_enabled "$section" || return 0

    config_get community_lists "$section" "community_lists"
    if [ -n "$community_lists" ]; then
        RELOAD_REMOTE_SING_BOX_RULESET_SOURCES_PRESENT=1
        return 0
    fi

    config_get rule_set "$section" "rule_set"
    if list_has_remote_sing_box_rulesets "$rule_set"; then
        RELOAD_REMOTE_SING_BOX_RULESET_SOURCES_PRESENT=1
        return 0
    fi

    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    if list_has_remote_sing_box_rulesets "$rule_set_with_subnets"; then
        RELOAD_REMOTE_SING_BOX_RULESET_SOURCES_PRESENT=1
    fi
}

has_remote_sing_box_ruleset_sources() {
    RELOAD_REMOTE_SING_BOX_RULESET_SOURCES_PRESENT=0
    config_foreach enabled_rule_has_remote_sing_box_ruleset_source "section"
    [ "$RELOAD_REMOTE_SING_BOX_RULESET_SOURCES_PRESENT" -eq 1 ]
}

get_settings_update_interval() {
    local enabled update_interval

    config_get_bool enabled "settings" "list_update_enabled" 1
    if [ "$enabled" -eq 0 ]; then
        printf '\n'
        return 0
    fi

    config_get update_interval "settings" "update_interval" "1d"
    if [ -n "$update_interval" ]; then
        printf '%s\n' "$update_interval"
    else
        printf '1d\n'
    fi
}

get_subscription_update_interval_for_rule() {
    local section="$1"
    local enabled value

    rule_has_subscription_urls "$section" || {
        printf '\n'
        return 0
    }

    config_get_bool enabled "$section" "subscription_update_enabled" 1
    if [ "$enabled" -eq 0 ]; then
        printf '\n'
        return 0
    fi

    config_get value "$section" "subscription_update_interval"
    if [ -n "$value" ]; then
        printf '%s\n' "$value"
    else
        printf '1h\n'
    fi
}

get_urltest_check_interval_for_rule() {
    local section="$1"
    local enabled value

    config_get_bool enabled "$section" "urltest_enabled" 0
    if [ "$enabled" -eq 0 ]; then
        printf '\n'
        return 0
    fi

    config_get value "$section" "urltest_check_interval"
    if [ -n "$value" ]; then
        printf '%s\n' "$value"
    else
        printf '3m\n'
    fi
}

get_remote_ruleset_update_interval() {
    local update_interval

    update_interval="$(get_settings_update_interval)"
    if [ -n "$update_interval" ]; then
        printf '%s\n' "$update_interval"
    else
        printf '%s\n' "$SING_BOX_DISABLED_UPDATE_INTERVAL"
    fi

}

append_urltest_enabled_section() {
    local section="$1"
    local action enabled

    rule_is_enabled "$section" || return 0

    action="$(get_rule_action "$section")"
    [ "$action" = "proxy" ] || return 0

    config_get_bool enabled "$section" "urltest_enabled" 0
    [ "$enabled" -eq 1 ] || return 0

    RELOAD_STATE_URLTEST_ENABLED_SECTIONS="${RELOAD_STATE_URLTEST_ENABLED_SECTIONS}${RELOAD_STATE_URLTEST_ENABLED_SECTIONS:+ }$section"
}

build_urltest_enabled_sections() {
    RELOAD_STATE_URLTEST_ENABLED_SECTIONS=""
    config_foreach append_urltest_enabled_section "section"
    printf '%s\n' "$RELOAD_STATE_URLTEST_ENABLED_SECTIONS"
}

list_has_word() {
    local list="$1"
    local value="$2"

    case " $list " in
    *" $value "*) return 0 ;;
    esac

    return 1
}

build_new_urltest_enabled_sections() {
    local previous_sections="$1"
    local current_sections="$2"
    local section new_sections

    new_sections=""
    for section in $current_sections; do
        list_has_word "$previous_sections" "$section" && continue
        new_sections="${new_sections}${new_sections:+ }$section"
    done

    printf '%s\n' "$new_sections"
}

capture_reload_state() {
    config_load "$PODKOP_CONFIG_NAME"
    RELOAD_STATE_SERVICE_TRIGGER_SIGNATURE="$(build_service_trigger_signature)"
    RELOAD_STATE_DNSMASQ_SIGNATURE="$(build_dnsmasq_signature)"
    RELOAD_STATE_SING_BOX_SIGNATURE="$(build_sing_box_signature)"
    RELOAD_STATE_NFT_SIGNATURE="$(build_nft_signature)"
    RELOAD_STATE_ZAPRET_QUEUE_SIGNATURE="$(build_zapret_queue_signature)"
    RELOAD_STATE_ZAPRET_RUNTIME_SIGNATURE="$(build_zapret_runtime_signature)"
    RELOAD_STATE_BYEDPI_RUNTIME_SIGNATURE="$(build_byedpi_runtime_signature)"
    RELOAD_STATE_LIST_SIGNATURE="$(build_list_update_signature)"
    RELOAD_STATE_CRON_SIGNATURE="$(build_cron_signature)"
    RELOAD_STATE_URLTEST_ENABLED_SECTIONS="$(build_urltest_enabled_sections)"
    config_get_bool RELOAD_STATE_DONT_TOUCH_DHCP "settings" "dont_touch_dhcp" 0
}

write_reload_state() {
    mkdir -p "$PODKOP_RUNTIME_STATE_DIR"
    cat > "$PODKOP_RELOAD_STATE_FILE" <<EOF
format=$PODKOP_RELOAD_STATE_FORMAT
service_trigger_signature=$RELOAD_STATE_SERVICE_TRIGGER_SIGNATURE
dnsmasq_signature=$RELOAD_STATE_DNSMASQ_SIGNATURE
sing_box_signature=$RELOAD_STATE_SING_BOX_SIGNATURE
nft_signature=$RELOAD_STATE_NFT_SIGNATURE
zapret_queue_signature=$RELOAD_STATE_ZAPRET_QUEUE_SIGNATURE
zapret_runtime_signature=$RELOAD_STATE_ZAPRET_RUNTIME_SIGNATURE
byedpi_runtime_signature=$RELOAD_STATE_BYEDPI_RUNTIME_SIGNATURE
list_signature=$RELOAD_STATE_LIST_SIGNATURE
cron_signature=$RELOAD_STATE_CRON_SIGNATURE
urltest_enabled_sections=$RELOAD_STATE_URLTEST_ENABLED_SECTIONS
dont_touch_dhcp=$RELOAD_STATE_DONT_TOUCH_DHCP
EOF
}

enabled_rule_has_list_sources() {
    local section="$1"
    local community_lists community_subnet_lists remote_domain_lists remote_subnet_lists rule_set_with_subnets domain_ip_lists

    rule_is_enabled "$section" || return 0
    config_get community_lists "$section" "community_lists"
    community_subnet_lists="$(filter_community_subnet_lists "$community_lists")"
    config_get remote_domain_lists "$section" "remote_domain_lists"
    config_get remote_subnet_lists "$section" "remote_subnet_lists"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"

    if [ -n "$community_subnet_lists" ] ||
        [ -n "$remote_domain_lists" ] ||
        [ -n "$remote_subnet_lists" ] ||
        [ -n "$rule_set_with_subnets" ] ||
        list_has_remote_references "$domain_ip_lists"; then
        RELOAD_LIST_SOURCES_PRESENT=1
    fi
}

has_list_update_sources() {
    RELOAD_LIST_SOURCES_PRESENT=0
    config_foreach enabled_rule_has_list_sources "section"
    [ "$RELOAD_LIST_SOURCES_PRESENT" -eq 1 ]
}

enabled_rule_has_subscription_update_source() {
    local section="$1"
    local subscription_update_interval

    rule_is_subscription_proxy "$section" || return 0

    subscription_update_interval="$(get_subscription_update_interval_for_rule "$section")"
    [ -n "$subscription_update_interval" ] || return 0

    RELOAD_SUBSCRIPTION_SOURCES_PRESENT=1
}

has_subscription_update_sources() {
    RELOAD_SUBSCRIPTION_SOURCES_PRESENT=0
    config_foreach enabled_rule_has_subscription_update_source "section"
    [ "$RELOAD_SUBSCRIPTION_SOURCES_PRESENT" -eq 1 ]
}

enabled_rule_has_nft_list_sources() {
    local section="$1"
    local community_lists community_subnet_lists remote_subnet_lists rule_set_with_subnets domain_ip_lists

    rule_is_enabled "$section" || return 0
    config_get community_lists "$section" "community_lists"
    community_subnet_lists="$(filter_community_subnet_lists "$community_lists")"
    config_get remote_subnet_lists "$section" "remote_subnet_lists"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"

    if [ -n "$community_subnet_lists" ] ||
        [ -n "$remote_subnet_lists" ] ||
        [ -n "$rule_set_with_subnets" ] ||
        list_has_remote_references "$domain_ip_lists"; then
        RELOAD_NFT_LIST_SOURCES_PRESENT=1
    fi
}

has_nft_list_update_sources() {
    RELOAD_NFT_LIST_SOURCES_PRESENT=0
    config_foreach enabled_rule_has_nft_list_sources "section"
    [ "$RELOAD_NFT_LIST_SOURCES_PRESENT" -eq 1 ]
}

populate_nft_runtime_sets_from_rule() {
    local section="$1"
    local ip_values

    rule_is_enabled "$section" || return 0
    subscription_section_is_deferred "$section" && return 0

    ip_values="$(get_rule_condition_commas_string "$section" "ip_cidr" "subnets")"
    add_section_ip_cidr_to_nft_sets "$section" "$ip_values"
    add_section_ports_to_nft_set_if_needed "$section"
}

populate_nft_runtime_sets() {
    config_foreach populate_nft_runtime_sets_from_rule "section"
}

rebuild_nft_runtime() {
    log "Rebuilding nft runtime"

    if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
        nft delete table inet "$NFT_TABLE_NAME"
    fi

    route_table_rule_mark
    create_nft_rules
}

refresh_list_update_cron() {
    local cron_job update_interval

    remove_cron_job

    if has_list_update_sources; then
        update_interval="$(get_settings_update_interval)"
        if [ -z "$update_interval" ]; then
            log "Remote list auto-update is disabled"
        else
            cron_job="$(build_list_update_cron_job)" || return 1
            crontab -l 2> /dev/null | {
                cat
                echo "$cron_job"
            } | crontab -
            log "The cron job has been created: $cron_job"
        fi
    fi

    if has_subscription_update_sources; then
        cron_job="$(build_subscription_update_cron_job)" || return 1
        crontab -l 2> /dev/null | {
            cat
            echo "$cron_job"
        } | crontab -
        log "The subscription cron job has been created: $cron_job"
    fi
}

reload_sing_box_runtime() {
    log "Reloading sing-box runtime"

    if ! /etc/init.d/sing-box reload; then
        log "Failed to reload sing-box. Aborted." "fatal"
        exit 1
    fi
}

schedule_urltest_selector_switch() {
    local selector_tag="$1"
    local urltest_tag="$2"

    PODKOP_URLTEST_SELECTOR_SWITCHES="${PODKOP_URLTEST_SELECTOR_SWITCHES}${selector_tag} ${urltest_tag}
"
}

apply_urltest_selector_switch() {
    local selector_tag="$1"
    local urltest_tag="$2"
    local attempt response

    attempt=1
    while [ "$attempt" -le 10 ]; do
        response="$(clash_api set_group_proxy "$selector_tag" "$urltest_tag" 2>/dev/null || true)"
        if printf '%s' "$response" | json_utils_ucode response-success >/dev/null 2>&1; then
            log "Selected URLTest outbound '$urltest_tag' for selector '$selector_tag'"
            return 0
        fi

        sleep 1
        attempt=$((attempt + 1))
    done

    log "Failed to select URLTest outbound '$urltest_tag' for selector '$selector_tag'" "warn"
}

apply_pending_urltest_selector_switches() {
    local selector_tag urltest_tag

    [ -n "$PODKOP_URLTEST_SELECTOR_SWITCHES" ] || return 0

    printf '%s' "$PODKOP_URLTEST_SELECTOR_SWITCHES" | while read -r selector_tag urltest_tag; do
        [ -n "$selector_tag" ] || continue
        [ -n "$urltest_tag" ] || continue
        apply_urltest_selector_switch "$selector_tag" "$urltest_tag"
    done
}

get_rule_action() {
    local section="$1"
    local action

    config_get action "$section" "action"
    echo "$action"
}

rule_is_enabled() {
    local section="$1"
    local enabled

    config_get_bool enabled "$section" "enabled" 1
    [ "$enabled" -eq 1 ]
}

server_is_enabled() {
    local section="$1"
    local enabled

    config_get_bool enabled "$section" "enabled" 0
    [ "$enabled" -eq 1 ]
}

_validate_download_lists_via_proxy_section_handler() {
    local section="$1"
    local action

    [ "$section" = "$PODKOP_VALIDATE_DOWNLOAD_SECTION" ] || return 0

    PODKOP_VALIDATE_DOWNLOAD_SECTION_FOUND=1
    if ! rule_is_enabled "$section"; then
        return 0
    fi

    PODKOP_VALIDATE_DOWNLOAD_SECTION_ENABLED=1
    action="$(get_rule_action "$section")"
    if [ "$action" = "proxy" ] || [ "$action" = "vpn" ] || [ "$action" = "outbound" ]; then
        PODKOP_VALIDATE_DOWNLOAD_SECTION_OUTBOUND=1
    fi
}

validate_download_lists_via_proxy_section() {
    local download_lists_via_proxy download_lists_via_proxy_section

    config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
    [ "$download_lists_via_proxy" -eq 1 ] || return 0

    config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
    if [ -z "$download_lists_via_proxy_section" ]; then
        log "Download lists/updates/subscriptions via Proxy/VPN is enabled, but no proxy/VPN/JSON outbound rule is selected. Aborted." "fatal"
        exit 1
    fi

    PODKOP_VALIDATE_DOWNLOAD_SECTION="$download_lists_via_proxy_section"
    PODKOP_VALIDATE_DOWNLOAD_SECTION_FOUND=0
    PODKOP_VALIDATE_DOWNLOAD_SECTION_ENABLED=0
    PODKOP_VALIDATE_DOWNLOAD_SECTION_OUTBOUND=0
    config_foreach _validate_download_lists_via_proxy_section_handler "section"

    if [ "$PODKOP_VALIDATE_DOWNLOAD_SECTION_FOUND" -eq 0 ]; then
        log "Download lists/updates/subscriptions via Proxy/VPN references missing rule '$download_lists_via_proxy_section'. Select an enabled proxy/VPN/JSON outbound rule or disable the option. Aborted." "fatal"
        exit 1
    fi

    if [ "$PODKOP_VALIDATE_DOWNLOAD_SECTION_ENABLED" -eq 0 ]; then
        log "Download lists/updates/subscriptions via Proxy/VPN references disabled rule '$download_lists_via_proxy_section'. Select an enabled proxy/VPN/JSON outbound rule or disable the option. Aborted." "fatal"
        exit 1
    fi

    if [ "$PODKOP_VALIDATE_DOWNLOAD_SECTION_OUTBOUND" -eq 0 ]; then
        log "Download lists/updates/subscriptions via Proxy/VPN references rule '$download_lists_via_proxy_section', but it is not a proxy/VPN/JSON outbound rule. Select an enabled proxy/VPN/JSON outbound rule or disable the option. Aborted." "fatal"
        exit 1
    fi
}

validate_list_update_settings() {
    local enabled update_interval

    config_get_bool enabled "settings" "list_update_enabled" 1
    [ "$enabled" -eq 1 ] || return 0

    config_get update_interval "settings" "update_interval" "1d"
    [ -n "$update_interval" ] || update_interval="1d"
    validate_required_sing_box_duration_option "$update_interval" "settings.update_interval"
}

validate_runtime_settings() {
    validate_list_update_settings
    validate_download_lists_via_proxy_section
}

commit_podkop_config() {
    # Podkop updates a small runtime flag in its own UCI config while starting
    # and stopping. Mark these commits so the init trigger can ignore the
    # self-induced config.change event instead of scheduling another reload.
    date +%s > "$PODKOP_INTERNAL_CONFIG_TRIGGER_GUARD" 2>/dev/null || echo 0 > "$PODKOP_INTERNAL_CONFIG_TRIGGER_GUARD"
    uci commit "$PODKOP_CONFIG_NAME" && config_load "$PODKOP_CONFIG_NAME"
}

podkop_uci_option_exists() {
    local section="$1"
    local option="$2"

    uci -q get "$PODKOP_CONFIG_NAME.$section.$option" >/dev/null 2>&1
}

podkop_uci_set_option() {
    local section="$1"
    local option="$2"
    local value="$3"
    local current

    current="$(uci -q get "$PODKOP_CONFIG_NAME.$section.$option" 2>/dev/null)"
    [ "$current" = "$value" ] && return 0

    uci set "$PODKOP_CONFIG_NAME.$section.$option=$value"
    podkop_config_migration_changed=1
}

podkop_uci_set_option_if_missing() {
    local section="$1"
    local option="$2"
    local value="$3"

    podkop_uci_option_exists "$section" "$option" && return 0
    podkop_uci_set_option "$section" "$option" "$value"
}

podkop_uci_migrate_urltest_filter_mode() {
    local section="$1"

    podkop_uci_option_exists "$section" "urltest_filter_mode" && return 0

    if podkop_uci_option_exists "$section" "urltest_exclude_countries" ||
        podkop_uci_option_exists "$section" "urltest_include_countries" ||
        podkop_uci_option_exists "$section" "urltest_exclude_outbounds" ||
        podkop_uci_option_exists "$section" "urltest_exclude_regex"; then
        podkop_uci_set_option "$section" "urltest_filter_mode" "exclude"
    fi
}

podkop_uci_delete_option() {
    local section="$1"
    local option="$2"

    podkop_uci_option_exists "$section" "$option" || return 0

    uci -q delete "$PODKOP_CONFIG_NAME.$section.$option" || true
    podkop_config_migration_changed=1
}

podkop_uci_list_contains_handler() {
    local item="$1"
    local expected="$2"

    [ "$item" = "$expected" ] && UCI_LIST_CONTAINS=1
}

podkop_uci_add_list_unique() {
    local section="$1"
    local option="$2"
    local value="$3"
    local quoted_value list_key

    [ -n "$value" ] || return 0
    list_key="$section.$option=$value"

    UCI_LIST_CONTAINS=0
    config_list_foreach "$section" "$option" podkop_uci_list_contains_handler "$value"
    [ "$UCI_LIST_CONTAINS" -eq 1 ] && return 0
    case "
$podkop_config_migration_added_lists
" in
    *"
$list_key
"*)
        return 0
        ;;
    esac

    quoted_value="$(printf "%s" "$value" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/")"
    printf 'add_list %s.%s.%s=%s\n' "$PODKOP_CONFIG_NAME" "$section" "$option" "$quoted_value" | uci -q batch
    podkop_config_migration_added_lists="${podkop_config_migration_added_lists}
${list_key}"
    podkop_config_migration_changed=1
}

migrate_0_7_17_8_urltest_link() {
    local link="$1"
    local section="$2"

    podkop_uci_add_list_unique "$section" "selector_proxy_links" "$link"
}

migrate_0_7_17_8_proxy_string() {
    local section="$1"
    local proxy_string link migrated

    config_get proxy_string "$section" "proxy_string"
    [ -n "$proxy_string" ] || return 0

    migrated=0
    while IFS= read -r link || [ -n "$link" ]; do
        link="$(printf '%s' "$link" | trim_string)"
        [ -n "$link" ] || continue
        case "$link" in
        '//'*) continue ;;
        esac

        podkop_uci_add_list_unique "$section" "selector_proxy_links" "$link"
        migrated=1
    done <<EOF
$proxy_string
EOF

    [ "$migrated" -eq 1 ] && podkop_uci_delete_option "$section" "proxy_string"
}

migrate_0_7_17_8_delete_subscription_cache() {
    local section="$1"

    case "$section" in
    "" | */* | *..*) return 0 ;;
    esac

    rm -f \
        "$(get_subscription_json_path "$section")" \
        "$(get_subscription_url_cache_path "$section")" \
        "$(get_subscription_user_agent_cache_path "$section")" \
        "$(get_subscription_metadata_path "$section")" \
        "$(get_subscription_links_path "$section")" \
        "$(get_outbound_metadata_path "$section")" \
        "$(get_section_cache_path "$section")" \
        "$TMP_SUBSCRIPTION_FOLDER/${section}-subscription-"*.json \
        "$TMP_SUBSCRIPTION_FOLDER/${section}-subscription-"*.url \
        "$TMP_SUBSCRIPTION_FOLDER/${section}-subscription-"*.user_agent
}

migrate_0_7_17_8_subscription_url() {
    local section="$1"
    local subscription_url subscription_user_agent subscription_entry

    config_get subscription_url "$section" "subscription_url"
    [ -n "$subscription_url" ] || return 0

    config_get subscription_user_agent "$section" "subscription_user_agent" ""
    if [ -n "$subscription_user_agent" ]; then
        subscription_entry="$subscription_url | $subscription_user_agent"
    else
        subscription_entry="$subscription_url"
    fi

    podkop_uci_add_list_unique "$section" "subscription_urls" "$subscription_entry"
    podkop_uci_delete_option "$section" "subscription_url"
    podkop_uci_delete_option "$section" "subscription_user_agent"
    migrate_0_7_17_8_delete_subscription_cache "$section"
}

migrate_0_7_17_8_interval_flags() {
    local section="$1"
    local proxy_config_type="$2"
    local urltest_interval_disabled subscription_update_interval_disabled

    if [ "$proxy_config_type" = "urltest" ] || [ "$proxy_config_type" = "subscription" ]; then
        config_get urltest_interval_disabled "$section" "urltest_check_interval_disabled"
        if [ "$urltest_interval_disabled" = "1" ]; then
            podkop_uci_set_option "$section" "urltest_enabled" "0"
        else
            podkop_uci_set_option_if_missing "$section" "urltest_enabled" "1"
        fi
    elif [ "$proxy_config_type" = "url" ] || [ "$proxy_config_type" = "selector" ]; then
        podkop_uci_set_option_if_missing "$section" "urltest_enabled" "0"
    fi

    if [ "$proxy_config_type" = "subscription" ]; then
        config_get subscription_update_interval_disabled "$section" "subscription_update_interval_disabled"
        if [ "$subscription_update_interval_disabled" = "1" ]; then
            podkop_uci_set_option "$section" "subscription_update_enabled" "0"
        else
            podkop_uci_set_option_if_missing "$section" "subscription_update_enabled" "1"
        fi
    fi

    podkop_uci_delete_option "$section" "urltest_check_interval_disabled"
    podkop_uci_delete_option "$section" "subscription_update_interval_disabled"
}

migrate_0_7_17_8_proxy_rule() {
    local section="$1"
    local proxy_config_type="$2"

    case "$proxy_config_type" in
    url)
        migrate_0_7_17_8_proxy_string "$section"
        ;;
    urltest)
        config_list_foreach "$section" "urltest_proxy_links" migrate_0_7_17_8_urltest_link "$section"
        podkop_uci_delete_option "$section" "urltest_proxy_links"
        ;;
    subscription)
        migrate_0_7_17_8_subscription_url "$section"
        migrate_0_7_17_8_delete_subscription_cache "$section"
        ;;
    esac

    migrate_0_7_17_8_interval_flags "$section" "$proxy_config_type"
    podkop_uci_delete_option "$section" "proxy_config_type"
}

migrate_0_7_17_8_rule_action() {
    local section="$1"
    local action proxy_config_type connection_type interface outbound_json selector_proxy_links subscription_urls

    config_get action "$section" "action"
    config_get proxy_config_type "$section" "proxy_config_type"
    config_get connection_type "$section" "connection_type"
    config_get interface "$section" "interface"
    config_get outbound_json "$section" "outbound_json"
    config_get selector_proxy_links "$section" "selector_proxy_links"
    config_get subscription_urls "$section" "subscription_urls"

    if [ "$action" = "proxy" ]; then
        case "$proxy_config_type" in
        interface)
            echo "vpn"
            return 0
            ;;
        outbound)
            echo "outbound"
            return 0
            ;;
        esac
    fi

    if [ -n "$action" ]; then
        echo "$action"
        return 0
    fi

    case "$connection_type" in
    proxy)
        case "$proxy_config_type" in
        interface)
            echo "vpn"
            ;;
        outbound)
            echo "outbound"
            ;;
        *)
            echo "proxy"
            ;;
        esac
        return 0
        ;;
    vpn)
        echo "vpn"
        return 0
        ;;
    block)
        echo "block"
        return 0
        ;;
    exclusion)
        echo "direct"
        return 0
        ;;
    esac

    case "$proxy_config_type" in
    interface)
        echo "vpn"
        ;;
    outbound)
        echo "outbound"
        ;;
    url | selector | urltest | subscription)
        echo "proxy"
        ;;
    *)
        if [ -n "$outbound_json" ]; then
            echo "outbound"
        elif [ -n "$interface" ]; then
            echo "vpn"
        elif [ -n "$selector_proxy_links" ] || [ -n "$subscription_urls" ]; then
            echo "proxy"
        else
            echo ""
        fi
        ;;
    esac
}

migrate_0_7_17_8_byedpi_cmd_opts() {
    local section="$1"
    local cmd_opts byedpi_cmd_opts

    config_get cmd_opts "$section" "cmd_opts"
    [ -n "$cmd_opts" ] || return 0

    config_get byedpi_cmd_opts "$section" "byedpi_cmd_opts"
    [ -z "$byedpi_cmd_opts" ] && podkop_uci_set_option "$section" "byedpi_cmd_opts" "$cmd_opts"
    podkop_uci_delete_option "$section" "cmd_opts"
}

migrate_0_7_17_8_zapret_nfqws_default() {
    local section="$1"
    local action nfqws_opt

    action="$(get_rule_action "$section")"
    [ "$action" = "zapret" ] || return 0

    config_get nfqws_opt "$section" "nfqws_opt"
    [ -n "$nfqws_opt" ] || return 0
    [ "$nfqws_opt" = "$ZAPRET_LEGACY_DEFAULT_NFQWS_OPT" ] || return 0

    podkop_uci_set_option "$section" "nfqws_opt" "$ZAPRET_DEFAULT_NFQWS_OPT"
}

migrate_0_7_17_8_rule() {
    local section="$1"
    local converted_from_rule="${2:-0}"
    local action proxy_config_type subscription_urls

    action="$(migrate_0_7_17_8_rule_action "$section")"
    config_get proxy_config_type "$section" "proxy_config_type"
    config_get subscription_urls "$section" "subscription_urls"

    [ -n "$action" ] && podkop_uci_set_option "$section" "action" "$action"

    podkop_uci_delete_option "$section" "connection_type"
    podkop_uci_delete_option "$section" "subscription_group_by_countries"
    podkop_uci_delete_option "$section" "group_by_countries"
    podkop_uci_delete_option "$section" "subscription_detect_server_countries"

    case "$action" in
    proxy)
        podkop_uci_migrate_urltest_filter_mode "$section"
        migrate_0_7_17_8_proxy_rule "$section" "$proxy_config_type"
        if [ "$converted_from_rule" -eq 1 ] && [ -n "$subscription_urls" ]; then
            migrate_0_7_17_8_delete_subscription_cache "$section"
        fi
        ;;
    vpn | outbound | block | direct | zapret | byedpi)
        podkop_uci_delete_option "$section" "proxy_config_type"
        podkop_uci_delete_option "$section" "proxy_string"
        podkop_uci_delete_option "$section" "urltest_proxy_links"
        podkop_uci_delete_option "$section" "subscription_url"
        podkop_uci_delete_option "$section" "subscription_user_agent"
        podkop_uci_delete_option "$section" "urltest_check_interval_disabled"
        podkop_uci_delete_option "$section" "subscription_update_interval_disabled"
        ;;
    esac

    migrate_0_7_17_8_byedpi_cmd_opts "$section"
    migrate_0_7_17_8_zapret_nfqws_default "$section"
}

migrate_0_7_17_8_rule_section() {
    local section="$1"

    uci set "$PODKOP_CONFIG_NAME.$section=section"
    podkop_config_migration_changed=1
    migrate_0_7_17_8_rule "$section" 1
}

migrate_list_update_enabled() {
    local list_update_enabled update_interval

    if podkop_uci_option_exists "settings" "list_update_enabled"; then
        config_get_bool list_update_enabled "settings" "list_update_enabled" 1
        if [ "$list_update_enabled" -eq 1 ]; then
            config_get update_interval "settings" "update_interval"
            [ -n "$update_interval" ] || podkop_uci_set_option "settings" "update_interval" "1d"
        fi
        return 0
    fi

    config_get update_interval "settings" "update_interval"
    if [ -n "$update_interval" ]; then
        podkop_uci_set_option "settings" "list_update_enabled" "1"
    else
        podkop_uci_set_option "settings" "list_update_enabled" "0"
        podkop_uci_set_option "settings" "update_interval" "1d"
    fi
}

check_requirements() {
    log "Check Requirements"

    local sing_box_version coreutils_base64_version
    sing_box_version="$(sing-box version | head -n1 | awk '{print $NF}')"
    coreutils_base64_version="$(base64 --version 2>/dev/null | awk 'NR == 1 {print $4}')"

    if [ -z "$sing_box_version" ]; then
        log "Package 'sing-box' is not installed. Aborted." "error"
        exit 1
    else
        if ! is_min_package_version "$sing_box_version" "$SB_REQUIRED_VERSION"; then
            log "Package 'sing-box' version ($sing_box_version) is lower than the required minimum ($SB_REQUIRED_VERSION). Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box. Aborted." "error"
            exit 1
        fi

        if ! service_exists "sing-box"; then
            log "Service 'sing-box' is missing. Please install the official package to ensure the service is available. Aborted." "error"
            exit 1
        fi
    fi

    if [ -z "$coreutils_base64_version" ]; then
        log "Package 'coreutils-base64' is not installed. Aborted." "error"
        exit 1
    elif ! is_min_package_version "$coreutils_base64_version" "$COREUTILS_BASE64_REQUIRED_VERSION"; then
        log "Package 'coreutils-base64' version ($coreutils_base64_version) is lower than the required minimum ($COREUTILS_BASE64_REQUIRED_VERSION). This may cause issues when decoding base64 streams with missing padding, as automatic padding support is not available in older versions." "warn"
    fi

    if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then
        log "Detected https-dns-proxy in DHCP config. Edit /etc/config/dhcp" "error"
    fi

    if has_outbound_section; then
        log "Outbound proxy section found" "debug"
    else
        log "No proxy outbound sections found. Podkop Plus will use direct and/or provider-only routing." "warn"
    fi

    check_zapret_requirements
    check_byedpi_requirements
}

_check_outbound_node() {
    local section="$1"
    local action interface outbound_json selector_proxy_links subscription_urls

    rule_is_enabled "$section" || return 0

    action="$(get_rule_action "$section")"
    config_get selector_proxy_links "$section" "selector_proxy_links"
    config_get subscription_urls "$section" "subscription_urls"
    config_get outbound_json "$section" "outbound_json"
    config_get interface "$section" "interface"

    if [ "$action" = "byedpi" ] && is_byedpi_installed; then
        section_exists=0
        return 0
    fi

    if [ -n "$selector_proxy_links" ] || [ -n "$subscription_urls" ] ||
        [ -n "$outbound_json" ] || [ -n "$interface" ]; then
        section_exists=0
    fi
}

has_outbound_section() {
    local section_exists=1

    config_foreach _check_outbound_node "section"

    return $section_exists
}

mwan3_has_enabled_interface() {
    local section

    [ -s /etc/config/mwan3 ] || return 1

    for section in $(uci -q show mwan3 2>/dev/null | sed -n 's/^mwan3\.\([^=]*\)=interface$/\1/p'); do
        [ "$(uci -q get "mwan3.$section.enabled" 2>/dev/null)" = "1" ] && return 0
    done

    return 1
}

mwan3_is_active() {
    [ -x /etc/init.d/mwan3 ] || return 1
    mwan3_has_enabled_interface || return 1

    /etc/init.d/mwan3 status >/dev/null 2>&1 ||
        /etc/init.d/mwan3 enabled >/dev/null 2>&1
}

start_main() {
    log "Starting Podkop Plus"

    migration

    check_requirements

    prepare_all_server_defaults
    config_foreach process_validate_rule "section"
    validate_runtime_settings

    br_netfilter_disable

    # Sync time only when the system clock is clearly invalid.
    sync_time_if_needed

    sleep 1

    ensure_runtime_dirs
    if ! prepare_subscription_caches_for_startup; then
        log "Subscription caches are not ready. Aborted." "fatal"
        exit 1
    fi

    # base
    route_table_rule_mark
    create_nft_rules
    sing_box_configure_service

    # sing-box
    sing_box_init_config
    refresh_list_update_cron
    start_byedpi_runtime
    /etc/init.d/sing-box start
    apply_pending_urltest_selector_switches
    run_deferred_subscription_bootstrap
    start_zapret_runtime

    (close_inherited_service_lock_fd; list_update) &
}

stop_main() {
    log "Stopping Podkop Plus"

    stop_deferred_subscription_bootstrap_retry_worker

    if [ -f /var/run/podkop_list_update.pid ]; then
        pid=$(cat /var/run/podkop_list_update.pid)
        if kill -0 "$pid" 2> /dev/null; then
            kill "$pid" 2> /dev/null
            log "Stopped list_update"
        fi
        rm -f /var/run/podkop_list_update.pid
    fi

    remove_cron_job

    rm -f "$TMP_RULESET_FOLDER"/*

    stop_zapret_runtime
    stop_byedpi_runtime

    log "Flush nft"
    if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
        nft delete table inet "$NFT_TABLE_NAME"
    fi

    log "Flush ip rule"
    if ip rule list | grep -q "$RT_TABLE_NAME"; then
        ip rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105
    fi

    log "Flush ip route"
    if ip route list table "$RT_TABLE_NAME" > /dev/null 2>&1; then
        ip route flush table "$RT_TABLE_NAME"
    fi

    log "Stop sing-box"
    /etc/init.d/sing-box stop
}

start_impl() {
    start_main

    config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
    if [ "$dont_touch_dhcp" -eq 0 ]; then
        dnsmasq_configure
    fi

    uci_set "$PODKOP_CONFIG_NAME" "settings" "shutdown_correctly" 0
    commit_podkop_config
    capture_reload_state
    write_reload_state
    (refresh_system_info_cache) >/dev/null 2>&1 &
}

start() {
    local status

    start_impl
    status="$?"

    [ "$status" -eq 0 ] && podkop_is_stably_running && run_pending_reload_if_requested
    return "$status"
}

stop_impl() {
    local dont_touch_dhcp
    config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
    if [ "$dont_touch_dhcp" -eq 0 ]; then
        dnsmasq_restore
    fi

    stop_main

    uci_set "$PODKOP_CONFIG_NAME" "settings" "shutdown_correctly" 1
    commit_podkop_config
    clear_reload_state
}

stop() {
    local status

    stop_impl
    status="$?"

    return "$status"
}

restart_runtime_for_reload() {
    local dont_touch_dhcp

    log "Reload fallback: restart Podkop Plus runtime in-process"

    stop_main
    start_main

    config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
    if [ "$dont_touch_dhcp" -eq 0 ]; then
        dnsmasq_configure force
    else
        dnsmasq_restore force
    fi

    uci_set "$PODKOP_CONFIG_NAME" "settings" "shutdown_correctly" 0
    commit_podkop_config
    capture_reload_state
    write_reload_state
}

reload() {
    local reload_reason="${1:-}"
    local status
    local force_runtime_reload=0
    local previous_service_trigger_signature previous_dnsmasq_signature previous_sing_box_signature previous_nft_signature
    local previous_zapret_queue_signature previous_zapret_runtime_signature previous_byedpi_runtime_signature previous_list_signature previous_cron_signature
    local previous_urltest_enabled_sections previous_urltest_enabled_sections_known previous_dont_touch_dhcp needs_sing_box_reload needs_nft_rebuild needs_zapret_restart needs_byedpi_restart
    local needs_dnsmasq_configure needs_dnsmasq_restore needs_cron_refresh needs_list_update
    local changed_service_triggers changed_dnsmasq changed_sing_box changed_nft changed_zapret_queue changed_zapret_runtime changed_byedpi_runtime changed_cron changed_list

    log "Podkop Plus reload"

    [ "$reload_reason" = "on_config_change" ] || force_runtime_reload=1

    migration
    config_load "$PODKOP_CONFIG_NAME"
    check_requirements
    prepare_all_server_defaults
    config_foreach process_validate_rule "section"
    validate_runtime_settings
    ensure_runtime_dirs

    if ! podkop_is_running; then
        log "Podkop Plus runtime is incomplete, falling back to in-process restart"
        restart_runtime_for_reload
        status="$?"
        return "$status"
    fi

    if ! reload_state_is_compatible; then
        log "Reload state is missing or incompatible, falling back to in-process restart"
        restart_runtime_for_reload
        status="$?"
        return "$status"
    fi

    previous_service_trigger_signature="$(read_reload_state_value "service_trigger_signature")"
    [ -n "$previous_service_trigger_signature" ] ||
        previous_service_trigger_signature="$(read_reload_state_value "restart_signature")"
    previous_dnsmasq_signature="$(read_reload_state_value "dnsmasq_signature")"
    previous_sing_box_signature="$(read_reload_state_value "sing_box_signature")"
    previous_nft_signature="$(read_reload_state_value "nft_signature")"
    previous_zapret_queue_signature="$(read_reload_state_value "zapret_queue_signature")"
    previous_zapret_runtime_signature="$(read_reload_state_value "zapret_runtime_signature")"
    previous_byedpi_runtime_signature="$(read_reload_state_value "byedpi_runtime_signature")"
    previous_list_signature="$(read_reload_state_value "list_signature")"
    previous_cron_signature="$(read_reload_state_value "cron_signature")"
    previous_urltest_enabled_sections_known=0
    if reload_state_has_key "urltest_enabled_sections"; then
        previous_urltest_enabled_sections_known=1
        previous_urltest_enabled_sections="$(read_reload_state_value "urltest_enabled_sections")"
    else
        previous_urltest_enabled_sections=""
    fi
    previous_dont_touch_dhcp="$(read_reload_state_value "dont_touch_dhcp")"
    capture_reload_state
    if [ "$previous_urltest_enabled_sections_known" -eq 1 ]; then
        PODKOP_URLTEST_NEW_ENABLED_SECTIONS="$(
            build_new_urltest_enabled_sections "$previous_urltest_enabled_sections" "$RELOAD_STATE_URLTEST_ENABLED_SECTIONS"
        )"
    else
        PODKOP_URLTEST_NEW_ENABLED_SECTIONS=""
    fi

    needs_sing_box_reload=0
    needs_nft_rebuild=0
    needs_zapret_restart=0
    needs_byedpi_restart=0
    needs_dnsmasq_configure=0
    needs_dnsmasq_restore=0
    needs_cron_refresh=0
    needs_list_update=0
    changed_service_triggers=0
    changed_dnsmasq=0
    changed_sing_box=0
    changed_nft=0
    changed_zapret_queue=0
    changed_zapret_runtime=0
    changed_byedpi_runtime=0
    changed_cron=0
    changed_list=0

    [ "$RELOAD_STATE_SERVICE_TRIGGER_SIGNATURE" != "$previous_service_trigger_signature" ] && changed_service_triggers=1

    if [ "$RELOAD_STATE_SING_BOX_SIGNATURE" != "$previous_sing_box_signature" ]; then
        changed_sing_box=1
        needs_sing_box_reload=1
    fi

    if [ "$RELOAD_STATE_NFT_SIGNATURE" != "$previous_nft_signature" ]; then
        changed_nft=1
        needs_nft_rebuild=1
    fi

    if [ "$RELOAD_STATE_ZAPRET_QUEUE_SIGNATURE" != "$previous_zapret_queue_signature" ]; then
        changed_zapret_queue=1
        needs_zapret_restart=1
        needs_nft_rebuild=1
        needs_sing_box_reload=1
    fi

    if [ "$RELOAD_STATE_ZAPRET_RUNTIME_SIGNATURE" != "$previous_zapret_runtime_signature" ]; then
        changed_zapret_runtime=1
        needs_zapret_restart=1
    fi

    if [ "$RELOAD_STATE_BYEDPI_RUNTIME_SIGNATURE" != "$previous_byedpi_runtime_signature" ]; then
        changed_byedpi_runtime=1
        needs_byedpi_restart=1
    fi

    if [ "$RELOAD_STATE_DNSMASQ_SIGNATURE" != "$previous_dnsmasq_signature" ]; then
        changed_dnsmasq=1
        if [ "$RELOAD_STATE_DONT_TOUCH_DHCP" -eq 0 ]; then
            needs_dnsmasq_configure=1
        elif [ "$previous_dont_touch_dhcp" = "0" ]; then
            needs_dnsmasq_restore=1
        fi
    fi

    if [ "$RELOAD_STATE_CRON_SIGNATURE" != "$previous_cron_signature" ]; then
        changed_cron=1
        needs_cron_refresh=1
    fi

    if [ "$RELOAD_STATE_LIST_SIGNATURE" != "$previous_list_signature" ]; then
        changed_list=1
        has_list_update_sources && needs_list_update=1
    fi

    if [ "$needs_nft_rebuild" -eq 1 ] && has_nft_list_update_sources; then
        needs_list_update=1
    fi

    if [ "$force_runtime_reload" -eq 1 ] &&
        [ "$needs_sing_box_reload" -eq 0 ] &&
        [ "$needs_nft_rebuild" -eq 0 ] &&
        [ "$needs_zapret_restart" -eq 0 ] &&
        [ "$needs_byedpi_restart" -eq 0 ] &&
        [ "$needs_dnsmasq_configure" -eq 0 ] &&
        [ "$needs_dnsmasq_restore" -eq 0 ]; then
        needs_sing_box_reload=1
    fi

    log "Reload signature changes: service_triggers=$changed_service_triggers sing-box=$changed_sing_box nft=$changed_nft zapret_queue=$changed_zapret_queue zapret_runtime=$changed_zapret_runtime byedpi_runtime=$changed_byedpi_runtime dnsmasq=$changed_dnsmasq cron=$changed_cron list=$changed_list forced=$force_runtime_reload" "debug"
    log "Reload plan: sing-box=$needs_sing_box_reload nft=$needs_nft_rebuild zapret=$needs_zapret_restart byedpi=$needs_byedpi_restart dnsmasq_configure=$needs_dnsmasq_configure dnsmasq_restore=$needs_dnsmasq_restore cron=$needs_cron_refresh list_update=$needs_list_update" "debug"

    if [ "$needs_sing_box_reload" -eq 0 ] &&
        [ "$needs_nft_rebuild" -eq 0 ] &&
        [ "$needs_zapret_restart" -eq 0 ] &&
        [ "$needs_byedpi_restart" -eq 0 ] &&
        [ "$needs_dnsmasq_configure" -eq 0 ] &&
        [ "$needs_dnsmasq_restore" -eq 0 ] &&
        [ "$needs_cron_refresh" -eq 0 ] &&
        [ "$needs_list_update" -eq 0 ]; then
        write_reload_state
        log "Reload skipped: runtime-relevant configuration is unchanged"
        return 0
    fi

    if [ "$needs_dnsmasq_restore" -eq 1 ]; then
        dnsmasq_restore force
    fi

    if [ "$needs_zapret_restart" -eq 1 ]; then
        stop_zapret_runtime
    fi

    if [ "$needs_byedpi_restart" -eq 1 ]; then
        stop_byedpi_runtime
    fi

    if [ "$needs_nft_rebuild" -eq 1 ]; then
        rebuild_nft_runtime
    fi

    if [ "$needs_sing_box_reload" -eq 1 ]; then
        sing_box_configure_service
        if [ "$needs_nft_rebuild" -eq 1 ]; then
            PODKOP_NFT_POPULATE_ENABLED=1
        else
            PODKOP_NFT_POPULATE_ENABLED=0
        fi
        sing_box_init_config
        PODKOP_NFT_POPULATE_ENABLED=1
        reload_sing_box_runtime
        apply_pending_urltest_selector_switches
    elif [ "$needs_nft_rebuild" -eq 1 ]; then
        populate_nft_runtime_sets
    fi

    if [ "$needs_zapret_restart" -eq 1 ]; then
        start_zapret_runtime
    fi

    if [ "$needs_byedpi_restart" -eq 1 ]; then
        start_byedpi_runtime
    fi

    if [ "$needs_dnsmasq_configure" -eq 1 ]; then
        dnsmasq_configure force
    fi

    if [ "$needs_cron_refresh" -eq 1 ]; then
        refresh_list_update_cron
    fi

    if [ "$needs_list_update" -eq 1 ]; then
        (close_inherited_service_lock_fd; list_update) &
    fi

    write_reload_state
    return 0
}

restart() {
    local status

    log "Podkop Plus restart"

    stop_impl
    status="$?"
    if [ "$status" -ne 0 ]; then
        return "$status"
    fi

    start_impl
    status="$?"

    [ "$status" -eq 0 ] && podkop_is_stably_running && run_pending_reload_if_requested
    return "$status"
}

package_manager_remove_if_installed() {
    local package_name="$1"

    if command -v apk >/dev/null 2>&1; then
        apk info -e "$package_name" >/dev/null 2>&1 && apk del "$package_name" >/dev/null 2>&1 || true
        return 0
    fi

    if command -v opkg >/dev/null 2>&1; then
        opkg list-installed | grep -Eq "^${package_name}[[:space:]-]" && \
            opkg remove --force-depends "$package_name" >/dev/null 2>&1 || true
    fi
}

uninstall() {
    log "Uninstalling Podkop Plus"

    if [ -x "$PODKOP_SERVICE_INIT" ]; then
        stop >/dev/null 2>&1 || true
        "$PODKOP_SERVICE_INIT" disable >/dev/null 2>&1 || true
    fi

    if [ -x /etc/init.d/podkop ]; then
        /etc/init.d/podkop stop >/dev/null 2>&1 || true
        /etc/init.d/podkop disable >/dev/null 2>&1 || true
    fi

    package_manager_remove_if_installed "luci-i18n-podkop-plus-ru"
    package_manager_remove_if_installed "luci-app-podkop-plus"

    rm -rf /usr/lib/podkop-plus
    rm -rf "$PODKOP_LUCI_VIEW_DIR"
    rm -f "$PODKOP_SERVICE_INIT"
    rm -f "$PODKOP_BIN"
    rm -f /usr/share/luci/menu.d/luci-app-podkop-plus.json
    rm -f /usr/share/rpcd/acl.d/luci-app-podkop-plus.json
    rm -f /etc/uci-defaults/50_luci-podkop-plus
    rm -f "/usr/lib/lua/luci/i18n/${PODKOP_LUCI_I18N_DOMAIN}."*.lmo
    rm -f "/usr/lib/lua/luci/i18n/${PODKOP_LUCI_I18N_DOMAIN}.ru.lua"
    rm -f "/usr/lib/lua/luci/i18n/${PODKOP_LUCI_I18N_DOMAIN}.en.lua"
    rm -f /var/luci-indexcache*
    rm -f /tmp/luci-indexcache*
    [ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload >/dev/null 2>&1 || true

    printf '%s\n' '{"removed":true}'
}

# Migrations and validation funcs
migration() {
    podkop_config_migration_changed=0
    podkop_config_migration_added_lists=""

    ensure_runtime_cache_format
    remove_legacy_server_country_cache
    migrate_list_update_enabled

    config_foreach migrate_0_7_17_8_rule_section "rule"
    config_foreach migrate_0_7_17_8_rule "section"

    if [ "$podkop_config_migration_changed" -eq 1 ]; then
        log "Migrated Podkop Plus UCI config"
        commit_podkop_config
    fi
}

validate_service() {
    local service="$1"

    for community_service in $COMMUNITY_SERVICES; do
        if [ "$service" = "$community_service" ]; then
            return 0
        fi
    done

    log "Invalid service in community lists: $service. Check config and LuCI cache. Aborted." "fatal"
    exit 1
}

validate_ruleset_reference() {
    local reference="$1"

    if [ -z "$reference" ]; then
        return 0
    fi

    if printf '%s\n' "$COMMUNITY_SERVICES" | tr ' ' '\n' | grep -Fxq "$reference"; then
        return 0
    fi

    case "$reference" in
    http://* | https://*)
        return 0
        ;;
    /*.srs | /*.json)
        return 0
        ;;
    esac

    log "Unknown rule set reference '$reference'. Aborted." "fatal"
    exit 1
}

validate_plain_domain_ip_list_reference() {
    local reference="$1"

    if [ -z "$reference" ]; then
        return 0
    fi

    case "$reference" in
    http://* | https://*)
        return 0
        ;;
    /*.lst)
        return 0
        ;;
    esac

    log "Unknown plain list reference '$reference'. Aborted." "fatal"
    exit 1
}

validate_sing_box_duration_option() {
    local value="$1"
    local label="$2"

    [ -z "$value" ] && return 0

    if duration_to_seconds "$value" >/dev/null 2>&1; then
        return 0
    fi

    log "Invalid duration value for $label: $value. Use sing-box duration format like 1d, 12h or 30m. Aborted." "fatal"
    exit 1
}

validate_required_sing_box_duration_option() {
    local value="$1"
    local label="$2"

    if [ -z "$value" ]; then
        log "Missing duration value for $label. Use sing-box duration format like 1d, 12h or 30m. Aborted." "fatal"
        exit 1
    fi

    validate_sing_box_duration_option "$value" "$label"
}

validate_subscription_source_entry() {
    local entry="$1"
    local section="$2"

    if ! parse_subscription_source_entry "$entry"; then
        log "Invalid subscription URL in rule '$section': $SUBSCRIPTION_SOURCE_PARSE_ERROR. Aborted." "fatal"
        exit 1
    fi
}

validate_country_code_option() {
    local value="$1"
    local section="$2"
    local normalized

    [ -n "$value" ] || return 0
    normalized="$(printf '%s' "$value" | tr 'a-z' 'A-Z')"
    if printf '%s' "$normalized" | grep -Eq '^[A-Z][A-Z]$'; then
        return 0
    fi

    log "Invalid country code '$value' in rule '$section'. Aborted." "fatal"
    exit 1
}

validate_urltest_filter_mode_option() {
    local value="$1"
    local section="$2"

    [ -n "$value" ] || return 0
    case "$value" in
    disabled | exclude | include | mixed)
        return 0
        ;;
    esac

    log "Invalid URLTest filter mode '$value' in rule '$section'. Aborted." "fatal"
    exit 1
}

validate_urltest_regex_option() {
    local value="$1"
    local section="$2"
    local grep_status

    [ -n "$value" ] || return 0
    printf '%s\n' "__podkop_regex_validation__" | grep -E -e "$value" >/dev/null 2>&1
    grep_status=$?
    if [ "$grep_status" -ne 2 ]; then
        return 0
    fi

    log "Invalid URLTest regular expression '$value' in rule '$section'. Aborted." "fatal"
    exit 1
}

ruleset_kind_from_reference_hint() {
    local reference="$1"

    case "$reference" in
    *geosite*|*domain*|*domains*|*adguard*|*filter*)
        echo "domains"
        ;;
    *geoip*|*subnet*|*subnets*|*cidr*)
        echo "subnets"
        ;;
    *)
        echo "unknown"
        ;;
    esac
}

json_ruleset_has_domain_matchers() {
    local filepath="$1"

    ucode "$PODKOP_LIB/rulesets.uc" has-domain-matchers "$filepath" >/dev/null 2>&1
}

detect_local_binary_ruleset_kind() {
    local filepath="$1"
    local json_tmpfile kind

    kind="$(ruleset_kind_from_reference_hint "$filepath")"
    if [ "$kind" != "unknown" ]; then
        echo "$kind"
        return 0
    fi

    if ! file_exists "$filepath"; then
        echo "unknown"
        return 0
    fi

    json_tmpfile="$(mktemp)"
    if ! decompile_binary_ruleset "$filepath" "$json_tmpfile"; then
        rm -f "$json_tmpfile"
        echo "unknown"
        return 0
    fi

    if json_ruleset_has_domain_matchers "$json_tmpfile"; then
        kind="domains"
    else
        kind="subnets"
    fi

    rm -f "$json_tmpfile"
    echo "$kind"
}

detect_local_source_ruleset_kind() {
    local filepath="$1"
    local kind

    kind="$(ruleset_kind_from_reference_hint "$filepath")"
    if [ "$kind" != "unknown" ]; then
        echo "$kind"
        return 0
    fi

    if ! file_exists "$filepath"; then
        echo "unknown"
        return 0
    fi

    if json_ruleset_has_domain_matchers "$filepath"; then
        echo "domains"
    else
        echo "subnets"
    fi
}

detect_inline_ruleset_reference_kind() {
    local reference="$1"
    local kind
    local extension

    kind="$(ruleset_kind_from_reference_hint "$reference")"
    if [ "$kind" != "unknown" ]; then
        echo "$kind"
        return 0
    fi

    case "$reference" in
    /*.srs)
        detect_local_binary_ruleset_kind "$reference"
        ;;
    /*.json)
        detect_local_source_ruleset_kind "$reference"
        ;;
    http://* | https://*)
        extension="$(url_get_file_extension "$reference")"
        [ "$extension" = "srs" ] || [ "$extension" = "json" ] || [ -z "$extension" ] || log "Unknown rule set URL extension '$extension' for '$reference'; assuming binary format" "debug"
        [ "$kind" = "unknown" ] && kind="domains"
        echo "$kind"
        ;;
    *)
        echo "unknown"
        ;;
    esac
}

get_inline_remote_ruleset_format() {
    local reference="$1"
    local extension

    extension="$(url_get_file_extension "$reference")"
    case "$extension" in
    json)
        echo "source"
        ;;
    srs | "")
        echo "binary"
        ;;
    *)
        log "Unknown rule set URL extension '$extension' for '$reference'; assuming binary format" "debug"
        echo "binary"
        ;;
    esac
}

validate_outbound_json_rule() {
    local section="$1"
    local outbound_json

    config_get outbound_json "$section" "outbound_json"
    if [ -z "$outbound_json" ]; then
        log "JSON outbound rule '$section' has empty outbound_json. Aborted." "fatal"
        exit 1
    fi

    if ! printf '%s' "$outbound_json" | json_utils_ucode valid-outbound >/dev/null 2>&1; then
        log "JSON outbound rule '$section' must contain a valid sing-box outbound JSON object with a type field. Aborted." "fatal"
        exit 1
    fi
}

validate_port_condition() {
    local value="$1"
    local section="$2"

    if ! is_port_condition "$value"; then
        log "Invalid port condition '$value' in rule '$section'. Use a single port or range like 80 or 1000-2000. Aborted." "fatal"
        exit 1
    fi
}

process_validate_rule() {
    local section="$1"
    local action urltest_check_interval subscription_update_interval

    rule_is_enabled "$section" || return 0

    action="$(get_rule_action "$section")"
    if [ -z "$action" ]; then
        log "Enabled rule '$section' has no action. Aborted." "fatal"
        exit 1
    fi

    config_list_foreach "$section" "ports" validate_port_condition "$section"

    if [ "$action" = "zapret" ]; then
        if ! is_zapret_installed; then
            if [ "$PODKOP_ZAPRET_MISSING_WARNING_EMITTED" -eq 0 ]; then
                log "There are enabled rules with action 'zapret', but the zapret package is not installed. These rules will be skipped." "error"
                PODKOP_ZAPRET_MISSING_WARNING_EMITTED=1
            fi
            config_list_foreach "$section" "community_lists" validate_service
            config_list_foreach "$section" "rule_set" validate_ruleset_reference
            config_list_foreach "$section" "rule_set_with_subnets" validate_ruleset_reference
            config_list_foreach "$section" "domain_ip_lists" validate_plain_domain_ip_list_reference
            return 0
        fi

        validate_rule_nfqws_opt "$section"
    fi

    if [ "$action" = "byedpi" ]; then
        if ! is_byedpi_installed; then
            if [ "$PODKOP_BYEDPI_MISSING_WARNING_EMITTED" -eq 0 ]; then
                log "There are enabled rules with action 'byedpi', but the byedpi package is not installed. These rules will be skipped." "error"
                PODKOP_BYEDPI_MISSING_WARNING_EMITTED=1
            fi
            config_list_foreach "$section" "community_lists" validate_service
            config_list_foreach "$section" "rule_set" validate_ruleset_reference
            config_list_foreach "$section" "rule_set_with_subnets" validate_ruleset_reference
            config_list_foreach "$section" "domain_ip_lists" validate_plain_domain_ip_list_reference
            return 0
        fi

        validate_byedpi_strategy "$(get_rule_byedpi_cmd_opts "$section")" "Invalid ByeDPI strategy for rule '$section'"
    fi

    if [ "$action" = "proxy" ]; then
        local urltest_enabled urltest_filter_mode
        config_get_bool urltest_enabled "$section" "urltest_enabled" 0
        if [ "$urltest_enabled" -eq 1 ]; then
            config_get urltest_filter_mode "$section" "urltest_filter_mode" "disabled"
            validate_urltest_filter_mode_option "$urltest_filter_mode" "$section"
            urltest_check_interval="$(get_urltest_check_interval_for_rule "$section")"
            validate_required_sing_box_duration_option "$urltest_check_interval" "rule.$section.urltest_check_interval"
            case "$urltest_filter_mode" in
            include | mixed)
                config_list_foreach "$section" "urltest_include_regex" validate_urltest_regex_option "$section"
                config_list_foreach "$section" "urltest_include_countries" validate_country_code_option "$section"
                ;;
            esac
            case "$urltest_filter_mode" in
            exclude | mixed)
                config_list_foreach "$section" "urltest_exclude_countries" validate_country_code_option "$section"
                config_list_foreach "$section" "urltest_exclude_regex" validate_urltest_regex_option "$section"
                ;;
            esac
        fi

        if rule_has_subscription_urls "$section"; then
            config_list_foreach "$section" "subscription_urls" validate_subscription_source_entry "$section"
            subscription_update_interval="$(get_subscription_update_interval_for_rule "$section")"
            if [ -n "$subscription_update_interval" ]; then
                validate_required_sing_box_duration_option "$subscription_update_interval" "rule.$section.subscription_update_interval"
            fi
        fi
    fi

    if [ "$action" = "outbound" ]; then
        validate_outbound_json_rule "$section"
    fi

    config_list_foreach "$section" "community_lists" validate_service
    config_list_foreach "$section" "rule_set" validate_ruleset_reference
    config_list_foreach "$section" "rule_set_with_subnets" validate_ruleset_reference
    config_list_foreach "$section" "domain_ip_lists" validate_plain_domain_ip_list_reference
}

br_netfilter_disable() {
    if lsmod | grep -q br_netfilter && [ "$(sysctl -n net.bridge.bridge-nf-call-iptables 2> /dev/null)" = "1" ]; then
        log "br_netfilter enabled detected. Disabling"
        sysctl -w net.bridge.bridge-nf-call-iptables=0
        sysctl -w net.bridge.bridge-nf-call-ip6tables=0
    fi
}

parse_generic_string_to_commas_string() {
    local string="$1"

    printf "%s\n" "$string" | sed -e 's/[[:space:]]*\/\/.*$//' -e 's/[[:space:]]*#.*$//' | tr ',' '\n' | sed 's/\r//g' | \
        sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | awk 'BEGIN { first = 1 } { if (!first) printf ","; printf "%s", $0; first = 0 } END { if (!first) printf "\n" }'
}

get_rule_condition_commas_string() {
    local section="$1"
    local key="$2"
    local kind="$3"

    local text_mode conditions_text_mode text_value list_value
    config_get_bool text_mode "$section" "${key}_text_mode" 0
    config_get_bool conditions_text_mode "$section" "conditions_text_mode" 0
    config_get text_value "$section" "${key}_text"
    config_get list_value "$section" "$key"

    if [ "$text_mode" -eq 1 ] || [ "$conditions_text_mode" -eq 1 ]; then
        case "$kind" in
        domains) parse_domain_or_subnet_string_to_commas_string "$text_value" "domains" ;;
        subnets) parse_domain_or_subnet_string_to_commas_string "$text_value" "subnets" ;;
        *)
            parse_generic_string_to_commas_string "$text_value"
            ;;
        esac
        return 0
    fi

    if [ -n "$list_value" ]; then
        echo "${list_value// /,}"
        return 0
    fi

    if [ -n "$text_value" ]; then
        case "$kind" in
        domains) parse_domain_or_subnet_string_to_commas_string "$text_value" "domains" ;;
        subnets) parse_domain_or_subnet_string_to_commas_string "$text_value" "subnets" ;;
        *)
            parse_generic_string_to_commas_string "$text_value"
            ;;
        esac
        return 0
    fi

    echo "${list_value// /,}"
}

get_rule_condition_json_array() {
    local section="$1"
    local key="$2"
    local kind="$3"
    local values

    values="$(get_rule_condition_commas_string "$section" "$key" "$kind")"
    comma_string_to_json_array "$values"
}

append_comma_value() {
    local current="$1"
    local value="$2"

    if [ -z "$current" ]; then
        printf '%s\n' "$value"
    else
        printf '%s,%s\n' "$current" "$value"
    fi
}

comma_string_has_value() {
    local values="$1"
    local value="$2"

    case ",$values," in
    *",$value,"*) return 0 ;;
    esac

    return 1
}

get_rule_ports_commas_string() {
    local section="$1"
    local values text_value item normalized result

    config_get values "$section" "ports"

    for item in $values; do
        normalized="$(normalize_port_condition_for_nft "$item" 2> /dev/null)" || continue
        comma_string_has_value "$result" "$normalized" && continue
        result="$(append_comma_value "$result" "$normalized")"
    done

    config_get text_value "$section" "ports_text"
    if [ -n "$text_value" ]; then
        for item in $(printf "%s\n" "$text_value" | sed -e 's/[[:space:]]*\/\/.*$//' -e 's/[[:space:]]*#.*$//' | tr ', ' '\n' | grep -v '^$'); do
            normalized="$(normalize_port_condition_for_nft "$item" 2> /dev/null)" || continue
            comma_string_has_value "$result" "$normalized" && continue
            result="$(append_comma_value "$result" "$normalized")"
        done
    fi

    printf '%s\n' "$result"
}

get_rule_port_values_json_array() {
    local section="$1"
    local ports item normalized result

    ports="$(get_rule_ports_commas_string "$section")"

    for item in $(printf '%s\n' "$ports" | tr ',' ' '); do
        case "$item" in
        *-*) continue ;;
        esac

        normalized="$(normalize_port_number "$item" 2> /dev/null)" || continue
        result="$(append_comma_value "$result" "$normalized")"
    done

    port_numbers_to_json_array "$result"
}

get_rule_port_ranges_json_array() {
    local section="$1"
    local ports item normalized result

    ports="$(get_rule_ports_commas_string "$section")"

    for item in $(printf '%s\n' "$ports" | tr ',' ' '); do
        case "$item" in
        *-*) ;;
        *) continue ;;
        esac

        normalized="$(normalize_port_range_for_sing_box "$item" 2> /dev/null)" || continue
        result="$(append_comma_value "$result" "$normalized")"
    done

    comma_string_to_json_array "$result"
}

section_has_destination_matchers() {
    local section="$1"
    local domain domain_suffix domain_keyword domain_regex ip_cidr community_lists rule_set rule_set_with_subnets domain_ip_lists

    domain="$(get_rule_condition_commas_string "$section" "domain" "domains")"
    domain_suffix="$(get_rule_condition_commas_string "$section" "domain_suffix" "domains")"
    domain_keyword="$(get_rule_condition_commas_string "$section" "domain_keyword" "generic")"
    domain_regex="$(get_rule_condition_commas_string "$section" "domain_regex" "generic")"
    ip_cidr="$(get_rule_condition_commas_string "$section" "ip_cidr" "subnets")"
    config_get community_lists "$section" "community_lists"
    config_get rule_set "$section" "rule_set"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"

    [ -n "$domain" ] ||
        [ -n "$domain_suffix" ] ||
        [ -n "$domain_keyword" ] ||
        [ -n "$domain_regex" ] ||
        [ -n "$ip_cidr" ] ||
        [ -n "$community_lists" ] ||
        [ -n "$rule_set" ] ||
        [ -n "$rule_set_with_subnets" ] ||
        [ -n "$domain_ip_lists" ]
}

add_section_ip_cidr_to_nft_sets() {
    local section="$1"
    local ip_values="$2"
    local ports

    [ -n "$ip_values" ] || return 0

    ports="$(get_rule_ports_commas_string "$section")"
    if [ -n "$ports" ]; then
        local tmpfile status
        tmpfile="$(mktemp)"
        printf '%s\n' "$ip_values" | tr ',' '\n' > "$tmpfile"
        nft_add_ip_port_set_elements_from_ip_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_IP_PORT_SET_NAME" "$ports"
        status=$?
        rm -f "$tmpfile"
        return "$status"
    else
        nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$ip_values"
    fi
}

add_section_ports_to_nft_set_if_needed() {
    local section="$1"
    local ports

    ports="$(get_rule_ports_commas_string "$section")"
    [ -n "$ports" ] || return 0

    if section_has_destination_matchers "$section"; then
        return 0
    fi

    nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_PORT_SET_NAME" "$ports"
}

ruleset_registered() {
    local tag="$1"
    echo ",$configured_rulesets," | grep -q ",$tag,"
}

register_ruleset_tag() {
    local tag="$1"
    if ! ruleset_registered "$tag"; then
        if [ -z "$configured_rulesets" ]; then
            configured_rulesets="$tag"
        else
            configured_rulesets="$configured_rulesets,$tag"
        fi
    fi
}

ENSURED_RULESET_TAG=""
ENSURED_RULESET_KIND="unknown"

get_community_ruleset_url() {
    local name="$1"

    case "$name" in
    ads_hagezi_pro)
        printf '%s\n' "$SRS_ADS_HAGEZI_PRO_URL"
        ;;
    supercell)
        printf '%s\n' "$SRS_SUPERCELL_URL"
        ;;
    *)
        printf '%s\n' "$SRS_MAIN_URL/$name.srs"
        ;;
    esac
}

ensure_builtin_ruleset() {
    local name="$1"
    local tag="builtin-$name-ruleset"

    ENSURED_RULESET_TAG="$tag"
    ENSURED_RULESET_KIND="domains"

    if ! ruleset_registered "$tag"; then
        local detour update_interval url
        detour="$(get_download_detour_tag)"
        update_interval="$(get_remote_ruleset_update_interval)"
        url="$(get_community_ruleset_url "$name")"
        config=$(sing_box_cm_add_remote_ruleset "$config" "$tag" "binary" "$url" "$detour" "$update_interval")
        register_ruleset_tag "$tag"
    fi
}

ensure_inline_custom_ruleset() {
    local reference="$1"
    local tag hash detour update_interval extension format

    hash="$(printf '%s' "$reference" | md5sum | cut -c1-12)"
    tag="inline-custom-$hash-ruleset"
    ENSURED_RULESET_TAG="$tag"
    ENSURED_RULESET_KIND="$(detect_inline_ruleset_reference_kind "$reference")"

    if ! ruleset_registered "$tag"; then
        update_interval="$(get_remote_ruleset_update_interval)"

        case "$reference" in
        /*.srs)
            config=$(sing_box_cm_add_local_ruleset "$config" "$tag" "binary" "$reference")
            ;;
        /*.json)
            config=$(sing_box_cm_add_local_ruleset "$config" "$tag" "source" "$reference")
            ;;
        http://* | https://*)
            extension="$(url_get_file_extension "$reference")"
            detour="$(get_download_detour_tag)"
            case "$extension" in
            srs)
                config=$(sing_box_cm_add_remote_ruleset "$config" "$tag" "binary" "$reference" "$detour" "$update_interval")
                ;;
            json)
                config=$(sing_box_cm_add_remote_ruleset "$config" "$tag" "source" "$reference" "$detour" "$update_interval")
                ;;
            *)
                format="$(get_inline_remote_ruleset_format "$reference")"
                config=$(sing_box_cm_add_remote_ruleset "$config" "$tag" "$format" "$reference" "$detour" "$update_interval")
                ;;
            esac
            ;;
        *)
            return 1
            ;;
        esac

        register_ruleset_tag "$tag"
    fi
}

resolve_ruleset_reference() {
    local reference="$1"
    ENSURED_RULESET_TAG=""
    ENSURED_RULESET_KIND="unknown"

    if printf '%s\n' "$COMMUNITY_SERVICES" | tr ' ' '\n' | grep -Fxq "$reference"; then
        ensure_builtin_ruleset "$reference"
        return 0
    fi

    ensure_inline_custom_ruleset "$reference"
}

# Main funcs

route_table_rule_mark() {
    grep -q "105 $RT_TABLE_NAME" /etc/iproute2/rt_tables || echo "105 $RT_TABLE_NAME" >> /etc/iproute2/rt_tables

    if ! ip route list table "$RT_TABLE_NAME" 2> /dev/null | grep -q "local default dev lo scope host"; then
        log "Added route for tproxy" "debug"
        ip route add local 0.0.0.0/0 dev lo table "$RT_TABLE_NAME" 2> /dev/null || {
            if ip route list table "$RT_TABLE_NAME" 2> /dev/null | grep -q "local default dev lo scope host"; then
                log "Route for tproxy exists" "debug"
            else
                log "Failed to add route for tproxy. Aborted." "fatal"
                exit 1
            fi
        }
    else
        log "Route for tproxy exists" "debug"
    fi

    if ! has_tproxy_marking_rule; then
        log "Create marking rule" "debug"
        ip -4 rule add fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 2> /dev/null || {
            if has_tproxy_marking_rule; then
                log "Marking rule exist" "debug"
            else
                log "Failed to create marking rule. Aborted." "fatal"
                exit 1
            fi
        }
    else
        log "Marking rule exist" "debug"
    fi
}

has_tproxy_marking_rule() {
    local rule_list fakeip_mark_hex fakeip_mark_dec

    rule_list="$(ip -4 rule list 2> /dev/null || true)"
    [ -n "$rule_list" ] || return 1

    fakeip_mark_hex="$(printf '%s' "$NFT_FAKEIP_MARK" | tr '[:upper:]' '[:lower:]' | sed -e 's/^0x//' -e 's/^0*//')"
    [ -n "$fakeip_mark_hex" ] || fakeip_mark_hex="0"
    fakeip_mark_dec="$((NFT_FAKEIP_MARK))"

    printf '%s\n' "$rule_list" | grep -Eq "lookup $RT_TABLE_NAME([[:space:]]|$)" || return 1
    printf '%s\n' "$rule_list" | grep -Eq "fwmark ($fakeip_mark_dec|0x0*$fakeip_mark_hex)/($fakeip_mark_dec|0x0*$fakeip_mark_hex)([[:space:]]|$)"
}

nft_init_interfaces_set() {
    nft_create_ifname_set "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME"

    local source_network_interfaces
    config_get source_network_interfaces "settings" "source_network_interfaces" "br-lan"

    for interface in $source_network_interfaces; do
        nft add element inet "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" "{ $interface }"
    done
}

create_nft_rules() {
    log "Create nft table"
    nft_create_table "$NFT_TABLE_NAME"

    log "Create localv4 set"
    nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_LOCALV4_SET_NAME"
    nft add element inet "$NFT_TABLE_NAME" localv4 '{
        0.0.0.0/8,
        10.0.0.0/8,
        127.0.0.0/8,
        169.254.0.0/16,
        172.16.0.0/12,
        192.0.0.0/24,
        192.0.2.0/24,
        192.88.99.0/24,
        192.168.0.0/16,
        198.51.100.0/24,
        203.0.113.0/24,
        224.0.0.0/4,
        240.0.0.0-255.255.255.255
    }'

    log "Create common set"
    nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"

    log "Create port set"
    nft_create_inet_service_set "$NFT_TABLE_NAME" "$NFT_PORT_SET_NAME"

    log "Create IP and port set"
    nft_create_ipv4_port_set "$NFT_TABLE_NAME" "$NFT_IP_PORT_SET_NAME"

    log "Create interface set"
    nft_init_interfaces_set

    log "Create nft rules"
    nft add chain inet "$NFT_TABLE_NAME" mangle '{ type filter hook prerouting priority -150; policy accept; }'
    nft add chain inet "$NFT_TABLE_NAME" mangle_output '{ type route hook output priority -150; policy accept; }'
    nft add chain inet "$NFT_TABLE_NAME" proxy '{ type filter hook prerouting priority -100; policy accept; }'

    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr . tcp dport "@$NFT_IP_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr . udp dport "@$NFT_IP_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr != "@$NFT_LOCALV4_SET_NAME" tcp dport "@$NFT_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr != "@$NFT_LOCALV4_SET_NAME" udp dport "@$NFT_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter

    nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip to 127.0.0.1:1602 counter
    nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip to 127.0.0.1:1602 counter

    nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_LOCALV4_SET_NAME" return
    nft add rule inet "$NFT_TABLE_NAME" mangle_output meta mark "$NFT_OUTBOUND_MARK" counter return
    create_zapret_nft_rules
    nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr . tcp dport "@$NFT_IP_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr . udp dport "@$NFT_IP_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle_output tcp dport "@$NFT_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle_output udp dport "@$NFT_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter
    nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter

    local exclude_ntp
    config_get_bool exclude_ntp "settings" "exclude_ntp" "0"
    if [ "$exclude_ntp" -eq 1 ]; then
        log "NTP traffic exclude for proxy"
        nft insert rule inet "$NFT_TABLE_NAME" mangle udp dport 123 return
    fi
}

list_has_item() {
    local list="$1"
    local needle="$2"
    local item

    for item in $list; do
        [ "$item" = "$needle" ] && return 0
    done

    return 1
}

uci_add_list_unique() {
    local package="$1"
    local section="$2"
    local option="$3"
    local value="$4"
    local current_values

    current_values="$(uci_get "$package" "$section" "$option")"
    list_has_item "$current_values" "$value" && return 0

    uci_add_list "$package" "$section" "$option" "$value"
}

backup_dnsmasq_config_list_option() {
    local key="$1"
    local backup_key="$2"
    local value

    for value in $(uci_get "dhcp" "@dnsmasq[0]" "$key"); do
        uci_add_list_unique "dhcp" "@dnsmasq[0]" "$backup_key" "$value"
    done
}

restore_dnsmasq_config_option() {
    local key="$1"
    local backup_key="$2"
    local default_value="${3:-}"
    local value

    value="$(uci_get "dhcp" "@dnsmasq[0]" "$backup_key")"
    if [ -n "$value" ]; then
        uci_set "dhcp" "@dnsmasq[0]" "$key" "$value"
        uci_remove "dhcp" "@dnsmasq[0]" "$backup_key"
    elif [ -n "$default_value" ]; then
        uci_set "dhcp" "@dnsmasq[0]" "$key" "$default_value"
    else
        uci_remove "dhcp" "@dnsmasq[0]" "$key"
    fi
}

restore_dnsmasq_config_list_option() {
    local key="$1"
    local backup_key="$2"
    local value

    uci_remove "dhcp" "@dnsmasq[0]" "$key"
    for value in $(uci_get "dhcp" "@dnsmasq[0]" "$backup_key"); do
        uci_add_list "dhcp" "@dnsmasq[0]" "$key" "$value"
    done
    uci_remove "dhcp" "@dnsmasq[0]" "$backup_key"
}

dnsmasq_inherit_default_options() {
    local option value

    for option in $(dnsmasq_inherited_default_options); do
        value="$(uci_get "dhcp" "@dnsmasq[0]" "$option")"
        [ -n "$value" ] || continue
        uci_set "dhcp" "$PODKOP_DNSMASQ_SECTION" "$option" "$value"
    done
}

dnsmasq_inherit_default_list_options() {
    local option value

    for option in $(dnsmasq_inherited_default_list_options); do
        for value in $(uci_get "dhcp" "@dnsmasq[0]" "$option"); do
            uci_add_list "dhcp" "$PODKOP_DNSMASQ_SECTION" "$option" "$value"
        done
    done
}

dnsmasq_inherit_default_domain_forward_servers() {
    local server

    for server in $(dnsmasq_default_domain_forward_servers); do
        uci_add_list "dhcp" "$PODKOP_DNSMASQ_SECTION" "server" "$server"
    done
}

dnsmasq_server_list_has_podkop_dns() {
    local server_list="$1"
    local server

    for server in $server_list; do
        [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ] && return 0
    done

    return 1
}

dnsmasq_default_has_podkop_dns() {
    dnsmasq_server_list_has_podkop_dns "$(uci_get "dhcp" "@dnsmasq[0]" "server")"
}

dnsmasq_split_has_podkop_dns() {
    dnsmasq_server_list_has_podkop_dns "$(uci_get "dhcp" "$PODKOP_DNSMASQ_SECTION" "server")"
}

dnsmasq_has_podkop_dns() {
    dnsmasq_split_has_podkop_dns || dnsmasq_default_has_podkop_dns
}

dnsmasq_restore_default_instance() {
    local server_list backup_servers server legacy_global_dns noresolv cachesize

    server_list="$(uci_get "dhcp" "@dnsmasq[0]" "server")"
    backup_servers="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_server")"
    legacy_global_dns=0
    dnsmasq_server_list_has_podkop_dns "$server_list" && legacy_global_dns=1

    uci_remove "dhcp" "@dnsmasq[0]" "server"
    if [ -n "$backup_servers" ]; then
        for server in $backup_servers; do
            uci_add_list "dhcp" "@dnsmasq[0]" "server" "$server"
        done
        uci_remove "dhcp" "@dnsmasq[0]" "podkop_server"
    else
        for server in $server_list; do
            [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ] && continue
            uci_add_list "dhcp" "@dnsmasq[0]" "server" "$server"
        done
    fi

    noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_noresolv")"
    if [ -n "$noresolv" ]; then
        restore_dnsmasq_config_option "noresolv" "podkop_noresolv"
    elif [ "$legacy_global_dns" -eq 1 ]; then
        uci_set "dhcp" "@dnsmasq[0]" "noresolv" 0
    fi

    cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_cachesize")"
    if [ -n "$cachesize" ]; then
        restore_dnsmasq_config_option "cachesize" "podkop_cachesize"
    elif [ "$legacy_global_dns" -eq 1 ]; then
        uci_set "dhcp" "@dnsmasq[0]" "cachesize" 150
    fi
}

dnsmasq_configure_split_instance() {
    local source_network_interfaces interface

    config_get source_network_interfaces "settings" "source_network_interfaces" "br-lan"

    backup_dnsmasq_config_list_option "notinterface" "podkop_notinterface"
    for interface in $source_network_interfaces; do
        uci_add_list_unique "dhcp" "@dnsmasq[0]" "notinterface" "$interface"
    done

    uci -q delete "dhcp.$PODKOP_DNSMASQ_SECTION"
    uci set "dhcp.$PODKOP_DNSMASQ_SECTION=dnsmasq"
    uci_set "dhcp" "$PODKOP_DNSMASQ_SECTION" "noresolv" 1
    uci_set "dhcp" "$PODKOP_DNSMASQ_SECTION" "cachesize" 0
    dnsmasq_inherit_default_options
    dnsmasq_inherit_default_list_options
    uci_add_list "dhcp" "$PODKOP_DNSMASQ_SECTION" "server" "$SB_DNS_INBOUND_ADDRESS"
    dnsmasq_inherit_default_domain_forward_servers
    uci_add_list "dhcp" "$PODKOP_DNSMASQ_SECTION" "notinterface" "lo"

    for interface in $source_network_interfaces; do
        uci_add_list "dhcp" "$PODKOP_DNSMASQ_SECTION" "interface" "$interface"
    done
}

dnsmasq_restore_split_instance() {
    uci -q delete "dhcp.$PODKOP_DNSMASQ_SECTION"
    restore_dnsmasq_config_list_option "notinterface" "podkop_notinterface"
}

dnsmasq_configure() {
    local force="${1:-0}"
    local shutdown_correctly
    config_get shutdown_correctly "settings" "shutdown_correctly"
    if [ "$force" != "force" ] && [ "$shutdown_correctly" -eq 0 ]; then
        if dnsmasq_split_has_podkop_dns; then
            log "Previous shutdown of Podkop Plus was not correct, dnsmasq is already configured"
            return 0
        fi

        log "Previous shutdown of Podkop Plus was not correct, but dnsmasq is not configured. Applying Podkop Plus DNS settings"
    fi

    log "Configure dnsmasq for sing-box"
    if dnsmasq_split_has_podkop_dns; then
        log "Refreshing existing interface-scoped Podkop Plus DNS settings"
        dnsmasq_restore_split_instance
    fi
    if dnsmasq_default_has_podkop_dns; then
        log "Migrating legacy global dnsmasq DNS settings to interface-scoped Podkop Plus DNS"
        dnsmasq_restore_default_instance
    fi
    dnsmasq_configure_split_instance
    uci_commit "dhcp"

    /etc/init.d/dnsmasq restart
}

dnsmasq_restore() {
    local force="${1:-0}"
    log "Restoring the dnsmasq configuration"
    local shutdown_correctly
    config_get shutdown_correctly "settings" "shutdown_correctly"
    if [ "$force" != "force" ] && [ "$shutdown_correctly" -eq 1 ]; then
        if ! dnsmasq_has_podkop_dns; then
            log "Previous shutdown of Podkop Plus was correct, reconfiguration of dnsmasq is not required"
            return 0
        fi

        log "Previous shutdown of Podkop Plus was correct, but Podkop Plus DNS is still present. Restoring dnsmasq"
    fi

    dnsmasq_restore_split_instance
    dnsmasq_restore_default_instance

    uci_commit "dhcp"

    /etc/init.d/dnsmasq restart
}

build_list_update_cron_job() {
    local update_interval
    local cron_schedule

    update_interval="$(get_settings_update_interval)"
    [ -n "$update_interval" ] || return 1

    if ! duration_to_seconds "$update_interval" >/dev/null 2>&1; then
        log "Invalid update_interval value: $update_interval" "error"
        return 1
    fi

    cron_schedule="$(duration_to_cron_schedule "$update_interval")" || cron_schedule="* * * * *"
    printf '%s %s list_update_if_due %s\n' "$cron_schedule" "$PODKOP_BIN" "$PODKOP_LIST_UPDATE_CRON_MARKER"
}

build_subscription_update_cron_job() {
    local min_interval_seconds cron_schedule

    SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS=0
    config_foreach collect_subscription_update_interval_seconds "section"

    [ "$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS" -gt 0 ] || return 1

    min_interval_seconds="$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS"
    cron_schedule="$(seconds_to_subscription_cron_schedule "$min_interval_seconds")"
    printf '%s %s subscription_update_if_due %s\n' "$cron_schedule" "$PODKOP_BIN" "$PODKOP_SUBSCRIPTION_UPDATE_CRON_MARKER"
}

collect_subscription_update_interval_seconds() {
    local section="$1"
    local subscription_update_interval interval_seconds

    rule_is_subscription_proxy "$section" || return 0

    subscription_update_interval="$(get_subscription_update_interval_for_rule "$section")"
    [ -n "$subscription_update_interval" ] || return 0

    interval_seconds="$(duration_to_seconds "$subscription_update_interval")" || {
        log "Invalid subscription_update_interval value for rule '$section': $subscription_update_interval" "error"
        return 0
    }

    if [ "$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS" -eq 0 ] ||
        [ "$interval_seconds" -lt "$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS" ]; then
        SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS="$interval_seconds"
    fi
}

seconds_to_subscription_cron_schedule() {
    local seconds="$1"
    local minutes hours

    if [ "$seconds" -le 60 ]; then
        printf '* * * * *\n'
        return 0
    fi

    if [ $((seconds % 86400)) -eq 0 ]; then
        printf '0 0 * * *\n'
        return 0
    fi

    if [ $((seconds % 3600)) -eq 0 ]; then
        hours=$((seconds / 3600))
        if [ "$hours" -ge 1 ] && [ "$hours" -le 23 ]; then
            if [ "$hours" -eq 1 ]; then
                printf '0 * * * *\n'
            else
                printf '0 */%s * * *\n' "$hours"
            fi
            return 0
        fi
    fi

    if [ $((seconds % 60)) -eq 0 ]; then
        minutes=$((seconds / 60))
        if [ "$minutes" -ge 1 ] && [ "$minutes" -le 59 ]; then
            if [ "$minutes" -eq 1 ]; then
                printf '* * * * *\n'
            else
                printf '*/%s * * * *\n' "$minutes"
            fi
            return 0
        fi
    fi

    printf '* * * * *\n'
}

remove_cron_job() {
    (crontab -l 2>/dev/null |
        grep -Fv "$PODKOP_LIST_UPDATE_CRON_MARKER" |
        grep -Fv "$PODKOP_SUBSCRIPTION_UPDATE_CRON_MARKER") | crontab -
    log "The cron job removed"
}

duration_to_seconds() {
    local input="$1"
    local rest value remainder unit multiplier total

    [ -n "$input" ] || return 1

    rest="$input"
    total="0"

    while [ -n "$rest" ]; do
        value="$(printf '%s' "$rest" | sed -n 's/^\([0-9][0-9]*\(\.[0-9][0-9]*\)\?\).*/\1/p')"
        [ -n "$value" ] || return 1

        remainder="${rest#$value}"
        case "$remainder" in
        ns*)
            unit="ns"
            multiplier="0.000000001"
            ;;
        us*)
            unit="us"
            multiplier="0.000001"
            ;;
        ms*)
            unit="ms"
            multiplier="0.001"
            ;;
        s*)
            unit="s"
            multiplier="1"
            ;;
        m*)
            unit="m"
            multiplier="60"
            ;;
        h*)
            unit="h"
            multiplier="3600"
            ;;
        d*)
            unit="d"
            multiplier="86400"
            ;;
        *)
            return 1
            ;;
        esac

        rest="${remainder#$unit}"
        total="$(awk -v total="$total" -v value="$value" -v multiplier="$multiplier" 'BEGIN { printf "%.9f", total + (value * multiplier) }')" || return 1
    done

    awk -v total="$total" 'BEGIN { if (total <= 0) exit 1; printf "%d\n", int(total + 0.5) }' || return 1
}

duration_to_cron_schedule() {
    local input="$1"
    local value unit

    value="$(printf '%s' "$input" | sed -n 's/^\([0-9][0-9]*\)\([mhd]\)$/\1/p')"
    unit="$(printf '%s' "$input" | sed -n 's/^\([0-9][0-9]*\)\([mhd]\)$/\2/p')"
    [ -n "$value" ] || return 1

    case "$unit" in
    m)
        [ "$value" -ge 1 ] && [ "$value" -le 59 ] || return 1
        if [ "$value" -eq 1 ]; then
            echo "* * * * *"
        else
            echo "*/$value * * * *"
        fi
        ;;
    h)
        [ "$value" -ge 1 ] && [ "$value" -le 23 ] || return 1
        if [ "$value" -eq 1 ]; then
            echo "0 * * * *"
        else
            echo "0 */$value * * *"
        fi
        ;;
    d)
        [ "$value" -eq 1 ] || return 1
        echo "0 0 * * *"
        ;;
    *)
        return 1
        ;;
    esac
}

read_list_update_timestamp() {
    sed -n '1p' "$PODKOP_LIST_UPDATE_STATE_FILE" 2>/dev/null
}

write_list_update_timestamp() {
    local timestamp="$1"

    mkdir -p "$PODKOP_RUNTIME_STATE_DIR"
    printf '%s\n' "$timestamp" > "$PODKOP_LIST_UPDATE_STATE_FILE"
}

list_update_if_due() {
    local update_interval interval_seconds now last_run

    config_load "$PODKOP_CONFIG_NAME"
    update_interval="$(get_settings_update_interval)"
    [ -n "$update_interval" ] || return 0

    interval_seconds="$(duration_to_seconds "$update_interval")" || {
        log "Invalid update_interval value: $update_interval" "error"
        return 1
    }
    now="$(date +%s 2>/dev/null)"
    last_run="$(read_list_update_timestamp)"

    case "$now" in
    '' | *[!0-9]*) return 1 ;;
    esac

    case "$last_run" in
    '' | *[!0-9]*) last_run=0 ;;
    esac

    if [ "$last_run" -gt 0 ] && [ $((now - last_run)) -lt "$interval_seconds" ]; then
        return 0
    fi

    list_update
}

list_update() {
    echolog "Starting lists update..."

    local pidfile="/var/run/podkop_list_update.pid"
    local existing_pid
    if [ -f "$pidfile" ]; then
        existing_pid=$(cat "$pidfile" 2> /dev/null)
        if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && kill -0 "$existing_pid" 2> /dev/null; then
            echolog "Another lists update is already running, skipping"
            return 0
        fi
    fi

    echo $$ > "$pidfile"
    trap 'rm -f "$pidfile"' EXIT INT TERM

    local nslookup_timeout=3
    local nslookup_attempts=10
    local curl_timeout=5
    local curl_attempts=10
    local curl_max_timeout=10
    local delay=3
    local i

    # DNS Check
    for i in $(seq 1 $nslookup_attempts); do
        if nslookup -timeout=$nslookup_timeout openwrt.org > /dev/null 2>&1; then
            echolog "DNS check passed"
            break
        fi
        echolog "DNS is unavailable [$i/$nslookup_attempts]"
        sleep $delay
    done

    if [ "$i" -eq $nslookup_attempts ]; then
        echolog "DNS check failed after $nslookup_attempts attempts"
        return 1
    fi

    # Github Check
    for i in $(seq 1 $curl_attempts); do
        local service_proxy_address
        service_proxy_address="$(get_service_proxy_address)"

        if [ -n "$service_proxy_address" ]; then
            if curl -s -x "http://$service_proxy_address" -m $curl_timeout https://github.com > /dev/null; then
                echolog "GitHub connection check passed (via proxy)"
                break
            fi
        else
            if curl -s -m $curl_timeout https://github.com > /dev/null; then
                echolog "GitHub connection check passed"
                break
            fi
        fi

        echolog "GitHub is unavailable [$i/$curl_attempts] (max-timeout=$curl_timeout)"
        if [ "$curl_timeout" -lt $curl_max_timeout ]; then
            curl_timeout=$((curl_timeout + 1))
        fi
        sleep $delay
    done

    if [ "$i" -eq $curl_attempts ]; then
        echolog "GitHub connection check failed after $curl_attempts attempts"
        return 1
    fi

    echolog "Downloading and processing lists..."

    local update_status now
    update_status=0

    config_foreach rebuild_domain_ip_lists_from_rule "section" || update_status=1
    config_foreach import_builtin_subnets_from_rule "section" || update_status=1
    config_foreach import_domains_from_remote_domain_lists "section" || update_status=1
    config_foreach import_subnets_from_remote_subnet_lists "section" || update_status=1
    config_foreach import_rule_sets_with_subnets_from_rule "section" || update_status=1
    # Keep legacy rule_set references inside sing-box only.
    # Mirroring them into nft via decompile/import regressed compared to the
    # original Podkop behavior and causes long-running list updates plus
    # unstable routing when third-party SRS files are large or incompatible.

    if [ "$update_status" -eq 0 ]; then
        now="$(date +%s 2>/dev/null)"
        case "$now" in
        '' | *[!0-9]*) ;;
        *)
            write_list_update_timestamp "$now"
            ;;
        esac
        echolog "Lists update completed successfully"
    else
        echolog "Lists update failed"
    fi

    [ "$update_status" -eq 0 ] || return 1
}

read_subscription_update_timestamp() {
    local section="$1"

    sed -n '1p' "$(get_subscription_update_timestamp_path "$section")" 2>/dev/null
}

write_subscription_update_timestamp() {
    local section="$1"
    local timestamp="$2"

    mkdir -p "$PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR"
    printf '%s\n' "$timestamp" > "$(get_subscription_update_timestamp_path "$section")"
}

subscription_update_section_is_due() {
    local section="$1"
    local update_interval interval_seconds now last_run

    update_interval="$(get_subscription_update_interval_for_rule "$section")"
    [ -n "$update_interval" ] || return 1

    interval_seconds="$(duration_to_seconds "$update_interval")" || {
        log "Invalid subscription_update_interval value for rule '$section': $update_interval" "error"
        return 2
    }

    now="$(date +%s 2>/dev/null)"
    last_run="$(read_subscription_update_timestamp "$section")"

    case "$now" in
    '' | *[!0-9]*) return 2 ;;
    esac

    case "$last_run" in
    '' | *[!0-9]*) last_run=0 ;;
    esac

    [ "$last_run" -le 0 ] || [ $((now - last_run)) -ge "$interval_seconds" ]
}

subscription_update_section() {
    local section="$1"
    local force="$2"
    local update_result now metadata_tmpfile metadata_count

    rule_is_subscription_proxy "$section" || return 3

    if [ "$force" -eq 0 ]; then
        subscription_update_section_is_due "$section"
        case "$?" in
        0) ;;
        1) return 3 ;;
        *) return 1 ;;
        esac
    fi

    mkdir -p "$TMP_SUBSCRIPTION_FOLDER"

    echolog "Updating subscriptions for rule '$section'..."

    metadata_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.metadata-section.XXXXXX" 2>/dev/null || true)"
    if [ -n "$metadata_tmpfile" ]; then
        printf '[]\n' > "$metadata_tmpfile"
        SUBSCRIPTION_SECTION_METADATA_TMP="$metadata_tmpfile"
    else
        SUBSCRIPTION_SECTION_METADATA_TMP=""
    fi

    SUBSCRIPTION_SECTION_UPDATE_CHANGED=0
    SUBSCRIPTION_SECTION_UPDATE_UNCHANGED=0
    SUBSCRIPTION_SECTION_UPDATE_FAILED=0
    SUBSCRIPTION_SECTION_UPDATE_SUPERSEDED=0
    SUBSCRIPTION_SECTION_UPDATE_TOTAL=0
    for_each_subscription_source "$section" subscription_update_source_handler
    SUBSCRIPTION_SECTION_METADATA_TMP=""

    if [ "$SUBSCRIPTION_SECTION_UPDATE_TOTAL" -eq 0 ]; then
        rm -f "$metadata_tmpfile"
        echolog "Subscription URL is not set for rule '$section'"
        return 1
    fi

    if [ "$SUBSCRIPTION_SECTION_UPDATE_CHANGED" -gt 0 ]; then
        update_result=0
    elif [ "$SUBSCRIPTION_SECTION_UPDATE_FAILED" -gt 0 ]; then
        update_result=1
    elif [ "$SUBSCRIPTION_SECTION_UPDATE_SUPERSEDED" -gt 0 ]; then
        update_result=4
    else
        update_result=2
    fi

    if [ -n "$metadata_tmpfile" ]; then
        if [ "$SUBSCRIPTION_SECTION_UPDATE_SUPERSEDED" -eq 0 ]; then
            metadata_count="$(json_utils_ucode length "$metadata_tmpfile" 2>/dev/null)"
            if [ -n "$metadata_count" ] && [ "$metadata_count" -gt 0 ]; then
                write_subscription_metadata_json "$section" "$metadata_tmpfile"
            else
                write_subscription_metadata_json "$section" ""
            fi
        fi
        rm -f "$metadata_tmpfile"
    fi

    case "$update_result" in
    0)
        now="$(date +%s 2>/dev/null)"
        case "$now" in
        '' | *[!0-9]*) ;;
        *) write_subscription_update_timestamp "$section" "$now" ;;
        esac

        echolog "Subscriptions updated for rule '$section'"
        return 0
        ;;
    2)
        now="$(date +%s 2>/dev/null)"
        case "$now" in
        '' | *[!0-9]*) ;;
        *) write_subscription_update_timestamp "$section" "$now" ;;
        esac

        echolog "Subscriptions for rule '$section' are unchanged"
        return 2
        ;;
    4)
        echolog "Subscription update for rule '$section' was superseded by newer URLs"
        return 4
        ;;
    *)
        echolog "Failed to download subscriptions for rule '$section'"
        return 1
        ;;
    esac
}

subscription_update_source_handler() {
    local section="$1"
    local index="$2"
    local entry="$3"
    local source_section subscription_json_path subscription_url_cache_path metadata_output_path \
        had_usable_cache service_proxy_address update_result

    SUBSCRIPTION_SECTION_UPDATE_TOTAL=$((SUBSCRIPTION_SECTION_UPDATE_TOTAL + 1))

    if ! parse_subscription_source_entry "$entry"; then
        echolog "Invalid subscription URL in rule '$section': $SUBSCRIPTION_SOURCE_PARSE_ERROR"
        SUBSCRIPTION_SECTION_UPDATE_FAILED=$((SUBSCRIPTION_SECTION_UPDATE_FAILED + 1))
        return 0
    fi

    source_section="$(subscription_source_id "$section" "$index")"
    subscription_json_path="$(get_subscription_json_path "$source_section")"
    subscription_url_cache_path="$(get_subscription_url_cache_path "$source_section")"
    had_usable_cache=0
    subscription_cache_is_usable "$subscription_json_path" && had_usable_cache=1
    service_proxy_address="$(get_subscription_download_proxy_address "$section" "$had_usable_cache" "runtime")"
    metadata_output_path=""
    if [ -n "$SUBSCRIPTION_SECTION_METADATA_TMP" ]; then
        metadata_output_path="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${source_section}.metadata-output.XXXXXX" 2>/dev/null || true)"
    fi

    download_subscription_into_cache \
        "$section" "$SUBSCRIPTION_SOURCE_URL" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" \
        "$SUBSCRIPTION_SOURCE_USER_AGENT" "$source_section" "$metadata_output_path"
    update_result=$?

    case "$update_result" in
    0)
        subscription_metadata_append_file "$SUBSCRIPTION_SECTION_METADATA_TMP" "$metadata_output_path" "$index" "$source_section"
        SUBSCRIPTION_SECTION_UPDATE_CHANGED=$((SUBSCRIPTION_SECTION_UPDATE_CHANGED + 1))
        ;;
    2)
        subscription_metadata_append_file "$SUBSCRIPTION_SECTION_METADATA_TMP" "$metadata_output_path" "$index" "$source_section"
        SUBSCRIPTION_SECTION_UPDATE_UNCHANGED=$((SUBSCRIPTION_SECTION_UPDATE_UNCHANGED + 1))
        ;;
    4)
        SUBSCRIPTION_SECTION_UPDATE_SUPERSEDED=$((SUBSCRIPTION_SECTION_UPDATE_SUPERSEDED + 1))
        ;;
    *)
        subscription_metadata_append_cached_source "$SUBSCRIPTION_SECTION_METADATA_TMP" "$section" "$index" "$source_section" || true
        SUBSCRIPTION_SECTION_UPDATE_FAILED=$((SUBSCRIPTION_SECTION_UPDATE_FAILED + 1))
        ;;
    esac
    rm -f "$metadata_output_path"
}

subscription_update_selected_source_handler() {
    local section="$1"
    local index="$2"
    local entry="$3"
    local source_section subscription_json_path subscription_url_cache_path metadata_output_path \
        had_usable_cache service_proxy_address update_result

    [ "$index" = "$SUBSCRIPTION_SELECTED_SOURCE_INDEX" ] || return 0

    SUBSCRIPTION_SELECTED_SOURCE_FOUND=1
    if ! parse_subscription_source_entry "$entry"; then
        echolog "Invalid subscription URL in rule '$section': $SUBSCRIPTION_SOURCE_PARSE_ERROR"
        SUBSCRIPTION_SELECTED_SOURCE_RESULT=1
        return 0
    fi

    source_section="$(subscription_source_id "$section" "$index")"
    subscription_json_path="$(get_subscription_json_path "$source_section")"
    subscription_url_cache_path="$(get_subscription_url_cache_path "$source_section")"
    had_usable_cache=0
    subscription_cache_is_usable "$subscription_json_path" && had_usable_cache=1
    service_proxy_address="$(get_subscription_download_proxy_address "$section" "$had_usable_cache" "runtime")"
    metadata_output_path="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${source_section}.metadata-output.XXXXXX" 2>/dev/null || true)"

    download_subscription_into_cache \
        "$section" "$SUBSCRIPTION_SOURCE_URL" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" \
        "$SUBSCRIPTION_SOURCE_USER_AGENT" "$source_section" "$metadata_output_path"
    update_result=$?

    case "$update_result" in
    0 | 2)
        write_subscription_source_metadata_json "$section" "$index" "$source_section" "$metadata_output_path"
        ;;
    esac

    rm -f "$metadata_output_path"
    SUBSCRIPTION_SELECTED_SOURCE_RESULT="$update_result"
}

subscription_update_selected_source() {
    local section="$1"
    local source_index="$2"
    local force="$3"
    local now

    case "$section" in
    "" | */* | *..*)
        echolog "Invalid subscription rule name"
        return 1
        ;;
    esac

    case "$source_index" in
    "" | *[!0-9]*)
        echolog "Invalid subscription source index for rule '$section'"
        return 1
        ;;
    esac

    [ "$source_index" -gt 0 ] || {
        echolog "Invalid subscription source index for rule '$section'"
        return 1
    }

    rule_is_subscription_proxy "$section" || {
        echolog "Rule '$section' has no subscription sources"
        return 1
    }

    if [ "$force" -eq 0 ]; then
        subscription_update_section_is_due "$section"
        case "$?" in
        0) ;;
        1) return 3 ;;
        *) return 1 ;;
        esac
    fi

    mkdir -p "$TMP_SUBSCRIPTION_FOLDER"

    echolog "Updating subscription source '$source_index' for rule '$section'..."

    SUBSCRIPTION_SELECTED_SOURCE_INDEX="$source_index"
    SUBSCRIPTION_SELECTED_SOURCE_FOUND=0
    SUBSCRIPTION_SELECTED_SOURCE_RESULT=3
    for_each_subscription_source "$section" subscription_update_selected_source_handler

    if [ "$SUBSCRIPTION_SELECTED_SOURCE_FOUND" -eq 0 ]; then
        echolog "Subscription source '$source_index' was not found for rule '$section'"
        return 1
    fi

    case "$SUBSCRIPTION_SELECTED_SOURCE_RESULT" in
    0)
        now="$(date +%s 2>/dev/null)"
        case "$now" in
        '' | *[!0-9]*) ;;
        *) write_subscription_update_timestamp "$section" "$now" ;;
        esac

        echolog "Subscription source '$source_index' updated for rule '$section'"
        return 0
        ;;
    2)
        now="$(date +%s 2>/dev/null)"
        case "$now" in
        '' | *[!0-9]*) ;;
        *) write_subscription_update_timestamp "$section" "$now" ;;
        esac

        echolog "Subscription source '$source_index' for rule '$section' is unchanged"
        return 2
        ;;
    4)
        echolog "Subscription source '$source_index' update for rule '$section' was superseded by newer URLs"
        return 4
        ;;
    *)
        echolog "Failed to download subscription source '$source_index' for rule '$section'"
        return 1
        ;;
    esac
}

subscription_update_handler() {
    local section="$1"
    local result

    subscription_update_section "$section" "$SUBSCRIPTION_UPDATE_FORCE"
    result=$?

    case "$result" in
    0) SUBSCRIPTION_UPDATED_SECTIONS=$((SUBSCRIPTION_UPDATED_SECTIONS + 1)) ;;
    1) SUBSCRIPTION_FAILED_SECTIONS=$((SUBSCRIPTION_FAILED_SECTIONS + 1)) ;;
    2) SUBSCRIPTION_UNCHANGED_SECTIONS=$((SUBSCRIPTION_UNCHANGED_SECTIONS + 1)) ;;
    4) SUBSCRIPTION_SUPERSEDED_SECTIONS=$((SUBSCRIPTION_SUPERSEDED_SECTIONS + 1)) ;;
    3) ;;
    esac
}

subscription_update_abort_cleanup() {
    local status="${1:-1}"

    trap - EXIT INT TERM
    release_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"
    release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
    exit "$status"
}

subscription_update_common_locked() {
    local force="$1"
    local target_section="${2:-}"
    local target_source_index="${3:-}"

    SUBSCRIPTION_UPDATE_FORCE="$force"
    SUBSCRIPTION_UPDATED_SECTIONS=0
    SUBSCRIPTION_FAILED_SECTIONS=0
    SUBSCRIPTION_UNCHANGED_SECTIONS=0
    SUBSCRIPTION_SUPERSEDED_SECTIONS=0

    if [ -n "$target_section" ]; then
        local result
        if [ -n "$target_source_index" ]; then
            subscription_update_selected_source "$target_section" "$target_source_index" "$force"
        else
            subscription_update_section "$target_section" "$force"
        fi
        result=$?

        case "$result" in
        0) SUBSCRIPTION_UPDATED_SECTIONS=$((SUBSCRIPTION_UPDATED_SECTIONS + 1)) ;;
        1) SUBSCRIPTION_FAILED_SECTIONS=$((SUBSCRIPTION_FAILED_SECTIONS + 1)) ;;
        2) SUBSCRIPTION_UNCHANGED_SECTIONS=$((SUBSCRIPTION_UNCHANGED_SECTIONS + 1)) ;;
        4) SUBSCRIPTION_SUPERSEDED_SECTIONS=$((SUBSCRIPTION_SUPERSEDED_SECTIONS + 1)) ;;
        3) ;;
        esac
    else
        config_foreach subscription_update_handler "section"
    fi

    if [ "$SUBSCRIPTION_UPDATED_SECTIONS" -eq 0 ]; then
        if [ "$SUBSCRIPTION_SUPERSEDED_SECTIONS" -gt 0 ]; then
            echolog "Subscription update was superseded by newer configuration"
            return 0
        fi

        if [ "$SUBSCRIPTION_FAILED_SECTIONS" -gt 0 ]; then
            echolog "Subscription update finished with errors; keeping the last working cache"
            return 1
        fi

        if [ "$SUBSCRIPTION_UNCHANGED_SECTIONS" -gt 0 ]; then
            echolog "Subscription update completed: no changes detected"
        else
            echolog "No subscription rules are due for update"
        fi
        return 0
    fi

    echolog "Reloading sing-box to apply updated subscriptions..."
    config_load "$PODKOP_CONFIG_NAME"
    prepare_all_server_defaults
    validate_runtime_settings
    sing_box_configure_service
    PODKOP_NFT_POPULATE_ENABLED=0
    sing_box_init_config
    PODKOP_NFT_POPULATE_ENABLED=1
    reload_sing_box_runtime
    capture_reload_state
    write_reload_state

    if [ "$SUBSCRIPTION_FAILED_SECTIONS" -gt 0 ]; then
        echolog "Subscription update applied for changed rules; failed rules kept their previous cache"
    else
        echolog "Subscription update completed"
    fi
}

subscription_update_common() {
    local force="$1"
    local target_section="${2:-}"
    local target_source_index="${3:-}"
    local result

    config_load "$PODKOP_CONFIG_NAME"
    migration
    ensure_runtime_dirs

    if [ "$force" -eq 1 ]; then
        if ! acquire_runtime_dir_lock_wait "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR" 300; then
            echolog "Subscription update is already running"
            mark_pending_reload "subscription_update_busy"
            return 1
        fi
    elif ! acquire_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"; then
        echolog "Subscription update is already running"
        return 0
    fi

    if [ "$force" -eq 1 ]; then
        if ! acquire_runtime_dir_lock_wait "$PODKOP_RELOAD_LOCK_DIR" 300; then
            release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
            echolog "Podkop Plus reload is already running; skipping subscription update"
            mark_pending_reload "reload_busy"
            return 1
        fi
    elif ! acquire_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"; then
        release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
        echolog "Podkop Plus reload is already running; skipping subscription update"
        return 0
    fi

    trap 'subscription_update_abort_cleanup "$?"' EXIT INT TERM

    subscription_update_common_locked "$force" "$target_section" "$target_source_index"
    result=$?

    trap - EXIT INT TERM

    release_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"
    release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
    run_pending_reload_if_requested
    return "$result"
}

subscription_update_if_due() {
    echolog "Starting due subscription update..."
    subscription_update_common 0
}

subscription_update() {
    local target_section="${1:-}"
    local target_source_index="${2:-}"

    if [ -n "$target_section" ]; then
        echolog "Starting subscription update for rule '$target_section'..."
    else
        echolog "Starting subscription update..."
    fi
    subscription_update_common 1 "$target_section" "$target_source_index"
}

# sing-box funcs
sing_box_configure_service() {
    local sing_box_enabled sing_box_user sing_box_config_path sing_box_conffile
    sing_box_enabled="$(uci_get "sing-box" "main" "enabled")"
    sing_box_user="$(uci_get "sing-box" "main" "user")"

    if [ "$sing_box_enabled" -ne 1 ]; then
        uci_set "sing-box" "main" "enabled" 1
        uci_commit "sing-box"
        log "sing-box service has been enabled"
    fi

    if [ "$sing_box_user" != "root" ]; then
        uci_set "sing-box" "main" "user" "root"
        uci_commit "sing-box"
        log "sing-box service user has been changed to root"
    fi

    config_get sing_box_config_path "settings" "config_path"
    sing_box_conffile="$(uci_get "sing-box" "main" "conffile")"
    log "sing-box config path: $sing_box_config_path" "debug"
    log "sing-box service conffile: $sing_box_conffile" "debug"
    if [ "$sing_box_conffile" != "$sing_box_config_path" ]; then
        uci_set "sing-box" "main" "conffile" "$sing_box_config_path"
        uci_commit "sing-box"
        log "Configuration file path has been set to $sing_box_config_path"
    fi

    [ -f /etc/rc.d/S99sing-box ] && log "Disable sing-box" && /etc/init.d/sing-box disable
}

sing_box_init_config() {
    local config='{"log":{},"dns":{},"ntp":{},"certificate":{},"endpoints":[],"inbounds":[],"outbounds":[],"route":{},"services":[],"experimental":{}}'

    PODKOP_URLTEST_SELECTOR_SWITCHES=""
    configured_rulesets=""
    sing_box_configure_log
    sing_box_configure_inbounds
    sing_box_configure_dns
    sing_box_configure_route
    sing_box_configure_experimental
    sing_box_additional_inbounds
    sing_box_configure_outbounds
    sing_box_save_config
}

sing_box_configure_log() {
    log "Configure the log section of a sing-box JSON configuration"

    local log_level
    config_get log_level "settings" "log_level" "warn"
    config=$(sing_box_cm_configure_log "$config" false "$log_level" false)
}

sing_box_configure_inbounds() {
    log "Configure the inbounds section of a sing-box JSON configuration"

    config=$(
        sing_box_cm_add_tproxy_inbound \
            "$config" "$SB_TPROXY_INBOUND_TAG" "$SB_TPROXY_INBOUND_ADDRESS" "$SB_TPROXY_INBOUND_PORT" true true
    )
    config=$(
        sing_box_cm_add_direct_inbound "$config" "$SB_DNS_INBOUND_TAG" "$SB_DNS_INBOUND_ADDRESS" "$SB_DNS_INBOUND_PORT"
    )

    config_foreach configure_server_inbound "server"
}

configure_server_inbound() {
    local section="$1"
    local protocol listen listen_port public_host inbound_tag users_tmp status \
        shadowsocks_method server_password hysteria2_up_mbps hysteria2_down_mbps \
        hysteria2_obfs_type hysteria2_obfs_password

    server_is_enabled "$section" || return 0

    config_get protocol "$section" "protocol" "vless"
    case "$protocol" in
    shadowsocks | socks | vmess | vless | trojan | hysteria2 | tailscale) ;;
    *)
        log "Server '$section' has unsupported protocol '$protocol'. Aborted." "fatal"
        exit 1
        ;;
    esac

    if [ "$protocol" = "tailscale" ]; then
        configure_tailscale_server_endpoint "$section"
        return 0
    fi

    config_get listen "$section" "listen" "0.0.0.0"
    config_get listen_port "$section" "listen_port"
    config_get public_host "$section" "public_host"

    if ! is_ipv4 "$listen"; then
        log "Server '$section' has invalid listen address '$listen'. Use an IPv4 address. Aborted." "fatal"
        exit 1
    fi
    if ! server_port_is_valid "$listen_port"; then
        log "Server '$section' has invalid listen port '$listen_port'. Aborted." "fatal"
        exit 1
    fi
    if [ -n "$public_host" ] && ! server_host_is_valid "$public_host"; then
        log "Server '$section' has invalid public host '$public_host'. Use a domain name or IPv4 address. Aborted." "fatal"
        exit 1
    fi

    inbound_tag="$(get_server_inbound_tag_by_section "$section")"

    case "$protocol" in
    shadowsocks)
        config_get shadowsocks_method "$section" "shadowsocks_method" "aes-128-gcm"
        config_get server_password "$section" "server_password"
        case "$shadowsocks_method" in
        aes-128-gcm | aes-256-gcm | chacha20-ietf-poly1305) ;;
        *)
            log "Server '$section' has unsupported Shadowsocks method '$shadowsocks_method'. Aborted." "fatal"
            exit 1
            ;;
        esac
        if [ -z "$server_password" ]; then
            log "Server '$section' has no client password. Aborted." "fatal"
            exit 1
        fi
        if ! server_client_value_is_valid "$server_password"; then
            log "Server '$section' has invalid Shadowsocks password. It must not contain control characters. Aborted." "fatal"
            exit 1
        fi
        config=$(sing_box_cm_add_shadowsocks_inbound "$config" "$inbound_tag" "$listen" "$listen_port" "$shadowsocks_method" "$server_password")
        status=$?
        ;;
    socks | vmess | vless | trojan | hysteria2)
        users_tmp="$(server_write_users_json "$section" "$protocol")" || {
            log "Failed to prepare users for server '$section'. Aborted." "fatal"
            exit 1
        }

        case "$protocol" in
        socks)
            config=$(sing_box_cm_add_socks_inbound "$config" "$inbound_tag" "$listen" "$listen_port" "$users_tmp")
            ;;
        vmess)
            config=$(sing_box_cm_add_vmess_inbound "$config" "$inbound_tag" "$listen" "$listen_port" "$users_tmp")
            ;;
        vless)
            config=$(sing_box_cm_add_vless_inbound "$config" "$inbound_tag" "$listen" "$listen_port" "$users_tmp")
            ;;
        trojan)
            config=$(sing_box_cm_add_trojan_inbound "$config" "$inbound_tag" "$listen" "$listen_port" "$users_tmp")
            ;;
        hysteria2)
            config_get hysteria2_up_mbps "$section" "hysteria2_up_mbps"
            config_get hysteria2_down_mbps "$section" "hysteria2_down_mbps"
            config_get hysteria2_obfs_type "$section" "hysteria2_obfs_type"
            config_get hysteria2_obfs_password "$section" "hysteria2_obfs_password"
            case "$hysteria2_obfs_type" in
            '' | salamander) ;;
            *)
                log "Server '$section' has unsupported Hysteria2 obfuscation '$hysteria2_obfs_type'. Aborted." "fatal"
                exit 1
                ;;
            esac
            if [ "$hysteria2_obfs_type" = "salamander" ] && [ -z "$hysteria2_obfs_password" ]; then
                log "Server '$section' uses Hysteria2 salamander obfuscation but password is not set. Aborted." "fatal"
                exit 1
            fi
            config=$(
                sing_box_cm_add_hysteria2_inbound \
                    "$config" "$inbound_tag" "$listen" "$listen_port" "$users_tmp" \
                    "$hysteria2_up_mbps" "$hysteria2_down_mbps" "$hysteria2_obfs_type" "$hysteria2_obfs_password"
            )
            ;;
        esac
        status=$?
        rm -f "$users_tmp"
        ;;
    esac
    [ "$status" -eq 0 ] || {
        log "Failed to configure server inbound '$section'. Aborted." "fatal"
        exit 1
    }

    server_apply_tls "$section" "$inbound_tag"
    server_apply_transport "$section" "$inbound_tag"
}

configure_tailscale_server_endpoint() {
    local section="$1"
    local endpoint_tag safe_name state_directory auth_key control_url hostname accept_routes \
        advertise_routes advertise_exit_node status

    endpoint_tag="$(get_server_inbound_tag_by_section "$section")"
    safe_name="$(server_safe_filename "$section")"

    state_directory="/etc/podkop-plus/tailscale/$safe_name"
    config_get auth_key "$section" "tailscale_auth_key"
    config_get control_url "$section" "tailscale_control_url" "https://controlplane.tailscale.com"
    config_get hostname "$section" "tailscale_hostname" "podkop-$safe_name"
    config_get_bool accept_routes "$section" "tailscale_accept_routes" 0
    config_list_foreach "$section" "tailscale_advertise_routes" validate_tailscale_advertise_route_handler "$section"
    advertise_routes="$(config_list_to_json "$section" "tailscale_advertise_routes")"
    config_get_bool advertise_exit_node "$section" "tailscale_advertise_exit_node" 0

    if ! server_absolute_path_is_valid "$state_directory"; then
        log "Server '$section' has invalid Tailscale state directory '$state_directory'. Use an absolute path. Aborted." "fatal"
        exit 1
    fi
    if [ -z "$auth_key" ]; then
        log "Server '$section' uses Tailscale but auth key is empty. Aborted." "fatal"
        exit 1
    fi
    if ! server_http_url_is_valid "$control_url"; then
        log "Server '$section' has invalid Tailscale control URL '$control_url'. Use an HTTP or HTTPS URL. Aborted." "fatal"
        exit 1
    fi
    if ! server_host_is_valid "$hostname"; then
        log "Server '$section' has invalid Tailscale hostname '$hostname'. Aborted." "fatal"
        exit 1
    fi

    mkdir -p "$state_directory" || {
        log "Failed to create Tailscale state directory for server '$section'. Aborted." "fatal"
        exit 1
    }

    config=$(
        sing_box_cm_add_tailscale_endpoint \
            "$config" \
            "$endpoint_tag" \
            "$state_directory" \
            "$auth_key" \
            "$control_url" \
            "0" \
            "$hostname" \
            "$accept_routes" \
            "" \
            "0" \
            "$advertise_routes" \
            "$advertise_exit_node" \
            ""
    )
    status=$?
    [ "$status" -eq 0 ] || {
        log "Failed to configure Tailscale endpoint '$section'. Aborted." "fatal"
        exit 1
    }
}

validate_tailscale_advertise_route_handler() {
    local value="$1"
    local section="$2"

    if ! server_cidr_is_valid "$value"; then
        log "Server '$section' has invalid Tailscale advertised route '$value'. Use CIDR prefixes like 192.168.1.0/24. Aborted." "fatal"
        exit 1
    fi
}

sing_box_configure_outbounds() {
    log "Configure the outbounds section of a sing-box JSON configuration"

    config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG")

    config_foreach configure_outbound_handler "section"
}

json_array_append_string() {
    local array_json="$1"
    local value="$2"
    local array_tmp result status

    array_tmp="$(mktemp)" || return 1
    printf '%s' "$array_json" > "$array_tmp" || {
        rm -f "$array_tmp"
        return 1
    }

    result="$(ucode "$PODKOP_LIB/json_utils.uc" array-append-string "$array_tmp" "$value" 2>/dev/null)"
    status=$?
    rm -f "$array_tmp"

    [ "$status" -eq 0 ] || return 1
    [ -n "$result" ] || return 1
    printf '%s\n' "$result"
}

json_arrays_concat() {
    local first_json="$1"
    local second_json="$2"
    local first_tmp second_tmp result status

    first_tmp="$(mktemp)" || return 1
    second_tmp="$(mktemp)" || {
        rm -f "$first_tmp"
        return 1
    }

    printf '%s' "$first_json" > "$first_tmp" || {
        rm -f "$first_tmp" "$second_tmp"
        return 1
    }
    printf '%s' "$second_json" > "$second_tmp" || {
        rm -f "$first_tmp" "$second_tmp"
        return 1
    }

    result="$(ucode "$PODKOP_LIB/json_utils.uc" arrays-concat "$first_tmp" "$second_tmp" 2>/dev/null)"
    status=$?
    rm -f "$first_tmp" "$second_tmp"

    [ "$status" -eq 0 ] || return 1
    [ -n "$result" ] || return 1
    printf '%s\n' "$result"
}

json_object_set_string() {
    local object_json="$1"
    local key="$2"
    local value="$3"
    local object_tmp result status

    object_tmp="$(mktemp)" || return 1
    printf '%s' "$object_json" > "$object_tmp" || {
        rm -f "$object_tmp"
        return 1
    }

    result="$(ucode "$PODKOP_LIB/json_utils.uc" object-set-string "$object_tmp" "$key" "$value" 2>/dev/null)"
    status=$?
    rm -f "$object_tmp"

    [ "$status" -eq 0 ] || return 1
    [ -n "$result" ] || return 1
    printf '%s\n' "$result"
}

json_objects_merge() {
    local first_json="$1"
    local second_json="$2"
    local first_tmp second_tmp result status

    first_tmp="$(mktemp)" || return 1
    second_tmp="$(mktemp)" || {
        rm -f "$first_tmp"
        return 1
    }

    printf '%s' "$first_json" > "$first_tmp" || {
        rm -f "$first_tmp" "$second_tmp"
        return 1
    }
    printf '%s' "$second_json" > "$second_tmp" || {
        rm -f "$first_tmp" "$second_tmp"
        return 1
    }

    result="$(ucode "$PODKOP_LIB/json_utils.uc" objects-merge "$first_tmp" "$second_tmp" 2>/dev/null)"
    status=$?
    rm -f "$first_tmp" "$second_tmp"

    [ "$status" -eq 0 ] || return 1
    [ -n "$result" ] || return 1
    printf '%s\n' "$result"
}

config_list_append_json() {
    local value="$1"

    CONFIG_LIST_VALUES_JSON="$(json_array_append_string "$CONFIG_LIST_VALUES_JSON" "$value")" || CONFIG_LIST_VALUES_JSON="[]"
}

config_list_to_json() {
    local section="$1"
    local option="$2"
    local value

    CONFIG_LIST_VALUES_JSON="[]"
    config_list_foreach "$section" "$option" config_list_append_json
    if [ "$CONFIG_LIST_VALUES_JSON" = "[]" ]; then
        config_get value "$section" "$option"
        if [ -n "$value" ]; then
            config_list_append_json "$value"
        fi
    fi
    printf '%s\n' "$CONFIG_LIST_VALUES_JSON"
}

server_port_is_valid() {
    local port="$1"

    case "$port" in
    '' | *[!0-9]*) return 1 ;;
    esac

    [ "$port" -ge 1 ] && [ "$port" -le 65535 ]
}

server_normalize_host() {
    local value="$1"

    value="${value#[}"
    value="${value%]}"
    printf '%s\n' "$value" | tr 'A-Z' 'a-z'
}

server_host_is_valid() {
    local host

    host="$(server_normalize_host "$1")"
    [ -n "$host" ] || return 1
    is_ipv4 "$host" || is_domain "$host"
}

server_absolute_path_is_valid() {
    case "$1" in
    /*) return 0 ;;
    *) return 1 ;;
    esac
}

server_file_path_is_valid() {
    local path="$1"

    [ -n "$path" ] || return 1

    case "$path" in
    / | */) return 1 ;;
    esac

    printf '%s\n' "$path" | grep -Eq '^/[A-Za-z0-9_./-]+$'
}

server_client_value_is_valid() {
    local value="$1"

    [ -n "$value" ] || return 1
    ! printf '%s' "$value" | grep -q '[[:cntrl:]]'
}

server_http_url_is_valid() {
    local value="$1" rest host

    case "$value" in
    http://* | https://*) ;;
    *) return 1 ;;
    esac

    rest="${value#*://}"
    host="${rest%%/*}"
    host="${host%%:*}"
    server_host_is_valid "$host"
}

server_duration_is_valid() {
    local value="$1"

    [ -n "$value" ] || return 1
    printf '%s\n' "$value" | grep -Eq '^([0-9]+(ns|us|ms|s|m|h))+$'
}

server_default_security_for_protocol() {
    case "$1" in
    vless) printf '%s\n' "reality" ;;
    trojan | hysteria2) printf '%s\n' "tls" ;;
    *) printf '%s\n' "none" ;;
    esac
}

server_effective_security() {
    local section="$1"
    local protocol="$2"
    local security

    config_get security "$section" "security"
    [ -n "$security" ] || security="$(server_default_security_for_protocol "$protocol")"

    case "$protocol" in
    shadowsocks | socks | tailscale)
        security="none"
        ;;
    hysteria2)
        security="tls"
        ;;
    vmess | trojan)
        [ "$security" != "reality" ] || security="$(server_default_security_for_protocol "$protocol")"
        ;;
    esac

    printf '%s\n' "$security"
}

server_default_set_option() {
    local section="$1"
    local option="$2"
    local value="$3"
    local current

    [ -n "$value" ] || return 0
    current="$(uci -q get "$PODKOP_CONFIG_NAME.$section.$option" 2>/dev/null)"
    [ -n "$current" ] && return 0

    uci set "$PODKOP_CONFIG_NAME.$section.$option=$value"
    SERVER_DEFAULTS_CHANGED=1
}

server_set_option() {
    local section="$1"
    local option="$2"
    local value="$3"
    local current

    [ -n "$value" ] || return 0
    current="$(uci -q get "$PODKOP_CONFIG_NAME.$section.$option" 2>/dev/null)"
    [ "$current" = "$value" ] && return 0

    uci set "$PODKOP_CONFIG_NAME.$section.$option=$value"
    SERVER_DEFAULTS_CHANGED=1
}

server_default_add_list() {
    local section="$1"
    local option="$2"
    local value="$3"
    local current quoted_value

    [ -n "$value" ] || return 0
    current="$(uci -q get "$PODKOP_CONFIG_NAME.$section.$option" 2>/dev/null)"
    [ -n "$current" ] && return 0

    quoted_value="$(printf "%s" "$value" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/")"
    printf 'add_list %s.%s.%s=%s\n' "$PODKOP_CONFIG_NAME" "$section" "$option" "$quoted_value" | uci -q batch
    SERVER_DEFAULTS_CHANGED=1
}

server_generate_uuid() {
    sing-box generate uuid 2>/dev/null | awk 'NF { print; exit }'
}

server_generate_password() {
    sing-box generate rand --base64 18 2>/dev/null | tr -d '\n'
}

server_generate_short_id() {
    sing-box generate rand --hex 4 2>/dev/null | tr -d '\n'
}

server_generate_reality_keypair() {
    local output

    SERVER_REALITY_PRIVATE_KEY=""
    SERVER_REALITY_PUBLIC_KEY=""
    output="$(sing-box generate reality-keypair 2>/dev/null)" || return 1
    SERVER_REALITY_PRIVATE_KEY="$(printf '%s\n' "$output" | sed -n 's/^PrivateKey:[[:space:]]*//p' | head -n 1)"
    SERVER_REALITY_PUBLIC_KEY="$(printf '%s\n' "$output" | sed -n 's/^PublicKey:[[:space:]]*//p' | head -n 1)"

    [ -n "$SERVER_REALITY_PRIVATE_KEY" ] && [ -n "$SERVER_REALITY_PUBLIC_KEY" ]
}

generate_reality_keypair() {
    if ! server_generate_reality_keypair; then
        printf '{"success":false,"message":"Failed to generate Reality key pair"}\n'
        return 1
    fi

    printf '{"success":true,"private_key":"%s","public_key":"%s"}\n' \
        "$SERVER_REALITY_PRIVATE_KEY" \
        "$SERVER_REALITY_PUBLIC_KEY"
}

server_safe_filename() {
    printf '%s' "$1" | tr -c 'A-Za-z0-9_.-' '_'
}

server_generate_tls_keypair_files() {
    local server_name="$1"
    local certificate_path="$2"
    local key_path="$3"
    local output key cert key_dir cert_dir

    output="$(sing-box generate tls-keypair "$server_name" 2>/dev/null)" || return 1
    key="$(printf '%s\n' "$output" | sed -n '/BEGIN PRIVATE KEY/,/END PRIVATE KEY/p')"
    cert="$(printf '%s\n' "$output" | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p')"
    [ -n "$key" ] && [ -n "$cert" ] || return 1

    key_dir="${key_path%/*}"
    cert_dir="${certificate_path%/*}"
    mkdir -p "$key_dir" "$cert_dir" || return 1

    printf '%s\n' "$key" > "$key_path" || return 1
    chmod 600 "$key_path" 2>/dev/null || true
    printf '%s\n' "$cert" > "$certificate_path" || return 1
}

server_is_public_ipv4() {
    local ip="$1" a b c d

    printf '%s' "$ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$' || return 1
    IFS=. read -r a b c d <<EOF
$ip
EOF
    [ "$a" -le 255 ] && [ "$b" -le 255 ] && [ "$c" -le 255 ] && [ "$d" -le 255 ] || return 1

    case "$ip" in
    0.* | 10.* | 127.* | 169.254.* | 192.168.* | 192.0.* | 192.2.* | 198.18.* | 198.19.* | 198.51.* | 203.0.*)
        return 1
        ;;
    esac

    [ "$a" -ge 224 ] && return 1
    [ "$a" -eq 100 ] && [ "$b" -ge 64 ] && [ "$b" -le 127 ] && return 1
    [ "$a" -eq 172 ] && [ "$b" -ge 16 ] && [ "$b" -le 31 ] && return 1

    return 0
}

server_cidr_is_valid() {
    local value="$1" address prefix extra

    case "$value" in
    */*) ;;
    *) return 1 ;;
    esac

    address="${value%%/*}"
    prefix="${value#*/}"
    extra="${prefix#*/}"
    [ "$extra" = "$prefix" ] || return 1

    case "$prefix" in
    '' | *[!0-9]*) return 1 ;;
    esac

    if is_ipv4 "$address"; then
        [ "$prefix" -ge 0 ] && [ "$prefix" -le 32 ]
        return
    fi

    if is_ipv6_literal "$address"; then
        [ "$prefix" -ge 0 ] && [ "$prefix" -le 128 ]
        return
    fi

    return 1
}

server_detect_default_public_host() {
    local interface ip lan_ip

    for interface in wan wwan; do
        ip="$(ubus -S call "network.interface.$interface" status 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)"
        if server_is_public_ipv4 "$ip"; then
            printf '%s\n' "$ip"
            return 0
        fi
    done

    lan_ip="$(uci -q get network.lan.ipaddr 2>/dev/null)"
    if printf '%s' "$lan_ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
        printf '%s\n' "$lan_ip"
    fi
}

server_first_user_entry_handler() {
    local entry="$1"

    [ -n "$SERVER_FIRST_USER_ENTRY" ] && return 0
    SERVER_FIRST_USER_ENTRY="$entry"
}

server_prepare_legacy_user_defaults() {
    local section="$1"
    local protocol="$2"
    local entry name rest credential extra

    SERVER_FIRST_USER_ENTRY=""
    config_list_foreach "$section" "server_users" server_first_user_entry_handler
    entry="$SERVER_FIRST_USER_ENTRY"
    SERVER_FIRST_USER_ENTRY=""
    [ -n "$entry" ] || return 0

    name="${entry%%|*}"
    if [ "$entry" = "$name" ]; then
        credential="$entry"
        name="client"
        extra=""
    else
        rest="${entry#*|}"
        credential="${rest%%|*}"
        if [ "$rest" = "$credential" ]; then
            extra=""
        else
            extra="${rest#*|}"
        fi
    fi

    case "$protocol" in
    vless | vmess)
        server_default_set_option "$section" "server_uuid" "$credential"
        ;;
    shadowsocks | socks | trojan | hysteria2)
        server_default_set_option "$section" "server_password" "$credential"
        ;;
    esac
    if [ "$protocol" = "socks" ] && [ -n "$name" ]; then
        server_default_set_option "$section" "server_username" "$name"
    fi
    if [ "$protocol" = "vless" ] && [ -n "$extra" ]; then
        server_default_set_option "$section" "vless_flow" "$extra"
    fi
}

server_prepare_tls_defaults() {
    local section="$1"
    local protocol="$2"
    local security="$3"
    local enabled tls_server_name certificate_path key_path safe_name default_dir

    [ "$security" = "tls" ] || return 0
    config_get_bool enabled "$section" "enabled" 1
    [ "$enabled" -eq 1 ] || return 0

    config_get tls_server_name "$section" "tls_server_name"
    [ -n "$tls_server_name" ] || tls_server_name="www.microsoft.com"
    server_default_set_option "$section" "tls_server_name" "$tls_server_name"

    config_get certificate_path "$section" "tls_certificate_path"
    config_get key_path "$section" "tls_key_path"
    if [ -z "$certificate_path" ] || [ -z "$key_path" ]; then
        safe_name="$(server_safe_filename "$section")"
        default_dir="/etc/podkop-plus/server-certs"
        certificate_path="${certificate_path:-$default_dir/$safe_name.crt}"
        key_path="${key_path:-$default_dir/$safe_name.key}"
        server_set_option "$section" "tls_certificate_path" "$certificate_path"
        server_set_option "$section" "tls_key_path" "$key_path"
    fi

    if ! server_file_path_is_valid "$certificate_path"; then
        log "Server '$section' has invalid TLS certificate path. Specify a file path. Aborted." "fatal"
        exit 1
    fi
    if ! server_file_path_is_valid "$key_path"; then
        log "Server '$section' has invalid TLS key path. Specify a file path. Aborted." "fatal"
        exit 1
    fi
    if [ "$certificate_path" = "$key_path" ]; then
        log "Server '$section' has the same TLS certificate and key path. Specify different files. Aborted." "fatal"
        exit 1
    fi

    if [ ! -s "$certificate_path" ] || [ ! -s "$key_path" ]; then
        log "Generating self-signed TLS certificate for server '$section'" "info"
        if ! server_generate_tls_keypair_files "$tls_server_name" "$certificate_path" "$key_path"; then
            log "Failed to generate TLS certificate for server '$section'. Aborted." "fatal"
            exit 1
        fi
    fi
}

prepare_server_defaults() {
    local section="$1"
    local protocol security uuid password server_username label short_id reality_private_key reality_public_key safe_name

    config_get protocol "$section" "protocol" "vless"
    case "$protocol" in
    shadowsocks | socks | vmess | vless | trojan | hysteria2 | tailscale) ;;
    *)
        log "Server '$section' has unsupported protocol '$protocol'. Aborted." "fatal"
        exit 1
        ;;
    esac

    server_default_set_option "$section" "protocol" "$protocol"
    server_default_set_option "$section" "label" "$section"
    server_default_set_option "$section" "enabled" "1"
    server_default_set_option "$section" "listen" "0.0.0.0"
    server_default_set_option "$section" "listen_port" "443"
    server_default_set_option "$section" "public_host" "$(server_detect_default_public_host)"
    server_default_set_option "$section" "routing_mode" "rules"

    security="$(server_effective_security "$section" "$protocol")"
    server_set_option "$section" "security" "$security"

    server_prepare_legacy_user_defaults "$section" "$protocol"

    case "$protocol" in
    vless | vmess)
        uuid="$(uci -q get "$PODKOP_CONFIG_NAME.$section.server_uuid" 2>/dev/null)"
        if [ -z "$uuid" ]; then
            uuid="$(server_generate_uuid)"
            server_default_set_option "$section" "server_uuid" "$uuid"
        fi
        ;;
    shadowsocks | socks | trojan | hysteria2)
        password="$(uci -q get "$PODKOP_CONFIG_NAME.$section.server_password" 2>/dev/null)"
        if [ -z "$password" ]; then
            password="$(server_generate_password)"
            server_default_set_option "$section" "server_password" "$password"
        fi
        ;;
    esac

    if [ "$protocol" = "socks" ]; then
        server_username="$(uci -q get "$PODKOP_CONFIG_NAME.$section.server_username" 2>/dev/null)"
        if [ -z "$server_username" ]; then
            config_get label "$section" "label" "$section"
            server_default_set_option "$section" "server_username" "$label"
        fi
    fi

    if [ "$protocol" = "vless" ]; then
        server_default_set_option "$section" "vless_flow" "none"
    fi
    if [ "$protocol" = "vmess" ]; then
        server_default_set_option "$section" "vmess_alter_id" "0"
    fi
    if [ "$protocol" = "shadowsocks" ]; then
        server_default_set_option "$section" "shadowsocks_method" "aes-128-gcm"
    fi
    if [ "$protocol" = "tailscale" ]; then
        safe_name="$(server_safe_filename "$section")"
        server_default_set_option "$section" "tailscale_control_url" "https://controlplane.tailscale.com"
        server_default_set_option "$section" "tailscale_hostname" "podkop-$safe_name"
        server_default_set_option "$section" "tailscale_advertise_exit_node" "1"
    fi

    if [ "$security" = "reality" ]; then
        server_default_set_option "$section" "tls_server_name" "www.microsoft.com"
        server_default_set_option "$section" "client_fingerprint" "chrome"
        server_default_set_option "$section" "reality_handshake_server" "www.microsoft.com"
        server_default_set_option "$section" "reality_handshake_server_port" "443"
        short_id="$(uci -q get "$PODKOP_CONFIG_NAME.$section.reality_short_id" 2>/dev/null)"
        if [ -z "$short_id" ]; then
            server_default_set_option "$section" "reality_short_id" "$(server_generate_short_id)"
        fi
        server_default_set_option "$section" "reality_max_time_difference" "1m"

        reality_private_key="$(uci -q get "$PODKOP_CONFIG_NAME.$section.reality_private_key" 2>/dev/null)"
        reality_public_key="$(uci -q get "$PODKOP_CONFIG_NAME.$section.reality_public_key" 2>/dev/null)"
        if [ -z "$reality_private_key" ] || [ -z "$reality_public_key" ]; then
            log "Generating Reality key pair for server '$section'" "info"
            if ! server_generate_reality_keypair; then
                log "Failed to generate Reality key pair for server '$section'. Aborted." "fatal"
                exit 1
            fi
            server_set_option "$section" "reality_private_key" "$SERVER_REALITY_PRIVATE_KEY"
            server_set_option "$section" "reality_public_key" "$SERVER_REALITY_PUBLIC_KEY"
        fi
    fi

    server_prepare_tls_defaults "$section" "$protocol" "$security"
}

prepare_server_defaults_handler() {
    prepare_server_defaults "$1"
}

prepare_all_server_defaults() {
    SERVER_DEFAULTS_CHANGED=0
    config_foreach prepare_server_defaults_handler "server"
    if [ "$SERVER_DEFAULTS_CHANGED" -eq 1 ]; then
        commit_podkop_config
    fi
}

server_write_users_json() {
    local section="$1"
    local protocol="$2"
    local tsv_tmp users_tmp user_count server_user_name server_uuid server_password vless_flow vmess_alter_id credential extra

    tsv_tmp="$(mktemp)" || return 1
    users_tmp="$(mktemp)" || {
        rm -f "$tsv_tmp"
        return 1
    }

    config_get server_user_name "$section" "label" "$section"
    [ -n "$server_user_name" ] || server_user_name="$section"

    case "$protocol" in
    vless | vmess)
        config_get server_uuid "$section" "server_uuid"
        credential="$server_uuid"
        if ! printf '%s\n' "$credential" | grep -Eq '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'; then
            rm -f "$tsv_tmp" "$users_tmp"
            log "Server '$section' has an invalid client UUID. Aborted." "fatal"
            exit 1
        fi
        if [ "$protocol" = "vless" ]; then
            config_get vless_flow "$section" "vless_flow"
            [ "$vless_flow" != "none" ] || vless_flow=""
            if [ -n "$vless_flow" ] && [ "$vless_flow" != "xtls-rprx-vision" ]; then
                rm -f "$tsv_tmp" "$users_tmp"
                log "Server '$section' has unsupported VLESS flow '$vless_flow'. Aborted." "fatal"
                exit 1
            fi
            extra="$vless_flow"
        else
            config_get vmess_alter_id "$section" "vmess_alter_id" "0"
            case "$vmess_alter_id" in
            '' | *[!0-9]*)
                rm -f "$tsv_tmp" "$users_tmp"
                log "Server '$section' has invalid VMess alter_id '$vmess_alter_id'. Use a non-negative integer. Aborted." "fatal"
                exit 1
                ;;
            esac
            extra="$vmess_alter_id"
        fi
        ;;
    socks)
        config_get server_user_name "$section" "server_username"
        if [ -z "$server_user_name" ]; then
            config_get server_user_name "$section" "label" "$section"
        fi
        [ -n "$server_user_name" ] || server_user_name="$section"
        config_get server_password "$section" "server_password"
        credential="$server_password"
        extra=""
        ;;
    trojan | hysteria2)
        config_get server_password "$section" "server_password"
        credential="$server_password"
        extra=""
        ;;
    *)
        rm -f "$tsv_tmp" "$users_tmp"
        return 1
        ;;
    esac

    if ! server_client_value_is_valid "$server_user_name"; then
        rm -f "$tsv_tmp" "$users_tmp"
        log "Server '$section' has invalid client name. It must not contain control characters. Aborted." "fatal"
        exit 1
    fi

    if [ -z "$credential" ]; then
        rm -f "$tsv_tmp" "$users_tmp"
        log "Server '$section' has no client credential. Aborted." "fatal"
        exit 1
    fi
    if ! server_client_value_is_valid "$credential"; then
        rm -f "$tsv_tmp" "$users_tmp"
        log "Server '$section' has invalid client credential. It must not contain control characters. Aborted." "fatal"
        exit 1
    fi

    printf '%s\t%s\t%s\n' "$server_user_name" "$credential" "$extra" > "$tsv_tmp"

    if ! json_utils_ucode server-users-from-tsv "$protocol" "$tsv_tmp" > "$users_tmp"; then
        rm -f "$tsv_tmp" "$users_tmp"
        return 1
    fi

    rm -f "$tsv_tmp"

    user_count="$(json_utils_ucode length "$users_tmp" 2>/dev/null)"
    case "$user_count" in
    '' | *[!0-9]*) user_count=0 ;;
    esac
    if [ "$user_count" -eq 0 ]; then
        rm -f "$users_tmp"
        log "Server '$section' has no valid users configured. Aborted." "fatal"
        exit 1
    fi

    printf '%s\n' "$users_tmp"
}

validate_server_reality_short_id() {
    local value="$1"
    local section="$2"

    if ! printf '%s\n' "$value" | grep -Eq '^[0-9a-fA-F]{1,8}$'; then
        log "Server '$section' has invalid Reality short_id '$value'. Use 1-8 hex digits. Aborted." "fatal"
        exit 1
    fi
}

validate_server_reality_short_id_counted() {
    SERVER_REALITY_SHORT_ID_COUNT=$((SERVER_REALITY_SHORT_ID_COUNT + 1))
    validate_server_reality_short_id "$@"
}

validate_server_reality_short_ids() {
    local section="$1"
    local value

    SERVER_REALITY_SHORT_ID_COUNT=0
    config_list_foreach "$section" "reality_short_id" validate_server_reality_short_id_counted "$section"
    if [ "$SERVER_REALITY_SHORT_ID_COUNT" -eq 0 ]; then
        config_get value "$section" "reality_short_id"
        if [ -n "$value" ]; then
            SERVER_REALITY_SHORT_ID_COUNT=1
            validate_server_reality_short_id "$value" "$section"
        fi
    fi
}

validate_server_transport_host_handler() {
    local value="$1"
    local section="$2"

    [ -n "$value" ] || return 0
    if ! server_host_is_valid "$value"; then
        log "Server '$section' has invalid HTTP transport host '$value'. Use a domain name or IPv4 address. Aborted." "fatal"
        exit 1
    fi
}

server_apply_tls() {
    local section="$1"
    local inbound_tag="$2"
    local protocol security tls_server_name tls_alpn tls_certificate_path tls_key_path \
        reality_handshake_server reality_handshake_server_port reality_private_key \
        reality_short_id reality_max_time_difference

    config_get protocol "$section" "protocol" "vless"
    security="$(server_effective_security "$section" "$protocol")"
    case "$security" in
    none | tls | reality) ;;
    *)
        log "Server '$section' has unsupported security '$security'. Aborted." "fatal"
        exit 1
        ;;
    esac

    [ "$security" != "none" ] || return 0

    config_get tls_server_name "$section" "tls_server_name"
    tls_alpn="$(config_list_to_json "$section" "tls_alpn")"
    config_get tls_certificate_path "$section" "tls_certificate_path"
    config_get tls_key_path "$section" "tls_key_path"
    config_get reality_handshake_server "$section" "reality_handshake_server" "www.microsoft.com"
    config_get reality_handshake_server_port "$section" "reality_handshake_server_port" "443"
    config_get reality_private_key "$section" "reality_private_key"
    reality_short_id="$(config_list_to_json "$section" "reality_short_id")"
    config_get reality_max_time_difference "$section" "reality_max_time_difference" "1m"

    if ! server_host_is_valid "$tls_server_name"; then
        log "Server '$section' has invalid TLS server name/SNI '$tls_server_name'. Use a domain name or IPv4 address. Aborted." "fatal"
        exit 1
    fi

    if [ "$security" = "tls" ]; then
        if [ -z "$tls_certificate_path" ] || [ -z "$tls_key_path" ]; then
            log "Server '$section' uses TLS but certificate/key paths are empty. Aborted." "fatal"
            exit 1
        fi
        if ! server_file_path_is_valid "$tls_certificate_path"; then
            log "Server '$section' has invalid TLS certificate path. Specify a file path. Aborted." "fatal"
            exit 1
        fi
        if ! server_file_path_is_valid "$tls_key_path"; then
            log "Server '$section' has invalid TLS key path. Specify a file path. Aborted." "fatal"
            exit 1
        fi
        if [ "$tls_certificate_path" = "$tls_key_path" ]; then
            log "Server '$section' has the same TLS certificate and key path. Specify different files. Aborted." "fatal"
            exit 1
        fi
    fi

    if [ "$security" = "reality" ]; then
        if [ -z "$reality_handshake_server" ] || ! server_host_is_valid "$reality_handshake_server" || ! server_port_is_valid "$reality_handshake_server_port"; then
            log "Server '$section' has invalid Reality handshake target. Aborted." "fatal"
            exit 1
        fi
        if [ -z "$reality_private_key" ]; then
            log "Server '$section' uses Reality but private key is not set. Aborted." "fatal"
            exit 1
        fi
        if ! server_duration_is_valid "$reality_max_time_difference"; then
            log "Server '$section' has invalid Reality max time difference '$reality_max_time_difference'. Use a duration like 30s, 5m, or 1h30m. Aborted." "fatal"
            exit 1
        fi
        validate_server_reality_short_ids "$section"
        if [ "$reality_short_id" = "[]" ]; then
            log "Server '$section' uses Reality but short_id is not set. Aborted." "fatal"
            exit 1
        fi
    fi

    config=$(
        sing_box_cm_set_tls_for_inbound \
            "$config" \
            "$inbound_tag" \
            "$security" \
            "$tls_server_name" \
            "$tls_alpn" \
            "$tls_certificate_path" \
            "$tls_key_path" \
            "$reality_handshake_server" \
            "$reality_handshake_server_port" \
            "$reality_private_key" \
            "$reality_short_id" \
            "$reality_max_time_difference"
    )
}

server_apply_transport() {
    local section="$1"
    local inbound_tag="$2"
    local protocol transport transport_path transport_host transport_service_name transport_hosts

    config_get protocol "$section" "protocol" "vless"
    case "$protocol" in
    vless | vmess | trojan) ;;
    *) return 0 ;;
    esac

    config_get transport "$section" "transport" "tcp"
    case "$transport" in
    tcp | raw | ws | grpc | http | httpupgrade) ;;
    *)
        log "Server '$section' has unsupported transport '$transport'. Aborted." "fatal"
        exit 1
        ;;
    esac

    [ "$transport" != "tcp" ] && [ "$transport" != "raw" ] || return 0

    config_get transport_path "$section" "transport_path"
    config_get transport_host "$section" "transport_host"
    config_get transport_service_name "$section" "transport_service_name"
    transport_hosts="$(config_list_to_json "$section" "transport_hosts")"

    case "$transport" in
    ws | http | httpupgrade)
        if [ -n "$transport_path" ]; then
            case "$transport_path" in
            /*) ;;
            *)
                log "Server '$section' has invalid transport path '$transport_path'. It must start with /. Aborted." "fatal"
                exit 1
                ;;
            esac
        fi
        ;;
    esac
    if [ -n "$transport_host" ] && ! server_host_is_valid "$transport_host"; then
        log "Server '$section' has invalid transport host '$transport_host'. Use a domain name or IPv4 address. Aborted." "fatal"
        exit 1
    fi
    config_list_foreach "$section" "transport_hosts" validate_server_transport_host_handler "$section"
    if [ -n "$transport_service_name" ] && ! printf '%s\n' "$transport_service_name" | grep -Eq '^[A-Za-z0-9_.-]+$'; then
        log "Server '$section' has invalid gRPC service name '$transport_service_name'. Aborted." "fatal"
        exit 1
    fi

    config=$(
        sing_box_cm_set_transport_for_inbound \
            "$config" \
            "$inbound_tag" \
            "$transport" \
            "$transport_path" \
            "$transport_host" \
            "$transport_service_name" \
            "$transport_hosts"
    )
}

get_urltest_regex_matching_tags_json() {
    local tags_json="$1"
    local names_json="$2"
    local regex_json="$3"
    local tag_count regex_count index regex_index tag name regex result_json

    result_json="[]"
    tag_count="$(printf '%s' "$tags_json" | json_utils_ucode stdin-length 2>/dev/null)"
    regex_count="$(printf '%s' "$regex_json" | json_utils_ucode stdin-length 2>/dev/null)"
    case "$tag_count:$regex_count" in
    *[!0-9:]* | :* | *:)
        printf '[]\n'
        return 0
        ;;
    esac

    [ "$tag_count" -gt 0 ] && [ "$regex_count" -gt 0 ] || {
        printf '[]\n'
        return 0
    }

    index=0
    while [ "$index" -lt "$tag_count" ]; do
        tag="$(printf '%s' "$tags_json" | json_utils_ucode array-item "$index" 2>/dev/null)"
        index=$((index + 1))
        [ -n "$tag" ] || continue

        name="$(printf '%s' "$names_json" | json_utils_ucode object-get "$tag" 2>/dev/null)"
        [ -n "$name" ] || name="$tag"

        regex_index=0
        while [ "$regex_index" -lt "$regex_count" ]; do
            regex="$(printf '%s' "$regex_json" | json_utils_ucode array-item "$regex_index" 2>/dev/null)"
            regex_index=$((regex_index + 1))
            [ -n "$regex" ] || continue

            if printf '%s\n' "$name" | grep -E -e "$regex" >/dev/null 2>&1; then
                result_json="$(json_array_append_string "$result_json" "$tag")" || result_json="[]"
                break
            fi
        done
    done

    printf '%s\n' "$result_json"
}

urltest_filter_json() {
    local mode="$1"
    local tags_json="$2"
    local names_json="$3"
    local countries_json="$4"
    local match_names_json="$5"
    local regex_tags_json="$6"
    local match_countries_json="$7"
    local tags_tmp names_tmp countries_tmp match_names_tmp regex_tags_tmp match_countries_tmp result status

    tags_tmp="$(mktemp)" || return 1
    names_tmp="$(mktemp)" || {
        rm -f "$tags_tmp"
        return 1
    }
    countries_tmp="$(mktemp)" || {
        rm -f "$tags_tmp" "$names_tmp"
        return 1
    }
    match_names_tmp="$(mktemp)" || {
        rm -f "$tags_tmp" "$names_tmp" "$countries_tmp"
        return 1
    }
    regex_tags_tmp="$(mktemp)" || {
        rm -f "$tags_tmp" "$names_tmp" "$countries_tmp" "$match_names_tmp"
        return 1
    }
    match_countries_tmp="$(mktemp)" || {
        rm -f "$tags_tmp" "$names_tmp" "$countries_tmp" "$match_names_tmp" "$regex_tags_tmp"
        return 1
    }

    if ! printf '%s' "$tags_json" > "$tags_tmp" ||
        ! printf '%s' "$names_json" > "$names_tmp" ||
        ! printf '%s' "$countries_json" > "$countries_tmp" ||
        ! printf '%s' "$match_names_json" > "$match_names_tmp" ||
        ! printf '%s' "$regex_tags_json" > "$regex_tags_tmp" ||
        ! printf '%s' "$match_countries_json" > "$match_countries_tmp"; then
        rm -f "$tags_tmp" "$names_tmp" "$countries_tmp" "$match_names_tmp" "$regex_tags_tmp" "$match_countries_tmp"
        return 1
    fi

    result="$(json_utils_ucode urltest-filter "$mode" "$tags_tmp" "$names_tmp" "$countries_tmp" "$match_names_tmp" "$regex_tags_tmp" "$match_countries_tmp" 2>/dev/null)"
    status=$?
    rm -f "$tags_tmp" "$names_tmp" "$countries_tmp" "$match_names_tmp" "$regex_tags_tmp" "$match_countries_tmp"

    [ "$status" -eq 0 ] || return 1
    [ -n "$result" ] || result="[]"
    printf '%s\n' "$result"
}

is_ipv6_literal() {
    local value="$1"

    case "$value" in
    *:*) ;;
    *) return 1 ;;
    esac

    printf '%s' "$value" | grep -Eq '^[0-9A-Fa-f:.]+$'
}

normalize_country_server_key() {
    printf '%s' "$1" | trim_string | tr 'A-Z' 'a-z'
}

resolve_country_server_ip() {
    local server="$1"
    local normalized ip

    normalized="$(normalize_country_server_key "$server")"
    [ -n "$normalized" ] || return 1

    if is_ipv4 "$normalized" || is_ipv6_literal "$normalized"; then
        printf '%s\n' "$normalized"
        return 0
    fi

    is_domain "$normalized" || return 1

    if command -v dig >/dev/null 2>&1; then
        ip="$(dig +short "$normalized" A +time=2 +tries=1 2>/dev/null | awk '
            /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { print; exit }
        ')"
        if [ -z "$ip" ]; then
            ip="$(dig +short "$normalized" AAAA +time=2 +tries=1 2>/dev/null | awk '
                /^[0-9A-Fa-f:]+$/ { print; exit }
            ')"
        fi
    elif command -v nslookup >/dev/null 2>&1; then
        ip="$(nslookup "$normalized" 2>/dev/null | awk '
            /^Address[[:space:]]*[0-9]*:[[:space:]]*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { print $NF; exit }
            /^Address[[:space:]]*[0-9]*:[[:space:]]*[0-9A-Fa-f:]+$/ { print $NF; exit }
        ')"
    fi

    [ -n "$ip" ] || return 1
    printf '%s\n' "$ip"
}

read_section_outbound_country_cache() {
    local section="$1"
    local cache_path countries_json

    case "$section" in
    "" | */* | *..*)
        printf '{}\n'
        return 0
        ;;
    esac

    cache_path="$(get_section_cache_path "$section")"
    if [ -s "$cache_path" ]; then
        countries_json="$(json_utils_ucode section-countries "$cache_path" 2>/dev/null)"
        if [ -n "$countries_json" ]; then
            printf '%s\n' "$countries_json"
            return 0
        fi
    fi

    printf '{}\n'
}

country_is_lookup_ips() {
    local ips_json="$1"
    local ip_count start end batch_json body_tmp http_code body_error
    local ips_tmp result_tmp result_json status

    ips_tmp="$(mktemp)" || return 1
    result_tmp="$(mktemp)" || return 1
    : > "$result_tmp"

    printf '%s' "$ips_json" > "$ips_tmp" || {
        rm -f "$ips_tmp" "$result_tmp"
        return 1
    }

    ip_count="$(printf '%s' "$ips_json" | json_utils_ucode stdin-length 2>/dev/null)"
    case "$ip_count" in
    '' | *[!0-9]*)
        rm -f "$ips_tmp" "$result_tmp"
        return 1
        ;;
    esac

    start=0
    while [ "$start" -lt "$ip_count" ]; do
        end=$((start + 100))
        batch_json="$(json_utils_ucode array-slice-file "$ips_tmp" "$start" "$end" 2>/dev/null)"
        if [ -z "$batch_json" ] || [ "$batch_json" = "[]" ]; then
            break
        fi

        body_tmp="$(mktemp)" || {
            rm -f "$ips_tmp" "$result_tmp"
            return 1
        }

        http_code="$(curl -sS -m 10 -o "$body_tmp" -w '%{http_code}' \
            -H 'Content-Type: application/json' \
            -d "$batch_json" \
            'https://api.country.is/' 2>/dev/null || true)"

        body_error="$(json_utils_ucode body-error "$body_tmp" 2>/dev/null)"
        if [ "$body_error" = "rate_limit" ]; then
            log "Server country lookup is rate-limited" "warn"
            rm -f "$body_tmp"
            break
        fi

        if [ "$http_code" = "200" ]; then
            json_utils_ucode ip-country-tsv "$body_tmp" >> "$result_tmp" 2>/dev/null || true
        elif [ "$http_code" = "429" ]; then
            log "Server country lookup is rate-limited" "warn"
            rm -f "$body_tmp"
            break
        else
            log "Server country lookup failed" "warn"
        fi

        rm -f "$body_tmp"
        start="$end"
        [ "$start" -lt "$ip_count" ] && sleep 1
    done

    result_json="$(json_utils_ucode tsv-to-object "$result_tmp" 2>/dev/null)"
    status=$?
    rm -f "$ips_tmp" "$result_tmp"

    [ "$status" -eq 0 ] || return 1
    [ -n "$result_json" ] || result_json="{}"
    printf '%s\n' "$result_json"
}

detect_server_countries_for_tags() {
    local servers_json="$1"
    local section="${2:-}"
    local cache_tmp servers_tmp cache_json cached_countries_json missing_tmp resolved_tmp ips_json ip_country_json ip_country_tmp
    local resolved_countries_json tag server ip
    local server_count missing_count resolved_count unresolved_count log_prefix

    server_count="$(printf '%s' "$servers_json" | json_utils_ucode stdin-length 2>/dev/null)"
    case "$server_count" in
    '' | *[!0-9]*) server_count=0 ;;
    esac
    if [ -z "$servers_json" ] || [ "$server_count" -eq 0 ]; then
        printf '{}\n'
        return 0
    fi

    if [ -n "$section" ]; then
        log_prefix="rule '$section'"
    else
        log_prefix="current proxy rule"
    fi

    log "Detecting server countries for $log_prefix" "info"

    cache_json="$(read_section_outbound_country_cache "$section")"
    servers_tmp="$(mktemp)" || {
        printf '{}\n'
        return 0
    }
    cache_tmp="$(mktemp)" || {
        rm -f "$servers_tmp"
        printf '{}\n'
        return 0
    }
    printf '%s' "$servers_json" > "$servers_tmp" || {
        rm -f "$servers_tmp" "$cache_tmp"
        printf '{}\n'
        return 0
    }
    printf '%s' "$cache_json" > "$cache_tmp" || {
        rm -f "$servers_tmp" "$cache_tmp"
        printf '{}\n'
        return 0
    }
    cached_countries_json="$(json_utils_ucode cached-countries-for-servers "$servers_tmp" "$cache_tmp" 2>/dev/null)"
    [ -n "$cached_countries_json" ] || cached_countries_json="{}"

    missing_tmp="$(mktemp)" || {
        rm -f "$servers_tmp" "$cache_tmp"
        printf '%s\n' "$cached_countries_json"
        return 0
    }
    resolved_tmp="$(mktemp)" || {
        rm -f "$servers_tmp" "$cache_tmp" "$missing_tmp"
        printf '%s\n' "$cached_countries_json"
        return 0
    }

    json_utils_ucode missing-servers-tsv "$servers_tmp" "$cache_tmp" > "$missing_tmp" 2>/dev/null || true

    missing_count="$(wc -l < "$missing_tmp" 2>/dev/null | tr -d ' ')"
    case "$missing_count" in
    '' | *[!0-9]*) missing_count=0 ;;
    esac
    while IFS="$(printf '\t')" read -r tag server || [ -n "$tag" ]; do
        [ -n "$tag" ] || continue
        ip="$(resolve_country_server_ip "$server")" || continue
        [ -n "$ip" ] || continue
        printf '%s\t%s\n' "$tag" "$ip" >> "$resolved_tmp"
    done < "$missing_tmp"

    resolved_count="$(wc -l < "$resolved_tmp" 2>/dev/null | tr -d ' ')"
    case "$resolved_count" in
    '' | *[!0-9]*) resolved_count=0 ;;
    esac

    if [ ! -s "$resolved_tmp" ]; then
        if [ "$missing_count" -gt 0 ]; then
            log "Server country detection for $log_prefix could not resolve some servers" "warn"
        fi
        rm -f "$servers_tmp" "$cache_tmp" "$missing_tmp" "$resolved_tmp"
        printf '%s\n' "$cached_countries_json"
        return 0
    fi

    unresolved_count=$((missing_count - resolved_count))
    [ "$unresolved_count" -le 0 ] || log "Server country detection for $log_prefix could not resolve some servers" "warn"

    ips_json="$(json_utils_ucode tsv-second-column-array "$resolved_tmp" 2>/dev/null)"
    [ -n "$ips_json" ] || ips_json="[]"
    ip_country_json="$(country_is_lookup_ips "$ips_json" 2>/dev/null || printf '{}')"
    [ -n "$ip_country_json" ] || ip_country_json="{}"
    if ! printf '%s' "$ip_country_json" | json_utils_ucode object-nonempty >/dev/null 2>&1; then
        log "Server country detection for $log_prefix returned no countries" "warn"
    fi

    ip_country_tmp="$(mktemp)" || {
        rm -f "$servers_tmp" "$cache_tmp" "$missing_tmp" "$resolved_tmp"
        printf '%s\n' "$cached_countries_json"
        return 0
    }
    printf '%s' "$ip_country_json" > "$ip_country_tmp" || {
        rm -f "$servers_tmp" "$cache_tmp" "$missing_tmp" "$resolved_tmp" "$ip_country_tmp"
        printf '%s\n' "$cached_countries_json"
        return 0
    }
    resolved_countries_json="$(json_utils_ucode resolved-countries-from-tsv "$resolved_tmp" "$ip_country_tmp" 2>/dev/null)"
    [ -n "$resolved_countries_json" ] || resolved_countries_json="{}"

    rm -f "$servers_tmp" "$cache_tmp" "$missing_tmp" "$resolved_tmp" "$ip_country_tmp"
    log "Server country detection for $log_prefix completed" "info"
    json_objects_merge "$cached_countries_json" "$resolved_countries_json" 2>/dev/null ||
        printf '%s\n' "$cached_countries_json"
}

select_urltest_included_outbounds() {
    local section="$1"
    local tags_json="$2"
    local names_json="$3"
    local countries_json="$4"
    local country_filter_enabled="$5"
    local include_names_json include_regex_json include_regex_tags_json include_countries_json filtered_json

    include_names_json="$(config_list_to_json "$section" "urltest_include_outbounds")"
    include_regex_json="$(config_list_to_json "$section" "urltest_include_regex")"
    include_regex_tags_json="$(get_urltest_regex_matching_tags_json "$tags_json" "$names_json" "$include_regex_json")"
    [ -n "$include_regex_tags_json" ] || include_regex_tags_json="[]"
    include_countries_json="[]"
    if [ "$country_filter_enabled" -eq 1 ]; then
        include_countries_json="$(config_list_to_json "$section" "urltest_include_countries" |
            json_utils_ucode normalized-country-list 2>/dev/null)"
        [ -n "$include_countries_json" ] || include_countries_json="[]"
    fi

    filtered_json="$(urltest_filter_json include "$tags_json" "$names_json" "$countries_json" \
        "$include_names_json" "$include_regex_tags_json" "$include_countries_json")"

    [ -n "$filtered_json" ] || filtered_json="[]"
    printf '%s\n' "$filtered_json"
}

exclude_urltest_outbounds() {
    local section="$1"
    local tags_json="$2"
    local names_json="$3"
    local countries_json="$4"
    local country_filter_enabled="$5"
    local exclude_names_json exclude_regex_json exclude_regex_tags_json exclude_countries_json filtered_json

    exclude_names_json="$(config_list_to_json "$section" "urltest_exclude_outbounds")"
    exclude_regex_json="$(config_list_to_json "$section" "urltest_exclude_regex")"
    exclude_regex_tags_json="$(get_urltest_regex_matching_tags_json "$tags_json" "$names_json" "$exclude_regex_json")"
    [ -n "$exclude_regex_tags_json" ] || exclude_regex_tags_json="[]"
    exclude_countries_json="[]"
    if [ "$country_filter_enabled" -eq 1 ]; then
        exclude_countries_json="$(config_list_to_json "$section" "urltest_exclude_countries" |
            json_utils_ucode normalized-country-list 2>/dev/null)"
        [ -n "$exclude_countries_json" ] || exclude_countries_json="[]"
    fi

    filtered_json="$(urltest_filter_json exclude "$tags_json" "$names_json" "$countries_json" \
        "$exclude_names_json" "$exclude_regex_tags_json" "$exclude_countries_json")"

    [ -n "$filtered_json" ] || filtered_json="[]"
    printf '%s\n' "$filtered_json"
}

filter_urltest_outbounds() {
    local section="$1"
    local tags_json="$2"
    local names_json="$3"
    local countries_json="$4"
    local country_filter_enabled="$5"
    local filter_mode filtered_json included_json filtered_count original_count

    config_get filter_mode "$section" "urltest_filter_mode" "disabled"

    case "$filter_mode" in
    disabled)
        printf '%s\n' "$tags_json"
        return 0
        ;;
    include)
        filtered_json="$(select_urltest_included_outbounds "$section" "$tags_json" "$names_json" "$countries_json" "$country_filter_enabled")"
        ;;
    exclude)
        filtered_json="$(exclude_urltest_outbounds "$section" "$tags_json" "$names_json" "$countries_json" "$country_filter_enabled")"
        ;;
    mixed)
        included_json="$(select_urltest_included_outbounds "$section" "$tags_json" "$names_json" "$countries_json" "$country_filter_enabled")"
        [ -n "$included_json" ] || included_json="[]"
        filtered_json="$(exclude_urltest_outbounds "$section" "$included_json" "$names_json" "$countries_json" "$country_filter_enabled")"
        ;;
    *)
        printf '%s\n' "$tags_json"
        return 0
        ;;
    esac

    [ -n "$filtered_json" ] || filtered_json="$tags_json"
    filtered_count="$(printf '%s' "$filtered_json" | json_utils_ucode stdin-length 2>/dev/null)"
    original_count="$(printf '%s' "$tags_json" | json_utils_ucode stdin-length 2>/dev/null)"

    if [ -z "$filtered_count" ] || [ "$filtered_count" -eq 0 ]; then
        if [ "$filter_mode" = "include" ]; then
            log "URLTest whitelist matched no outbounds for rule '$section'; URLTest will be disabled for this rule" "warn"
        elif [ "$filter_mode" = "mixed" ]; then
            log "URLTest mixed filters matched no outbounds for rule '$section'; URLTest will be disabled for this rule" "warn"
        else
            log "URLTest filters excluded all outbounds for rule '$section'; URLTest will be disabled for this rule" "warn"
        fi
        printf '[]\n'
        return 0
    fi

    if [ -n "$original_count" ] && [ "$filtered_count" -lt "$original_count" ]; then
        if [ "$filter_mode" = "include" ]; then
            log "URLTest whitelist selected $filtered_count of $original_count outbound(s) for rule '$section'" "info"
        elif [ "$filter_mode" = "mixed" ]; then
            log "URLTest mixed filters selected $filtered_count of $original_count outbound(s) for rule '$section'" "info"
        else
            log "URLTest filters excluded $((original_count - filtered_count)) outbound(s) for rule '$section'" "info"
        fi
    fi

    printf '%s\n' "$filtered_json"
}

get_urltest_check_interval_for_config() {
    local section="$1"
    local urltest_check_interval

    urltest_check_interval="$(get_urltest_check_interval_for_rule "$section")"
    if [ -n "$urltest_check_interval" ]; then
        printf '%s\n' "$urltest_check_interval"
    else
        printf '%s\n' "$SING_BOX_DISABLED_UPDATE_INTERVAL"
    fi
}

get_urltest_idle_timeout_for_config() {
    local section="$1"
    local urltest_check_interval interval_seconds default_idle_seconds

    urltest_check_interval="$(get_urltest_check_interval_for_rule "$section")"
    if [ -z "$urltest_check_interval" ]; then
        printf '%s\n' "$SING_BOX_DISABLED_UPDATE_INTERVAL"
        return 0
    fi

    interval_seconds="$(duration_to_seconds "$urltest_check_interval" 2>/dev/null)" || {
        printf '\n'
        return 0
    }
    default_idle_seconds="$(duration_to_seconds "$SING_BOX_URLTEST_DEFAULT_IDLE_TIMEOUT")"

    if [ "$interval_seconds" -gt "$default_idle_seconds" ]; then
        printf '%s\n' "$urltest_check_interval"
    else
        printf '\n'
    fi
}

proxy_group_append_outbound() {
    local tag="$1"
    local link="${2:-}"
    local display_name="${3:-$tag}"
    local server="${4:-}"

    PROXY_GROUP_OUTBOUND_TAGS_JSON="$(json_array_append_string "$PROXY_GROUP_OUTBOUND_TAGS_JSON" "$tag")" || {
        log "Failed to append outbound '$tag' for proxy rule '$PROXY_GROUP_SECTION'. Aborted." "fatal"
        exit 1
    }

    PROXY_GROUP_NAMES_JSON="$(json_object_set_string "$PROXY_GROUP_NAMES_JSON" "$tag" "$display_name")" || {
        log "Failed to record outbound name for proxy rule '$PROXY_GROUP_SECTION'. Aborted." "fatal"
        exit 1
    }

    if [ -n "$link" ]; then
        PROXY_GROUP_LINKS_JSON="$(json_object_set_string "$PROXY_GROUP_LINKS_JSON" "$tag" "$link")" || {
            log "Failed to record outbound link for proxy rule '$PROXY_GROUP_SECTION'. Aborted." "fatal"
            exit 1
        }
    fi

    if [ -n "$server" ]; then
        PROXY_GROUP_SERVERS_JSON="$(json_object_set_string "$PROXY_GROUP_SERVERS_JSON" "$tag" "$server")" || {
            log "Failed to record outbound server for proxy rule '$PROXY_GROUP_SECTION'. Aborted." "fatal"
            exit 1
        }
    fi
}

proxy_group_add_manual_link() {
    local link="$1"
    local section="$2"
    local udp_over_tcp="$3"
    local outbound_tag display_name server

    PROXY_GROUP_MANUAL_INDEX=$((PROXY_GROUP_MANUAL_INDEX + 1))
    config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$PROXY_GROUP_MANUAL_INDEX" "$link" "$udp_over_tcp")"
    outbound_tag="$(get_outbound_tag_by_section "$section-$PROXY_GROUP_MANUAL_INDEX")"
    display_name="$(subscription_url_get_fragment "$link")"
    [ -n "$display_name" ] || display_name="$outbound_tag"
    server="$(printf '%s' "$config" | json_utils_ucode outbound-server-by-tag "$outbound_tag" 2>/dev/null)"
    proxy_group_append_outbound "$outbound_tag" "$link" "$display_name" "$server"
}

proxy_group_add_subscription_source() {
    local section="$1"
    local index="$2"
    local entry="$3"
    local source_section subscription_json_path

    if ! parse_subscription_source_entry "$entry"; then
        log "Invalid subscription source for rule '$section': $SUBSCRIPTION_SOURCE_PARSE_ERROR. Aborted." "fatal"
        exit 1
    fi

    source_section="$(subscription_source_id "$section" "$index")"
    if subscription_source_is_marked_unavailable "$source_section"; then
        log "Skipping unavailable subscription source '$index' for rule '$section'" "warn"
        return 0
    fi

    if ! ensure_subscription_cache_for_source \
        "$section" "$source_section" "$SUBSCRIPTION_SOURCE_URL" "$SUBSCRIPTION_SOURCE_USER_AGENT" 0 "$index"; then
        subscription_mark_unavailable_source "$source_section"
        log "Skipping unavailable subscription source '$index' for rule '$section'" "warn"
        return 0
    fi

    subscription_json_path="$(get_subscription_json_path "$source_section")"
    if ! sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path" "$source_section" > /dev/null; then
        log "Skipping subscription source '$index' for rule '$section': no valid proxy outbounds found" "warn"
        return 0
    fi

    config="$SING_BOX_CF_LAST_CONFIG"
    if [ "$PROXY_GROUP_OUTBOUND_TAGS_JSON" = "[]" ]; then
        PROXY_GROUP_OUTBOUND_TAGS_JSON="$SUBSCRIPTION_OUTBOUND_TAGS_JSON"
    else
        PROXY_GROUP_OUTBOUND_TAGS_JSON="$(json_arrays_concat "$PROXY_GROUP_OUTBOUND_TAGS_JSON" "$SUBSCRIPTION_OUTBOUND_TAGS_JSON")" || {
            log "Failed to merge subscription outbounds for rule '$section'. Aborted." "fatal"
            exit 1
        }
    fi
    if [ "$PROXY_GROUP_LINK_REFS_JSON" = "{}" ]; then
        PROXY_GROUP_LINK_REFS_JSON="$SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON"
    else
        PROXY_GROUP_LINK_REFS_JSON="$(json_objects_merge "$PROXY_GROUP_LINK_REFS_JSON" "$SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON")" || {
            log "Failed to merge subscription outbound link refs for rule '$section'. Aborted." "fatal"
            exit 1
        }
    fi
    if [ "$PROXY_GROUP_NAMES_JSON" = "{}" ]; then
        PROXY_GROUP_NAMES_JSON="$SUBSCRIPTION_OUTBOUND_NAMES_JSON"
    else
        PROXY_GROUP_NAMES_JSON="$(json_objects_merge "$PROXY_GROUP_NAMES_JSON" "$SUBSCRIPTION_OUTBOUND_NAMES_JSON")" || {
            log "Failed to merge subscription outbound names for rule '$section'. Aborted." "fatal"
            exit 1
        }
    fi
    if [ "$PROXY_GROUP_SERVERS_JSON" = "{}" ]; then
        PROXY_GROUP_SERVERS_JSON="$SUBSCRIPTION_OUTBOUND_SERVERS_JSON"
    else
        PROXY_GROUP_SERVERS_JSON="$(json_objects_merge "$PROXY_GROUP_SERVERS_JSON" "$SUBSCRIPTION_OUTBOUND_SERVERS_JSON")" || {
            log "Failed to merge subscription outbound servers for rule '$section'. Aborted." "fatal"
            exit 1
        }
    fi
}

configure_outbound_handler() {
    local section="$1"
    local action

    rule_is_enabled "$section" || return 0
    if subscription_section_is_deferred "$section"; then
        log "Skipping outbound for deferred subscription rule '$section' until its cache is recovered" "warn"
        write_subscription_metadata_json "$section" ""
        write_subscription_outbound_link_cache "$section" "{}" "{}"
        write_outbound_metadata "$section" "{}" "{}" "{}"
        return 0
    fi

    action="$(get_rule_action "$section")"

    if [ "$action" = "zapret" ]; then
        write_subscription_metadata_json "$section" ""
        if ! is_zapret_installed; then
            return 0
        fi

        local routing_mark outbound_tag zapret_index

        zapret_index="$(get_zapret_rule_index "$section")"
        if [ "${zapret_index:-0}" -le 0 ]; then
            log "Unable to resolve Zapret index for rule '$section'. Aborted." "fatal"
            exit 1
        fi

        routing_mark="$(get_zapret_rule_mark_value "$zapret_index")"
        outbound_tag="$(get_outbound_tag_by_section "$section")"
        config=$(sing_box_cm_add_direct_outbound "$config" "$outbound_tag" "$routing_mark")
        return 0
    fi

    if [ "$action" = "byedpi" ]; then
        if ! is_byedpi_installed; then
            return 0
        fi

        local outbound_tag byedpi_index byedpi_port

        byedpi_index="$(get_byedpi_rule_index "$section")"
        if [ "${byedpi_index:-0}" -le 0 ]; then
            log "Unable to resolve ByeDPI index for rule '$section'. Aborted." "fatal"
            exit 1
        fi

        byedpi_port="$(get_byedpi_rule_port "$byedpi_index")"
        outbound_tag="$(get_outbound_tag_by_section "$section")"
        config=$(sing_box_cm_add_socks_outbound "$config" "$outbound_tag" "$BYEDPI_LISTEN_ADDRESS" "$byedpi_port" "5" "" "" "" "")
        write_subscription_metadata_json "$section" ""
        return 0
    fi

    if [ "$action" = "outbound" ]; then
        local outbound_json

        config_get outbound_json "$section" "outbound_json"
        if [ -z "$outbound_json" ]; then
            log "JSON outbound is not set for rule '$section'. Aborted." "fatal"
            exit 1
        fi

        config=$(sing_box_cf_add_json_outbound "$config" "$section" "$outbound_json")
        write_subscription_outbound_link_cache "$section" "{}" "{}"
        write_subscription_metadata_json "$section" ""
        return 0
    fi

    if [ "$action" = "proxy" ]; then
        local selector_proxy_links udp_over_tcp urltest_enabled urltest_tag selector_tag selector_outbounds selector_default \
            urltest_check_interval urltest_idle_timeout urltest_tolerance urltest_testing_url outbounds_count \
            detect_server_country urltest_outbounds urltest_outbounds_count has_subscription_sources \
            metadata_tmpfile metadata_count

        config_get selector_proxy_links "$section" "selector_proxy_links"
        config_get udp_over_tcp "$section" "enable_udp_over_tcp"
        config_get_bool urltest_enabled "$section" "urltest_enabled" 0
        config_get_bool detect_server_country "$section" "detect_server_country" 0
        rule_has_subscription_urls "$section" && has_subscription_sources=1 || has_subscription_sources=0

        PROXY_GROUP_SECTION="$section"
        PROXY_GROUP_MANUAL_INDEX=0
        PROXY_GROUP_OUTBOUND_TAGS_JSON="[]"
        PROXY_GROUP_LINKS_JSON="{}"
        PROXY_GROUP_LINK_REFS_JSON="{}"
        PROXY_GROUP_NAMES_JSON="{}"
        PROXY_GROUP_SERVERS_JSON="{}"
        PROXY_GROUP_COUNTRIES_JSON="{}"
        PROXY_GROUP_DETECT_COUNTRIES="$detect_server_country"

        config_list_foreach "$section" "selector_proxy_links" proxy_group_add_manual_link "$section" "$udp_over_tcp"

        metadata_tmpfile=""
        if [ "$has_subscription_sources" -eq 1 ]; then
            metadata_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.metadata-section.XXXXXX" 2>/dev/null || true)"
            if [ -n "$metadata_tmpfile" ]; then
                printf '[]\n' > "$metadata_tmpfile"
                SUBSCRIPTION_SECTION_METADATA_TMP="$metadata_tmpfile"
            else
                SUBSCRIPTION_SECTION_METADATA_TMP=""
            fi
            for_each_subscription_source "$section" proxy_group_add_subscription_source
            SUBSCRIPTION_SECTION_METADATA_TMP=""

            if [ -n "$metadata_tmpfile" ]; then
                metadata_count="$(json_utils_ucode length "$metadata_tmpfile" 2>/dev/null)"
                if [ -n "$metadata_count" ] && [ "$metadata_count" -gt 0 ]; then
                    write_subscription_metadata_json "$section" "$metadata_tmpfile"
                else
                    write_subscription_metadata_json "$section" ""
                fi
                rm -f "$metadata_tmpfile"
            fi
        else
            write_subscription_metadata_json "$section" ""
        fi

        if [ "$PROXY_GROUP_DETECT_COUNTRIES" -eq 1 ]; then
            PROXY_GROUP_COUNTRIES_JSON="$(detect_server_countries_for_tags "$PROXY_GROUP_SERVERS_JSON" "$section")"
            [ -n "$PROXY_GROUP_COUNTRIES_JSON" ] || PROXY_GROUP_COUNTRIES_JSON="{}"
        fi

        outbounds_count="$(printf '%s' "$PROXY_GROUP_OUTBOUND_TAGS_JSON" | json_utils_ucode stdin-length 2>/dev/null)"
        if [ -z "$outbounds_count" ] || [ "$outbounds_count" -eq 0 ]; then
            log "Proxy rule '$section' has no usable proxy outbounds configured. Aborted." "fatal"
            exit 1
        fi

        selector_tag="$(get_outbound_tag_by_section "$section")"

        if [ "$urltest_enabled" -eq 1 ]; then
            urltest_check_interval="$(get_urltest_check_interval_for_config "$section")"
            urltest_idle_timeout="$(get_urltest_idle_timeout_for_config "$section")"
            config_get urltest_tolerance "$section" "urltest_tolerance" 50
            config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204"

            urltest_outbounds="$(filter_urltest_outbounds "$section" "$PROXY_GROUP_OUTBOUND_TAGS_JSON" \
                "$PROXY_GROUP_NAMES_JSON" "$PROXY_GROUP_COUNTRIES_JSON" "$detect_server_country")"
            [ -n "$urltest_outbounds" ] || urltest_outbounds="[]"
            urltest_outbounds_count="$(printf '%s' "$urltest_outbounds" | json_utils_ucode stdin-length 2>/dev/null)"
            case "$urltest_outbounds_count" in
            '' | *[!0-9]*) urltest_outbounds_count=0 ;;
            esac

            if [ "$urltest_outbounds_count" -gt 0 ]; then
                urltest_tag="$(get_outbound_tag_by_section "$section-urltest")"
                selector_outbounds="$(json_array_append_string "$PROXY_GROUP_OUTBOUND_TAGS_JSON" "$urltest_tag")"
                if [ -z "$selector_outbounds" ]; then
                    log "Failed to build selector outbounds for proxy rule '$section'. Aborted." "fatal"
                    exit 1
                fi

                config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \
                    "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance" "$urltest_idle_timeout")"
                if list_has_word "$PODKOP_URLTEST_NEW_ENABLED_SECTIONS" "$section"; then
                    schedule_urltest_selector_switch "$selector_tag" "$urltest_tag"
                fi
                config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")"
            else
                selector_default="$(printf '%s' "$PROXY_GROUP_OUTBOUND_TAGS_JSON" | json_utils_ucode array-item 0 2>/dev/null)"
                if [ -z "$selector_default" ]; then
                    log "Unable to determine default selector outbound for rule '$section'. Aborted." "fatal"
                    exit 1
                fi

                config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$PROXY_GROUP_OUTBOUND_TAGS_JSON" "$selector_default" "true")"
            fi
        else
            selector_default="$(printf '%s' "$PROXY_GROUP_OUTBOUND_TAGS_JSON" | json_utils_ucode array-item 0 2>/dev/null)"
            if [ -z "$selector_default" ]; then
                log "Unable to determine default selector outbound for rule '$section'. Aborted." "fatal"
                exit 1
            fi

            config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$PROXY_GROUP_OUTBOUND_TAGS_JSON" "$selector_default" "true")"
        fi

        write_subscription_outbound_link_cache "$section" "$PROXY_GROUP_LINKS_JSON" "$PROXY_GROUP_LINK_REFS_JSON"
        write_outbound_metadata "$section" "$PROXY_GROUP_NAMES_JSON" "$PROXY_GROUP_COUNTRIES_JSON" "$PROXY_GROUP_SERVERS_JSON"
        return 0
    fi

    if [ "$action" = "vpn" ]; then
        local interface_name domain_resolver_enabled domain_resolver_dns_type domain_resolver_dns_server \
            domain_resolver_dns_server_address outbound_tag domain_resolver_tag dns_domain_resolver outbound_mark

        config_get interface_name "$section" "interface"
        config_get_bool domain_resolver_enabled "$section" "domain_resolver_enabled" 0
        config_get domain_resolver_dns_type "$section" "domain_resolver_dns_type"
        config_get domain_resolver_dns_server "$section" "domain_resolver_dns_server"

        if [ -z "$interface_name" ]; then
            log "VPN interface is not set for rule '$section'. Aborted." "fatal"
            exit 1
        fi

        outbound_tag="$(get_outbound_tag_by_section "$section")"

        if [ "$domain_resolver_enabled" -eq 1 ]; then
            domain_resolver_dns_server_address="$(url_get_host "$domain_resolver_dns_server")"
            if ! is_ipv4 "$domain_resolver_dns_server_address"; then
                dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG
            fi
            domain_resolver_tag="$(get_domain_resolver_tag "$section")"
            config=$(sing_box_cf_add_dns_server "$config" "$domain_resolver_dns_type" "$domain_resolver_tag" \
                "$domain_resolver_dns_server" "$dns_domain_resolver" "$outbound_tag")
        fi

        outbound_mark="$((NFT_OUTBOUND_MARK))"
        config=$(sing_box_cm_add_interface_outbound "$config" "$outbound_tag" "$interface_name" "$domain_resolver_tag" "$outbound_mark")
        write_subscription_metadata_json "$section" ""
        return 0
    fi

    case "$action" in
    block | direct)
        write_subscription_metadata_json "$section" ""
        return 0
        ;;
    "")
        log "Rule '$section' has no action. Aborted." "fatal"
        ;;
    *)
        log "Unsupported action '$action' for rule '$section'. Aborted." "fatal"
        ;;
    esac
    exit 1
}

sing_box_configure_dns() {
    log "Configure the DNS section of a sing-box JSON configuration"
    config=$(sing_box_cm_configure_dns "$config" "$SB_DNS_SERVER_TAG" "ipv4_only" true)

    log "Adding DNS Servers" "debug"
    local dns_type dns_server bootstrap_dns_server dns_domain_resolver dns_server_address
    config_get dns_type "settings" "dns_type" "doh"
    config_get dns_server "settings" "dns_server" "1.1.1.1"
    config_get bootstrap_dns_server "settings" "bootstrap_dns_server" "77.88.8.8"

    dns_server_address="$(url_get_host "$dns_server")"
    if ! is_ipv4 "$dns_server_address"; then
        dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG
    fi

    config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_BOOTSTRAP_SERVER_TAG" "$bootstrap_dns_server" 53)
    config=$(sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "$dns_domain_resolver")
    config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE")

    log "Adding DNS Rules"
    local rewrite_ttl service_domains
    config_get rewrite_ttl "settings" "dns_rewrite_ttl" "60"
    service_domains=$(comma_string_to_json_array "$FAKEIP_TEST_DOMAIN,$CHECK_PROXY_IP_DOMAIN")

    config=$(sing_box_cm_add_dns_reject_rule "$config" "query_type" "HTTPS")
    config=$(sing_box_cm_add_dns_reject_rule "$config" "query_type" "AAAA")
    config=$(sing_box_cm_add_dns_reject_rule "$config" "domain_suffix" '"use-application-dns.net"')
    config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_SERVICE_FAKEIP_DNS_RULE_TAG")
    config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_SERVICE_FAKEIP_DNS_RULE_TAG" "rewrite_ttl" "$rewrite_ttl")
    config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_SERVICE_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
}

sing_box_configure_route() {
    log "Configure the route section of a sing-box JSON configuration"

    local output_network_interface outbound_mark
    outbound_mark="$((NFT_OUTBOUND_MARK))"
    config_get output_network_interface "settings" "output_network_interface"
    if [ -z "$output_network_interface" ]; then
        if mwan3_is_active; then
            log "mwan3 is active; disabling sing-box auto_detect_interface so mwan3 can control egress routing" "warn"
            config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" false "$SB_DNS_SERVER_TAG" "" "$outbound_mark")
        else
            config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" true "$SB_DNS_SERVER_TAG" "" "$outbound_mark")
        fi
    else
        if mwan3_is_active; then
            log "mwan3 is active and Output Network Interface is set to '$output_network_interface'; sing-box egress is pinned to this interface" "warn"
        fi
        config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" false "$SB_DNS_SERVER_TAG" \
            "$output_network_interface" "$outbound_mark")
    fi

    local sniff_inbounds
    sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG")
    config=$(sing_box_cm_sniff_route_rule "$config" "inbound" "$sniff_inbounds")
    config_foreach add_server_sniff_route_rule "server"

    config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "port" "53")
    config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "protocol" "dns")

    config=$(sing_box_cf_add_single_key_reject_rule "$config" "" "ip_version" 6)

    local disable_quic
    config_get_bool disable_quic "settings" "disable_quic" 0
    if [ "$disable_quic" -eq 1 ]; then
        config=$(sing_box_cf_add_single_key_reject_rule "$config" "$SB_TPROXY_INBOUND_TAG" "protocol" "quic")
    fi

    local first_outbound_section
    first_outbound_section="$(get_first_outbound_section)"
    if [ -n "$first_outbound_section" ]; then
        first_outbound_tag="$(get_outbound_tag_by_section "$first_outbound_section")"
        config=$(sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$first_outbound_tag")
    fi
    config=$(sing_box_cf_override_domain_port "$config" "$FAKEIP_TEST_DOMAIN" 8443)

    local routing_excluded_ips
    config_get routing_excluded_ips "settings" "routing_excluded_ips"
    if [ -n "$routing_excluded_ips" ]; then
        rule_tag="$(gen_id)"
        config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$SB_DIRECT_OUTBOUND_TAG")
        config_list_foreach "settings" "routing_excluded_ips" exclude_source_ip_from_routing_handler "$rule_tag"
    fi

    config_foreach include_source_ips_in_routing_handler "section"
    config_foreach configure_route_rule_handler "section"
    config_foreach configure_server_route "server"
}

add_server_sniff_route_rule() {
    local section="$1"
    local inbound_tag

    server_is_enabled "$section" || return 0

    inbound_tag="$(get_server_inbound_tag_by_section "$section")"
    config=$(sing_box_cm_sniff_route_rule "$config" "inbound" "\"$inbound_tag\"")
}

configure_server_route() {
    local section="$1"
    local routing_mode routing_section inbound_tag action route_rule_tag

    server_is_enabled "$section" || return 0

    inbound_tag="$(get_server_inbound_tag_by_section "$section")"
    config_get routing_mode "$section" "routing_mode" "rules"

    case "$routing_mode" in
    rules)
        config=$(
            sing_box_cm_clone_route_rules_for_inbound \
                "$config" \
                "$SB_TPROXY_INBOUND_TAG" \
                "$inbound_tag" \
                "$CHECK_PROXY_IP_DOMAIN"
        )
        ;;
    direct)
        route_rule_tag="$(gen_id)"
        config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$inbound_tag" "$SB_DIRECT_OUTBOUND_TAG")
        ;;
    section)
        config_get routing_section "$section" "routing_section"
        if [ -z "$routing_section" ]; then
            log "Server '$section' routing mode is section, but routing_section is empty. Aborted." "fatal"
            exit 1
        fi
        action="$(get_rule_action "$routing_section")"
        if [ -z "$action" ]; then
            log "Server '$section' references missing routing section '$routing_section'. Aborted." "fatal"
            exit 1
        fi
        if ! rule_is_enabled "$routing_section"; then
            log "Server '$section' references disabled routing section '$routing_section'. Aborted." "fatal"
            exit 1
        fi
        route_rule_tag="$(gen_id)"
        create_route_rule_for_action "$routing_section" "$action" "$route_rule_tag" "$inbound_tag"
        ;;
    *)
        log "Server '$section' has unsupported routing mode '$routing_mode'. Aborted." "fatal"
        exit 1
        ;;
    esac
}

create_route_rule_for_action() {
    local section="$1"
    local action="$2"
    local rule_tag="$3"
    local inbound_tag="${4:-$SB_TPROXY_INBOUND_TAG}"

    case "$action" in
    proxy | outbound | vpn | byedpi)
        local outbound_tag
        outbound_tag="$(get_outbound_tag_by_section "$section")"
        config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$inbound_tag" "$outbound_tag")
        ;;
    zapret)
        local zapret_outbound_tag
        zapret_outbound_tag="$(get_outbound_tag_by_section "$section")"
        config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$inbound_tag" "$zapret_outbound_tag")
        ;;
    direct)
        config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$inbound_tag" "$SB_DIRECT_OUTBOUND_TAG")
        ;;
    block)
        config=$(sing_box_cm_add_reject_route_rule "$config" "$rule_tag" "$inbound_tag")
        ;;
    *)
        log "Unsupported action '$action' for rule '$section'. Aborted." "fatal"
        exit 1
        ;;
    esac
}

dns_route_rule_exists() {
    local tag="$1"

    printf '%s' "$config" | json_utils_ucode dns-route-rule-exists "$SERVICE_TAG" "$tag" >/dev/null 2>&1
}

ensure_fakeip_dns_route_rule() {
    local tag="$1"
    local rewrite_ttl

    dns_route_rule_exists "$tag" && return 0

    config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$tag")
    config_get rewrite_ttl "settings" "dns_rewrite_ttl" "60"
    config=$(sing_box_cm_patch_dns_route_rule "$config" "$tag" "rewrite_ttl" "$rewrite_ttl")
}

route_rule_has_resolve_matchers() {
    local tag="$1"

    printf '%s' "$config" | json_utils_ucode route-rule-has-resolve-matchers "$SERVICE_TAG" "$tag" >/dev/null 2>&1
}

configure_rule_set_reference_handler() {
    local reference="$1"
    local route_rule_tag="$2"

    resolve_ruleset_reference "$reference"
    if [ -n "$ENSURED_RULESET_TAG" ]; then
        config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ENSURED_RULESET_TAG")
        if [ "$ENSURED_RULESET_KIND" = "domains" ]; then
            ensure_fakeip_dns_route_rule "$SB_FAKEIP_RULESET_DNS_RULE_TAG"
            config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_RULESET_DNS_RULE_TAG" "rule_set" "$ENSURED_RULESET_TAG")
        else
            log "Skip DNS FakeIP rule patch for '$reference' ruleset kind '$ENSURED_RULESET_KIND'" "debug"
        fi
    fi
}

get_domain_ip_list_ruleset_tag() {
    local section="$1"
    local kind="$2"

    get_ruleset_tag "$section" "lists" "$kind"
}

get_domain_ip_list_ruleset_path() {
    local section="$1"
    local kind="$2"

    printf '%s/%s.json\n' "$TMP_RULESET_FOLDER" "$(get_domain_ip_list_ruleset_tag "$section" "$kind")"
}

reset_domain_ip_list_rulesets() {
    local section="$1"
    local domain_ruleset_filepath subnet_ruleset_filepath

    domain_ruleset_filepath="$(get_domain_ip_list_ruleset_path "$section" "domains")"
    subnet_ruleset_filepath="$(get_domain_ip_list_ruleset_path "$section" "subnets")"

    rm -f "$domain_ruleset_filepath" "$subnet_ruleset_filepath"
    create_source_rule_set "$domain_ruleset_filepath" >/dev/null 2>&1
    create_source_rule_set "$subnet_ruleset_filepath" >/dev/null 2>&1
}

ensure_domain_ip_list_rulesets() {
    local section="$1"
    local route_rule_tag="$2"
    local domain_ruleset_tag subnet_ruleset_tag domain_ruleset_filepath subnet_ruleset_filepath

    domain_ruleset_tag="$(get_domain_ip_list_ruleset_tag "$section" "domains")"
    subnet_ruleset_tag="$(get_domain_ip_list_ruleset_tag "$section" "subnets")"
    domain_ruleset_filepath="$(get_domain_ip_list_ruleset_path "$section" "domains")"
    subnet_ruleset_filepath="$(get_domain_ip_list_ruleset_path "$section" "subnets")"

    config=$(sing_box_cm_add_local_ruleset "$config" "$domain_ruleset_tag" "source" "$domain_ruleset_filepath")
    config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$domain_ruleset_tag")
    ensure_fakeip_dns_route_rule "$SB_FAKEIP_RULESET_DNS_RULE_TAG"
    config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_RULESET_DNS_RULE_TAG" "rule_set" "$domain_ruleset_tag")

    config=$(sing_box_cm_add_local_ruleset "$config" "$subnet_ruleset_tag" "source" "$subnet_ruleset_filepath")
    config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$subnet_ruleset_tag")
}

import_domain_ip_list_file_into_rulesets() {
    local filepath="$1"
    local section="$2"
    local domains_tmpfile subnets_tmpfile domain_ruleset_filepath subnet_ruleset_filepath

    [ -f "$filepath" ] || return 0

    domains_tmpfile="$(mktemp)"
    subnets_tmpfile="$(mktemp)"
    domain_ruleset_filepath="$(get_domain_ip_list_ruleset_path "$section" "domains")"
    subnet_ruleset_filepath="$(get_domain_ip_list_ruleset_path "$section" "subnets")"

    split_domain_or_subnet_file "$filepath" "$domains_tmpfile" "$subnets_tmpfile"
    import_plain_domain_list_to_local_source_ruleset_chunked "$domains_tmpfile" "$domain_ruleset_filepath"
    import_plain_subnet_list_to_local_source_ruleset_chunked "$subnets_tmpfile" "$subnet_ruleset_filepath"
    add_plain_subnet_file_to_nft_for_section "$section" "$subnets_tmpfile"

    rm -f "$domains_tmpfile" "$subnets_tmpfile"
}

import_domain_ip_list_reference_into_rulesets() {
    local reference="$1"
    local section="$2"
    local tmpfile http_proxy_address

    case "$reference" in
    http://* | https://*)
        tmpfile="$(mktemp)"
        http_proxy_address="$(get_service_proxy_address)"
        download_to_file "$reference" "$tmpfile" "$http_proxy_address"
        if [ $? -eq 0 ] && [ -s "$tmpfile" ]; then
            convert_crlf_to_lf "$tmpfile"
            import_domain_ip_list_file_into_rulesets "$tmpfile" "$section"
        else
            log "Download $reference list failed" "error"
        fi
        rm -f "$tmpfile"
        ;;
    *)
        import_domain_ip_list_file_into_rulesets "$reference" "$section"
        ;;
    esac
}

configure_domain_ip_lists() {
    local section="$1"
    local route_rule_tag="$2"

    reset_domain_ip_list_rulesets "$section"
    ensure_domain_ip_list_rulesets "$section" "$route_rule_tag"

    config_list_foreach "$section" "domain_ip_lists" configure_domain_ip_lists_reference_handler "$section"
}

configure_domain_ip_lists_reference_handler() {
    local reference="$1"
    local section="$2"

    case "$reference" in
    http://* | https://*) return 0 ;;
    esac

    import_domain_ip_list_reference_into_rulesets "$reference" "$section"
}

add_plain_subnet_file_to_nft_for_section() {
    local section="$1"
    local filepath="$2"
    local ports

    [ -s "$filepath" ] || return 0

    ports="$(get_rule_ports_commas_string "$section")"
    if [ -n "$ports" ]; then
        nft_add_ip_port_set_elements_from_ip_file_chunked "$filepath" "$NFT_TABLE_NAME" "$NFT_IP_PORT_SET_NAME" "$ports"
    else
        nft_add_set_elements_from_file_chunked "$filepath" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
    fi
}

extract_json_ruleset_nft_files_for_section() {
    local json_file="$1"
    local unscoped_output_file="$2"
    local scoped_output_file="$3"
    local section="$4"
    local port_values_json port_ranges_json

    port_values_json="[]"
    port_ranges_json="[]"
    if [ -n "$section" ]; then
        port_values_json="$(get_rule_port_values_json_array "$section")"
        port_ranges_json="$(get_rule_port_ranges_json_array "$section")"
    fi

    extract_ip_cidr_nft_elements_from_json_ruleset_to_files \
        "$json_file" "$unscoped_output_file" "$scoped_output_file" "$port_values_json" "$port_ranges_json"
}

add_extracted_ruleset_subnets_to_nft_for_section() {
    local section="$1"
    local unscoped_file="$2"
    local scoped_file="$3"
    local label="$4"
    local has_entries

    has_entries=0

    if [ -s "$unscoped_file" ]; then
        nft_add_set_elements_from_file_chunked "$unscoped_file" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" || return 1
        has_entries=1
    fi

    if [ -s "$scoped_file" ]; then
        nft_add_ip_port_set_elements_from_file_chunked "$scoped_file" "$NFT_TABLE_NAME" "$NFT_IP_PORT_SET_NAME" || return 1
        has_entries=1
    fi

    if [ "$has_entries" -eq 0 ]; then
        log "$label has no ip_cidr entries for nftables" "warn"
    fi

    return 0
}

rebuild_domain_ip_lists_from_rule() {
    local section="$1"
    local references

    rule_is_enabled "$section" || return 0
    config_get references "$section" "domain_ip_lists"
    [ -n "$references" ] || return 0

    reset_domain_ip_list_rulesets "$section"
    config_list_foreach "$section" "domain_ip_lists" import_domain_ip_list_reference_into_rulesets "$section"
}

rule_has_primary_matchers() {
    local section="$1"
    local domain domain_suffix domain_keyword domain_regex ip_cidr source_ip_cidr ports community_lists rule_set rule_set_with_subnets domain_ip_lists

    domain="$(get_rule_condition_commas_string "$section" "domain" "domains")"
    domain_suffix="$(get_rule_condition_commas_string "$section" "domain_suffix" "domains")"
    domain_keyword="$(get_rule_condition_commas_string "$section" "domain_keyword" "generic")"
    domain_regex="$(get_rule_condition_commas_string "$section" "domain_regex" "generic")"
    ip_cidr="$(get_rule_condition_commas_string "$section" "ip_cidr" "subnets")"
    source_ip_cidr="$(get_rule_condition_commas_string "$section" "source_ip_cidr" "subnets")"
    ports="$(get_rule_ports_commas_string "$section")"
    config_get community_lists "$section" "community_lists"
    config_get rule_set "$section" "rule_set"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_get domain_ip_lists "$section" "domain_ip_lists"

    if [ -n "$domain" ] ||
        [ -n "$domain_suffix" ] ||
        [ -n "$domain_keyword" ] ||
        [ -n "$domain_regex" ] ||
        [ -n "$ip_cidr" ] ||
        [ -n "$source_ip_cidr" ] ||
        [ -n "$ports" ] ||
        [ -n "$community_lists" ] ||
        [ -n "$rule_set" ] ||
        [ -n "$rule_set_with_subnets" ] ||
        [ -n "$domain_ip_lists" ]; then
        return 0
    fi

    return 1
}

configure_route_rule_handler() {
    local section="$1"
    local action route_rule_tag ip_values_json ip_values port_values_json port_ranges_json

    rule_is_enabled "$section" || return 0
    if subscription_section_is_deferred "$section"; then
        log "Skipping route rule '$section' until its subscription cache is recovered" "warn"
        return 0
    fi

    action="$(get_rule_action "$section")"
    [ -n "$action" ] || return 0

    if [ "$action" = "zapret" ] && ! is_zapret_installed; then
        return 0
    fi

    if [ "$action" = "byedpi" ] && ! is_byedpi_installed; then
        return 0
    fi

    log "Configuring route rule '$section' with action '$action'"

    if ! rule_has_primary_matchers "$section"; then
        log "Rule '$section' has no matchers and action '$action', skipping route creation" "warn"
        return 0
    fi

    route_rule_tag="$(gen_id)"
    create_route_rule_for_action "$section" "$action" "$route_rule_tag"

    local domain_json domain_suffix_json domain_keyword_json domain_regex_json
    domain_json="$(get_rule_condition_json_array "$section" "domain" "domains")"
    [ "$domain_json" != "[]" ] && config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "domain" "$domain_json") && \
        ensure_fakeip_dns_route_rule "$SB_FAKEIP_DNS_RULE_TAG" && \
        config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain" "$domain_json")

    domain_suffix_json="$(get_rule_condition_json_array "$section" "domain_suffix" "domains")"
    [ "$domain_suffix_json" != "[]" ] && config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "domain_suffix" "$domain_suffix_json") && \
        ensure_fakeip_dns_route_rule "$SB_FAKEIP_DNS_RULE_TAG" && \
        config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain_suffix" "$domain_suffix_json")

    domain_keyword_json="$(get_rule_condition_json_array "$section" "domain_keyword" "generic")"
    [ "$domain_keyword_json" != "[]" ] && config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "domain_keyword" "$domain_keyword_json") && \
        ensure_fakeip_dns_route_rule "$SB_FAKEIP_DNS_RULE_TAG" && \
        config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain_keyword" "$domain_keyword_json")

    domain_regex_json="$(get_rule_condition_json_array "$section" "domain_regex" "generic")"
    [ "$domain_regex_json" != "[]" ] && config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "domain_regex" "$domain_regex_json") && \
        ensure_fakeip_dns_route_rule "$SB_FAKEIP_DNS_RULE_TAG" && \
        config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain_regex" "$domain_regex_json")

    ip_values_json="$(get_rule_condition_json_array "$section" "ip_cidr" "subnets")"
    if [ "$ip_values_json" != "[]" ]; then
        config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "ip_cidr" "$ip_values_json")
        if [ "$PODKOP_NFT_POPULATE_ENABLED" = "1" ]; then
            ip_values="$(get_rule_condition_commas_string "$section" "ip_cidr" "subnets")"
            add_section_ip_cidr_to_nft_sets "$section" "$ip_values"
        fi
    fi

    port_values_json="$(get_rule_port_values_json_array "$section")"
    [ "$port_values_json" != "[]" ] && config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "port" "$port_values_json")

    port_ranges_json="$(get_rule_port_ranges_json_array "$section")"
    [ "$port_ranges_json" != "[]" ] && config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "port_range" "$port_ranges_json")

    if [ "$PODKOP_NFT_POPULATE_ENABLED" = "1" ]; then
        add_section_ports_to_nft_set_if_needed "$section"
    fi

    local source_ip_values_json
    source_ip_values_json="$(get_rule_condition_json_array "$section" "source_ip_cidr" "subnets")"
    if [ "$source_ip_values_json" != "[]" ]; then
        config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "source_ip_cidr" "$source_ip_values_json")
    fi

    local domain_ip_lists rule_set_with_subnets
    config_get domain_ip_lists "$section" "domain_ip_lists"
    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    config_list_foreach "$section" "community_lists" configure_community_list_handler "$section" "$route_rule_tag"
    config_list_foreach "$section" "rule_set" configure_rule_set_reference_handler "$route_rule_tag"
    config_list_foreach "$section" "rule_set_with_subnets" configure_rule_set_reference_handler "$route_rule_tag"
    if [ -n "$domain_ip_lists" ]; then
        configure_domain_ip_lists "$section" "$route_rule_tag"
    fi

    if [ "$action" = "proxy" ] || [ "$action" = "outbound" ] || [ "$action" = "vpn" ] || [ "$action" = "byedpi" ]; then
        local resolve_real_ip_for_routing
        if [ "$action" = "byedpi" ]; then
            resolve_real_ip_for_routing=1
        else
            config_get_bool resolve_real_ip_for_routing "$section" "resolve_real_ip_for_routing" 0
        fi
        if [ "$resolve_real_ip_for_routing" -eq 1 ]; then
            if route_rule_has_resolve_matchers "$route_rule_tag"; then
                config=$(sing_box_cm_add_resolve_rule "$config" "$route_rule_tag" "$(gen_id)" "$SB_DNS_SERVER_TAG")
                log "Added resolve rule for '$section' rule" "debug"
            else
                log "Resolve real IP is enabled for '$section', but no domain or rule-set matchers found" "warn"
            fi
        fi
    fi
}

include_source_ips_in_routing_handler() {
    local section="$1"

    rule_is_enabled "$section" || return 0
    subscription_section_is_deferred "$section" && return 0

    local action fully_routed_ips rule_tag
    action="$(get_rule_action "$section")"
    [ -n "$action" ] || return 0

    if [ "$action" = "zapret" ] && ! is_zapret_installed; then
        return 0
    fi

    if [ "$action" = "byedpi" ] && ! is_byedpi_installed; then
        return 0
    fi

    config_get fully_routed_ips "$section" "fully_routed_ips"
    if [ -n "$fully_routed_ips" ]; then
        rule_tag="$(gen_id)"
        create_route_rule_for_action "$section" "$action" "$rule_tag"
        config_list_foreach "$section" "fully_routed_ips" include_source_ip_in_routing_handler "$rule_tag"
    fi
}

include_source_ip_in_routing_handler() {
    local source_ip="$1"
    local rule_tag="$2"
    nft_list_all_traffic_from_ip "$source_ip"
    config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip")
}

exclude_source_ip_from_routing_handler() {
    local source_ip="$1"
    local rule_tag="$2"

    config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip")
}

configure_community_list_handler() {
    local tag="$1"
    local section="$2"
    local route_rule_tag="$3"

    local ruleset_tag format url update_interval detour
    ruleset_tag="$(get_ruleset_tag "$section" "$tag" "community")"
    format="binary"
    url="$(get_community_ruleset_url "$tag")"
    detour="$(get_download_detour_tag)"
    update_interval="$(get_remote_ruleset_update_interval)"

    config=$(sing_box_cm_add_remote_ruleset "$config" "$ruleset_tag" "$format" "$url" "$detour" "$update_interval")
    config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
    ensure_fakeip_dns_route_rule "$SB_FAKEIP_RULESET_DNS_RULE_TAG"
    config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_RULESET_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
}

sing_box_configure_experimental() {
    log "Configure the experimental section of a sing-box JSON configuration"

    log "Configuring cache database"
    local cache_file
    config_get cache_file "settings" "cache_path" "/tmp/sing-box/cache.db"
    config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true)

    log "Configuring Clash API"
    local enable_yacd enable_yacd_wan_access clash_api_controller_address
    config_get_bool enable_yacd "settings" "enable_yacd" 0
    config_get_bool enable_yacd_wan_access "settings" "enable_yacd_wan_access" 0

    if [ "$enable_yacd" -eq 1 ] && [ "$enable_yacd_wan_access" -eq 1 ]; then
        clash_api_controller_address="0.0.0.0"
    else
        clash_api_controller_address="$(get_service_listen_address)"
        if [ -z "$clash_api_controller_address" ]; then
            log "Could not determine the listening IP address for the Clash API controller. It will run only on localhost." "warn"
            clash_api_controller_address="127.0.0.1"
        fi
    fi

    if [ "$enable_yacd" -eq 1 ]; then
        log "YACD is enabled, enabling Clash API with downloadable YACD" "debug"
        local yacd_secret_key external_controller_ui
        config_get yacd_secret_key "settings" "yacd_secret_key"
        external_controller_ui="ui"

        config=$(
            sing_box_cm_configure_clash_api \
                "$config" \
                "$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT" \
                "$external_controller_ui" \
                "$yacd_secret_key"
        )
    else
        log "YACD is disabled, enabling Clash API in online mode" "debug"
        config=$(
            sing_box_cm_configure_clash_api "$config" "$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT"
        )
    fi
}

sing_box_additional_inbounds() {
    log "Configure the additional inbounds of a sing-box JSON configuration"

    local download_lists_via_proxy
    config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
    if [ "$download_lists_via_proxy" -eq 1 ]; then
        local download_lists_via_proxy_section section_outbound_tag
        config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
        section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")"
        config=$(
            sing_box_cf_add_mixed_inbound_and_route_rule \
                "$config" \
                "$SB_SERVICE_MIXED_INBOUND_TAG" \
                "$SB_SERVICE_MIXED_INBOUND_ADDRESS" \
                "$SB_SERVICE_MIXED_INBOUND_PORT" \
                "$section_outbound_tag"
        )
    fi

    config_foreach configure_section_mixed_proxy "section"
}

configure_section_mixed_proxy() {
    local section="$1"
    local action

    rule_is_enabled "$section" || return 0
    subscription_section_is_deferred "$section" && return 0

    action="$(get_rule_action "$section")"
    [ "$action" = "proxy" ] || [ "$action" = "outbound" ] || [ "$action" = "vpn" ] || [ "$action" = "byedpi" ] || [ "$action" = "zapret" ] || return 0
    if [ "$action" = "byedpi" ] && ! is_byedpi_installed; then
        return 0
    fi
    if [ "$action" = "zapret" ] && ! is_zapret_installed; then
        return 0
    fi

    local mixed_inbound_enabled mixed_proxy_port mixed_inbound_tag mixed_outbound_tag mixed_proxy_address \
        mixed_proxy_auth_enabled mixed_proxy_username mixed_proxy_password
    config_get_bool mixed_inbound_enabled "$section" "mixed_proxy_enabled" 0
    mixed_proxy_address="$(get_service_listen_address)"
    if [ -z "$mixed_proxy_address" ]; then
        log "Could not determine the listening IP address for the Mixed Proxy. The proxy will not be created." "warn"
        return 1
    fi
    config_get mixed_proxy_port "$section" "mixed_proxy_port"
    if [ "$mixed_inbound_enabled" -eq 1 ]; then
        config_get_bool mixed_proxy_auth_enabled "$section" "mixed_proxy_auth_enabled" 0
        if [ "$mixed_proxy_auth_enabled" -eq 1 ]; then
            config_get mixed_proxy_username "$section" "mixed_proxy_username"
            config_get mixed_proxy_password "$section" "mixed_proxy_password"
            if [ -z "$mixed_proxy_username" ] || [ -z "$mixed_proxy_password" ]; then
                log "Mixed Proxy authentication for '$section' is enabled, but username or password is empty. The proxy will not be created." "warn"
                return 1
            fi
        fi
        mixed_inbound_tag="$(get_inbound_tag_by_section "$section-mixed")"
        mixed_outbound_tag="$(get_outbound_tag_by_section "$section")"
        config=$(
            sing_box_cf_add_mixed_inbound_and_route_rule \
                "$config" \
                "$mixed_inbound_tag" \
                "$mixed_proxy_address" \
                "$mixed_proxy_port" \
                "$mixed_outbound_tag" \
                "$mixed_proxy_username" \
                "$mixed_proxy_password"
        )
    fi
}

sing_box_save_config() {
    local sing_box_config_path temp_file_path current_config_hash temp_config_hash
    config_get sing_box_config_path "settings" "config_path"
    temp_file_path="$(mktemp)"

    log "Save sing-box temporary config to $temp_file_path" "debug"
    sing_box_cm_save_config_to_file "$config" "$temp_file_path"

    sing_box_config_check "$temp_file_path"

    current_config_hash=$(md5sum "$sing_box_config_path" 2> /dev/null | awk '{print $1}')
    temp_config_hash=$(md5sum "$temp_file_path" | awk '{print $1}')
    log "Current sing-box config hash: $current_config_hash" "debug"
    log "Temporary sing-box config hash: $temp_config_hash" "debug"
    if [ "$current_config_hash" != "$temp_config_hash" ]; then
        log "sing-box configuration has changed and will be updated"
        mv "$temp_file_path" "$sing_box_config_path"
    else
        log "sing-box configuration is unchanged"
        rm "$temp_file_path"
    fi
}

sing_box_config_check() {
    local config_path="$1"

    if ! sing-box -c "$config_path" check > /dev/null 2>&1; then
        log "Sing-box configuration $config_path is invalid. Aborted." "fatal"
        exit 1
    fi
}

import_builtin_subnets_from_rule() {
    local section="$1"

    rule_is_enabled "$section" || return 0
    config_list_foreach "$section" "community_lists" import_builtin_subnets_reference_handler "$section"
}

import_builtin_subnets_reference_handler() {
    local reference="$1"
    local section="$2"

    if printf '%s\n' "$COMMUNITY_SERVICES" | tr ' ' '\n' | grep -Fxq "$reference"; then
        import_community_service_subnet_list_handler "$reference" "$section"
    fi
}

import_rule_sets_with_subnets_from_rule() {
    local section="$1"
    local rule_set_with_subnets

    rule_is_enabled "$section" || return 0

    config_get rule_set_with_subnets "$section" "rule_set_with_subnets"
    if [ -n "$rule_set_with_subnets" ]; then
        log "Importing subnets from rule sets with subnets for '$section' section"
        config_list_foreach "$section" "rule_set_with_subnets" import_rule_set_with_subnets_reference_handler "$section"
    fi
}

import_rule_set_with_subnets_reference_handler() {
    local reference="$1"
    local section="$2"
    local extension format

    log "Importing subnets from rule set reference for '$section': $reference"

    case "$reference" in
    /*.srs)
        import_custom_ruleset_subnets_from_local "$reference" "binary" "$section" || \
            log "Failed to import subnets from local SRS rule set: $reference" "error"
        ;;
    /*.json)
        import_custom_ruleset_subnets_from_local "$reference" "source" "$section" || \
            log "Failed to import subnets from local JSON rule set: $reference" "error"
        ;;
    http://* | https://*)
        extension="$(url_get_file_extension "$reference")"
        case "$extension" in
        srs)
            import_custom_ruleset_subnets_from_remote "$reference" "binary" "$section" || \
                log "Failed to import subnets from remote SRS rule set: $reference" "error"
            ;;
        json)
            import_custom_ruleset_subnets_from_remote "$reference" "source" "$section" || \
                log "Failed to import subnets from remote JSON rule set: $reference" "error"
            ;;
        *)
            format="$(get_inline_remote_ruleset_format "$reference")"
            import_custom_ruleset_subnets_from_remote "$reference" "$format" "$section" || \
                log "Failed to import subnets from remote rule set: $reference" "error"
            ;;
        esac
        ;;
    *)
        log "Unsupported rule set reference for subnet import: $reference" "error"
        ;;
    esac

    return 0
}

import_custom_ruleset_subnets_from_local() {
    local path="$1"
    local format="$2"
    local section="$3"
    local json_tmpfile unscoped_tmpfile scoped_tmpfile

    if [ ! -f "$path" ]; then
        log "Local rule set file $path not found" "error"
        return 1
    fi

    json_tmpfile="$(mktemp)"
    unscoped_tmpfile="$(mktemp)"
    scoped_tmpfile="$(mktemp)"

    if [ "$format" = "binary" ]; then
        if ! decompile_binary_ruleset "$path" "$json_tmpfile"; then
            rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
            return 1
        fi
    else
        if ! cp "$path" "$json_tmpfile"; then
            log "Failed to copy source rule set file $path" "error"
            rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
            return 1
        fi
    fi

    if ! extract_json_ruleset_nft_files_for_section "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile" "$section"; then
        log "Failed to extract ip_cidr entries from rule set $path" "error"
        rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if ! add_extracted_ruleset_subnets_to_nft_for_section "$section" "$unscoped_tmpfile" "$scoped_tmpfile" "Rule set $path"; then
        log "Failed to add subnets from rule set $path to nftables" "error"
        rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
}

import_custom_ruleset_subnets_from_remote() {
    local url="$1"
    local format="$2"
    local section="$3"
    local remote_tmpfile json_tmpfile unscoped_tmpfile scoped_tmpfile http_proxy_address

    remote_tmpfile="$(mktemp)"
    json_tmpfile="$(mktemp)"
    unscoped_tmpfile="$(mktemp)"
    scoped_tmpfile="$(mktemp)"
    http_proxy_address="$(get_service_proxy_address)"

    download_to_file "$url" "$remote_tmpfile" "$http_proxy_address"

    if [ $? -ne 0 ] || [ ! -s "$remote_tmpfile" ]; then
        log "Download $url rule set failed" "error"
        rm -f "$remote_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if [ "$format" = "binary" ]; then
        if ! decompile_binary_ruleset "$remote_tmpfile" "$json_tmpfile"; then
            rm -f "$remote_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
            return 1
        fi
    else
        if ! cp "$remote_tmpfile" "$json_tmpfile"; then
            log "Failed to copy downloaded source rule set $url" "error"
            rm -f "$remote_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
            return 1
        fi
    fi

    if ! extract_json_ruleset_nft_files_for_section "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile" "$section"; then
        log "Failed to extract ip_cidr entries from rule set $url" "error"
        rm -f "$remote_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if ! add_extracted_ruleset_subnets_to_nft_for_section "$section" "$unscoped_tmpfile" "$scoped_tmpfile" "Rule set $url"; then
        log "Failed to add subnets from rule set $url to nftables" "error"
        rm -f "$remote_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    rm -f "$remote_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
}

ensure_discord_udp_fakeip_rule() {
    nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME"

    if nft list chain inet "$NFT_TABLE_NAME" mangle 2> /dev/null |
        grep -Fq "iifname @$NFT_INTERFACE_SET_NAME ip daddr @$NFT_DISCORD_SET_NAME udp dport { 19000-20000, 50000-65535 } meta mark set $NFT_FAKEIP_MARK"; then
        return 0
    fi

    nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr \
        "@$NFT_DISCORD_SET_NAME" udp dport '{ 19000-20000, 50000-65535 }' meta mark set "$NFT_FAKEIP_MARK" counter
}

import_community_service_subnet_list_handler() {
    local service="$1"
    local section="$2"
    local ports

    ports="$(get_rule_ports_commas_string "$section")"

    case "$service" in
    "twitter")
        URL=$SUBNETS_TWITTER
        ;;
    "meta")
        URL=$SUBNETS_META
        ;;
    "telegram")
        URL=$SUBNETS_TELERAM
        ;;
    "cloudflare")
        URL=$SUBNETS_CLOUDFLARE
        ;;
    "hetzner")
        URL=$SUBNETS_HETZNER
        ;;
    "ovh")
        URL=$SUBNETS_OVH
        ;;
    "digitalocean")
        URL=$SUBNETS_DIGITALOCEAN
        ;;
    "cloudfront")
        URL=$SUBNETS_CLOUDFRONT
        ;;
    "discord")
        URL=$SUBNETS_DISCORD
        [ -n "$ports" ] || ensure_discord_udp_fakeip_rule
        ;;
    "roblox")
        URL=$SUBNETS_ROBLOX
        ;;
    *) return 0 ;;
    esac

    local tmpfile http_proxy_address
    tmpfile=$(mktemp)
    http_proxy_address="$(get_service_proxy_address)"

    download_to_file "$URL" "$tmpfile" "$http_proxy_address"

    if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
        log "Download $service list failed" "error"
        return 1
    fi

    if [ "$service" = "discord" ] && [ -z "$ports" ]; then
        nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME"
    else
        add_plain_subnet_file_to_nft_for_section "$section" "$tmpfile"
    fi

    rm -f "$tmpfile"
}

import_domains_from_remote_domain_lists() {
    local section="$1"
    local remote_domain_lists

    rule_is_enabled "$section" || return 0

    config_get remote_domain_lists "$section" "remote_domain_lists"
    if [ -n "$remote_domain_lists" ]; then
        log "Importing domains from remote domain lists for '$section' section"
        config_list_foreach "$section" "remote_domain_lists" import_domains_from_remote_domain_list_handler "$section"
    fi
}

import_domains_from_remote_domain_list_handler() {
    local url="$1"
    local section="$2"

    log "Importing domains from URL: $url"

    local file_extension
    file_extension=$(url_get_file_extension "$url")
    log "Detected file extension: '$file_extension'" "debug"
    case "$file_extension" in
    json | srs)
        log "No update needed - sing-box manages updates automatically."
        ;;
    *)
        log "Import domains from a remote plain-text list"
        import_domains_from_remote_plain_file "$url" "$section"
        ;;
    esac
}

import_domains_from_remote_plain_file() {
    local url="$1"
    local section="$2"

    local tmpfile http_proxy_address items json_array
    tmpfile=$(mktemp)
    http_proxy_address="$(get_service_proxy_address)"

    download_to_file "$url" "$tmpfile" "$http_proxy_address"

    if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
        log "Download $url list failed" "error"
        return 1
    fi

    convert_crlf_to_lf "$tmpfile"
    ruleset_tag=$(get_ruleset_tag "$section" "remote" "domains")
    ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
    import_plain_domain_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath"

    rm -f "$tmpfile"
}

import_subnets_from_remote_subnet_lists() {
    local section="$1"
    local remote_subnet_lists

    rule_is_enabled "$section" || return 0

    config_get remote_subnet_lists "$section" "remote_subnet_lists"
    if [ -n "$remote_subnet_lists" ]; then
        log "Importing subnets from remote subnet lists for '$section' section"
        config_list_foreach "$section" "remote_subnet_lists" import_subnets_from_remote_subnet_list_handler "$section"
    fi
}

import_subnets_from_remote_subnet_list_handler() {
    local url="$1"
    local section="$2"

    log "Importing subnets from URL: $url"

    local file_extension
    file_extension="$(url_get_file_extension "$url")"
    log "Detected file extension: '$file_extension'" "debug"
    case "$file_extension" in
    json)
        log "Import subnets from a remote JSON list" "info"
        import_subnets_from_remote_json_file "$url" "$section"
        ;;
    srs)
        log "Import subnets from a remote SRS list" "info"
        import_subnets_from_remote_srs_file "$url" "$section"
        ;;
    *)
        log "Import subnets from a remote plain-text list" "info"
        import_subnets_from_remote_plain_file "$url" "$section"
        ;;
    esac
}

import_subnets_from_remote_json_file() {
    local url="$1"
    local section="$2"
    local json_tmpfile unscoped_tmpfile scoped_tmpfile http_proxy_address
    json_tmpfile="$(mktemp)"
    unscoped_tmpfile="$(mktemp)"
    scoped_tmpfile="$(mktemp)"
    http_proxy_address="$(get_service_proxy_address)"

    download_to_file "$url" "$json_tmpfile" "$http_proxy_address"

    if [ $? -ne 0 ] || [ ! -s "$json_tmpfile" ]; then
        log "Download $url list failed" "error"
        rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if ! extract_json_ruleset_nft_files_for_section "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile" "$section"; then
        log "Failed to extract ip_cidr entries from remote JSON list $url" "error"
        rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if ! add_extracted_ruleset_subnets_to_nft_for_section "$section" "$unscoped_tmpfile" "$scoped_tmpfile" "Remote JSON rule set $url"; then
        log "Failed to add subnets from remote JSON list $url to nftables" "error"
        rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    rm -f "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
}

import_subnets_from_remote_srs_file() {
    local url="$1"
    local section="$2"

    local binary_tmpfile json_tmpfile unscoped_tmpfile scoped_tmpfile http_proxy_address
    binary_tmpfile="$(mktemp)"
    json_tmpfile="$(mktemp)"
    unscoped_tmpfile="$(mktemp)"
    scoped_tmpfile="$(mktemp)"
    http_proxy_address="$(get_service_proxy_address)"

    download_to_file "$url" "$binary_tmpfile" "$http_proxy_address"

    if [ $? -ne 0 ] || [ ! -s "$binary_tmpfile" ]; then
        log "Download $url list failed" "error"
        rm -f "$binary_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if ! decompile_binary_ruleset "$binary_tmpfile" "$json_tmpfile"; then
        log "Failed to decompile binary rule set file" "error"
        rm -f "$binary_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if ! extract_json_ruleset_nft_files_for_section "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile" "$section"; then
        log "Failed to extract ip_cidr entries from remote SRS list $url" "error"
        rm -f "$binary_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    if ! add_extracted_ruleset_subnets_to_nft_for_section "$section" "$unscoped_tmpfile" "$scoped_tmpfile" "Remote SRS rule set $url"; then
        log "Failed to add subnets from remote SRS list $url to nftables" "error"
        rm -f "$binary_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
        return 1
    fi

    rm -f "$binary_tmpfile" "$json_tmpfile" "$unscoped_tmpfile" "$scoped_tmpfile"
}

import_subnets_from_remote_plain_file() {
    local url="$1"
    local section="$2"

    local tmpfile http_proxy_address items json_array
    tmpfile=$(mktemp)
    http_proxy_address="$(get_service_proxy_address)"

    download_to_file "$url" "$tmpfile" "$http_proxy_address"

    if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
        log "Download $url list failed" "error"
        return 1
    fi

    convert_crlf_to_lf "$tmpfile"

    ruleset_tag=$(get_ruleset_tag "$section" "remote" "subnets")
    ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
    import_plain_subnet_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath"
    add_plain_subnet_file_to_nft_for_section "$section" "$tmpfile"

    rm -f "$tmpfile"
}

## Support functions
get_service_proxy_address() {
    local download_lists_via_proxy
    config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
    if [ "$download_lists_via_proxy" -eq 1 ]; then
        echo "$SB_SERVICE_MIXED_INBOUND_ADDRESS:$SB_SERVICE_MIXED_INBOUND_PORT"
    else
        echo ""
    fi
}

get_download_detour_tag() {
    config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
    if [ "$download_lists_via_proxy" -eq 1 ]; then
        local download_lists_via_proxy_section section_outbound_tag
        config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
        section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")"
        echo "$section_outbound_tag"
    else
        echo ""
    fi
}

_determine_first_outbound_section() {
    local section="$1"
    local action

    rule_is_enabled "$section" || return 0
    subscription_section_is_deferred "$section" && return 0

    action="$(get_rule_action "$section")"
    if [ "$action" = "proxy" ] || [ "$action" = "outbound" ] || [ "$action" = "vpn" ] || { [ "$action" = "byedpi" ] && is_byedpi_installed; }; then
        [ -z "$first_section" ] && first_section="$1"
    fi
}

get_first_outbound_section() {
    local first_section=""

    config_foreach _determine_first_outbound_section "section"

    echo "$first_section"
}

get_device_ipv4_address() {
    local device="$1"

    ip -4 addr show dev "$device" 2> /dev/null | awk '$1 == "inet" { split($2, parts, "/"); print parts[1]; exit }'
}

get_service_listen_address() {
    local service_listen_address interface

    config_get service_listen_address "settings" "service_listen_address"
    if [ -n "$service_listen_address" ]; then
        log "Attention! The service_listen_address option is being used, overriding the automatic detection of the listening IP address!" "warn"
        echo "$service_listen_address"
        return 0
    fi

    interface="lan"
    network_get_ipaddr service_listen_address "$interface"
    if [ -n "$service_listen_address" ]; then
        echo "$service_listen_address"
        return 0
    fi

    local source_network_interfaces
    config_get source_network_interfaces "settings" "source_network_interfaces" "br-lan"
    for interface in $source_network_interfaces; do
        network_get_ipaddr service_listen_address "$interface"
        if [ -n "$service_listen_address" ]; then
            echo "$service_listen_address"
            return 0
        fi

        service_listen_address="$(get_device_ipv4_address "$interface")"
        if [ -n "$service_listen_address" ]; then
            echo "$service_listen_address"
            return 0
        fi
    done

    log "Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/ushan0v/podkop-plus/issues" "error"
    return 1
}

## nftables
nft_list_all_traffic_from_ip() {
    local ip="$1"

    if ! nft list chain inet "$NFT_TABLE_NAME" mangle | grep -q "ip saddr $ip"; then
        nft insert rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" \
            meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter
        nft insert rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" \
            meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter
        nft insert rule inet "$NFT_TABLE_NAME" mangle ip saddr "$ip" ip daddr @localv4 return
    fi
}

# Diagnotics
cleanup_check_proxy_dir() {
    local dir="$1"

    case "$dir" in
        "$TMP_SING_BOX_FOLDER"/check-proxy-*)
            rm -rf "$dir"
            ;;
    esac
}

check_proxy() {
    local sing_box_config_path
    config_get sing_box_config_path "settings" "config_path"

    if ! command -v sing-box > /dev/null 2>&1; then
        nolog "sing-box is not installed"
        return 1
    fi

    if [ ! -f "$sing_box_config_path" ]; then
        nolog "Configuration file not found"
        return 1
    fi

    nolog "Checking sing-box configuration..."

    if ! sing-box -c "$sing_box_config_path" check > /dev/null; then
        nolog "Invalid configuration"
        return 1
    fi

    json_utils_ucode mask-sing-box-config "$sing_box_config_path"

    nolog "Checking proxy connection..."

    local check_proxy_dir check_proxy_config check_proxy_cache check_proxy_outbound_tag
    check_proxy_dir="$TMP_SING_BOX_FOLDER/check-proxy-$$"
    check_proxy_config="$check_proxy_dir/config.json"
    check_proxy_cache="$check_proxy_dir/cache.db"

    cleanup_check_proxy_dir "$check_proxy_dir"
    mkdir -p "$check_proxy_dir"
    if ! json_utils_ucode prepare-check-proxy-config "$sing_box_config_path" "$check_proxy_config" "$check_proxy_cache"; then
        nolog "Failed to prepare temporary configuration"
        cleanup_check_proxy_dir "$check_proxy_dir"
        return 1
    fi

    check_proxy_outbound_tag="$(json_utils_ucode check-proxy-outbound-tag "$check_proxy_config" "$CHECK_PROXY_IP_DOMAIN" 2>/dev/null | sed -n '1p')"

    for attempt in $(seq 1 5); do
        if [ -n "$check_proxy_outbound_tag" ]; then
            response=$(sing-box tools fetch ifconfig.me -c "$check_proxy_config" -D "$check_proxy_dir" \
                --disable-color -o "$check_proxy_outbound_tag" 2> /dev/null)
        else
            response=$(sing-box tools fetch ifconfig.me -c "$check_proxy_config" -D "$check_proxy_dir" \
                --disable-color 2> /dev/null)
        fi
        if echo "$response" | grep -q "^<html\|403 Forbidden"; then
            continue
        fi
        if printf '%s\n' "$response" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
            ip=$(echo "$response" | sed -n 's/^[0-9]\+\.[0-9]\+\.[0-9]\+\.\([0-9]\+\)$/X.X.X.\1/p')
            nolog "$ip - should match proxy IP"
            cleanup_check_proxy_dir "$check_proxy_dir"
            return 0
        elif printf '%s\n' "$response" | grep -Eq '^[0-9a-fA-F:]*::[0-9a-fA-F:]*$|^[0-9a-fA-F:]+$'; then
            ip=$(echo "$response" | sed 's/\([0-9a-fA-F]\+:[0-9a-fA-F]\+:[0-9a-fA-F]\+\):.*/\1:XXXX:XXXX:XXXX/')
            nolog "$ip - should match proxy IP"
            cleanup_check_proxy_dir "$check_proxy_dir"
            return 0
        fi
        if [ $attempt -eq 5 ]; then
            nolog "Failed to get valid IP address after 5 attempts"
            if [ -z "$response" ]; then
                nolog "Error: Empty response"
            else
                nolog "Error response: $response"
            fi
            cleanup_check_proxy_dir "$check_proxy_dir"
            return 1
        fi
    done

    cleanup_check_proxy_dir "$check_proxy_dir"
}

check_nft() {
    if ! command -v nft > /dev/null 2>&1; then
        nolog "nft is not installed"
        return 1
    fi

    nolog "Checking $NFT_TABLE_NAME rules..."

    # Check if table exists
    if ! nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
        nolog "❌ $NFT_TABLE_NAME not found"
        return 1
    fi

    local found_hetzner=0
    local found_ovh=0

    check_domain_list_contains() {
        local section="$1"

        config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
        if [ "$domain_list_enabled" -eq 1 ]; then
            config_list_foreach "$section" "domain_list" check_domain_value
        fi
    }

    check_domain_value() {
        local domain_value="$1"

        if [ "$domain_value" = "hetzner" ]; then
            found_hetzner=1
        elif [ "$domain_value" = "ovh" ]; then
            found_ovh=1
        fi
    }

    config_foreach check_domain_list_contains

    if [ "$found_hetzner" -eq 1 ] || [ "$found_ovh" -eq 1 ]; then

        local sets="podkop_subnets podkop_domains interfaces podkop_discord_subnets localv4"

        nolog "Sets statistics:"
        for set_name in $sets; do
            if nft list set inet "$NFT_TABLE_NAME" $set_name > /dev/null 2>&1; then
                # Count elements using grep to count commas and add 1 (last element has no comma)
                local count=$(nft list set inet "$NFT_TABLE_NAME" $set_name 2> /dev/null | grep -o ',\|{' | wc -l)
                echo "- $set_name: $count elements"
            fi
        done

        nolog "Chain configurations:"

        # Create a temporary file for processing
        local tmp_file=$(mktemp)
        nft list table inet "$NFT_TABLE_NAME" > "$tmp_file"

        # Extract chain configurations without element listings
        sed -n '/chain mangle {/,/}/p' "$tmp_file" | grep -v "elements" | grep -v "^[[:space:]]*[0-9]"
        sed -n '/chain proxy {/,/}/p' "$tmp_file" | grep -v "elements" | grep -v "^[[:space:]]*[0-9]"

        # Clean up
        rm -f "$tmp_file"
    else
        # Simple view as originally implemented
        nolog "Sets configuration:"
        nft list table inet "$NFT_TABLE_NAME"
    fi

    nolog "NFT check completed"
}

check_logs() {
    if ! command -v logread > /dev/null 2>&1; then
        nolog "Error: logread command not found"
        return 1
    fi

    local logs
    logs=$(logread | grep -E "podkop-plus|sing-box")

    if [ -z "$logs" ]; then
        nolog "Logs not found"
        return 1
    fi

    # Find the last occurrence of "Starting Podkop Plus"
    local start_line
    start_line=$(echo "$logs" | grep -n "podkop-plus.*Starting Podkop Plus" | tail -n 1 | cut -d: -f1)

    if [ -n "$start_line" ]; then
        echo "$logs" | tail -n +"$start_line"
    else
        nolog "No 'Starting Podkop Plus' message found, showing last 100 lines"
        echo "$logs" | tail -n 100
    fi
}

check_sing_box_logs() {
    if ! command -v logread > /dev/null 2>&1; then
        nolog "Error: logread command not found"
        return 1
    fi

    local logs
    logs=$(logread | grep -E "sing-box")

    if [ -z "$logs" ]; then
        nolog "sing-box logs not found"
        return 1
    fi

    echo "$logs" | tail -n 100
}

show_sing_box_config() {
    local sing_box_config_path
    config_get sing_box_config_path "settings" "config_path"
    nolog "Current sing-box configuration:"

    if [ ! -f "$sing_box_config_path" ]; then
        nolog "Configuration file not found"
        return 1
    fi

    json_utils_ucode mask-sing-box-config "$sing_box_config_path"
}

show_config() {
    if [ ! -f "$PODKOP_CONFIG" ]; then
        nolog "Configuration file not found"
        return 1
    fi

    tmp_config=$(mktemp)

    sed -e 's/\(option proxy_string\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option subscription_url\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(list subscription_urls\).*/\1 '\''MASKED'\''/g' \
        -e '/option outbound_json/,/^}/c\	option outbound_json '\''MASKED'\''' \
        -e 's/\(list urltest_proxy_links\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(list selector_proxy_links\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(list domain[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list domain_suffix[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list domain_keyword[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list domain_regex[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list ip_cidr[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list source_ip_cidr[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list fully_routed_ips[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list server_users\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option dns_server[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option bootstrap_dns_server[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option domain_resolver_dns_server[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option listen[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option listen_port[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option public_host[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option server_uuid\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option server_username\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option server_password\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option tls_server_name[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option reality_handshake_server[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option reality_handshake_server_port[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option transport_host[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list transport_hosts[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option tailscale_auth_key[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option tailscale_control_url[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option tailscale_hostname[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(list tailscale_advertise_routes[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e '/option tailscale_ephemeral[[:space:]]/d' \
        -e '/option tailscale_exit_node[[:space:]]/d' \
        -e '/option tailscale_exit_node_allow_lan_access[[:space:]]/d' \
        -e 's/\(option hysteria2_obfs_password\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option reality_private_key\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option reality_public_key\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option reality_short_id\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(list reality_short_id\).*/\1 '\''MASKED'\''/g' \
        -e 's/\(option mixed_proxy_username[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e 's/\(option mixed_proxy_password[[:space:]]\).*/\1'\''MASKED'\''/g' \
        -e "s@\\(option dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \
        -e "s@\\(option domain_resolver_dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \
        -e 's/\(option yacd_secret_key\).*/\1 '\''MASKED'\''/g' \
        "$PODKOP_CONFIG" > "$tmp_config"

    cat "$tmp_config"
    rm -f "$tmp_config"
}

show_version() {
    echo "$PODKOP_VERSION"
}

show_sing_box_version() {
    local version
    version=$(sing-box version | head -n 1 | awk '{print $3}')
    echo "$version"
}

fetch_latest_podkop_release_json() {
    local owner repo

    owner="${PODKOP_RELEASE_REPO%%/*}"
    repo="${PODKOP_RELEASE_REPO#*/}"
    [ -n "$owner" ] || return 1
    [ -n "$repo" ] || return 1
    [ "$owner" != "$repo" ] || return 1

    updates_fetch_github_release_json "$owner" "$repo"
}

fetch_latest_podkop_version() {
    local response tag

    response="$(fetch_latest_podkop_release_json)" || return 1
    tag="$(printf '%s' "$response" | json_utils_ucode object-get-default tag_name "" 2>/dev/null)"

    [ -n "$tag" ] || return 1
    echo "$tag"
}

normalize_podkop_release_version() {
    printf '%s\n' "$1" | sed 's/^v//;s/-r\([0-9][0-9]*\)$/-\1/'
}

is_podkop_release_version() {
    normalize_podkop_release_version "$1" | grep -Eq '^([0-9]+([.][0-9]+)*(-[0-9]+)?|[0-9]+([.][0-9]+){3})$'
}

podkop_release_version_parts() {
    local version release old_ifs

    version="$(normalize_podkop_release_version "$1")"
    release=0

    case "$version" in
    *-*)
        release="${version##*-}"
        version="${version%-*}"
        ;;
    *.*.*.*)
        release="${version##*.}"
        version="${version%.*}"
        ;;
    esac

    case "$version" in '' | *[!0-9.]* | .* | *.) return 1 ;; esac
    case "$release" in '' | *[!0-9]*) return 1 ;; esac

    old_ifs="$IFS"
    IFS='.'
    set -- $version
    IFS="$old_ifs"

    [ -n "$1" ] || return 1
    case "$1" in *[!0-9]*) return 1 ;; esac
    case "${2:-0}" in *[!0-9]*) return 1 ;; esac
    case "${3:-0}" in *[!0-9]*) return 1 ;; esac

    printf '%s %s %s %s\n' "$1" "${2:-0}" "${3:-0}" "$release"
}

podkop_release_version_compare() {
    local lhs_parts rhs_parts lhs_major lhs_minor lhs_patch lhs_release rhs_major rhs_minor rhs_patch rhs_release

    lhs_parts="$(podkop_release_version_parts "$1")" || return 1
    rhs_parts="$(podkop_release_version_parts "$2")" || return 1

    set -- $lhs_parts
    lhs_major="$1"
    lhs_minor="$2"
    lhs_patch="$3"
    lhs_release="$4"

    set -- $rhs_parts
    rhs_major="$1"
    rhs_minor="$2"
    rhs_patch="$3"
    rhs_release="$4"

    [ "$lhs_major" -lt "$rhs_major" ] && echo -1 && return 0
    [ "$lhs_major" -gt "$rhs_major" ] && echo 1 && return 0
    [ "$lhs_minor" -lt "$rhs_minor" ] && echo -1 && return 0
    [ "$lhs_minor" -gt "$rhs_minor" ] && echo 1 && return 0
    [ "$lhs_patch" -lt "$rhs_patch" ] && echo -1 && return 0
    [ "$lhs_patch" -gt "$rhs_patch" ] && echo 1 && return 0
    [ "$lhs_release" -lt "$rhs_release" ] && echo -1 && return 0
    [ "$lhs_release" -gt "$rhs_release" ] && echo 1 && return 0
    echo 0
}

write_podkop_latest_version_cache() {
    local value="$1"
    local timestamp="$2"
    local cache_file="/tmp/podkop-plus.latest-version.cache"

    [ -n "$value" ] || return 0

    {
        printf '%s\n' "$value"
        printf '%s\n' "$timestamp"
    } > "$cache_file"
}

get_cached_podkop_latest_version() {
    local cache_file="/tmp/podkop-plus.latest-version.cache"
    local cache_value

    cache_value="$(sed -n '1p' "$cache_file" 2>/dev/null)"
    [ -n "$cache_value" ] || cache_value="unknown"
    echo "$cache_value"
}

get_luci_app_version() {
    if [ -f "$PODKOP_LUCI_VIEW_DIR/main.js" ]; then
        grep 'var PODKOP_LUCI_APP_VERSION' "$PODKOP_LUCI_VIEW_DIR/main.js" | cut -d'"' -f2
        return 0
    fi

    echo "not installed"
}

system_info_cache_is_valid() {
    local now luci_app_version

    [ -s "$PODKOP_SYSTEM_INFO_CACHE_FILE" ] || return 1

    now="$(date +%s 2>/dev/null)"
    case "$now" in
    '' | *[!0-9]*) now=0 ;;
    esac

    luci_app_version="$(get_luci_app_version)"
    json_utils_ucode system-info-cache-valid "$PODKOP_SYSTEM_INFO_CACHE_FILE" "$PODKOP_VERSION" "$luci_app_version" "$PODKOP_SYSTEM_INFO_CACHE_TTL" "$now" >/dev/null 2>&1
}

write_system_info_cache() {
    local tmpfile

    ensure_runtime_dirs

    tmpfile="${PODKOP_SYSTEM_INFO_CACHE_FILE}.$$"
    cat > "$tmpfile" && mv "$tmpfile" "$PODKOP_SYSTEM_INFO_CACHE_FILE"
}

build_system_info() {
    local podkop_version podkop_latest_version luci_app_version sing_box_version sing_box_extended zapret_version zapret_installed byedpi_version byedpi_installed openwrt_version device_model
    local generated_at

    podkop_version="$PODKOP_VERSION"

    podkop_latest_version="$(get_cached_podkop_latest_version)"
    [ -n "$podkop_latest_version" ] || podkop_latest_version="unknown"

    luci_app_version="$(get_luci_app_version)"

    if command -v sing-box > /dev/null 2>&1; then
        sing_box_version=$(sing-box version 2> /dev/null | head -n 1 | awk '{print $NF}')
        [ -z "$sing_box_version" ] && sing_box_version="unknown"
    else
        sing_box_version="not installed"
    fi
    sing_box_extended=0
    is_sing_box_extended "$sing_box_version" && sing_box_extended=1

    zapret_installed=0
    if is_zapret_installed; then
        zapret_installed=1
        zapret_version="$(get_zapret_package_version)"
        [ -z "$zapret_version" ] && zapret_version="unknown"
    else
        zapret_version="not installed"
    fi

    byedpi_installed=0
    if is_byedpi_installed; then
        byedpi_installed=1
        byedpi_version="$(get_byedpi_package_version)"
        [ -z "$byedpi_version" ] && byedpi_version="unknown"
    else
        byedpi_version="not installed"
    fi

    if [ -f /etc/os-release ]; then
        openwrt_version=$(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2)
        [ -z "$openwrt_version" ] && openwrt_version="unknown"
    else
        openwrt_version="unknown"
    fi

    if [ -f /tmp/sysinfo/model ]; then
        device_model=$(cat /tmp/sysinfo/model)
        [ -z "$device_model" ] && device_model="unknown"
    else
        device_model="unknown"
    fi

    generated_at="$(date +%s 2>/dev/null)"
    case "$generated_at" in
    '' | *[!0-9]*) generated_at=0 ;;
    esac

    json_utils_ucode system-info-json \
        "$podkop_version" "$podkop_latest_version" "$luci_app_version" \
        "$sing_box_version" "$sing_box_extended" \
        "$zapret_version" "$zapret_installed" \
        "$byedpi_version" "$byedpi_installed" \
        "$openwrt_version" "$device_model" "$generated_at"
}

refresh_system_info_cache() {
    local system_info

    system_info="$(build_system_info)" || return 1
    [ -n "$system_info" ] || return 1

    printf '%s\n' "$system_info" | write_system_info_cache
}

get_system_info() {
    local system_info

    if system_info_cache_is_valid; then
        cat "$PODKOP_SYSTEM_INFO_CACHE_FILE"
        return 0
    fi

    system_info="$(build_system_info)" || return 1
    [ -n "$system_info" ] || return 1

    printf '%s\n' "$system_info" | write_system_info_cache
    printf '%s\n' "$system_info"
}

get_zapret_status() {
    get_zapret_status_json
}

neutralize_zapret_defaults() {
    log "Standalone zapret is not neutralized automatically; Podkop Plus uses /opt/zapret/nfq/nfqws as an external provider and manages only its own NFQUEUE range."
}

check_zapret_runtime() {
    check_zapret_runtime_json
}

get_byedpi_status() {
    get_byedpi_status_json
}

check_byedpi_runtime() {
    check_byedpi_runtime_json
}

get_sing_box_status() {
    local running=0
    local enabled=0
    local status=""
    local version=""
    local dns_configured=0

    # Check if service is enabled
    if [ -x /etc/rc.d/S99sing-box ]; then
        enabled=1
    fi

    # Check if service is running
    if pgrep -f "sing-box" > /dev/null; then
        running=1
        version=$(sing-box version | head -n 1 | awk '{print $3}')
    fi

    # Check DNS configuration
    if dnsmasq_has_podkop_dns; then
        dns_configured=1
    fi

    # Format status message
    if [ $running -eq 1 ]; then
        if [ $enabled -eq 1 ]; then
            status="running & enabled"
        else
            status="running but disabled"
        fi
    else
        if [ $enabled -eq 1 ]; then
            status="stopped but enabled"
        else
            status="stopped & disabled"
        fi
    fi

    echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\",\"dns_configured\":$dns_configured}"
}

get_status() {
    local running=0
    local enabled=0
    local status=""
    local dns_configured=0

    if podkop_is_running; then
        running=1
    fi

    # Check if service is enabled
    if [ -x "/etc/rc.d/S99$PODKOP_SERVICE_NAME" ]; then
        enabled=1
    fi

    if dnsmasq_has_podkop_dns; then
        dns_configured=1
    fi

    if [ "$running" -eq 1 ]; then
        if [ "$enabled" -eq 1 ]; then
            status="running & enabled"
        else
            status="running but disabled"
        fi
    else
        if [ "$enabled" -eq 1 ]; then
            status="stopped but enabled"
        else
            status="stopped & disabled"
        fi
    fi

    json_utils_ucode object-json \
        running n "$running" \
        enabled n "$enabled" \
        status s "$status" \
        dns_configured n "$dns_configured"
}

get_outbound_link() {
    local section="$1"
    local outbound_tag="$2"

    ensure_runtime_dirs
    case "$section" in
    "" | */* | *..*)
        printf '{"link":""}\n'
        return 0
        ;;
    esac

    subscription_cache_ucode get-link \
        "$PODKOP_SECTION_CACHE_DIR" "$TMP_SUBSCRIPTION_FOLDER" "$section" "$outbound_tag" "$PODKOP_SUBSCRIPTION_LINKS_DIR" ||
        printf '{"link":""}\n'
}

get_outbound_link_states() {
    local section="$1"

    ensure_runtime_dirs
    case "$section" in
    "" | */* | *..*)
        printf '{}\n'
        return 0
        ;;
    esac

    subscription_cache_ucode get-link-states "$PODKOP_SECTION_CACHE_DIR" "$section" "$PODKOP_SUBSCRIPTION_LINKS_DIR" ||
        printf '{}\n'
}

get_outbound_metadata() {
    local section="$1"
    local metadata_path

    ensure_runtime_dirs
    case "$section" in
    "" | */* | *..*)
        printf '{"names":{},"countries":{}}\n'
        return 0
        ;;
    esac

    metadata_path="$(get_outbound_metadata_path "$section")"
    subscription_cache_ucode get-outbound-metadata "$PODKOP_SECTION_CACHE_DIR" "$section" "$metadata_path" ||
        printf '{"names":{},"countries":{}}\n'
}

get_subscription_metadata() {
    local section="$1"
    local metadata_path

    ensure_runtime_dirs

    case "$section" in
    "" | */* | *..*)
        printf '{}\n'
        return 0
        ;;
    esac

    metadata_path="$(get_subscription_metadata_path "$section")"
    subscription_cache_ucode get-subscription-metadata "$PODKOP_SECTION_CACHE_DIR" "$section" "$metadata_path" ||
        printf '{}\n'
}

validate_nfqws_strategy_json() {
    local raw_opt="${1:-}"

    if check_nfqws_strategy "$raw_opt"; then
        json_utils_ucode nfqws-strategy-validation true "" "" ""
        return 0
    fi

    json_utils_ucode nfqws-strategy-validation false "$NFQWS_VALIDATE_ERROR" "$NFQWS_VALIDATE_NEEDLE" "$NFQWS_VALIDATE_NEEDLES"
}

check_dns_available() {
    local dns_type dns_server bootstrap_dns_server
    config_get dns_type "settings" "dns_type"
    config_get dns_server "settings" "dns_server"
    config_get bootstrap_dns_server "settings" "bootstrap_dns_server"

    local dns_status=0
    local dns_on_router=0
    local bootstrap_dns_status=0
    local dhcp_config_status=1
    local domain="google.com"

    # Mask NextDNS ID if present
    local display_dns_server="$dns_server"
    if echo "$dns_server" | grep -q "\.dns\.nextdns\.io$"; then
        local nextdns_id
        nextdns_id=$(echo "$dns_server" | cut -d'.' -f1)
        display_dns_server="$(echo "$nextdns_id" | sed 's/./*/g').dns.nextdns.io"
    elif echo "$dns_server" | grep -q "^dns\.nextdns\.io/"; then
        local masked_path
        masked_path=$(echo "$dns_server" | cut -d'/' -f2- | sed 's/./*/g')
        display_dns_server="dns.nextdns.io/$masked_path"
    fi

    if [ "$dns_type" = "doh" ]; then
        # Check if dns_server already contains a path
        local doh_path="/dns-query"
        if echo "$dns_server" | grep -q "/"; then
            # Path is already present, extract it
            doh_path="/$(echo "$dns_server" | cut -d'/' -f2-)"
            dns_server="$(echo "$dns_server" | cut -d'/' -f1)"
        fi

        if dig @"$dns_server" "$domain" +https="$doh_path" +timeout=2 +tries=1 > /dev/null 2>&1; then
            dns_status=1
        fi
    elif [ "$dns_type" = "dot" ]; then
        if dig @"$dns_server" "$domain" +tls +timeout=2 +tries=1 > /dev/null 2>&1; then
            dns_status=1
        fi
    elif [ "$dns_type" = "udp" ]; then
        if dig @"$dns_server" "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then
            dns_status=1
        fi
    fi

    # Check if local DNS resolver is working
    if dig @127.0.0.1 "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then
        dns_on_router=1
    fi

    # Check bootstrap DNS server
    if [ -n "$bootstrap_dns_server" ]; then
        if dig @"$bootstrap_dns_server" "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then
            bootstrap_dns_status=1
        fi
    fi

    # Check if dnsmasq sends only selected source interfaces through Podkop DNS.
    config_load dhcp
    check_dhcp_has_podkop_dns
    config_load "$PODKOP_CONFIG_NAME"

    json_utils_ucode object-json \
        dns_type s "$dns_type" \
        dns_server s "$display_dns_server" \
        dns_status n "$dns_status" \
        dns_on_router n "$dns_on_router" \
        bootstrap_dns_server s "$bootstrap_dns_server" \
        bootstrap_dns_status n "$bootstrap_dns_status" \
        dhcp_config_status n "$dhcp_config_status"
}

check_dhcp_has_podkop_dns() {
    local source_network_interfaces podkop_interfaces podkop_notinterfaces default_notinterfaces server_list cachesize noresolv
    local server server_found interface default_domain_forward_servers

    source_network_interfaces="$(uci_get "$PODKOP_CONFIG_NAME" "settings" "source_network_interfaces")"
    [ -n "$source_network_interfaces" ] || source_network_interfaces="br-lan"

    server_list="$(uci_get "dhcp" "$PODKOP_DNSMASQ_SECTION" "server")"
    cachesize="$(uci_get "dhcp" "$PODKOP_DNSMASQ_SECTION" "cachesize")"
    noresolv="$(uci_get "dhcp" "$PODKOP_DNSMASQ_SECTION" "noresolv")"
    podkop_interfaces="$(uci_get "dhcp" "$PODKOP_DNSMASQ_SECTION" "interface")"
    podkop_notinterfaces="$(uci_get "dhcp" "$PODKOP_DNSMASQ_SECTION" "notinterface")"
    default_notinterfaces="$(uci_get "dhcp" "@dnsmasq[0]" "notinterface")"
    default_domain_forward_servers="$(dnsmasq_default_domain_forward_servers)"

    server_found=0

    if [ -n "$server_list" ]; then
        for server in $server_list; do
            if [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ]; then
                server_found=1
                break
            fi
        done
    fi

    if [ "$cachesize" != "0" ] || [ "$noresolv" != "1" ] || [ "$server_found" != "1" ]; then
        dhcp_config_status=0
        return 0
    fi

    for server in $default_domain_forward_servers; do
        if ! list_has_item "$server_list" "$server"; then
            dhcp_config_status=0
            return 0
        fi
    done

    if ! list_has_item "$podkop_notinterfaces" "lo"; then
        dhcp_config_status=0
        return 0
    fi

    for interface in $source_network_interfaces; do
        if ! list_has_item "$podkop_interfaces" "$interface" ||
            ! list_has_item "$default_notinterfaces" "$interface"; then
            dhcp_config_status=0
            return 0
        fi
    done

    if dnsmasq_default_has_podkop_dns; then
        dhcp_config_status=0
    fi
}

check_nft_rules() {
    local table_exist=0
    local rules_mangle_exist=0
    local rules_mangle_counters=0
    local rules_mangle_output_exist=0
    local rules_mangle_output_counters=0
    local rules_proxy_exist=0
    local rules_proxy_counters=0
    local rules_other_mark_exist=0

    # Generate traffic through PodkopTable
    curl -m 3 -s "https://$CHECK_PROXY_IP_DOMAIN/check" > /dev/null 2>&1 &
    local pid1=$!
    curl -m 3 -s "https://$FAKEIP_TEST_DOMAIN/check" > /dev/null 2>&1 &
    local pid2=$!

    wait $pid1 2> /dev/null
    wait $pid2 2> /dev/null
    sleep 1

    # Check if PodkopPlusTable exists
    if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
        table_exist=1

        # Check mangle chain rules
        if nft list chain inet "$NFT_TABLE_NAME" mangle > /dev/null 2>&1; then
            local mangle_output
            mangle_output=$(nft list chain inet "$NFT_TABLE_NAME" mangle)
            if echo "$mangle_output" | grep -q "counter"; then
                rules_mangle_exist=1

                if echo "$mangle_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then
                    rules_mangle_counters=1
                fi
            fi
        fi

        # Check mangle_output chain rules
        if nft list chain inet "$NFT_TABLE_NAME" mangle_output > /dev/null 2>&1; then
            local mangle_output_output
            mangle_output_output=$(nft list chain inet "$NFT_TABLE_NAME" mangle_output)
            if echo "$mangle_output_output" | grep -q "counter"; then
                rules_mangle_output_exist=1

                if echo "$mangle_output_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then
                    rules_mangle_output_counters=1
                fi
            fi
        fi

        # Check proxy chain rules
        if nft list chain inet "$NFT_TABLE_NAME" proxy > /dev/null 2>&1; then
            local proxy_output
            proxy_output=$(nft list chain inet "$NFT_TABLE_NAME" proxy)
            if echo "$proxy_output" | grep -q "counter"; then
                rules_proxy_exist=1

                if echo "$proxy_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then
                    rules_proxy_counters=1
                fi
            fi
        fi
    fi

    # Check for other mark rules outside PodkopPlusTable
    nft list tables 2> /dev/null | while read -r _ family table_name; do
        [ -z "$table_name" ] && continue

        [ "$table_name" = "$NFT_TABLE_NAME" ] && continue

        if nft list table "$family" "$table_name" 2> /dev/null | grep -q "meta mark set"; then
            touch /tmp/podkop_mark_check.$$
            break
        fi
    done

    if [ -f /tmp/podkop_mark_check.$$ ]; then
        rules_other_mark_exist=1
        rm -f /tmp/podkop_mark_check.$$
    fi

    json_utils_ucode object-json \
        table_exist n "$table_exist" \
        rules_mangle_exist n "$rules_mangle_exist" \
        rules_mangle_counters n "$rules_mangle_counters" \
        rules_mangle_output_exist n "$rules_mangle_output_exist" \
        rules_mangle_output_counters n "$rules_mangle_output_counters" \
        rules_proxy_exist n "$rules_proxy_exist" \
        rules_proxy_counters n "$rules_proxy_counters" \
        rules_other_mark_exist n "$rules_other_mark_exist"
}

check_sing_box() {
    local sing_box_installed=0
    local sing_box_version_ok=0
    local sing_box_extended=0
    local sing_box_service_exist=0
    local sing_box_autostart_disabled=0
    local sing_box_process_running=0
    local sing_box_ports_listening=0

    # Check if sing-box is installed
    if command -v sing-box > /dev/null 2>&1; then
        sing_box_installed=1

        # Check version (must be >= 1.12.4)
        local version
        version=$(sing-box version 2> /dev/null | head -n 1 | awk '{print $NF}')
        if [ -n "$version" ]; then
            version=$(echo "$version" | sed 's/^v//')
            is_sing_box_extended "$version" && sing_box_extended=1

            if is_min_package_version "$version" "1.12.4"; then
                sing_box_version_ok=1
            fi
        fi
    fi

    # Check if service exists
    if [ -f /etc/init.d/sing-box ]; then
        sing_box_service_exist=1

        if ! /etc/init.d/sing-box enabled 2> /dev/null; then
            sing_box_autostart_disabled=1
        fi
    fi

    # Check if process is running
    if pgrep "sing-box" > /dev/null 2>&1; then
        sing_box_process_running=1
    fi

    # Check if sing-box is listening on required ports
    local port_53_ok=0
    local port_1602_ok=0

    if netstat -ln 2> /dev/null | grep -q "127.0.0.42:53"; then
        port_53_ok=1
    fi

    if netstat -ln 2> /dev/null | grep -q "127.0.0.1:1602"; then
        port_1602_ok=1
    fi

    # Both ports must be listening
    if [ "$port_53_ok" = "1" ] && [ "$port_1602_ok" = "1" ]; then
        sing_box_ports_listening=1
    fi

    json_utils_ucode object-json \
        sing_box_installed n "$sing_box_installed" \
        sing_box_version_ok n "$sing_box_version_ok" \
        sing_box_extended n "$sing_box_extended" \
        sing_box_service_exist n "$sing_box_service_exist" \
        sing_box_autostart_disabled n "$sing_box_autostart_disabled" \
        sing_box_process_running n "$sing_box_process_running" \
        sing_box_ports_listening n "$sing_box_ports_listening"
}

check_fakeip() {
    local fakeip_address fakeip_status

    fakeip_address="$(
        dig +short @"$SB_DNS_INBOUND_ADDRESS" "$FAKEIP_TEST_DOMAIN" A +timeout=2 +tries=1 2> /dev/null |
            awk '/^[0-9]+\./ { print; exit }'
    )"

    fakeip_status=false
    if echo "$fakeip_address" | grep -Eq "^198\.(18|19)\."; then
        fakeip_status=true
    fi

    json_utils_ucode object-json fakeip b "$fakeip_status" IP s "$fakeip_address"
}

#######################################
# Clash API interface for managing proxies and groups
# Arguments:
#   $1 - Action: get_proxies, get_connections, get_proxy_latency, get_group_latency, set_group_proxy,
#        close_connection, close_all_connections
#   $2 - Proxy/Group tag (required for latency and set operations) or connection id
#   $3 - Timeout in ms (optional, defaults: 2000 for proxy, 5000 for group) or target proxy tag for set_group_proxy
# Outputs:
#   JSON formatted response
# Usage:
#   clash_api get_proxies
#   clash_api get_connections
#   clash_api get_proxy_latency <proxy_tag> [timeout]
#   clash_api get_group_latency <group_tag> [timeout]
#   clash_api set_group_proxy <group_tag> <proxy_tag>
#   clash_api close_connection <connection_id>
#   clash_api close_all_connections
#######################################

clash_api_urlencode_path_segment() {
    json_utils_ucode url-encode "$1"
}

clash_api() {
    local action="$1"
    local clash_api_controller_address CLASH_URL TEST_URL
    clash_api_controller_address="$(get_service_listen_address)"
    if [ -z "$clash_api_controller_address" ]; then
        clash_api_controller_address="127.0.0.1"
    fi
    CLASH_URL="$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT"
    TEST_URL="https://www.gstatic.com/generate_204"

    local enable_yacd_wan_access yacd_secret_key auth_header
    config_get_bool enable_yacd_wan_access "settings" "enable_yacd_wan_access" 0
    config_get yacd_secret_key "settings" "yacd_secret_key"

    if [ "$enable_yacd_wan_access" -eq 1 ]; then
        auth_header="Authorization: Bearer $yacd_secret_key"
    else
        auth_header=""
    fi

    case "$action" in
    get_proxies)
        curl -s --header "$auth_header" "$CLASH_URL/proxies" | json_utils_ucode stdin-json
        ;;

    get_connections)
        curl -s --header "$auth_header" "$CLASH_URL/connections" | json_utils_ucode stdin-json
        ;;

    get_proxy_latency)
        local proxy_tag="$2"
        local timeout="${3:-2000}"

        if [ -z "$proxy_tag" ]; then
            json_utils_ucode object-json error s "proxy_tag required"
            return 1
        fi

        curl -G -s "$CLASH_URL/proxies/$proxy_tag/delay" \
            --header "$auth_header" \
            --data-urlencode "url=$TEST_URL" \
            --data-urlencode "timeout=$timeout" | json_utils_ucode stdin-json
        ;;

    get_group_latency)
        local group_tag="$2"
        local timeout="${3:-5000}"

        if [ -z "$group_tag" ]; then
            json_utils_ucode object-json error s "group_tag required"
            return 1
        fi

        curl -G -s "$CLASH_URL/group/$group_tag/delay" \
            --header "$auth_header" \
            --data-urlencode "url=$TEST_URL" \
            --data-urlencode "timeout=$timeout" | json_utils_ucode stdin-json
        ;;

    set_group_proxy)
        local group_tag="$2"
        local proxy_tag="$3"

        if [ -z "$group_tag" ] || [ -z "$proxy_tag" ]; then
            json_utils_ucode object-json error s "group_tag and proxy_tag required"
            return 1
        fi

        local response
        response=$(
            curl -X PUT -s -w "\n%{http_code}" "$CLASH_URL/proxies/$group_tag" \
                --header "$auth_header" \
                --data-raw "$(json_utils_ucode object-json name s "$proxy_tag")"
        )

        local http_code
        local body
        http_code=$(echo "$response" | tail -n 1)
        body=$(echo "$response" | sed '$d')

        case "$http_code" in
        204)
            json_utils_ucode object-json success b true group s "$group_tag" proxy s "$proxy_tag"
            ;;
        404)
            json_utils_ucode object-json success b false error s "group_not_found" message s "$group_tag does not exist"
            return 1
            ;;
        400)
            if echo "$body" | grep -q "not found"; then
                json_utils_ucode object-json success b false error s "proxy_not_found" message s "$proxy_tag not found in group $group_tag"
            else
                json_utils_ucode object-json success b false error s "bad_request" message s "Invalid request"
            fi
            return 1
            ;;
        *)
            if [ -n "$body" ]; then
                json_utils_ucode object-json success b false http_code n "$http_code" body a "$body"
            else
                json_utils_ucode object-json success b false http_code n "$http_code"
            fi
            return 1
            ;;
        esac
        ;;

    close_connection)
        local connection_id="$2"

        if [ -z "$connection_id" ]; then
            json_utils_ucode object-json error s "connection_id required"
            return 1
        fi

        local encoded_connection_id response http_code body
        encoded_connection_id="$(clash_api_urlencode_path_segment "$connection_id")" || return 1
        response=$(
            curl -X DELETE -s -w "\n%{http_code}" "$CLASH_URL/connections/$encoded_connection_id" \
                --header "$auth_header"
        )
        http_code=$(echo "$response" | tail -n 1)
        body=$(echo "$response" | sed '$d')

        case "$http_code" in
        200 | 204)
            json_utils_ucode object-json success b true connection_id s "$connection_id"
            ;;
        404)
            json_utils_ucode object-json success b false error s "connection_not_found" connection_id s "$connection_id"
            return 1
            ;;
        *)
            if [ -n "$body" ]; then
                json_utils_ucode object-json success b false http_code n "$http_code" body a "$body"
            else
                json_utils_ucode object-json success b false http_code n "$http_code"
            fi
            return 1
            ;;
        esac
        ;;

    close_all_connections)
        local response http_code body
        response=$(
            curl -X DELETE -s -w "\n%{http_code}" "$CLASH_URL/connections" \
                --header "$auth_header"
        )
        http_code=$(echo "$response" | tail -n 1)
        body=$(echo "$response" | sed '$d')

        case "$http_code" in
        200 | 204)
            json_utils_ucode object-json success b true
            ;;
        *)
            if [ -n "$body" ]; then
                json_utils_ucode object-json success b false http_code n "$http_code" body a "$body"
            else
                json_utils_ucode object-json success b false http_code n "$http_code"
            fi
            return 1
            ;;
        esac
        ;;

    *)
        json_utils_ucode object-json \
            error s "unknown action" \
            available j '["get_proxies","get_connections","get_proxy_latency","get_group_latency","set_group_proxy","close_connection","close_all_connections"]'
        return 1
        ;;
    esac
}

print_global() {
    local message="$1"
    echo "$message"
}

json_get_default() {
    local json="$1"
    local key="$2"
    local fallback="$3"

    printf '%s' "$json" | json_utils_ucode object-get-default "$key" "$fallback" 2>/dev/null
}

global_check() {
    local PODKOP_LUCI_VERSION="Unknown"
    [ -n "$1" ] && PODKOP_LUCI_VERSION="$1"

    print_global "📡 Global check run!"
    print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    print_global "🛠️ System info"

    local system_info_json
    system_info_json=$(get_system_info)

    if [ -n "$system_info_json" ]; then
        local podkop_version podkop_latest_version luci_app_version sing_box_version sing_box_extended zapret_version zapret_installed byedpi_version byedpi_installed openwrt_version device_model

        podkop_version=$(json_get_default "$system_info_json" podkop_version "unknown")
        podkop_latest_version=$(json_get_default "$system_info_json" podkop_latest_version "unknown")
        luci_app_version=$(json_get_default "$system_info_json" luci_app_version "unknown")
        sing_box_version=$(json_get_default "$system_info_json" sing_box_version "unknown")
        sing_box_extended=$(json_get_default "$system_info_json" sing_box_extended 0)
        zapret_version=$(json_get_default "$system_info_json" zapret_version "unknown")
        zapret_installed=$(json_get_default "$system_info_json" zapret_installed 0)
        byedpi_version=$(json_get_default "$system_info_json" byedpi_version "unknown")
        byedpi_installed=$(json_get_default "$system_info_json" byedpi_installed 0)
        openwrt_version=$(json_get_default "$system_info_json" openwrt_version "unknown")
        device_model=$(json_get_default "$system_info_json" device_model "unknown")
        if [ "$sing_box_extended" -eq 1 ]; then
            print_global "Sing-box core: extended"
        else
            print_global "Sing-box core: regular"
        fi

        print_global "🕳️ Podkop Plus:   $podkop_version"
        print_global "🕳️ LuCI App:      $luci_app_version"
        print_global "📦 Sing-box:      $sing_box_version"
        if [ "$zapret_installed" -eq 1 ]; then
            print_global "🧵 Zapret:        $zapret_version"
        fi
        if [ "$byedpi_installed" -eq 1 ]; then
            print_global "🧵 ByeDPI:        $byedpi_version"
        fi
        print_global "🛜 OpenWrt:       $openwrt_version"
        print_global "🛜 Device:        $device_model"
    else
        print_global "❌ Failed to get system info"
    fi

    print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    print_global "➡️ DNS status"

    local dns_check_json
    dns_check_json=$(check_dns_available)

    if [ -n "$dns_check_json" ]; then
        local dns_type dns_server dns_status dns_on_router bootstrap_dns_server bootstrap_dns_status dhcp_config_status

        dns_type=$(json_get_default "$dns_check_json" dns_type "unknown")
        dns_server=$(json_get_default "$dns_check_json" dns_server "unknown")
        dns_status=$(json_get_default "$dns_check_json" dns_status 0)
        dns_on_router=$(json_get_default "$dns_check_json" dns_on_router 0)
        bootstrap_dns_server=$(json_get_default "$dns_check_json" bootstrap_dns_server "")
        bootstrap_dns_status=$(json_get_default "$dns_check_json" bootstrap_dns_status 0)
        dhcp_config_status=$(json_get_default "$dns_check_json" dhcp_config_status 0)

        # Bootstrap DNS
        if [ -n "$bootstrap_dns_server" ]; then
            if [ "$bootstrap_dns_status" -eq 1 ]; then
                print_global "✅ Bootstrap DNS: $bootstrap_dns_server"
            else
                print_global "❌ Bootstrap DNS: $bootstrap_dns_server"
            fi
        fi

        # DNS server status
        if [ "$dns_status" -eq 1 ]; then
            print_global "✅ Main DNS: $dns_server [$dns_type]"
        else
            print_global "❌ Main DNS: $dns_server [$dns_type]"
        fi

        # DNS on router
        if [ "$dns_on_router" -eq 1 ]; then
            print_global "✅ DNS on router"
        else
            print_global "❌ DNS on router"
        fi

        # DHCP configuration check
        local dont_touch_dhcp
        config_get dont_touch_dhcp "settings" "dont_touch_dhcp"

        if [ "$dont_touch_dhcp" = "1" ]; then
            print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:"
            awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
        elif [ "$dhcp_config_status" -eq 0 ]; then
            print_global "❌ DHCP configuration differs from template. 📄 DHCP config:"
            awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
        else
            print_global "✅ /etc/config/dhcp"
        fi
    else
        print_global "❌ Failed to get DNS info"
    fi

    print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    print_global "📦 Sing-box status"

    local singbox_check_json
    singbox_check_json=$(check_sing_box)

    if [ -n "$singbox_check_json" ]; then
        local sing_box_installed sing_box_version_ok sing_box_extended sing_box_service_exist sing_box_autostart_disabled sing_box_process_running sing_box_ports_listening

        sing_box_installed=$(json_get_default "$singbox_check_json" sing_box_installed 0)
        sing_box_version_ok=$(json_get_default "$singbox_check_json" sing_box_version_ok 0)
        sing_box_extended=$(json_get_default "$singbox_check_json" sing_box_extended 0)
        sing_box_service_exist=$(json_get_default "$singbox_check_json" sing_box_service_exist 0)
        sing_box_autostart_disabled=$(json_get_default "$singbox_check_json" sing_box_autostart_disabled 0)
        sing_box_process_running=$(json_get_default "$singbox_check_json" sing_box_process_running 0)
        sing_box_ports_listening=$(json_get_default "$singbox_check_json" sing_box_ports_listening 0)

        if [ "$sing_box_installed" -eq 1 ]; then
            print_global "✅ Sing-box installed"
        else
            print_global "❌ Sing-box installed"
        fi

        if [ "$sing_box_version_ok" -eq 1 ]; then
            print_global "✅ Sing-box version is compatible (newer than 1.12.4)"
        else
            print_global "❌ Sing-box version is not compatible (older than 1.12.4)"
        fi

        if [ "$sing_box_extended" -eq 1 ]; then
            print_global "Sing-box extended detected"
        else
            print_global "Sing-box regular build detected"
        fi

        if [ "$sing_box_service_exist" -eq 1 ]; then
            print_global "✅ Sing-box service exist"
        else
            print_global "❌ Sing-box service exist"
        fi

        if [ "$sing_box_autostart_disabled" -eq 1 ]; then
            print_global "✅ Sing-box autostart disabled"
        else
            print_global "❌ Sing-box autostart disabled"
        fi

        if [ "$sing_box_process_running" -eq 1 ]; then
            print_global "✅ Sing-box process running"
        else
            print_global "❌ Sing-box process running"
        fi

        if [ "$sing_box_ports_listening" -eq 1 ]; then
            print_global "✅ Sing-box listening ports"
        else
            print_global "❌ Sing-box listening ports"
        fi
    else
        print_global "❌ Failed to get sing-box info"
    fi

    local zapret_check_json
    zapret_check_json=$(check_zapret_runtime)

    if [ -n "$zapret_check_json" ] && [ "$(json_get_default "$zapret_check_json" zapret_installed 0)" = "1" ]; then
        print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        print_global "🧵 Zapret status"
        print_global "✅ Zapret provider available"
    fi

    local byedpi_check_json
    byedpi_check_json=$(check_byedpi_runtime)

    if [ -n "$byedpi_check_json" ] && [ "$(json_get_default "$byedpi_check_json" byedpi_installed 0)" = "1" ]; then
        print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        print_global "🧵 ByeDPI status"
        print_global "✅ ByeDPI provider available"
    fi

    print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    print_global "🧱 NFT rules status"

    local nft_check_json
    nft_check_json=$(check_nft_rules)

    if [ -n "$nft_check_json" ]; then
        local table_exist rules_mangle_exist rules_mangle_counters rules_mangle_output_exist rules_mangle_output_counters rules_proxy_exist rules_proxy_counters rules_other_mark_exist

        table_exist=$(json_get_default "$nft_check_json" table_exist 0)
        rules_mangle_exist=$(json_get_default "$nft_check_json" rules_mangle_exist 0)
        rules_mangle_counters=$(json_get_default "$nft_check_json" rules_mangle_counters 0)
        rules_mangle_output_exist=$(json_get_default "$nft_check_json" rules_mangle_output_exist 0)
        rules_mangle_output_counters=$(json_get_default "$nft_check_json" rules_mangle_output_counters 0)
        rules_proxy_exist=$(json_get_default "$nft_check_json" rules_proxy_exist 0)
        rules_proxy_counters=$(json_get_default "$nft_check_json" rules_proxy_counters 0)
        rules_other_mark_exist=$(json_get_default "$nft_check_json" rules_other_mark_exist 0)

        if [ "$table_exist" -eq 1 ]; then
            print_global "✅ Table exist"
        else
            print_global "❌ Table exist"
        fi

        if [ "$rules_mangle_exist" -eq 1 ]; then
            print_global "✅ Rules mangle exist"
        else
            print_global "❌ Rules mangle exist"
        fi

        if [ "$rules_mangle_counters" -eq 1 ]; then
            print_global "✅ Rules mangle counters"
        else
            print_global "⚠️  Rules mangle counters"
        fi

        if [ "$rules_mangle_output_exist" -eq 1 ]; then
            print_global "✅ Rules mangle output exist"
        else
            print_global "❌ Rules mangle output exist"
        fi

        if [ "$rules_mangle_output_counters" -eq 1 ]; then
            print_global "✅ Rules mangle output counters"
        else
            print_global "⚠️  Rules mangle output counters"
        fi

        if [ "$rules_proxy_exist" -eq 1 ]; then
            print_global "✅ Rules proxy exist"
        else
            print_global "❌ Rules proxy exist"
        fi

        if [ "$rules_proxy_counters" -eq 1 ]; then
            print_global "✅ Rules proxy counters"
        else
            print_global "⚠️  Rules proxy counters"
        fi

        if [ "$rules_other_mark_exist" -eq 1 ]; then
            print_global "⚠️  Additional marking rules found:"
            nft list ruleset | awk '/table inet '"$NFT_TABLE_NAME"'/{flag=1; next} /^table/{flag=0} !flag' | grep -E "mark set|meta mark"
        else
            print_global "✅ Additional marking rules found"
        fi
    else
        print_global "❌ Failed to get NFT rules info"
    fi

    print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    print_global "📄 Podkop Plus config"
    show_config

    # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    # print_global "🔧 System check"

    # if grep -E "^nameserver\s+([0-9]{1,3}\.){3}[0-9]{1,3}" "$RESOLV_CONF" | grep -vqE "127\.0\.0\.1|0\.0\.0\.0"; then
    #     print_global "❌ /etc/resolv.conf contains external nameserver:"
    #     cat /etc/resolv.conf
    #     echo ""
    # else
    #     print_global "✅ /etc/resolv.conf"
    # fi

    # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    # print_global "🧱 NFT table"
    # check_nft

    print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    print_global "📄 WAN config"
    if uci show network.wan > /dev/null 2>&1; then
        awk '
            /^config / {
                p = ($2 == "interface" && $3 == "'\''wan'\''")
                proto = ""
            }
            p {
                if ($1 == "option" && $2 == "proto") {
                    proto = $3
                    print
                } else if (proto == "'\''static'\''" && $1 == "option" && ($2 == "ipaddr" || $2 == "netmask" || $2 == "gateway")) {
                    print "        option", $2, "'\''******'\''"
                } else if (proto == "'\''pppoe'\''" && $1 == "option" && ($2 == "username" || $2 == "password")) {
                    print "        option", $2, "'\''******'\''"
                } else {
                    print
                }
            }
        ' /etc/config/network
    else
        print_global "❌ WAN configuration not found"
    fi

    if uci show network | grep -q endpoint_host; then
        uci show network | grep endpoint_host | cut -d'=' -f2 | tr -d "'\" " | while read -r host; do
            if [ "$host" = "engage.cloudflareclient.com" ]; then
                print_global "⚠️ WARP detected: $host"
                continue
            fi

            ip_prefix=$(echo "$host" | cut -d'.' -f1,2)
            if echo "$CLOUDFLARE_OCTETS" | grep -wq "$ip_prefix"; then
                print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
                print_global "⚠️ WARP detected: $host"
            fi
        done
    fi

    if uci show network | grep -q route_allowed_ips; then
        uci show network | grep "wireguard_.*\.route_allowed_ips='1'" | cut -d'.' -f1-2 | while read -r peer_section; do
            local allowed_ips
            allowed_ips=$(uci get "${peer_section}.allowed_ips" 2> /dev/null)

            if [ "$allowed_ips" = "0.0.0.0/0" ]; then
                print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
                print_global "⚠️ WG Route allowed IP enabled with 0.0.0.0/0"
            fi
        done
    fi

    if [ -x /etc/init.d/zapret ] && /etc/init.d/zapret status >/dev/null 2>&1; then
        print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        print_global "⚠️ Standalone zapret service is active. Podkop Plus uses separate queues, but packet-level policy overlap is possible."
    elif [ -x /etc/init.d/zapret ] && /etc/init.d/zapret enabled >/dev/null 2>&1; then
        print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        print_global "⚠️ Standalone zapret autostart is enabled. Podkop Plus will not modify /etc/config/zapret."
    fi

    print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    print_global "🥸 FakeIP status"

    local fakeip_check_json
    fakeip_check_json=$(check_fakeip)

    if [ -n "$fakeip_check_json" ]; then
        local fakeip_status fakeip_address

        fakeip_status=$(json_get_default "$fakeip_check_json" fakeip false)
        fakeip_address=$(json_get_default "$fakeip_check_json" IP "")

        if [ "$fakeip_status" = "true" ]; then
            print_global "✅ Sing-box FakeIP DNS works: $fakeip_address"
        else
            print_global "❌ Sing-box FakeIP DNS does NOT work"
        fi
    else
        print_global "❌ Failed to get FakeIP info"
    fi
}

show_help() {
    cat << EOF
Usage: $0 COMMAND

Available commands:
    start                   Start Podkop Plus service
    stop                    Stop Podkop Plus service
    reload                  Reload Podkop Plus configuration
    restart                 Restart Podkop Plus service
    enable                  Enable Podkop Plus autostart
    disable                 Disable Podkop Plus autostart
    uninstall               Remove Podkop Plus files installed outside opkg/apk
    main                    Run main Podkop Plus process
    list_update             Update remote lists
    list_update_if_due      Update remote lists only when the configured interval has elapsed
    subscription_update [rule] [source_index]
                            Update all subscription proxies, one rule, or one source in a rule
    subscription_update_if_due
                            Update subscription proxies only when per-rule intervals have elapsed
    check_proxy             Check proxy connectivity
    check_nft               Check NFT rules
    check_nft_rules         Check NFT rules status
    check_sing_box          Check sing-box installation and status
    check_logs              Show Podkop Plus logs from system journal
    check_sing_box_logs     Show sing-box logs
    check_fakeip            Test sing-box FakeIP DNS
    check_zapret_runtime    Check external zapret provider state
    check_byedpi_runtime    Check external ByeDPI provider state
    neutralize_zapret_defaults
                            Compatibility no-op; Podkop Plus does not manage standalone zapret
    clash_api               Clash API interface for managing proxies and groups
    show_config             Display current Podkop Plus configuration
    show_version            Show Podkop Plus version
    show_sing_box_config    Show sing-box configuration
    show_sing_box_version   Show sing-box version
    get_status              Get Podkop Plus service status
    get_outbound_link       Get source proxy link for a generated outbound
    get_outbound_metadata   Get generated outbound display names and countries
    get_subscription_metadata
                            Get UI-only metadata for a subscription rule
    get_sing_box_status     Get sing-box service status
    get_zapret_status       Get zapret provider and Podkop Plus-managed nfqws status
    get_byedpi_status       Get ByeDPI provider and Podkop Plus-managed ciadpi status
    get_system_info         Get system information in JSON format
    generate_reality_keypair
                            Generate a sing-box Reality key pair in JSON format
    component_action        Manage Podkop Plus, sing-box, zapret, and ByeDPI components
    component_action_async  Start a component action in the background
    component_action_status Get background component action status
    check_dns_available     Check DNS server availability
    global_check            Run global system check
    validate_nfqws_strategy_json
                            Validate an NFQWS strategy and print JSON result
    validate_byedpi_strategy_json
                            Validate a ByeDPI ciadpi strategy and print JSON result
EOF
}

case "$1" in
start)
    start
    ;;
stop)
    stop
    ;;
reload)
    reload "$2"
    ;;
restart)
    restart
    ;;
enable)
    "$PODKOP_SERVICE_INIT" enable
    ;;
disable)
    "$PODKOP_SERVICE_INIT" disable
    ;;
uninstall)
    uninstall
    ;;
main)
    main
    ;;
list_update)
    list_update
    ;;
list_update_if_due)
    list_update_if_due
    ;;
subscription_update)
    subscription_update "$2" "$3"
    ;;
subscription_update_if_due)
    subscription_update_if_due
    ;;
check_proxy)
    check_proxy
    ;;
check_nft)
    check_nft
    ;;
check_nft_rules)
    check_nft_rules
    ;;
check_sing_box)
    check_sing_box
    ;;
check_logs)
    check_logs
    ;;
check_sing_box_logs)
    check_sing_box_logs
    ;;
check_fakeip)
    check_fakeip
    ;;
check_zapret_runtime)
    check_zapret_runtime
    ;;
check_byedpi_runtime)
    check_byedpi_runtime
    ;;
neutralize_zapret_defaults)
    neutralize_zapret_defaults
    ;;
clash_api)
    clash_api "$2" "$3" "$4"
    ;;
show_config)
    show_config
    ;;
show_version)
    show_version
    ;;
show_sing_box_config)
    show_sing_box_config
    ;;
show_sing_box_version)
    show_sing_box_version
    ;;
get_status)
    get_status
    ;;
get_outbound_link)
    get_outbound_link "$2" "$3"
    ;;
get_outbound_link_states)
    get_outbound_link_states "$2"
    ;;
get_outbound_metadata)
    get_outbound_metadata "$2"
    ;;
get_subscription_metadata)
    get_subscription_metadata "$2"
    ;;
get_sing_box_status)
    get_sing_box_status
    ;;
get_zapret_status)
    get_zapret_status
    ;;
get_byedpi_status)
    get_byedpi_status
    ;;
get_system_info)
    get_system_info
    ;;
generate_reality_keypair)
    generate_reality_keypair
    ;;
component_action)
    component_action "$2" "$3"
    ;;
component_action_async)
    component_action_async "$2" "$3"
    ;;
component_action_status)
    component_action_status "$2"
    ;;
check_dns_available)
    check_dns_available
    ;;
global_check)
    global_check "${2:-}"
    ;;
validate_nfqws_strategy_json)
    validate_nfqws_strategy_json "${2:-}"
    ;;
validate_byedpi_strategy_json)
    validate_byedpi_strategy_json "${2:-}"
    ;;
*)
    show_help
    exit 1
    ;;
esac
