<?php

declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\FraudProtection\Recaptcha;

use Automattic\WooCommerce\Utilities\OrderUtil;
use WooCommerce\PayPalCommerce\Vendor\Psr\Log\LoggerInterface;
use WC_Order;
use WC_Product;
use WooCommerce\PayPalCommerce\Assets\AssetGetter;
use WooCommerce\PayPalCommerce\FraudProtection\PersistentCounter;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WP_Error;
use WP_Post;
class Recaptcha
{
    private const V2_CONTAINER_ID = 'ppcp-recaptcha-v2-container';
    private const ERROR_CODE_MISSING_TOKEN = 'ppcp_recaptcha_missing_token';
    private const ERROR_CODE_VERIFICATION_FAILED = 'ppcp_recaptcha_verification_failed';
    private const CAPTCHA_USAGE_LIMIT = 5;
    private const CAPTCHA_RESULT_TRANSIENT_KEY = 'ppcp_recaptcha_result_';
    private const CAPTCHA_RESULT_META_KEY = 'ppcp_recaptcha_captcha_result';
    public const REJECTION_LOGGER_SOURCE = 'woocommerce-paypal-payments-recaptcha';
    public const REJECTION_COUNTER_OPTION = 'ppcp_recaptcha_rejection_counter';
    private \WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\RecaptchaIntegration $integration;
    /**
     * The methods that require captcha.
     *
     * @var string[]
     */
    private array $payment_methods;
    private AssetGetter $asset_getter;
    private string $asset_version;
    private LoggerInterface $logger;
    private PersistentCounter $rejection_counter;
    private SettingsStatus $settings_status;
    private float $last_v3_score = 0;
    /**
     * @param RecaptchaIntegration $integration
     * @param string[]             $payment_methods The methods that require captcha.
     * @param AssetGetter          $asset_getter
     * @param string               $asset_version
     * @param LoggerInterface      $logger
     * @param PersistentCounter    $rejection_counter
     * @param SettingsStatus       $settings_status
     */
    public function __construct(\WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\RecaptchaIntegration $integration, array $payment_methods, AssetGetter $asset_getter, string $asset_version, LoggerInterface $logger, PersistentCounter $rejection_counter, SettingsStatus $settings_status)
    {
        $this->integration = $integration;
        $this->payment_methods = $payment_methods;
        $this->asset_getter = $asset_getter;
        $this->asset_version = $asset_version;
        $this->logger = $logger;
        $this->rejection_counter = $rejection_counter;
        $this->settings_status = $settings_status;
    }
    protected function should_use_recaptcha(): bool
    {
        if (!wc_string_to_bool($this->integration->enabled)) {
            return \false;
        }
        if (wc_string_to_bool($this->integration->get_option('guest_only')) && is_user_logged_in()) {
            return \false;
        }
        $has_v3 = !empty($this->integration->get_option('site_key_v3')) && !empty($this->integration->get_option('secret_key_v3'));
        $has_v2 = !empty($this->integration->get_option('site_key_v2')) && !empty($this->integration->get_option('secret_key_v2'));
        if (!$has_v3 || !$has_v2) {
            return \false;
        }
        return \true;
    }
    public function render_settings_page_log(): string
    {
        $count = esc_attr((string) $this->rejection_counter->current());
        $url = esc_url(admin_url('admin.php?page=wc-status&tab=logs&source=' . self::REJECTION_LOGGER_SOURCE));
        return "\n\t\t<tr valign='top'>\n\t\t\t<td colspan='2' style='padding: 0;'>\n\t\t\t\t<h4>Requests rejected by reCAPTCHA v3:\n\t\t\t\t\t{$count}\n\t\t\t\t\t<a href='{$url}'>view logs</a>\n\t\t\t\t</h4>\n\t\t\t</td>\n\t\t</tr>";
    }
    public function enqueue_scripts(): void
    {
        if (!$this->should_enqueue_for_current_location()) {
            return;
        }
        if (!$this->should_use_recaptcha()) {
            return;
        }
        $is_blocks = has_block('woocommerce/checkout') || has_block('woocommerce/cart');
        wp_enqueue_script('ppcp-recaptcha', 'https://www.google.com/recaptcha/api.js?render=' . esc_attr($this->integration->get_option('site_key_v3')), array(), $this->asset_version, \true);
        $dependencies = array('ppcp-recaptcha');
        if ($is_blocks) {
            $dependencies[] = 'wp-data';
        }
        wp_enqueue_script('ppcp-recaptcha-handler', $this->asset_getter->get_asset_url('recaptcha-handler.js'), $dependencies, $this->asset_version, \true);
        wp_localize_script('ppcp-recaptcha-handler', 'ppcpRecaptchaSettings', array('siteKeyV3' => $this->integration->get_option('site_key_v3'), 'siteKeyV2' => $this->integration->get_option('site_key_v2'), 'theme' => $this->integration->get_option('v2_theme', 'light'), 'isBlocks' => $is_blocks, 'isCheckout' => is_checkout(), 'isCart' => is_cart(), 'isSingleProduct' => is_product(), 'v2ContainerId' => self::V2_CONTAINER_ID, 'errorCodeMissingToken' => self::ERROR_CODE_MISSING_TOKEN, 'errorCodeVerificationFailed' => self::ERROR_CODE_VERIFICATION_FAILED));
    }
    public function render_v2_container(): string
    {
        if (!$this->should_use_recaptcha()) {
            return '';
        }
        if (!$this->should_enqueue_for_current_location()) {
            return '';
        }
        return '<div id="' . esc_attr(self::V2_CONTAINER_ID) . '" style="margin:20px 0;"></div>';
    }
    /**
     * Returns the current page's smart button location key, or null when the
     * current request is not one of the locations reCAPTCHA supports.
     *
     * Mirrors the location keys SmartButton uses via SettingsStatus. Checkout
     * is split into classic vs. block/express since a merchant may enable
     * PayPal for one but not the other.
     */
    private function current_smart_button_location(): ?string
    {
        if (is_product()) {
            return 'product';
        }
        if (is_cart()) {
            return 'cart';
        }
        if (is_checkout()) {
            return has_block('woocommerce/checkout') ? 'checkout-block-express' : 'checkout';
        }
        return null;
    }
    /**
     * Whether reCAPTCHA assets/markup are allowed to load for the current
     * page, based on whether the merchant has PayPal enabled for this smart
     * button location — the same gating SmartButton uses, so reCAPTCHA never
     * loads where no PayPal button/gateway can actually be used.
     */
    private function should_enqueue_for_current_location(): bool
    {
        $location = $this->current_smart_button_location();
        $enabled_for_location = null !== $location && $this->settings_status->is_smart_button_enabled_for_location($location);
        $should_enqueue = $enabled_for_location || $this->has_protected_gateway_on_current_page();
        /**
         * Filters whether reCAPTCHA assets and markup should be enqueued or
         * rendered for the current request.
         *
         * @param bool        $should_enqueue Whether reCAPTCHA should be enqueued.
         * @param string|null $location       The detected smart button location, or null when none applies.
         */
        return (bool) apply_filters('woocommerce_paypal_payments_recaptcha_should_enqueue', $should_enqueue, $location);
    }
    /**
     * True when a reCAPTCHA-protected gateway (ACDC card fields, AXO, card button,
     * standard PayPal) is available on a checkout / order-pay / add-payment-method
     * page — surfaces whose availability is independent of the smart-button location.
     */
    private function has_protected_gateway_on_current_page(): bool
    {
        if (!is_checkout() && !is_add_payment_method_page()) {
            return \false;
        }
        $available = WC()->payment_gateways->get_available_payment_gateways();
        foreach ($this->payment_methods as $id) {
            if (isset($available[$id])) {
                return \true;
            }
        }
        return \false;
    }
    public function intercept_paypal_ajax(array $request_data): void
    {
        if (!$this->should_use_recaptcha()) {
            return;
        }
        $token = sanitize_text_field(wp_unslash($request_data['ppcp_recaptcha_token'] ?? ''));
        $version = sanitize_text_field(wp_unslash($request_data['ppcp_recaptcha_version'] ?? ''));
        if (empty($token)) {
            wp_send_json_error(array('message' => __('Please complete the CAPTCHA verification.', 'woocommerce-paypal-payments'), 'code' => self::ERROR_CODE_MISSING_TOKEN), 400);
        }
        $success = $version === 'v3' ? $this->verify_v3($token, $this->integration->get_option('secret_key_v3'), $this->score_threshold()) : $this->verify_v2($token, $this->integration->get_option('secret_key_v2'));
        if (!$success) {
            if ($version === 'v3') {
                $content = $request_data;
                // Sending only form to reduce the amount of data.
                if (isset($request_data['form'])) {
                    $content = $request_data['form'];
                }
                $this->log_rejection('Create Order AJAX', $content);
            }
            wp_send_json_error(array('message' => __('CAPTCHA verification failed. Please try again.', 'woocommerce-paypal-payments'), 'code' => self::ERROR_CODE_VERIFICATION_FAILED), 403);
        }
    }
    public function validate_classic_checkout(): void
    {
        if (!$this->should_use_recaptcha()) {
            return;
        }
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification handled by WooCommerce before this hook fires
        /** @psalm-suppress PossiblyInvalidCast */
        $payment_method = sanitize_text_field(wp_unslash((string) ($_POST['payment_method'] ?? '')));
        /** @psalm-suppress PossiblyInvalidCast */
        $token = sanitize_text_field(wp_unslash((string) ($_POST['ppcp_recaptcha_token'] ?? '')));
        /** @psalm-suppress PossiblyInvalidCast */
        $version = sanitize_text_field(wp_unslash((string) ($_POST['ppcp_recaptcha_version'] ?? '')));
        if (!in_array($payment_method, $this->payment_methods, \true)) {
            return;
        }
        if (empty($token)) {
            wc_add_notice(__('Please complete the CAPTCHA verification.', 'woocommerce-paypal-payments'), 'error');
            return;
        }
        $success = $version === 'v3' ? $this->verify_v3($token, $this->integration->get_option('secret_key_v3'), $this->score_threshold()) : $this->verify_v2($token, $this->integration->get_option('secret_key_v2'));
        if (!$success) {
            if ($version === 'v3') {
                $content = $_POST;
                $this->log_rejection('Classic Checkout Submission', $content);
            }
            wc_add_notice(__('CAPTCHA verification failed. Please try again.', 'woocommerce-paypal-payments'), 'error');
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }
    /**
     * @param  WP_Error|null|true $errors
     *
     * @return WP_Error|null|true WP_Error
     */
    public function validate_blocks_request($errors)
    {
        $request_uri = sanitize_url(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
        if (!is_wp_error($errors) && strpos($request_uri, '/wc/store/v1/checkout') !== \false) {
            if (!$this->should_use_recaptcha()) {
                return $errors;
            }
            $request_body = file_get_contents('php://input');
            if (!is_string($request_body)) {
                return $errors;
            }
            $data = json_decode($request_body, \true);
            // Not an order creation request.
            if (!is_array($data) || !isset($data['billing_address'])) {
                return $errors;
            }
            $ext_data = $data['extensions']['ppcp_recaptcha'] ?? null;
            $payment_method = sanitize_text_field($data['payment_method'] ?? '');
            if (!in_array($payment_method, $this->payment_methods, \true)) {
                return $errors;
            }
            if (empty($ext_data) || empty($ext_data['token']) || empty($ext_data['version'])) {
                return new WP_Error(self::ERROR_CODE_MISSING_TOKEN, __('Please complete the CAPTCHA verification.', 'woocommerce-paypal-payments'), array('status' => 400));
            }
            $token = sanitize_text_field($ext_data['token']);
            $version = sanitize_text_field($ext_data['version']);
            // Initialize WooCommerce session as it doesn't exist in REST API requests.
            WC()->initialize_session();
            $success = $version === 'v3' ? $this->verify_v3($token, $this->integration->get_option('secret_key_v3'), $this->score_threshold()) : $this->verify_v2($token, $this->integration->get_option('secret_key_v2'));
            if (!$success) {
                if ($version === 'v3') {
                    $content = $data;
                    unset($content['extensions']);
                    $this->log_rejection('Block Checkout Submission', $content);
                }
                return new WP_Error(self::ERROR_CODE_VERIFICATION_FAILED, __('CAPTCHA verification failed. Please try again.', 'woocommerce-paypal-payments'), array('status' => 403));
            }
        }
        return $errors;
    }
    public function add_result_meta(WC_Order $order): void
    {
        $customer_id = $this->customer_identifier();
        if (!$customer_id) {
            $this->logger->debug('Skipping reCAPTCHA meta addition: No customer identifier available', array('order_id' => $order->get_id(), 'backtrace' => \true));
            return;
        }
        $result = get_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
        if (!$result) {
            return;
        }
        $order->update_meta_data(self::CAPTCHA_RESULT_META_KEY, $result);
        $order->save();
        delete_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
    }
    public function add_metabox(): void
    {
        if (!wc_string_to_bool($this->integration->get_option('show_metabox'))) {
            return;
        }
        $screen = OrderUtil::custom_orders_table_usage_is_enabled() ? wc_get_page_screen_id('shop-order') : 'shop_order';
        add_meta_box(
            'ppcp_recaptcha_status',
            __('reCAPTCHA Status', 'woocommerce-paypal-payments'),
            /**
             * @param $order WC_Order|WP_Post
             * @psalm-suppress MissingClosureParamType
             */
            function ($order): void {
                $order = $order instanceof WC_Order ? $order : wc_get_order($order);
                if (!$order instanceof WC_Order) {
                    return;
                }
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                echo $this->render_metabox($order);
            },
            $screen,
            'normal'
        );
    }
    private function render_metabox(WC_Order $order): string
    {
        $captcha_result = $order->get_meta(self::CAPTCHA_RESULT_META_KEY);
        if (empty($captcha_result)) {
            return '<p>' . esc_html__('No reCAPTCHA data', 'woocommerce-paypal-payments') . '</p>';
        }
        // Truncate token to last 10 characters for display.
        if (isset($captcha_result['token']) && is_string($captcha_result['token'])) {
            if (strlen($captcha_result['token']) > 10) {
                $captcha_result['token'] = '...' . (string) substr($captcha_result['token'], -10);
            }
        }
        return '<pre>' . esc_html((string) wp_json_encode($captcha_result, \JSON_PRETTY_PRINT)) . '</pre>';
    }
    private function verify_v3(string $token, string $secret, float $threshold): bool
    {
        if ($this->check_cached_verification($token)) {
            return \true;
        }
        $response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', array('body' => array('secret' => $secret, 'response' => $token, 'remoteip' => $this->customer_ip())));
        if (is_wp_error($response)) {
            $this->logger->error('reCAPTCHA v3 API error: ' . $response->get_error_message());
            return \false;
        }
        $result = json_decode(wp_remote_retrieve_body($response), \true);
        $score = isset($result['score']) ? floatval($result['score']) : 0;
        $is_above_threshold = !empty($result['success']) && $score >= $threshold;
        $is_valid = apply_filters('woocommerce_paypal_payments_recaptcha_verify_v3_result', $is_above_threshold, $threshold, $result);
        $this->last_v3_score = $score;
        if ($is_valid) {
            $customer_id = $this->customer_identifier();
            if ($customer_id) {
                $cached_data = array('result' => $result, 'token' => $token, 'usage_count' => 1);
                set_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id, $cached_data, 300);
            } else {
                $this->logger->debug('reCAPTCHA v3 verification successful but not cached: No customer identifier available', array('backtrace' => \true));
            }
        }
        return $is_valid;
    }
    private function verify_v2(string $token, string $secret): bool
    {
        if ($this->check_cached_verification($token)) {
            return \true;
        }
        $response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', array('body' => array('secret' => $secret, 'response' => $token, 'remoteip' => $this->customer_ip())));
        if (is_wp_error($response)) {
            $this->logger->error('reCAPTCHA v2 API error: ' . $response->get_error_message());
            return \false;
        }
        $result = json_decode(wp_remote_retrieve_body($response), \true);
        $is_valid = apply_filters('woocommerce_paypal_payments_recaptcha_verify_v2_result', $result['success'], $result);
        if ($is_valid) {
            $customer_id = $this->customer_identifier();
            if ($customer_id) {
                $cached_data = array('result' => $result, 'token' => $token, 'usage_count' => 1);
                set_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id, $cached_data, 300);
            } else {
                $this->logger->debug('reCAPTCHA v2 verification successful but not cached: No customer identifier available', array('backtrace' => \true));
            }
        }
        return $is_valid;
    }
    private function check_cached_verification(string $token): bool
    {
        $customer_id = $this->customer_identifier();
        if (!$customer_id) {
            $this->logger->debug('Skipping cached verification check: No customer identifier available', array('backtrace' => \true));
            return \false;
        }
        $cached_data = get_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
        if ($cached_data === \false || !isset($cached_data['usage_count'], $cached_data['token'])) {
            return \false;
        }
        if ($cached_data['token'] === $token && $cached_data['usage_count'] < self::CAPTCHA_USAGE_LIMIT) {
            ++$cached_data['usage_count'];
            set_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id, $cached_data, 300);
            return \true;
        }
        if ($cached_data['usage_count'] >= self::CAPTCHA_USAGE_LIMIT) {
            delete_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
        }
        return \false;
    }
    private function customer_identifier(): ?string
    {
        if (!WC()->session || !WC()->session->get_customer_id()) {
            return null;
        }
        return (string) WC()->session->get_customer_id();
    }
    private function customer_ip(): string
    {
        return filter_var(wp_unslash($_SERVER['REMOTE_ADDR'] ?? ''), \FILTER_VALIDATE_IP) ?: '';
    }
    private function customer_user_agent(): string
    {
        return sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT'] ?? ''));
    }
    private function score_threshold(): float
    {
        return floatval($this->integration->get_option('score_threshold', 0.5));
    }
    private function cart_contents(): array
    {
        $cart = WC()->cart;
        if (!isset($cart) || $cart->is_empty()) {
            return array();
        }
        $cart_data = array('items' => array(), 'totals' => array('subtotal' => $cart->subtotal, 'tax' => $cart->get_total_tax(), 'shipping' => $cart->get_shipping_total(), 'total' => $cart->total));
        foreach ($cart->get_cart() as $cart_item) {
            $product = $cart_item['data'];
            if (!$product instanceof WC_Product) {
                continue;
            }
            $cart_data['items'][] = array('product_id' => $cart_item['product_id'], 'name' => $product->get_name(), 'quantity' => $cart_item['quantity'], 'price' => $product->get_price(), 'line_total' => $cart_item['line_total'], 'variation' => $cart_item['variation'] ?? array());
        }
        return $cart_data;
    }
    private function log_rejection(string $endpoint_name, array $request_data): void
    {
        $this->rejection_counter->increment();
        if (!wc_string_to_bool($this->integration->get_option('log_rejections'))) {
            return;
        }
        $ip = $this->customer_ip();
        $user_agent = $this->customer_user_agent();
        unset($request_data['ppcp_recaptcha_token']);
        unset($request_data['ppcp_recaptcha_version']);
        unset($request_data['g-recaptcha-response']);
        $cart = $this->cart_contents();
        $this->logger->debug("Rejected by v3 reCAPTCHA at {$endpoint_name} with score {$this->last_v3_score}, IP: {$ip}, User Agent: {$user_agent}.", array('source' => self::REJECTION_LOGGER_SOURCE, 'request' => $request_data, 'cart' => $cart));
    }
}
