<?php

declare(strict_types=1);

namespace SecurWP\Includes;

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

/**
 * Utility class for IP address operations.
 *
 */
class IpUtils {

    /**
     * Check if an IP address is within a CIDR range.
     * Supports both IPv4 and IPv6.
     *
     * @param string $ip    The IP address to check.
     * @param string $range The CIDR range (e.g., '192.168.1.0/24' or '2001:db8::/32'), or a single IP.
     * @return bool
     */
    public static function ip_in_range( string $ip, string $range ): bool {
        // Exact match (no CIDR notation). Use binary comparison via
        // inet_pton so different IPv6 representations match correctly
        // (e.g. "2001:db8::1" vs "2001:0db8:0:0:0:0:0:1").
        if ( strpos( $range, '/' ) === false ) {
            $a = @inet_pton( $ip );
            $b = @inet_pton( $range );
            return $a !== false && $b !== false && $a === $b;
        }

        [ $subnet, $bits_str ] = explode( '/', $range, 2 );
        $bits = (int) $bits_str;

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

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

        // Same address family required (IPv4 = 4 bytes, IPv6 = 16 bytes).
        if ( strlen( $ip_bin ) !== strlen( $subnet_bin ) ) {
            return false;
        }

        $max_bits = strlen( $ip_bin ) * 8;
        if ( $bits < 0 || $bits > $max_bits ) {
            return false;
        }

        // Build bitmask.
        $full_bytes      = (int) floor( $bits / 8 );
        $remaining_bits  = $bits % 8;

        $mask = str_repeat( "\xff", $full_bytes );
        if ( $remaining_bits > 0 ) {
            $mask .= chr( ( 0xff << ( 8 - $remaining_bits ) ) & 0xff );
        }
        $mask = str_pad( $mask, strlen( $ip_bin ), "\x00" );

        return ( $ip_bin & $mask ) === ( $subnet_bin & $mask );
    }

    /**
     * Check if an IP matches any entry in a list of IPs/CIDRs.
     *
     * Convenience wrapper around ip_in_range() for checking against
     * multiple ranges (e.g. trusted proxy lists, provider presets).
     *
     * @param string   $ip     The IP address to check.
     * @param string[] $ranges Array of IPs or CIDR ranges.
     * @return bool
     */
    public static function ip_in_range_list( string $ip, array $ranges ): bool {
        foreach ( $ranges as $range ) {
            $range = trim( (string) $range );
            if ( '' === $range ) {
                continue;
            }
            if ( self::ip_in_range( $ip, $range ) ) {
                return true;
            }
        }
        return false;
    }
}
