<?php
/**
 * Hybrid IP Validation and Resolution Class
 * Combines Cloudflare optimization + full IPv6 support + configurable headers
 * 
 * @package BotFend\Utilities
 * @version 5.0.0
 */

namespace BotFend\Utilities;

use BotFend\Core\Config;

defined('ABSPATH') || exit;

class IPValidator {

    private $config;
    private $ip_cache = [];
    private $header_ip_cache = null;
    private $trusted_proxies_cache = null;

    private const MAX_CACHE_SIZE = 100;
    private const MAX_IP_LENGTH = 45;

    // Comprehensive private IP ranges (IPv4 + IPv6)
    private const PRIVATE_IP_RANGES = [
        // IPv4 private ranges
        '10.0.0.0/8',
        '172.16.0.0/12',
        '192.168.0.0/16',
        '127.0.0.0/8',
        '169.254.0.0/16',
        '0.0.0.0/8',
        '192.0.2.0/24',
        '198.51.100.0/24',
        '203.0.113.0/24',
        '224.0.0.0/4',
        '240.0.0.0/4',
        // IPv6 private/special ranges
        '::1/128',
        '::/128',
        '::ffff:0:0/96',
        '64:ff9b::/96',
        '100::/64',
        '2001:db8::/32',
        'fc00::/7',
        'fe80::/10',
        'ff00::/8',
    ];

    public function __construct(Config $config) {
        $this->config = $config;
    }

    /**
     * Main IP resolver - Hybrid approach
     * Cloudflare-aware + proxy-safe + IPv6 compatible
     *
     * @param bool $skip_cache Bypass request cache
     * @return string Validated IP or '0.0.0.0'
     */
    public function get_client_ip($skip_cache = false) {
        $request_id = $this->get_request_id();

        if (!$skip_cache && isset($this->ip_cache[$request_id])) {
            return $this->ip_cache[$request_id];
        }

        $mode = $this->config->get_string('ip_validation_mode', 'balanced');
        $remote = $this->get_remote_addr();

        if (!$this->is_valid_ip($remote)) {
            return $this->cache_ip($request_id, '0.0.0.0');
        }

        if ($mode === 'strict') {
            return $this->cache_ip($request_id, $remote);
        }

        $trusted = $this->is_trusted_proxy($remote);

        if ($mode === 'permissive' || $trusted) {
            $ip = $this->get_ip_from_headers($remote);

            if ($ip && $this->is_valid_ip($ip)) {
                return $this->cache_ip($request_id, $ip);
            }
        }

        return $this->cache_ip($request_id, $remote);
    }

    /**
     * Get IP from headers - Hybrid approach
     * Cloudflare-first + configurable priority + XFF private IP skipping
     *
     * @param string|null $remote_addr The remote address (for context)
     * @return string|null
     */
    private function get_ip_from_headers($remote_addr = null) {
        if ($this->header_ip_cache !== null) {
            return $this->header_ip_cache;
        }

        // ============================================
        // 1. CLOUDFLARE (Highest priority)
        // ============================================
        if ($this->is_cloudflare_request()) {
            $cf_ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? '';
            if ($this->is_valid_ip($cf_ip)) {
                return $this->cache_header_ip($cf_ip);
            }
        }

        // ============================================
        // 2. Configurable headers (from settings)
        // ============================================
        $headers = $this->config->get_array('ip_headers', [
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'HTTP_CLIENT_IP',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        ]);

        foreach ($headers as $header) {
            if (!isset($_SERVER[$header])) {
                continue;
            }

            $value = wp_unslash($_SERVER[$header]);
            if (!is_string($value) || empty($value)) {
                continue;
            }

            if (!$this->is_safe_header_value($value)) {
                continue;
            }

            $value = sanitize_text_field($value);
            $ips = $this->parse_header_value($value, $header);

            foreach ($ips as $ip) {
                if (!$this->is_valid_ip($ip)) {
                    continue;
                }

                // Skip private IPs in XFF chain (prevents spoofing)
                if ($header === 'HTTP_X_FORWARDED_FOR' && $this->is_private_ip($ip)) {
                    continue;
                }

                return $this->cache_header_ip($ip);
            }
        }

        return null;
    }

    /**
     * Parse header value based on header type
     *
     * @param string $value Raw header value
     * @param string $header Header name
     * @return array
     */
    private function parse_header_value($value, $header) {
        $ips = [];

        switch ($header) {
            case 'HTTP_X_FORWARDED_FOR':
                $parts = explode(',', $value);
                $ips = array_map('trim', $parts);
                break;

            case 'HTTP_FORWARDED':
                // RFC 7239 Forwarded header parsing
                if (preg_match('/for=([^;]+)/', $value, $matches)) {
                    $for = trim($matches[1]);
                    $for = str_replace(['[', ']'], '', $for);
                    $ips = [$for];
                }
                break;

            default:
                $ips = [trim($value)];
        }

        return $ips;
    }

    /**
     * Detect if request is coming through Cloudflare
     *
     * @return bool
     */
    private function is_cloudflare_request(): bool {
        return isset($_SERVER['HTTP_CF_RAY']) && isset($_SERVER['HTTP_CF_CONNECTING_IP']);
    }

    /**
     * Cache header result per request
     *
     * @param string $ip
     * @return string
     */
    private function cache_header_ip($ip) {
        $this->header_ip_cache = sanitize_text_field($ip);
        return $this->header_ip_cache;
    }

    /**
     * Cache IP with size limit
     *
     * @param string $request_id
     * @param string $ip
     * @return string
     */
    private function cache_ip($request_id, $ip) {
        if (count($this->ip_cache) >= self::MAX_CACHE_SIZE) {
            array_shift($this->ip_cache);
        }
        $this->ip_cache[$request_id] = $ip;
        return $ip;
    }

    /**
     * Get REMOTE_ADDR safely
     *
     * @return string
     */
    private function get_remote_addr() {
        if (empty($_SERVER['REMOTE_ADDR'])) {
            return '';
        }
        $ip = wp_unslash($_SERVER['REMOTE_ADDR']);
        return (strlen($ip) <= self::MAX_IP_LENGTH) ? sanitize_text_field($ip) : '';
    }

    /**
     * Check if IP is a trusted proxy
     *
     * @param string $ip
     * @return bool
     */
    private function is_trusted_proxy($ip) {
        if ($this->trusted_proxies_cache === null) {
            $this->trusted_proxies_cache = $this->load_trusted_proxies();
        }

        foreach ($this->trusted_proxies_cache as $proxy) {
            if ($proxy === $ip) {
                return true;
            }
            if (strpos($proxy, '/') !== false) {
                if ($this->ip_in_range($ip, $proxy)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Load and validate trusted proxies from config
     *
     * @return array
     */
    private function load_trusted_proxies() {
        $proxies = $this->config->get_array('trusted_proxies', []);
        $validated = [];

        foreach ($proxies as $proxy) {
            $proxy = trim($proxy);
            if (empty($proxy)) continue;

            if (strpos($proxy, '/') !== false) {
                list($ip, $mask) = explode('/', $proxy);
                $mask = (int) $mask;
                if (filter_var($ip, FILTER_VALIDATE_IP) && $mask >= 0 && $mask <= 128) {
                    $validated[] = $proxy;
                }
            } elseif (filter_var($proxy, FILTER_VALIDATE_IP)) {
                $validated[] = $proxy;
            }
        }

        return $validated;
    }

    /**
     * Check if header value is safe (prevents CRLF injection)
     *
     * @param string $value
     * @return bool
     */
    private function is_safe_header_value($value) {
        if (!is_string($value)) return false;
        if (preg_match('/[\r\n]/', $value)) return false;
        if (strpos($value, "\0") !== false) return false;
        if (stripos($value, '%0d') !== false || stripos($value, '%0a') !== false) return false;
        return true;
    }

    /**
     * Validate IP address format and safety
     * PUBLIC METHOD - can be used by other classes
     *
     * @param string $ip IP to validate
     * @return bool
     */
    public function is_valid_ip($ip) {
        if (!is_string($ip) || empty($ip) || strlen($ip) > self::MAX_IP_LENGTH) {
            return false;
        }

        $ip = trim($ip);

        if (strpos($ip, "\0") !== false) {
            return false;
        }

        // Block injection patterns
        $blocked_patterns = [
            'script', 'javascript', 'vbscript', 'data:',
            '<?', '?>', '<%', '%>', '${', '$(', '`',
        ];
        foreach ($blocked_patterns as $pattern) {
            if (stripos($ip, $pattern) !== false) {
                return false;
            }
        }

        // IPv6 validation with inet_pton
        if (strpos($ip, ':') !== false) {
            if (function_exists('inet_pton')) {
                return @inet_pton($ip) !== false;
            }
        }

        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
    }

    /**
     * Check if IP is in CIDR range - Hybrid (IPv4 + IPv6)
     * PUBLIC METHOD - can be used by other classes
     *
     * @param string $ip IP to check
     * @param string $range CIDR range
     * @return bool
     */
    public function ip_in_range($ip, $range) {
        if (strpos($range, '/') === false) {
            return $ip === $range;
        }

        list($subnet, $mask) = explode('/', $range);
        $mask = (int) $mask;

        if (!filter_var($subnet, FILTER_VALIDATE_IP)) {
            return false;
        }

        // IPv4
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            if ($mask < 0 || $mask > 32) return false;
            $ip_long = ip2long($ip);
            $subnet_long = ip2long($subnet);
            if ($ip_long === false || $subnet_long === false) return false;
            $network = $subnet_long & ((-1 << (32 - $mask)));
            $broadcast = $subnet_long | (~(-1 << (32 - $mask)));
            return ($ip_long >= $network && $ip_long <= $broadcast);
        }

        // IPv6
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            return $this->ipv6_in_range($ip, $subnet, $mask);
        }

        return false;
    }

    /**
     * IPv6 CIDR range check
     *
     * @param string $ip IPv6 address
     * @param string $subnet IPv6 subnet
     * @param int $mask CIDR mask
     * @return bool
     */
    private function ipv6_in_range($ip, $subnet, $mask) {
        if (!function_exists('inet_pton')) {
            return false;
        }

        $ip_bin = @inet_pton($ip);
        $subnet_bin = @inet_pton($subnet);

        if ($ip_bin === false || $subnet_bin === false) {
            return false;
        }

        // Pad IPv4-mapped addresses to full 16 bytes
        if (strlen($ip_bin) === 4) {
            $ip_bin = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . $ip_bin;
        }
        if (strlen($subnet_bin) === 4) {
            $subnet_bin = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . $subnet_bin;
        }

        $full_bytes = floor($mask / 8);
        $remaining_bits = $mask % 8;

        if (substr($ip_bin, 0, $full_bytes) !== substr($subnet_bin, 0, $full_bytes)) {
            return false;
        }

        if ($remaining_bits > 0) {
            $ip_byte = ord(substr($ip_bin, $full_bytes, 1));
            $subnet_byte = ord(substr($subnet_bin, $full_bytes, 1));
            $mask_byte = 0xFF << (8 - $remaining_bits);
            if (($ip_byte & $mask_byte) !== ($subnet_byte & $mask_byte)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if IP is private/internal
     * PUBLIC METHOD - can be used by other classes
     *
     * @param string $ip IP to check
     * @return bool
     */
    public function is_private_ip($ip) {
        if (!$this->is_valid_ip($ip)) {
            return false;
        }

        foreach (self::PRIVATE_IP_RANGES as $range) {
            if ($this->ip_in_range($ip, $range)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get or generate request ID for caching
     *
     * @return string
     */
    private function get_request_id() {
        static $request_id = null;
        if ($request_id === null) {
            $request_id = uniqid('ip_', true);
        }
        return $request_id;
    }

    /**
     * Clear all caches
     * PUBLIC METHOD - can be called to reset state
     *
     * @return void
     */
    public function clear_cache() {
        $this->ip_cache = [];
        $this->header_ip_cache = null;
        $this->trusted_proxies_cache = null;
    }
}