#!/bin/bash
#
# ferm-systemd - Systemd wrapper for ferm with caching support
# Usage: ferm-systemd {activate|deactivate|activate_early}
#

set -e

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Parse command
COMMAND=${1:-activate}

debug() {
    [ -n "${DEBUGLOG_DIR:-}" ] || return 0
    printf 'DEBUG: %s\n' "$@"
}

dump_ruleset() {
    local STAGE=$1
    local TIMESTAMP LOGFILE

    [ -n "${DEBUGLOG_DIR:-}" ] || return 0

    if [ ! -d "${DEBUGLOG_DIR}" ]; then
        if ! mkdir -p "${DEBUGLOG_DIR}"; then
            printf 'DEBUG: cannot create DEBUGLOG_DIR %s\n' \
                "${DEBUGLOG_DIR}" >&2
            return 0
        fi
    fi

    TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ)
    LOGFILE="${DEBUGLOG_DIR}/ferm${DEBUG_PREFIX:-}-${TIMESTAMP}-${STAGE}-ruleset.log"

    printf 'DEBUG: dumping firewall state to %s\n' "${LOGFILE}"

    {
        printf '# ferm-systemd firewall state dump\n'
        printf '# stage: %s\n' "${STAGE}"
        printf '# timestamp: %s\n' "${TIMESTAMP}"
        printf '# command: %s\n' "${COMMAND:-unknown}"
        printf '\n'

        for family in ip ip6; do
            case "${family}" in
                ip)
                    EXECUTABLE=iptables
                    ;;
                ip6)
                    EXECUTABLE=ip6tables
                    ;;
            esac

            for backend in nft legacy; do
                printf '### %s-%s-%s\n\n' "${EXECUTABLE}" "${backend}" "save"

                if command -v "${EXECUTABLE}-${backend}-save" >/dev/null 2>&1; then
                    "${EXECUTABLE}-${backend}-save" 2>&1 || true
                else
                    printf '%s-%s-%s not found\n' "${EXECUTABLE}" "${backend}" "save"
                fi

                printf '\n'
            done
        done

        printf '### nft list ruleset\n\n'

        if command -v nft >/dev/null 2>&1; then
            nft list ruleset 2>&1 || true
        else
            printf 'nft not found\n'
        fi
    } > "${LOGFILE}" 2>&1 || {
        printf 'DEBUG: failed to write firewall state to %s\n' \
            "${LOGFILE}" >&2
        return 0
    }

    printf 'DEBUG: firewall state written to %s\n' "${LOGFILE}"
}

copy_debug_file() {
    local SOURCE=$1
    local STAGE=$2
    local TIMESTAMP BASENAME DESTINATION

    [ -n "${DEBUGLOG_DIR:-}" ] || return 0

    if [ ! -d "${DEBUGLOG_DIR}" ]; then
        if ! mkdir -p "${DEBUGLOG_DIR}"; then
            printf 'DEBUG: cannot create DEBUGLOG_DIR %s\n' \
                "${DEBUGLOG_DIR}" >&2
            return 0
        fi
    fi

    if [ ! -f "${SOURCE}" ]; then
        printf 'DEBUG: file %s does not exist; not copying\n' \
            "${SOURCE}" >&2
        return 0
    fi

    TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ)
    BASENAME=$(basename "${SOURCE}")
    DESTINATION="${DEBUGLOG_DIR}/ferm${DEBUG_PREFIX:-}-${TIMESTAMP}-${STAGE}-${BASENAME}"

    if cp -- "${SOURCE}" "${DESTINATION}"; then
        printf 'DEBUG: copied %s to %s\n' \
            "${SOURCE}" "${DESTINATION}"
    else
        printf 'DEBUG: failed to copy %s to %s\n' \
            "${SOURCE}" "${DESTINATION}" >&2
    fi

    return 0
}

debug "starting up"
dump_ruleset startup

case "$COMMAND" in
    activate_early)
        # These should be set by systemd or have defaults
        : "${FERM_EARLY:=/usr/sbin/ferm}"
        : "${CONFIG_EARLY:=/etc/ferm/ferm-early.conf}"
        : "${CACHE_DIR_EARLY:=/var/cache/ferm/early}"
        : "${CACHE_EARLY:=no}"
        : "${OPTIONS_EARLY:=}"
        : "${NAME_EARLY:=Early Firewall rules}"
        : "${FERM:=$FERM_EARLY}"
        : "${CONFIG:=$CONFIG_EARLY}"
        : "${CACHE_DIR:=$CACHE_DIR_EARLY}"
        : "${CACHE:=$CACHE_EARLY}"
        : "${OPTIONS:=$OPTIONS_EARLY}"
        : "${NAME:=$NAME_EARLY}"
        DEBUG_PREFIX="-early"
        ;;
    activate|deactivate)
        # These should be set by systemd or have defaults
        : "${FERM:=/usr/sbin/ferm}"
        : "${CONFIG:=/etc/ferm/ferm.conf}"
        : "${CACHE_DIR:=/var/cache/ferm}"
        : "${CACHE:=no}"
        : "${OPTIONS:=}"
        : "${NAME:=Firewall rules}"
        DEBUG_PREFIX=""
        ;;
    *)
        echo "Usage: $0 {activate|activate_early|deactivate}" >&2
        exit 1
        ;;
esac

debug "variable FERM=${FERM}"
debug "variable CONFIG=${CONFIG}"
debug "variable CACHE_DIR=${CACHE_DIR}"
debug "variable CACHE=${CACHE}"
debug "variable OPTIONS=${OPTIONS}"
debug "variable NAME=${NAME}"
debug "variable DEBUG_PREFIX=${DEBUG_PREFIX}"

# SLOW takes precedence; if not set, initialize from FAST for backwards compatibility
if [ -z "${SLOW}" ]; then
    if [ "${FAST}" = "no" ]; then
        OPTIONS="${OPTIONS} --slow"
    fi
fi
debug "variable OPTIONS=${OPTIONS}"

# Validate cache directory
if [ ! -d "${CACHE_DIR}" ] || [ ! -w "${CACHE_DIR}" ]; then
    CACHE="no"
fi
debug "variable CACHE=${CACHE}"

# Helper function to check if cache needs regeneration
cache_needs_regen() {
    local CACHE_FILE=$1
    local KERNEL_FILE=$2
    debug "cache_needs_regen ${CACHE_FILE} ${KERNEL_FILE}"

    # Check kernel version
    if ! diff /proc/version "${KERNEL_FILE}" >/dev/null 2>&1; then
        debug "cache needs regen: other kernel"
        return 0  # needs regen
    fi

    # Check if cache file exists
    if [ ! -f "${CACHE_FILE}" ]; then
        debug "cache needs regen: cache file does not exist"
        return 0
    fi

    # Check if config is newer
    if [ "${CACHE_FILE}" -ot "${CONFIG}" ]; then
        debug "cache needs regen: config file is newer than cache file"
        return 0
    fi

    # Check if any file in /etc/ferm is newer
    if [ -n "$(find /etc/ferm -maxdepth 2 -newer "${CACHE_FILE}" 2>/dev/null)" ]; then
        debug "cache needs regen: some file in /etc/ferm is newer than cache file"
        return 0
    fi

    # Check if ferm executable is newer
    if [ "${CACHE_FILE}" -ot "${FERM}" ]; then
        debug "cache needs regen: ferm executable is newer than cache file"
        return 0
    fi

    debug "cache is valid"
    return 1
}

# Prepare cache and run ferm
prepare_and_run() {
    local CONFIG=${1:-ferm-cache}
    local CACHE_NAME
    CACHE_NAME="$(systemd-escape "${CONFIG}")"

    if [ "${CACHE}" = "yes" ]; then
        local CACHE_FILE="${CACHE_DIR}/${CACHE_NAME}.sh"
        local KERNEL_FILE="${CACHE_DIR}/${CACHE_NAME}.kernel"
        debug "variable CACHE_FILE=${CACHE_FILE}"
        debug "variable KERNEL_FILE=${KERNEL_FILE}"

        if cache_needs_regen "${CACHE_FILE}" "${KERNEL_FILE}"; then
            copy_debug_file "${CACHE_FILE}" before-cache-regeneration

            echo "Regenerating ferm cache..."
            rm -f "${CACHE_FILE}" "${CACHE_FILE}.tmp" "${KERNEL_FILE}"

            "${FERM}" ${OPTIONS} --shell "${CONFIG}" > "${CACHE_FILE}.tmp" || return $?

            copy_debug_file "${CACHE_FILE}.tmp" generated-cache-before-write

            cp /proc/version "${KERNEL_FILE}"
            mv "${CACHE_FILE}.tmp" "${CACHE_FILE}" || return $?

            echo "Cache generated successfully. Now using fresh cache to activate rules"
        else
            echo "Activating firewall rules Using existing ferm cache"
        fi

        dump_ruleset before-cache-activation
        copy_debug_file "${CACHE_FILE}" before-cache-read

        debug "execute cache file ${CACHE_FILE}"
        . "${CACHE_FILE}" || return $?
    else
        dump_ruleset before-direct-activation

        echo "Activating firewall rules: ${FERM} ${OPTIONS} ${CONFIG}"
        "${FERM}" ${OPTIONS} "${CONFIG}" || return $?
    fi
}

# Main execution
case "$COMMAND" in
    activate|activate_early)
        debug "activate or activate_early, COMMAND=${COMMAND}, CONFIG=${CONFIG}"
        prepare_and_run "${CONFIG}" || exit $?
        echo "${NAME} activated successfully"
        ;;
    deactivate)
        debug "deactivate, CONFIG=${CONFIG}"
        dump_ruleset before-deactivate

        echo "Flushing firewall rules..."
        "${FERM}" ${OPTIONS} --flush "${CONFIG}" || exit $?

        dump_ruleset after-deactivate

        echo "${NAME} deactivated successfully"
        ;;
    deactivate_early)
        echo "Doing nothing, deactivate_early is a no-op..."
        ;;
esac

dump_ruleset post-action

exit 0
