#!/usr/bin/env bash
#
# onx-firewall-threat-sync — Public tehdit listesini indir, parse et, nftables set'e yaz.
#
# Desteklenen feed'ler (hepsi key-free):
#   - Spamhaus DROP/EDROP (https://www.spamhaus.org/drop/)
#   - DShield Top Block List (https://feeds.dshield.org/block.txt)
#   - Emerging Threats Compromised (rules.emergingthreats.net)
#   - Tor Exit Nodes (check.torproject.org/torbulkexitlist)
#   - FireHOL Level 1 (iplists.firehol.org)
#   - Generic: herhangi bir IP/CIDR satır-bazlı txt feed
#
# Input (stdin JSON):
#   {
#     "slug": "spamhaus_drop",        # zorunlu, ^[a-z0-9_]+$
#     "url": "https://...",            # zorunlu, https://
#     "apply": true,                   # opsiyonel, set'e gerçekten yaz
#     "force_refresh": false           # opsiyonel, cache bypass
#   }
#
# Output (stdout JSON):
#   {"slug":"...","ip_count":N,"ip_count_v6":N,"synced_at":"...","set_name":"onox-threat-X","cached":bool}
#
# Exit codes:
#   0 = ok, 1 = bad input, 2 = download fail, 3 = parse fail, 4 = nft fail

set -euo pipefail

readonly CACHE_DIR="/var/cache/onox/threats"
readonly LOCK_DIR="/var/lib/onox/lock"
readonly LOG_TAG="onox-threat-sync"
readonly CACHE_MAX_AGE_MIN=1440   # 24 saat

mkdir -p "$CACHE_DIR" "$LOCK_DIR"

# ── Stdin parse ───────────────────────────────────────────────────────────
input="$(cat)"
slug="$(echo "$input" | jq -r '.slug // empty')"
url="$(echo  "$input" | jq -r '.url // empty')"
apply="$(echo "$input" | jq -r '.apply // false')"
force="$(echo "$input" | jq -r '.force_refresh // false')"

if [[ -z "$slug" || -z "$url" ]]; then
    jq -nc '{ok:false,error:"slug and url required"}' >&2
    exit 1
fi

if ! [[ "$slug" =~ ^[a-z0-9_]+$ ]]; then
    jq -nc --arg s "$slug" '{ok:false,error:"invalid slug format",slug:$s}' >&2
    exit 1
fi

# Sadece HTTPS feed'leri kabul et (downgrade attack riski)
if ! [[ "$url" =~ ^https:// ]]; then
    jq -nc --arg u "$url" '{ok:false,error:"only https:// urls allowed",url:$u}' >&2
    exit 1
fi

# ── Lock (concurrent sync engelle) ────────────────────────────────────────
readonly LOCK_FILE="$LOCK_DIR/threat-sync-${slug}.lock"
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
    jq -nc --arg s "$slug" '{ok:false,error:"another sync in progress",slug:$s}' >&2
    exit 3
fi

readonly LIST_FILE="$CACHE_DIR/${slug}.txt"
readonly SET_V4="onox-threat-${slug//_/-}"
readonly SET_V6="onox-threat-${slug//_/-}-v6"

# ── Download (cache 24 saat) ──────────────────────────────────────────────
cached=true
needs_download=true
if [[ -f "$LIST_FILE" && "$force" != "true" ]]; then
    age_min=$(( ( $(date +%s) - $(stat -c %Y "$LIST_FILE") ) / 60 ))
    if [[ $age_min -lt $CACHE_MAX_AGE_MIN ]]; then
        needs_download=false
    fi
fi

if [[ "$needs_download" == "true" ]]; then
    cached=false
    logger -t "$LOG_TAG" "Downloading $slug from $url"
    tmpfile="${LIST_FILE}.tmp.$$"
    if ! curl -sSL --max-time 120 --retry 2 --retry-delay 5 \
        --user-agent "ONOXSOFT-Panel/1.0" \
        -o "$tmpfile" "$url"; then
        rm -f "$tmpfile"
        jq -nc --arg u "$url" '{ok:false,error:"download failed",url:$u}' >&2
        exit 2
    fi

    # Sanity: dosya boyutu min 100 byte olmalı
    size=$(stat -c%s "$tmpfile" 2>/dev/null || echo 0)
    if [[ $size -lt 100 ]]; then
        rm -f "$tmpfile"
        jq -nc '{ok:false,error:"downloaded file too small (<100 bytes), feed possibly empty or rate-limited"}' >&2
        exit 2
    fi

    mv -f "$tmpfile" "$LIST_FILE"
fi

# ── Parse: IPv4 ve IPv6 ayrı topla ────────────────────────────────────────
v4_tmp="$(mktemp)"
v6_tmp="$(mktemp)"
trap 'rm -f "$v4_tmp" "$v6_tmp"' EXIT

# Spamhaus format: "1.2.3.0/24 ; SBL12345"
# DShield: "1.2.3.4\t8\t..."
# Tor: plain IP per line
# FireHOL: plain CIDR or IP per line
while IFS= read -r line; do
    # Strip comments (#, ;) ve trailing whitespace
    cleaned="$(echo "$line" | sed -E 's/[#;].*$//' | awk '{print $1}' | tr -d '\r\t ')"
    [[ -z "$cleaned" ]] && continue

    # IPv4 / IPv4 CIDR
    if [[ "$cleaned" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then
        echo "$cleaned" >> "$v4_tmp"
    # IPv6 / IPv6 CIDR (basic check — `:` içeriyor)
    elif [[ "$cleaned" == *:* ]]; then
        # Filter validation: yalnız hex + ":" + opsiyonel /N
        if [[ "$cleaned" =~ ^[0-9a-fA-F:]+(/[0-9]{1,3})?$ ]]; then
            echo "$cleaned" >> "$v6_tmp"
        fi
    fi
done < "$LIST_FILE"

ip_count_v4=$(wc -l < "$v4_tmp")
ip_count_v6=$(wc -l < "$v6_tmp")
total=$((ip_count_v4 + ip_count_v6))

if [[ "$total" -eq 0 ]]; then
    jq -nc --arg s "$slug" '{ok:false,error:"no valid IPs parsed from feed",slug:$s}' >&2
    exit 3
fi

# ── Apply to nftables ─────────────────────────────────────────────────────
if [[ "$apply" == "true" ]]; then
    # Base table garantisi (idempotent)
    nft list table inet onox &>/dev/null || nft add table inet onox

    # Threat chain garantisi
    nft list chain inet onox threat_in &>/dev/null || \
        nft 'add chain inet onox threat_in { type filter hook input priority -50; }'

    # ─ IPv4 set ─
    if [[ $ip_count_v4 -gt 0 ]]; then
        # Atomic replacement: yeni set oluştur, swap, eskiyi sil
        readonly SET_V4_NEW="${SET_V4}-new"
        nft delete set inet onox "$SET_V4_NEW" 2>/dev/null || true
        nft "add set inet onox $SET_V4_NEW { type ipv4_addr; flags interval; auto-merge; }"

        # Bulk-load — nft tek komutta tüm elementleri kabul eder, comma separated
        # max 200K element için yeterli (chunked yaklaşımı çok büyük listeler için)
        elements="$(paste -sd',' "$v4_tmp")"

        # nft uzun komutu argv'ye sığmayabilir; -f ile dosyadan oku
        nft_cmdfile="$(mktemp)"
        echo "add element inet onox $SET_V4_NEW { $elements }" > "$nft_cmdfile"

        if ! nft -f "$nft_cmdfile" 2>"$nft_cmdfile.err"; then
            err="$(cat "$nft_cmdfile.err" 2>/dev/null || echo 'unknown')"
            rm -f "$nft_cmdfile" "$nft_cmdfile.err"
            nft delete set inet onox "$SET_V4_NEW" 2>/dev/null || true
            jq -nc --arg s "$slug" --arg err "$err" \
                '{ok:false,error:"nft add element failed",slug:$s,nft_err:$err}' >&2
            exit 4
        fi
        rm -f "$nft_cmdfile" "$nft_cmdfile.err"

        # Swap sets (atomic) — eskisi varsa ismi değiştir, yenisini yeni isimle koy
        if nft list set inet onox "$SET_V4" &>/dev/null; then
            nft delete set inet onox "$SET_V4"
        fi
        nft "rename set inet onox $SET_V4_NEW $SET_V4" 2>/dev/null || {
            # rename desteklenmiyorsa (eski nft) fallback: ekleyip silelim
            nft "add set inet onox $SET_V4 { type ipv4_addr; flags interval; auto-merge; }" 2>/dev/null || true
            nft -f "$nft_cmdfile" 2>/dev/null || true
            nft delete set inet onox "$SET_V4_NEW" 2>/dev/null || true
        }

        # Drop rule (idempotent)
        if ! nft -a list chain inet onox threat_in 2>/dev/null | grep -q "@$SET_V4"; then
            nft "add rule inet onox threat_in ip saddr @$SET_V4 counter drop comment \"onox-threat-${slug}\""
        fi
    fi

    # ─ IPv6 set ─
    if [[ $ip_count_v6 -gt 0 ]]; then
        nft list set inet onox "$SET_V6" &>/dev/null || \
            nft "add set inet onox $SET_V6 { type ipv6_addr; flags interval; auto-merge; }"

        nft flush set inet onox "$SET_V6"
        elements_v6="$(paste -sd',' "$v6_tmp")"
        nft_cmdfile6="$(mktemp)"
        echo "add element inet onox $SET_V6 { $elements_v6 }" > "$nft_cmdfile6"
        nft -f "$nft_cmdfile6" 2>/dev/null || true
        rm -f "$nft_cmdfile6"

        if ! nft -a list chain inet onox threat_in 2>/dev/null | grep -q "@$SET_V6"; then
            nft "add rule inet onox threat_in ip6 saddr @$SET_V6 counter drop comment \"onox-threat-${slug}-v6\""
        fi
    fi

    logger -t "$LOG_TAG" "Applied $ip_count_v4 IPv4 + $ip_count_v6 IPv6 to ${SET_V4}"
fi

jq -nc \
    --arg slug "$slug" \
    --argjson ip_count "$ip_count_v4" \
    --argjson ip_count_v6 "$ip_count_v6" \
    --arg synced_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    --arg set_name "$SET_V4" \
    --argjson cached "$cached" \
    '{ok:true,slug:$slug,ip_count:$ip_count,ip_count_v6:$ip_count_v6,synced_at:$synced_at,set_name:$set_name,cached:$cached}'
