<?php
/**
 * Simple CAPTCHA Module
 *
 * Provides human verification using math problems, custom text questions,
 * or image challenges. All processed locally without external services.
 *
 * @package SimpleCaptcha
 * @since 1.0.0
 */

namespace SimpleCaptcha;

use WP_PowerSuite\Captcha\AbstractCaptchaModule;
use WP_PowerSuite\Captcha\Shared\CaptchaRuntime;
use WP_PowerSuite\Captcha\Shared\ClientIp;
use WP_PowerSuite\Captcha\Shared\Counters;
use WP_PowerSuite\Captcha\Shared\DashboardWidget;
use WP_PowerSuite\Captcha\Shared\LoginLayoutStyles;
use WP_PowerSuite\Captcha\Shared\CaptchaFormContext;
use WP_PowerSuite\Captcha\Shared\SkipRules;
use WP_PowerSuite\Captcha\Shared\WooCheckoutContext;

require_once __DIR__ . '/FeatureGate.php';
wp_powersuite_require_freemium_file( 'simple-captcha', 'ExtendedIntegrations.php' );
wp_powersuite_require_freemium_file( 'simple-captcha', 'FormIntegrations.php' );
wp_powersuite_require_freemium_file( 'simple-captcha', 'includes/PremiumSkipRules.php' );

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

/**
 * Simple CAPTCHA Module Class
 *
 * @since 1.0.0
 */
class Module extends AbstractCaptchaModule {

	private const MODULE_ID = 'simple-captcha';
	private const MODULE_VERSION = '1.2.3';
	private const IMAGE_REFRESH_HANDLE = 'wpps-simple-captcha-image-refresh';
	private const COMMENT_RESET_HANDLE = 'wpps-simple-captcha-comment-reset';
	private const MAX_FAILS_PER_CHALLENGE = 5;
	/** @var string Store API namespace for WooCommerce Blocks checkout */
	public const WOO_STORE_API_NAMESPACE = 'wp-powersuite-simple-captcha';
	private const WOO_CHECKOUT_FRONT_HANDLE = 'wpps-simple-captcha-woo-checkout';
	private const TRANSIENT_PREFIX = 'wpps_sc_';
	private const TRANSIENT_EXPIRE = 600;
	private const KEY_LENGTH = 16;
	private const RATE_LIMIT_IMAGE = 80;
	private const RATE_LIMIT_REFRESH = 25;
	private const RATE_LIMIT_VERIFY = 40;
	private const RATE_WINDOW = 60;
	private const ANSWER_MAX_LEN = 100;

	private static bool $captcha_style_enqueued  = false;
	private static bool $captcha_generator_loaded = false;

	/** @var array<string, true> Successful verify() outcomes this request (wp_signon may validate twice). */
	private static array $verify_ok_memo = [];

	/** @var bool Prevent duplicate classic checkout validation in one request */
	private static bool $woo_classic_checkout_gate_done = false;

	/** @var bool Classic checkout hook already printed the widget */
	private static bool $woo_checkout_classic_emitted = false;

	/** @var bool Blocks checkout actions block already prefixed with the widget */
	private static bool $woo_checkout_block_emitted = false;

	/** @var bool Whether custom-login template_redirect hook is attached */
	private static bool $custom_login_url_template_redirect_attached = false;

	/** @var bool Elementor Pro form field registration hooks attached */
	private static bool $elementor_forms_hooks_registered = false;

	/**
	 * Store API honeypot values (name => posted value) for Blocks checkout.
	 *
	 * @var array<string, string>|null
	 */
	private ?array $honeypot_posted_override = null;

	private ?array $settings_cache = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	/**
	 * Simple Captcha has no external keys — once enabled it is always configured
	 * (challenges generate themselves). Used by the cross-module admin banner.
	 */
	public function isConfigured(): bool {
		return true;
	}

	public function getName(): string {
		return __( 'Simple CAPTCHA', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Ask a quick math question, custom prompt, or image check before someone can submit a form or log in. Everything runs on your server—no third-party CAPTCHA account required.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return 'both';
	}

	public function getDependencies(): array {
		return [];
	}

	protected function registerHooks(): void {
		$settings = $this->getSettings();

		if ( CaptchaRuntime::needsWpLoginScreenAssets( $settings ) ) {
			add_action( 'login_enqueue_scripts', [ $this, 'enqueueCaptchaStyles' ] );
			add_action( 'login_enqueue_scripts', [ $this, 'enqueueLoginCaptchaLayoutStyles' ], 100 );
			add_action( 'init', [ $this, 'registerCustomLoginUrlCaptchaHook' ], 999 );
		}

		$captcha_type = $settings['captcha_type'] ?? 'math';
		if ( 'image' === ( $captcha_type ?? 'math' ) ) {
			add_action( 'wp_ajax_wpps_simple_captcha_image', [ $this, 'ajaxOutputCaptchaImage' ] );
			add_action( 'wp_ajax_nopriv_wpps_simple_captcha_image', [ $this, 'ajaxOutputCaptchaImage' ] );
			add_action( 'wp_ajax_wpps_simple_captcha_refresh', [ $this, 'ajaxRefreshImageCaptcha' ] );
			add_action( 'wp_ajax_nopriv_wpps_simple_captcha_refresh', [ $this, 'ajaxRefreshImageCaptcha' ] );
		}

		if ( ! empty( $settings['protect_comments'] ) && ! empty( $settings['ajax_comment_reset'] ) ) {
			add_action( 'wp_enqueue_scripts', [ $this, 'enqueueCommentResetScript' ], 20 );
		}

		if ( ! empty( $settings['protect_login'] ) ) {
			add_action( 'login_form', [ $this, 'renderLoginWidget' ] );
			add_filter( 'authenticate', [ $this, 'validateLogin' ], 30, 3 );
		}

		if ( ! empty( $settings['protect_registration'] ) ) {
			add_action( 'register_form', [ $this, 'renderWidget' ] );
			add_filter( 'registration_errors', [ $this, 'validateRegistration' ], 10, 3 );
		}

		if ( ! empty( $settings['protect_password_reset'] ) ) {
			add_action( 'lostpassword_form', [ $this, 'renderWidget' ] );
			add_action( 'resetpass_form', [ $this, 'renderWidgetOnResetPass' ], 10, 1 );
			add_filter( 'allow_password_reset', [ $this, 'validatePasswordReset' ], 10, 2 );
			add_action( 'validate_password_reset', [ $this, 'validateResetPassForm' ], 10, 2 );
		}

		// Independent hooks; woo_reviews only registers when protect_comments is off,
		// to avoid rendering or validating twice on product pages where both apply.
		if ( ! empty( $settings['protect_comments'] ) ) {
			add_filter( 'comment_form_submit_field', [ $this, 'injectCommentWidget' ], 10, 2 );
			add_action( 'comment_form_top', [ $this, 'renderCommentSimpleCaptchaErrorNotice' ] );
			add_filter( 'preprocess_comment', [ $this, 'validateComment' ] );
		}

		if ( ! empty( $settings['woo_reviews'] ) && empty( $settings['protect_comments'] ) ) {
			add_filter( 'comment_form_submit_field', [ $this, 'injectCommentWidgetForReviews' ], 10, 2 );
			add_action( 'comment_form_top', [ $this, 'renderCommentSimpleCaptchaErrorNotice' ] );
			add_filter( 'preprocess_comment', [ $this, 'validateProductReview' ] );
		}

		if ( class_exists( 'WooCommerce' ) ) {
			if ( CaptchaRuntime::hasWooCommerceIntegration( $settings ) ) {
				add_action( 'before_woocommerce_init', [ $this, 'declareWooCommerceCompatibility' ] );
			}
			$this->initWooCommerce( $settings );
		}

		$this->initElementorForms( $settings );

		add_filter( 'wpps_verify_simple_captcha', [ $this, 'filterVerify' ], 10, 3 );
		add_filter( 'wpps_captcha_error_message', [ $this, 'filterCaptchaErrorMessage' ], 10, 3 );

		if ( is_admin() ) {
			add_action( 'init', [ $this, 'registerCaptchaDashboardWidget' ], 5 );
		}

		if ( class_exists( FormIntegrations::class, false ) ) {
			FormIntegrations::boot( $this );
		}

		if ( class_exists( ExtendedIntegrations::class, false ) ) {
			ExtendedIntegrations::boot( $this );
		}
	}

	/** @return array<string, mixed> */
	public function getCaptchaSettings(): array {
		return $this->getSettings();
	}

	public function enqueueLoginCaptchaLayoutStyles(): void {
		if ( ! CaptchaRuntime::needsWpLoginScreenAssets( $this->getSettings() ) ) {
			return;
		}
		LoginLayoutStyles::enqueue();
	}

	public function registerCustomLoginUrlCaptchaHook(): void {
		if ( self::$custom_login_url_template_redirect_attached ) {
			return;
		}
		if ( ! CaptchaRuntime::needsWpLoginScreenAssets( $this->getSettings() ) || ! has_filter( 'wp_powersuite_is_custom_login_url_request' ) ) {
			return;
		}
		self::$custom_login_url_template_redirect_attached = true;
		add_action( 'template_redirect', [ $this, 'enqueueCaptchaStylesForCustomLoginUrl' ], 0 );
	}

	public function enqueueCaptchaStylesForCustomLoginUrl(): void {
		if ( ! CaptchaRuntime::needsWpLoginScreenAssets( $this->getSettings() ) ) {
			return;
		}
		if ( ! apply_filters( 'wp_powersuite_is_custom_login_url_request', false ) ) {
			return;
		}
		$this->enqueueCaptchaStyles();
		LoginLayoutStyles::enqueueStandalone();
	}

	public function declareWooCommerceCompatibility(): void {
		if ( ! class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
			return;
		}
		$main = defined( 'WP_POWERSUITE_PLUGIN_FILE' ) ? WP_POWERSUITE_PLUGIN_FILE : '';
		if ( $main === '' ) {
			return;
		}
		\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', $main, true );
		\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', $main, true );
		\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'product_block_editor', $main, true );
	}

	/**
	 * Register dashboard row after init so getName() does not trigger translations on plugins_loaded (WP 6.7+).
	 */
	public function registerCaptchaDashboardWidget(): void {
		if ( ! is_admin() ) {
			return;
		}
		DashboardWidget::register( self::MODULE_ID, $this->getName() );
	}

	public function filterCaptchaErrorMessage( string $default, string $context = '', string $module_id = '' ): string {
		if ( $module_id !== '' && $module_id !== self::MODULE_ID ) {
			return $default;
		}
		return $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() );
	}

	public function filterVerify( $pre, string $key, string $answer ): bool {
		return $this->verify( $key, $answer );
	}

	private function initWooCommerce( array $settings ): void {
		if ( ! empty( $settings['woo_login'] ) ) {
			add_action( 'woocommerce_login_form', [ $this, 'renderWidget' ] );
			add_filter( 'woocommerce_process_login_errors', [ $this, 'validateWooLogin' ], 10, 3 );
		}

		if ( ! empty( $settings['woo_registration'] ) ) {
			add_action( 'woocommerce_register_form', [ $this, 'renderWidget' ] );
			add_filter( 'woocommerce_registration_errors', [ $this, 'validateWooRegistration' ], 10, 3 );
		}

		if ( ! empty( $settings['woo_lost_password'] ) ) {
			add_action( 'woocommerce_lostpassword_form', [ $this, 'renderWidget' ] );
			add_filter( 'allow_password_reset', [ $this, 'validatePasswordReset' ], 10, 2 );
		}

		if ( ! empty( $settings['woo_checkout'] ) ) {
			add_action( 'woocommerce_review_order_before_submit', [ $this, 'renderWooCheckoutWidget' ], 5 );
			add_filter( 'render_block_woocommerce/checkout-actions-block', [ $this, 'prependCaptchaToCheckoutActionsBlock' ], 999, 1 );
			add_action( 'woocommerce_checkout_process', [ $this, 'validateWooCheckout' ] );
			add_action( 'woocommerce_store_api_checkout_update_order_from_request', [ $this, 'validateWooCheckoutStoreApi' ], 10, 2 );
			add_action( 'woocommerce_loaded', [ $this, 'registerWooCheckoutStoreApiExtension' ], 20 );
			add_action( 'wp_enqueue_scripts', [ $this, 'enqueueWooCheckoutFrontScript' ], 30 );
		}
	}

	/**
	 * Elementor Pro Forms: register a real Form field (server-rendered).
	 * Deferred to `elementor/loaded` when Elementor boots after this module.
	 */
	private function initElementorForms( array $settings ): void {
		if ( empty( $settings['elementor_forms'] ) ) {
			return;
		}
		if ( did_action( 'elementor/loaded' ) ) {
			$this->registerElementorProFormsIntegration();
		} else {
			add_action( 'elementor/loaded', [ $this, 'registerElementorProFormsIntegration' ] );
		}
	}

	/**
	 * Attach Elementor Pro form-field registration once Pro is available.
	 */
	public function registerElementorProFormsIntegration(): void {
		if ( self::$elementor_forms_hooks_registered ) {
			return;
		}
		$settings = $this->getSettings();
		if ( empty( $settings['elementor_forms'] ) || ! did_action( 'elementor/loaded' ) || ! defined( 'ELEMENTOR_PRO_VERSION' ) ) {
			return;
		}
		self::$elementor_forms_hooks_registered = true;
		add_action( 'elementor_pro/forms/fields/register', [ $this, 'registerElementorProFormField' ] );
	}

	/**
	 * @param \ElementorPro\Modules\Forms\Registrars\Form_Fields_Registrar|mixed $form_fields_registrar
	 */
	public function registerElementorProFormField( $form_fields_registrar ): void {
		if ( ! class_exists( '\ElementorPro\Modules\Forms\Fields\Field_Base' ) || ! is_object( $form_fields_registrar ) || ! method_exists( $form_fields_registrar, 'register' ) ) {
			return;
		}
		if ( ! wp_powersuite_require_freemium_file( 'simple-captcha', 'includes/elementor-pro-form-field.php' ) ) {
			return;
		}
		$form_fields_registrar->register( new Elementor_Pro_Form_Field( $this ) );
	}

	/**
	 * Output Simple CAPTCHA inside an Elementor Pro Form “Simple CAPTCHA” field.
	 */
	public function renderForElementorField( string $context_key, string $html_id_suffix ): void {
		$this->renderWidget( $context_key, false, $html_id_suffix );
	}

	/**
	 * @return bool True if verification passed or was skipped.
	 */
	public function validateSimpleCaptchaRequest(): bool {
		if ( $this->shouldSkip() ) {
			return true;
		}
		return $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() );
	}

	/**
	 * User-facing error when {@see validateSimpleCaptchaRequest()} is false.
	 */
	public function getSimpleCaptchaRequestErrorMessage(): string {
		return $this->failedCaptchaMessage();
	}

	public function failedCaptchaMessage( string $key = '', string $answer = '' ): string {
		if ( $key === '' && $answer === '' ) {
			$key    = $this->getKeyFromRequest();
			$answer = $this->getAnswerFromRequest();
		}
		return $this->messageForFailedVerification( $key, $answer );
	}

	/**
	 * @param array<string, mixed> $form_data Fluent submission array.
	 * @return array{0: string, 1: string}
	 */
	public function captchaPairFromFluent( array $form_data ): array {
		return $this->getCaptchaPairFromFluentRequest( $form_data );
	}

	public function verifyPostedCaptcha( ?string $key = null, ?string $answer = null ): bool {
		return $this->verify(
			$key ?? $this->getKeyFromRequest(),
			$answer ?? $this->getAnswerFromRequest()
		);
	}

	/**
	 * GD image CAPTCHA only — load only when image mode is used (saves parse for math/text).
	 */
	private function loadCaptchaGenerator(): void {
		if ( self::$captcha_generator_loaded ) {
			return;
		}
		self::$captcha_generator_loaded = true;
		require_once __DIR__ . '/includes/class-captcha-generator.php';
	}

	public function enqueueCaptchaStyles(): void {
		if ( self::$captcha_style_enqueued ) {
			return;
		}
		self::$captcha_style_enqueued = true;
		wp_enqueue_style(
			'wpps-simple-captcha',
			WP_POWERSUITE_PLUGIN_URL . 'modules/simple-captcha/assets/captcha.css',
			[],
			self::MODULE_VERSION
		);
		if ( ( $this->getSettings()['captcha_type'] ?? 'math' ) === 'image' ) {
			$this->enqueueImageRefreshScript();
		}
	}

	public function enqueueImageRefreshScript(): void {
		if ( ! defined( 'WP_POWERSUITE_PLUGIN_URL' ) || wp_script_is( self::IMAGE_REFRESH_HANDLE, 'enqueued' ) ) {
			return;
		}
		wp_enqueue_script(
			self::IMAGE_REFRESH_HANDLE,
			WP_POWERSUITE_PLUGIN_URL . 'modules/simple-captcha/assets/image-refresh.js',
			[],
			self::MODULE_VERSION,
			true
		);
		wp_localize_script(
			self::IMAGE_REFRESH_HANDLE,
			'wppsSimpleCaptchaRefresh',
			[
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'wpps_captcha_refresh' ),
			]
		);
	}

	public function enqueueCommentResetScript(): void {
		if ( ! defined( 'WP_POWERSUITE_PLUGIN_URL' ) || ! is_singular() || ! comments_open() ) {
			return;
		}
		if ( wp_script_is( self::COMMENT_RESET_HANDLE, 'enqueued' ) ) {
			return;
		}
		wp_enqueue_script(
			self::COMMENT_RESET_HANDLE,
			WP_POWERSUITE_PLUGIN_URL . 'modules/simple-captcha/assets/comment-reset.js',
			[],
			self::MODULE_VERSION,
			true
		);
	}

	/**
	 * @param array<string, mixed> $payload Challenge data (answer/code/answers).
	 * @return array<string, mixed>
	 */
	private function challengePayload( array $payload ): array {
		$settings = $this->getSettings();
		$base     = [
			'created' => time(),
			'fails'   => 0,
		];
		if ( ! empty( $settings['anti_bot_honeypot'] ) ) {
			$base['hp'] = 'wpps_sc_hp_' . bin2hex( random_bytes( 4 ) );
		}
		return array_merge( $base, $payload );
	}

	/**
	 * @param array<string, mixed> $payload
	 * @return array<string, mixed> Stored payload (for honeypot field name).
	 */
	private function storeChallenge( string $key, array $payload ): array {
		$full = $this->challengePayload( $payload );
		set_transient( self::TRANSIENT_PREFIX . $key, $full, self::TRANSIENT_EXPIRE );
		return $full;
	}

	private function printHoneypotField( array $stored ): void {
		$hp = isset( $stored['hp'] ) ? (string) $stored['hp'] : '';
		if ( $hp === '' || ! preg_match( '/^wpps_sc_hp_[a-f0-9]{8}$/', $hp ) ) {
			return;
		}
		?>
		<div class="wpps-sc-honeypot" aria-hidden="true">
			<label for="<?php echo esc_attr( $hp ); ?>"><?php esc_html_e( 'Leave this field empty', 'wp-powersuite' ); ?></label>
			<input type="text" name="<?php echo esc_attr( $hp ); ?>" id="<?php echo esc_attr( $hp ); ?>" value="" tabindex="-1" autocomplete="off" />
		</div>
		<?php
	}

	public function ajaxOutputCaptchaImage(): void {
		if ( ( $this->getSettings()['captcha_type'] ?? 'math' ) !== 'image' ) {
			status_header( 404 );
			exit;
		}

		if ( $this->isRateLimited( 'img', self::RATE_LIMIT_IMAGE ) ) {
			status_header( 429 );
			exit;
		}

		$id = isset( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : '';
		if ( ! $this->isValidCaptchaKey( $id ) ) {
			status_header( 404 );
			exit;
		}

		$stored = get_transient( self::TRANSIENT_PREFIX . $id );
		if ( ! is_array( $stored ) || empty( $stored['code'] ) ) {
			status_header( 404 );
			exit;
		}

		$this->loadCaptchaGenerator();

		$settings = $this->getSettings();
		$style    = $settings['image_style'] ?? 'distorted';

		CaptchaGenerator::outputImage( (string) $stored['code'], $style );
	}

	public function ajaxRefreshImageCaptcha(): void {
		if ( ( $this->getSettings()['captcha_type'] ?? 'math' ) !== 'image' ) {
			wp_send_json_error( [ 'message' => __( 'Invalid request.', 'wp-powersuite' ) ], 404 );
		}

		if ( $this->isRateLimited( 'refresh', self::RATE_LIMIT_REFRESH ) ) {
			wp_send_json_error( [ 'message' => __( 'Too many requests. Please wait a moment.', 'wp-powersuite' ) ], 429 );
		}
		if ( ! wp_verify_nonce( isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '', 'wpps_captcha_refresh' ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid request.', 'wp-powersuite' ) ] );
		}
		$this->loadCaptchaGenerator();

		$settings   = $this->getSettings();
		$length     = max( 4, min( 8, (int) ( $settings['image_length'] ?? 5 ) ) );
		$case_sensitive = ! empty( $settings['image_case_sensitive'] );

		$code = CaptchaGenerator::generateCode( $length );
		$key = self::generateCaptchaSecretKey();

		$this->storeChallenge( $key, [ 'code' => $code, 'case_sensitive' => $case_sensitive ] );

		$img_url = add_query_arg(
			[ 'action' => 'wpps_simple_captcha_image', 'id' => $key ],
			admin_url( 'admin-ajax.php' )
		);

		wp_send_json_success( [ 'key' => $key, 'img_url' => $img_url ] );
	}

	public function renderLoginWidget(): void {
		if ( CaptchaRuntime::isMagicLoginLoginScreen() ) {
			return;
		}
		$this->renderWidget();
	}

	public function renderWidget( $action = '', bool $force_show = false, string $html_id_suffix = '' ): void {
		$this->enqueueCaptchaStyles();

		if ( ! $force_show && $this->shouldSkip() ) {
			return;
		}

		$settings = $this->getSettings();
		$type     = $settings['captcha_type'] ?? 'math';

		if ( $type === 'math' ) {
			$this->renderMathWidget( $html_id_suffix );
		} elseif ( $type === 'text' ) {
			$this->renderTextWidget( $html_id_suffix );
		} elseif ( $type === 'image' ) {
			$this->renderImageWidget( $html_id_suffix );
		}
	}

	public function shouldSkip(): bool {
		$settings = $this->getSettings();
		// Skip-rules (roles / IPs) — must suppress render as well as verify.
		if ( SkipRules::shouldSkip( is_array( $settings['skip_rules'] ?? null ) ? $settings['skip_rules'] : [] ) ) {
			return true;
		}
		if ( empty( $settings['skip_logged_in'] ) ) {
			return false;
		}
		return is_user_logged_in();
	}

	private function renderMathWidget( string $html_id_suffix = '' ): void {
		$answer_id = 'wpps_simple_captcha_answer' . $html_id_suffix;
		$settings  = $this->getSettings();
		$difficulty = $settings['math_difficulty'] ?? 'easy';
		$problem = $this->generateMathProblem( $difficulty );
		$stored  = $this->storeChallenge( $problem['key'], [ 'answer' => (string) $problem['answer'] ] );
		?>
		<div class="wpps-simple-captcha-wrap wpps-simple-captcha wpps-captcha-math" aria-live="polite">
		<?php $this->printHoneypotField( $stored ); ?>
			<div class="wpps-captcha-row">
				<label for="<?php echo esc_attr( $answer_id ); ?>" class="wpps-captcha-math-line">
					<span class="wpps-captcha-math-question" aria-hidden="true"><?php echo esc_html( $problem['question'] ); ?> =</span>
					<input type="hidden" name="wpps_simple_captcha_key" value="<?php echo esc_attr( $problem['key'] ); ?>" />
					<input
						type="text"
						name="wpps_simple_captcha_answer"
						id="<?php echo esc_attr( $answer_id ); ?>"
						value=""
						size="5"
						autocomplete="off"
						required
						class="wpps-captcha-input"
						<?php /* translators: 1: string, 2: string. */ ?>
						aria-label="<?php echo esc_attr( sprintf( /* translators: %s: e.g. "2 + 3" */ __( 'Your answer to %s =', 'wp-powersuite' ), $problem['question'] ) ); ?>"
					/>
				</label>
			</div>
		</div>
		<?php
	}

	private function renderTextWidget( string $html_id_suffix = '' ): void {
		$questions = $this->getEffectiveTextQuestions();
		if ( $questions === [] ) {
			return;
		}

		$idx = array_rand( $questions );
		$q   = $questions[ $idx ];
		$key = self::generateCaptchaSecretKey();

		$answer_id = 'wpps_simple_captcha_answer' . $html_id_suffix;

		$stored = $this->storeChallenge( $key, [ 'answers' => $q['answers'] ] );
		?>
		<div class="wpps-simple-captcha-wrap wpps-simple-captcha wpps-captcha-text" aria-live="polite">
		<?php $this->printHoneypotField( $stored ); ?>
			<label for="<?php echo esc_attr( $answer_id ); ?>" class="wpps-captcha-label">
				<?php echo esc_html( $q['question'] ); ?>
			</label>
			<div class="wpps-captcha-row">
				<input type="hidden" name="wpps_simple_captcha_key" value="<?php echo esc_attr( $key ); ?>" />
				<input type="hidden" name="wpps_simple_captcha_question" value="<?php echo esc_attr( $q['question'] ); ?>" />
				<input type="text" name="wpps_simple_captcha_answer" id="<?php echo esc_attr( $answer_id ); ?>" value="" autocomplete="off" required class="wpps-captcha-input" />
			</div>
		</div>
		<?php
	}

	private function renderImageWidget( string $html_id_suffix = '' ): void {
		$this->loadCaptchaGenerator();

		$settings   = $this->getSettings();
		$length     = max( 4, min( 8, (int) ( $settings['image_length'] ?? 5 ) ) );
		$case_sensitive = ! empty( $settings['image_case_sensitive'] );

		$code = CaptchaGenerator::generateCode( $length );
		$key = self::generateCaptchaSecretKey();

		$answer_id = 'wpps_simple_captcha_answer' . $html_id_suffix;

		$stored = $this->storeChallenge( $key, [ 'code' => $code, 'case_sensitive' => $case_sensitive ] );
		$img_url = add_query_arg(
			[ 'action' => 'wpps_simple_captcha_image', 'id' => $key ],
			admin_url( 'admin-ajax.php' )
		);
		?>
		<div class="wpps-simple-captcha-wrap wpps-simple-captcha wpps-captcha-image" aria-live="polite">
		<?php $this->printHoneypotField( $stored ); ?>
			<label for="<?php echo esc_attr( $answer_id ); ?>" class="wpps-captcha-label">
				<?php esc_html_e( 'Enter the characters you see in the image:', 'wp-powersuite' ); ?>
			</label>
			<div class="wpps-captcha-image-wrap">
				<img src="<?php echo esc_url( $img_url ); ?>" alt="<?php esc_attr_e( 'CAPTCHA', 'wp-powersuite' ); ?>" class="wpps-captcha-img" />
				<button type="button" class="wpps-captcha-refresh" aria-label="<?php esc_attr_e( 'Refresh CAPTCHA', 'wp-powersuite' ); ?>">↻</button>
			</div>
			<div class="wpps-captcha-row">
				<input type="hidden" name="wpps_simple_captcha_key" value="<?php echo esc_attr( $key ); ?>" />
				<input type="text" name="wpps_simple_captcha_answer" id="<?php echo esc_attr( $answer_id ); ?>" value="" size="<?php echo esc_attr( $length + 2 ); ?>" autocomplete="off" required class="wpps-captcha-input" />
			</div>
		</div>
		<?php
	}

	private function generateMathProblem( string $difficulty ): array {
		$key = self::generateCaptchaSecretKey();

		if ( $difficulty === 'hard' ) {
			$a = random_int( 10, 99 );
			$b = random_int( 2, 9 );
			$op = [ '+', '-', '*' ][ random_int( 0, 2 ) ];
		} elseif ( $difficulty === 'medium' ) {
			$a = random_int( 5, 20 );
			$b = random_int( 2, 9 );
			$op = [ '+', '-' ][ random_int( 0, 1 ) ];
		} else {
			$a = random_int( 1, 10 );
			$b = random_int( 1, 10 );
			$op = '+';
		}

		if ( $op === '-' && $b > $a ) {
			[ $a, $b ] = [ $b, $a ];
		}

		$answer = match ( $op ) {
			'+' => $a + $b,
			'-' => $a - $b,
			'*' => $a * $b,
			default => $a + $b,
		};

		$question = $a . ' ' . $op . ' ' . $b;

		return [ 'key' => $key, 'question' => $question, 'answer' => $answer ];
	}

	public function injectCommentWidget( string $submit_field, array $args ): string {
		ob_start();
		$this->renderWidget( 'comment' );
		$widget = ob_get_clean();
		return $widget . $submit_field;
	}

	public function injectCommentWidgetForReviews( string $submit_field, array $args ): string {
		if ( ! is_singular( 'product' ) ) {
			return $submit_field;
		}
		ob_start();
		$this->renderWidget( 'woo_review' );
		$widget = ob_get_clean();
		return $widget . $submit_field;
	}

	/**
	 * Built-in text questions when none are configured (keeps text CAPTCHA usable out of the box).
	 *
	 * @return array<int, array{question: string, answers: array<int, string>}>
	 */
	private function getBuiltinTextQuestions(): array {
		return [
			[
				'question' => __( 'What color is the sky on a clear day?', 'wp-powersuite' ),
				'answers'  => [ 'blue' ],
			],
			[
				'question' => __( 'What is the opposite of hot?', 'wp-powersuite' ),
				'answers'  => [ 'cold' ],
			],
			[
				'question' => __( 'How many days are in a week?', 'wp-powersuite' ),
				'answers'  => [ '7', 'seven' ],
			],
			[
				'question' => __( 'What animal says "meow"?', 'wp-powersuite' ),
				'answers'  => [ 'cat', 'a cat', 'cats' ],
			],
			[
				'question' => __( 'What comes after Monday?', 'wp-powersuite' ),
				'answers'  => [ 'tuesday' ],
			],
			[
				'question' => __( 'What season comes after winter?', 'wp-powersuite' ),
				'answers'  => [ 'spring' ],
			],
			[
				'question' => __( 'What is 2 + 2?', 'wp-powersuite' ),
				'answers'  => [ '4', 'four' ],
			],
			[
				'question' => __( 'What planet do we live on?', 'wp-powersuite' ),
				'answers'  => [ 'earth' ],
			],
		];
	}

	/**
	 * Built-in text questions for settings UI and documentation (same pool as when custom list is empty).
	 *
	 * @return array<int, array{question: string, answers: array<int, string>}>
	 */
	public function getBuiltinTextQuestionDefinitions(): array {
		return $this->getBuiltinTextQuestions();
	}

	/**
	 * Normalize a stored row to question + list of acceptable answers (pipe-separated string supported).
	 *
	 * @param array<string, mixed> $q Raw row from settings.
	 * @return array{question: string, answers: array<int, string>}|null
	 */
	private function normalizeTextQuestionRow( array $q ): ?array {
		$question = trim( (string) ( $q['question'] ?? '' ) );
		if ( $question === '' ) {
			return null;
		}

		$answers = [];
		if ( isset( $q['answers'] ) && is_array( $q['answers'] ) ) {
			foreach ( $q['answers'] as $a ) {
				$t = trim( (string) $a );
				if ( $t !== '' ) {
					$answers[] = $t;
				}
			}
		}

		if ( $answers === [] && isset( $q['answer'] ) ) {
			$answers = self::splitAnswerVariants( (string) $q['answer'] );
		}

		if ( $answers === [] ) {
			return null;
		}

		return [
			'question' => $question,
			'answers'  => $answers,
		];
	}

	/**
	 * Split admin "answer" field on | for multiple acceptable answers (e.g. 7|seven).
	 *
	 * @return array<int, string>
	 */
	private static function splitAnswerVariants( string $raw ): array {
		$parts = preg_split( '/\s*\|\s*/', $raw );
		if ( ! is_array( $parts ) ) {
			return [];
		}
		$out = [];
		foreach ( $parts as $p ) {
			$t = trim( (string) $p );
			if ( $t !== '' ) {
				$out[] = $t;
			}
		}
		return $out;
	}

	/**
	 * Custom questions from settings, or built-in defaults when empty.
	 *
	 * @return array<int, array{question: string, answers: array<int, string>}>
	 */
	private function getEffectiveTextQuestions(): array {
		$settings = $this->getSettings();
		$raw      = $settings['text_questions'] ?? [];
		if ( ! is_array( $raw ) ) {
			$raw = [];
		}

		$normalized = [];
		foreach ( $raw as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$n = $this->normalizeTextQuestionRow( $row );
			if ( $n !== null ) {
				$normalized[] = $n;
			}
		}

		if ( $normalized !== [] ) {
			return $normalized;
		}

		return $this->getBuiltinTextQuestions();
	}

	public function renderWooCheckoutWidget(): void {
		if ( self::$woo_checkout_classic_emitted ) {
			return;
		}
		self::$woo_checkout_classic_emitted = true;
		$this->renderWooCheckoutWidgetMarkup();
	}

	private function renderWooCheckoutWidgetMarkup(): void {
		$s = $this->getSettings();
		if ( ! empty( $s['woo_checkout_guest_only'] ) && is_user_logged_in() ) {
			return;
		}
		$this->renderWidget( 'woo_checkout', false, '-woo-checkout' );
	}

	/**
	 * Woo Blocks: mount the challenge immediately before the Place Order button.
	 *
	 * Official WooCommerce guidance targets `checkout-actions-block`. Client-side
	 * insert into `.wc-block-checkout__actions_row` throws React #60 — this only
	 * prints SSR markup; woo-checkout.js must not invent nodes inside React-owned parents.
	 *
	 * @param string $block_content Rendered block HTML.
	 */
	public function prependCaptchaToCheckoutActionsBlock( string $block_content ): string {
		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
			return $block_content;
		}
		$s = $this->getSettings();
		if ( empty( $s['woo_checkout'] ) ) {
			return $block_content;
		}
		if ( ! empty( $s['woo_checkout_guest_only'] ) && is_user_logged_in() ) {
			return $block_content;
		}
		if ( $this->shouldSkip() ) {
			return $block_content;
		}
		if ( self::$woo_checkout_block_emitted ) {
			return $block_content;
		}
		self::$woo_checkout_block_emitted = true;
		ob_start();
		echo '<div id="wpps-simple-captcha-woo-checkout" class="wpps-captcha-woo-checkout wpps-captcha-cushion" style="clear:both;margin:0 0 16px;">';
		$this->renderWooCheckoutWidgetMarkup();
		echo '</div>';
		return (string) ob_get_clean() . $block_content;
	}

	public function registerWooCheckoutStoreApiExtension(): void {
		if ( ! function_exists( 'woocommerce_store_api_register_endpoint_data' ) ) {
			return;
		}

		$schema_type = 'array';
		$abstract    = \Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema::class;
		if ( class_exists( $abstract ) && defined( $abstract . '::ARRAY_TYPE' ) ) {
			$schema_type = constant( $abstract . '::ARRAY_TYPE' );
		}

		woocommerce_store_api_register_endpoint_data(
			[
				'endpoint'        => 'checkout',
				'namespace'       => self::WOO_STORE_API_NAMESPACE,
				'schema_callback' => static function () {
					return [
						'key'      => [
							'description' => __( 'Simple CAPTCHA session key.', 'wp-powersuite' ),
							'type'        => 'string',
							'context'     => [ 'view', 'edit' ],
							'readonly'    => false,
						],
						'answer'   => [
							'description' => __( 'Simple CAPTCHA answer.', 'wp-powersuite' ),
							'type'        => 'string',
							'context'     => [ 'view', 'edit' ],
							'readonly'    => false,
						],
						'hp'       => [
							'description' => __( 'Honeypot field name.', 'wp-powersuite' ),
							'type'        => 'string',
							'context'     => [ 'view', 'edit' ],
							'readonly'    => false,
						],
						'hp_value' => [
							'description' => __( 'Honeypot field value.', 'wp-powersuite' ),
							'type'        => 'string',
							'context'     => [ 'view', 'edit' ],
							'readonly'    => false,
						],
					];
				},
				'schema_type'     => $schema_type,
			]
		);
	}

	/**
	 * @param \WC_Order|\WC_Order|null $order Order instance.
	 * @param \WP_REST_Request         $request Request.
	 */
	public function validateWooCheckoutStoreApi( $order, $request ): void {
		unset( $order );
		if ( ! $request instanceof \WP_REST_Request ) {
			return;
		}

		$s = $this->getSettings();
		if ( ! empty( $s['woo_checkout_guest_only'] ) && is_user_logged_in() ) {
			return;
		}
		if ( SkipRules::shouldSkip( is_array( $s['skip_rules'] ?? null ) ? $s['skip_rules'] : [] ) ) {
			return;
		}

		$key    = $this->getKeyFromRequest();
		$answer = $this->getAnswerFromRequest();

		$params = $request->get_json_params();
		if ( is_array( $params ) ) {
			$ext = $params['extensions'][ self::WOO_STORE_API_NAMESPACE ] ?? [];
			if ( is_array( $ext ) ) {
				if ( $key === '' && isset( $ext['key'] ) ) {
					$key = sanitize_text_field( (string) $ext['key'] );
				}
				if ( $answer === '' && isset( $ext['answer'] ) ) {
					$answer = sanitize_text_field( (string) $ext['answer'] );
				}
				$hp_name = isset( $ext['hp'] ) ? sanitize_text_field( (string) $ext['hp'] ) : '';
				if ( $hp_name !== '' && preg_match( '/^wpps_sc_hp_[a-f0-9]{8}$/', $hp_name ) ) {
					$this->honeypot_posted_override = [
						$hp_name => isset( $ext['hp_value'] ) ? (string) $ext['hp_value'] : '',
					];
				}
			}
		}

		try {
			if ( $this->verify( $key, $answer ) ) {
				return;
			}
		} finally {
			$this->honeypot_posted_override = null;
		}

		$msg = $this->messageForFailedVerification( $key, $answer );

		if ( class_exists( \Automattic\WooCommerce\StoreApi\Exceptions\RouteException::class ) ) {
			throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
				'wpps_simple_captcha_checkout',
				esc_html( wp_strip_all_tags( $msg ) ),
				400
			);
		}

		throw new \RuntimeException( esc_html( wp_strip_all_tags( $msg ) ) );
	}

	public function enqueueWooCheckoutFrontScript(): void {
		$s = $this->getSettings();
		if ( empty( $s['woo_checkout'] ) ) {
			return;
		}
		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
			return;
		}
		if ( ! defined( 'WP_POWERSUITE_PLUGIN_URL' ) ) {
			return;
		}

		$deps = [ 'jquery' ];
		if ( wp_script_is( 'wc-blocks-checkout', 'registered' ) ) {
			$deps[] = 'wc-blocks-checkout';
		}

		wp_enqueue_script(
			self::WOO_CHECKOUT_FRONT_HANDLE,
			WP_POWERSUITE_PLUGIN_URL . 'modules/simple-captcha/assets/woo-checkout.js',
			$deps,
			self::MODULE_VERSION,
			true
		);
		wp_localize_script(
			self::WOO_CHECKOUT_FRONT_HANDLE,
			'wpPsSimpleCaptchaWoo',
			[ 'ns' => self::WOO_STORE_API_NAMESPACE ]
		);
	}

	/**
	 * Render CAPTCHA on the set-new-password screen (after email reset link).
	 *
	 * @param \WP_User $user User resetting password.
	 */
	public function renderWidgetOnResetPass( $user ): void {
		unset( $user );
		$this->renderWidget( 'resetpass' );
	}

	/**
	 * Validate CAPTCHA when submitting the new password on resetpass action.
	 *
	 * @param \WP_Error           $errors Error object.
	 * @param \WP_User|\WP_Error $user   User or error from key check.
	 */
	public function validateResetPassForm( \WP_Error $errors, $user ): void {
		if ( $user instanceof \WP_Error ) {
			return;
		}
		if ( ! isset( $_POST['pass1'] ) ) {
			return;
		}
		if ( $this->shouldSkip() ) {
			return;
		}
		if ( $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			return;
		}
		$errors->add( 'simple_captcha_failed', $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) );
	}

	/**
	 * Transient lookup key — must stay alphanumeric; do not use wp_generate_password()
	 * because the {@see random_password} filter can inject symbols and break validation.
	 */
	private static function generateCaptchaSecretKey(): string {
		try {
			return bin2hex( random_bytes( 8 ) );
		} catch ( \Throwable $e ) {
			$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
			$key   = '';
			for ( $i = 0; $i < self::KEY_LENGTH; $i++ ) {
				$key .= $chars[ random_int( 0, 61 ) ];
			}
			return $key;
		}
	}

	private function isValidCaptchaKey( string $key ): bool {
		return strlen( $key ) === self::KEY_LENGTH && ctype_alnum( $key );
	}

	/**
	 * Simple per-IP rate limit for anonymous AJAX (abuse mitigation).
	 */
	private function isRateLimited( string $bucket, int $max ): bool {
		$ip = ClientIp::resolve();
		if ( $ip === '' ) {
			$ip = '0';
		}
		$tkey = 'wpps_sc_rl_' . $bucket . '_' . md5( $ip );
		$n    = (int) get_transient( $tkey );
		if ( $n >= $max ) {
			return true;
		}
		set_transient( $tkey, $n + 1, self::RATE_WINDOW );
		return false;
	}

	private function verify( string $key, string $answer ): bool {
		// Skip rules: trusted users — before transient lookup so we do not burn one-time keys.
		if ( SkipRules::shouldSkip( $this->getSettings()['skip_rules'] ?? [] ) ) {
			return true;
		}

		$answer = substr( trim( $answer ), 0, self::ANSWER_MAX_LEN );
		$memo_key = hash( 'sha256', $key . "\0" . strtolower( $answer ) );
		if ( isset( self::$verify_ok_memo[ $memo_key ] ) ) {
			return true;
		}

		if ( $this->isRateLimited( 'verify', self::RATE_LIMIT_VERIFY ) ) {
			Counters::record( self::MODULE_ID, false );
			return false;
		}

		if ( ! $this->isValidCaptchaKey( $key ) || $answer === '' ) {
			Counters::record( self::MODULE_ID, false );
			return false;
		}

		$stored = get_transient( self::TRANSIENT_PREFIX . $key );
		if ( ! is_array( $stored ) ) {
			Counters::record( self::MODULE_ID, false );
			return false;
		}

		if ( $this->isHoneypotTripped( $stored ) || $this->isSubmittedTooFast( $stored ) ) {
			delete_transient( self::TRANSIENT_PREFIX . $key );
			Counters::record( self::MODULE_ID, false );
			return false;
		}

		if ( isset( $stored['answers'] ) && is_array( $stored['answers'] ) ) {
			$given = strtolower( trim( $answer ) );
			foreach ( $stored['answers'] as $valid ) {
				$candidate = strtolower( trim( (string) $valid ) );
				if ( $candidate !== '' && $given === $candidate ) {
					delete_transient( self::TRANSIENT_PREFIX . $key );
					self::$verify_ok_memo[ $memo_key ] = true;
					Counters::record( self::MODULE_ID, true );
					return true;
				}
			}
			$this->recordFailedAttempt( $key, $stored );
			Counters::record( self::MODULE_ID, false );
			return false;
		}

		if ( isset( $stored['answer'] ) ) {
			$ok = strtolower( trim( $answer ) ) === strtolower( trim( (string) $stored['answer'] ) );
			if ( $ok ) {
				delete_transient( self::TRANSIENT_PREFIX . $key );
				self::$verify_ok_memo[ $memo_key ] = true;
			}
			if ( ! $ok ) {
				$this->recordFailedAttempt( $key, $stored );
			}
			Counters::record( self::MODULE_ID, $ok );
			return $ok;
		}

		if ( isset( $stored['code'] ) ) {
			$expected       = (string) $stored['code'];
			$case_sensitive = ! empty( $stored['case_sensitive'] );
			$ok             = $case_sensitive
				? ( trim( $answer ) === $expected )
				: ( strtolower( trim( $answer ) ) === strtolower( $expected ) );
			if ( $ok ) {
				delete_transient( self::TRANSIENT_PREFIX . $key );
				self::$verify_ok_memo[ $memo_key ] = true;
			}
			if ( ! $ok ) {
				$this->recordFailedAttempt( $key, $stored );
			}
			Counters::record( self::MODULE_ID, $ok );
			return $ok;
		}

		Counters::record( self::MODULE_ID, false );
		return false;
	}

	/**
	 * @param array<string, mixed> $stored
	 */
	private function isHoneypotTripped( array $stored ): bool {
		if ( empty( $this->getSettings()['anti_bot_honeypot'] ) ) {
			return false;
		}
		$hp = isset( $stored['hp'] ) ? (string) $stored['hp'] : '';
		if ( $hp === '' || ! preg_match( '/^wpps_sc_hp_[a-f0-9]{8}$/', $hp ) ) {
			return false;
		}
		if ( is_array( $this->honeypot_posted_override ) && array_key_exists( $hp, $this->honeypot_posted_override ) ) {
			return trim( (string) $this->honeypot_posted_override[ $hp ] ) !== '';
		}
		$val = isset( $_POST[ $hp ] ) ? (string) wp_unslash( $_POST[ $hp ] ) : '';
		return trim( $val ) !== '';
	}

	/**
	 * @param array<string, mixed> $stored
	 */
	private function isSubmittedTooFast( array $stored ): bool {
		if ( empty( $this->getSettings()['anti_bot_timing'] ) ) {
			return false;
		}
		$created = isset( $stored['created'] ) ? (int) $stored['created'] : 0;
		if ( $created <= 0 ) {
			return false;
		}
		$min = max( 1, min( 30, (int) ( $this->getSettings()['min_submit_seconds'] ?? 2 ) ) );
		return ( time() - $created ) < $min;
	}

	/**
	 * @param array<string, mixed> $stored
	 */
	private function recordFailedAttempt( string $key, array $stored ): void {
		$fails = (int) ( $stored['fails'] ?? 0 ) + 1;
		if ( $fails >= self::MAX_FAILS_PER_CHALLENGE ) {
			delete_transient( self::TRANSIENT_PREFIX . $key );
			return;
		}
		$stored['fails'] = $fails;
		set_transient( self::TRANSIENT_PREFIX . $key, $stored, self::TRANSIENT_EXPIRE );
	}

	/**
	 * User-visible text when verify() fails (empty vs wrong vs missing session).
	 * If "Error message" is set in module settings, it overrides all failure cases.
	 */
	private function messageForFailedVerification( string $key, string $answer ): string {
		$custom = trim( (string) ( $this->getSettings()['error_message'] ?? '' ) );
		if ( $custom !== '' ) {
			return $custom;
		}

		if ( trim( $answer ) === '' ) {
			return __( 'Please complete the verification.', 'wp-powersuite' );
		}

		if ( ! $this->isValidCaptchaKey( $key ) ) {
			return __( 'Verification session is invalid. Please refresh the page and try again.', 'wp-powersuite' );
		}

		$stored = get_transient( self::TRANSIENT_PREFIX . $key );
		if ( ! is_array( $stored ) ) {
			return __( 'Verification has expired. Please refresh the page and try again.', 'wp-powersuite' );
		}

		return __( 'That answer is incorrect. Please try again.', 'wp-powersuite' );
	}

	private function getKeyFromRequest(): string {
		return isset( $_POST['wpps_simple_captcha_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wpps_simple_captcha_key'] ) ) : '';
	}

	private function getAnswerFromRequest(): string {
		$raw = isset( $_POST['wpps_simple_captcha_answer'] ) ? (string) wp_unslash( $_POST['wpps_simple_captcha_answer'] ) : '';
		if ( $raw === '' ) {
			return '';
		}
		$raw = substr( $raw, 0, self::ANSWER_MAX_LEN );
		return sanitize_text_field( $raw );
	}

	/**
	 * Whether this request is an actual wp-login.php credential submission.
	 *
	 * WordPress calls wp_signon() on GET for the login screen; the authenticate
	 * filter must not treat that as a failed captcha.
	 */
	private function isCoreLoginSubmission(): bool {
		$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : '';
		if ( $request_method !== 'POST' ) {
			return false;
		}
		return isset( $_POST['log'], $_POST['pwd'] );
	}

	public function validateLogin( $user, string $username, string $password ) {
		$settings = $this->getSettings();
		// WooCommerce My Account runs its own `woocommerce_process_login_errors` validator;
		// skip here so we do not double-consume the one-time transient and block a valid submission.
		if ( ! empty( $settings['woo_login'] ) && $this->isWooCommerceMyAccountLoginSubmission() ) {
			return $user;
		}
		// PMPro frontend login posts log/pwd too; ExtendedIntegrations verifies action `pmp_login`.
		if ( ! empty( $settings['integration_pmp'] ) && CaptchaFormContext::isPmproFrontendAuthPost() ) {
			return $user;
		}
		if ( ! $this->isCoreLoginSubmission() ) {
			return $user;
		}
		if ( ! empty( $settings['login_canonical_only'] ) && ! $this->isRequestPathWpLogin() ) {
			return $user;
		}

		if ( $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			return $user;
		}
		return new \WP_Error( 'simple_captcha_failed', $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) );
	}

	public function validateRegistration( \WP_Error $errors, string $sanitized_user_login, string $user_email ): \WP_Error {
		$settings = $this->getSettings();
		if ( ! empty( $settings['woo_registration'] ) && $this->isWooCommerceRegistrationSubmission() ) {
			return $errors;
		}
		if ( ! empty( $settings['register_canonical_only'] ) && ! $this->isWpLoginRegisterScreen() ) {
			return $errors;
		}
		if ( $this->shouldSkip() ) {
			return $errors;
		}
		if ( ! $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			$errors->add( 'simple_captcha_failed', $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) );
		}
		return $errors;
	}

	private function isRequestPathWpLogin(): bool {
		$login_path = wp_parse_url( wp_login_url(), PHP_URL_PATH );
		$req_uri    = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( (string) $_SERVER['REQUEST_URI'] ) : '';
		$req_path   = wp_parse_url( $req_uri, PHP_URL_PATH );
		if ( ! is_string( $login_path ) || $login_path === '' || ! is_string( $req_path ) || $req_path === '' ) {
			return false;
		}

		return untrailingslashit( $login_path ) === untrailingslashit( $req_path );
	}

	private function isWpLoginRegisterScreen(): bool {
		if ( ! $this->isRequestPathWpLogin() ) {
			return false;
		}
		$action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( (string) $_REQUEST['action'] ) ) : '';

		return $action === 'register';
	}

	/**
	 * Whether this request is WooCommerce My Account login (not wp-login.php).
	 */
	private function isWooCommerceMyAccountLoginSubmission(): bool {
		return isset( $_POST['woocommerce-login-nonce'] ) && ! isset( $_POST['log'] );
	}

	/**
	 * Whether this request is WooCommerce registration on My Account.
	 */
	private function isWooCommerceRegistrationSubmission(): bool {
		return CaptchaFormContext::isWooMyAccountRegistrationPost();
	}

	public function validatePasswordReset( $allow, int $user_id ) {
		if ( ! isset( $_POST['user_login'] ) ) {
			return $allow;
		}
		// PMPro lost-password form; ExtendedIntegrations verifies action `pmp_lostpassword`.
		$settings = $this->getSettings();
		if ( ! empty( $settings['integration_pmp'] ) && CaptchaFormContext::isPmproFrontendAuthPost() ) {
			return $allow;
		}
		if ( $this->shouldSkip() ) {
			return $allow;
		}
		if ( $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			return $allow;
		}
		return new \WP_Error( 'simple_captcha_failed', $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) );
	}

	public function validateComment( array $comment_data ): array {
		if ( $this->shouldSkip() ) {
			return $comment_data;
		}
		if ( $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			return $comment_data;
		}

		$message = $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() );
		$referer = wp_get_referer();
		if ( is_string( $referer ) && $referer !== '' ) {
			wp_safe_redirect( add_query_arg( 'wpps_simple_captcha_error', '1', $referer ) );
			exit;
		}

		wp_die( esc_html( $message ), esc_html__( 'Verification Required', 'wp-powersuite' ), [ 'response' => 403 ] );
	}

	public function validateProductReview( array $comment_data ): array {
		$post_id = isset( $comment_data['comment_post_ID'] ) ? (int) $comment_data['comment_post_ID'] : 0;
		if ( $post_id <= 0 || get_post_type( $post_id ) !== 'product' ) {
			return $comment_data;
		}
		if ( $this->shouldSkip() ) {
			return $comment_data;
		}
		if ( $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			return $comment_data;
		}

		$message = $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() );
		$referer = wp_get_referer();
		if ( is_string( $referer ) && $referer !== '' ) {
			wp_safe_redirect( add_query_arg( 'wpps_simple_captcha_error', '1', $referer ) );
			exit;
		}

		wp_die( esc_html( $message ), esc_html__( 'Verification Required', 'wp-powersuite' ), [ 'response' => 403 ] );
	}

	/**
	 * Show a comment-form notice after a failed Simple CAPTCHA verification redirect.
	 */
	public function renderCommentSimpleCaptchaErrorNotice(): void {
		if ( ! isset( $_GET['wpps_simple_captcha_error'] ) || sanitize_text_field( wp_unslash( (string) $_GET['wpps_simple_captcha_error'] ) ) !== '1' ) {
			return;
		}
		echo '<p class="wpps-simple-captcha-comment-error" role="alert" style="color:#b32d2e;margin-bottom:12px;">';
		echo esc_html( $this->messageForFailedVerification( '', '' ) );
		echo '</p>';
	}

	public function validateWooLogin( \WP_Error $errors, string $username, string $password ): \WP_Error {
		if ( $this->shouldSkip() ) {
			return $errors;
		}
		if ( ! $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			$errors->add( 'simple_captcha_failed', $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) );
		}
		return $errors;
	}

	public function validateWooRegistration( \WP_Error $errors, string $username, string $email ): \WP_Error {
		if ( $this->shouldSkip() ) {
			return $errors;
		}
		if ( ! $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			$errors->add( 'simple_captcha_failed', $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) );
		}
		return $errors;
	}

	public function validateWooCheckout(): void {
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			return;
		}

		if ( ! WooCheckoutContext::shouldEnforceClassicCheckoutCaptcha() ) {
			return;
		}

		$s = $this->getSettings();
		if ( ! empty( $s['woo_checkout_guest_only'] ) && is_user_logged_in() ) {
			return;
		}
		if ( $this->shouldSkip() ) {
			return;
		}

		if ( self::$woo_classic_checkout_gate_done ) {
			return;
		}
		self::$woo_classic_checkout_gate_done = true;

		if ( $this->verify( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ) ) {
			return;
		}
		wc_add_notice( $this->messageForFailedVerification( $this->getKeyFromRequest(), $this->getAnswerFromRequest() ), 'error' );
	}

	/**
	 * Fluent Forms may nest fields under data.data or only send them in $_POST — read both.
	 *
	 * @param array<string, mixed> $fluent_submission Fluent submission array.
	 * @return array{0: string, 1: string} Key and answer for verify().
	 */
	private function getCaptchaPairFromFluentRequest( array $fluent_submission ): array {
		$key = isset( $_POST['wpps_simple_captcha_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wpps_simple_captcha_key'] ) ) : '';
		$ans = isset( $_POST['wpps_simple_captcha_answer'] ) ? sanitize_text_field( wp_unslash( $_POST['wpps_simple_captcha_answer'] ) ) : '';

		if ( $key !== '' || $ans !== '' ) {
			return [ $key, $ans ];
		}

		$nested = $fluent_submission;
		if ( isset( $fluent_submission['data'] ) && is_array( $fluent_submission['data'] ) ) {
			$nested = $fluent_submission['data'];
		}

		if ( is_array( $nested ) ) {
			if ( isset( $nested['wpps_simple_captcha_key'] ) ) {
				$key = sanitize_text_field( (string) $nested['wpps_simple_captcha_key'] );
			}
			if ( isset( $nested['wpps_simple_captcha_answer'] ) ) {
				$ans = sanitize_text_field( (string) $nested['wpps_simple_captcha_answer'] );
			}
		}

		return [ $key, $ans ];
	}

	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}
		$settings = $this->moduleSettings()->all( [] );
		if ( ! is_array( $settings ) ) {
			$settings = [];
		}
		$skip_raw   = isset( $settings['skip_rules'] ) && is_array( $settings['skip_rules'] ) ? $settings['skip_rules'] : [];
		$skip_rules = [
			'enabled'     => ! empty( $skip_raw['enabled'] ),
			'roles'       => isset( $skip_raw['roles'] ) && is_array( $skip_raw['roles'] ) ? array_map( 'sanitize_key', $skip_raw['roles'] ) : [],
			'ips'         => isset( $skip_raw['ips'] ) && is_array( $skip_raw['ips'] ) ? array_values( array_filter( array_map( 'trim', $skip_raw['ips'] ) ) ) : [],
			'user_agents' => isset( $skip_raw['user_agents'] ) && is_array( $skip_raw['user_agents'] ) ? array_values( array_filter( array_map( 'trim', $skip_raw['user_agents'] ) ) ) : [],
		];

		$this->settings_cache = [
			'captcha_type'         => in_array( $settings['captcha_type'] ?? 'math', [ 'math', 'text', 'image' ], true ) ? ( $settings['captcha_type'] ?? 'math' ) : 'math',
			'math_difficulty'      => in_array( $settings['math_difficulty'] ?? 'easy', [ 'easy', 'medium', 'hard' ], true ) ? ( $settings['math_difficulty'] ?? 'easy' ) : 'easy',
			'text_questions'       => is_array( $settings['text_questions'] ?? null ) ? $settings['text_questions'] : [],
			'image_style'          => in_array( $settings['image_style'] ?? 'distorted', [ 'distorted', 'simple' ], true ) ? ( $settings['image_style'] ?? 'distorted' ) : 'distorted',
			'image_length'         => max( 4, min( 8, (int) ( $settings['image_length'] ?? 5 ) ) ),
			'image_case_sensitive' => ! empty( $settings['image_case_sensitive'] ),
			'skip_logged_in'       => ! isset( $settings['skip_logged_in'] ) || ! empty( $settings['skip_logged_in'] ),
			'error_message'        => trim( (string) ( $settings['error_message'] ?? '' ) ),
			'protect_login'        => ! empty( $settings['protect_login'] ),
			'protect_registration' => ! empty( $settings['protect_registration'] ),
			'protect_password_reset' => ! empty( $settings['protect_password_reset'] ),
			'protect_comments'     => ! empty( $settings['protect_comments'] ),
			'woo_login'            => ! empty( $settings['woo_login'] ),
			'woo_registration'     => ! empty( $settings['woo_registration'] ),
			'woo_lost_password'    => ! empty( $settings['woo_lost_password'] ),
			'woo_checkout'           => ! empty( $settings['woo_checkout'] ),
			'woo_checkout_guest_only' => ! empty( $settings['woo_checkout_guest_only'] ),
			'login_canonical_only'   => ! empty( $settings['login_canonical_only'] ),
			'register_canonical_only' => ! empty( $settings['register_canonical_only'] ),
			'woo_reviews'            => ! empty( $settings['woo_reviews'] ),
			'integration_gravity'    => ! empty( $settings['integration_gravity'] ),
			'integration_jetpack'    => ! empty( $settings['integration_jetpack'] ),
			'integration_bbpress'    => ! empty( $settings['integration_bbpress'] ),
			'integration_buddypress' => ! empty( $settings['integration_buddypress'] ),
			'integration_ultimate_member' => ! empty( $settings['integration_ultimate_member'] ),
			'integration_memberpress' => ! empty( $settings['integration_memberpress'] ),
			'integration_edd'        => ! empty( $settings['integration_edd'] ),
			'integration_pmp'        => ! empty( $settings['integration_pmp'] ),
			'integration_mc4wp'      => ! empty( $settings['integration_mc4wp'] ),
			'cf7'                  => ! empty( $settings['cf7'] ),
			'wpforms'              => ! empty( $settings['wpforms'] ),
			'formidable'           => ! empty( $settings['formidable'] ),
			'fluent_forms'         => ! empty( $settings['fluent_forms'] ),
			'elementor_forms'    => ! empty( $settings['elementor_forms'] ),
			'basic_forms'          => ! empty( $settings['basic_forms'] ),
			'skip_rules'              => $skip_rules,
			'anti_bot_honeypot'       => ! isset( $settings['anti_bot_honeypot'] ) || ! empty( $settings['anti_bot_honeypot'] ),
			'anti_bot_timing'         => ! isset( $settings['anti_bot_timing'] ) || ! empty( $settings['anti_bot_timing'] ),
			'min_submit_seconds'      => max( 1, min( 30, (int) ( $settings['min_submit_seconds'] ?? 2 ) ) ),
			'ajax_comment_reset'      => ! isset( $settings['ajax_comment_reset'] ) || ! empty( $settings['ajax_comment_reset'] ),
		];

		$this->settings_cache = FeatureGate::applyPremiumTierLimits( $this->settings_cache );

		return $this->settings_cache;
	}
}

