#!/bin/bash
# wg-policy-lib.sh — Shared functions for WireGuard Policy Firewall
# Source this file; do not execute directly.

set -euo pipefail

# ============================================================
# CONFIGURATION
# ============================================================
readonly WG_IF="${WG_IF:-wg0}"
readonly CHAIN="WG_POLICY"
readonly CHAIN_BACKUP="WG_POLICY_BAK"
readonly POLICY_FILE="/etc/wireguard/policy.json"
readonly WG_CONF="/etc/wireguard/wg0.conf"
readonly LOCK_FILE="/var/lock/wg-policy.lock"
readonly BACKUP_DIR="/etc/wireguard/backups"
readonly LOG_PREFIX="WG_DROP"
readonly LOG_RATE="10/min"
readonly IPSET_V4="wg_allowed_v4"
readonly IPSET_V6="wg_allowed_v6"
readonly MAX_RETRY=3
readonly RETRY_DELAY=2
readonly DEBOUNCE_SEC=2

# ============================================================
# LOGGING
# ============================================================
log_info()  { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO]  $*"; }
log_warn()  { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN]  $*" >&2; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; }

# ============================================================
# VALIDATION
# ============================================================

# Validate IPv4 address (strict: 0-255 per octet, no leading zeros)
validate_ipv4() {
    local ip="$1"
    # Match basic pattern
    if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
        return 1
    fi
    local IFS='.'
    read -ra octets <<< "$ip"
    for octet in "${octets[@]}"; do
        # Reject leading zeros (except "0" itself)
        if [[ "$octet" =~ ^0[0-9] ]]; then
            return 1
        fi
        if (( octet < 0 || octet > 255 )); then
            return 1
        fi
    done
    return 0
}

# Validate IPv4 CIDR (e.g., 192.168.1.0/24)
validate_ipv4_cidr() {
    local cidr="$1"
    local ip prefix

    if [[ "$cidr" == *"/"* ]]; then
        ip="${cidr%%/*}"
        prefix="${cidr##*/}"
    else
        # Single IP treated as /32
        ip="$cidr"
        prefix="32"
    fi

    if ! validate_ipv4 "$ip"; then
        return 1
    fi

    if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 0 || prefix > 32 )); then
        return 1
    fi
    return 0
}

# Validate IPv6 address (basic check)
validate_ipv6() {
    local ip="$1"
    # Basic IPv6 pattern — covers full, compressed, and mixed notation
    if [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$ ]] || \
       [[ "$ip" =~ ^::([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$ ]] || \
       [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){1,7}:$ ]] || \
       [[ "$ip" == "::" ]] || \
       [[ "$ip" == "::1" ]]; then
        return 0
    fi
    return 1
}

# Validate IPv6 CIDR
validate_ipv6_cidr() {
    local cidr="$1"
    local ip prefix

    if [[ "$cidr" == *"/"* ]]; then
        ip="${cidr%%/*}"
        prefix="${cidr##*/}"
    else
        ip="$cidr"
        prefix="128"
    fi

    if ! validate_ipv6 "$ip"; then
        return 1
    fi

    if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 0 || prefix > 128 )); then
        return 1
    fi
    return 0
}

# Generic CIDR validator — dispatches to v4 or v6
validate_cidr() {
    local cidr="$1"
    if [[ "$cidr" == *":"* ]]; then
        validate_ipv6_cidr "$cidr"
    else
        validate_ipv4_cidr "$cidr"
    fi
}

# ============================================================
# IPSET MANAGEMENT
# ============================================================

has_ipset() {
    command -v ipset &>/dev/null
}

ensure_ipset() {
    local name="$1" family="$2"
    has_ipset || return 0
    if ! ipset list "$name" &>/dev/null; then
        ipset create "$name" hash:net,net family "$family" hashsize 1024 maxelem 65536 timeout 0
        log_info "Created ipset: $name (family=$family)"
    fi
}

flush_ipset() {
    local name="$1"
    has_ipset || return 0
    if ipset list "$name" &>/dev/null; then
        ipset flush "$name"
    fi
}

destroy_ipset() {
    local name="$1"
    has_ipset || return 0
    if ipset list "$name" &>/dev/null; then
        ipset destroy "$name"
    fi
}

# ============================================================
# RETRY MECHANISM
# ============================================================

retry() {
    local max_attempts="${MAX_RETRY}"
    local delay="${RETRY_DELAY}"
    local attempt=1
    local exit_code=0

    while (( attempt <= max_attempts )); do
        if "$@"; then
            return 0
        fi
        exit_code=$?
        log_warn "Attempt $attempt/$max_attempts failed (exit=$exit_code), retrying in ${delay}s..."
        sleep "$delay"
        (( attempt++ ))
        (( delay *= 2 ))  # exponential backoff
    done

    log_error "All $max_attempts attempts failed for: $*"
    return "$exit_code"
}

# ============================================================
# LOCK MANAGEMENT
# ============================================================

acquire_lock() {
    local lock_fd=200
    eval "exec ${lock_fd}>\"${LOCK_FILE}\""
    if ! flock -x -w 10 "$lock_fd"; then
        log_error "Failed to acquire lock: ${LOCK_FILE} (timeout 10s)"
        return 1
    fi
    log_info "Lock acquired: ${LOCK_FILE}"
}

release_lock() {
    # Lock released automatically when fd closes, but we clean up file
    rm -f "$LOCK_FILE" 2>/dev/null || true
}

# ============================================================
# BACKUP
# ============================================================

backup_policy() {
    mkdir -p "$BACKUP_DIR"
    local timestamp
    timestamp="$(date '+%Y%m%d_%H%M%S')"

    if [[ -f "$POLICY_FILE" ]]; then
        cp "$POLICY_FILE" "${BACKUP_DIR}/policy_${timestamp}.json"
        log_info "Backup created: ${BACKUP_DIR}/policy_${timestamp}.json"
    fi

    # Keep only last 50 backups
    local count
    count=$(find "$BACKUP_DIR" -name 'policy_*.json' -type f | wc -l)
    if (( count > 50 )); then
        find "$BACKUP_DIR" -name 'policy_*.json' -type f -printf '%T@ %p\n' \
            | sort -n \
            | head -n $(( count - 50 )) \
            | awk '{print $2}' \
            | xargs rm -f
        log_info "Pruned old backups (kept 50)"
    fi
}

backup_iptables() {
    mkdir -p "$BACKUP_DIR"
    local timestamp
    timestamp="$(date '+%Y%m%d_%H%M%S')"

    if iptables-save > "${BACKUP_DIR}/iptables_${timestamp}.rules" 2>/dev/null; then
        log_info "iptables backup: ${BACKUP_DIR}/iptables_${timestamp}.rules"
    fi

    if command -v ip6tables-save &>/dev/null; then
        ip6tables-save > "${BACKUP_DIR}/ip6tables_${timestamp}.rules" 2>/dev/null || true
    fi

    # Keep only last 20 iptables backups
    for prefix in iptables ip6tables; do
        local count
        count=$(find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f | wc -l)
        if (( count > 20 )); then
            find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f -printf '%T@ %p\n' \
                | sort -n \
                | head -n $(( count - 20 )) \
                | awk '{print $2}' \
                | xargs rm -f
        fi
    done
}

# ============================================================
# DETECTION HELPERS
# ============================================================

detect_lan_subnets() {
    ip -o -4 route show scope link \
        | awk '{print $1}' \
        | grep -E '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' \
        | sort -u
}

detect_wg_subnet() {
    local family="${1:-inet}"
    if [[ "$family" == "inet6" ]]; then
        ip -o -6 addr show "$WG_IF" 2>/dev/null \
            | awk '{print $4; exit}'
    else
        ip -o -4 addr show "$WG_IF" 2>/dev/null \
            | awk '{print $4; exit}'
    fi
}

detect_default_if() {
    local def_if
    def_if=$(ip -4 route ls 2>/dev/null | grep default | grep -Po '(?<=dev )(\S+)' | head -1 || true)
    if [[ -z "$def_if" ]]; then
        echo "eth0"
    else
        echo "$def_if"
    fi
}

# ============================================================
# HEALTH CHECK
# ============================================================

health_check() {
    local status=0
    local report=""

    # 1. Check interface exists
    if ip link show "$WG_IF" &>/dev/null; then
        report+="[OK] Interface $WG_IF is UP\n"
    else
        report+="[FAIL] Interface $WG_IF not found\n"
        status=1
    fi

    # 2. Check policy.json exists and is valid
    if [[ -f "$POLICY_FILE" ]] && jq empty "$POLICY_FILE" 2>/dev/null; then
        local client_count
        client_count=$(jq '(.clients // {}) | length' "$POLICY_FILE")
        report+="[OK] policy.json valid ($client_count clients)\n"
    else
        report+="[FAIL] policy.json missing or corrupt\n"
        status=1
    fi

    # 3. Check chain exists
    if iptables -L "$CHAIN" -n &>/dev/null; then
        local rule_count
        rule_count=$(iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l)
        report+="[OK] Chain $CHAIN active ($rule_count rules)\n"
    else
        report+="[WARN] Chain $CHAIN not found\n"
        status=1
    fi

    # 4. Check FORWARD reference
    if iptables -L FORWARD -n 2>/dev/null | grep -q "$CHAIN"; then
        report+="[OK] FORWARD chain references $CHAIN\n"
    else
        report+="[WARN] FORWARD chain has no reference to $CHAIN\n"
        status=1
    fi

    # 5. Check ipset
    for set_name in "$IPSET_V4" "$IPSET_V6"; do
        if ipset list "$set_name" &>/dev/null; then
            local entry_count
            entry_count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0)
            report+="[OK] ipset $set_name active ($entry_count entries)\n"
        else
            report+="[INFO] ipset $set_name not created (may not be needed)\n"
        fi
    done

    # 6. Check watcher service
    if systemctl is-active --quiet wg-policy.service 2>/dev/null; then
        report+="[OK] wg-policy.service is running\n"
    else
        report+="[INFO] wg-policy.service not running\n"
    fi

    # 7. Check lock file not stale
    if [[ -f "$LOCK_FILE" ]]; then
        local lock_age
        lock_age=$(( $(date +%s) - $(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0) ))
        if (( lock_age > 300 )); then
            report+="[WARN] Stale lock file (${lock_age}s old)\n"
        else
            report+="[OK] Lock file age: ${lock_age}s\n"
        fi
    else
        report+="[OK] No stale lock file\n"
    fi

    echo -e "$report"
    return $status
}
