<?php

namespace Formatrica\Support;

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

/**
 * Matches a client IP against an allowlist of exact IPs and CIDR ranges
 * (IPv4 + IPv6, inet_pton-based).
 */
final class Client_Ip_Matcher
{
    /**
     * @param list<string> $allowlist Exact IPs or CIDR ranges.
     */
    public static function matches_any(string $ip, array $allowlist): bool
    {
        $ip_bin = @inet_pton($ip);
        if ($ip_bin === false) {
            return false;
        }

        foreach ($allowlist as $entry) {
            if (!is_string($entry) || $entry === '') {
                continue;
            }
            $entry = trim($entry);

            if (strpos($entry, '/') === false) {
                $entry_bin = @inet_pton($entry);
                if ($entry_bin !== false && hash_equals($entry_bin, $ip_bin)) {
                    return true;
                }
                continue;
            }

            [$subnet, $bits] = explode('/', $entry, 2);
            if (!is_numeric($bits)) {
                continue;
            }
            $subnet_bin = @inet_pton(trim($subnet));
            if ($subnet_bin === false || strlen($subnet_bin) !== strlen($ip_bin)) {
                continue; // family mismatch (v4 list entry vs v6 client, etc.)
            }
            if (self::cidr_match($ip_bin, $subnet_bin, (int) $bits)) {
                return true;
            }
        }

        return false;
    }

    private static function cidr_match(string $ip_bin, string $subnet_bin, int $bits): bool
    {
        $max_bits = strlen($ip_bin) * 8;
        if ($bits < 0 || $bits > $max_bits) {
            return false;
        }
        if ($bits === 0) {
            return true;
        }

        $full_bytes = intdiv($bits, 8);
        $remainder = $bits % 8;

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

        $mask = 0xFF << (8 - $remainder) & 0xFF;
        return (ord($ip_bin[$full_bytes]) & $mask) === (ord($subnet_bin[$full_bytes]) & $mask);
    }

    /**
     * Validate a single allowlist entry (exact IP or CIDR). Used by the
     * sanitizer so junk lines are dropped at save.
     */
    public static function is_valid_entry(string $entry): bool
    {
        $entry = trim($entry);
        if ($entry === '') {
            return false;
        }
        if (strpos($entry, '/') === false) {
            return @inet_pton($entry) !== false;
        }
        [$subnet, $bits] = explode('/', $entry, 2);
        if (!is_numeric($bits)) {
            return false;
        }
        $subnet_bin = @inet_pton(trim($subnet));
        if ($subnet_bin === false) {
            return false;
        }
        $max_bits = strlen($subnet_bin) * 8;
        return (int) $bits >= 0 && (int) $bits <= $max_bits;
    }
}
