<?php
/**
 * Shared Client IP Resolver
 *
 * Single implementation of the "what IP is this request from" logic that the
 * three captcha modules previously duplicated (Simple, Google, Turnstile).
 *
 * Order mirrors the proxy-aware chain used by hosts in front of WordPress:
 * Cloudflare → generic reverse proxy → the direct REMOTE_ADDR from PHP.
 *
 * @package WP_PowerSuite\Captcha\Shared
 * @since 1.1.0
 */

namespace WP_PowerSuite\Captcha\Shared;

/**
 * Client IP utility.
 *
 * Stateless static helpers — the class never holds state so the same
 * resolution logic is safe across the login, checkout, and AJAX lifecycles.
 */
final class ClientIp {

	/**
	 * Return the best client IP for this request, or an empty string.
	 *
	 * Callers that want a guaranteed non-empty fallback can use {@see self::resolveOrDefault()}.
	 */
	public static function resolve(): string {
		$cf_ip = self::headerIp( 'HTTP_CF_CONNECTING_IP' );
		if ( $cf_ip !== '' ) {
			return $cf_ip;
		}

		if ( self::shouldTrustProxyHeaders() ) {
			$real_ip = self::headerIp( 'HTTP_X_REAL_IP' );
			if ( $real_ip !== '' ) {
				return $real_ip;
			}

			$xff = self::headerIp( 'HTTP_X_FORWARDED_FOR' );
			if ( $xff !== '' ) {
				return $xff;
			}
		}

		return self::headerIp( 'REMOTE_ADDR' );
	}

	/**
	 * Honor X-Real-IP / X-Forwarded-For only behind Cloudflare or when explicitly enabled.
	 */
	private static function shouldTrustProxyHeaders(): bool {
		if ( self::headerIp( 'HTTP_CF_CONNECTING_IP' ) !== '' ) {
			return true;
		}

		return (bool) apply_filters( 'wp_powersuite_trust_proxy_headers', false );
	}

	private static function headerIp( string $key ): string {
		if ( empty( $_SERVER[ $key ] ) ) {
			return '';
		}
		$raw = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
		if ( strpos( $raw, ',' ) !== false ) {
			$parts = explode( ',', $raw );
			$raw   = trim( (string) $parts[0] );
		}
		if ( filter_var( $raw, FILTER_VALIDATE_IP ) ) {
			return $raw;
		}

		return '';
	}

	/**
	 * Resolve the client IP or return `0.0.0.0` when nothing is available.
	 *
	 * Used by siteverify calls (reCAPTCHA/Turnstile) which prefer the explicit
	 * sentinel over an empty field.
	 */
	public static function resolveOrDefault(): string {
		$ip = self::resolve();
		return $ip !== '' ? $ip : '0.0.0.0';
	}
}
