<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Support;

if (! defined('ABSPATH')) {
    exit;
}

final class IpTools
{
    public static function remoteAddr(): string
    {
        $remote = self::serverValue('REMOTE_ADDR');
        return is_string($remote) && filter_var($remote, FILTER_VALIDATE_IP) ? $remote : '';
    }

    public static function currentIp(): string
    {
        $remote = self::remoteAddr();
        $trustedProxyCidrs = apply_filters('h4bs_trusted_proxy_cidrs', []);
        if (! is_array($trustedProxyCidrs)) {
            $trustedProxyCidrs = [];
        }

        $trustForwardedHeaders = $remote !== ''
            && self::inAnyCidr($remote, array_values(array_filter(array_map('strval', $trustedProxyCidrs))));

        if (! $trustForwardedHeaders) {
            if ($remote !== '') {
                return $remote;
            }

            $server = self::serverValue('SERVER_ADDR');
            return is_string($server) && filter_var($server, FILTER_VALIDATE_IP) ? $server : '';
        }

        $candidates = [
            self::serverValue('HTTP_CF_CONNECTING_IP'),
            self::serverValue('HTTP_TRUE_CLIENT_IP'),
            self::serverValue('HTTP_X_FORWARDED_FOR'),
            $remote,
        ];

        foreach ($candidates as $candidate) {
            if (! is_string($candidate) || $candidate === '') {
                continue;
            }

            if (str_contains($candidate, ',')) {
                $parts = array_map('trim', explode(',', $candidate));
                $candidate = (string) ($parts[0] ?? '');
            }

            if (filter_var($candidate, FILTER_VALIDATE_IP)) {
                return $candidate;
            }
        }

        return '';
    }

    private static function serverValue(string $key): string
    {
        if (! isset($_SERVER[$key])) {
            return '';
        }

        return sanitize_text_field((string) wp_unslash($_SERVER[$key]));
    }

    /**
     * @param string[] $cidrs
     */
    public static function inAnyCidr(string $ip, array $cidrs): bool
    {
        foreach ($cidrs as $cidr) {
            if (self::inCidr($ip, trim($cidr))) {
                return true;
            }
        }
        return false;
    }

    public static function inCidr(string $ip, string $cidr): bool
    {
        if ($ip === '' || $cidr === '') {
            return false;
        }

        if (! str_contains($cidr, '/')) {
            return hash_equals($ip, $cidr);
        }

        [$subnet, $mask] = array_map('trim', explode('/', $cidr, 2));
        $maskInt = (int) $mask;

        $ipBinary = @inet_pton($ip);
        $subnetBinary = @inet_pton($subnet);
        if (! is_string($ipBinary) || ! is_string($subnetBinary)) {
            return false;
        }
        if (strlen($ipBinary) !== strlen($subnetBinary)) {
            return false;
        }

        $totalBits = strlen($ipBinary) * 8;
        if ($maskInt < 0 || $maskInt > $totalBits) {
            return false;
        }

        $fullBytes = intdiv($maskInt, 8);
        $remainingBits = $maskInt % 8;

        if ($fullBytes > 0 && substr($ipBinary, 0, $fullBytes) !== substr($subnetBinary, 0, $fullBytes)) {
            return false;
        }

        if ($remainingBits === 0) {
            return true;
        }

        if ($fullBytes >= strlen($ipBinary)) {
            return false;
        }

        $maskByte = (0xFF << (8 - $remainingBits)) & 0xFF;
        $ipByte = ord($ipBinary[$fullBytes]);
        $subnetByte = ord($subnetBinary[$fullBytes]);

        return ($ipByte & $maskByte) === ($subnetByte & $maskByte);
    }
}
