<?php

/**
 * Form Handler Class
 *
 * @package Giraforms
 */
// Exit if accessed directly.
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
/**
 * Class GiraForms_Form_Handler
 */
class GiraForms_Form_Handler {
    /**
     * Instance of this class.
     *
     * @var GiraForms_Form_Handler
     */
    private static $instance = null;

    /**
     * Get instance.
     *
     * @return GiraForms_Form_Handler
     */
    public static function instance() {
        if ( is_null( self::$instance ) ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor.
     */
    private function __construct() {
        add_action( 'wp_ajax_giraForms_submit', array($this, 'handle_ajax_submission') );
        add_action( 'wp_ajax_nopriv_giraForms_submit', array($this, 'handle_ajax_submission') );
        add_action( 'init', array($this, 'handle_post_submission') );
    }

    /**
     * Handle AJAX form submission.
     */
    public function handle_ajax_submission() {
        // Verify nonce
        if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'giraForms_submit' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Security error. Please refresh the page.', 'giraforms' ),
            ), 403 );
        }
        // Get form data
        $form_id = ( isset( $_POST['formId'] ) ? sanitize_text_field( wp_unslash( $_POST['formId'] ) ) : '' );
        $post_id = ( isset( $_POST['postId'] ) ? intval( $_POST['postId'] ) : 0 );
        // Decode JSON fields - handle both slashed and unslashed input
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- JSON data is decoded and sanitized below.
        $fields_raw = ( isset( $_POST['fields'] ) ? $_POST['fields'] : '' );
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- JSON data is decoded and sanitized below.
        $data_raw = ( isset( $_POST['data'] ) ? $_POST['data'] : '' );
        // Try to decode, if fails try with wp_unslash
        $fields = json_decode( $fields_raw, true );
        if ( null === $fields ) {
            $fields = json_decode( wp_unslash( $fields_raw ), true );
        }
        if ( null === $fields ) {
            $fields = array();
        }
        $data = json_decode( $data_raw, true );
        if ( null === $data ) {
            $data = json_decode( wp_unslash( $data_raw ), true );
        }
        if ( null === $data ) {
            $data = array();
        }
        if ( empty( $form_id ) ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid form data: missing form ID.', 'giraforms' ),
            ), 400 );
        }
        // Process submission
        $result = $this->process_submission(
            $form_id,
            $post_id,
            $fields,
            $data
        );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array(
                'message' => $result->get_error_message(),
                'errors'  => $result->get_error_data(),
            ), 400 );
        }
        wp_send_json_success( array(
            'message'      => __( 'Form submitted successfully!', 'giraforms' ),
            'submissionId' => $result,
        ) );
    }

    /**
     * Handle POST form submission (non-AJAX).
     */
    public function handle_post_submission() {
        // Skip if this is an AJAX request (handled by handle_ajax_submission)
        if ( wp_doing_ajax() ) {
            return;
        }
        if ( !isset( $_POST['giraForms_submit'], $_POST['giraForms_nonce'] ) ) {
            return;
        }
        if ( !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['giraForms_nonce'] ) ), 'giraForms_submit' ) ) {
            return;
        }
        $form_id = ( isset( $_POST['giraForms_id'] ) ? sanitize_text_field( wp_unslash( $_POST['giraForms_id'] ) ) : '' );
        $post_id = ( isset( $_POST['giraForms_post_id'] ) ? (int) $_POST['giraForms_post_id'] : 0 );
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data is decoded and fields are sanitized in process_submission.
        $fields = ( isset( $_POST['giraForms_fields'] ) ? json_decode( wp_unslash( $_POST['giraForms_fields'] ), true ) : array() );
        // Get custom success message and redirect URL from form
        $custom_success_message = ( isset( $_POST['giraForms_success_message'] ) ? sanitize_text_field( wp_unslash( $_POST['giraForms_success_message'] ) ) : '' );
        $custom_redirect_url = ( isset( $_POST['giraForms_redirect_url'] ) ? esc_url_raw( wp_unslash( $_POST['giraForms_redirect_url'] ) ) : '' );
        // Remove meta fields from data and sanitize
        $data = array_map( 'sanitize_text_field', wp_unslash( $_POST ) );
        unset(
            $data['giraForms_submit'],
            $data['giraForms_nonce'],
            $data['giraForms_id'],
            $data['giraForms_post_id'],
            $data['giraForms_fields'],
            $data['giraForms_success_message'],
            $data['giraForms_redirect_url'],
            $data['action'],
            $data['giraforms_email_notifications'],
            $data['giraforms_email_template_id'],
            $data['giraforms_hp'],
            $data['giraforms_timestamp'],
            $data['giraforms_name']
        );
        $result = $this->process_submission(
            $form_id,
            $post_id,
            $fields,
            $data
        );
        if ( is_wp_error( $result ) ) {
            set_transient( 'giraforms_error_' . $form_id, $result, 60 );
        } else {
            $success_message = ( !empty( $custom_success_message ) ? $custom_success_message : __( 'Form submitted successfully!', 'giraforms' ) );
            set_transient( 'giraforms_success_' . $form_id, $success_message, 60 );
        }
        // Determine redirect URL
        // Priority: 1. Custom redirect URL (same domain only), 2. Referer, 3. Post permalink, 4. Home URL
        $redirect_url = '';
        if ( !empty( $custom_redirect_url ) && $this->is_internal_url( $custom_redirect_url ) ) {
            $redirect_url = $custom_redirect_url;
        }
        if ( empty( $redirect_url ) ) {
            $redirect_url = wp_get_referer();
        }
        if ( empty( $redirect_url ) && $post_id ) {
            $redirect_url = get_permalink( $post_id );
        }
        if ( empty( $redirect_url ) ) {
            $redirect_url = home_url();
        }
        // Security: Only allow redirects to the same domain
        // No need to add external hosts to allowed_redirect_hosts
        wp_safe_redirect( $redirect_url );
        exit;
    }

    /**
     * Check if URL is internal.
     *
     * @param string $url URL to check.
     * @return bool
     */
    private function is_internal_url( $url ) {
        $home_url = home_url();
        $home_host = wp_parse_url( $home_url, PHP_URL_HOST );
        $url_host = wp_parse_url( $url, PHP_URL_HOST );
        return $home_host === $url_host;
    }

    /**
     * Process form submission.
     *
     * @param string $form_id Form ID.
     * @param int    $post_id Post ID.
     * @param array  $fields Form fields configuration.
     * @param array  $data Submitted data.
     * @return int|WP_Error Submission ID on success, WP_Error on failure.
     */
    private function process_submission(
        $form_id,
        $post_id,
        $fields,
        $data
    ) {
        $trusted_context = $this->get_trusted_form_context( $form_id, $post_id );
        if ( is_wp_error( $trusted_context ) ) {
            return $trusted_context;
        }
        $fields = ( isset( $trusted_context['fields'] ) && is_array( $trusted_context['fields'] ) ? $trusted_context['fields'] : array() );
        $form_attrs = ( isset( $trusted_context['form_attrs'] ) && is_array( $trusted_context['form_attrs'] ) ? $trusted_context['form_attrs'] : array() );
        // Sanitize data
        $data = GiraForms_Validation::sanitize_form_data( $data );
        $validation_context = array(
            'form_id' => $form_id,
            'post_id' => $post_id,
        );
        // Validate data
        $validation = GiraForms_Validation::validate_form( $data, $fields, $validation_context );
        if ( is_array( $validation ) ) {
            return new WP_Error('validation_failed', __( 'Please correct the errors below.', 'giraforms' ), $validation);
        }
        // Verify Turnstile captcha if present.
        $turnstile_response = ( isset( $data['cf-turnstile-response'] ) ? $data['cf-turnstile-response'] : '' );
        if ( !empty( $turnstile_response ) ) {
            $turnstile_result = $this->verify_turnstile( $turnstile_response );
            if ( is_wp_error( $turnstile_result ) ) {
                return $turnstile_result;
            }
            unset($data['cf-turnstile-response']);
        }
        // Verify reCAPTCHA (v2 or v3) if present.
        $recaptcha_response = ( isset( $data['g-recaptcha-response'] ) ? $data['g-recaptcha-response'] : '' );
        if ( !empty( $recaptcha_response ) ) {
            $recaptcha_result = $this->verify_recaptcha( $recaptcha_response );
            if ( is_wp_error( $recaptcha_result ) ) {
                return $recaptcha_result;
            }
            unset($data['g-recaptcha-response']);
        }
        // Verify hCaptcha if present.
        $hcaptcha_response = ( isset( $data['h-captcha-response'] ) ? $data['h-captcha-response'] : '' );
        if ( !empty( $hcaptcha_response ) ) {
            $hcaptcha_result = $this->verify_hcaptcha( $hcaptcha_response );
            if ( is_wp_error( $hcaptcha_result ) ) {
                return $hcaptcha_result;
            }
            unset($data['h-captcha-response']);
        }
        // Apply spam filters
        if ( $this->is_spam( $data, $form_id ) ) {
            return new WP_Error('spam_detected', __( 'Your submission has been identified as spam.', 'giraforms' ));
        }
        // Check rate limiting
        // Note: When IP collection is disabled (privacy setting), $user_ip will be empty
        // and rate limiting will be naturally bypassed since it relies on IP addresses.
        $db = GiraForms_Database::instance();
        $settings_rl = get_option( 'giraforms_settings', array() );
        $disable_ip = isset( $settings_rl['disable_ip_collection'] ) && '1' === $settings_rl['disable_ip_collection'];
        $user_ip = ( $disable_ip ? '' : (( isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' )) );
        // Validate IP format
        if ( !empty( $user_ip ) && !filter_var( $user_ip, FILTER_VALIDATE_IP ) ) {
            $user_ip = '';
        }
        // Allow customization via filter: default 5 submissions per hour
        $rate_limit = apply_filters( 'giraforms_rate_limit', 5 );
        $rate_timeframe = apply_filters( 'giraforms_rate_timeframe', 3600 );
        // 1 hour in seconds
        if ( !empty( $user_ip ) && $db->is_rate_limited( $user_ip, $rate_limit, $rate_timeframe ) ) {
            return new WP_Error('rate_limit_exceeded', sprintf( 
                /* translators: %d: number of allowed submissions */
                __( 'You have reached the limit of %d submissions per hour. Please try again later.', 'giraforms' ),
                $rate_limit
             ));
        }
        $submission_guard = null;
        // Insert submission into database
        $db = GiraForms_Database::instance();
        $submission_id = $db->insert_submission( array(
            'form_id' => $form_id,
            'post_id' => $post_id,
        ) );
        if ( !$submission_id ) {
            return new WP_Error('db_error', __( 'There was an error saving the submission.', 'giraforms' ));
        }
        // Insert submission meta
        foreach ( $data as $key => $value ) {
            $db->insert_submission_meta( $submission_id, $key, $value );
        }
        // Store field labels for display purposes
        if ( !empty( $fields ) ) {
            $field_labels = array();
            foreach ( $fields as $field ) {
                if ( !empty( $field['name'] ) && !empty( $field['label'] ) ) {
                    $field_name = (string) $field['name'];
                    $field_label = trim( wp_strip_all_tags( (string) $field['label'] ) );
                    if ( '' !== $field_label && $field_label !== $field_name ) {
                        $field_labels[$field_name] = $field_label;
                    }
                }
            }
            if ( !empty( $field_labels ) ) {
                $db->insert_submission_meta( $submission_id, '_field_labels', $field_labels );
            }
        }
        // Send email notifications
        GiraForms_Email_Notifications::instance()->send_notifications(
            $submission_id,
            $form_id,
            $data,
            $fields,
            $form_attrs
        );
        // Hook for additional processing
        do_action(
            'giraforms_after_submission',
            $submission_id,
            $form_id,
            $data,
            $fields
        );
        return $submission_id;
    }

    /**
     * Check if submission is spam.
     *
     * @param array  $data Form data.
     * @param string $form_id Form ID.
     * @return bool
     */
    private function is_spam( $data, $form_id ) {
        // Honeypot field check
        if ( isset( $data['giraforms_hp'] ) && !empty( $data['giraforms_hp'] ) ) {
            return true;
        }
        // Time-based check (form submitted too quickly)
        if ( isset( $data['giraforms_timestamp'] ) ) {
            $timestamp = intval( $data['giraforms_timestamp'] );
            $time_diff = time() - $timestamp;
            // If submitted in less than 3 seconds, likely spam
            if ( $time_diff < 3 ) {
                return true;
            }
        }
        // WordPress disallowed words list check
        if ( $this->matches_disallowed_list( $data ) ) {
            return true;
        }
        // Allow custom spam filters
        return apply_filters(
            'giraforms_is_spam',
            false,
            $data,
            $form_id
        );
    }

    /**
     * Check submission data against WordPress disallowed words list.
     *
     * @param array $data Form data.
     * @return bool True if a disallowed word is found.
     */
    private function matches_disallowed_list( $data ) {
        $mod_keys = trim( (string) get_option( 'disallowed_keys', '' ) );
        if ( '' === $mod_keys ) {
            return false;
        }
        // Build target string from all submitted values + IP + user agent.
        $values = array();
        foreach ( $data as $key => $value ) {
            if ( strpos( $key, 'giraforms_' ) === 0 || strpos( $key, 'giraForms_' ) === 0 ) {
                continue;
            }
            $values[] = ( is_array( $value ) ? implode( ' ', $value ) : (string) $value );
        }
        $values[] = ( isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' );
        $values[] = ( isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '' );
        $target = implode( "\n", array_filter( $values ) );
        foreach ( explode( "\n", $mod_keys ) as $word ) {
            $word = trim( $word );
            $len = strlen( $word );
            if ( $len < 2 || $len > 256 ) {
                continue;
            }
            if ( preg_match( '#' . preg_quote( $word, '#' ) . '#i', $target ) ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Resolve trusted form context (container attributes + fields) from saved block content.
     *
     * @param string $form_id Form ID.
     * @param int    $post_id Current post ID from submission context.
     * @return array|WP_Error
     */
    private function get_trusted_form_context( $form_id, $post_id ) {
        if ( empty( $form_id ) ) {
            return new WP_Error('invalid_form', __( 'Invalid form.', 'giraforms' ));
        }
        $form_container = $this->get_trusted_form_container_block( $form_id, $post_id );
        if ( !is_array( $form_container ) ) {
            return new WP_Error('form_config_not_found', __( 'Unable to load trusted form configuration.', 'giraforms' ));
        }
        $form_attrs = ( isset( $form_container['attrs'] ) && is_array( $form_container['attrs'] ) ? $form_container['attrs'] : array() );
        $fields = array();
        $blocks = ( isset( $form_container['innerBlocks'] ) && is_array( $form_container['innerBlocks'] ) ? $form_container['innerBlocks'] : array() );
        $this->collect_trusted_form_fields( $blocks, $fields );
        return array(
            'form_attrs' => $form_attrs,
            'fields'     => array_values( $fields ),
        );
    }

    /**
     * Load trusted form container block by form ID.
     *
     * @param string $form_id Form ID.
     * @param int    $post_id Current post ID from submission context.
     * @return array|null
     */
    private function get_trusted_form_container_block( $form_id, $post_id ) {
        $candidate_post_ids = array();
        if ( $post_id > 0 ) {
            $candidate_post_ids[] = (int) $post_id;
        }
        $fallback_post_ids = $this->get_candidate_post_ids_for_form( $form_id );
        foreach ( $fallback_post_ids as $fallback_post_id ) {
            if ( !in_array( $fallback_post_id, $candidate_post_ids, true ) ) {
                $candidate_post_ids[] = $fallback_post_id;
            }
        }
        foreach ( $candidate_post_ids as $candidate_post_id ) {
            $post = get_post( $candidate_post_id );
            if ( !$post || empty( $post->post_content ) ) {
                continue;
            }
            $blocks = parse_blocks( $post->post_content );
            if ( !is_array( $blocks ) ) {
                continue;
            }
            $form_container = $this->find_form_container_block( $blocks, $form_id );
            if ( is_array( $form_container ) ) {
                return $form_container;
            }
        }
        return null;
    }

    /**
     * Find possible post IDs containing a given form ID.
     *
     * @param string $form_id Form ID.
     * @return array
     */
    private function get_candidate_post_ids_for_form( $form_id ) {
        global $wpdb;
        if ( empty( $form_id ) ) {
            return array();
        }
        $needle = '"formId":"' . $form_id . '"';
        $like = '%' . $wpdb->esc_like( $needle ) . '%';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is from WordPress core global.
        $sql = $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_content LIKE %s ORDER BY ID DESC LIMIT 25", $like );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Read-only lookup for trusted form source.
        $post_ids = $wpdb->get_col( $sql );
        if ( !is_array( $post_ids ) ) {
            return array();
        }
        return array_map( 'intval', $post_ids );
    }

    /**
     * Find a form container by form ID in a parsed block tree.
     *
     * @param array  $blocks  Parsed blocks.
     * @param string $form_id Form ID.
     * @return array|null
     */
    private function find_form_container_block( $blocks, $form_id ) {
        foreach ( $blocks as $block ) {
            if ( isset( $block['blockName'] ) && 'giraforms/form-container' === $block['blockName'] ) {
                $block_form_id = ( isset( $block['attrs']['formId'] ) ? $block['attrs']['formId'] : '' );
                if ( $block_form_id === $form_id ) {
                    return $block;
                }
            }
            if ( !empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
                $found = $this->find_form_container_block( $block['innerBlocks'], $form_id );
                if ( $found ) {
                    return $found;
                }
            }
        }
        return null;
    }

    /**
     * Collect trusted field definitions from inner blocks recursively.
     *
     * @param array $blocks Parsed blocks.
     * @param array $fields Collected fields (keyed by field name).
     */
    private function collect_trusted_form_fields( $blocks, &$fields ) {
        foreach ( $blocks as $block ) {
            if ( isset( $block['blockName'] ) && $this->is_supported_field_block( $block ) ) {
                $field = $this->build_trusted_field_from_block( $block );
                if ( !empty( $field['name'] ) ) {
                    $fields[$field['name']] = $field;
                }
            }
            if ( !empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
                $this->collect_trusted_form_fields( $block['innerBlocks'], $fields );
            }
        }
    }

    /**
     * Check whether a block is a supported GiraForms input field.
     *
     * @param array $block Parsed block.
     * @return bool
     */
    private function is_supported_field_block( $block ) {
        $block_name = ( isset( $block['blockName'] ) ? $block['blockName'] : '' );
        $field_id = ( isset( $block['attrs']['fieldId'] ) ? $block['attrs']['fieldId'] : '' );
        if ( 0 !== strpos( $block_name, 'giraforms/' ) ) {
            return false;
        }
        if ( in_array( $block_name, array('giraforms/form-container', 'giraforms/submit-button'), true ) ) {
            return false;
        }
        return is_string( $field_id ) && '' !== $field_id;
    }

    /**
     * Build a trusted field config (validation schema) from a parsed block.
     *
     * @param array $block Parsed block.
     * @return array
     */
    private function build_trusted_field_from_block( $block ) {
        $block_name = ( isset( $block['blockName'] ) ? $block['blockName'] : '' );
        $attrs = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array() );
        $field_id = ( isset( $attrs['fieldId'] ) ? sanitize_text_field( $attrs['fieldId'] ) : '' );
        if ( '' === $field_id ) {
            return array();
        }
        $field_type = $this->map_block_name_to_field_type( $block_name );
        $field_label = ( isset( $attrs['label'] ) && is_string( $attrs['label'] ) ? sanitize_text_field( $attrs['label'] ) : $field_id );
        $required = ( isset( $attrs['required'] ) ? (bool) $attrs['required'] : false );
        $field = $attrs;
        $field['name'] = $field_id;
        $field['type'] = $field_type;
        $field['label'] = $field_label;
        $field['required'] = $required;
        if ( 'giraforms/file-upload-field' === $block_name ) {
            if ( isset( $attrs['maxFileSize'] ) && !isset( $field['maxSize'] ) ) {
                $field['maxSize'] = max( 1, (int) round( (float) $attrs['maxFileSize'] * 1024 * 1024 ) );
            }
            if ( isset( $attrs['allowedTypes'] ) && is_array( $attrs['allowedTypes'] ) ) {
                $field['allowedTypes'] = $this->map_file_allowed_types_to_extensions( $attrs['allowedTypes'] );
            }
        }
        return $field;
    }

    /**
     * Map a block name to validation field type.
     *
     * @param string $block_name Block name.
     * @return string
     */
    private function map_block_name_to_field_type( $block_name ) {
        $map = array(
            'giraforms/email-field'          => 'email',
            'giraforms/phone-field'          => 'phone',
            'giraforms/phone-advanced-field' => 'phone',
            'giraforms/url-field'            => 'url',
            'giraforms/number-field'         => 'number',
            'giraforms/range-slider-field'   => 'number',
            'giraforms/rating-field'         => 'number',
            'giraforms/date-field'           => 'date',
            'giraforms/calendar-field'       => 'calendar',
            'giraforms/file-upload-field'    => 'file',
            'giraforms/booking-field'        => 'booking',
            'giraforms/checkbox-field'       => 'checkbox',
            'giraforms/consent-field'        => 'checkbox',
            'giraforms/stripe-payment-field' => 'stripe',
        );
        if ( isset( $map[$block_name] ) ) {
            return $map[$block_name];
        }
        $type = preg_replace( '/^giraforms\\//', '', $block_name );
        $type = preg_replace( '/-field$/', '', $type );
        return sanitize_key( $type );
    }

    /**
     * Convert configured file allowed types (MIME or ext) to extension list.
     *
     * @param array $allowed_types Allowed types list.
     * @return array
     */
    private function map_file_allowed_types_to_extensions( $allowed_types ) {
        $mime_to_ext = array(
            'image/jpeg'                                                              => array('jpg', 'jpeg'),
            'image/png'                                                               => array('png'),
            'image/gif'                                                               => array('gif'),
            'image/webp'                                                              => array('webp'),
            'application/pdf'                                                         => array('pdf'),
            'application/msword'                                                      => array('doc'),
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => array('docx'),
            'application/vnd.ms-excel'                                                => array('xls'),
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'       => array('xlsx'),
            'text/plain'                                                              => array('txt'),
        );
        $extensions = array();
        foreach ( $allowed_types as $type ) {
            if ( !is_string( $type ) || '' === $type ) {
                continue;
            }
            if ( isset( $mime_to_ext[$type] ) ) {
                $extensions = array_merge( $extensions, $mime_to_ext[$type] );
                continue;
            }
            $extensions[] = strtolower( sanitize_key( $type ) );
        }
        return array_values( array_unique( array_filter( $extensions ) ) );
    }

    /**
     * Verify Cloudflare Turnstile response.
     *
     * @param string $response The Turnstile response token.
     * @return bool|WP_Error True on success, WP_Error on failure.
     */
    private function verify_turnstile( $response ) {
        $settings = get_option( 'giraforms_settings', array() );
        $secret_key = GiraForms_Encryption::decrypt( ( isset( $settings['turnstile_secret_key'] ) ? $settings['turnstile_secret_key'] : '' ) );
        if ( empty( $secret_key ) ) {
            return new WP_Error('turnstile_config', __( 'Turnstile configuration error.', 'giraforms' ));
        }
        $verify_url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
        $verify_response = wp_remote_post( $verify_url, array(
            'body' => array(
                'secret'   => $secret_key,
                'response' => $response,
                'remoteip' => ( isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' ),
            ),
        ) );
        if ( is_wp_error( $verify_response ) ) {
            return new WP_Error('turnstile_error', __( 'Captcha verification error.', 'giraforms' ));
        }
        $body = wp_remote_retrieve_body( $verify_response );
        $result = json_decode( $body, true );
        if ( !isset( $result['success'] ) || !$result['success'] ) {
            $error_codes = ( isset( $result['error-codes'] ) ? implode( ', ', $result['error-codes'] ) : '' );
            return new WP_Error('turnstile_failed', __( 'Captcha verification failed. Please try again.', 'giraforms' ), array(
                'error_codes' => $error_codes,
            ));
        }
        return true;
    }

    /**
     * Verify Google reCAPTCHA response (v2 or v3).
     *
     * @param string $response The reCAPTCHA response token.
     * @return bool|WP_Error True on success, WP_Error on failure.
     */
    private function verify_recaptcha( $response ) {
        $settings = get_option( 'giraforms_settings', array() );
        // Determine version: if v3 secret key is set and the hidden field marker is present, use v3.
        $is_v3 = !empty( $settings['recaptcha_v3_secret_key'] ) && isset( $_POST['giraforms_recaptcha_version'] ) && 'v3' === sanitize_text_field( wp_unslash( $_POST['giraforms_recaptcha_version'] ) );
        $secret_key = GiraForms_Encryption::decrypt( ( $is_v3 ? $settings['recaptcha_v3_secret_key'] : (( isset( $settings['recaptcha_v2_secret_key'] ) ? $settings['recaptcha_v2_secret_key'] : '' )) ) );
        if ( empty( $secret_key ) ) {
            return new WP_Error('recaptcha_config', __( 'reCAPTCHA configuration error.', 'giraforms' ));
        }
        $verify_response = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', array(
            'body' => array(
                'secret'   => $secret_key,
                'response' => $response,
                'remoteip' => ( isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' ),
            ),
        ) );
        if ( is_wp_error( $verify_response ) ) {
            return new WP_Error('recaptcha_error', __( 'Captcha verification error.', 'giraforms' ));
        }
        $body = wp_remote_retrieve_body( $verify_response );
        $result = json_decode( $body, true );
        if ( !isset( $result['success'] ) || !$result['success'] ) {
            $error_codes = ( isset( $result['error-codes'] ) ? implode( ', ', $result['error-codes'] ) : '' );
            return new WP_Error('recaptcha_failed', __( 'Captcha verification failed. Please try again.', 'giraforms' ), array(
                'error_codes' => $error_codes,
            ));
        }
        // For v3, check the score.
        if ( $is_v3 && isset( $result['score'] ) ) {
            $threshold = apply_filters( 'giraforms_recaptcha_v3_threshold', 0.5 );
            if ( (float) $result['score'] < $threshold ) {
                return new WP_Error('recaptcha_low_score', __( 'Captcha verification failed. Please try again.', 'giraforms' ), array(
                    'score' => $result['score'],
                ));
            }
        }
        return true;
    }

    /**
     * Verify hCaptcha response.
     *
     * @param string $response The hCaptcha response token.
     * @return bool|WP_Error True on success, WP_Error on failure.
     */
    private function verify_hcaptcha( $response ) {
        $settings = get_option( 'giraforms_settings', array() );
        $secret_key = GiraForms_Encryption::decrypt( ( isset( $settings['hcaptcha_secret_key'] ) ? $settings['hcaptcha_secret_key'] : '' ) );
        $site_key = ( isset( $settings['hcaptcha_site_key'] ) ? $settings['hcaptcha_site_key'] : '' );
        if ( empty( $secret_key ) ) {
            return new WP_Error('hcaptcha_config', __( 'hCaptcha configuration error.', 'giraforms' ));
        }
        $verify_response = wp_remote_post( 'https://api.hcaptcha.com/siteverify', array(
            'body' => array(
                'secret'   => $secret_key,
                'response' => $response,
                'remoteip' => ( isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' ),
                'sitekey'  => $site_key,
            ),
        ) );
        if ( is_wp_error( $verify_response ) ) {
            return new WP_Error('hcaptcha_error', __( 'Captcha verification error.', 'giraforms' ));
        }
        $body = wp_remote_retrieve_body( $verify_response );
        $result = json_decode( $body, true );
        if ( !isset( $result['success'] ) || !$result['success'] ) {
            $error_codes = ( isset( $result['error-codes'] ) ? implode( ', ', $result['error-codes'] ) : '' );
            return new WP_Error('hcaptcha_failed', __( 'Captcha verification failed. Please try again.', 'giraforms' ), array(
                'error_codes' => $error_codes,
            ));
        }
        return true;
    }

}
