#!/bin/bash
### BEGIN INIT INFO
# Provides:          ipset
# Required-Start:    $local_fs $network
# Required-Stop:     $local_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Atomic restore/swap of ipset from /etc/ipset.d/*.conf
### END INIT INFO

IPSET='/sbin/ipset'
IPSET_DIR='/etc/ipset.d'
[ -x "$IPSET" ] || exit 1

# LSB logging (if not available, define simple ones)
if [ -r "/lib/lsb/init-functions" ]; then
    . /lib/lsb/init-functions
else
    log_success_msg() { echo "[ OK ] $*"; }
    log_failure_msg() { echo "[FAIL] $*" >&2; }
    log_warning_msg() { echo "[WARN] $*" >&2; }
fi

# Get base set name from .conf file (filename without .conf)
get_set_name() {
    local conf="$1"
    basename "$conf" .conf
}

# Create temporary file with swapped set name (name -> name_new)
create_temp_ipset_file() {
    local name="$1"
    local conf="$IPSET_DIR/$name.conf"
    local tmpfile="$IPSET_DIR/$name.ipset"

    # Replace all occurrences of 'name' as a whole word with 'name_new'
    # This handles lines: "create name hash:ip ..." -> "create name_new hash:ip ..."
    # and "add name 1.2.3.4 ..." -> "add name_new 1.2.3.4 ..."
    sed -E "
        s/^([[:space:]]*)(create|add)[[:space:]]+$name([[:space:]]|$)/\1\2 ${name}_new\3/g;
        s/^([[:space:]]*)(create|add)[[:space:]]+$name([[:space:]]|$)/\1\2 ${name}_new\3/g
    " "$conf" > "$tmpfile"

    echo "$tmpfile"
}

# Load entries into a set from a file (which may be a full ipset save format)
load_from_file() {
    local setname="$1"
    local file="$2"
    "$IPSET" restore -! < "$file" 2>/dev/null
}

# Atomically replace live set with new configuration
replace_atomic() {
    local name="$1"
    local conf="$IPSET_DIR/$name.conf"
    local tmpfile

    # 1. Create temporary ipset file with name_new
    tmpfile=$(create_temp_ipset_file "$name")
    if [ ! -s "$tmpfile" ]; then
        log_failure_msg "Failed to create temporary ipset file for $name"
        rm -f "$tmpfile"
        return 1
    fi

    # 2. Create temporary ipset set (if already exists, destroy it first)
    if "$IPSET" list "${name}_new" &>/dev/null; then
        "$IPSET" destroy "${name}_new" 2>/dev/null
    fi
    # Extract type from conf to create empty set (necessary before restore)
    local type=$(sed -n -E 's/^create\s+'"$name"'\s+(\S+).*/\1/p' "$conf" | head -1)
    if [ -z "$type" ]; then
        log_failure_msg "Cannot determine type for $name from $conf"
        rm -f "$tmpfile"
        return 1
    fi
    if ! "$IPSET" create "${name}_new" "$type" comment 2>/dev/null; then
        log_failure_msg "Cannot create temporary set ${name}_new"
        rm -f "$tmpfile"
        return 1
    fi

    # 3. Load data into temporary set
    if ! load_from_file "${name}_new" "$tmpfile"; then
        log_failure_msg "Failed to load data into ${name}_new"
        "$IPSET" destroy "${name}_new" 2>/dev/null
        rm -f "$tmpfile"
        return 1
    fi

    # 4. Atomic swap
    if ! "$IPSET" swap "${name}_new" "$name"; then
        log_failure_msg "Atomic swap failed for $name"
        "$IPSET" destroy "${name}_new" 2>/dev/null
        rm -f "$tmpfile"
        return 1
    fi

    # 5. Destroy old set (now named _new) and remove temp file
    "$IPSET" destroy "${name}_new" 2>/dev/null
    rm -f "$tmpfile"
    log_success_msg "Atomically replaced ipset $name"
    return 0
}

# Simple restore (when set does not exist)
restore_simple() {
    local name="$1"
    local conf="$IPSET_DIR/$name.conf"
    if "$IPSET" restore -! < "$conf" 2>/dev/null; then
        log_success_msg "Restored ipset $name from $conf"
        return 0
    else
        log_failure_msg "Failed to restore $name from $conf"
        return 1
    fi
}

start_ipset() {
    local ret=0
    for conf in "$IPSET_DIR"/*.conf; do
        [ -f "$conf" ] || continue
        name=$(get_set_name "$conf")
        [ -z "$name" ] && continue

        if "$IPSET" list "$name" &>/dev/null; then
            # Set already exists → atomic replace
            replace_atomic "$name" || ret=1
        else
            # Set does not exist → simple restore
            restore_simple "$name" || ret=1
        fi
    done
    return $ret
}

stop_ipset() {
    local ret=0
    for conf in "$IPSET_DIR"/*.conf; do
        [ -f "$conf" ] || continue
        name=$(get_set_name "$conf")
        [ -z "$name" ] && continue
        if "$IPSET" list "$name" &>/dev/null; then
            "$IPSET" destroy "$name" 2>/dev/null || ret=1
            log_success_msg "Destroyed $name"
        fi
        # Also clean any leftover temporary sets
        if "$IPSET" list "${name}_new" &>/dev/null; then
            "$IPSET" destroy "${name}_new" 2>/dev/null
            log_success_msg "Destroyed leftover ${name}_new"
        fi
        # Remove any stray .ipset temp files (if left)
        rm -f "$IPSET_DIR/$name.ipset"
    done
    return $ret
}

save_ipset() {
    local ret=0
    for name in $(ipset list -n); do
        [ -z "$name" ] && continue
        conf="$IPSET_DIR/$name"
        if "$IPSET" save "$name" > "$conf.tmp" 2>/dev/null; then
                mv "$conf.tmp" "$conf.conf"
                log_success_msg "Saved $name to $conf.conf"
            else
                rm -f "$conf.tmp"
                log_failure_msg "Failed to save $name"
                ret=1
            fi
    done
    return $ret
}

status_ipset() {
    local ret=0
    for conf in "$IPSET_DIR"/*.conf; do
        [ -f "$conf" ] || continue
        name=$(get_set_name "$conf")
        [ -z "$name" ] && continue
        if "$IPSET" list "$name" &>/dev/null; then
            echo "$name loaded"
        else
            echo "$name NOT loaded"
            ret=1
        fi
    done
    return $ret
}

case "$1" in
    start)   start_ipset ;;
    stop)    stop_ipset ;;
    restart) stop_ipset; start_ipset ;;
    reload)  stop_ipset; start_ipset ;;
    save)    save_ipset ;;
    status)  status_ipset ;;
    *)
        echo "Usage: $0 {start|stop|restart|reload|save|status}"
        exit 1
esac

exit $?
