<?php
/**
 * reCAPTCHA Verifier
 *
 * Stateless wrapper around Google's siteverify endpoint. Used by
 * RecaptchaExtension to validate a client-submitted token server-side.
 *
 * Handles both flavours:
 *   - v2 (checkbox): a token is valid when Google returns success === true.
 *   - v3 (invisible): valid when success === true AND score >= threshold
 *     AND (when an expected action is supplied) the returned action matches.
 *
 * No WordPress option/UI coupling lives here — the caller supplies the secret,
 * version, threshold and (optional) expected action. The only WP dependency is
 * wp_remote_post as the HTTP transport (acceptable thin-integration per Rule 4).
 *
 * @package TitanCart\Extensions
 */

namespace TitanCart\Extensions;

defined( 'ABSPATH' ) || exit;

class RecaptchaVerifier {

	/** Google's verification endpoint. */
	const SITEVERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';

	/**
	 * Verify a token with Google.
	 *
	 * @param string      $token           The client token (g-recaptcha-response).
	 * @param string      $version         'v2' or 'v3'.
	 * @param string      $secret          The matching secret key.
	 * @param string|null $expected_action v3 action to match (null = skip check).
	 * @param float       $min_score       v3 minimum passing score (ignored for v2).
	 * @param string|null $remote_ip       Optional client IP for Google.
	 *
	 * @return array {
	 *     @type bool        $success  Whether the token passed all checks.
	 *     @type float|null  $score    v3 score if present.
	 *     @type string|null $action   v3 action if present.
	 *     @type string      $error    Machine-readable failure reason ('' on success).
	 * }
	 */
	public static function verify(
		string $token,
		string $version,
		string $secret,
		?string $expected_action = null,
		float $min_score = 0.5,
		?string $remote_ip = null
	): array {
		$fail = static function ( string $error, $score = null, $action = null ): array {
			return [ 'success' => false, 'score' => $score, 'action' => $action, 'error' => $error ];
		};

		$token  = trim( $token );
		$secret = trim( $secret );

		if ( $token === '' ) {
			return $fail( 'missing_token' );
		}
		if ( $secret === '' ) {
			return $fail( 'missing_secret' );
		}

		$body = [
			'secret'   => $secret,
			'response' => $token,
		];
		if ( $remote_ip ) {
			$body['remoteip'] = $remote_ip;
		}

		$response = wp_remote_post(
			self::SITEVERIFY_URL,
			[
				'timeout' => 8,
				'body'    => $body,
			]
		);

		if ( is_wp_error( $response ) ) {
			// Network/transport failure. Treat as a hard failure (fail-closed):
			// we cannot prove the token is genuine, so we must not let it pass.
			return $fail( 'transport_error' );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( $code !== 200 ) {
			return $fail( 'http_' . $code );
		}

		$data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) ) {
			return $fail( 'bad_response' );
		}

		// WP.org R2 item 5: decoded remote-body fields normalized at the boundary —
		// success consumed via empty() (bool semantics), score floatval'd, action
		// sanitized (flows back to callers and may reach logs/messages).
		$score  = isset( $data['score'] ) ? (float) $data['score'] : null;
		$action = isset( $data['action'] ) ? sanitize_text_field( (string) $data['action'] ) : null;

		if ( empty( $data['success'] ) ) {
			// Google returns error-codes[] e.g. timeout-or-duplicate, invalid-input-response.
			$codes = isset( $data['error-codes'] ) && is_array( $data['error-codes'] )
				? implode( ',', array_map( 'sanitize_text_field', array_map( 'strval', $data['error-codes'] ) ) )
				: 'verification_failed';
			return $fail( $codes, $score, $action );
		}

		// v3-specific gates.
		if ( $version === 'v3' ) {
			if ( $score === null ) {
				return $fail( 'no_score', $score, $action );
			}
			if ( $score < $min_score ) {
				return $fail( 'low_score', $score, $action );
			}
			if ( $expected_action !== null && $expected_action !== '' && $action !== null && $action !== $expected_action ) {
				return $fail( 'action_mismatch', $score, $action );
			}
		}

		return [ 'success' => true, 'score' => $score, 'action' => $action, 'error' => '' ];
	}
}
