<?php
/**
 * ConvesioPay Webhook Handler.
 *
 * @package PeachPay
 */

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

/**
 * ConvesioPay Webhook Handler class.
 */
class PeachPay_ConvesioPay_Webhook {
	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register webhook routes.
	 */
	public function register_routes() {
		register_rest_route(
			'peachpay/v1',
			'/convesiopay/webhook',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handle_webhook' ),
				'permission_callback' => '__return_true',
			)
		);
		
		register_rest_route(
			'peachpay/v1',
			'/convesiopay/config',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_config' ),
				'permission_callback' => array( $this, 'check_config_permissions' ),
			)
		);
		
	}

	/**
	 * Check permissions for config endpoint.
	 * Only allow authenticated users with proper capabilities.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return bool
	 */
	public function check_config_permissions( $request ) {
		// Allow only authenticated users with WooCommerce management capabilities
		if ( current_user_can( 'manage_woocommerce' ) ) {
			return true;
		}
		
		// Allow users who can edit shop orders
		if ( current_user_can( 'edit_shop_orders' ) ) {
			return true;
		}
		
		// Deny all other requests
		return false;
	}
	
	/**
	 * Get ConvesioPay configuration.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_config( $request ) {
		try {
			$config = get_option( 'peachpay_connected_convesiopay_config', array() );
			
			if ( empty( $config ) ) {
				return new WP_REST_Response( 
					array( 
						'success' => false, 
						'error' => 'ConvesioPay not configured' 
					), 
					200 
				);
			}
			
			// Return only necessary frontend configuration
			$frontend_config = array(
				'integrationId' => $config['integrationId'] ?? '',
				'publicKey' => $config['client_key'] ?? '',
				'environment' => peachpay_is_test_mode() ? 'test' : 'live',
				'currency' => get_woocommerce_currency(),
			);
			
			return new WP_REST_Response( 
				array( 
					'success' => true, 
					'data' => $frontend_config 
				), 
				200 
			);
			
		} catch ( \Throwable $e ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: Failed to get config - ' . $e->getMessage() );
			peachpay_log(
				'error',
				'ConvesioPay Webhook config load - failed.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook: Failed to get config - ' . $e->getMessage()
					)
				)
			);
			return new WP_REST_Response(
				array(
					'success' => false,
					'error' => 'Failed to get configuration'
				),
				500
			);
		}
	}

	/**
	 * Handle webhook.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function handle_webhook( $request ) {
		
		// Get webhook data
		$webhook_data = $request->get_json_params();
		$headers = $request->get_headers();
		$raw_body = $request->get_body();
		
		// Log specific webhook fields
		$event_type = $webhook_data['eventType'] ?? 'unknown';
		$payment_id = $webhook_data['paymentId'] ?? 'unknown';
		$order_number = $webhook_data['orderNumber'] ?? 'unknown';
		$status = $webhook_data['status'] ?? 'unknown';
		$amount = $webhook_data['amount'] ?? 'unknown';
		$currency = $webhook_data['currency'] ?? 'unknown';
		
		// Log payment method details
		$payment_method_details = $webhook_data['paymentMethodDetails'] ?? array();
		$payment_method_type = $payment_method_details['type'] ?? 'unknown';
		$payment_method_brand = $payment_method_details['brand'] ?? 'unknown';
		
		// Log additional webhook fields
		$integration = $webhook_data['integration'] ?? 'unknown';
		$email = $webhook_data['email'] ?? 'unknown';
		$name = $webhook_data['name'] ?? 'unknown';
		$created_at = $webhook_data['createdAt'] ?? 'unknown';
		$updated_at = $webhook_data['updatedAt'] ?? 'unknown';
		

		peachpay_log(
				'debug',
				'ConvesioPay Webhook request received - debug.',
				array(
				'event'     => 'webhooks.convesiopay.webhook.process.debug',
				'component' => 'webhooks',
				'feature'   => 'convesiopay',
				'operation' => 'webhook.process',
				'outcome'   => 'debug',
				'method'    => __METHOD__,
				'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook: Received event=' . $event_type . ' order_number=' . $order_number . ' payment_id=' . $payment_id
				)
			)
		);

		// Validate webhook signature (ConvesioPay documentation requirement)
		if ( ! $this->validate_webhook_signature( $request ) ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: Invalid signature' );
			peachpay_log(
				'error',
				'ConvesioPay Webhook signature validation - failed.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				)
			);
			return new WP_REST_Response( array( 'error' => 'Invalid signature' ), 401 );
		}

		// Validate webhook (basic validation)
		if ( empty( $webhook_data ) ) {
			peachpay_log(
				'error',
				'ConvesioPay Webhook payload validation - failed.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				)
			);
			return new WP_REST_Response( array( 'error' => 'Invalid webhook data' ), 400 );
		}

		$event_type = $webhook_data['eventType'] ?? '';

		// Payment method update events (no order; sync user stored payment methods)
		if ( $event_type === 'paymentMethods' ) {
			$handler = new PeachPay_ConvesioPay_Webhook_Payment_Methods();
			return $handler->handle( $webhook_data );
		}

		// Payment status events: require order and process payment lifecycle
		// Check for duplicate events (ConvesioPay documentation requirement)
		if ( $this->is_duplicate_event( $webhook_data ) ) {
			peachpay_log(
				'info',
				'ConvesioPay Webhook duplicate event check - already processed.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.success',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'success',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook: Duplicate event found for event=' . $event_type . ' order_number=' . $order_number,
						'webhook_data' => $webhook_data
					)
				)
			);
			return new WP_REST_Response( array( 'status' => 'duplicate_skipped' ), 200 );
		}
		
		try {
			// Extract webhook information (ConvesioPay documentation format)
			$event_type = $webhook_data['eventType'] ?? '';
			$payment_id = $webhook_data['paymentId'] ?? '';
			$order_number = $webhook_data['orderNumber'] ?? '';
			$status = $webhook_data['status'] ?? '';
			$amount = $webhook_data['amount'] ?? '';
			$currency = $webhook_data['currency'] ?? '';
			$integration = $webhook_data['integration'] ?? '';
			$email = $webhook_data['email'] ?? '';
			$name = $webhook_data['name'] ?? '';
			$created_at = $webhook_data['createdAt'] ?? '';
			$updated_at = $webhook_data['updatedAt'] ?? '';
			
			// Extract payment method information from ConvesioPay documentation structure
			$payment_method_details = $webhook_data['paymentMethodDetails'] ?? array();
			// error_log( '[ConvesioPay webhook] Payment method details: ' . print_r( $payment_method_details, true ) );
			$payment_method_type = $payment_method_details['type'] ?? '';
			$payment_method_brand = $payment_method_details['brand'] ?? '';
			$payment_method_cardholder = $payment_method_details['cardHolderName'] ?? '';
			
			// Determine payment method for title (supporting card, btcPay, etc.)
			$payment_method = $payment_method_type;
			if ( $payment_method_type === 'card' && ! empty( $payment_method_brand ) ) {
				$payment_method = $payment_method_brand; // visa, mastercard, etc.
			}elseif ($payment_method_type === 'card' && $payment_method_brand == '' && $payment_method_cardholder == 'Apple Pay PlaceHolder') {
				$payment_method = 'applepay';
			}elseif ($payment_method_type === 'card' && $payment_method_brand == '' && $payment_method_cardholder == 'Google Pay PlaceHolder') {
				$payment_method = 'googlepay';
			}
			
			// Find WooCommerce order by order number
			$order = $this->find_order_by_number( $order_number, $payment_id );
			if ( ! $order ) {
				error_log( '[PP-WEBHOOK] PeachPay Webhook: Order not found for orderNumber ' . $order_number );
				peachpay_log(
					'error',
					'ConvesioPay Webhook order lookup - failed.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.failed',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'failed',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: Order not found for orderNumber ' . $order_number
						)
					)
				);
				return new WP_REST_Response( array( 'error' => 'Order not found' ), 404 );
			}

			peachpay_log(
				'debug',
				'ConvesioPay Webhook order lookup - order found.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.debug',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'debug',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook: Order found for orderNumber ' . $order_number . ' => order #' . $order->get_id() . ' (display number ' . $order->get_order_number() . ')'
					)
				)
			);

			// SECURITY: Validate that the order belongs to ConvesioPay before processing
			// This prevents unauthorized modification of orders from other payment gateways
			$is_convesiopay = $this->is_convesiopay_order( $order );
			// Fallback 1: webhook paymentId matches order's _convesiopay_payment_id (race where webhook arrives before return URL saves meta)
			if ( ! $is_convesiopay && ! empty( $payment_id ) && is_object( $order ) ) {
				$stored_payment_id = $order->get_meta( '_convesiopay_payment_id' );
				if ( is_string( $stored_payment_id ) || is_numeric( $stored_payment_id ) ) {
					if ( (string) $stored_payment_id === (string) $payment_id ) {
						$is_convesiopay = true;
						peachpay_log(
							'debug',
							'ConvesioPay Webhook order validation fallback - payment ID matched.',
							array(
								'event'     => 'webhooks.convesiopay.webhook.process.debug',
								'component' => 'webhooks',
								'feature'   => 'convesiopay',
								'operation' => 'webhook.process',
								'outcome'   => 'debug',
								'method'    => __METHOD__,
								'line'      => __LINE__,
								'data'      => array(
									'details' => 'PeachPay Webhook: Order #' . $order->get_id() . ' accepted via fallback (webhook paymentId matches _convesiopay_payment_id)'
								)
							)
						);
					} else {
						peachpay_log(
							'debug',
							'ConvesioPay Webhook order validation fallback - payment ID mismatch.',
							array(
								'event'     => 'webhooks.convesiopay.webhook.process.debug',
								'component' => 'webhooks',
								'feature'   => 'convesiopay',
								'operation' => 'webhook.process',
								'outcome'   => 'debug',
								'method'    => __METHOD__,
								'line'      => __LINE__,
								'data'      => array(
									'details' => 'PeachPay Webhook: Order #' . $order->get_id() . ' fallback did not match: stored _convesiopay_payment_id=' . (string) $stored_payment_id . ' webhook paymentId=' . (string) $payment_id
								)
							)
						);
					}
				} else {
					peachpay_log(
						'debug',
						'ConvesioPay Webhook order validation fallback - skipped.',
						array(
							'event'     => 'webhooks.convesiopay.webhook.process.debug',
							'component' => 'webhooks',
							'feature'   => 'convesiopay',
							'operation' => 'webhook.process',
							'outcome'   => 'debug',
							'method'    => __METHOD__,
							'line'      => __LINE__,
							'data'      => array(
								'details' => 'PeachPay Webhook: Order #' . $order->get_id() . ' fallback skipped: _convesiopay_payment_id empty or non-scalar, webhook paymentId=' . (string) $payment_id
							)
						)
					);
				}
			}
			if ( ! $is_convesiopay ) {
				peachpay_log(
					'warning',
					'ConvesioPay Webhook order validation - rejected non-ConvesioPay order.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.warning',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'warning',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: Rejected - non-ConvesioPay order #' . $order->get_id() . ' (order_number=' . $order_number . ' payment_id=' . $payment_id . '). Check is_convesiopay_order() logs above for reason.'
						)
					)
				);
				return new WP_REST_Response( array( 'error' => 'Order does not belong to ConvesioPay' ), 403 );
			}
			
			// Process webhook based on event type and status
			$this->process_webhook_event( $order, $event_type, $status, $payment_id, $webhook_data, $payment_method, $payment_method_type );
			
			return new WP_REST_Response( array( 'status' => 'success' ), 200 );
			
		} catch ( \Throwable $e ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: Failed to process - ' . $e->getMessage() );
			peachpay_log(
				'error',
				'ConvesioPay Webhook processing - failed.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook: Failed to process - ' . $e->getMessage()
					)
				)
			);
			return new WP_REST_Response( array( 'error' => 'Webhook processing failed' ), 500 );
		}
	}
	
	/**
	 * Check if an order belongs to ConvesioPay.
	 *
	 * @param WC_Order $order Order object.
	 * @return bool
	 */
	private function is_convesiopay_order( $order ) {
		if ( ! $order ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: is_convesiopay_order() failed - order is null/empty' );
			return false;
		}
		
		// WC_Order_Refund (and Admin Overrides like OrderRefund) do not have get_payment_method() - use parent order
		if ( is_a( $order, 'WC_Order_Refund' ) ) {
			$parent_id = $order->get_parent_id();
			if ( ! $parent_id ) {
				error_log( '[PP-WEBHOOK] PeachPay Webhook: is_convesiopay_order() failed - refund order #' . $order->get_id() . ' has no parent_id' );
				peachpay_log(
					'error',
					'ConvesioPay Webhook refund validation - missing parent order id.',
					array(
						'event'     => 'webhooks.convesiopay.refund.failed',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'refund',
						'outcome'   => 'failed',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: is_convesiopay_order() failed - refund order #' . $order->get_id() . ' has no parent_id'
						)
					)
				);
				return false;
			}
			$order = wc_get_order( $parent_id );
			if ( ! $order ) {
				error_log( '[PP-WEBHOOK] PeachPay Webhook: is_convesiopay_order() failed - parent order #' . $parent_id . ' not found' );
				peachpay_log(
					'error',
					'ConvesioPay Webhook order validation - missing parent order.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.failed',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'failed',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: is_convesiopay_order() failed - parent order #' . $parent_id . ' not found'
						)
					)
				);
				return false;
			}
		}
		
		$order_id = $order->get_id();
		$payment_method = $order->get_payment_method();
		if ( ! is_string( $payment_method ) ) {
			$payment_method = '';
		}
		$original_payment_method = $order->get_meta( '_original_payment_method' );
		$convesiopay_payment_id = $order->get_meta( '_convesiopay_payment_id' );
		// Log once with all key values so we can see why the order passed or failed
		$log_payment_id = ( is_scalar( $convesiopay_payment_id ) && (string) $convesiopay_payment_id !== '' ) ? (string) $convesiopay_payment_id : '(empty)';
		peachpay_log(
			'debug',
			'ConvesioPay Webhook order validation - inspect order context.',
			array(
				'event'     => 'webhooks.convesiopay.webhook.process.debug',
				'component' => 'webhooks',
				'feature'   => 'convesiopay',
				'operation' => 'webhook.process',
				'outcome'   => 'debug',
				'method'    => __METHOD__,
				'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook: is_convesiopay_order() order #' . $order_id . ' payment_method=' . ( $payment_method !== '' ? $payment_method : '(empty)' ) . ' _original_payment_method=' . ( is_string( $original_payment_method ) && $original_payment_method !== '' ? $original_payment_method : '(empty)' ) . ' _convesiopay_payment_id=' . $log_payment_id
				)
			)
		);
		
		// Check if the payment method is a ConvesioPay gateway
		if ( class_exists( 'PeachPay_ConvesioPay_Integration' ) && method_exists( 'PeachPay_ConvesioPay_Integration', 'is_payment_gateway' ) ) {
			if ( PeachPay_ConvesioPay_Integration::is_payment_gateway( $payment_method ) ) {
				peachpay_log(
					'debug',
					'ConvesioPay Webhook order validation - payment method matched.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.debug',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'debug',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: is_convesiopay_order() order #' . $order_id . ' => true (payment_method is ConvesioPay gateway)'
						)
					)
				);
				return true;
			}
			// Unified checkout may store original gateway before redirecting to card/btcpay/applepay
			if ( is_string( $original_payment_method ) && $original_payment_method !== '' && PeachPay_ConvesioPay_Integration::is_payment_gateway( $original_payment_method ) ) {
				peachpay_log(
					'debug',
					'ConvesioPay Webhook order validation - original payment method matched.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.debug',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'debug',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: is_convesiopay_order() order #' . $order_id . ' => true (_original_payment_method is ConvesioPay gateway)'
						)
					)
				);
				return true;
			}
		}
		
		// Check if order has ConvesioPay payment ID stored
		if ( is_string( $convesiopay_payment_id ) && $convesiopay_payment_id !== '' ) {
			peachpay_log(
				'debug',
				'ConvesioPay Webhook order validation - payment meta matched.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.debug',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'debug',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook: is_convesiopay_order() order #' . $order_id . ' => true (_convesiopay_payment_id meta set)'
					)
				)
			);
			return true;
		}
		if ( is_numeric( $convesiopay_payment_id ) && (string) $convesiopay_payment_id !== '' ) {
			peachpay_log(
				'debug',
				'ConvesioPay Webhook order validation - numeric payment meta matched.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.debug',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'debug',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook: is_convesiopay_order() order #' . $order_id . ' => true (_convesiopay_payment_id meta set)'
					)
				)
			);
			return true;
		}
		
		// If order has no payment method set yet, allow it (for new orders)
		// but only if ConvesioPay is connected
		if ( empty( $payment_method ) ) {
			if ( class_exists( 'PeachPay_ConvesioPay_Integration' ) && method_exists( 'PeachPay_ConvesioPay_Integration', 'connected' ) ) {
				$connected = PeachPay_ConvesioPay_Integration::connected();
				peachpay_log(
					'debug',
					'ConvesioPay Webhook order validation - connected fallback check.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.debug',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'debug',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: is_convesiopay_order() order #' . $order_id . ' => ' . ( $connected ? 'true' : 'false' ) . ' (empty payment_method, ConvesioPay connected=' . ( $connected ? 'yes' : 'no' ) . ')'
						)
					)
				);
				return $connected;
			}
		}
		return false;
	}
	
	/**
	 * Find WooCommerce order by order number from webhook.
	 * We send get_order_number() (display number) to ConvesioPay when creating payments, so webhook
	 * orderNumber is usually the display number. We prefer display-number match, then fall back to order ID.
	 *
	 * @param string $order_number Order number from webhook (display number or, rarely, order ID).
	 * @return WC_Order|false
	 */
	private function find_order_by_number( $order_number, $payment_id = '' ) {
		if ( empty( $order_number ) ) {
			return false;
		}
		$order_number_str = strval( $order_number );
		$hpos_active_bool = ( class_exists( \Automattic\WooCommerce\Utilities\OrderUtil::class ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() );
		$hpos_active      = $hpos_active_bool ? 'yes' : 'no';
		peachpay_log(
			'info',
			'ConvesioPay Webhook order lookup - started.',
			array(
				'event'     => 'webhooks.convesiopay.webhook.process.success',
				'component' => 'webhooks',
				'feature'   => 'convesiopay',
				'operation' => 'webhook.process',
				'outcome'   => 'success',
				'method'    => __METHOD__,
				'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook [find_order_by_number]: called with order_number=' . $order_number . ', payment_id=' . ( ! empty( $payment_id ) ? $payment_id : '(none)' ) . ', HPOS=' . $hpos_active
				)
			)
		);
		$order_id_int     = (int) $order_number;
		$is_numeric       = ( (string) $order_id_int === $order_number_str && $order_id_int > 0 );

		// 0) Fast path — direct meta_query lookup by payment ID (O(1) indexed DB query).
		// meta_query is only supported when HPOS is enabled (WooCommerce 8.2+ default for new installs).
		// On legacy post storage it is silently ignored, which would return a wrong order with limit=1.
		if ( ! empty( $payment_id ) && $hpos_active_bool ) {
			$orders = wc_get_orders(
				array(
					'limit'      => 1,
					'type'       => 'shop_order',
					'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
						array(
							'key'     => '_convesiopay_payment_id',
							'value'   => $payment_id,
							'compare' => '=',
						),
					),
				)
			);
			if ( ! empty( $orders ) ) {
				peachpay_log(
					'debug',
					'ConvesioPay Webhook order lookup - payment ID matched order.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.debug',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'debug',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: find_order_by_number(' . $order_number . ') resolved by _convesiopay_payment_id meta => order #' . $orders[0]->get_id()
						)
					)
				);
				return $orders[0];
			}
			peachpay_log(
				'info',
				'ConvesioPay Webhook order lookup - payment ID step no match.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.success',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'success',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook [find_order_by_number]: Step 0 found no match for payment_id=' . $payment_id . '; falling through to Step 1'
					)
				)
			);
		} else {
			$skip_reason = empty( $payment_id ) ? 'no payment_id provided' : 'HPOS disabled';
			peachpay_log(
				'info',
				'ConvesioPay Webhook order lookup - payment ID step skipped.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.success',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'success',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook [find_order_by_number]: Step 0 skipped — ' . $skip_reason . '; falling through to Step 1'
					)
				)
			);
		}

		// 1) Match by display number (what we send to ConvesioPay: order->get_order_number()).
		//    Try plugin order_number param first; only accept if returned order actually matches.
		$orders = wc_get_orders( array(
			'limit'        => 1,
			'type'         => 'shop_order',
			'order_number' => $order_number,
		) );
		if ( ! empty( $orders ) ) {
			$candidate = $orders[0];
			if ( strval( $candidate->get_order_number() ) === $order_number_str ) {
				peachpay_log(
					'debug',
					'ConvesioPay Webhook order lookup - display number matched order.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.debug',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'debug',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: find_order_by_number(' . $order_number . ') resolved by order_number param (display) => order #' . $candidate->get_id()
						)
					)
				);
				return $candidate;
			}
			peachpay_log(
				'debug',
				'ConvesioPay Webhook order lookup - display number mismatch.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.debug',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'debug',
					'method'    => __METHOD__,
					'line'      => __LINE__,
					'data'      => array(
						'details' => 'PeachPay Webhook: find_order_by_number(' . $order_number . ') order_number param returned order #' . $candidate->get_id() . ' (display ' . $candidate->get_order_number() . ') – no match'
					)
				)
			);
		} else {
			peachpay_log(
				'info',
				'ConvesioPay Webhook order lookup - wc_get_order step no match.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.success',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'success',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				)
			);
		}

		// 2) Loop fallback — fetch IDs only to avoid bulk hydration.
		$recent_order_ids = wc_get_orders(
			array(
				'limit'   => 20,
				'type'    => 'shop_order',
				'orderby' => 'date',
				'order'   => 'DESC',
				'return'  => 'ids',
			)
		);
		foreach ( $recent_order_ids as $order_id ) {
			$check_order = wc_get_order( $order_id );
			if ( ! $check_order ) {
				continue;
			}
			if ( strval( $check_order->get_order_number() ) === $order_number_str ) {
				peachpay_log(
				'debug',
				'ConvesioPay Webhook order lookup - recent orders match found.',
				array(
						'event'     => 'webhooks.convesiopay.webhook.process.debug',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'debug',
						'method'    => __METHOD__,
						'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook: find_order_by_number(' . $order_number . ') resolved by get_order_number() => order #' . $check_order->get_id()
				)));
				return $check_order;
			}
		}

		peachpay_log(
			'info',
			'ConvesioPay Webhook order lookup - no match found.',
			array(
				'event'     => 'webhooks.convesiopay.webhook.process.success',
				'component' => 'webhooks',
				'feature'   => 'convesiopay',
				'operation' => 'webhook.process',
				'outcome'   => 'success',
				'method'    => __METHOD__,
				'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook [find_order_by_number]: Step 2 exhausted after checking ' . count( $recent_order_ids ) . ' orders; no match found'
				)
			)
		);

		// 3) Fallback: treat as order ID (e.g. stored-card renewal sends get_id()).
		if ( $is_numeric ) {
			$order_by_id = wc_get_order( $order_id_int );
			if ( $order_by_id && is_a( $order_by_id, 'WC_Order' ) && ! is_a( $order_by_id, 'WC_Order_Refund' ) ) {
				peachpay_log(
					'debug',
					'ConvesioPay Webhook order lookup - order ID matched order.',
					array(
						'event'     => 'webhooks.convesiopay.webhook.process.debug',
						'component' => 'webhooks',
						'feature'   => 'convesiopay',
						'operation' => 'webhook.process',
						'outcome'   => 'debug',
						'method'    => __METHOD__,
						'line'      => __LINE__,
						'data'      => array(
							'details' => 'PeachPay Webhook: find_order_by_number(' . $order_number . ') resolved by order ID => order #' . $order_by_id->get_id()
						)
					)
				);
				return $order_by_id;
			}
		}

		peachpay_log(
			'debug',
			'ConvesioPay Webhook order lookup - no order found.',
			array(
				'event'     => 'webhooks.convesiopay.webhook.process.debug',
				'component' => 'webhooks',
				'feature'   => 'convesiopay',
				'operation' => 'webhook.process',
				'outcome'   => 'debug',
				'method'    => __METHOD__,
				'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook: find_order_by_number(' . $order_number . ') no order found'
				)
			)
		);
		return false;
	}
	
	/**
	 * Process webhook event based on type and status.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $event_type Event type.
	 * @param string   $status Payment status.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Full payment data.
	 * @param string   $payment_method Payment method (card, btcPay, applePay).
	 * @param string   $payment_type Payment type.
	 */
	private function process_webhook_event( $order, $event_type, $status, $payment_id, $payment_data, $payment_method = '', $payment_type = '' ) {
		
		// Normalize event type and status
		$normalized_event = strtolower( $event_type );
		$normalized_status = strtolower( $status );
		
		// Handle specific event types first (manual capture/cancel from Dashboard)
		switch ( $normalized_event ) {
			case 'payment.captured':
			case 'capture.succeeded':
			case 'capture.completed':
				$this->handle_manual_capture_webhook( $order, $payment_id, $payment_data );
				return;
				
			case 'payment.cancelled':
			case 'payment.canceled':
			case 'cancel.succeeded':
			case 'cancel.completed':
				$this->handle_manual_cancel_webhook( $order, $payment_id, $payment_data );
				return;
				
			case 'payment.refunded':
			case 'refund.succeeded':
			case 'refund.completed':
				$this->handle_refund_webhook( $order, $payment_id, $payment_data );
				return;
		}
		
		// Handle different payment statuses (original payment flow)
		switch ( $normalized_status ) {
			case 'succeeded':  // ConvesioPay documentation status
				$this->handle_payment_success( $order, $payment_id, $payment_data, $payment_method );
				break;
			
			case 'authorized': // Authorized payments wait for capture
				$this->handle_payment_authorized( $order, $payment_id, $payment_data, $payment_method );
				break;
			// case 'captured':
			// 	$this->handle_payment_success( $order, $payment_id, $payment_data, $payment_method );
			// 	break;
			case 'cancelled':  // ConvesioPay documentation status
				$this->handle_payment_cancelled( $order, $payment_id, $payment_data );
				break;

			case 'failed':     // ConvesioPay documentation status
			case 'blocked':    // ConvesioPay documentation status
			case 'disputed':   // ConvesioPay documentation status
				$this->handle_payment_failure( $order, $payment_id, $payment_data );
				break;
				
			case 'refunded':   // ConvesioPay documentation status - handle as refund
				$this->handle_refund_webhook( $order, $payment_id, $payment_data );
				break;

			case 'pending':    // ConvesioPay documentation status
				$this->handle_payment_pending( $order, $payment_id, $payment_data );
				break;
				
			default:
				$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $payment_id;
				$order->add_order_note( "ConvesioPay webhook received with unknown status: $status (Payment ID: $payment_id) | <a href=\"$payment_url\" target=\"_blank\">View Payment</a>" );
				break;
		}
	}
	
	/**
	 * Handle successful payment.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Payment data.
	 * @param string   $payment_method Payment method (card, btcPay, applePay).
	 */
	private function handle_payment_success( $order, $payment_id, $payment_data, $payment_method = '' ) {

		// Check if order is already completed or refunded
		if ( $order->has_status( array( 'processing', 'completed', 'refunded' ) ) ) {
			return;
		}
		
		// Store payment status as "captured" to prevent duplicate capture attempts
		$order->update_meta_data( '_convesiopay_payment_status', 'captured' );
		
		// Add payment ID and URL to order notes
		$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $payment_id;
		$note = sprintf(
			'ConvesioPay payment successful. Payment ID: %s | <a href="%s" target="_blank">View Payment</a>',
			$payment_id,
			$payment_url
		);
		
		// Add amount and currency if available
		if ( isset( $payment_data['amount'] ) && isset( $payment_data['currency'] ) ) {
			$amount = $payment_data['amount'];
			$currency = $payment_data['currency'];
			
			// Convert from minor units if needed (cents to dollars)
			if ( is_numeric( $amount ) && $amount >= 50 ) {
				$formatted_amount = number_format( $amount / 100, 2 );
			} else {
				$formatted_amount = $amount;
			}
			
			$note .= sprintf( ' Amount: %s %s', $formatted_amount, $currency );
		}
		
		$order->add_order_note( $note );

		// Check if payment ID has changed and update accordingly
		$existing_payment_id = $order->get_meta( '_convesiopay_payment_id' );
		if ( ! empty( $existing_payment_id ) && $existing_payment_id !== $payment_id ) {
			$order->add_order_note( sprintf( __( 'ConvesioPay Payment ID updated from %s to %s', 'peachpay-for-woocommerce' ), $existing_payment_id, $payment_id ) );
		}

		// Store original payment ID only if not already set (never overwrite - needed for refunds)
		if ( empty( $order->get_meta( '_convesiopay_original_payment_id' ) ) ) {
			$order->update_meta_data( '_convesiopay_original_payment_id', $payment_id );
		}

		// Store payment ID as meta
		$order->update_meta_data( '_convesiopay_payment_id', $payment_id );

		// Store payment method information for future reference
		if ( ! empty( $payment_method ) ) {
			$order->update_meta_data( '_convesiopay_payment_method', $payment_method );
		}

		// Extract and store subscription-related tokens for Card On File renewals.
		// Canonical identity for stored cards is customerId (ConvesioPay), persisted in custom table.
		$customer_id = $payment_data['customerId'] ?? '';
		$payment_method_details = $payment_data['paymentMethodDetails'] ?? array();
		$stored_payment_method_id = $payment_method_details['storedPaymentMethodId'] ?? '';
		$user_id = $order->get_user_id();
		$email   = $payment_data['email'] ?? '';

		if ( ! empty( $customer_id ) ) {
			$order->update_meta_data( '_convesiopay_customer_id', $customer_id );
			if ( $user_id ) {
				update_user_meta( $user_id, '_convesiopay_customer_id', $customer_id );
			}
		}

		if ( ! empty( $stored_payment_method_id ) ) {
			$order->update_meta_data( '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
			if ( $user_id ) {
				update_user_meta( $user_id, '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
				update_user_meta( $user_id, '_convesiopay_payment_method_details', $payment_method_details );
			}
		}

		// Upsert stored card list in custom customer-token table (customerId keyed).
		if ( ! empty( $customer_id ) && ! empty( $stored_payment_method_id ) && ! empty( $payment_method_details ) ) {
			$this->payment_webhook_upsert_customer_card( $customer_id, $email, $payment_method_details );
		}

		$order->set_payment_method_title( $this->get_payment_method_title( $order ) );

		// Update transaction ID
		$order->set_transaction_id( $payment_id );

		// Update order status to processing - payment is confirmed and captured
		$order->update_status( 'processing', 'Payment completed via ConvesioPay webhook.' );
		
		$order->save();

		// Create mapping ConvesioPay customer → WooCommerce subscription: propagate to subscriptions created by this order
		if ( function_exists( 'wcs_get_subscriptions_for_order' ) && ( ! empty( $customer_id ) || ! empty( $stored_payment_method_id ) ) ) {
			$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) );
			foreach ( $subscriptions as $subscription ) {
				if ( ! $subscription || ! is_callable( array( $subscription, 'update_meta_data' ) ) ) {
					continue;
				}
				if ( ! empty( $customer_id ) ) {
					$subscription->update_meta_data( '_convesiopay_customer_id', $customer_id );
				}
				// Selected renewal token is stored on parent order meta only.
				$subscription->save();
			}
		}
		
	}
	
	/**
	 * Handle failed payment.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Payment data.
	 */
	private function handle_payment_failure( $order, $payment_id, $payment_data ) {
		
		// Get error details
		$error_message = $payment_data['errorMessage'] ?? $payment_data['failureReason'] ?? $payment_data['reason'] ?? $payment_data['message'] ?? __( 'Payment failed', 'peachpay-for-woocommerce' );
		$error_code = $payment_data['errorCode'] ?? $payment_data['code'] ?? '';
		
		// Create detailed error note with payment URL
		$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $payment_id;
		$note = sprintf(
			'ConvesioPay payment failed. Payment ID: %s. Error: %s | <a href="%s" target="_blank">View Payment</a>',
			$payment_id,
			$error_message,
			$payment_url
		);
		
		if ( $error_code ) {
			$note .= sprintf( ' (Code: %s)', $error_code );
		}
		
		// Don't fail orders that are already captured/processed
		$payment_status = $order->get_meta( '_convesiopay_payment_status' );
		if ( $payment_status === 'captured' || $order->has_status( array( 'processing', 'completed' ) ) ) {
			$order->add_order_note( $note . ' (Ignored - order already processed)' );
			$order->save();
			return;
		}
		
		$order->add_order_note( $note );
		$order->update_meta_data( '_convesiopay_failure_reason', $error_message );
		$order->update_status( 'failed', 'Payment failed via ConvesioPay webhook: ' . $error_message );
		$order->save();

	}

	/**
	 * Upsert the card used in payment webhook into the customer-token table.
	 *
	 * @param string $customer_id           payload.customerId
	 * @param string $email                 payload.email
	 * @param array  $payment_method_details payload.paymentMethodDetails
	 */
	private function payment_webhook_upsert_customer_card( $customer_id, $email, $payment_method_details ) {
		if ( class_exists( 'PeachPay_ConvesioPay_Customer_Token_Store' ) ) {
			PeachPay_ConvesioPay_Customer_Token_Store::upsert_from_payment_webhook( $customer_id, $email, $payment_method_details );
		}
	}

	/**
	 * Handle cancelled payment.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Payment data.
	 */
	private function handle_payment_cancelled( $order, $payment_id, $payment_data ) {

		// Only update if order is not already cancelled
		if ( $order->get_status() === 'cancelled' ) {
			// Order is already cancelled by merchant, just add a note
			$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $payment_id;
			$note = sprintf(
				'ConvesioPay payment cancelled. Payment ID: %s | <a href="%s" target="_blank">View Payment</a>',
				$payment_id,
				$payment_url
			);
			$order->add_order_note( $note );

			// Check if payment ID has changed and update accordingly
			$existing_payment_id = $order->get_meta( '_convesiopay_payment_id' );
			if ( ! empty( $existing_payment_id ) && $existing_payment_id !== $payment_id ) {
				$order->add_order_note( sprintf( __( 'ConvesioPay Payment ID updated from %s to %s', 'peachpay-for-woocommerce' ), $existing_payment_id, $payment_id ) );
			}

			// Store payment ID and update transaction ID
			$order->update_meta_data( '_convesiopay_payment_id', $payment_id );
			$order->set_transaction_id( $payment_id );
			$order->save();

			return;
		}

		// If order is in a different status, update it to cancelled
		$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $payment_id;
		$note = sprintf(
			'ConvesioPay payment cancelled. Payment ID: %s | <a href="%s" target="_blank">View Payment</a>',
			$payment_id,
			$payment_url
		);

		$order->add_order_note( $note );

		// Check if payment ID has changed and update accordingly
		$existing_payment_id = $order->get_meta( '_convesiopay_payment_id' );
		if ( ! empty( $existing_payment_id ) && $existing_payment_id !== $payment_id ) {
			$order->add_order_note( sprintf( __( 'ConvesioPay Payment ID updated from %s to %s', 'peachpay-for-woocommerce' ), $existing_payment_id, $payment_id ) );
		}

		// Store payment ID and update transaction ID
		$order->update_meta_data( '_convesiopay_payment_id', $payment_id );
		$order->set_transaction_id( $payment_id );

		// Update status to Cancelled
		$order->update_status( 'cancelled', 'Payment cancelled via ConvesioPay webhook.' );

		$order->save();

	}

	/**
	 * Handle pending payment.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Payment data.
	 */
	private function handle_payment_pending( $order, $payment_id, $payment_data ) {
		
		$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $payment_id;
		$note = sprintf(
			'ConvesioPay payment pending. Payment ID: %s | <a href="%s" target="_blank">View Payment</a>',
			$payment_id,
			$payment_url
		);
		
		$order->add_order_note( $note );

		// Check if payment ID has changed and update accordingly
		$existing_payment_id = $order->get_meta( '_convesiopay_payment_id' );
		if ( ! empty( $existing_payment_id ) && $existing_payment_id !== $payment_id ) {
			$order->add_order_note( sprintf( __( 'ConvesioPay Payment ID updated from %s to %s', 'peachpay-for-woocommerce' ), $existing_payment_id, $payment_id ) );
		}

		// Store original payment ID only if not already set (never overwrite - needed for refunds)
		if ( empty( $order->get_meta( '_convesiopay_original_payment_id' ) ) ) {
			$order->update_meta_data( '_convesiopay_original_payment_id', $payment_id );
		}

		$order->update_meta_data( '_convesiopay_payment_id', $payment_id );
		$order->set_transaction_id( $payment_id );
		$order->save();
		
	}
	
	/**
	 * Handle authorized payment (for manual capture).
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Payment data.
	 * @param string   $payment_method Payment method (card, btcPay, applePay).
	 */
	private function handle_payment_authorized( $order, $payment_id, $payment_data, $payment_method = '' ) {
		
		// Check if order is already processed
		if ( $order->has_status( array( 'processing', 'completed' ) ) ) {
			return;
		}
		
		// Store payment status as "authorized" to track that this payment needs manual capture
		$order->update_meta_data( '_convesiopay_payment_status', 'authorized' );
		
		$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $payment_id;
		$note = sprintf(
			'ConvesioPay payment authorized and ready for manual capture. Payment ID: %s | <a href="%s" target="_blank">View Payment</a>',
			$payment_id,
			$payment_url
		);
		
		// Add amount and currency if available
		if ( isset( $payment_data['amount'] ) && isset( $payment_data['currency'] ) ) {
			$amount = $payment_data['amount'];
			$currency = $payment_data['currency'];
			
			// Convert from minor units if needed (cents to dollars)
			if ( is_numeric( $amount ) && $amount > 50 ) {
				$formatted_amount = number_format( $amount / 100, 2 );
			} else {
				$formatted_amount = $amount;
			}
			
			$note .= sprintf( ' Amount: %s %s', $formatted_amount, $currency );
		}
		
		$order->add_order_note( $note );

		// Check if payment ID has changed and update accordingly
		$existing_payment_id = $order->get_meta( '_convesiopay_payment_id' );
		if ( ! empty( $existing_payment_id ) && $existing_payment_id !== $payment_id ) {
			$order->add_order_note( sprintf( __( 'ConvesioPay Payment ID updated from %s to %s', 'peachpay-for-woocommerce' ), $existing_payment_id, $payment_id ) );
		}

		// Store original payment ID only if not already set (never overwrite - needed for refunds)
		if ( empty( $order->get_meta( '_convesiopay_original_payment_id' ) ) ) {
			$order->update_meta_data( '_convesiopay_original_payment_id', $payment_id );
		}

		$order->update_meta_data( '_convesiopay_payment_id', $payment_id );

		// Store payment method information for future reference
		if ( ! empty( $payment_method ) ) {
			$order->update_meta_data( '_convesiopay_payment_method', $payment_method );
		}

		// Extract and store subscription-related tokens for Card On File renewals.
		$customer_id = $payment_data['customerId'] ?? '';
		$payment_method_details = $payment_data['paymentMethodDetails'] ?? array();
		$stored_payment_method_id = isset( $payment_method_details['storedPaymentMethodId'] ) ? $payment_method_details['storedPaymentMethodId'] : '';
		$user_id = $order->get_user_id();
		$email   = $payment_data['email'] ?? '';

		if ( ! empty( $customer_id ) ) {
			$order->update_meta_data( '_convesiopay_customer_id', $customer_id );
			if ( $user_id ) {
				update_user_meta( $user_id, '_convesiopay_customer_id', $customer_id );
			}
		}

		if ( ! empty( $stored_payment_method_id ) ) {
			$order->update_meta_data( '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
			if ( $user_id ) {
				update_user_meta( $user_id, '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
				update_user_meta( $user_id, '_convesiopay_payment_method_details', $payment_method_details );
			}
		}

		if ( ! empty( $customer_id ) && ! empty( $stored_payment_method_id ) && ! empty( $payment_method_details ) ) {
			$this->payment_webhook_upsert_customer_card( $customer_id, $email, $payment_method_details );
		}

		$title = $this->get_payment_method_title( $order );
		$title = preg_replace( '/\s*\(Authorized\)/', '', $title );
		$order->set_payment_method_title( $title . ' (Authorized)' );
		$order->set_transaction_id( $payment_id );
		
		// Keep order in pending status for manual capture
		if ( ! $order->has_status( 'pending' ) ) {
			$order->update_status( 'pending', 'Payment authorized via ConvesioPay. Ready for manual capture.' );
		}
		
		$order->save();
		
	}
	
	/**
	 * Handle manual capture webhook from ConvesioPay Dashboard.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Payment data.
	 */
	private function handle_manual_capture_webhook( $order, $payment_id, $payment_data ) {
		
		// Check if order is already captured/completed
		if ( $order->has_status( array( 'processing', 'completed' ) ) ) {
			return;
		}
		
		// Update payment status to captured to prevent duplicate capture attempts
		$order->update_meta_data( '_convesiopay_payment_status', 'captured' );
		
		// Get capture details
		$capture_id = $payment_data['id'] ?? $payment_id;
		$original_payment_id = $payment_data['originalPaymentId'] ?? $payment_id;
		
		$note = sprintf(
			'ConvesioPay payment captured from Dashboard. Capture ID: %s, Original Payment ID: %s',
			$capture_id,
			$original_payment_id
		);
		
		// Add amount and currency if available
		if ( isset( $payment_data['amount'] ) && isset( $payment_data['currency'] ) ) {
			$amount = $payment_data['amount'];
			$currency = $payment_data['currency'];
			
			// Convert from minor units if needed (cents to dollars)
			if ( is_numeric( $amount ) && $amount > 100 ) {
				$formatted_amount = number_format( $amount / 100, 2 );
			} else {
				$formatted_amount = $amount;
			}
			
			$note .= sprintf( ' Amount: %s %s', $formatted_amount, $currency );
		}
		
		$order->add_order_note( $note );

		// Check if transaction ID has changed (from payment ID to capture ID)
		$existing_transaction_id = $order->get_transaction_id();
		if ( ! empty( $existing_transaction_id ) && $existing_transaction_id !== $capture_id ) {
			$order->add_order_note( sprintf( __( 'ConvesioPay Transaction ID updated from %s to %s (capture)', 'peachpay-for-woocommerce' ), $existing_transaction_id, $capture_id ) );
		}

		$order->update_meta_data( '_convesiopay_capture_id', $capture_id );

		$order->set_payment_method_title( $this->get_payment_method_title( $order ) );
		$order->set_transaction_id( $capture_id );
		
		// Update status to processing (captured)
		$order->update_status( 'processing', 'Payment captured from ConvesioPay Dashboard.' );
		
		$order->save();
		
	}
	
	/**
	 * Handle manual cancel webhook from ConvesioPay Dashboard.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID.
	 * @param array    $payment_data Payment data.
	 */
	private function handle_manual_cancel_webhook( $order, $payment_id, $payment_data ) {

		// Check if order is already cancelled or completed
		if ( $order->has_status( array( 'cancelled', 'processing', 'completed' ) ) ) {
			return;
		}

		// Get cancel details
		$cancel_id = $payment_data['id'] ?? $payment_id;
		$original_payment_id = $payment_data['originalPaymentId'] ?? $payment_id;
		$cancel_status = $payment_data['status'] ?? 'cancelled';

		$note = sprintf(
			'ConvesioPay payment cancelled from Dashboard. Cancel ID: %s, Original Payment ID: %s, Status: %s',
			$cancel_id,
			$original_payment_id,
			$cancel_status
		);

		$order->add_order_note( $note );

		// Check if payment ID has changed and update accordingly
		$existing_payment_id = $order->get_meta( '_convesiopay_payment_id' );
		if ( ! empty( $existing_payment_id ) && $existing_payment_id !== $cancel_id ) {
			$order->add_order_note( sprintf( __( 'ConvesioPay Payment ID updated from %s to %s (cancelled)', 'peachpay-for-woocommerce' ), $existing_payment_id, $cancel_id ) );
		}

		$order->update_meta_data( '_convesiopay_cancel_id', $cancel_id );
		$order->update_meta_data( '_convesiopay_payment_id', $cancel_id );
		$order->set_transaction_id( $cancel_id );

		// Update status to cancelled
		$order->update_status( 'cancelled', 'Payment cancelled from ConvesioPay Dashboard.' );

		$order->save();

	}
	
	/**
	 * Handle refund webhook from ConvesioPay Dashboard.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $payment_id Payment ID from webhook.
	 * @param array    $payment_data Full webhook payload.
	 */
	private function handle_refund_webhook( $order, $payment_id, $payment_data ) {

		// Extract refund details from ConvesioPay webhook format
		$refund_payment_id = $payment_id; // This is the refund transaction ID
		$currency = $payment_data['currency'] ?? $order->get_currency();

		// Use refund-specific amount first (ConvesioPay sends refundAmount in cents); top-level 'amount' is the original payment amount
		$refund_amount_cents = $payment_data['refundAmount'] ?? $payment_data['refundedAmount'] ?? $payment_data['refund_amount'] ?? $payment_data['refunded_amount'] ?? $payment_data['body']['refundAmount'] ?? $payment_data['body']['refundedAmount'] ?? null;
		if ( $refund_amount_cents === null || $refund_amount_cents === '' ) {
			$refund_amount_cents = $payment_data['amount'] ?? 0;
		}

		// Convert from cents to dollars (ConvesioPay uses cents: 1000 = $10.00)
		$refund_amount = is_numeric( $refund_amount_cents ) ? ( (float) $refund_amount_cents / 100 ) : 0;

		// Cap at remaining refundable amount so we never create a refund larger than the order (handles wrong/missing refund amount in payload)
		$order_total       = (float) $order->get_total();
		$total_refunded    = (float) $order->get_total_refunded();
		$remaining         = max( 0, $order_total - $total_refunded );
		$refund_amount     = min( $refund_amount, $remaining );

		// Check if this refund was already processed from WooCommerce admin
		// If a WooCommerce refund with this ConvesioPay refund ID already exists,
		// it means the refund was created from WooCommerce (not from ConvesioPay Dashboard)
		$refunds = $order->get_refunds();
		$woocommerce_initiated_refund = false;

		foreach ( $refunds as $existing_refund ) {
			$stored_refund_id = $existing_refund->get_meta( '_convesiopay_refund_id' );
			if ( $stored_refund_id === $refund_payment_id ) {
				// This webhook is a confirmation of a WooCommerce-initiated refund
				$woocommerce_initiated_refund = true;
				break;
			}

			// Also check if refund amount matches (for refunds created before we started tracking IDs)
			$existing_refund_amount = abs( $existing_refund->get_amount() );
			if ( abs( $existing_refund_amount - $refund_amount ) < 0.01 ) {
				// Same amount refund exists, likely from WooCommerce admin
				// Tag it with the ConvesioPay refund ID
				$existing_refund->update_meta_data( '_convesiopay_refund_id', $refund_payment_id );
				$existing_refund->save();
				$woocommerce_initiated_refund = true;
				break;
			}
		}

		// If this was a WooCommerce-initiated refund, just update transaction ID and payment ID, then exit
		if ( $woocommerce_initiated_refund ) {
			// Update transaction ID and payment ID metadata
			$existing_transaction_id = $order->get_transaction_id();
			if ( ! empty( $existing_transaction_id ) && $existing_transaction_id !== $refund_payment_id ) {
				$order->add_order_note( sprintf( __( 'ConvesioPay Transaction ID updated from %s to %s (refunded)', 'peachpay-for-woocommerce' ), $existing_transaction_id, $refund_payment_id ) );
			}

			$order->update_meta_data( '_convesiopay_payment_id', $refund_payment_id );
			$order->update_meta_data( '_convesiopay_refund_id', $refund_payment_id );
			$order->set_transaction_id( $refund_payment_id );
			$order->save();

			// Skip further processing to avoid duplicate notes and status updates
			return;
		}

		// Store original payment ID from order before updating
		$original_payment_id = $order->get_meta( '_convesiopay_payment_id' );

		// Create payment URL for ConvesioPay Dashboard
		$payment_url = peachpay_convesiopay_dashboard_url() . '/payments/' . $refund_payment_id;

		// Create detailed note with refund information and payment URL
		$note = sprintf(
			'ConvesioPay refund processed from Dashboard. Refund Payment ID: %s, Amount: %s %s | <a href="%s" target="_blank">View Payment</a>',
			$refund_payment_id,
			number_format( $refund_amount, 2 ),
			$currency,
			$payment_url
		);

		if ( ! empty( $original_payment_id ) && $original_payment_id !== $refund_payment_id ) {
			$note .= sprintf( ' (Original Payment ID: %s)', $original_payment_id );
		}

		$order->add_order_note( $note );

		// Check if transaction ID has changed (from payment ID to refund ID)
		$existing_transaction_id = $order->get_transaction_id();
		if ( ! empty( $existing_transaction_id ) && $existing_transaction_id !== $refund_payment_id ) {
			$order->add_order_note( sprintf( __( 'ConvesioPay Transaction ID updated from %s to %s (refunded)', 'peachpay-for-woocommerce' ), $existing_transaction_id, $refund_payment_id ) );
		}

		// Update payment ID to refund payment ID
		$order->update_meta_data( '_convesiopay_payment_id', $refund_payment_id );
		$order->update_meta_data( '_convesiopay_refund_id', $refund_payment_id );
		$order->update_meta_data( '_convesiopay_last_refund_amount', $refund_amount );
		$order->set_transaction_id( $refund_payment_id );

		// Save the order to persist payment ID and transaction ID updates
		$order->save();

		// Create WooCommerce refund only if we have a positive amount (webhook only gets here if refund doesn't exist from WooCommerce admin)
		if ( $refund_amount > 0 ) {
			$wc_refund = wc_create_refund( array(
				'order_id' => $order->get_id(),
				'amount'   => $refund_amount,
				'reason'   => sprintf( 'ConvesioPay Dashboard refund - ID: %s', $refund_payment_id ),
			) );

			if ( $wc_refund && ! is_wp_error( $wc_refund ) ) {
				// Store ConvesioPay refund ID in the WooCommerce refund object
				$wc_refund->update_meta_data( '_convesiopay_refund_id', $refund_payment_id );
				$wc_refund->save();
			} else {
				$error_message = is_wp_error( $wc_refund ) ? $wc_refund->get_error_message() : 'Unknown error';
				$order->add_order_note( 'Warning: ConvesioPay refund received but WooCommerce refund creation failed: ' . $error_message );
			}
		}

		// Refresh order data to get updated refund totals
		$order = wc_get_order( $order->get_id() );

		if ( ! $order ) {
			return;
		}

		// Check if this is a full or partial refund
		$total_refunded = $order->get_total_refunded();
		$order_total = $order->get_total();

		// Update order status based on refund completeness
		if ( $total_refunded >= $order_total ) {
			// Full refund - update order status to refunded
			$order->update_status( 'refunded', 'Order fully refunded via ConvesioPay Dashboard.' );
		} else {
			// Partial refund - add informative note
			$refund_percentage = ( $total_refunded / $order_total ) * 100;
			$order->add_order_note( sprintf(
				'Partial refund: %s %s of %s %s total (%.1f%% refunded)',
				number_format( $total_refunded, 2 ),
				$currency,
				number_format( $order_total, 2 ),
				$currency,
				$refund_percentage
			) );
		}

		$order->save();

	}
	
	/**
	 * Validate webhook signature (ConvesioPay documentation requirement).
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return bool
	 */
	private function validate_webhook_signature( $request ) {
		// Get signature headers following ConvesioPay documentation
		$headers = $request->get_headers();
		
		// ConvesioPay sends headers in lowercase format
		$convesiopay_timestamp = $headers['x_convesiopay_timestamp'][0] ?? $headers['x-convesiopay-timestamp'][0] ?? '';
		$convesiopay_signature = $headers['x_convesiopay_hmacsha256_signature'][0] ?? $headers['x-convesiopay-hmacsha256-signature'][0] ?? '';
		
		// SECURITY: Always require signature headers - no bypass allowed
		if ( empty( $convesiopay_timestamp ) || empty( $convesiopay_signature ) ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: Missing required signature headers - rejecting webhook' );
			peachpay_log(
				'error',
				'ConvesioPay Webhook signature headers - missing.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				)
			);
			return false;
		}
		
		// Get webhook signature key from configuration
		$config = ( class_exists( 'PeachPay_ConvesioPay_Integration' ) && method_exists( 'PeachPay_ConvesioPay_Integration', 'config' ) ) ? PeachPay_ConvesioPay_Integration::config() : array();
		// Check both possible locations for the signature key
		$webhook_signature_key = $config['webhook_data']['signature_key'] ?? $config['webhook_signature'] ?? '';
		
		// SECURITY: Require webhook signature to be configured - never allow unauthenticated webhooks
		if ( empty( $webhook_signature_key ) ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: No webhook signature key configured - rejecting webhook for security' );
			peachpay_log(
				'error',
				'ConvesioPay Webhook signature key - missing.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				)
			);
			return false;
		}
		
		// Generate HMAC signature exactly as per ConvesioPay documentation
		// Use timestamp from header, not request body, as specified in docs
		$expected_signature = base64_encode( hash_hmac( 'sha256', $convesiopay_timestamp, $webhook_signature_key, true ) );
		
		// Use constant-time comparison to prevent timing attacks as recommended
		$is_valid = hash_equals( $convesiopay_signature, $expected_signature );
		
		if ( ! $is_valid ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: Invalid signature - rejecting webhook' );
			peachpay_log(
				'error',
				'ConvesioPay Webhook signature validation - mismatch.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				)
			);
		}
		
		return $is_valid;
	}
	
	
	/**
	 * Check for duplicate events (ConvesioPay documentation requirement).
	 *
	 * @param array $webhook_data Webhook data.
	 * @return bool
	 */
	private function is_duplicate_event( $webhook_data ) {
		$payment_id = $webhook_data['paymentId'] ?? '';
		$event_type = $webhook_data['eventType'] ?? '';
		
		if ( empty( $payment_id ) || empty( $event_type ) ) {
			error_log( '[PP-WEBHOOK] PeachPay Webhook: Missing payment ID or event type inside is_duplicate_event() Line : ' . __LINE__ );
			peachpay_log(
				'error',
				'ConvesioPay Webhook duplicate event check - missing identifiers.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.failed',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'failed',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook: Missing payment ID or event type inside is_duplicate_event() Line : ' . __LINE__
				)));
			return false;
		}
		
		// Create unique key for this event
		$event_key = $payment_id . '_' . $event_type;
		
		// Check if we've seen this event before (using WordPress transients)
		$cache_key = 'convesiopay_webhook_' . md5( $event_key );
		$existing_event = function_exists( 'get_transient' ) ? get_transient( $cache_key ) : false;
		
		if ( $existing_event ) {
			peachpay_log(
				'debug',
				'ConvesioPay Webhook duplicate event check - duplicate found.',
				array(
					'event'     => 'webhooks.convesiopay.webhook.process.debug',
					'component' => 'webhooks',
					'feature'   => 'convesiopay',
					'operation' => 'webhook.process',
					'outcome'   => 'debug',
					'method'    => __METHOD__,
					'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook: Duplicate event found inside is_duplicate_event() Line : ' . __LINE__
				)));
			return true;
		}
		
		// Store this event for 24 hours to prevent duplicates
		$cache_duration = defined( 'HOUR_IN_SECONDS' ) ? 24 * HOUR_IN_SECONDS : 86400; // 24 hours
		if ( function_exists( 'set_transient' ) ) {
			set_transient( $cache_key, true, $cache_duration );
		}
		
		peachpay_log(
				'debug',
				'ConvesioPay Webhook duplicate event check - no duplicate found.',
				array(
				'event'     => 'webhooks.convesiopay.webhook.process.debug',
				'component' => 'webhooks',
				'feature'   => 'convesiopay',
				'operation' => 'webhook.process',
				'outcome'   => 'debug',
				'method'    => __METHOD__,
				'line'      => __LINE__,
				'data'      => array(
					'details' => 'PeachPay Webhook: No duplicate event found inside is_duplicate_event() Line : ' . __LINE__
				)));
		return false;
	}
	
	/**
	 * Resolves the payment method title from the WC-registered gateway instance.
	 *
	 * When the order still carries `peachpay_convesiopay_unified` (e.g. webhook
	 * arrived before the unified gateway saved the sub-gateway ID), the method
	 * uses order meta to resolve the correct sub-gateway before looking up the
	 * title from the WC gateway registry.
	 *
	 * @param WC_Order $order Order object.
	 * @return string Payment method title.
	 */
	private function get_payment_method_title( $order ) {
		$gateway_id = $order->get_payment_method();

		if ( empty( $gateway_id ) || 'peachpay_convesiopay_unified' === $gateway_id ) {
			$resolved = $this->resolve_sub_gateway_id( $order );
			if ( ! empty( $resolved ) ) {
				$gateway_id = $resolved;
			}
		}

		$wc = function_exists( 'WC' ) ? WC() : null;
		if ( empty( $gateway_id ) || ! $wc || ! $wc->payment_gateways() ) {
			return $order->get_payment_method_title() ?: 'ConvesioPay';
		}

		$gateways = $wc->payment_gateways()->payment_gateways();

		if ( ! isset( $gateways[ $gateway_id ] ) ) {
			return $order->get_payment_method_title() ?: 'ConvesioPay';
		}

		$gateway = $gateways[ $gateway_id ];
		return $gateway ? $gateway->get_title() : ( $order->get_payment_method_title() ?: 'ConvesioPay' );
	}

	/**
	 * Resolve the actual sub-gateway ID from order meta.
	 *
	 * Uses `_convesiopay_payment_type` or `_convesiopay_payment_method` to map
	 * values like 'card', 'visa', 'applepay', 'btcpay' to the registered
	 * WooCommerce gateway ID (e.g. `peachpay_convesiopay_card`).
	 *
	 * @param WC_Order $order Order object.
	 * @return string Sub-gateway ID or empty string.
	 */
	private function resolve_sub_gateway_id( $order ) {
		$payment_type = $order->get_meta( '_convesiopay_payment_type' );
		if ( empty( $payment_type ) ) {
			$payment_type = $order->get_meta( '_convesiopay_payment_method' );
		}
		if ( empty( $payment_type ) || ! is_string( $payment_type ) ) {
			return '';
		}

		$normalized = strtolower( trim( $payment_type ) );

		if ( in_array( $normalized, array( 'applepay', 'apple_pay', 'apple-pay', 'apple pay' ), true ) ) {
			return 'peachpay_convesiopay_applepay';
		}
		if ( in_array( $normalized, array( 'btcpay', 'btc_pay', 'btc-pay', 'crypto' ), true ) ) {
			return 'peachpay_convesiopay_btcpay';
		}
		if ( in_array( $normalized, array( 'googlepay', 'google_pay', 'google-pay', 'gpay' ), true ) ) {
			return 'peachpay_convesiopay_googlepay';
		}
		$card_values = array( 'card', 'visa', 'mastercard', 'amex', 'discover', 'mc', 'diners', 'jcb', 'unionpay', 'cup', 'elo', 'hiper', 'hipercard', 'cartesbancaires' );
		if ( in_array( $normalized, $card_values, true ) ) {
			return 'peachpay_convesiopay_card';
		}

		if ( strpos( $normalized, 'peachpay_convesiopay_' ) === 0 ) {
			return $payment_type;
		}

		return '';
	}
} 
