#!/usr/bin/env bash
# =============================================================================
# onx-modsec-rule-write — Per-domain ModSecurity overlay. DRIVER-AWARE (v3.41).
#
# Customer-side per-domain overlay (ModSecurityProvisioner çağırır):
#   Apache → /etc/httpd/conf.d/modsec-onx-<domain>.conf  (<Directory> bloğu — mevcut)
#   v3 (nginx/ols/caddy) → neutral 40-domain-<domain>.conf
#       CONTAINER-FREE: tek bir SecRule SERVER_NAME "@streq <domain>" altında
#       ctl:ruleEngine + ctl:ruleRemoveById birleştirilir (runtime, load-order
#       bağımsız). Apache <Directory>/<IfModule> v3'te parse edilemez.
#
# Input (stdin JSON):
#   { "domain":"acme.com.tr", "username":"onx_acme01", "enabled":true,
#     "global_engine":"On|Off|DetectionOnly", "paranoia_level":1,
#     "excluded_rules":["949110","981318"], "custom_rules":[{id,action,pattern}] }
#
# Output: { domain, conf_path, driver, engine, rules_written, reloaded,
#           reload_required, applied }
#
# Exit: 0=ok 1=invalid 2=preflight 4=rolled-back 5=rollback-fail
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

require_root
onx_json_input

DOMAIN=$(onx_json_field        "domain")
USERNAME=$(onx_json_field      "username")
USER_HOME=$(onx_resolve_home "${USERNAME}" 2>/dev/null || echo "/home/${USERNAME}")
ENABLED=$(onx_json_get_bool "${INPUT}" "enabled" "true")
GLOBAL_ENGINE=$(onx_json_field "global_engine" "")
PARANOIA=$(onx_json_field      "paranoia_level" "1")

# ── Input validation (shared) ────────────────────────────────────────────────
onx_validate_domain   "${DOMAIN}"
onx_validate_username "${USERNAME}"

if [[ -z "${GLOBAL_ENGINE}" ]]; then
    if [[ "${ENABLED}" == "true" ]]; then GLOBAL_ENGINE="On"; else GLOBAL_ENGINE="Off"; fi
fi
case "${GLOBAL_ENGINE}" in
    On|Off|DetectionOnly) : ;;
    *) onx_die 1 "global_engine must be On|Off|DetectionOnly (got '${GLOBAL_ENGINE}')" ;;
esac
[[ "${PARANOIA}" =~ ^[1-4]$ ]] || onx_die 1 "paranoia_level must be 1..4"

# Collect excluded rule ids (shared)
EXCLUDED_IDS=()
while IFS= read -r rid; do
    [[ -z "${rid}" ]] && continue
    [[ "${rid}" =~ ^[0-9]+$ ]] || continue
    EXCLUDED_IDS+=("${rid}")
done < <(onx_json_array_items "${INPUT}" "excluded_rules")

DRIVER=$(onx_ws_active_driver)

# ═══════════════════════════════════════════════════════════════════════════
# APACHE — <Directory> overlay (mevcut, kanıtlanmış davranış)
# ═══════════════════════════════════════════════════════════════════════════
if [[ "${DRIVER}" == "apache" ]] || [[ "${MOCK_MODE}" == "1" ]]; then
    APACHE_CONF_DIR=""
    APACHE_SERVICE=""
    if [[ -d /etc/httpd/conf.d ]]; then
        APACHE_CONF_DIR="/etc/httpd/conf.d"; APACHE_SERVICE="httpd"
    elif [[ -d /etc/apache2/conf-enabled ]]; then
        APACHE_CONF_DIR="/etc/apache2/conf-enabled"; APACHE_SERVICE="apache2"
    elif [[ "${MOCK_MODE}" == "1" ]]; then
        APACHE_CONF_DIR="$(mktemp -d -t onx-modsec.XXXXXX)"; APACHE_SERVICE="mock"
    else
        onx_die 2 "neither /etc/httpd/conf.d nor /etc/apache2/conf-enabled exists"
    fi

    if [[ "${MOCK_MODE}" != "1" ]]; then
        command -v apachectl >/dev/null 2>&1 || onx_die 2 "apachectl not found"
    fi

    DOMAIN_HASH=$(printf '%s' "${DOMAIN}" | cksum | awk '{print $1 % 65536}')
    SECACTION_ID=$((9000000 + DOMAIN_HASH))
    CONF_PATH="${APACHE_CONF_DIR}/modsec-onx-${DOMAIN}.conf"
    DOC_ROOT="${USER_HOME}/public_html"
    TS=$(date -Iseconds)

    TMP_OUT="$(mktemp -t onx-modsec.XXXXXX)"
    chmod 0644 "${TMP_OUT}"
    {
        printf '# ONOX-managed ModSecurity overlay for %s\n' "${DOMAIN}"
        printf '# Generated %s by onx-modsec-rule-write — do not edit manually.\n' "${TS}"
        printf '<IfModule security2_module>\n'
        printf '<Directory %s>\n' "\"${DOC_ROOT}\""
        printf '    SecRuleEngine %s\n' "${GLOBAL_ENGINE}"
        if [[ "${GLOBAL_ENGINE}" != "Off" ]]; then
            printf '    SecAction "id:%d,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=%s,setvar:tx.blocking_paranoia_level=%s"\n' \
                "${SECACTION_ID}" "${PARANOIA}" "${PARANOIA}"
        fi
    } > "${TMP_OUT}"

    RULES_WRITTEN=0
    if [[ "$(printf '%s' "${INPUT}" | jq -r 'has("custom_rules") and (.custom_rules | type=="array") and (.custom_rules|length>0)')" == "true" ]]; then
        while IFS= read -r rule; do
            [[ -z "${rule}" ]] && continue
            RULE_ID=$(printf '%s' "${rule}"  | jq -r '.id // empty')
            RULE_ACT=$(printf '%s' "${rule}" | jq -r '.action // "deny"')
            RULE_PAT=$(printf '%s' "${rule}" | jq -r '.pattern // empty')
            [[ -z "${RULE_ID}" || -z "${RULE_PAT}" ]] && continue
            [[ "${RULE_ID}" =~ ^[0-9]+$ ]] || continue
            case "${RULE_ACT}" in deny|allow|block|pass|drop) : ;; *) RULE_ACT="deny" ;; esac
            printf '    SecRule %s "id:%s,phase:2,%s,log,t:none,msg:\x27onox-custom-%s\x27"\n' \
                "${RULE_PAT}" "${RULE_ID}" "${RULE_ACT}" "${RULE_ID}" >> "${TMP_OUT}"
            RULES_WRITTEN=$((RULES_WRITTEN + 1))
        done < <(printf '%s' "${INPUT}" | jq -c '.custom_rules[]?')
    fi

    if [[ ${#EXCLUDED_IDS[@]} -gt 0 ]]; then
        printf '    SecRuleRemoveById %s\n' "${EXCLUDED_IDS[*]}" >> "${TMP_OUT}"
        RULES_WRITTEN=$((RULES_WRITTEN + ${#EXCLUDED_IDS[@]}))
    fi

    {
        printf '</Directory>\n'
        printf '</IfModule>\n'
    } >> "${TMP_OUT}"

    BACKUP_PATH=""
    if [[ -f "${CONF_PATH}" ]]; then
        BACKUP_PATH="${CONF_PATH}.onx-bak.$$"
        cp -p "${CONF_PATH}" "${BACKUP_PATH}"
    fi
    mv "${TMP_OUT}" "${CONF_PATH}"
    chmod 0644 "${CONF_PATH}"

    RELOADED="false"; RELOAD_REQUIRED="true"
    if [[ "${MOCK_MODE}" == "1" ]] || [[ "${APACHE_SERVICE}" == "mock" ]]; then
        RELOADED="true"; RELOAD_REQUIRED="false"
    else
        if ! apachectl configtest >/dev/null 2>&1; then
            if [[ -n "${BACKUP_PATH}" ]]; then
                mv -f "${BACKUP_PATH}" "${CONF_PATH}" 2>/dev/null || true
            else
                rm -f "${CONF_PATH}" 2>/dev/null || true
            fi
            apachectl configtest >/dev/null 2>&1 || onx_die 5 "configtest failed AND rollback could not restore a valid state"
            onx_die 4 "apachectl configtest rejected new ModSecurity overlay (rolled back)"
        fi
        [[ -n "${BACKUP_PATH}" ]] && rm -f "${BACKUP_PATH}" 2>/dev/null || true
        if apachectl graceful >/dev/null 2>&1; then
            RELOADED="true"; RELOAD_REQUIRED="false"
        elif command -v systemctl >/dev/null 2>&1 && systemctl reload "${APACHE_SERVICE}" >/dev/null 2>&1; then
            RELOADED="true"; RELOAD_REQUIRED="false"
        fi
    fi

    onx_audit "onx-modsec" "write domain=${DOMAIN} driver=apache engine=${GLOBAL_ENGINE} rules=${RULES_WRITTEN} reloaded=${RELOADED}"
    onx_json_out \
        "domain"          "${DOMAIN}" \
        "conf_path"       "${CONF_PATH}" \
        "driver"          "apache" \
        "engine"          "${GLOBAL_ENGINE}" \
        "rules_written"   "${RULES_WRITTEN}" \
        "reloaded"        "${RELOADED}" \
        "reload_required" "${RELOAD_REQUIRED}" \
        "applied"         "true"
    exit 0
fi

# ═══════════════════════════════════════════════════════════════════════════
# v3 (nginx/ols/caddy) — neutral SERVER_NAME-scoped overlay (container-free)
# ═══════════════════════════════════════════════════════════════════════════
if [[ "${DRIVER}" == "unknown" ]]; then
    onx_die 2 "Aktif web sunucusu tespit edilemedi (apache/ols/nginx/caddy hiçbiri active değil)"
fi

NEUTRAL="${ONX_MODSEC_NEUTRAL_DIR}/${ONX_MODSEC_NEUTRAL_SUBDIR}"
mkdir -p "${NEUTRAL}" 2>/dev/null || true
# Domain → güvenli dosya adı (zaten validate edildi; yine de tireli normalize)
SAFE_DOMAIN=$(printf '%s' "${DOMAIN}" | tr -c 'a-zA-Z0-9.-' '_')
CONF_PATH="${NEUTRAL}/40-domain-${SAFE_DOMAIN}.conf"

# Tek SecRule altında birleşik aksiyonlar — global benzersiz id (8 haneli)
DOMAIN_HASH=$(printf '%s' "${DOMAIN}" | cksum | awk '{print $1 % 89000000}')
BASE_ID=$((10000000 + DOMAIN_HASH))

# ctl/setvar aksiyonlarını birleştir
ACTIONS=""
case "${GLOBAL_ENGINE}" in
    Off)           ACTIONS+=",ctl:ruleEngine=Off" ;;
    DetectionOnly) ACTIONS+=",ctl:ruleEngine=DetectionOnly" ;;
esac
if [[ "${GLOBAL_ENGINE}" != "Off" ]]; then
    ACTIONS+=",setvar:tx.paranoia_level=${PARANOIA},setvar:tx.blocking_paranoia_level=${PARANOIA},setvar:tx.detection_paranoia_level=${PARANOIA}"
fi
for rid in "${EXCLUDED_IDS[@]:-}"; do
    [[ -z "${rid}" ]] && continue
    ACTIONS+=",ctl:ruleRemoveById=${rid}"
done

RULES_WRITTEN=0
BACKUP_PATH=""
[[ -f "${CONF_PATH}" ]] && { BACKUP_PATH="${CONF_PATH}.onx-bak.$$"; cp -p "${CONF_PATH}" "${BACKUP_PATH}"; }

TMP_OUT="$(mktemp -t onx-modsec.XXXXXX)"
{
    printf '# ONOX-managed per-domain ModSecurity overlay for %s (driver=%s)\n' "${DOMAIN}" "${DRIVER}"
    printf '# Generated %s by onx-modsec-rule-write — container-free (libmodsecurity v3).\n' "$(date -Iseconds)"
    if [[ -n "${ACTIONS}" ]]; then
        # Baştaki virgülü at
        printf 'SecRule SERVER_NAME "@streq %s" "id:%d,phase:1,nolog,pass,t:none%s"\n' \
            "${DOMAIN}" "${BASE_ID}" "${ACTIONS}"
        RULES_WRITTEN=$((RULES_WRITTEN + 1))
    fi
} > "${TMP_OUT}"

# Custom rules (nadir — provisioner genelde [] gönderir): skipAfter ile domain-scope
if [[ "$(printf '%s' "${INPUT}" | jq -r 'has("custom_rules") and (.custom_rules | type=="array") and (.custom_rules|length>0)')" == "true" ]]; then
    cidx=1
    while IFS= read -r rule; do
        [[ -z "${rule}" ]] && continue
        RULE_ID=$(printf '%s' "${rule}"  | jq -r '.id // empty')
        RULE_ACT=$(printf '%s' "${rule}" | jq -r '.action // "deny"')
        RULE_PAT=$(printf '%s' "${rule}" | jq -r '.pattern // empty')
        [[ -z "${RULE_ID}" || -z "${RULE_PAT}" ]] && continue
        [[ "${RULE_ID}" =~ ^[0-9]+$ ]] || continue
        case "${RULE_ACT}" in deny|allow|block|pass|drop) : ;; *) RULE_ACT="deny" ;; esac
        guard_id=$((BASE_ID + cidx)); cidx=$((cidx + 1))
        marker="END-onox-dom-${RULE_ID}"
        {
            printf 'SecRule SERVER_NAME "!@streq %s" "id:%d,phase:2,nolog,pass,t:none,skipAfter:%s"\n' "${DOMAIN}" "${guard_id}" "${marker}"
            printf 'SecRule %s "id:%s,phase:2,%s,log,t:none,msg:\x27onox-custom-%s\x27"\n' "${RULE_PAT}" "${RULE_ID}" "${RULE_ACT}" "${RULE_ID}"
            printf 'SecMarker %s\n' "${marker}"
        } >> "${TMP_OUT}"
        RULES_WRITTEN=$((RULES_WRITTEN + 1))
    done < <(printf '%s' "${INPUT}" | jq -c '.custom_rules[]?')
fi

chmod 0644 "${TMP_OUT}"
mv -f "${TMP_OUT}" "${CONF_PATH}"

RELOADED=$(onx_ws_modsec_reload "${DRIVER}")
if [[ "${RELOADED}" != "true" ]]; then
    if [[ -n "${BACKUP_PATH}" ]]; then
        mv -f "${BACKUP_PATH}" "${CONF_PATH}" 2>/dev/null || true
    else
        rm -f "${CONF_PATH}" 2>/dev/null || true
    fi
    onx_ws_modsec_reload "${DRIVER}" >/dev/null 2>&1 || true
    onx_die 4 "${DRIVER} configtest/reload rejected per-domain overlay (rolled back)"
fi
[[ -n "${BACKUP_PATH}" ]] && rm -f "${BACKUP_PATH}" 2>/dev/null || true

onx_audit "onx-modsec" "write domain=${DOMAIN} driver=${DRIVER} engine=${GLOBAL_ENGINE} rules=${RULES_WRITTEN} reloaded=${RELOADED}"
onx_json_out \
    "domain"          "${DOMAIN}" \
    "conf_path"       "${CONF_PATH}" \
    "driver"          "${DRIVER}" \
    "engine"          "${GLOBAL_ENGINE}" \
    "rules_written"   "${RULES_WRITTEN}" \
    "reloaded"        "${RELOADED}" \
    "reload_required" "false" \
    "applied"         "true"
