<?php
/**
 * Abstract PeachPay Square WC gateway.
 *
 * @PHPCS:disable Squiz.Commenting.VariableComment.Missing
 *
 * @package PeachPay
 */

defined( 'PEACHPAY_ABSPATH' ) || exit;
require_once PEACHPAY_ABSPATH . 'core/abstract/class-peachpay-payment-gateway.php';

/**
 * Base class for all PeachPay Square payment gateways.
 * Handles payment processing, refunds, and subscription renewals
 * via direct Square SDK calls (no proxy).
 */
abstract class PeachPay_Square_Payment_Gateway extends PeachPay_Payment_Gateway {

	public $payment_provider            = 'Square';
	public $max_amount                  = 50000;
	protected $requires_verification_id = false;
	protected $requires_transaction_id  = false;

	/**
	 * Sets up the Square gateway with method titles, currency filtering, and subscription support.
	 */
	public function __construct() {
		if ( ! $this->method_title ) {
			// translators: %s: gateway title
			$this->method_title = sprintf( __( '%s via Square (PeachPay)', 'peachpay-for-woocommerce' ), $this->title );
		}
		if ( ! $this->method_description ) {
			// translators: %s: gateway title
			$this->method_description = sprintf( __( 'Accept %s payments through Square', 'peachpay-for-woocommerce' ), $this->title );
		}

		$account_currency = PeachPay_Square_Credentials::get_account_currency();
		if ( $account_currency ) {
			if ( is_array( $this->currencies ) ) {
				$this->currencies = array_intersect( array( $account_currency ), $this->currencies );
			} else {
				$this->currencies = array( $account_currency );
			}
		}

		$this->supports = array(
			'products',
			'refunds',
		);

		parent::__construct();

		// Subscription support.
		$gateway = $this;
		add_action(
			'woocommerce_scheduled_subscription_payment_' . $this->id,
			function ( $renewal_total, $renewal_order ) use ( $gateway ) {
				$subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order );
				$subscription  = array_pop( $subscriptions );
				if ( null === $subscription ) {
					peachpay_log( 'error', '[Square] Scheduled renewal: no subscription found for renewal order #' . $renewal_order->get_id() );
					return;
				}
				$parent_order  = wc_get_order( $subscription->get_parent_id() );

				$gateway->process_subscription_renewal( $parent_order, $renewal_order, $renewal_total );
			},
			10,
			2
		);
	}

	/**
	 * Validates a PeachPay Square order.
	 */
	public function validate_fields() {
		$result = parent::validate_fields();

		// PHPCS:disable WordPress.Security.NonceVerification.Missing
		$token_id        = isset( $_POST[ "wc-$this->id-payment-token" ] ) ? sanitize_text_field( wp_unslash( $_POST[ "wc-$this->id-payment-token" ] ) ) : null;
		$source_id       = isset( $_POST['peachpay_square_source_id'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_square_source_id'] ) ) : null;
		$verification_id = isset( $_POST['peachpay_square_verification_id'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_square_verification_id'] ) ) : null;
		// PHPCS:enable

		if ( $this->supports( 'tokenization' ) && null !== $token_id && get_current_user_id() !== 0 ) {
			if ( 'new' !== $token_id ) {
				$token = WC_Payment_Tokens::get( $token_id );

				if ( null === $token ) {
					// translators: %s the name of the field.
					$message = sprintf( __( 'Invalid field "%s". Token can\'t be found', 'peachpay-for-woocommerce' ), "wc-$this->id-payment-token" );
					peachpay_log( 'error', '[Square] validate_fields: ' . $message );
					wc_add_notice( $message, 'error' );
					return false;
				}

				if ( $token->get_user_id() !== get_current_user_id() ) {
					// translators: %s the name of the field.
					$message = sprintf( __( 'Invalid field "%s". Token does not belong to the logged in user.', 'peachpay-for-woocommerce' ), "wc-$this->id-payment-token" );
					peachpay_log( 'error', '[Square] validate_fields: ' . $message );
					wc_add_notice( $message, 'error' );
					return false;
				}

				return $result;
			}
		}

		// Skip source_id validation for change-PM requests — no new nonce is required when
		// WC Subscriptions is updating the payment method on an existing subscription.
		if ( $this->is_change_payment_method_request() ) {
			return $result;
		}

		if ( ! $source_id ) {
			peachpay_log( 'error', '[Square] validate_fields: Missing required field "peachpay_square_source_id"' );
			wc_add_notice( __( 'Missing required field "peachpay_square_source_id"', 'peachpay-for-woocommerce' ), 'error' );
			$result = false;
		}

		if ( ! $this->requires_verification_id ) {
			return $result;
		}

		if ( ! $verification_id ) {
			peachpay_log( 'error', '[Square] validate_fields: Missing required field "peachpay_square_verification_id"' );
			wc_add_notice( __( 'Missing required field "peachpay_square_verification_id"', 'peachpay-for-woocommerce' ), 'error' );
			$result = false;
		}

		return $result;
	}

	/**
	 * Process the PeachPay Square Payment via direct Square SDK call.
	 *
	 * @param int $order_id The id of the order.
	 */
	public function process_payment( $order_id ) {
		try {
			// PHPCS:disable WordPress.Security.NonceVerification.Missing
			$source_id          = isset( $_POST['peachpay_square_source_id'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_square_source_id'] ) ) : null;
			$verification_token = isset( $_POST['peachpay_square_verification_id'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_square_verification_id'] ) ) : null;
			$token_id           = isset( $_POST[ "wc-$this->id-payment-token" ] ) ? sanitize_text_field( wp_unslash( $_POST[ "wc-$this->id-payment-token" ] ) ) : null;
			$save_to_account    = isset( $_POST[ "wc-$this->id-new-payment-method" ] ) ? sanitize_text_field( wp_unslash( $_POST[ "wc-$this->id-new-payment-method" ] ) ) : null;
			// PHPCS:enable

			// Change-PM: delegate before loading the order as a WC_Order.
			$maybe_subscription = wc_get_order( $order_id );
			if ( $this->is_change_payment_method_request() && function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $maybe_subscription ) ) {
				return $this->process_change_payment_method_request( $maybe_subscription, $source_id, $verification_token );
			}

			$square_mode = PeachPay_Square_Integration::mode();
			$order       = parent::process_payment( $order_id );

			$customer_id = PeachPay_Square::get_customer( get_current_user_id() );

			PeachPay_Square_Order_Data::set_peachpay_details(
				$order,
				array(
					'peachpay_mode'          => peachpay_is_test_mode() ? 'test' : 'live',
					'square_mode'            => $square_mode,
					'service_fee_percentage' => PeachPay::service_fee_enabled() ? PeachPay::service_fee_percentage() : 0,
				)
			);

			// Resolve saved token -> source_id.
			if ( $this->supports( 'tokenization' ) && get_current_user_id() !== 0 ) {
				if ( 'new' !== $token_id && null !== $token_id ) {
					$token = WC_Payment_Tokens::get( $token_id );
					if ( null !== $token ) {
						$source_id = $token->get_token();
						PeachPay_Square_Order_Data::set_reusable_payment_details( $order, $source_id, $customer_id );
						WC_Payment_Tokens::set_users_default( get_current_user_id(), $token->get_id() );
					}
				}
			}

			// Subscription checkout: ensure a Square customer exists and handle zero-total.
			if ( $this->is_subscription_checkout_order( $order ) ) {
				$customer_id = $this->maybe_create_square_customer_for_order( $order, $square_mode );

				if ( 0.0 === floatval( $order->get_total() ) ) {
					if ( null === $customer_id ) {
						peachpay_log( 'error', '[Square] process_payment: Failed to create Square customer for zero-total order #' . $order_id );
						wc_add_notice( __( 'Unable to save payment method. Please try again.', 'peachpay-for-woocommerce' ), 'error' );
						return array(
							'result'   => 'failure',
							'redirect' => $this->get_return_url( $order ),
						);
					}
					return $this->process_zero_total_payment( $order, $source_id, $verification_token, $customer_id, $square_mode );
				}

				// Paid subscription: store card on file using the nonce BEFORE charging.
				// Square nonces are single-use — if we charge first the nonce is consumed and
				// Cards API will reject it. Storing first gives us a reusable card_id for renewals.
				if ( $customer_id && $source_id ) {
					$card = PeachPay_Square::store_card_on_file( $source_id, $verification_token, $customer_id, $square_mode, $order );
					if ( null !== $card ) {
						// Use card_id for the payment so the nonce is not double-consumed.
						$source_id          = $card['id'];
						$verification_token = null;
					} else {
						peachpay_log( 'error', '[Square] process_payment: Card storage failed for subscription order #' . $order_id . '; aborting to prevent nonce consumption.' );
						wc_add_notice( __( 'Unable to save payment method for subscription. Please try again.', 'peachpay-for-woocommerce' ), 'error' );
						return array(
							'result'   => 'failure',
							'redirect' => $this->get_return_url( $order ),
						);
					}
				}
			}

			// Create payment directly via Square SDK.
			$result = PeachPay_Square::create_payment( $order, $source_id, $verification_token, $square_mode, $customer_id );

			// Handle null (config error) or error response.
			if ( null === $result ) {
				peachpay_log( 'error', '[Square] process_payment: Square is not configured (order #' . $order_id . ')' );
				return array(
					'result'   => 'failure',
					'redirect' => $this->get_return_url( $order ),
					'message'  => __( 'Square is not configured.', 'peachpay-for-woocommerce' ),
				);
			}

			if ( isset( $result['error'] ) && $result['error'] ) {
				$error_msg = isset( $result['message'] ) ? $result['message'] : __( 'Payment failed', 'peachpay-for-woocommerce' );
				peachpay_log( 'error', '[Square] process_payment: ' . $error_msg . ' (order #' . $order_id . ')' );
				if ( ! is_null( $token_id ) && 'new' !== $token_id ) {
					$token = WC_Payment_Tokens::get( $token_id );
					if ( ! is_null( $token ) ) {
						$token->delete();
					}
				}
				return array(
					'result'   => 'failure',
					'redirect' => $this->get_return_url( $order ),
					'message'  => $error_msg,
				);
			}

			// Payment succeeded. Status already set by calculate_payment_state() inside create_payment().
			$payment_id = isset( $result['id'] ) ? $result['id'] : 'unknown';
			peachpay_log( 'info', '[Square] process_payment: Payment successful (order #' . $order_id . ', payment_id: ' . $payment_id . ')' );

			// Subscription: persist card-on-file details stored before payment.
			if ( $this->is_subscription_checkout_order( $order ) && $customer_id ) {
				if ( isset( $card ) && null !== $card ) {
					PeachPay_Square_Order_Data::set_reusable_payment_details( $order, $card['id'], $customer_id, $card['id'] );
				} else {
					// Card was not stored (storage failed before payment) — store the payment source
					// so at least the subscription record has something, but renewals may fail.
					PeachPay_Square_Order_Data::set_reusable_payment_details( $order, $source_id, $customer_id );
				}
			} elseif ( $save_to_account && $this->supports( 'tokenization' ) && get_current_user_id() !== 0 && ( 'new' === $token_id || null === $token_id ) ) {
				// Handle save-to-account for non-subscription tokenizable gateways.
				$card_details = isset( $result['card_details'] ) ? $result['card_details'] : null;
				if ( $card_details && $customer_id ) {
					PeachPay_Square_Order_Data::set_reusable_payment_details(
						$order,
						isset( $card_details['card'] ) && isset( $card_details['card']['id'] ) ? $card_details['card']['id'] : $source_id,
						$customer_id
					);
				}
			}

			if ( $this->is_subscription_checkout_order( $order ) && ! $customer_id ) {
				peachpay_log( 'warning', '[Square] process_payment: Subscription order #' . $order->get_id() . ' completed but Square customer could not be created. Renewals will fail.' );
			}

			$order->set_payment_method_title( $this->get_title() );
			$order->save();

			return array(
				'result'   => 'success',
				'redirect' => $this->get_return_url( $order ),
			);
		} catch ( Exception $exception ) {
			$message = __( 'Error: ', 'peachpay-for-woocommerce' ) . $exception->getMessage();
			peachpay_log( 'error', '[Square] process_payment exception (order #' . $order_id . '): ' . $exception->getMessage() );
			if ( function_exists( 'wc_add_notice' ) ) {
				wc_add_notice( $message, 'error' );
			}

			if ( isset( $order ) && $order instanceof WC_Order ) {
				$order->add_order_note( $message );
			}

			return array(
				'result'   => 'failure',
				'redirect' => isset( $order ) && $order instanceof WC_Order ? $this->get_return_url( $order ) : '',
				'message'  => $message,
			);
		}
	}

	/**
	 * Square gateways require setup if Square is not connected.
	 */
	public function needs_setup() {
		return ! PeachPay_Square_Integration::connected();
	}

	/**
	 * If Square is not connected we should prompt the merchant to connect while viewing any square gateway.
	 */
	protected function action_needed_form() {
		if ( ! PeachPay_Square_Integration::connected() ) {
			?>
			<div class="settings-container action-needed">
				<h1><?php esc_html_e( 'Action needed', 'peachpay-for-woocommerce' ); ?></h1>
				<hr/>
				<br/>
				<?php require PeachPay::get_plugin_path() . '/core/payments/square/admin/views/html-square-connect.php'; ?>
			</div>
			<?php
		}
	}

	/**
	 * Square gateways require square to be connected in order to use.
	 *
	 * @param bool $skip_cart_check If true, the cart availability check will be skipped.
	 */
	public function is_available( $skip_cart_check = false ) {
		$is_available = parent::is_available( $skip_cart_check );

		if ( ! PeachPay_Square_Integration::connected() ) {
			$is_available = false;
		}

		return $is_available;
	}

	/**
	 * Enqueue native checkout scripts. Enqueueing data here will result in duplication so only enqueue static scripts here.
	 */
	public function enqueue_checkout_scripts() { }

	/**
	 * Enqueue admin scripts.
	 */
	public function enqueue_admin_scripts() { }

	/**
	 * Gets the Square script src URL.
	 */
	public function square_script_src() {
		if ( peachpay_is_test_mode() || peachpay_is_local_development_site() || peachpay_is_staging_site() ) {
			return 'https://sandbox.web.squarecdn.com/v1/square.js';
		} else {
			return 'https://web.squarecdn.com/v1/square.js';
		}
	}

	/**
	 * Process refund via direct Square SDK call.
	 *
	 * @param  int        $order_id Order ID.
	 * @param  float|null $amount Refund amount.
	 * @param  string     $reason Refund reason.
	 * @return boolean True or false based on success, or a WP_Error object.
	 */
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			peachpay_log( 'error', '[Square] process_refund: Order #' . $order_id . ' not found' );
			// translators: %s the order id.
			return new \WP_Error( 'wc_' . $order_id . '_refund_failed', sprintf( __( 'Refund error: The order_id %s did not match any orders.', 'peachpay-for-woocommerce' ), strval( $order_id ) ) );
		}

		try {
			if ( ! is_numeric( $amount ) || floatval( $amount ) <= 0 ) {
				peachpay_log( 'error', '[Square] process_refund: Invalid amount for order #' . $order_id );
				return new \WP_Error( 'wc_' . $order_id . '_refund_failed', __( 'Refund amount must be greater then 0', 'peachpay-for-woocommerce' ) );
			}

			$formatted_amount = PeachPay_Square::format_amount( $amount, $order->get_currency() );
			$result           = PeachPay_Square::refund_payment( $order, $formatted_amount, $reason );

			if ( ! $result['success'] ) {
				peachpay_log( 'error', '[Square] process_refund: ' . $result['message'] . ' (order #' . $order_id . ')' );
				return new \WP_Error( 'wc_' . $order_id . '_refund_failed', __( 'Refund error: ', 'peachpay-for-woocommerce' ) . $result['message'] );
			}

			return true;
		} catch ( Exception $exception ) {
			$message = __( 'Refund error: ', 'peachpay-for-woocommerce' ) . $exception->getMessage();
			peachpay_log( 'error', '[Square] process_refund exception (order #' . $order_id . '): ' . $exception->getMessage() );
			$order->add_order_note( $message );

			return new \WP_Error( 'wc_' . $order_id . '_refund_failed', $message );
		}
	}

	/**
	 * Processes subscription renewal payments via direct Square SDK call.
	 *
	 * @param WC_Order $parent_order The parent order.
	 * @param WC_Order $renewal_order The renewal order to create a payment for.
	 * @param float    $renewal_total The amount to charge the renewal for.
	 */
	public function process_subscription_renewal( $parent_order, $renewal_order, $renewal_total ) {
		if ( ! in_array( 'subscriptions', $this->supports, true ) ) {
			peachpay_log( 'warning', '[Square] process_subscription_renewal: gateway does not support subscriptions | ' . json_encode( array( 'gateway_id' => $this->id, 'renewal_order_id' => $renewal_order->get_id(), 'renewal_order_number' => $renewal_order->get_order_number() ) ) );
			$renewal_order->update_status(
				'failed',
				__( 'This Square payment method does not support subscription renewals.', 'peachpay-for-woocommerce' )
			);
			return;
		}

		try {
			// Idempotency: skip if renewal order is already paid.
			if ( $renewal_order->has_status( array( 'processing', 'completed' ) ) ) {
				peachpay_log( 'debug', '[Square] process_subscription_renewal: Renewal order #' . $renewal_order->get_id() . ' already paid, skipping.' );
				return;
			}

			// Idempotency: skip if renewal order already has a transaction ID.
			if ( ! empty( $renewal_order->get_transaction_id() ) ) {
				peachpay_log( 'debug', '[Square] process_subscription_renewal: Renewal order #' . $renewal_order->get_id() . ' already has transaction ID, skipping.' );
				return;
			}

			if ( ! PeachPay_Square_Order_Data::is_payment_reusable( $parent_order ) ) {
				peachpay_log( 'error', '[Square] process_subscription_renewal: Reusable payment details missing (parent order #' . $parent_order->get_id() . ')' );
				$renewal_order->update_status( 'failed', __( 'Square reusable payment method details missing from subscription order.', 'peachpay-for-woocommerce' ) );
				return;
			}

			$square_mode = PeachPay_Square_Order_Data::get_peachpay( $parent_order, 'square_mode' );
			// prefer card-on-file ID (set for subscriptions created after this change);
			// fall back to the raw source_id for older subscriptions created before it.
			$source_id   = PeachPay_Square_Order_Data::get_card_on_file_id( $parent_order ) ?? PeachPay_Square_Order_Data::get_reusable_payment_id( $parent_order );
			$customer_id = PeachPay_Square_Order_Data::get_payment_customer_id( $parent_order );

			if ( is_null( $source_id ) || is_null( $customer_id ) ) {
				peachpay_log( 'error', '[Square] process_subscription_renewal: source_id or customer_id missing (parent order #' . $parent_order->get_id() . ')' );
				$renewal_order->update_status( 'failed', __( 'Square reusable payment method details missing from subscription order.', 'peachpay-for-woocommerce' ) );
				return;
			}

			PeachPay_Square_Order_Data::set_peachpay_details(
				$renewal_order,
				array(
					'peachpay_mode'          => PeachPay_Square_Order_Data::get_peachpay( $parent_order, 'peachpay_mode' ),
					'square_mode'            => $square_mode,
					'service_fee_percentage' => PeachPay::service_fee_enabled() ? PeachPay::service_fee_percentage() : 0,
				)
			);

			// Create payment directly via Square SDK using saved card-on-file.
			$result = PeachPay_Square::create_payment( $renewal_order, $source_id, null, $square_mode, $customer_id );

			if ( null === $result || ( isset( $result['error'] ) && $result['error'] ) ) {
				$error_message = isset( $result['message'] ) ? $result['message'] : __( 'Payment failed', 'peachpay-for-woocommerce' );
				peachpay_log( 'error', '[Square] process_subscription_renewal: ' . $error_message . ' (renewal order #' . $renewal_order->get_id() . ')' );
				// translators: %1$s Payment method title, %2$s failure reason.
				$renewal_order->update_status( 'failed', sprintf( __( 'Square %1$s payment failed: %2$s', 'peachpay-for-woocommerce' ), $renewal_order->get_payment_method_title(), $error_message ) );
				return;
			}

			// Status already set by calculate_payment_state() inside create_payment().
		} catch ( Exception $exception ) {
			$message = __( 'Error: ', 'peachpay-for-woocommerce' ) . $exception->getMessage();
			peachpay_log( 'error', '[Square] process_subscription_renewal exception (renewal order #' . $renewal_order->get_id() . '): ' . $exception->getMessage() );
			$renewal_order->add_order_note( $message );
			$renewal_order->update_status( 'failed', $message );
		}
	}

	/**
	 * Gets the Square transaction URL for the order dashboard.
	 *
	 * @param WC_Order $order Order object related to transaction.
	 * @return string URL linking the transaction ID with the Square merchant dashboard.
	 */
	public function get_transaction_url( $order ) {
		if ( ! $order->get_transaction_id() ) {
			return '';
		}

		$square_mode = PeachPay_Square_Order_Data::get_payment( $order, 'mode' );
		if ( ! $square_mode ) {
			$square_mode = PeachPay_Square_Order_Data::get_peachpay( $order, 'square_mode' );
		}

		return PeachPay_Square::dashboard_url( $square_mode ? $square_mode : 'live', $order->get_transaction_id() );
	}

	/**
	 * For tokenizable methods, tokenizes that method for further reuse.
	 * Up to the individual payment method gateways to implement, all tokenizable
	 * methods should have an implementation.
	 *
	 * @param WC_Order $order The WC order.
	 * @param mixed    $reuse_data Reuse data object from payment create response.
	 */
	public function create_payment_token( $order, $reuse_data ) {
		$user_id      = $order->get_user_id();
		$card_details = $reuse_data['details'];
		$source_id    = $reuse_data['source_id'];
		if ( null === $source_id || null === $card_details ) {
			return;
		}

		$token = new WC_Payment_Token_PeachPay_Square_Card();
		$token->set_mode( PeachPay_Square_Order_Data::get_peachpay( $order, 'square_mode' ) );
		$token->set_gateway_id( $this->id );
		$token->set_user_id( $user_id );
		$token->set_card_type( $card_details['brand'] );
		$token->set_last4( $card_details['last_4'] );
		$token->set_expiry_month( $card_details['exp_month'] );
		$token->set_expiry_year( $card_details['exp_year'] );
		$token->set_token( $source_id );

		$token->save();

		WC_Payment_Tokens::set_users_default( get_current_user_id(), $token->get_id() );
	}

	/**
	 * Overrides default get_token behavior to only show tokens supported under the current square mode.
	 * This is because the users customer id is associated with square mode, and tokens are tied to a specific square customer id.
	 */
	public function get_tokens() {
		$tokens       = parent::get_tokens();
		$square_mode  = PeachPay_Square_Integration::mode();
		$valid_tokens = array();

		foreach ( $tokens as $token ) {
			$token_square_mode = method_exists( $token, 'get_mode' ) ? $token->get_mode( 'edit' ) : null;
			if ( null === $token_square_mode || $square_mode === $token_square_mode ) {
				$valid_tokens[] = $token;
			}
		}

		return $valid_tokens;
	}

	// -------------------------------------------------------------------------
	// Subscription helpers 
	// -------------------------------------------------------------------------

	/**
	 * Returns true when the order contains a WooCommerce Subscription
	 * created during initial checkout (parent subscription, not a renewal).
	 *
	 * @param WC_Order $order The order to inspect.
	 * @return bool
	 */
	private function is_subscription_checkout_order( WC_Order $order ): bool {
		if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order ) ) {
			return true;
		}
		// Fallback: WC Subscriptions may not have linked the subscription yet during process_payment().
		// Check order line items directly for subscription product types.
		if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! method_exists( 'WC_Subscriptions_Product', 'is_subscription' ) ) {
			return false;
		}
		foreach ( $order->get_items() as $item ) {
			$product = $item->get_product();
			if ( $product && WC_Subscriptions_Product::is_subscription( $product ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Ensures a Square Customer record exists for the order's user.
	 * Delegates to PeachPay_Square::create_customer() which avoids duplicates by
	 * checking user-meta before hitting the API.
	 *
	 * @param WC_Order $order The WooCommerce order.
	 * @param string   $mode  Square mode ('live', 'test', or 'detect').
	 * @return string|null Square customer ID on success, null on failure.
	 */
	private function maybe_create_square_customer_for_order( WC_Order $order, string $mode ): ?string {
		return PeachPay_Square::create_customer( $order, $mode );
	}

	/**
	 * Handles a $0-total subscription checkout (e.g. free trial).
	 * Stores the payment nonce as a card-on-file so future renewals can charge it.
	 *
	 * @param WC_Order    $order              The WooCommerce order.
	 * @param string      $source_id          Payment nonce from the Square Web Payments SDK.
	 * @param string|null $verification_token SCA/3DS verification token (optional).
	 * @param string      $customer_id        Square customer ID to attach the card to.
	 * @param string      $mode               Square mode ('live', 'test', or 'detect').
	 * @return array WC payment result array.
	 */
	protected function process_zero_total_payment( WC_Order $order, string $source_id, ?string $verification_token, string $customer_id, string $mode ): array {
		$card = PeachPay_Square::store_card_on_file( $source_id, $verification_token, $customer_id, $mode, $order );

		if ( null === $card ) {
			peachpay_log( 'error', '[Square] process_zero_total_payment: Failed to store card on file (order #' . $order->get_id() . ')' );
			wc_add_notice( __( 'Unable to save payment method. Please try again.', 'peachpay-for-woocommerce' ), 'error' );
			return array(
				'result'   => 'failure',
				'redirect' => $this->get_return_url( $order ),
			);
		}

		PeachPay_Square_Order_Data::set_reusable_payment_details( $order, $card['id'], $customer_id, $card['id'] );

		$order->payment_complete();

		peachpay_log( 'info', '[Square] process_zero_total_payment: Zero-total subscription order #' . $order->get_id() . ' completed, card stored.' );

		return array(
			'result'   => 'success',
			'redirect' => $order->get_checkout_order_received_url(),
		);
	}

	/**
	 * Handles a "Change Payment Method" request for an existing WooCommerce Subscription.
	 * Stores the new card on file with Square, updates the parent order's reusable payment details,
	 * and updates the subscription's payment method title.
	 *
	 * @param WC_Subscription $subscription       The WooCommerce Subscription object.
	 * @param string|null     $source_id          Payment nonce from the Square Web Payments SDK.
	 * @param string|null     $verification_token SCA/3DS verification token (optional).
	 * @return array WC payment result array.
	 */
	protected function process_change_payment_method_request( $subscription, ?string $source_id, ?string $verification_token ): array {
		$parent_order = wc_get_order( $subscription->get_parent_id() );
		if ( ! $parent_order ) {
			peachpay_log( 'error', '[Square] process_change_payment_method_request: No valid parent order for subscription #' . $subscription->get_id() . ' — aborting.' );
			wc_add_notice( __( 'Unable to update payment method. Please try again or contact support.', 'peachpay-for-woocommerce' ), 'error' );
			return array(
				'result'   => 'failure',
				'redirect' => $this->get_return_url( $subscription ),
			);
		}

		$mode        = PeachPay_Square_Integration::mode();
		$customer_id = PeachPay_Square_Order_Data::get_payment_customer_id( $parent_order );

		// Ensure a Square customer exists — subscriptions may not have one if created before this feature.
		if ( empty( $customer_id ) ) {
			$customer_id = PeachPay_Square::create_customer( $parent_order, $mode );
		}

		if ( empty( $customer_id ) || empty( $source_id ) ) {
			peachpay_log( 'error', '[Square] process_change_payment_method_request: customer_id or source_id missing (subscription #' . $subscription->get_id() . ')' );
			wc_add_notice( __( 'Unable to update payment method. Please try again.', 'peachpay-for-woocommerce' ), 'error' );
			return array(
				'result'   => 'failure',
				'redirect' => $this->get_return_url( $subscription ),
			);
		}

		$card = PeachPay_Square::store_card_on_file( $source_id, $verification_token, $customer_id, $mode );

		if ( null === $card ) {
			peachpay_log( 'error', '[Square] process_change_payment_method_request: Failed to store card on file (subscription #' . $subscription->get_id() . ')' );
			wc_add_notice( __( 'Unable to save new payment method. Please try again.', 'peachpay-for-woocommerce' ), 'error' );
			return array(
				'result'   => 'failure',
				'redirect' => $this->get_return_url( $subscription ),
			);
		}

		PeachPay_Square_Order_Data::set_reusable_payment_details( $parent_order, $card['id'], $customer_id, $card['id'] );

		$payment_title = sprintf(
			// translators: %1$s card brand, %2$s last 4 digits, %3$s expiry month, %4$s expiry year.
			__( '%1$s ending in %2$s (expires %3$s/%4$s)', 'peachpay-for-woocommerce' ),
			$card['card_brand'],
			$card['last_4'],
			$card['exp_month'],
			$card['exp_year']
		);

		$subscription->set_payment_method_title( $payment_title );
		$subscription->save();

		peachpay_log( 'info', '[Square] process_change_payment_method_request: Payment method updated (subscription #' . $subscription->get_id() . ')' );

		return array(
			'result'   => 'success',
			'redirect' => wc_get_page_permalink( 'myaccount' ),
		);
	}

}
