<?php

namespace WeeConnectPay\WordPress\Plugin\includes;

use Exception;
use InvalidArgumentException;
use WC_Order;
use WeeConnectPay\Api\Requests\CreateCloverCustomerRequest;
use WeeConnectPay\Api\Requests\CreateCloverOrderChargeRequest;
use WeeConnectPay\Api\Requests\CreateCloverChargeRequest;
use WeeConnectPay\Api\Requests\CreateCloverOrderCustomTenderChargeRequest;
use WeeConnectPay\Api\Requests\FinalizeThreeDSChargeRequest;
use WeeConnectPay\Api\Requests\UpdatePaymentMethodRequest;
use WeeConnectPay\CloverReceiptsHelper;
use WeeConnectPay\ThreeDSecureFormatter;
use WeeConnectPay\Dependencies\GuzzleHttp\Exception\ClientException;
use WeeConnectPay\Dependencies\Psr\Http\Message\ResponseInterface;
use WeeConnectPay\Integrations\GoogleRecaptcha;
use WeeConnectPay\Integrations\IntegrationHealth;
use WeeConnectPay\Integrations\IntegrationSettings;
use WeeConnectPay\Integrations\RecaptchaVerifier;
use WeeConnectPay\Integrations\LogService;
use WeeConnectPay\Integrations\CloverOrderPrintingService;
use WeeConnectPay\Exceptions\WeeConnectPayException;
use WeeConnectPay\Exceptions\Codes\ExceptionCode;
use WeeConnectPay\WordPress\Plugin\includes\WeeConnectPayHelper;

class WeeConnectPayOrderProcessor
{
    protected IntegrationSettings $integrationSettings;

    public function __construct($integrationSettings)
    {
        $this->integrationSettings = $integrationSettings;
    }

    /**
     * Main method to process the order payment.
     * This method is called in two different contexts:
     * 1. By WC_Gateway_Weeconnectpay::process_payment() - For normal transactions where order total > 0
     * 2. By WC_Gateway_Weeconnectpay::weeconnectpay_maybe_handle_zero_total_order() - For orders where:
     *    - Total is 0 (possibly due to gift cards being applied)
     *    - There are pending custom tenders that need to be processed through Clover
     *
     * @param WC_Order $order
     * @param array  $postData Typically $_POST data from checkout form. May be empty when handling zero-total orders.
     *
     * @return array
     */
    public function processOrderPayment(WC_Order $order, array $postData ): array
    {
        try {
            LogService::debug(sprintf(
                'Starting payment processing for WooCommerce order (#%d) of type %s',
                $order->get_id(),
                WeeConnectPayHelper::get_order_type( $order )
            ));
            
            // Get pending custom tenders total before proceeding
            $pendingCustomTendersTotal = WeeConnectPayCustomTenderHelper::getCustomTendersPendingTotal($order);
            $isZeroTotalOrder = $order->get_total() <= 0;
            $hasCustomTenders = $pendingCustomTendersTotal > 0;

            LogService::debug(sprintf(
                'Initial state for WooCommerce order (#%d): isZeroTotal=%s, hasCustomTenders=%s, pendingCustomTendersTotal=%.2f %s',
                $order->get_id(),
                $isZeroTotalOrder ? 'true' : 'false',
                $hasCustomTenders ? 'true' : 'false',
                $pendingCustomTendersTotal / 100,
                $order->get_currency()
            ));

            // Early return for zero-total orders with no custom tenders
            // This can happen when the hook runs for zero-total orders where no processing is needed
            if ($isZeroTotalOrder && !$hasCustomTenders) {
                LogService::info(sprintf(
                    'Skipping payment processing for zero-total WooCommerce order (#%d) with no custom tenders',
                    $order->get_id()
                ));
                LogService::debug('Early return - zero total order with no custom tenders for WooCommerce order (#%d)', $order->get_id());
                return $this->successResponse($order);
            }

            // zero-total orders where processing is needed for a pending custom tender
            if ($isZeroTotalOrder) {
                LogService::info(sprintf(
                    'Processing zero-total WooCommerce order (#%d) with pending custom tenders (total: %.2f %s)',
                    $order->get_id(),
                    $pendingCustomTendersTotal / 100,
                    $order->get_currency()
                ));
            }
            // 1. Validate the honeypot field (if enabled)
            // We check honeypot even for zero-total orders as it's a basic bot protection
            if ( 'subscription_renewal' === WeeConnectPayHelper::get_order_type( $order ) ) {
                LogService::info('Skipping honeypot check for subscription renewal order #' . $order->get_id());
            } elseif ($this->integrationSettings->getHoneypotFieldOrDefault()) {
                if ($this->isHoneypotFilled($postData)) {
                    LogService::warning(sprintf(
                        'Returning failure - honeypot triggered for WooCommerce order (#%d)',
                        $order->get_id()
                    ));
                    $this->handleHoneypotFailure($order, $postData);
                    return $this->failureResponse();
                }

                LogService::debug('Honeypot check passed for order #' . $order->get_id());
            }

            // 2. Verify Google reCAPTCHA if enabled
            // Only verify for orders with actual payment (total > 0)
            if ( 'subscription_renewal' === WeeConnectPayHelper::get_order_type( $order ) ) {
                LogService::info('Skipping reCAPTCHA check for subscription renewal order #' . $order->get_id());
            } elseif (!$isZeroTotalOrder && GoogleRecaptcha::isEnabledAndReady()) {
                LogService::info('Verifying reCAPTCHA for non-zero total order #' . $order->get_id());
                $recaptchaResult = $this->handleRecaptcha($order, $postData);
                if (is_array($recaptchaResult)) {
                    LogService::warning('reCAPTCHA verification failed (low score) for order #' . $order->get_id());
                    LogService::debug('Returning recaptcha result array due to low score');
                    return $recaptchaResult;
                }
                if (!$recaptchaResult) {
                    LogService::error('reCAPTCHA verification failed for order #' . $order->get_id());
                    LogService::debug('Returning failure - recaptcha verification failed');
                    return $this->failureResponse();
                }
            }

            // 3. Extract payment token and card details
            // Only needed for orders with remaining balance after custom tenders
            $cardDetails = null;
            if (!$isZeroTotalOrder) {
                $cardDetails = $this->extractCardDetails($postData);
                if (empty($cardDetails['token'])) {
                    LogService::error(sprintf(
                        'Returning failure - missing card token for WooCommerce order (#%d)',
                        $order->get_id()
                    ));
                    return $this->failureResponse();
                }
                LogService::debug(sprintf(
                    'Successfully extracted card details for WooCommerce order (#%d)',
                    $order->get_id()
                ));
            }

            // 4. Create or retrieve a Clover customer
            // Always needed as custom tenders require a customer in Clover
            LogService::info('Creating/retrieving Clover customer for order #' . $order->get_id());
            $customerPayload = $this->buildCustomerPayload($order);
            $customerId = $this->createCloverCustomer($customerPayload, $order);
            if (!$customerId) {
                LogService::error(sprintf(
                    'Returning failure - customer creation failed for WooCommerce order (#%d)',
                    $order->get_id()
                ));
                return $this->failureResponse();
            }

            // 5. Prepare the Clover order (or retrieve the UUID if it exists)
            // Always needed as custom tenders require an order in Clover
            LogService::info('Creating/retrieving Clover order for order #' . $order->get_id());
            $cloverOrderUuid = $this->prepareCloverOrder($order, $customerId);
            if (!$cloverOrderUuid) {
                LogService::error(sprintf(
                    'Returning failure - Clover order preparation failed for WooCommerce order (#%d)',
                    $order->get_id()
                ));
                return $this->failureResponse();
            }

            // 6. Process custom tenders (if any) and adjust amount due accordingly
            if ($hasCustomTenders) {
                LogService::info(sprintf(
                    'Processing custom tenders for order #%d (pending total: %.2f %s)',
                    $order->get_id(),
                    $pendingCustomTendersTotal / 100,
                    $order->get_currency()
                ));
                $customTenderResult = $this->processCustomTenders($order, $cloverOrderUuid);
                if ($customTenderResult['result'] === 'fail') {
                    LogService::error(sprintf(
                        'Returning failure - custom tender processing failed for WooCommerce order (#%d)',
                        $order->get_id()
                    ));
                    return $customTenderResult;
                }
                // If custom tenders fully covered the order and were processed successfully, we're done
                if (isset($customTenderResult['skip_payment']) && $customTenderResult['skip_payment']) {
                    LogService::info('Custom tenders fully processed for order #' . $order->get_id());
                    LogService::debug('Returning success - custom tenders fully covered order');
                    return $customTenderResult;
                }
                $amountDue = $this->calculateAmountDue($order, $customTenderResult);
                LogService::info(sprintf(
                    'Remaining amount due after custom tenders for order #%d: %.2f %s',
                    $order->get_id(),
                    $amountDue / 100,
                    $order->get_currency()
                ));
            } else {
                $amountDue = WeeConnectPayHelper::safe_amount_to_cents_int($order->get_total());
                LogService::debug(sprintf(
                    'No custom tenders, full amount due for order #%d: %.2f %s',
                    $order->get_id(),
                    $amountDue / 100,
                    $order->get_currency()
                ));
            }

            // 7. Only charge the remaining amount if there is any
            if ($amountDue > 0) {
                if (!$cardDetails) {
                    LogService::error(sprintf(
                        'Missing card details for order #%d with remaining balance of %.2f %s',
                        $order->get_id(),
                        $amountDue / 100,
                        $order->get_currency()
                    ));
                    return $this->failureResponse();
                }
                LogService::info(sprintf(
                    'Processing card payment for order #%d (amount: %.2f %s)',
                    $order->get_id(),
                    $amountDue / 100,
                    $order->get_currency()
                ));
                $chargeResponse = $this->createOrderCharge($order, $cloverOrderUuid, $cardDetails, $amountDue);
                LogService::debug(sprintf(
                    'Processing charge response for WooCommerce order (#%d)',
                    $order->get_id()
                ));
                return $this->handleOrderChargeResponse($order, $chargeResponse, $cardDetails);
            }

            // If we get here, all custom tenders were processed successfully and no additional charge was needed
            LogService::info('Successfully completed processing for order #' . $order->get_id());
            LogService::debug('Returning success - all processing completed successfully');
            return $this->successResponse($order);
        } catch (WeeConnectPayException $e) {
	        return $this->handlePaymentProcessingWeeConnectPayException( $e );
        } catch (Exception $e) {
            // Handle any other unexpected exceptions
            LogService::error('Unexpected exception during payment processing: ' . $e->getMessage());
            wc_add_notice(__('An unexpected error occurred during payment processing. Please try again.', 'weeconnectpay'), 'error');
            return $this->failureResponse();
        }
    }

    /**
     * Processes a charge-based payment for a WooCommerce order.
     *
     * Used for both pre-authorization (capture=false, order goes on-hold) and
     * auth+capture (capture=true, order goes to processing via payment_complete).
     *
     * @param WC_Order $order The WooCommerce order instance.
     * @param array $postData The payment form data.
     * @param bool $capture Whether to capture immediately (true for auth_capture) or authorize only (false for pre_auth).
     * @return array The response from the charge process.
     */
    public function processPreAuthorization(WC_Order $order, array $postData, bool $capture = false): array {
        $isZeroTotalOrder = $order->get_total() <= 0;

        // THIS CODE IS DUPLICATED FROM processOrderPayment and should be refactored to avoid duplication.
        // Early return for zero-total orders with no custom tenders
        // This can happen when the hook runs for zero-total orders where no processing is needed
        if ( $isZeroTotalOrder ) {
            LogService::info(sprintf(
                'Skipping payment processing for zero-total WooCommerce order (#%d) with no custom tenders',
                $order->get_id()
            ));
            LogService::debug('Early return - zero total order with no custom tenders for WooCommerce order (#%d)', $order->get_id());
            return $this->successResponse($order);
        }

        if ($this->integrationSettings->getHoneypotFieldOrDefault()) {
            if ($this->isHoneypotFilled($postData)) {
                LogService::warning(sprintf(
                    'Returning failure - honeypot triggered for WooCommerce order (#%d)',
                    $order->get_id()
                ));
                $this->handleHoneypotFailure($order, $postData);
                return $this->failureResponse();
            }

            LogService::debug('Honeypot check passed for order #' . $order->get_id());
        }

        if (GoogleRecaptcha::isEnabledAndReady() ) {
            LogService::info('Verifying reCAPTCHA for non-zero total order #' . $order->get_id());
            $recaptchaResult = $this->handleRecaptcha($order, $postData);
            if (is_array($recaptchaResult)) {
                LogService::warning('reCAPTCHA verification failed (low score) for order #' . $order->get_id());
                LogService::debug('Returning recaptcha result array due to low score');
                return $recaptchaResult;
            }
            if (!$recaptchaResult) {
                LogService::error('reCAPTCHA verification failed for order #' . $order->get_id());
                LogService::debug('Returning failure - recaptcha verification failed');
                return $this->failureResponse();
            }
        }

        // Make the request to the WeeConnectPay API to authorize the payment here.
        $amountDue = WeeConnectPayHelper::safe_amount_to_cents_int($order->get_total());

        if ($amountDue > 0) {
            $cardDetails = $this->extractCardDetails($postData);
            if (empty($cardDetails['token'])) {
                LogService::error(sprintf(
                    'Returning failure - missing card token for WooCommerce order (#%d)',
                    $order->get_id()
                ));
                return $this->failureResponse();
            }
            LogService::debug(sprintf(
                'Successfully extracted card details for WooCommerce order (#%d)',
                $order->get_id()
            ));

            if (!$cardDetails) {
                LogService::error(sprintf(
                    'Missing card details for order #%d with remaining balance of %.2f %s',
                    $order->get_id(),
                    $amountDue / 100,
                    $order->get_currency()
                ));
                return $this->failureResponse();
            }
            LogService::info(sprintf(
                'Processing card payment for order #%d (amount: %.2f %s)',
                $order->get_id(),
                $amountDue / 100,
                $order->get_currency()
            ));
            $chargeResponse = $this->createCharge($order, $cardDetails, $amountDue, $capture);
            LogService::debug(sprintf(
                'Processing charge response for WooCommerce order (#%d)',
                $order->get_id()
            ));
	        try {
		        return $this->handleChargeResponse( $order, $chargeResponse, $cardDetails );
	        } catch ( WeeConnectPayException $e ) {
		        return $this->handlePaymentProcessingWeeConnectPayException( $e );
	        }
        }

        LogService::info('Processing Pre-Authorization for WooCommerce order #' . $order->get_id());
        return $this->successResponse($order);
    }

    public function updatePaymentMethod(WC_Order $order, array $postData)
    {
        LogService::info('Updating Payment Method for WooCommerce order #' . $order->get_id());
        try {
            $uuid  = WeeConnectPayHelper::get_token_uuid_from_subscription_order($order);
            $token = ! empty($postData['token']) ? $postData['token'] : null;

            LogService::info(sprintf(
                'Updating Payment Method using existing UUID %s and new token %s for WooCommerce order (#%d)',
                substr( $uuid, 0, 8 ) . '-****-****-****-************', // Anonymizes the UUID token displayed in the logs.
                $token,
                $order->get_id()
            ));
            
            $updatePaymentMethodResponse = (new UpdatePaymentMethodRequest())->PUT($uuid, $token);

            try {
                $decodedResponse = WeeConnectPayUtilities::validateJsonObject($updatePaymentMethodResponse->getBody()->getContents());
                LogService::info(sprintf(
                    'Successfully updated payment method (%s) with UUID %s for WooCommerce order (#%d)',
                    $decodedResponse->data->credential_type,
                    substr( $decodedResponse->data->saved_credential_uuid, 0, 8 ) . '-****-****-****-************', // Anonymizes the UUID token displayed in the logs.
                    $order->get_id()
                ));

                $order->add_meta_data('weeconnectpay_subscription_uuid', $decodedResponse->data->saved_credential_uuid, true);
                $order->save_meta_data();
                /** @noinspection HtmlUnknownTarget */
                $order->add_order_note(
                    sprintf(
                        '<b>%s</b><br><b>%s</b> %s',
                        __('Successfully updated payment method!', 'weeconnectpay'),
                        __('Token UUID: ', 'weeconnectpay'),
                        esc_html($decodedResponse->data->saved_credential_uuid)                    )
                );
            } catch (\WeeConnectPay\Exceptions\WeeConnectPayException $e) {
                LogService::error(sprintf(
                    'Malformed payment method update response for WooCommerce order (#%d): %s',
                    $order->get_id(),
                    $e->getMessage()
                ));
                if ($e->getCode() === ExceptionCode::INVALID_JSON_EXCEPTION) {
                    throw $e;
                }
                return $this->failureResponse();
            }

        //    wc_add_notice( sprintf( __( 'Payment method for order #%d has been updated.', 'weeconnectpay'), $order->get_id() ), 'success' );

            return $this->redirectResponse($order);
        } catch (Exception $e) {
            LogService::error(sprintf(
                'Updating Payment Method failed for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
            wc_add_notice(esc_html($e->getMessage()), 'error');

            return $this->failureResponse();
        } catch (\Throwable $t) {
            LogService::error(sprintf(
                'Fatal error during Payment Method update for WooCommerce order (#%d): %s',
                $order->get_id(),
                $t->getMessage()
            ));
            wc_add_notice(__('An unexpected error occurred while trying to update the payment method. Please try again.', 'weeconnectpay'), 'error');

            return $this->failureResponse();
        }
    }

    /**
     * Captures a pre-authorized charge for a WooCommerce order.
     *
     * This method retrieves the pre-authorized charge UUID from the order metadata,
     * sends a capture request to the WeeConnectPay API, and handles the response
     * by updating the order status and adding appropriate order notes.
     *
     * @param WC_Order $order The WooCommerce order with a pre-authorized charge.
     * @return array Response array with 'result' and 'message' keys.
     * @since 3.17.0
     */
    public function capturePreauthorizedCharge(WC_Order $order)
    {
        LogService::info('Capturing Pre-Authorization for WooCommerce order #' . $order->get_id());
        
        /**
         * Fires before attempting to capture a preauthorized charge.
         *
         * @since 3.17.0
         * @param WC_Order $order The WooCommerce order object.
         */
        do_action( 'weeconnectpay_before_charge_capture', $order );
        
        // Prevent double-capture using transients as a lock mechanism
        $transient_key = 'wcp_capturing_' . $order->get_id();
        if ( get_transient( $transient_key ) ) {
            LogService::info( sprintf(
                'Capture already in progress for order #%d - blocking duplicate request',
                $order->get_id()
            ) );
            return array(
                'result' => 'fail',
                'message' => __( 'Capture already in progress. Please wait.', 'weeconnectpay' ),
            );
        }
        
        // Set transient lock (5 minute duration)
        set_transient( $transient_key, time(), 5 * MINUTE_IN_SECONDS );
        
        try {
            // Verify the order has a pre-authorized charge
            $chargeUuid = $order->get_meta('weeconnectpay_clover_charge_uuid');
            $chargeStatus = $order->get_meta('weeconnectpay_clover_charge_status');
            
            if (empty($chargeUuid)) {
                LogService::error(sprintf(
                    'No charge UUID found for WooCommerce order (#%d)',
                    $order->get_id()
                ));
                return [
                    'result' => 'fail',
                    'message' => __('No pre-authorized charge found for this order.', 'weeconnectpay'),
                ];
            }
            
            if ($chargeStatus !== 'preauth') {
                LogService::error(sprintf(
                    'Charge status is not "preauth" for WooCommerce order (#%d). Current status: %s',
                    $order->get_id(),
                    $chargeStatus
                ));
                return [
                    'result' => 'fail',
                    'message' => __('This charge cannot be captured. Current status: ', 'weeconnectpay') . esc_html($chargeStatus),
                ];
            }
            
            // Prepare capture request parameters
            $amountToCapture = WeeConnectPayHelper::safe_amount_to_cents_int($order->get_total());
            $ipAddress = $order->get_customer_ip_address();
            
            LogService::info(sprintf(
                'Initiating capture of %.2f %s for pre-authorized charge (ID: %s) on WooCommerce order (#%d)',
                $amountToCapture / 100,
                $order->get_currency(),
                $chargeUuid,
                $order->get_id()
            ));
            
            // Make the capture request
            $captureResponse = (new \WeeConnectPay\Api\Requests\CaptureCloverChargeRequest())->POST(
                $chargeUuid,
                $amountToCapture,
                $ipAddress
            );
            
            // Handle the capture response
            return $this->handleCaptureResponse($order, $captureResponse, $chargeUuid, $amountToCapture);
            
        } catch (Exception $e) {
            LogService::error(sprintf(
                'Exception during charge capture for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
            
            // Try to extract a meaningful error message from the exception. Use this one as a fallback.
            $errorMessage = __('An error occurred while capturing the charge. Please try again.', 'weeconnectpay');
            
            // Check if this is a Guzzle HTTP exception with a response body
            if (method_exists($e, 'hasResponse') && $e->hasResponse()) {
                $response = $e->getResponse();
                $body = (string) $response->getBody();
                $decoded = json_decode($body, true);

                // Extract the most specific error message available
                if (isset($decoded['data']['error']['message'])) {
                    $errorMessage = $decoded['data']['error']['message'];
                } elseif (isset($decoded['data']['message'])) {
                    $errorMessage = $decoded['data']['message'];
                } elseif (isset($decoded['message'])) {
                    $errorMessage = $decoded['message'];
                }
            }
            
            return [
                'result' => 'fail',
                'message' => $errorMessage,
            ];
        } finally {
            // Always release the transient lock when capture completes (success or failure)
            delete_transient( $transient_key );
        }
    }

    /**
     * Handles the response from a charge capture request.
     *
     * @param WC_Order $order The WooCommerce order being processed.
     * @param mixed $captureResponse The response from the capture API request.
     * @param string $chargeUuid The UUID of the captured charge.
     * @param int|null $amount The amount captured in cents.
     * @return array Response array with 'result' and 'message' keys.
     * @since 3.17.0
     */
    protected function handleCaptureResponse(WC_Order $order, ResponseInterface $captureResponse, string $chargeUuid, ?int $amount): array
    {

        // Should already be resolved
//        if (!$captureResponse) {
//            LogService::error(sprintf(
//                'Capture response was empty for WooCommerce order (#%d)',
//                $order->get_id()
//            ));
//            return [
//                'result' => 'fail',
//                'message' => __('No response received from payment processor.', 'weeconnectpay'),
//            ];
//        }
        
        try {
            $response = $this->decodeResponse($captureResponse, $order->get_id());
            
            // Debug: log the actual response structure
            LogService::debug("Raw Capture Response: " . json_encode($response));
            
            if (!isset($response->clover_charge_status)) {
                LogService::error(sprintf(
                    'Malformed capture response for WooCommerce order (#%d) - Missing clover_charge_status. Response: %s',
                    $order->get_id(),
                    json_encode($response)
                ));
                return [
                    'result' => 'fail',
                    'message' => __('Invalid response from payment processor.', 'weeconnectpay'),
                ];
            }
            
            LogService::debug("Decoded Capture Response: " . json_encode($response));
            
            if ('succeeded' === $response->clover_charge_status || 'capture' === $response->clover_charge_status) {
                // Capture successful
                LogService::info(sprintf(
                    'Successfully captured charge (ID: %s) of %.2f %s for WooCommerce order (#%d)',
                    $chargeUuid,
                    $amount / 100,
                    $order->get_currency(),
                    $order->get_id()
                ));
                
                $paymentReceiptUrl = \WeeConnectPay\CloverReceiptsHelper::getEnvReceiptUrl(
                    $chargeUuid,
                    \WeeConnectPay\CloverReceiptsHelper::RECEIPT_TYPES['CHARGE']
                );
                
                // Update charge status to captured/payment
                $order->update_meta_data( 'weeconnectpay_clover_charge_status', 'charged' );
                
                /** @noinspection HtmlUnknownTarget */
                $order->add_order_note(sprintf(
                    '<b>%s</b><br><b>%s</b> <a href="%s">%s</a><br><b>%s</b> %.2f %s',
                    __('Pre-authorized charge successfully captured!', 'weeconnectpay'),
                    __('Charge ID: ', 'weeconnectpay'),
                    esc_url($paymentReceiptUrl),
                    esc_html($chargeUuid),
                    __('Amount captured: ', 'weeconnectpay'),
                    $amount / 100,
                    $order->get_currency()
                ));
                
                // Mark order as payment complete (saves all changes including meta and note)
                $order->payment_complete($chargeUuid);

                // Save the capture charge to the charges table
                if ( isset( $response->clover_charge_id ) ) {
                    // Card details are nested in charge_response.source
                    $source = $response->charge_response->source ?? null;
                    $cardDetails = [
                        'card_brand'     => $source->brand ?? '',
                        'card_last4'     => $source->last4 ?? '',
                        'card_exp_month' => $source->exp_month ?? '',
                        'card_exp_year'  => $source->exp_year ?? '',
                        'tokenized_zip'  => $source->address_zip ?? '',
                    ];
                    $this->saveCreditCardCharge( $order, $cardDetails, $response->clover_charge_id, $response->clover_charge_type ?? 'capture' );

                    // Update the original preauth charge status to "captured" so Void button won't show
                    WeeConnectPayHelper::updateCreditCardChargeToRefunded( $order, $chargeUuid, 'captured' );
                }

                /**
                 * Fires after a preauthorized charge has been successfully captured.
                 *
                 * @since 3.17.0
                 * @param WC_Order $order The WooCommerce order object.
                 * @param string $chargeUuid The Clover charge UUID.
                 * @param int $amount The amount captured in cents.
                 */
                do_action( 'weeconnectpay_charge_captured', $order, $chargeUuid, $amount );
                
                return [
                    'result' => 'success',
                    'message' => __('Charge captured successfully!', 'weeconnectpay'),
                ];
                
            } elseif ('failed' === $response->clover_charge_status) {
                // Capture failed
                $errorMessage = isset($response->error->message) ? $response->error->message : __('Unknown error', 'weeconnectpay');
                $errorCode = isset($response->error->code) ? $response->error->code : '';
                
                LogService::error(sprintf(
                    'Charge capture failed for WooCommerce order (#%d) - Code: %s, Message: %s',
                    $order->get_id(),
                    $errorCode,
                    $errorMessage
                ));
                
                $failureNote = '<b>' . __('Charge capture failed.', 'weeconnectpay') . '</b><br>';
                $failureNote .= '<b>' . __('Charge ID: ', 'weeconnectpay') . '</b>' . esc_html($chargeUuid) . '<br>';
                
                if (!empty($errorCode)) {
                    $failureNote .= '<b>' . __('Error code: ', 'weeconnectpay') . '</b>' . esc_html($errorCode) . '<br>';
                }
                
                if (!empty($errorMessage)) {
                    $failureNote .= '<b>' . __('Error message: ', 'weeconnectpay') . '</b>' . esc_html($errorMessage);
                }
                
                $order->add_order_note($failureNote);
                
                /**
                 * Fires after a preauthorized charge capture has failed.
                 *
                 * @since 3.17.0
                 * @param WC_Order $order The WooCommerce order object.
                 * @param string $chargeUuid The Clover charge UUID.
                 * @param object $response The capture response object.
                 */
                do_action( 'weeconnectpay_charge_capture_failed', $order, $chargeUuid, $response );
                
                return [
                    'result' => 'fail',
                    'message' => esc_html($errorMessage),
                ];
            }
            
            // Unknown status
            LogService::error(sprintf(
                'Unexpected charge status "%s" in capture response for WooCommerce order (#%d)',
                $response->clover_charge_status,
                $order->get_id()
            ));
            
            return [
                'result' => 'fail',
                'message' => __('Unexpected response from payment processor.', 'weeconnectpay'),
            ];
            
        } catch (\WeeConnectPay\Exceptions\WeeConnectPayException $e) {
            LogService::error(sprintf(
                'WeeConnectPayException while handling capture response for WooConnerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));

            return $this->handlePaymentProcessingWeeConnectPayException( $e );
        }
    }

    /**
     * Checks if the honeypot field is filled out.
     */
    protected function isHoneypotFilled(array $postData): bool
    {
        return !empty($postData['hp-feedback-required']);
    }

    /**
     * Handles a honeypot failure by adding an order note and cancelling the order.
     */
    protected function handleHoneypotFailure(WC_Order $order, array $postData): void
    {
        $sanitizedField = _sanitize_text_fields($postData['hp-feedback-required']);
        $orderNote = __('The hidden honeypot field was filled out. Likely a bot. Cancelling order. Field Value: ', 'weeconnectpay') . esc_html($sanitizedField);
        $order->add_order_note($orderNote);
        LogService::warning('Honeypot triggered. Order cancelled.');
        $order->update_status('cancelled');
    }

    /**
     * Handles Google reCAPTCHA by verifying the token, logging details, and cancelling
     * the order if the score is below the configured minimum.
     *
     * @param WC_Order $order
     * @param array    $postData
     * @return bool|array Returns true if reCAPTCHA is verified and processing should continue,
     *                    an array (with redirect response) if the order is cancelled due to a low score,
     *                    or false on fatal error.
     */
    protected function handleRecaptcha(WC_Order $order, array $postData)
    {
        if (empty($postData['recaptcha-token'])) {
            LogService::error('Missing reCAPTCHA token.');
            return false;
        }
        $recaptchaToken = _sanitize_text_fields($postData['recaptcha-token']);

        if (GoogleRecaptcha::tokenContainsErrorJson($recaptchaToken)) {
            $errorMessage = GoogleRecaptcha::extractErrorMessageFromToken($recaptchaToken);
            $order->add_order_note(
                __('<b>Google reCAPTCHA API.js (front-end/customer-facing) has encountered an error.</b> Google reCAPTCHA checks will be disabled for this transaction. Here is the error message: ', 'weeconnectpay') .
                esc_html($errorMessage)
            );
            // Continue processing despite the front-end error.
            return true;
        }

        $remoteIp = $order->get_customer_ip_address();
        $recaptchaVerifier = new RecaptchaVerifier();
        $recaptchaResponse = $recaptchaVerifier->verifyToken($recaptchaToken, $remoteIp);

        if (isset($recaptchaResponse['success']) && $recaptchaResponse['success'] === true) {
            if (isset($recaptchaResponse['score'])) {
                $score = $recaptchaResponse['score'];
                $minimumScore = $this->integrationSettings->getGoogleRecaptchaMinimumHumanScoreThresholdOrDefault();
                $googleRecaptchaText = __('Google reCAPTCHA: ', 'weeconnectpay');
                $googleRecaptchaScoreText = __('Google reCAPTCHA score: ', 'weeconnectpay');
                $minimumScoreText = __('Minimum human score setting: ', 'weeconnectpay');
                $isABot = false;
                if ($score >= $minimumScore) {
                    $recaptchaScoreOrderNote = '<b>' . $googleRecaptchaText . '</b>' .
                        __('According to your plugin settings for Google reCAPTCHA, the customer who paid for the order is likely a human being.', 'weeconnectpay') . '<br>';
                } else {
                    $recaptchaScoreOrderNote = '<b>' . $googleRecaptchaText . '</b>' .
                        __('According to your plugin settings for Google reCAPTCHA, the customer who paid for the order is <b>NOT</b> likely a human being. The order will be cancelled. If you are sure that this order was legitimate, please decrease the minimum human score threshold in the gateway settings.', 'weeconnectpay') . '<br>';
                    $isABot = true;
                }
                $recaptchaScoreOrderNote .= '<b>' . $googleRecaptchaScoreText . '</b>' . esc_html($score) . '<br>';
                $recaptchaScoreOrderNote .= '<b>' . $minimumScoreText . '</b>' . esc_html($minimumScore);
                $order->add_order_note($recaptchaScoreOrderNote);

                if ($isABot) {
                    LogService::warning('WeeConnectPay detected a potential bot!');
                    $order->update_status('cancelled');
                    return [
                        'result'   => 'success',
                        'redirect' => $order->get_view_order_url(),
                    ];
                }
            } else {
                $unknownErrorOrderNote = __('The request to Google was successful but is missing the score. Full response: ', 'weeconnectpay') . json_encode($recaptchaResponse);
                $order->add_order_note($unknownErrorOrderNote);
            }
            return true;
        } else {
            LogService::error('The response from Google reCAPTCHA contains errors: ' . json_encode($recaptchaResponse));
            if (isset($recaptchaResponse['exception'])) {
                $order->add_order_note(
                    __('The request to Google reCAPTCHA triggered an exception. See exception message: ', 'weeconnectpay') .
                    esc_html($recaptchaResponse['exception'])
                );
            } elseif (isset($recaptchaResponse['error-codes'])) {
                $order->add_order_note(
                    __('The response from Google reCAPTCHA contains errors. See error codes: ', 'weeconnectpay') .
                    json_encode($recaptchaResponse['error-codes'])
                );
            } else {
                $order->add_order_note(
                    __('The response from Google reCAPTCHA contains unexpected errors. Full response: ', 'weeconnectpay') .
                    json_encode($recaptchaResponse)
                );
            }
            return false;
        }
    }

    /**
     * Extracts card token and details from the POST data.
     */
    protected function extractCardDetails(array $postData): array
    {
        $browserInfo = null;
        if ( ! empty( $postData['wcp-3ds-browser-info'] ) ) {
            $decoded = json_decode( wp_unslash( $postData['wcp-3ds-browser-info'] ), true );
            if ( is_array( $decoded ) ) {
                $browserInfo = $decoded;
            }
        }

        return [
            'token' => !empty($postData['token']) ? _sanitize_text_fields($postData['token']) : '',
            'card_brand' => !empty($postData['card-brand']) ? _sanitize_text_fields($postData['card-brand']) : '',
            'card_last4' => !empty($postData['card-last4']) ? _sanitize_text_fields($postData['card-last4']) : '',
            'card_exp_month' => !empty($postData['card-exp-month']) ? _sanitize_text_fields($postData['card-exp-month']) : '',
            'card_exp_year' => !empty($postData['card-exp-year']) ? _sanitize_text_fields($postData['card-exp-year']) : '',
            'tokenized_zip' => !empty($postData['tokenized-zip']) ? WeeConnectPayUtilities::formatPostalCode(sanitize_text_field($postData['tokenized-zip'])) : '',
            'browser_info'  => $browserInfo,
        ];
    }

    /**
     * Creates the customer creation payload for the WeeConnectPay API using only the available resources given to us
     *
     * @param WC_Order $order
     *
     * @return array
     * @since 2.4.1
     * @updated 3.12.6
     */
    protected function buildCustomerPayload(WC_Order $order): array
    {
        $customer = [];

        if ( $order->get_billing_first_name() ) {
            $customer['firstName'] = $order->get_billing_first_name();
        }

        if ( $order->get_billing_last_name() ) {
            $customer['lastName'] = $order->get_billing_last_name();
        }

        // Let customers follow your business on their own using the email sent to them by Clover -- Do not force this
        $customer['marketingAllowed'] = false;

        // Required: Address1, State, Country, City, Zip
        if ( $order->get_billing_address_1()
            && $order->get_billing_state()
            && $order->get_billing_country()
            && $order->get_billing_city()
            && $order->get_billing_postcode() ) {

            $customer['addresses'] = [
                [
                    "address1"    => $order->get_billing_address_1(),
                    "address2"    => $order->get_billing_address_2(),
                    "city"        => $order->get_billing_city(),
                    "country"     => $order->get_billing_country(),
                    "phoneNumber" => $order->get_billing_phone(),
                    "state"       => $order->get_billing_state(),
                    "zip"         => $order->get_billing_postcode()
                ]
            ];
        }

        // Required by Clover regardless and used for email DNS validation on our end
        $customer["emailAddresses"] = [
            [
                "emailAddress" => $order->get_billing_email(), // Required for the gateway regardless
                "primaryEmail" => true
            ]
        ];

        // Is there a phone number available?
        if ( $order->get_billing_phone() ) {
            $customer["phoneNumbers"] = [
                [
                    "phoneNumber" => $order->get_billing_phone()
                ]
            ];
        }

        $customer['metadata'] = [
            "note" => "Customer created by WeeConnectPay WooCommerce integration using the information provided by the customer during checkout.",
        ];

        if ( $order->get_billing_company() ) {
            $customer['metadata']['businessName'] = $order->get_billing_company();
        }

        return $customer;
    }


    /**
     * Creates a Clover customer using the provided payload.
     */
    protected function createCloverCustomer(array $customerPayload, WC_Order $order)
    {
        try {
            $request = new CreateCloverCustomerRequest();
            $response = $request->POST($customerPayload);
            $decoded = json_decode($response->getBody()->getContents(), true);
            if (isset($decoded['result'], $decoded['data']['id']) && $decoded['result'] === 'success') {
                LogService::info(sprintf(
                    'Successfully created/retrieved Clover customer (ID: %s) for WooCommerce order (#%d)',
                    $decoded['data']['id'],
                    $order->get_id()
                ));
                return $decoded['data']['id'];
            }
            throw new Exception('Customer creation failed.');
        } catch (Exception $e) {
            LogService::error("Failed to create customer for order #{$order->get_id()}: " . $e->getMessage());
            // Optionally add an order note.
            $order->add_order_note(__('Customer creation failed.', 'weeconnectpay'));
            return false;
        }
    }

    /**
     * Prepares a Clover order. If a Clover order UUID already exists in the order metadata, it returns that.
     * 
     * @throws WeeConnectPayException If there's a WeeConnectPay-specific error that should be handled by the top-level handler
     */
    protected function prepareCloverOrder(WC_Order $order, $customerId)
    {
        if (!$order->meta_exists('weeconnectpay_clover_order_uuid')) {
            try {
                $api = new WeeConnectPayAPI;
                $orderResponse = $api->prepare_order_for_payment($order, $customerId);
                if (isset($orderResponse['uuid'])) {
                    LogService::info(sprintf(
                        'Successfully created Clover order (ID: %s) for WooCommerce order (#%d)',
                        $orderResponse['uuid'],
                        $order->get_id()
                    ));
                    if ( 'default' === WeeConnectPayHelper::get_order_type( $order ) ) {
                        $order->add_meta_data('weeconnectpay_clover_order_uuid', $orderResponse['uuid']);
                        $order->save_meta_data();
                        /** @noinspection HtmlUnknownTarget */
                        $order->add_order_note(sprintf(
                            '<b>%s</b><br><b>%s</b> <a href="%s">%s</a>',
                            __('Clover order created.', 'weeconnectpay'),
                            __('Order ID: ', 'weeconnectpay'),
                            esc_url(CloverReceiptsHelper::getEnvReceiptUrl($orderResponse['uuid'], CloverReceiptsHelper::RECEIPT_TYPES['ORDER'])),
                            esc_html($orderResponse['uuid'])
                        ));
                    }

                    return $orderResponse['uuid'];
                }
            } catch (WeeConnectPayException $e) {
                // Let WeeConnectPayException bubble up to be handled by the top-level handler
                throw $e;
            } catch (Exception $e) {
                // Handle other exceptions with a generic error
                LogService::error(sprintf(
                    'Error preparing Clover order for WooCommerce order (#%d): %s',
                    $order->get_id(),
                    $e->getMessage()
                ));
                $order->add_order_note(__('Error preparing payment order with Clover.', 'weeconnectpay'));
                return false;
            }
        }
        $existingUuid = $order->get_meta('weeconnectpay_clover_order_uuid');
        LogService::info(sprintf(
            'Retrieved existing Clover order (ID: %s) for WooCommerce order (#%d)',
            $existingUuid,
            $order->get_id()
        ));
        return $existingUuid;
    }

    /**
     * Processes any custom tenders (e.g., gift cards) attached to the order.
     *
     * @return array Should contain at least a 'result' key and optionally 'clover_order_amount_due'.
     */
    protected function processCustomTenders(WC_Order $order, $cloverOrderUuid): array
    {
        try {
            $customTenders = WeeConnectPayCustomTenderHelper::getCustomTenders($order, 'pending');
            if (!empty($customTenders)) {
                return $this->process_custom_tenders($order, $cloverOrderUuid);
            }
        } catch (Exception $e) {
            LogService::error(sprintf(
                'Error processing custom tenders for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
            wc_add_notice(__('Payment processing failed. Please try again.', 'weeconnectpay'), 'error');
            return ['result' => 'fail', 'redirect' => ''];
        }
        return ['result' => 'success']; // No custom tenders found.
    }

    /**
     * Calculates the remaining amount due after processing custom tenders.
     */
    protected function calculateAmountDue(WC_Order $order, array $customTenderResult): int
    {
        if (isset($customTenderResult['clover_order_amount_due'])) {
            return $customTenderResult['clover_order_amount_due'];
        }
        return WeeConnectPayHelper::safe_amount_to_cents_int($order->get_total());
    }

    /**
     * Creates the charge for the remaining amount due.
     * @param WC_Order $order
     * @param string $cloverOrderUuid
     * @param array $cardDetails
     * @param int $amountDue
     * @return ResponseInterface
     */
    protected function createOrderCharge(WC_Order $order, string $cloverOrderUuid, array $cardDetails, int $amountDue): ResponseInterface
    {
        $ipAddress = $order->get_customer_ip_address();
        $threeDSecureOptIn = (bool) $this->integrationSettings->getThreeDSecureBetaOrDefault();
        $browserInfo = isset( $cardDetails['browser_info'] ) && is_array( $cardDetails['browser_info'] ) ? $cardDetails['browser_info'] : null;
        try {
            LogService::info(sprintf(
                'Initiating card charge of %.2f %s on Clover order (ID: %s) for WooCommerce order (#%d) (3DS opt-in: %s)',
                $amountDue / 100,
                $order->get_currency(),
                $cloverOrderUuid,
                $order->get_id(),
                $threeDSecureOptIn ? 'true' : 'false'
            ));
            return (new CreateCloverOrderChargeRequest())->POST(
                $cloverOrderUuid,
                $cardDetails['token'],
                $ipAddress,
                $amountDue,
                $order,
                $threeDSecureOptIn,
                $browserInfo
            );
        } catch (ClientException $e) {
            LogService::error(sprintf(
                'Card Order charge creation failed for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
            return $e->getResponse();
        }
    }

    /**
     * Creates the charge for the remaining amount due.
     *
     * @param bool $capture Whether to capture immediately (true for auth_capture) or authorize only (false for pre_auth).
     */
    protected function createCharge(WC_Order $order, array $cardDetails, int $amountDue, bool $capture = false)
    {
        $ipAddress = $order->get_customer_ip_address();
        $threeDSecureOptIn = (bool) $this->integrationSettings->getThreeDSecureBetaOrDefault();
        $browserInfo = isset( $cardDetails['browser_info'] ) && is_array( $cardDetails['browser_info'] ) ? $cardDetails['browser_info'] : null;
        try {
            LogService::info(sprintf(
                'Initiating card charge of %.2f %s for WooCommerce order (#%d) (capture: %s, 3DS opt-in: %s)',
                $amountDue / 100,
                $order->get_currency(),
                $order->get_id(),
                $capture ? 'true' : 'false',
                $threeDSecureOptIn ? 'true' : 'false'
            ));
            $chargeResponse = (new CreateCloverChargeRequest())->POST(
                $cardDetails['token'],
                $ipAddress,
                $amountDue,
                $order,
                $capture,
                $threeDSecureOptIn,
                $browserInfo
            );
            return $chargeResponse;
        } catch (Exception $e) {
            LogService::error(sprintf(
                'Card charge creation failed for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
            return false;
        }
    }

    /**
     * PUSH-channel reconcile: apply the backend's authoritative integration-health
     * verdict carried on a charge response — the top-level `health` block on a
     * failure envelope, or the `X-WCP-Health` header on a guard 401 — so the local
     * disconnect flag flips in the same request the charge resolves in (no settings
     * visit needed). {@see IntegrationHealth::applyFromResponse} flips only on an
     * authoritative disconnect and is a safe no-op when no verdict is present, so
     * this is fine to call on both success and failure responses.
     *
     * Reads the body via __toString and rewinds the (seekable) stream so the
     * downstream {@see decodeResponse} is unaffected. Never throws into the payment
     * flow — a health-reconcile failure must never break a charge.
     */
    private function applyIntegrationHealthFromResponse(ResponseInterface $chargeResponse): void
    {
        try {
            $rawBody = (string) $chargeResponse->getBody();
            if ($chargeResponse->getBody()->isSeekable()) {
                $chargeResponse->getBody()->rewind();
            }

            $decoded = json_decode($rawBody, true);

            (new IntegrationHealth($this->integrationSettings))->applyFromResponse(
                is_array($decoded) ? $decoded : array(),
                $chargeResponse->getHeaders()
            );
        } catch (\Throwable $e) {
            LogService::debug('Integration health PUSH reconcile skipped: ' . $e->getMessage());
        }
    }

    /**
     * @throws WeeConnectPayException
     */
    private function decodeResponse(ResponseInterface $response, $order_id ): object {
        $responseContent = $response->getBody()->getContents();
        try {
            $decodedResponse = WeeConnectPayUtilities::validateJsonObject($responseContent);

        } catch (\WeeConnectPay\Exceptions\WeeConnectPayException $e) {
            LogService::error(sprintf(
                'Malformed charge response for WooCommerce order (#%d): %s',
                $order_id,
                $e->getMessage()
            ));
            if ($e->getCode() === ExceptionCode::INVALID_JSON_EXCEPTION) {
                // This will display to the customer if it bubbles up to process_payment because it's a WeeConnectPayException
                // and this ExceptionCode is being handled in the catch over there
                throw $e;
            } else {
                throw $e; // Catch-all for other WeeConnectPayExceptions for future proofing
            }
        }
        return $decodedResponse->data;
    }

    private function saveCreditCardCharge(WC_Order $order, array $cardDetails, string $chargeId, string $type = 'payment', ?array $threedsData = null)
    {
        try {
            WeeConnectPayHelper::saveCreditCardCharge(
                $order,
                $this->calculateAmountDue($order, []),
                $order->get_currency(),
                $cardDetails['card_brand'],
                $cardDetails['card_last4'],
                $cardDetails['card_exp_month'],
                $cardDetails['card_exp_year'],
                $cardDetails['tokenized_zip'],
                $chargeId,
                $type,
                $threedsData
            );
        } catch (Exception $e) {
            LogService::error(sprintf(
                'Error saving charge metadata for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
        }
    }

    /**
     * Interprets and handles the order charge response.
     * @throws WeeConnectPayException
     */
    protected function handleOrderChargeResponse(WC_Order $order, ResponseInterface $chargeResponse, array $cardDetails): array
    {
        // Leaving this here just in case we need ResponseInterface|null for timeout / 504, but I think we could handle that better than compromising type safety.
//        if (!$chargeResponse) {
//            LogService::error(sprintf(
//                'Order charge response was empty for WooCommerce order (#%d)',
//                $order->get_id()
//            ));
//            return $this->failureResponse();
//        }

        LogService::debug("Card details: ".json_encode($cardDetails));

        // PUSH: flip/clear the disconnect flag from this response before decoding,
        // so a guard 401 (header) or disconnect envelope (body) is honoured even if
        // the body turns out malformed below.
        $this->applyIntegrationHealthFromResponse($chargeResponse);

        $response = $this->decodeResponse($chargeResponse, $order->get_id());

        if (isset($response->clover_payment_status)) {
            LogService::debug("Decoded Order Charge Response: ".json_encode($response));
            // Update metadata regardless of the result.
            $order->update_meta_data('weeconnectpay_card_brand', $cardDetails['card_brand']);

            if ('paid' === $response->clover_payment_status) {
                // Payment successful – record and complete.
                $amountDue = $this->calculateAmountDue($order, []);
                LogService::info(sprintf(
                    'Successfully created card charge (ID: %s) of %.2f %s on Clover order (ID: %s) for WooCommerce order (#%d)',
                    $response->clover_payment_id,
                    $amountDue / 100,
                    $order->get_currency(),
                    $response->clover_order_id ?? 'N/A',
                    $order->get_id()
                ));
                $paymentReceiptUrl = CloverReceiptsHelper::getEnvReceiptUrl(
                    $response->clover_payment_id,
                    CloverReceiptsHelper::RECEIPT_TYPES['CHARGE']
                );
                $order->add_meta_data('weeconnectpay_clover_payment_uuid', $response->clover_payment_id);
                $threedsData     = ThreeDSecureFormatter::extractFromChargeResponse($response);
                $threedsOptIn    = (bool) $this->integrationSettings->getThreeDSecureBetaOrDefault();
                $order->add_order_note( $this->buildSuccessOrderNote(
                    __('Clover payment successful!', 'weeconnectpay'),
                    __('Payment ID: ', 'weeconnectpay'),
                    $response->clover_payment_id,
                    $paymentReceiptUrl,
                    isset($response->clover_order_amount_paid) ? (int) $response->clover_order_amount_paid : $amountDue,
                    $response->clover_charge_currency ?? $order->get_currency(),
                    $cardDetails,
                    $response->charge_response->auth_code ?? null,
                    isset($response->charge_response->ref_num) ? (string) $response->charge_response->ref_num : null,
                    $threedsData,
                    $threedsOptIn,
                    $response
                ) );

                if ( isset($response->saved_credentials) ) {
                    foreach ( $response->saved_credentials as $saved_credential ) {
                        if ( 'Clover' === $saved_credential->credential_type && ! empty( $saved_credential->saved_credential_uuid ) ) {
                            LogService::info(sprintf(
                                'Adding subscription UUID %s to WooCommerce order (#%d)',
                                $saved_credential->saved_credential_uuid,
                                $order->get_id()
                            ));
                            $order->add_meta_data( 'weeconnectpay_subscription_uuid', $saved_credential->saved_credential_uuid, true );
                            $order->save_meta_data();

                            // Also add the subscription UUID to the parent order.
                            if ( function_exists( 'wcs_get_subscriptions_for_order' ) ) {
                                $subscription = wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) );
                                $subscription = is_array( $subscription ) ? reset( $subscription ) : $subscription;
                                $parent_order = ! empty( $subscription ) ? $subscription->get_parent() : null;
                                if ( ! empty( $parent_order ) ) {
                                    LogService::info(sprintf(
                                        'Adding subscription UUID %s to parent WooCommerce order (#%d)',
                                        $saved_credential->saved_credential_uuid,
                                        $parent_order->get_id()
                                    ));
                                    $parent_order->add_meta_data( 'weeconnectpay_subscription_uuid', $saved_credential->saved_credential_uuid, true );
                                    $parent_order->save_meta_data();
                                }
                            }
                        }
                    }
                }

                // Save credit card charge details
                $this->saveCreditCardCharge($order, $cardDetails, $response->clover_payment_id, $response->clover_charge_type ?? 'payment', $threedsData);

                $order->payment_complete();
                $this->addPostTokenizationNotes($order);
                
                // Attempt to print the Clover order (best effort - will not interrupt processing)
                $this->attemptCloverOrderPrint($order);
                
                return $this->successResponse($order);
            } elseif ('failed' === $response->clover_payment_status) {
                $this->handlePaymentFailure($order, $response, $cardDetails, 'Payment declined');
                return $this->redirectResponse($order);
            }
        }

        // 3-D Secure interim state — surface the payload to checkout JS so the
        // browser can drive Clover's method/challenge flow and call back via
        // /weeconnectpay/v1/woocommerce/3ds/finalize.
        $interim = $this->extractThreeDSInterimPayload($response);
        if ($interim !== null) {
            return $this->buildThreeDSInterimResponse($order, $interim, $cardDetails);
        }

        LogService::error(sprintf(
            'Malformed charge response for WooCommerce order (#%d): %s',
            $order->get_id(),
            $chargeResponse->getBody()->getContents()
        ));
        $order->add_order_note(
            '<b>' . esc_html__( 'Clover payment failed.', 'weeconnectpay' ) . '</b><br>'
            . esc_html__( 'The Clover charge response was malformed and could not be processed. Check the WeeConnectPay debug log for the raw payload.', 'weeconnectpay' )
        );
        return $this->failureResponse();
    }

	/**
	 * Interprets and handles the order charge response.
	 *
	 * @param WC_Order $order
	 * @param ResponseInterface $chargeResponse
	 * @param array $cardDetails
	 *
	 * @return array
	 * @throws WeeConnectPayException
	 */
    protected function handleChargeResponse(WC_Order $order, ResponseInterface $chargeResponse, array $cardDetails): array
    {

        LogService::debug("Card details: ".json_encode($cardDetails));

        // PUSH: flip/clear the disconnect flag from this response before decoding,
        // so a guard 401 (header) or disconnect envelope (body) is honoured even if
        // the body turns out malformed below.
        $this->applyIntegrationHealthFromResponse($chargeResponse);

        $response = $this->decodeResponse( $chargeResponse, $order->get_id() );

        if (isset($response->clover_charge_status)) {
            LogService::debug("Decoded Charge Response: ".json_encode($response));
            // Update metadata regardless of the result.
            $order->update_meta_data('weeconnectpay_card_brand', $cardDetails['card_brand']);
            if ('succeeded' === $response->clover_charge_status) {
                // Charge successful.
                $amountDue = $this->calculateAmountDue($order, []);

                LogService::info(sprintf(
                    'Successfully created %s charge (ID: %s) of %.2f %s for WooCommerce order (#%d)',
                    $response->clover_charge_type,
                    $response->clover_charge_id,
                    $amountDue / 100,
                    $order->get_currency(),
                    $order->get_id(),
                ));
                $paymentReceiptUrl = CloverReceiptsHelper::getEnvReceiptUrl(
                    $response->clover_charge_id,
                    CloverReceiptsHelper::RECEIPT_TYPES['CHARGE']
                );
                $order->add_meta_data('weeconnectpay_clover_charge_uuid', $response->clover_charge_id);

                $charge_type     = $response->clover_charge_type;
                $threedsData     = ThreeDSecureFormatter::extractFromChargeResponse($response);
                $threedsOptIn    = (bool) $this->integrationSettings->getThreeDSecureBetaOrDefault();
                $order->add_order_note( $this->buildSuccessOrderNote(
                    'preauth' === $charge_type
                        ? __( 'Clover pre-authorization charge successful!', 'weeconnectpay' )
                        : __( 'Clover capture charge successful!', 'weeconnectpay' ),
                    __('Charge ID: ', 'weeconnectpay'),
                    $response->clover_charge_id,
                    $paymentReceiptUrl,
                    isset($response->clover_charge_amount) ? (int) $response->clover_charge_amount : null,
                    $response->clover_charge_currency ?? $order->get_currency(),
                    $cardDetails,
                    $response->charge_response->auth_code ?? null,
                    isset($response->charge_response->ref_num) ? (string) $response->charge_response->ref_num : null,
                    $threedsData,
                    $threedsOptIn,
                    $response
                ) );

                // @todo: Maybe add logic to handle saved credentials here.

                // Save credit card charge details
                $this->saveCreditCardCharge($order, $cardDetails, $response->clover_charge_id, $response->clover_charge_type, $threedsData);

                // @todo: Logic already in place in case we enventually want to use the 'charge' endpoint for all orders.
                $order->add_meta_data('weeconnectpay_clover_charge_status', $charge_type);
                if ( 'payment' === $charge_type || 'capture' === $charge_type ) {
                    $order->payment_complete();
                } elseif ( 'preauth' === $charge_type ) {
                    $order->update_status('on-hold');
                }

                $this->addPostTokenizationNotes($order);
                
                // Attempt to print the Clover order (best effort - will not interrupt processing)
                $this->attemptCloverOrderPrint($order);
                
                return $this->successResponse($order);
            } elseif ('failed' === $response->clover_charge_status) {
                $this->handlePaymentFailure($order, $response, $cardDetails, 'Charge declined');
                return $this->redirectResponse($order);
            }
        }

        $interim = $this->extractThreeDSInterimPayload($response);
        if ($interim !== null) {
            return $this->buildThreeDSInterimResponse($order, $interim, $cardDetails);
        }

	    LogService::error(sprintf(
		    'Malformed charge response for WooCommerce order (#%d): %s',
		    $order->get_id(),
		    $chargeResponse->getBody()->getContents()
	    ));
	    $order->add_order_note(
	        '<b>' . esc_html__( 'Clover charge failed.', 'weeconnectpay' ) . '</b><br>'
	        . esc_html__( 'The Clover charge response was malformed and could not be processed. Check the WeeConnectPay debug log for the raw payload.', 'weeconnectpay' )
	    );
	    return $this->failureResponse();
    }

    /**
     * Extract a 3-D Secure interim-state payload from a decoded API response.
     *
     * Returns a normalized array `{ state, charge_id, threeds }` when the
     * response carries either the new top-level `state` field
     * (`method_required` / `challenge_required`) or the legacy raw
     * `charge_response.threeds.validation_result` fallback. Returns null when
     * the response is terminal (success / failure / malformed).
     *
     * Pure detection — no logging, order notes, or notices. Callers decide
     * what to do with the payload (propagate to checkout JS for the challenge
     * flow, or fail the order with a stub message until the JS lands).
     *
     * @param mixed $response Decoded response object from {@see decodeResponse()}.
     * @return array{state: string, charge_id: string|null, threeds: object|array}|null
     */
    protected function extractThreeDSInterimPayload($response): ?array
    {
        if (!is_object($response)) {
            return null;
        }

        $state = null;
        $chargeId = null;
        $threeds = null;

        if (isset($response->state) && is_string($response->state) && in_array($response->state, ['method_required', 'challenge_required'], true)) {
            $state = $response->state;
            $chargeId = isset($response->charge_id) ? (string) $response->charge_id : null;
            $threeds = $response->threeds ?? new \stdClass();
        } else {
            // Legacy fallback for the pre-WSO-24 API shape — the backend used
            // to passthrough the raw Clover `charge_response.threeds` block
            // instead of a normalized state. Kept inert when running against a
            // post-WSO-24 backend (the `state` branch above wins first).
            $rawValidation = $response->charge_response->threeds->validation_result ?? null;
            $rawChargeId = $response->charge_response->id ?? null;
            $rawThreeds = $response->charge_response->threeds->data ?? null;

            if ($rawValidation === 'CHALLENGE_FLOW_REQUIRED') {
                $state = 'challenge_required';
            } elseif ($rawValidation === 'THREEDS_METHOD_FLOW_REQUIRED') {
                $state = 'method_required';
            }

            if ($state !== null) {
                $chargeId = is_string($rawChargeId) ? $rawChargeId : null;
                $threeds = $rawThreeds ?? new \stdClass();
            }
        }

        if ($state === null) {
            return null;
        }

        return [
            'state' => $state,
            'charge_id' => $chargeId,
            'threeds' => $threeds,
        ];
    }

    /**
     * Build the process_payment-shaped response that carries a 3-D Secure
     * interim payload back to checkout JS. Called from both
     * handleOrderChargeResponse and handleChargeResponse when an interim state
     * is detected on the initial charge.
     *
     * Returns `result: 'success'` (not `'fail'`) so WooCommerce's default
     * checkout JS doesn't auto-render an error notice — the plugin's own
     * ajaxSuccess interceptor reads `weeconnectpay_threeds_interim` and
     * suppresses the redirect before WC navigates to the sentinel fragment.
     *
     * The card details from the initial tokenization are stashed on the
     * order so {@see finalizeThreeDSCharge()} can pass them to the same
     * terminal-state handler used by the initial charge.
     *
     * @param array{state: string, charge_id: string|null, threeds: mixed} $interim
     * @param array<string, mixed>                                          $cardDetails
     * @return array{result: string, redirect: string, weeconnectpay_threeds_interim: array<string, mixed>}
     */
    protected function buildThreeDSInterimResponse(WC_Order $order, array $interim, array $cardDetails): array
    {
        LogService::info(sprintf(
            '3-D Secure interim state "%s" propagated to checkout for WooCommerce order (#%d) — charge_id %s',
            $interim['state'],
            $order->get_id(),
            $interim['charge_id'] ?? '(none)'
        ));

        // Stash card details so the finalize call can pass them to the
        // terminal-state handler. Stored as a serialized array via WC's
        // standard meta storage.
        $order->update_meta_data('weeconnectpay_threeds_pending_card_details', $cardDetails);
        $order->save();

        $order->add_order_note(sprintf(
            /* translators: %s: 3-D Secure interim state name (method_required or challenge_required) */
            __('3-D Secure %s flow initiated. Waiting for the browser to complete verification.', 'weeconnectpay'),
            $interim['state']
        ));

        $payload = [
            'state'     => $interim['state'],
            'charge_id' => $interim['charge_id'],
            'threeds'   => $interim['threeds'],
            'order_id'  => $order->get_id(),
            'order_key' => $order->get_order_key(),
            'nonce'     => wp_create_nonce('wcp_3ds_finalize_' . $order->get_id()),
        ];

        // Top-level `weeconnectpay_threeds_interim` is read by Classic
        // checkout's ajaxSuccess interceptor (raw response body). Nested
        // `payment_details.weeconnectpay_threeds_interim` is read by the
        // Blocks payment method's onCheckoutSuccess subscription —
        // WC Blocks only forwards fields under `payment_details` through to
        // the React side. Both paths receive the same payload.
        return [
            'result'                        => 'success',
            'redirect'                      => '#wcp-3ds-pending',
            'weeconnectpay_threeds_interim' => $payload,
            'payment_details'               => [
                'weeconnectpay_threeds_interim' => $payload,
            ],
        ];
    }

    /**
     * Finalize a 3-D Secure charge after the browser completes the method or
     * challenge flow. Called from {@see WC_ThreeDSFinalizeController::finalize()}
     * with the values the browser collected from the `executePatch` event.
     *
     * Calls the API's `/v1/clover/charges/3ds/finalize` endpoint via
     * {@see FinalizeThreeDSChargeRequest} and routes the response through the
     * same terminal-state handlers used by the initial charge so order notes,
     * payment_complete, and credential persistence behave identically to a
     * frictionless 3DS charge. On method->challenge escalation, the same
     * handler propagates a new interim payload back to checkout JS.
     *
     * @return array Same shape as process_payment results — may be a success envelope, failure envelope, or a `weeconnectpay_threeds_interim`-carrying success envelope on escalation.
     */
    public function finalizeThreeDSCharge(WC_Order $order, string $chargeId, string $flowStatus): array
    {
        $cloverOrderUuid = $order->get_meta('weeconnectpay_clover_order_uuid');
        if (!is_string($cloverOrderUuid) || $cloverOrderUuid === '') {
            $cloverOrderUuid = null;
        }

        // Recover the card details stashed at initial-charge time. May be
        // empty for older interim states that pre-date the stash — that just
        // means `weeconnectpay_card_brand` meta won't get refreshed, the
        // charge itself still completes correctly.
        $stashedCardDetails = $order->get_meta('weeconnectpay_threeds_pending_card_details');
        $cardDetails = is_array($stashedCardDetails) ? $stashedCardDetails : [];

        // Derive the subscription credential flags from the WC order type
        // (matches CreateCloverOrderChargeRequest::setRequestBody). The
        // browser never sees these — keeping them server-side avoids
        // exposing subscription internals through the interim round-trip.
        $orderType = WeeConnectPayHelper::get_order_type($order);
        $saveCredential = ($orderType === 'subscription');
        $savedCredentialUuid = null;
        if (in_array($orderType, ['subscription_renewal', 'subscription_early_renewal'], true)) {
            $savedCredentialUuid = WeeConnectPayHelper::get_token_uuid_from_subscription_order($order);
        }

        LogService::info(sprintf(
            'Finalizing 3-D Secure charge for WooCommerce order (#%d) — charge_id %s, flow_status %s, order_uuid %s, order_type %s',
            $order->get_id(),
            $chargeId,
            $flowStatus,
            $cloverOrderUuid ?? '(none)',
            $orderType
        ));

        try {
            $finalizeResponse = (new FinalizeThreeDSChargeRequest())->POST(
                $chargeId,
                $flowStatus,
                $cloverOrderUuid,
                $savedCredentialUuid !== '' ? $savedCredentialUuid : null,
                $saveCredential
            );
        } catch (ClientException $e) {
            // Guzzle treats 4xx as exceptions; the API returns 402 on terminal
            // 3DS failures (e.g. card_declined after challenge) with the same
            // body shape it uses for successful charges, just with
            // clover_charge_status=failed + an error block. Hand the PSR-7
            // response off to the regular terminal-state handler so
            // handlePaymentFailure renders the rich note and redirectResponse
            // points the customer at the order page. Mirrors the existing
            // CreateCloverOrderChargeRequest catch at line 1072.
            LogService::error(sprintf(
                '3-D Secure finalize returned 4xx for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
            $finalizeResponse = $e->getResponse();
        } catch (\Throwable $e) {
            LogService::error(sprintf(
                '3-D Secure finalize request threw for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
            return $this->failureResponse();
        }

        // Route through the same terminal-state handler the initial charge uses.
        // Order-scoped charges hit handleOrderChargeResponse; standalone preauth
        // / auth+capture charges hit handleChargeResponse.
        if ($cloverOrderUuid !== null) {
            return $this->handleOrderChargeResponse($order, $finalizeResponse, $cardDetails);
        }

        return $this->handleChargeResponse($order, $finalizeResponse, $cardDetails);
    }

    /**
     * Build the success-side WooCommerce order note with the same rhythm
     * {@see handlePaymentFailure()} uses for failures — title + linked charge
     * id + amount + card brand + last 4 + auth/ref num + 3-D Secure line.
     *
     * Used by both successful charge paths (handleOrderChargeResponse and
     * handleChargeResponse) so successful and failed charges carry parity in
     * the order-notes panel. Sparse output today was the user-visible bug.
     *
     * @param string                $title          Localised heading, e.g. "Clover payment successful!"
     * @param string                $idLabel        Localised id label, e.g. "Payment ID: "
     * @param string                $chargeId       Clover-side payment or charge id (used for link + display)
     * @param string                $receiptUrl     Pre-built Clover receipt URL (from CloverReceiptsHelper)
     * @param int|null              $amountCents    Charge amount in cents (e.g. clover_charge_amount or computed amountDue)
     * @param string|null           $currency       Three-letter ISO code (e.g. usd, cad)
     * @param array<string, mixed>  $cardDetails    Same shape extractCardDetails() returns
     * @param string|null           $authCode       Optional Clover auth code (charge_response.auth_code)
     * @param string|null           $refNum         Optional Clover reference number (charge_response.ref_num)
     * @param array<string, mixed>|null $threedsData 3-D Secure fields from {@see ThreeDSecureFormatter::extractFromChargeResponse}
     * @param bool                  $threedsOptIn   Whether the merchant has 3DS beta enabled (gates the 3DS line)
     * @param object|null           $rawResponse    The full decoded charge response — used to pull authorization / AVS / CVV context when no 3DS step ran.
     */
    protected function buildSuccessOrderNote(
        string $title,
        string $idLabel,
        string $chargeId,
        string $receiptUrl,
        ?int $amountCents,
        ?string $currency,
        array $cardDetails,
        ?string $authCode,
        ?string $refNum,
        ?array $threedsData,
        bool $threedsOptIn,
        $rawResponse = null
    ): string {
        $note = '<b>' . esc_html($title) . '</b><br>';

        $note .= '<b>' . esc_html($idLabel) . '</b><a href="' . esc_url($receiptUrl) . '">'
            . esc_html($chargeId) . '</a><br>';

        if ($amountCents !== null && $currency !== null && $currency !== '') {
            $note .= '<b>' . esc_html__('Amount: ', 'weeconnectpay') . '</b>'
                . esc_html(sprintf('%.2f', $amountCents / 100)) . ' '
                . esc_html(strtoupper($currency)) . '<br>';
        }

        if (! empty($cardDetails['card_brand'])) {
            $note .= '<b>' . esc_html__('Card Type: ', 'weeconnectpay') . '</b>'
                . esc_html($cardDetails['card_brand']) . '<br>';
        }

        if (! empty($cardDetails['card_last4'])) {
            $note .= '<b>' . esc_html__('Last 4: ', 'weeconnectpay') . '</b>'
                . esc_html($cardDetails['card_last4']) . '<br>';
        }

        if (! empty($authCode)) {
            $note .= '<b>' . esc_html__('Auth Code: ', 'weeconnectpay') . '</b>'
                . esc_html($authCode) . '<br>';
        }

        if (! empty($refNum)) {
            $note .= '<b>' . esc_html__('Reference: ', 'weeconnectpay') . '</b>'
                . esc_html((string) $refNum) . '<br>';
        }

        $note .= ThreeDSecureFormatter::formatOrderNoteLine(
            $threedsData,
            $threedsOptIn,
            self::extractNoThreeDSContext($rawResponse)
        );

        return $note;
    }

    /**
     * Pull authorization-side facts (network outcome + AVS + CVV) from a
     * decoded Clover charge response so we can show them when no 3DS step
     * ran. Returns null when nothing useful is present — keeps the caller
     * agnostic about which response shape it has.
     *
     * @param object|null $response The decoded charge response.
     * @return array<string, string>|null
     */
    private static function extractNoThreeDSContext($response): ?array
    {
        if (! is_object($response)) {
            return null;
        }

        $chargeBody = $response->charge_response ?? null;
        if (! is_object($chargeBody)) {
            return null;
        }

        $outcome = $chargeBody->outcome ?? null;
        $source  = $chargeBody->source  ?? null;

        $context = array_filter([
            'network_status'      => is_object($outcome) ? ($outcome->network_status ?? null) : null,
            'outcome_type'        => is_object($outcome) ? ($outcome->type ?? null) : null,
            'address_line1_check' => is_object($source)  ? ($source->address_line1_check ?? null) : null,
            'address_zip_check'   => is_object($source)  ? ($source->address_zip_check   ?? null) : null,
            'cvc_check'           => is_object($source)  ? ($source->cvc_check           ?? null) : null,
        ], fn ($v) => $v !== null && $v !== '');

        return $context ?: null;
    }

    /**
     * Handles payment failure by logging errors and adding detailed order notes.
     */
    protected function handlePaymentFailure(WC_Order $order, $response, array $cardDetails, string $logMessage = 'Payment declined'): void
    {
        // Extract error information from response
        $errorCode = isset($response->error->code) ? $response->error->code : '';
        $errorMessage = $response->error->message ?? $response->message ?? '';
        $declineCode = isset($response->error->declineCode) ? $response->error->declineCode : '';
        $chargeId = $response->error->charge ?? $response->error->charge ?? $response->clover_payment_id ?? $response->clover_charge_id ??'';

        LogService::error(sprintf(
            '%s for WooCommerce order (#%d) - Code: %s, Message: %s, Decline Code: %s',
            $logMessage,
            $order->get_id(),
            $errorCode ?? 'Unknown',
            $errorMessage ?? 'None',
            $declineCode ?? 'None'
        ));

        // Build detailed error note
        $errorNote = '<b>' . __('Clover payment failed.', 'weeconnectpay') . '</b><br>';
        
        // Add charge ID if available
        if (!empty($chargeId)) {
            $paymentReceiptUrl = CloverReceiptsHelper::getEnvReceiptUrl(
                $chargeId,
                CloverReceiptsHelper::RECEIPT_TYPES['CHARGE']
            );
            $errorNote .= '<b>' . __('Charge ID: ', 'weeconnectpay') . '</b>' . 
                '<a href="' . esc_url($paymentReceiptUrl) . '">' . esc_html($chargeId) . '</a><br>';
        }
        
        // Include error code if available
        if (!empty($errorCode)) {
            $errorNote .= '<b>' . __('Error code: ', 'weeconnectpay') . '</b>' . 
                esc_html($errorCode) . '<br>';
        }
        
        // Include decline code if available
        if (!empty($declineCode)) {
            $errorNote .= '<b>' . __('Decline code: ', 'weeconnectpay') . '</b>' . 
                esc_html($declineCode) . '<br>';
        }
        
        // Include error message if available
        if (!empty($errorMessage)) {
            $errorNote .= '<b>' . __('Clover error message: ', 'weeconnectpay') . '</b>' .
                esc_html($errorMessage) . '<br>';
        }
        
        // Add card details if available
        if (isset($cardDetails) && is_array($cardDetails)) {
            if (!empty($cardDetails['card_brand'])) {
                $errorNote .= '<b>' . __('Card Type', 'weeconnectpay') . ': </b>' .
                    esc_html($cardDetails['card_brand']) . '<br>';
            }

            if (!empty($cardDetails['card_last4'])) {
                $errorNote .= '<b>' . __('Last 4', 'weeconnectpay') . ': </b>' .
                    esc_html($cardDetails['card_last4']) . '<br>';
            }
        }

        // 3-D Secure context on failure. Two distinct shapes possible:
        //   1. Frictionless decline — response carries threeds.validation_result
        //      directly (e.g. AUTHENTICATION_FAILED). Render via the formatter
        //      the same way the success path does.
        //   2. Terminal failure after challenge — response has no threeds
        //      block but the Clover error message contains the literal string
        //      "3DS Authentication" (e.g. "3DS Authentication was
        //      unsuccessful."). Surface a dedicated line so the failure type
        //      is unambiguous to the merchant.
        $threedsOptIn = (bool) $this->integrationSettings->getThreeDSecureBetaOrDefault();
        $threedsData  = ThreeDSecureFormatter::extractFromChargeResponse($response);
        if ($threedsData !== null) {
            $errorNote .= ThreeDSecureFormatter::formatOrderNoteLine($threedsData, $threedsOptIn);
        } elseif ($threedsOptIn && is_string($errorMessage) && stripos($errorMessage, '3DS Authentication') !== false) {
            $errorNote .= '<br><b>' . esc_html__('3-D Secure:', 'weeconnectpay') . '</b> '
                . esc_html__('Authentication failed during challenge.', 'weeconnectpay');
        }

        $order->add_order_note($errorNote);
        $order->update_status('failed');
    }

    /**
     * @return array<string, string>
     *
     * A generic success response.
     */
    protected function successResponse(WC_Order $order): array
    {
        $orderStatus = $order->get_status();
        LogService::debug(sprintf(
            'Generating redirect response for WooCommerce order (#%d) - Redirecting to order received page (Order Status: %s)',
            $order->get_id(),
            $orderStatus
        ));
        return [
            'result' => 'success',
            'redirect' => $order->get_checkout_order_received_url(),
        ];
    }

    /**
     * @return array<string, string>
     *
     * A generic failure response.
     */
    protected function failureResponse(): array
    {
        LogService::debug('Payment processing halted - No payment was processed as a pre-processing condition was not met. This is a generic failure response used before payment processing begins.');

        // WooCommerce builds its AJAX `result: failure` + rendered messages from
        // the error notices (send_ajax_failure_response). Without a notice the
        // classic checkout receives an empty/non-actionable response and the form
        // hangs. Add a generic notice only when a caller hasn't already queued a
        // more specific one, so we never double up.
        if ( function_exists( 'wc_add_notice' ) && function_exists( 'wc_notice_count' ) && wc_notice_count( 'error' ) === 0 ) {
            wc_add_notice( __( 'We were unable to verify your checkout and no payment was processed. Please try again.', 'weeconnectpay' ), 'error' );
        }

        return [
            'result' => 'fail',
            'redirect' => '',
        ];
    }

    /**
     * Helper: Gets the appropriate redirect URL, preferring subscription page over order page when available.
     */
    protected function getRedirectUrl(WC_Order $order): string
    {
        // Check if WooCommerce Subscriptions is active and if this order has associated subscriptions
        if (function_exists('wcs_get_subscriptions_for_order')) {
            $subscriptions = wcs_get_subscriptions_for_order($order, array('order_type' => 'any'));
            
            if (!empty($subscriptions) && is_array($subscriptions)) {
                // Get the first subscription and return its view URL
                $subscription = reset($subscriptions);
                if ($subscription && method_exists($subscription, 'get_view_order_url')) {
                    return $subscription->get_view_order_url();
                }
            }
        }
        
        // Default to order URL if no subscription found
        return $order->get_view_order_url();
    }

    /**
     * Helper: Returns a redirect response array based on the order.
     */
    protected function redirectResponse(WC_Order $order): array
    {
        $orderStatus = $order->get_status();
        $redirectUrl = $this->getRedirectUrl($order);
        
        LogService::debug(sprintf(
            'Generating redirect response for WooCommerce order (#%d) - Redirecting to %s page (Order Status: %s)',
            $order->get_id(),
            strpos($redirectUrl, 'view-subscription') !== false ? 'subscription' : 'order details',
            $orderStatus
        ));
        return [
            'result'   => 'success',
            'redirect' => $redirectUrl,
        ];
    }


    // If you already have methods like process_custom_tenders() or customerPayload(),
    // you can either call them here or integrate their logic.

    /**
     * Processes a single custom tender by making an API request.
     *
     * @param WC_Order $order The WooCommerce order object.
     * @param string $tenderId           The unique identifier of the custom tender.
     * @param string $cloverOrderUuid    The Clover Order UUID.
     * @param string $customTenderLabel  The label of the custom tender.
     * @param int $amount             The amount in cents.
     * @param string $ipAddress          The customer's IP address.
     *
     * @return ResponseInterface
     * @throws WeeConnectPayException
     */
    private function processCustomTender(WC_Order $order, string $tenderId, string $cloverOrderUuid, string $customTenderLabel, int $amount, string $ipAddress): ResponseInterface
    {
        try {
            LogService::info(sprintf(
                'Processing custom tender charge (Type: %s) of %.2f %s on Clover order (ID: %s) for WooCommerce order (#%d)',
                $customTenderLabel,
                $amount / 100,
                $order->get_currency(),
                $cloverOrderUuid,
                $order->get_id()
            ));
            return (new CreateCloverOrderCustomTenderChargeRequest())->POST($cloverOrderUuid, $customTenderLabel, $amount, $ipAddress);
        } catch (Exception $e) {
            throw new WeeConnectPayException('Failed to process custom tender: ' . $e->getMessage());
        }
    }

    /**
     * Handles the logic when a custom tender payment is successful.
     *
     * @param WC_Order $order The WooCommerce order object.
     * @param string $tenderLabel
     * @param string $tenderId The unique identifier of the custom tender.
     * @param string $cloverChargeId The Clover charge ID.
     * @param string $cloverOrderUuid The Clover Order UUID.
     * @param int $amount The amount in cents.
     */
    private function handleCustomTenderPaymentSuccess(WC_Order $order, string $tenderLabel, string $tenderId, string $cloverChargeId, string $cloverOrderUuid, int $amount) {
        LogService::info(sprintf(
            'Successfully created custom tender charge (ID: %s, Type: %s) of %.2f %s on Clover order (ID: %s) for WooCommerce order (#%d)',
            $cloverChargeId,
            $tenderLabel,
            $amount / 100,
            $order->get_currency(),
            $cloverOrderUuid,
            $order->get_id()
        ));
        $paymentReceiptUrl = CloverReceiptsHelper::getEnvReceiptUrl(
            $cloverChargeId,
            CloverReceiptsHelper::RECEIPT_TYPES['CHARGE']
        );

        $successOrderNote  = '<b>' . __( 'Clover custom tender payment successful!', 'weeconnectpay' ) . '</b><br>';
        $successOrderNote .= '<b>' . __( 'Payment ID: ', 'weeconnectpay' ) . '</b>' . '<a href="' . esc_url( $paymentReceiptUrl ) . '">' . esc_html( $cloverChargeId ) . '</a><br>';

        if ( ! empty( $tenderLabel ) ) {
            $successOrderNote .= '<b>' . __( 'Custom Tender', 'weeconnectpay' ) . ': </b>' . esc_html( $tenderLabel ) . ' (ID: ' . esc_html( $tenderId ) . ')';
        }

        $order->add_order_note( $successOrderNote );

        WeeConnectPayCustomTenderHelper::executeCustomTenderCallback($order,$tenderId, 'chargeCreationCallback');
    }

    /**
     * Handles the logic when a custom tender payment fails.
     *
     * @param WC_Order $order The WooCommerce order object.
     * @param object $decodedChargeResponse The decoded API response.
     * @param string $tenderLabel
     * @param string $tenderId The unique identifier of the custom tender.
     */
    private function handleCustomTenderPaymentFailure(WC_Order $order, object $decodedChargeResponse, string $tenderLabel, string $tenderId ) {
        // Handle specific error: order already paid
        if ( isset( $decodedChargeResponse->error->code ) && 'order_already_paid' === $decodedChargeResponse->error->code ) {
            $message = isset( $decodedChargeResponse->error->message ) ? esc_html( $decodedChargeResponse->error->message ) : '';
            $alreadyPaidOrderNote  = '<b>' . __( 'Clover error message: ', 'weeconnectpay' ) . '</b><br>';
            $alreadyPaidOrderNote .= $message . '<br>';
            $alreadyPaidOrderNote .= __( 'Please check the order in the Clover dashboard for the full payment information.', 'weeconnectpay' );
            $order->add_order_note( $alreadyPaidOrderNote );

            // Mark payment as complete since it's already paid
            $order->payment_complete();
            
            // Attempt to print the Clover order (best effort - will not interrupt processing)
            $this->attemptCloverOrderPrint($order);
            
            return;
        }

        LogService::error( "decodedChargeResponse: " . json_encode( $decodedChargeResponse ) );

        if ( isset( $decodedChargeResponse->error->charge ) ) {
            $chargeErrorNote  = '<b>' . __( 'Clover custom tender payment failed.', 'weeconnectpay' ) . '</b><br>';
            $chargeErrorNote .= '<b>' . __( 'Payment ID: ', 'weeconnectpay' ) . '</b>' . esc_html( $decodedChargeResponse->error->charge ) . '<br>';

            if ( ! empty( $tenderLabel ) ) {
                $chargeErrorNote .= '<b>' . __( 'Custom Tender', 'weeconnectpay' ) . ': </b>' . esc_html( $tenderLabel );
            }

            if ( isset( $decodedChargeResponse->error->message ) ) {
                $chargeErrorNote .= '<b>' . __( 'Clover error message: ', 'weeconnectpay' ) . '</b>' . esc_html( $decodedChargeResponse->error->message ) . '<br>';
            }

            $order->add_order_note( $chargeErrorNote );

        } elseif ( isset( $decodedChargeResponse->message ) || isset( $decodedChargeResponse->error->message ) ) {
            $errorNote  = '<b>' . __( 'Clover custom tender payment failed.', 'weeconnectpay' ) . '</b><br>';

            if ( isset( $decodedChargeResponse->message ) ) {
                $errorNote .= '<b>' . __( 'Clover response message: ', 'weeconnectpay' ) . '</b>' . esc_html( $decodedChargeResponse->message ) . '<br>';
            }

            if ( isset( $decodedChargeResponse->error->code ) ) {
                $errorNote .= '<b>' . __( 'Clover error code: ', 'weeconnectpay' ) . '</b>' . esc_html( $decodedChargeResponse->error->code ) . '<br>';
            }

            if ( isset( $decodedChargeResponse->error->message ) ) {
                $errorNote .= '<b>' . __( 'Clover error message: ', 'weeconnectpay' ) . '</b>' . esc_html( $decodedChargeResponse->error->message ) . '<br>';
            }

            $order->add_order_note( $errorNote );

        } else {
            $otherNote  = '<b>' . __( 'Clover custom tender payment failed - Unhandled context, see response payload: ', 'weeconnectpay' ) . '</b>';
            $otherNote .= json_encode( $decodedChargeResponse );
            $order->add_order_note( $otherNote );
        }

        $order->update_status( 'failed' );

        // Notify the user about the failure.
        wc_add_notice( __( 'Payment failed. Please try again.', 'weeconnectpay' ), 'error' );
        WeeConnectPayCustomTenderHelper::executeCustomTenderCallback($order,$tenderId, 'chargeCreationCallback');
    }

    /**
     * Processes all pending custom tenders associated with the order.
     *
     * @param WC_Order $order           The WooCommerce order object.
     * @param string $cloverOrderUuid The Clover Order UUID.
     * @return array                    The result of processing custom tenders.
     *                                   'result' => 'success' or 'fail',
     *                                   'redirect' => URL or '',
     *                                   'skip_payment' => bool (true if no further payment needed)
     *                                   'clover_order_amount_due' => int|null
     */
    private function process_custom_tenders(WC_Order $order, string $cloverOrderUuid ): array
    {
        // Retrieve all custom tenders for the order using WeeConnectPayCustomTenderHelper
        try {
            $customTenders = WeeConnectPayCustomTenderHelper::getCustomTenders($order);
        } catch (InvalidArgumentException $e) {
            LogService::error('Error retrieving custom tenders: ' . $e->getMessage());
            wc_add_notice(__('Payment processing failed. Please try again.', 'weeconnectpay'), 'error');
            return [
                'result' => 'fail',
                'redirect' => '',
            ];
        }

        if (empty($customTenders)) {
            // No custom tenders to process; proceed normally.
            return [
                'result' => 'success',
                'redirect' => '',
                'skip_payment' => false,
            ];
        }

        $ipAddress = $order->get_customer_ip_address();

        // Flag to determine if any tenders were successfully processed
        $anyTendersProcessed = false;
        // Variable to determine how much we have left to pay. Is updated each custom tender processed successfully
        $cloverOrderAmountDue = null;

        foreach ($customTenders as $tender) {
            // Process only tenders with 'pending' status
            if (isset($tender['status']) && 'pending' !== $tender['status']) {
                continue;
            }

            LogService::info('Processing custom tender: ' . json_encode($tender));

            $customTenderLabel = $tender['provider'];
            $amount = $tender['amount'];
            $tenderId = $tender['id'];

            try {
                // Process each custom tender
                $chargeResponse = $this->processCustomTender($order, $tenderId, $cloverOrderUuid, $customTenderLabel, $amount, $ipAddress);

                // Decode and validate the response
                $chargeResponseContent = $chargeResponse->getBody()->getContents();
                $decodedChargeResponse = WeeConnectPayUtilities::validateJsonObject($chargeResponseContent);

                LogService::info('Decoded custom tender charge response: ' . json_encode($decodedChargeResponse));

                // Extract necessary fields from the response
                $tenderStatus = isset($decodedChargeResponse->data->clover_payment_status) ? sanitize_text_field($decodedChargeResponse->data->clover_payment_status) : '';
                $cloverChargeId = isset($decodedChargeResponse->data->clover_payment_id) ? sanitize_text_field($decodedChargeResponse->data->clover_payment_id) : '';

                $cloverOrderAmountDue = $decodedChargeResponse->data->clover_order_amount_due ?? null;

                switch ($tenderStatus) {
                    case 'paid':
                    case 'created':
                        // Update the tender's status to 'success' and assign the Clover charge ID
                        WeeConnectPayCustomTenderHelper::updateCustomTenderToPaid($order, $tenderId, $cloverChargeId);
                        $this->handleCustomTenderPaymentSuccess($order, $customTenderLabel, $tenderId, $cloverChargeId, $cloverOrderUuid, $amount);
                        $anyTendersProcessed = true;
                        break;

                    case 'failed':
                        // Update the tender's status to 'failed'
                        WeeConnectPayCustomTenderHelper::updateCustomTenderToFailed($order, $tenderId);
                        $this->handleCustomTenderPaymentFailure($order, $decodedChargeResponse, $customTenderLabel, $tenderId);
                        break;

                    default:
                        LogService::error(sprintf(
                            'Invalid clover_payment_status (%s) for custom tender charge on WooCommerce order (#%d)',
                            $tenderStatus,
                            $order->get_id()
                        ));
                        wc_add_notice(__('Payment processing failed due to an unexpected error.', 'weeconnectpay'), 'error');
                        WeeConnectPayCustomTenderHelper::updateCustomTenderToFailed($order, $tenderId);
                        return [
                            'result' => 'fail',
                            'redirect' => '',
                        ];
                }

            } catch (Exception $e) {
                LogService::error(sprintf(
                    'Exception during custom tender processing for WooCommerce order (#%d): %s',
                    $order->get_id(),
                    $e->getMessage()
                ));
                WeeConnectPayCustomTenderHelper::updateCustomTenderToFailed($order, $tenderId);
                wc_add_notice(__('Payment processing failed. Please try again.', 'weeconnectpay'), 'error');
                return [
                    'result' => 'fail',
                    'redirect' => '',
                ];
            }
        }

        // If any tenders were processed successfully and they cover the entire order total
        // or if the order total is 0 and all pending tenders were processed
        if ($anyTendersProcessed && ($this->isCustomTendersCoverOrderTotal($order) || $order->get_total() <= 0)) {
            $this->addPostTokenizationNotes($order);
            $order->payment_complete();
            
            // Attempt to print the Clover order (best effort - will not interrupt processing)
            $this->attemptCloverOrderPrint($order);
            
            return [
                'result' => 'success',
                'redirect' => $order->get_checkout_order_received_url(),
                'skip_payment' => true,
            ];
        }

        // If some tenders were processed successfully but don't cover the total, proceed to actual payment processing
        return [
            'result' => 'success',
            'redirect' => '',
            'skip_payment' => false,
            'clover_order_amount_due' => $cloverOrderAmountDue,
        ];
    }

    /**
     * Determines if custom tenders cover the entire order total.
     *
     * @param WC_Order $order The WooCommerce order object.
     * @return bool        True if custom tenders cover the total, false otherwise.
     */
    private function isCustomTendersCoverOrderTotal(WC_Order $order): bool
    {
        LogService::debug('Checking if custom tenders cover order total for WooCommerce order (#' . $order->get_id() . ')');
        
        // Get current order total in cents
        $currentOrderTotalInCents = intval(round($order->get_total() * 100));
        LogService::debug('Current order total in cents: ' . $currentOrderTotalInCents);
        
        // Get all custom tenders
        $customTenders = WeeConnectPayCustomTenderHelper::getCustomTenders($order);
        LogService::debug('Custom tenders found: ' . json_encode($customTenders));

        // Calculate total of all tenders (both pending and successful)
        $totalTenderAmount = 0;
        foreach ($customTenders as $tender) {
            // Include both pending and successful tenders since we validated the total earlier
            if (isset($tender['amount'])) {
                $totalTenderAmount += $tender['amount'];
                LogService::debug(sprintf(
                    'Added tender amount %d, status: %s, new total: %d', 
                    $tender['amount'],
                    $tender['status'] ?? 'unknown',
                    $totalTenderAmount
                ));
            }
        }

        // Reconstruct original order total by adding current total and all tender amounts
        $reconstructedTotalInCents = $currentOrderTotalInCents + $totalTenderAmount;
        LogService::debug(sprintf(
            'Reconstructed total: %d cents (current total: %d + total tenders: %d)',
            $reconstructedTotalInCents,
            $currentOrderTotalInCents,
            $totalTenderAmount
        ));

        // Calculate successful tender total
        $successfulTenderTotal = 0;
        foreach ($customTenders as $tender) {
            if (isset($tender['status']) && $tender['status'] === 'success') {
                $successfulTenderTotal += $tender['amount'];
                LogService::debug(sprintf(
                    'Added successful tender amount %d, new successful total: %d',
                    $tender['amount'],
                    $successfulTenderTotal
                ));
            }
        }

        // Check if successful tenders cover the reconstructed total
        $tendersCoversTotal = $successfulTenderTotal >= $reconstructedTotalInCents;
        LogService::debug(sprintf(
            'Successful tenders (%d) %s reconstructed total (%d)',
            $successfulTenderTotal,
            $tendersCoversTotal ? '>=' : '<',
            $reconstructedTotalInCents
        ));

        return $tendersCoversTotal;
    }


    /**
     * Adds post-tokenization verification notes to the order.
     *
     * @param WC_Order $order The WooCommerce order object.
     * @return void
     */
    private function addPostTokenizationNotes(WC_Order $order ): void {
        try {
	        LogService::debug(sprintf(
		        'Entering addPostTokenizationNotes for order #%d',
		        $order->get_id()
	        ));
            $isPostTokenizationVerificationActive = (new IntegrationSettings())->getPostTokenizationVerificationOrDefault();

            if ( $isPostTokenizationVerificationActive ) {

                // Retrieve all saved credit card charge metadata for a given order.
                $charges = WeeConnectPayHelper::getAllCreditCardCharges($order);

                if (!empty($charges)) {
                    // Currently assuming only one charge exists per order.
                    $chargeData = $charges[0];
                    $chargePostalCode = $chargeData['card_postal_code'] ?? null;
                }

                $shippingPostalCode      = WeeConnectPayUtilities::formatPostalCode( $order->get_shipping_postcode() );
                $billingPostalCode       = WeeConnectPayUtilities::formatPostalCode( $order->get_billing_postcode() );

                if (empty($chargePostalCode)) {
                    $warning_note = __('⚠️ Warning: An error has occurred: We could not detect the postal code used for the transaction.', 'weeconnectpay');
                    LogService::warning(sprintf(
                        'addPostTokenizationNotes: chargePostalCode missing for order #%d',
                        $order->get_id()
                    ));
                    $order->add_order_note($warning_note);
                    return;
                }


                $tokenizationPostalCode  = WeeConnectPayUtilities::formatPostalCode( $chargePostalCode );

                if ( $shippingPostalCode && $shippingPostalCode !== $billingPostalCode ) {
                    $info_note = sprintf(
                        __( 'ℹ️ Info: Please note that the shipping ZIP/Postal code "%s" and the billing ZIP/Postal code "%s" are different.', 'weeconnectpay' ),
                        $shippingPostalCode,
                        $billingPostalCode
                    );
                    $order->add_order_note( $info_note );
                }

                if ( $billingPostalCode !== $tokenizationPostalCode ) {
                    $warning_note = sprintf(
                        __( '⚠️ Warning: Please note that the billing ZIP/Postal code "%s" and the payment card ZIP/Postal code "%s" are different. These should be the same.', 'weeconnectpay' ),
                        $billingPostalCode,
                        $tokenizationPostalCode
                    );
                    $order->add_order_note( $warning_note );
                }
            }
            LogService::debug(sprintf(
                'Exiting addPostTokenizationNotes for order #%d',
                $order->get_id()
            ));
        } catch (\Throwable $e) {
            LogService::error(sprintf(
                'Exception in addPostTokenizationNotes for order #%d: %s',
                $order->get_id(),
                $e->getMessage()
            ));
        }
    }

    /**
     * Attempt to print the Clover order using the printing service
     * 
     * This is a "best effort" operation that will never interrupt order processing.
     * All outcomes are logged appropriately.
     * 
     * @param WC_Order $order The WooCommerce order to print
     * @return void
     */
    private function attemptCloverOrderPrint(WC_Order $order): void {
        try {
            $printingService = new CloverOrderPrintingService();
            $printingService->attemptOrderPrint($order);
        } catch (\Throwable $e) {
            // Extra safety net - this should never happen since the printing service
            // is designed to never throw exceptions, but we include this just in case
            LogService::error(sprintf(
                'Unexpected exception in printing service for WooCommerce order (#%d): %s',
                $order->get_id(),
                $e->getMessage()
            ));
        }
    }

	/**
	 * Handles payment processing exceptions by logging and displaying appropriate error messages.
	 *
	 * For known exception codes (user-facing errors), displays the specific exception message.
	 * For unknown exceptions, displays a generic error message to protect sensitive information.
	 * Always logs the full error details and returns a failure response.
	 *
	 * @param WeeConnectPayException $exception The exception thrown during payment processing.
	 * @return array<string, string> Failure response array with 'result' and 'redirect' keys.
	 */
	public function handlePaymentProcessingWeeConnectPayException( WeeConnectPayException $exception ): array {
// Handle specific exception codes with user-friendly messages
		if ( $exception->getCode() === ExceptionCode::MISSING_SHIPPING_STATE
		     || $exception->getCode() === ExceptionCode::CUSTOMER_CREATION_EXCEPTION
		     || $exception->getCode() === ExceptionCode::STANDARDIZED_RESPONSE_EXCEPTION
		     || $exception->getCode() === ExceptionCode::INVALID_JSON_EXCEPTION
		     || $exception->getCode() === ExceptionCode::ORDER_LINE_ITEM_TOTAL_MISMATCH
		     || $exception->getCode() === ExceptionCode::UNSUPPORTED_ORDER_ITEM_TYPE
		) {
			LogService::error( sprintf(
				'Payment processing error shown to customer: %s',
				$exception->getMessage()
			) );
			wc_add_notice( esc_html( $exception->getMessage() ), 'error' );
		} else {
			LogService::error( 'An unhandled exception happened with the payment processor. Message: ' . $exception->getMessage() );
			wc_add_notice( __( 'Payment processing failed. Please try again.', 'weeconnectpay' ), 'error' );
		}

		return $this->failureResponse();
	}

}