<?php
/**
 * Skip-rule evaluator — Pro overlay only.
 *
 * Secure design (rafter-secure-design / auth):
 * - Identity: WordPress users and client IPs. No new auth primitive.
 * - AuthZ: This file is absent from the wordpress.org Lite zip. Lite
 *   SkipRules::shouldSkip() always returns false. Overlay + license
 *   unlocks role / IP / User-Agent bypass.
 *
 * @package WP_PowerSuite\Captcha\Shared
 */

namespace WP_PowerSuite\Captcha\Shared;

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

if ( class_exists( PremiumSkipRules::class, false ) ) {
	return;
}

final class PremiumSkipRules {

	/**
	 * @param array<string, mixed> $rules
	 */
	public static function shouldSkip( array $rules ): bool {
		if ( empty( $rules['enabled'] ) ) {
			return false;
		}

		if ( ! empty( $rules['roles'] ) && is_array( $rules['roles'] ) && self::userHasRole( $rules['roles'] ) ) {
			return true;
		}

		if ( ! empty( $rules['ips'] ) && is_array( $rules['ips'] ) && self::ipMatches( $rules['ips'] ) ) {
			return true;
		}

		if ( ! empty( $rules['user_agents'] ) && is_array( $rules['user_agents'] ) && self::userAgentMatches( $rules['user_agents'] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * @param array<string> $roles
	 */
	private static function userHasRole( array $roles ): bool {
		if ( ! is_user_logged_in() ) {
			return false;
		}
		$user = wp_get_current_user();
		if ( ! $user || empty( $user->roles ) ) {
			return false;
		}
		foreach ( $roles as $role ) {
			$role = (string) $role;
			if ( $role !== '' && in_array( $role, (array) $user->roles, true ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param array<string> $fragments
	 */
	private static function userAgentMatches( array $fragments ): bool {
		$ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
		if ( $ua === '' ) {
			return false;
		}
		foreach ( $fragments as $frag ) {
			$frag = trim( (string) $frag );
			if ( $frag !== '' && str_contains( $ua, $frag ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param array<string> $entries
	 */
	private static function ipMatches( array $entries ): bool {
		$ip = ClientIp::resolve();
		if ( $ip === '' ) {
			return false;
		}
		foreach ( $entries as $entry ) {
			$entry = trim( (string) $entry );
			if ( $entry === '' ) {
				continue;
			}
			if ( strpos( $entry, '/' ) !== false ) {
				if ( self::cidrMatch( $ip, $entry ) ) {
					return true;
				}
				continue;
			}
			if ( hash_equals( $entry, $ip ) ) {
				return true;
			}
		}
		return false;
	}

	private static function cidrMatch( string $ip, string $cidr ): bool {
		$parts = explode( '/', $cidr, 2 );
		if ( count( $parts ) !== 2 ) {
			return false;
		}
		$subnet     = $parts[0];
		$prefix     = (int) $parts[1];
		$ip_bin     = @inet_pton( $ip );
		$subnet_bin = @inet_pton( $subnet );
		if ( $ip_bin === false || $subnet_bin === false || strlen( $ip_bin ) !== strlen( $subnet_bin ) ) {
			return false;
		}
		$bits   = strlen( $ip_bin ) * 8;
		$prefix = max( 0, min( $prefix, $bits ) );
		$bytes  = intdiv( $prefix, 8 );
		$remain = $prefix % 8;
		if ( $bytes > 0 && substr( $ip_bin, 0, $bytes ) !== substr( $subnet_bin, 0, $bytes ) ) {
			return false;
		}
		if ( $remain === 0 ) {
			return true;
		}
		$mask     = chr( 0xFF << ( 8 - $remain ) );
		$ip_part  = isset( $ip_bin[ $bytes ] ) ? $ip_bin[ $bytes ] : "\0";
		$sub_part = isset( $subnet_bin[ $bytes ] ) ? $subnet_bin[ $bytes ] : "\0";
		return ( ( $ip_part & $mask ) === ( $sub_part & $mask ) );
	}
}
