<?php

namespace ZenoChatbot\Helper;

class IpDetector
{
    /**
     * Get the client IP address.
     *
     * @param bool $cached Whether to use static cache for this call
     * @return string
     */
    public static function getClientIp($cached = true): string
    {
        static $ipCache = null;

        if ($cached && $ipCache !== null) {
            return $ipCache;
        }

        $ip = static::detectIp();

        if ($cached) {
            $ipCache = $ip;
        }

        return $ip;
    }

    /**
     * Detect IP address from server variables.
     *
     * Important: All `$_SERVER` variables must be passed through `wp_unslash()`
     * and `sanitize_text_field()` before use, to comply with WordPress coding
     * standards and to ensure input is safe.
     *
     * @return string
     */
    protected static function detectIp(): string
    {
        if (empty($_SERVER['REMOTE_ADDR'])) {
            return '127.0.0.1';
        }

        // Always unslash + sanitize any superglobal access.
        $remoteAddr = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
        $ip = null;

        // Cloudflare support (check if the request came through CF).
        if (
            !empty($_SERVER['HTTP_CF_CONNECTING_IP'])
            && static::isCloudflareIp($remoteAddr)
        ) {
            $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
        }

        // Proxy headers if running locally.
        if (!$ip && $remoteAddr === '127.0.0.1') {
            $ip = static::getIpFromProxy();
        }

        // Fallback to REMOTE_ADDR.
        if (!$ip) {
            $ip = $remoteAddr;
        }

        return static::sanitizeIp($ip);
    }

    /**
     * Try to extract IP from common proxy headers.
     *
     * Note: Proxy headers are user-controllable and should never be trusted
     * blindly. We still sanitize + validate them, and fallback to REMOTE_ADDR
     * if invalid.
     *
     * @return string|null
     */
    protected static function getIpFromProxy(): ?string
    {
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
            return sanitize_text_field(wp_unslash($_SERVER['HTTP_CLIENT_IP']));
        }

        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $forwarded = explode(
                ',',
                sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']))
            );
            return trim(reset($forwarded));
        }

        return null;
    }

    /**
     * Sanitize and validate an IP address.
     *
     * Removes port numbers if present, then validates with FILTER_VALIDATE_IP.
     * Falls back to localhost (127.0.0.1) if the result is not a valid IP.
     *
     * @param string $ip
     * @return string
     */
    protected static function sanitizeIp(string $ip): string
    {
        // Strip port if present (IPv4 only: "1.2.3.4:5678").
        if (strpos($ip, ':') !== false && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $ip = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '$1', $ip);
        }

        return filter_var($ip, FILTER_VALIDATE_IP) ?: '127.0.0.1';
    }

    /**
     * Validate Cloudflare source IP.
     *
     * Cloudflare provides a known list of CIDR ranges. If REMOTE_ADDR matches
     * one of these, then HTTP_CF_CONNECTING_IP can be trusted.
     *
     * @param string $ip
     * @return bool
     */
    protected static function isCloudflareIp(string $ip): bool
    {
        static $cfRanges = [
            '173.245.48.0/20',
            '103.21.244.0/22',
            '103.22.200.0/22',
            '103.31.4.0/22',
            '141.101.64.0/18',
            '108.162.192.0/18',
            '190.93.240.0/20',
            '188.114.96.0/20',
            '197.234.240.0/22',
            '198.41.128.0/17',
            '162.158.0.0/15',
            '104.16.0.0/13',
            '104.24.0.0/14',
            '172.64.0.0/13',
            '131.0.72.0/22',
        ];

        foreach ($cfRanges as $range) {
            if (static::ipInRange($ip, $range)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if an IP is inside a CIDR range.
     *
     * @param string $ip
     * @param string $cidr
     * @return bool
     */
    protected static function ipInRange(string $ip, string $cidr): bool
    {
        [$subnet, $mask] = explode('/', $cidr);
        $ipLong     = ip2long($ip);
        $subnetLong = ip2long($subnet);
        $mask       = ~((1 << (32 - $mask)) - 1);

        return ($ipLong & $mask) === ($subnetLong & $mask);
    }
}
