<?php
/**
 * Shared WooCommerce checkout request context for captcha modules.
 *
 * Two situations make a checkout captcha reject shoppers who did nothing wrong,
 * and every captcha module needs the same answer to both:
 *
 * 1. Account creation during checkout. WooCommerce funnels checkout signups
 *    through the same `woocommerce_registration_errors` filter as the My Account
 *    register form, inside the same request that already verified the checkout
 *    token. Tokens are single-use, so re-verifying burns the token
 *    (timeout-or-duplicate) — and the checkout form never rendered a register
 *    widget in the first place. Checkout validation owns that request.
 *
 * 2. Express wallet payments (Apple Pay / Google Pay / Stripe Link). The wallet
 *    sheet replaces the checkout form, the widget is never shown or solvable,
 *    so requiring a token guarantees rejection.
 *
 * @package WP_PowerSuite\Captcha\Shared
 * @since 1.1.0
 */

namespace WP_PowerSuite\Captcha\Shared;

final class WooCheckoutContext {

	/**
	 * Payment credential keys Stripe / WooPayments attach to a real wallet charge.
	 *
	 * Type markers alone (`payment_request_type`, `express_payment_type`) are
	 * client-supplied and trivial to forge; a captcha skip must also see one of
	 * these non-empty credential fields.
	 *
	 * @var list<string>
	 */
	private const WALLET_CREDENTIAL_KEYS = [
		'wc-stripe-payment-method',
		'wcpay-payment-method',
		'stripe_source',
		'payment_method',
		'wc-stripe-payment-intent',
		'wcpay-payment-intent',
	];

	/**
	 * Whether the current registration validation is running inside a checkout flow.
	 *
	 * Classic checkout fires `woocommerce_checkout_process` before it creates the
	 * account; Blocks checkout creates accounts inside a Store API checkout request.
	 * XML-RPC never renders a widget at all. In each of those cases the checkout
	 * validator (or nothing) is authoritative and the register check must stand down.
	 *
	 * Important: do not treat every `REST_REQUEST` as checkout — that would skip
	 * Woo registration captcha for any REST caller of `wc_create_new_customer()`.
	 */
	public static function registrationIsPartOfCheckout(): bool {
		if ( did_action( 'woocommerce_checkout_process' ) > 0 ) {
			return true;
		}
		if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
			return true;
		}
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			$uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
			// Store API checkout (and batch requests that may embed it).
			if ( $uri !== '' && preg_match( '#/wc/store(?:/v[0-9]+)?/(?:checkout|batch)(?:/|\?|$)#', $uri ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Whether classic checkout captcha should run for this request.
	 *
	 * This runs only on `woocommerce_checkout_process`, so a localized slug
	 * such as `/kasse/` is still checkout. Do not require "checkout" in REQUEST_URI.
	 */
	public static function shouldEnforceClassicCheckoutCaptcha(): bool {
		if ( function_exists( 'is_admin' ) && is_admin() && ! wp_doing_ajax() ) {
			return false;
		}

		$wc_ajax = isset( $_REQUEST['wc-ajax'] ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wc-ajax'] ) ) : '';
		if ( $wc_ajax !== '' && $wc_ajax !== 'checkout' ) {
			return false;
		}

		return true;
	}

	/**
	 * Whether a Store API checkout request pays via an express wallet.
	 *
	 * @param \WP_REST_Request $request Store API checkout request.
	 */
	public static function isExpressPayStoreApi( \WP_REST_Request $request ): bool {
		$payment_method = (string) ( $request->get_param( 'payment_method' ) ?? '' );
		$payment_data   = $request->get_param( 'payment_data' );

		$express    = false;
		$credential = false;
		if ( is_array( $payment_data ) ) {
			foreach ( $payment_data as $item ) {
				if ( ! is_array( $item ) || ! isset( $item['key'], $item['value'] ) || $item['value'] === '' || $item['value'] === null ) {
					continue;
				}
				$key = (string) $item['key'];
				if ( in_array( $key, [ 'express_payment_type', 'payment_request_type' ], true ) ) {
					$express = true;
				}
				if ( in_array( $key, self::WALLET_CREDENTIAL_KEYS, true ) ) {
					// Store API already carries gateway id in `payment_method`; ignore the
					// same key in payment_data unless it looks like a token (pm_/pi_/src_).
					$value = (string) $item['value'];
					if ( $key !== 'payment_method' || self::looksLikeWalletToken( $value ) ) {
						$credential = true;
					}
				}
			}
		}

		return self::filterExpressSkip( $payment_method, $express && $credential, $payment_data );
	}

	/**
	 * Whether a classic checkout POST pays via an express wallet
	 * (Stripe / WooPayments payment request buttons post these markers).
	 */
	public static function isExpressPayClassic(): bool {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- WooCommerce owns nonce handling for its checkout POST; this only inspects gateway markers.
		$payment_method = isset( $_POST['payment_method'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['payment_method'] ) ) : '';

		$express = false;
		foreach ( [ 'payment_request_type', 'express_payment_type' ] as $marker ) {
			if ( isset( $_POST[ $marker ] ) && is_string( $_POST[ $marker ] ) && $_POST[ $marker ] !== '' ) {
				$express = true;
				break;
			}
		}

		$credential = false;
		foreach ( self::WALLET_CREDENTIAL_KEYS as $key ) {
			if ( $key === 'payment_method' ) {
				continue; // Gateway id is always present on classic checkout; not a wallet token.
			}
			if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) && $_POST[ $key ] !== '' ) {
				$credential = true;
				break;
			}
		}
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		return self::filterExpressSkip( $payment_method, $express && $credential, null );
	}

	/**
	 * Only Stripe-family gateways are known to submit wallet payments without the
	 * checkout form; other gateways keep full protection unless a site opts in
	 * through the filter.
	 *
	 * @param string $payment_method Gateway id from the request.
	 * @param bool   $express        Whether express wallet markers + credentials were detected.
	 * @param mixed  $payment_data   Raw payment data for filter consumers.
	 */
	private static function filterExpressSkip( string $payment_method, bool $express, $payment_data ): bool {
		$skip = $express && in_array( $payment_method, [ 'woocommerce_payments', 'stripe' ], true );

		/**
		 * Whether to skip captcha verification for an express wallet checkout.
		 *
		 * @param bool   $skip           Computed default.
		 * @param string $payment_method Gateway id from the request.
		 * @param mixed  $payment_data   Raw payment data (Store API) or null (classic).
		 */
		return (bool) apply_filters( 'wp_powersuite_captcha_skip_express_pay', $skip, $payment_method, $payment_data );
	}

	/**
	 * @param string $value Candidate payment token / intent id.
	 */
	private static function looksLikeWalletToken( string $value ): bool {
		return (bool) preg_match( '/^(pm_|pi_|src_|seti_)/', $value );
	}
}
