#!/bin/sh
# UCI to shell config generator for zapret2
# Reads /etc/config/zapret2 and generates /opt/zapret2/config

ZAPRET_BASE="${ZAPRET_BASE:-/opt/zapret2}"
CONFIG_FILE="$ZAPRET_BASE/config"
CONFIG_TEMPLATE="$ZAPRET_BASE/config.template"
LOCK_FILE="/var/lock/zapret2_config.lock"

# Sanitize value - remove dangerous shell characters
# Only allow: alphanumeric, space, comma, dot, hyphen, underscore, colon, equals, slash, newline
# Note: hyphen must be at the end of character class to avoid being interpreted as range
sanitize_value() {
    printf '%s' "$1" | tr -cd 'a-zA-Z0-9 ,.:=/\n_-'
}

# Validate hex value (for marks)
validate_hex() {
    printf '%s' "$1" | grep -qE '^0x[0-9a-fA-F]+$'
}

# Validate integer
validate_int() {
    case "$1" in
        ''|*[!0-9]*) return 1 ;;
        *) return 0 ;;
    esac
}

# Validate port list (comma-separated integers)
validate_ports() {
    printf '%s' "$1" | grep -qE '^[0-9]+(,[0-9]+)*$'
}

# Acquire lock to prevent race conditions
exec 200>"$LOCK_FILE"
flock -n 200 || {
    echo "Another uci2config instance is running, waiting..."
    flock 200
}

# Check if UCI config exists
[ -f /etc/config/zapret2 ] || {
    echo "No UCI config found, using default config"
    exit 0
}

. /lib/functions.sh
config_load zapret2

# Read main section with validation
config_get_bool ENABLED main enabled 1
config_get_bool DEBUG main debug 0
config_get_bool CUSTOM_SCRIPTS main custom_scripts 1

config_get QNUM main qnum 300
validate_int "$QNUM" || QNUM=300
[ "$QNUM" -ge 0 ] && [ "$QNUM" -le 65535 ] || QNUM=300

config_get DESYNC_MARK main desync_mark "0x40000000"
validate_hex "$DESYNC_MARK" || DESYNC_MARK="0x40000000"

config_get DESYNC_MARK_POSTNAT main desync_mark_postnat "0x20000000"
validate_hex "$DESYNC_MARK_POSTNAT" || DESYNC_MARK_POSTNAT="0x20000000"

config_get WS_USER main ws_user "daemon"
WS_USER=$(sanitize_value "$WS_USER")
[ -z "$WS_USER" ] && WS_USER="daemon"

config_get NFQWS_PORTS_TCP main nfqws_ports_tcp "80,443"
validate_ports "$NFQWS_PORTS_TCP" || NFQWS_PORTS_TCP="80,443"

config_get NFQWS_PORTS_UDP main nfqws_ports_udp "443"
validate_ports "$NFQWS_PORTS_UDP" || NFQWS_PORTS_UDP="443"

config_get NFQWS_TCP_PKT_OUT main nfqws_tcp_pkt_out "25"
validate_int "$NFQWS_TCP_PKT_OUT" || NFQWS_TCP_PKT_OUT="25"
[ "$NFQWS_TCP_PKT_OUT" -ge 1 ] && [ "$NFQWS_TCP_PKT_OUT" -le 1000 ] || NFQWS_TCP_PKT_OUT="25"

config_get NFQWS_TCP_PKT_IN main nfqws_tcp_pkt_in "5"
validate_int "$NFQWS_TCP_PKT_IN" || NFQWS_TCP_PKT_IN="5"
[ "$NFQWS_TCP_PKT_IN" -ge 0 ] && [ "$NFQWS_TCP_PKT_IN" -le 1000 ] || NFQWS_TCP_PKT_IN="5"

config_get FILTER_MARK main filter_mark ""
[ -n "$FILTER_MARK" ] && { validate_hex "$FILTER_MARK" || FILTER_MARK=""; }

config_get_bool POSTNAT main postnat 1

config_get_bool ENABLE_IPV6 main enable_ipv6 0

# Autohostlist settings with validation
config_get_bool AUTOHOSTLIST_ENABLED main autohostlist_enabled 0

config_get AUTOHOSTLIST_FILE main autohostlist_file "$ZAPRET_BASE/ipset/zapret_hosts_auto.txt"
AUTOHOSTLIST_FILE=$(sanitize_value "$AUTOHOSTLIST_FILE")

config_get AUTOHOSTLIST_FAIL_THRESHOLD main autohostlist_fail_threshold 3
validate_int "$AUTOHOSTLIST_FAIL_THRESHOLD" || AUTOHOSTLIST_FAIL_THRESHOLD=3
[ "$AUTOHOSTLIST_FAIL_THRESHOLD" -ge 1 ] && [ "$AUTOHOSTLIST_FAIL_THRESHOLD" -le 100 ] || AUTOHOSTLIST_FAIL_THRESHOLD=3

config_get AUTOHOSTLIST_FAIL_TIME main autohostlist_fail_time 60
validate_int "$AUTOHOSTLIST_FAIL_TIME" || AUTOHOSTLIST_FAIL_TIME=60
[ "$AUTOHOSTLIST_FAIL_TIME" -ge 1 ] && [ "$AUTOHOSTLIST_FAIL_TIME" -le 3600 ] || AUTOHOSTLIST_FAIL_TIME=60

config_get AUTOHOSTLIST_RETRANS_THRESHOLD main autohostlist_retrans_threshold 3
validate_int "$AUTOHOSTLIST_RETRANS_THRESHOLD" || AUTOHOSTLIST_RETRANS_THRESHOLD=3
[ "$AUTOHOSTLIST_RETRANS_THRESHOLD" -ge 1 ] && [ "$AUTOHOSTLIST_RETRANS_THRESHOLD" -le 100 ] || AUTOHOSTLIST_RETRANS_THRESHOLD=3

config_get_bool AUTOHOSTLIST_DEBUGLOG main autohostlist_debuglog 0

# Count enabled strategies
STRATEGY_COUNT=0
_count_strategy() {
    local enabled
    config_get_bool enabled "$1" enabled 1
    [ "$enabled" = "1" ] && STRATEGY_COUNT=$((STRATEGY_COUNT + 1))
}
config_foreach _count_strategy strategy

# Generate NFQWS2_OPT from strategies
# Lua garbage collector interval (from UCI, default 600 seconds)
config_get lua_gc main lua_gc "600"
[ -n "$lua_gc" ] && [ "$lua_gc" != "0" ] && NFQWS2_OPT="--lua-gc=$lua_gc" || NFQWS2_OPT=""

# Connection tracking timeouts (SYN:EST:FIN) - required for orchestration
config_get ctrack_timeouts main ctrack_timeouts ""
[ -n "$ctrack_timeouts" ] && NFQWS2_OPT="$NFQWS2_OPT --ctrack-timeouts=$ctrack_timeouts"

STRATEGY_NUM=0

_process_blob() {
    local section="$1"
    local path enabled
    config_get_bool enabled "$section" enabled 0
    [ "$enabled" = "1" ] || return
    config_get path "$section" path
    [ -n "$path" ] && [ -f "$path" ] && {
        NFQWS2_OPT="$NFQWS2_OPT --blob=$section:@$path"
    }
}

_process_strategy() {
    local section="$1"
    local enabled port script
    local filter_opts="" hostlist_opts="" exclude_opts="" autohostlist_opts=""

    config_get_bool enabled "$section" enabled 1
    [ "$enabled" = "0" ] && return

    config_get port "$section" port "443"
    validate_ports "$port" || port="443"

    config_get script "$section" script ""

    # Normalize script - only normalize whitespace, no character filtering
    [ -n "$script" ] && {
        # Normalize whitespace (newlines to spaces, collapse multiple spaces)
        script=$(printf '%s' "$script" | tr '\n' ' ' | sed 's/\\[[:space:]]*/ /g; s/[[:space:]]\+/ /g; s/^[[:space:]]*//; s/[[:space:]]*$//')
    }

    # Protocol filter (single value: tcp or udp)
    local protocol
    config_get protocol "$section" protocol "tcp"
    # Validate protocol - only tcp or udp allowed
    case "$protocol" in
        tcp|udp) ;;
        *) protocol="tcp" ;;
    esac
    filter_opts="--filter-${protocol}=${port}"

    # L3 filters (comma-separated: ipv4, ipv6)
    local l3_list=""
    _add_l3() {
        local l3="$1"
        case "$l3" in
            ipv4|ipv6)
                [ -n "$l3_list" ] && l3_list="$l3_list,$l3" || l3_list="$l3"
                ;;
        esac
    }
    config_list_foreach "$section" filter_l3 _add_l3
    [ -z "$l3_list" ] && {
        local filter_l3_str=""
        config_get filter_l3_str "$section" filter_l3 ""
        [ -n "$filter_l3_str" ] && _add_l3 "$filter_l3_str"
    }
    [ -n "$l3_list" ] && filter_opts="$filter_opts --filter-l3=$l3_list"

    # L7 filters (comma-separated) - validate each value
    # Handles both string option (single value) and list option (multiple values)
    local l7_list=""
    _add_l7() {
        local l7="$1"
        # Only allow known L7 protocols
        case "$l7" in
            tls|http|quic|dns|stun|wireguard|dht|discord|xmpp|mtproto)
                [ -n "$l7_list" ] && l7_list="$l7_list,$l7" || l7_list="$l7"
                ;;
        esac
    }
    # First try list option
    config_list_foreach "$section" filter_l7 _add_l7
    # If list was empty, try string option
    [ -z "$l7_list" ] && {
        local filter_l7_str=""
        config_get filter_l7_str "$section" filter_l7 ""
        [ -n "$filter_l7_str" ] && _add_l7 "$filter_l7_str"
    }
    [ -n "$l7_list" ] && filter_opts="$filter_opts --filter-l7=$l7_list"

    # Hostlists - validate paths
    # Handles both string option (single value) and list option (multiple values)
    local hostlist_processed=0
    _add_hostlist() {
        local list_name="$1" list_path opt_name
        config_get list_path "$list_name" path
        [ -n "$list_path" ] || return
        # Sanitize path and check for path traversal
        list_path=$(sanitize_value "$list_path")
        case "$list_path" in
            *../*|*..*) return ;; # Reject path traversal
        esac
        case "$list_name" in
            *exclude*) opt_name="--hostlist-exclude" ;;
            *) opt_name="--hostlist" ;;
        esac
        if [ -f "${list_path}.gz" ]; then
            hostlist_opts="$hostlist_opts ${opt_name}=${list_path}.gz"
            hostlist_processed=1
        elif [ -f "$list_path" ]; then
            hostlist_opts="$hostlist_opts ${opt_name}=$list_path"
            hostlist_processed=1
        fi
    }
    # First try list option
    config_list_foreach "$section" hostlist _add_hostlist
    # If list was empty, try string option
    [ "$hostlist_processed" = "0" ] && {
        local hostlist_str=""
        config_get hostlist_str "$section" hostlist ""
        [ -n "$hostlist_str" ] && _add_hostlist "$hostlist_str"
    }

    # Exclude hostlists - validate paths
    # Handles both string option (single value) and list option (multiple values)
    local exclude_processed=0
    _add_exclude() {
        local list_name="$1" list_path
        config_get list_path "$list_name" path
        [ -n "$list_path" ] || return
        list_path=$(sanitize_value "$list_path")
        case "$list_path" in *../*|*..*) return ;; esac
        if [ -f "${list_path}.gz" ]; then
            exclude_opts="$exclude_opts --hostlist-exclude=${list_path}.gz"
            exclude_processed=1
        elif [ -f "$list_path" ]; then
            exclude_opts="$exclude_opts --hostlist-exclude=$list_path"
            exclude_processed=1
        fi
    }
    # First try list option
    config_list_foreach "$section" hostlist_exclude _add_exclude
    # If list was empty, try string option
    [ "$exclude_processed" = "0" ] && {
        local exclude_str=""
        config_get exclude_str "$section" hostlist_exclude ""
        [ -n "$exclude_str" ] && _add_exclude "$exclude_str"
    }

    # IP sets - validate paths
    local ipset_opts=""
    _add_ipset() {
        local list_name="$1" list_path
        config_get list_path "$list_name" path
        [ -n "$list_path" ] || return
        list_path=$(sanitize_value "$list_path")
        case "$list_path" in *../*|*..*) return ;; esac
        if [ -f "${list_path}.gz" ]; then
            ipset_opts="$ipset_opts --ipset=${list_path}.gz"
        elif [ -f "$list_path" ]; then
            ipset_opts="$ipset_opts --ipset=$list_path"
        fi
    }
    config_list_foreach "$section" ipset _add_ipset

    # Exclude IP sets - validate paths
    local ipset_exclude_opts=""
    _add_ipset_exclude() {
        local list_name="$1" list_path
        config_get list_path "$list_name" path
        [ -n "$list_path" ] || return
        list_path=$(sanitize_value "$list_path")
        case "$list_path" in *../*|*..*) return ;; esac
        if [ -f "${list_path}.gz" ]; then
            ipset_exclude_opts="$ipset_exclude_opts --ipset-exclude=${list_path}.gz"
        elif [ -f "$list_path" ]; then
            ipset_exclude_opts="$ipset_exclude_opts --ipset-exclude=$list_path"
        fi
    }
    config_list_foreach "$section" ipset_exclude _add_ipset_exclude

    # Autohostlist - per-strategy setting
    local strategy_autohostlist strategy_autohostlist_debuglog
    config_get_bool strategy_autohostlist "$section" autohostlist 0
    [ "$strategy_autohostlist" = "1" ] && {
        autohostlist_opts="--hostlist-auto=$ZAPRET_BASE/ipset/zapret_hosts_auto.txt"
        # Add debug log if enabled per-strategy
        config_get_bool strategy_autohostlist_debuglog "$section" autohostlist_debuglog 0
        [ "$strategy_autohostlist_debuglog" = "1" ] && {
            autohostlist_opts="$autohostlist_opts --hostlist-auto-debug=$ZAPRET_BASE/ipset/zapret_hosts_auto_debug.log"
        }
    }

    # Combine
    local strategy_opts="--name=$section $filter_opts $hostlist_opts $exclude_opts $ipset_opts $ipset_exclude_opts $autohostlist_opts"
    [ -n "$script" ] && strategy_opts="$strategy_opts $script"

    # Add --new separator between strategies
    [ "$STRATEGY_NUM" -gt 0 ] && NFQWS2_OPT="$NFQWS2_OPT --new"
    NFQWS2_OPT="$NFQWS2_OPT $strategy_opts"
    STRATEGY_NUM=$((STRATEGY_NUM + 1))
}

# Process blobs first
config_foreach _process_blob blob

# Process strategies
config_foreach _process_strategy strategy

# Backup original config if exists and is template
[ -f "$CONFIG_FILE" ] && [ ! -f "$CONFIG_TEMPLATE" ] && {
    cp "$CONFIG_FILE" "$CONFIG_TEMPLATE"
}

# Read template or create minimal config
if [ -f "$CONFIG_TEMPLATE" ]; then
    # Create backup before modifying
    cp "$CONFIG_FILE" "${CONFIG_FILE}.bak" 2>/dev/null

    # Use template as base, override specific values
    cp "$CONFIG_TEMPLATE" "$CONFIG_FILE"

    # Update values in config
    sed -i "s/^NFQWS2_ENABLE=.*/NFQWS2_ENABLE=$ENABLED/" "$CONFIG_FILE"

    # Update or add QNUM
    if grep -q "^#*QNUM=" "$CONFIG_FILE"; then
        sed -i "s/^#*QNUM=.*/QNUM=$QNUM/" "$CONFIG_FILE"
    else
        echo "QNUM=$QNUM" >> "$CONFIG_FILE"
    fi
    sed -i "s/^DESYNC_MARK=.*/DESYNC_MARK=$DESYNC_MARK/" "$CONFIG_FILE"
    sed -i "s/^DESYNC_MARK_POSTNAT=.*/DESYNC_MARK_POSTNAT=$DESYNC_MARK_POSTNAT/" "$CONFIG_FILE"

    # Update or add WS_USER
    if grep -q "^#*WS_USER=" "$CONFIG_FILE"; then
        sed -i "s/^#*WS_USER=.*/WS_USER=$WS_USER/" "$CONFIG_FILE"
    else
        echo "WS_USER=$WS_USER" >> "$CONFIG_FILE"
    fi

    sed -i "s/^NFQWS2_PORTS_TCP=.*/NFQWS2_PORTS_TCP=$NFQWS_PORTS_TCP/" "$CONFIG_FILE"
    sed -i "s/^NFQWS2_PORTS_UDP=.*/NFQWS2_PORTS_UDP=$NFQWS_PORTS_UDP/" "$CONFIG_FILE"
    # Update NFQWS2_TCP_PKT_OUT (formula with AUTOHOSTLIST_RETRANS_THRESHOLD)
    sed -i "s/^NFQWS2_TCP_PKT_OUT=.*/NFQWS2_TCP_PKT_OUT=\$((${NFQWS_TCP_PKT_OUT}+\$AUTOHOSTLIST_RETRANS_THRESHOLD))/" "$CONFIG_FILE"
    sed -i "s/^NFQWS2_TCP_PKT_IN=.*/NFQWS2_TCP_PKT_IN=$NFQWS_TCP_PKT_IN/" "$CONFIG_FILE"
    sed -i "s/^AUTOHOSTLIST_RETRANS_THRESHOLD=.*/AUTOHOSTLIST_RETRANS_THRESHOLD=$AUTOHOSTLIST_RETRANS_THRESHOLD/" "$CONFIG_FILE"
    sed -i "s/^AUTOHOSTLIST_FAIL_THRESHOLD=.*/AUTOHOSTLIST_FAIL_THRESHOLD=$AUTOHOSTLIST_FAIL_THRESHOLD/" "$CONFIG_FILE"
    sed -i "s/^AUTOHOSTLIST_FAIL_TIME=.*/AUTOHOSTLIST_FAIL_TIME=$AUTOHOSTLIST_FAIL_TIME/" "$CONFIG_FILE"
    sed -i "s/^AUTOHOSTLIST_DEBUGLOG=.*/AUTOHOSTLIST_DEBUGLOG=$AUTOHOSTLIST_DEBUGLOG/" "$CONFIG_FILE"

    # Update or add DAEMON_LOG_ENABLE (from debug option)
    if grep -q "^#*DAEMON_LOG_ENABLE=" "$CONFIG_FILE"; then
        sed -i "s/^#*DAEMON_LOG_ENABLE=.*/DAEMON_LOG_ENABLE=$DEBUG/" "$CONFIG_FILE"
    else
        echo "DAEMON_LOG_ENABLE=$DEBUG" >> "$CONFIG_FILE"
    fi

    # Update POSTNAT
    if grep -q "^#*POSTNAT=" "$CONFIG_FILE"; then
        sed -i "s/^#*POSTNAT=.*/POSTNAT=$POSTNAT/" "$CONFIG_FILE"
    else
        echo "POSTNAT=$POSTNAT" >> "$CONFIG_FILE"
    fi

    # Update DISABLE_IPV6 (inverted from enable_ipv6)
    if [ "$ENABLE_IPV6" = "0" ]; then
        if grep -q "^#*DISABLE_IPV6=" "$CONFIG_FILE"; then
            sed -i "s/^#*DISABLE_IPV6=.*/DISABLE_IPV6=1/" "$CONFIG_FILE"
        else
            echo "DISABLE_IPV6=1" >> "$CONFIG_FILE"
        fi
    else
        if grep -q "^#*DISABLE_IPV6=" "$CONFIG_FILE"; then
            sed -i "s/^#*DISABLE_IPV6=.*/DISABLE_IPV6=0/" "$CONFIG_FILE"
        else
            echo "DISABLE_IPV6=0" >> "$CONFIG_FILE"
        fi
    fi

    # Update DISABLE_CUSTOM (inverted from custom_scripts)
    if [ "$CUSTOM_SCRIPTS" = "0" ]; then
        if grep -q "^#*DISABLE_CUSTOM=" "$CONFIG_FILE"; then
            sed -i "s/^#*DISABLE_CUSTOM=.*/DISABLE_CUSTOM=1/" "$CONFIG_FILE"
        else
            echo "DISABLE_CUSTOM=1" >> "$CONFIG_FILE"
        fi
    else
        # Comment out or remove DISABLE_CUSTOM when custom scripts enabled
        sed -i "s/^DISABLE_CUSTOM=.*/#DISABLE_CUSTOM=0/" "$CONFIG_FILE" 2>/dev/null
    fi

    # Update or add FILTER_MARK (only if set and not empty/whitespace)
    case "$FILTER_MARK" in
        ''|*[[:space:]]*)
            # Empty or whitespace only - comment out if exists
            sed -i "s/^FILTER_MARK=.*/#FILTER_MARK=/" "$CONFIG_FILE" 2>/dev/null
            ;;
        *)
            # Valid value - update or add
            if grep -q "^#*FILTER_MARK=" "$CONFIG_FILE"; then
                sed -i "s/^#*FILTER_MARK=.*/FILTER_MARK=$FILTER_MARK/" "$CONFIG_FILE"
            else
                echo "FILTER_MARK=$FILTER_MARK" >> "$CONFIG_FILE"
            fi
            ;;
    esac

    # Remove old NFQWS2_OPT (can be multiline)
    # First remove single-line version
    sed -i '/^NFQWS2_OPT="[^"]*"$/d' "$CONFIG_FILE"
    # Remove multiline version (starts with NFQWS2_OPT=" and ends with ")
    sed -i '/^NFQWS2_OPT="/,/"$/d' "$CONFIG_FILE"
    # Remove any remaining lines with <HOSTLIST> placeholders
    sed -i '/<HOSTLIST/d' "$CONFIG_FILE"
fi

# Append generated NFQWS2_OPT
[ -n "$NFQWS2_OPT" ] && [ "$STRATEGY_COUNT" -gt 0 ] && {
    # Escape all shell metacharacters for security: \ " $ `
    NFQWS2_OPT_ESCAPED=$(printf '%s' "$NFQWS2_OPT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/\\$/g; s/`/\\`/g')
    echo "" >> "$CONFIG_FILE"
    echo "# Generated from UCI config" >> "$CONFIG_FILE"
    echo "NFQWS2_OPT=\"$NFQWS2_OPT_ESCAPED\"" >> "$CONFIG_FILE"
}

# Process custom scripts - generate DISABLE_<SCRIPTNAME> variables
# First remove old DISABLE_ variables (except DISABLE_CUSTOM)
sed -i '/^DISABLE_[A-Z_]*=.*$/{ /^DISABLE_CUSTOM=/!d; }' "$CONFIG_FILE"

_process_script() {
    local section="$1"
    local enabled path script_name script_var

    config_get_bool enabled "$section" enabled 1
    config_get path "$section" path

    # Extract script name from path: /opt/zapret2/init.d/openwrt/custom.d/50-discord_media.sh -> discord_media
    [ -n "$path" ] && {
        script_name="$(basename "$path" .sh)"
        # Remove order prefix (e.g. "50-")
        script_name="${script_name#*-}"
        # Convert to uppercase and replace - with _
        script_var="DISABLE_$(echo "$script_name" | tr 'a-z-' 'A-Z_')"

        # Only write DISABLE_<SCRIPTNAME>=1 if script is disabled
        [ "$enabled" = "0" ] && {
            echo "$script_var=1" >> "$CONFIG_FILE"
        }
    }
}
config_foreach _process_script script

echo "Config generated: $CONFIG_FILE"
echo "Strategies: $STRATEGY_COUNT"
