<?php
/**
 * Super-simple, minimum abstraction Square API wrapper.
 *
 * @package Charitable Square/Classes
 * @author  WP Charitable
 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since   1.8.7
 * @version 1.8.7
 */

use Square\Legacy\Exceptions\ApiException;
use Square\Types\CreateOrderRequest;
use Square\Types\Order;
use Square\Types\OrderSource;
use Square\Types\OrderLineItem;
use Square\Types\Money;
use Square\Exceptions\SquareApiException;
use Square\Payments\Requests\CreatePaymentRequest;
use Square\ApiResponse;
use Square\Types\Address;
use Square\Types\Payment;
use Square\Types\OrderLineItemDiscount;
use Square\Types\OrderState;
use Square\Refunds\Requests\RefundPaymentRequest;
use Square\Customers\Requests\CreateCustomerRequest;
use Square\Cards\Requests\CreateCardRequest;
use Square\Types\Card;
use Square\Types\CatalogQuery;
use Square\Types\CatalogQueryExact;
use Square\Types\CatalogObject;
use Square\Types\CatalogObjectType;
use Square\Types\Customer;
use Square\Types\SubscriptionSource;
use Square\Types\SubscriptionPricing;
use Square\Types\SubscriptionPhase;
use Square\Types\CatalogSubscriptionPlan;
use Square\Types\CatalogSubscriptionPlanVariation;
use Square\Types\CatalogObjectSubscriptionPlan;
use Square\Types\CatalogObjectSubscriptionPlanVariation;
use Square\Orders\Requests\UpdateOrderRequest;
use Square\Catalog\Requests\SearchCatalogObjectsRequest;
use Square\Catalog\Object\Requests\UpsertCatalogObjectRequest;
use Square\Subscriptions\Requests\CreateSubscriptionRequest;
use Square\Legacy\Models\Builders\CatalogObjectBuilder;
use Square\Subscriptions\Requests\CancelSubscriptionsRequest;


use Square\Customers\CustomersClient;
use Square\Core\Client\RawClient;
use Square\Cards\CardsClient;
use Square\Catalog\CatalogClient;

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

/**
 * API class.
 *
 * @since 1.8.7
 */
class Charitable_Square_API {

	/**
	 * The Square API version to target
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	const SQUARE_VERSION = '2025-03-19';

	/**
	 * Square application name.
	 *
	 * @since 1.8.7
	 */
	public const APP_NAME = 'WPCharitable';

	/**
	 * Square API client instance.
	 *
	 * @since 1.8.7
	 *
	 * @var SquareClient
	 */
	private $client;

	/**
	 * Payment token (card nonce) generated by the Web Payments SDK.
	 *
	 * @since 1.8.7
	 *
	 * @var string
	 */
	private $source_id;

	/**
	 * Last API call response.
	 *
	 * @since 1.8.7
	 *
	 * @var ApiResponse
	 */
	private $response;

	/**
	 * Last API call exception.
	 *
	 * @since 1.8.7
	 *
	 * @var ApiException
	 */
	private $exception;

	/**
	 * The base URL that we will call with our API requests.
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $api_base_url;

	/**
	 * The Live Access Token supplied by the user.
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $live_access_token;

	/**
	 * The Test Access Token supplied by the user.
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $test_access_token;

	/**
	 * The Access Token supplied by the user.
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $access_token;

	/**
	 * The Refresh Token supplied by the user.
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $refresh_token;

	/**
	 * The Client ID supplied by the user.
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $merchant_id;

	/**
	 *
	 * The ID of the business location to associate the payment with
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $location_id;

	/**
	 * Whether the Access Token was validated.
	 *
	 * @since 1.8.7
	 *
	 * @var   boolean
	 */
	private $access_token_validated;

	/**
	 * The response to the most recent request.
	 *
	 * @since 1.8.7
	 *
	 * @var   WP_Error|?
	 */
	private $last_response;

	/**
	 * Whether the test mode is enabled.
	 *
	 * @since 1.8.7
	 *
	 * @var   boolean
	 */
	private $test_mode;

	/**
	 * The Client ID supplied by the user.
	 *
	 * @since 1.8.7
	 *
	 * @var   string
	 */
	private $client_id;

	/**
	 * API errors.
	 *
	 * @since 1.8.7
	 *
	 * @var array
	 */
	private $errors;

	/**
	 * Maximum number of retries for rate-limited requests.
	 *
	 * @since 1.8.7
	 *
	 * @var int
	 */
	const MAX_RETRIES = 5;

	/**
	 * Base delay in seconds for exponential backoff.
	 *
	 * @since 1.8.7
	 *
	 * @var int
	 */
	const BASE_DELAY = 1;

	/**
	 * Create a new instance.
	 *
	 * @since 1.8.7
	 *
	 * @param Charitable_Square_Connection $connection The connection object.
	 *
	 * Connection object structure:
	 * - live_mode (private): null
	 * - access_token (private): string - OAuth access token
	 * - refresh_token (private): string - OAuth refresh token
	 * - client_id (private): string - Square application client ID
	 * - merchant_id (private): string - Square merchant ID
	 * - currency (private): null
	 * - status (private): string - Connection status ("valid")
	 * - renew_at (private): int - Unix timestamp for token renewal
	 * - encrypted (private): bool - Whether tokens are encrypted
	 * - scopes_updated (private): int - Unix timestamp of last scope update
	 */


	/**
	 * Constructor.
	 *
	 * @since 1.8.7
	 *
	 * @param Charitable_Square_Connection $connection The connection object.
	 */
	public function __construct( $connection ) {

		if ( empty( $connection ) ) {
			return;
		}

		$this->client = new \Square\SquareClient(
			token: $connection->get_access_token(),
			options: array(
				'baseUrl' => $connection->get_mode() !== 'live' ? \Square\Environments::Sandbox->value : \Square\Environments::Production->value,
			),
		);

		// Set the location ID.
		$this->location_id = charitable_square_get_location_id();

		if ( empty( $this->location_id ) ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Square API: No location ID found. Please configure your Square location ID in the plugin settings.' );
				// phpcs:enable
			}
			return;
		}

		$this->test_mode     = $connection->get_mode();
		$this->api_base_url  = $this->test_mode ? 'https://connect.squareupsandbox.com/v2' : 'https://connect.squareup.com/v2';
		$this->access_token  = $connection->get_access_token();
		$this->refresh_token = $connection->get_refresh_token();
		$this->client_id     = $connection->get_client_id();
		$this->merchant_id   = $connection->get_merchant_id();
	}

	/**
	 * Set tokens from a submitted form data.
	 *
	 * @since 1.8.7
	 *
	 * @param string $square_token The Square token.
	 */
	public function set_payment_tokens( string $square_token ) {

		if ( ! empty( $square_token ) ) {
			$this->source_id = $square_token;
		}
	}

	/**
	 * Set up hooks.
	 *
	 * @since 1.8.7
	 *
	 * @return void
	 */
	public function init() {
		// Nothing much here yet. Add any filters pertaining to API here etc..
	}

	/**
	 * Check if OAuth connection is present and ready to use.
	 *
	 * @since 1.8.7
	 */
	private function check_connection() {

		$connection = Charitable_Square_Connection::get();

		if ( ! $connection || ! $connection->is_configured() ) {
			$this->errors[] = esc_html__( 'Square account connection is missing.', 'charitable' );

			return;
		}

		if ( ! $connection->is_valid() ) {
			$this->errors[] = esc_html__( 'Square account connection is invalid.', 'charitable' );

			return;
		}

		if ( ! $connection->is_currency_matched() ) {
			$this->errors[] = esc_html__( 'The currency associated with the payment is not valid for the provided business location.', 'charitable' );
		}
	}

	/**
	 * Check if single payment tokens are present.
	 *
	 * @since 1.8.7
	 */
	private function check_payment_tokens() {

		if ( empty( $this->source_id ) ) {
			$this->errors[] = esc_html__( 'Square payment stopped, missing card tokens.', 'charitable' );
		}
	}

	/**
	 * Check if all required general arguments are present.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Arguments to check.
	 */
	private function check_required_args_general( array $args ) {

		if ( empty( $args['location_id'] ) ) {
			$this->errors[] = esc_html__( 'Missing location ID.', 'charitable' );
		}

		if ( empty( $args['currency'] ) ) {
			$this->errors[] = esc_html__( 'Missing currency.', 'charitable' );
		}

		if ( empty( $args['amount'] ) && ! is_numeric( $args['amount'] ) ) {
			$this->errors[] = esc_html__( 'Missing amount.', 'charitable' );
		}
	}

	/**
	 * Check if all required single payment arguments are present.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Arguments to check.
	 */
	private function check_required_args_single( array $args ) {

		if ( empty( $args['order_items'] ) ) {
			$this->errors[] = esc_html__( 'Missing order/payment items.', 'charitable' );
		}
	}

	/**
	 * Process single transaction.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 */
	public function process_single_transaction( array $args ) {

		$this->check_connection();
		$this->check_payment_tokens();
		$this->check_required_args_general( $args );
		$this->check_required_args_single( $args );

		if ( $this->errors ) {

			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Charitable_Square_API. process_single_transaction - before perform_single_transaction errors' );
				error_log( print_r( $this->errors, true ) );
				// phpcs:enable
			}

			return $this->errors;
		}

		$result = $this->perform_single_transaction( $args );

		if ( $this->errors ) {

			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Charitable_Square_API. process_single_transaction - after perform_single_transaction errors' );
				error_log( print_r( $this->errors, true ) );
				// phpcs:enable
			}

			return $this->errors;
		}

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'process_single_transaction - result' );
			error_log( print_r( $result, true ) );
			// phpcs:enable
		}

		/**
		 * Fire when a single transaction is performed.
		 *
		 * @since 1.8.7
		 *
		 * @param array $result Single transaction result.
		 * @param array $args   Payment arguments.
		 * @param Api   $api    Api class instance.
		 */
		do_action( 'charitable_square_api_process_single_transaction_after', $result, $args, $this );

		return $result;
	}

	/**
	 * Process subscription transaction.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 */
	public function process_subscription_transaction( array $args ) {

		$this->check_connection();
		$this->check_payment_tokens();
		$this->check_required_args_general( $args );
		$this->check_required_args_subscription( $args );

		if ( $this->errors ) {
			return;
		}

		$result = $this->perform_subscription_transaction( $args );

		return $result;
	}

	/**
	 * Perform a single transaction.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 *
	 * @return array
	 */
	private function perform_single_transaction( array $args ): array {

		$result = array();
		// Create an order.
		$order_request = $this->prepare_create_order_request( $args );

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'Charitable_Square_API. perform_single_transaction - order_request' );
			error_log( print_r( $order_request, true ) );
			// phpcs:enable
		}

		$order = $this->send_create_order_request( $order_request );

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'Charitable_Square_API. perform_single_transaction - order' );
			error_log( print_r( $order, true ) );
			// phpcs:enable
		}

		if ( ! $order instanceof Order ) {
			error_log( 'Charitable_Square_API. perform_single_transaction - order not created' ); // phpcs:ignore
			return $result;
		}

		$result['order'] = $order;

		// Create a payment.
		$payment_request = $this->prepare_create_payment_request( $order, $args );
		$payment         = $this->send_create_payment_request( $payment_request );

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'Charitable_Square_API. perform_single_transaction - payment' );
			error_log( print_r( $payment, true ) );
			// phpcs:enable
		}

		// In this case we should cancel an order.
		if ( ! $payment instanceof Payment ) {
			$update_order_request = $this->prepare_update_order_request( $order );
			$updated_order        = $this->send_update_order_request( $order->getId(), $update_order_request );

			return $updated_order instanceof Order ? array( 'order' => $updated_order ) : $result;
		}

		$result['payment'] = $payment;

		return $result;
	}

	/**
	 * Prepare a create order request object for sending to API.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Single payment arguments.
	 *
	 * @return CreateOrderRequest
	 */
	private function prepare_create_order_request( array $args ) {

		$request = $this->get_create_order_request_object( $args );
		$request = $this->create_order_request_set_line_items( $request, $args );

		/**
		 * Filter a create order request object.
		 *
		 * @since 1.8.7
		 *
		 * @param CreateOrderRequest $request Create order request object.
		 * @param array              $args    Payment arguments.
		 * @param Api                $api     Api class instance.
		 */
		return apply_filters( 'charitable_square_api_prepare_create_order_request', $request, $args, $this );
	}

	/**
	 * Retrieve a create order request object.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 *
	 * @return CreateOrderRequest
	 */
	private function get_create_order_request_object( array $args ) {

		$request = new CreateOrderRequest();

		$request->setIdempotencyKey( $this->get_idempotency_key() );

		$request->setOrder( new Order( array( 'locationId' => $args['location_id'] ) ) );
		$request->getOrder()->setSource( new OrderSource( array( 'name' => self::APP_NAME ) ) );

		return $request;
	}

	/**
	 * Retrieve an update order request object.
	 *
	 * @since 1.8.7
	 *
	 * @param Order $order Order object.
	 *
	 * @return UpdateOrderRequest
	 */
	private function get_update_order_request_object( $order ): UpdateOrderRequest {

		$request = new UpdateOrderRequest(
			array(
				'orderId'        => $order->getId(),
				'order'          => $order,
				'idempotencyKey' => $this->get_idempotency_key(),
			)
		);

		$request->getOrder()->setState( \Square\Types\OrderState::Canceled->value );

		return $request;
	}

	/**
	 * Set line items to a create order request object.
	 *
	 * @since 1.8.7
	 *
	 * @param CreateOrderRequest $request Request to create an order.
	 * @param array              $args    Payment arguments.
	 *
	 * @return CreateOrderRequest
	 */
	private function create_order_request_set_line_items( $request, array $args ): CreateOrderRequest {

		$line_items = array();

		foreach ( (array) $args['order_items'] as $item ) {

			// Order item without variations.
			if ( empty( $item['variations'] ) ) {
				$line_items[] = $this->get_order_line_item_object( $item, $args );

				continue;
			}

			// Order item with variations (e.g. Small, Medium and Large).
			foreach ( (array) $item['variations'] as $variation ) {

				$order_line_item = $this->get_order_line_item_object( $variation, $args );

				$order_line_item->setVariationName( $variation['variation_name'] );

				$line_items[] = $order_line_item;
			}
		}

		$request->getOrder()->setLineItems( $line_items );

		return $request;
	}

	/**
	 * Retrieve a single order line item object.
	 *
	 * @since 1.8.7
	 *
	 * @param array $item Order item data.
	 * @param array $args Payment arguments.
	 *
	 * @return OrderLineItem
	 */
	private function get_order_line_item_object( array $item, array $args ) {

		$base_price_money = new Money();
		$base_price_money->setAmount( round( $item['amount'] ) );
		$base_price_money->setCurrency( $args['currency'] );

		$order_line_items = new OrderLineItem(
			array(
				'quantity'       => $item['quantity'],
				'name'           => $item['name'],
				'basePriceMoney' => $base_price_money,
			)
		);

		return $order_line_items;
	}

	/**
	 * Retrieve a single order discount object.
	 *
	 * @since 1.8.7
	 *
	 * @param array $discount Discount data.
	 * @param array $args     Payment arguments.
	 *
	 * @return OrderLineItemDiscount
	 */
	private function get_order_discount_object( array $discount, array $args ): OrderLineItemDiscount {

		$order_discounts = new OrderLineItemDiscount();

		$order_discounts->setName( $discount['name'] );
		$order_discounts->setAmountMoney( new Money() );
		$order_discounts->getAmountMoney()->setAmount( $discount['amount'] );
		$order_discounts->getAmountMoney()->setCurrency( $args['currency'] );

		return $order_discounts;
	}

	/**
	 * Set discounts to a create order request object.
	 *
	 * @since 1.8.7
	 *
	 * @param CreateOrderRequest $request Request to create an order.
	 * @param array              $args    Payment arguments.
	 *
	 * @return CreateOrderRequest
	 */
	private function create_order_request_set_discounts( $request, array $args ): CreateOrderRequest {

		$discounts = array();

		if ( empty( $args['discounts'] ) ) {
			return $request;
		}

		foreach ( (array) $args['discounts'] as $discount ) {

			$discounts[] = $this->get_order_discount_object( $discount, $args );
		}

		if ( ! empty( $discounts ) ) {
			$request->getOrder()->setDiscounts( $discounts );
		}

		return $request;
	}

	/**
	 * Set order to a create payment request object.
	 *
	 * @since 1.8.7
	 *
	 * @param CreatePaymentRequest $request Create payment request object.
	 * @param Order                $order   Order object.
	 *
	 * @return CreatePaymentRequest
	 */
	private function create_payment_request_set_order( $request, $order ): CreatePaymentRequest {

		$request->setOrderId( $order->getId() );
		$request->setLocationId( $order->getLocationId() );

		$amount   = $order->getTotalMoney()->getAmount();
		$currency = $order->getTotalMoney()->getCurrency();

		$request->getAmountMoney()->setAmount( $amount );
		$request->getAmountMoney()->setCurrency( $currency );

		if ( ! Charitable_Square_Helpers::is_license_ok() && Charitable_Square_Helpers::is_application_fee_supported( $currency ) ) {
			$request->setAppFeeMoney( new Money() );
			$request->getAppFeeMoney()->setAmount( (int) ( round( $amount * 0.03 ) ) );
			$request->getAppFeeMoney()->setCurrency( $currency );
		}

		return $request;
	}

	/**
	 * Retrieve a create payment request object.
	 *
	 * @since 1.8.7
	 *
	 * @return CreatePaymentRequest
	 */
	private function get_create_payment_request_object(): CreatePaymentRequest {

		$request = new CreatePaymentRequest(
			array(
				'sourceId'       => $this->source_id,
				'idempotencyKey' => $this->get_idempotency_key(),
			)
		);

		$request->setAmountMoney( new Money() );

		return $request;
	}

	/**
	 * Send a create order request object to API.
	 *
	 * @since 1.8.7
	 *
	 * @param CreateOrderRequest $request Create order request object.
	 *
	 * @return Order|null
	 */
	private function send_create_order_request( $request ) {
		try {
			$this->response = $this->handle_rate_limiting(
				function () use ( $request ) {
					if ( charitable_is_debug( 'square' ) ) {
						// phpcs:disable
						error_log( 'Charitable_Square_API. send_create_order_request - inside rate limiting function' );
						// phpcs:enable
					}
					return $this->client->orders->create( $request );
				}
			);

			if ( $this->response->getErrors() ) {
				$this->errors[] = esc_html__( 'Square fail: order was not created.', 'charitable' );
				return null;
			}

			return $this->response->getOrder();

		} catch ( \Square\Exceptions\SquareApiException $e ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Charitable_Square_API. send_create_order_request - caught exception: ' . $e->getMessage() );
				// phpcs:enable
			}
			$this->exception = $e;

			// Get the error category and code from the exception message using regex.
			$error_category = '';
			$error_code     = '';

			// Look for "category": "SOME_CATEGORY" pattern.
			if ( preg_match( '/"category":\s*"([^"]+)"/', $e->getMessage(), $category_matches ) ) {
				$error_category = $category_matches[1];
				if ( charitable_is_debug( 'square' ) ) {
					// phpcs:disable
					error_log( 'Charitable_Square_API. send_create_order_request - error category: ' . $error_category );
					// phpcs:enable
				}
			}

			// Look for "code": "SOME_CODE" pattern.
			if ( preg_match( '/"code":\s*"([^"]+)"/', $e->getMessage(), $code_matches ) ) {
				$error_code = $code_matches[1];
				if ( charitable_is_debug( 'square' ) ) {
					// phpcs:disable
					error_log( 'Charitable_Square_API. send_create_order_request - error code: ' . $error_code );
					// phpcs:enable
				}
			}

			$this->errors[] = array(
				'message'  => esc_html__( 'Square fail: order was not created.', 'charitable' ),
				'category' => $error_category,
				'code'     => $error_code,
			);
			return null;
		}
	}

	/**
	 * Send an update order request object to API.
	 *
	 * @since 1.8.7
	 *
	 * @param string             $order_id The ID of the order to update.
	 * @param UpdateOrderRequest $request  Update order request object.
	 *
	 * @return Order|null
	 */
	private function send_update_order_request( string $order_id, $request ) {
		try {
			$response = $this->handle_rate_limiting(
				function () use ( $order_id, $request ) {
					return $this->client->getOrdersApi()->updateOrder( $order_id, $request );
				}
			);

			if ( ! $response->isSuccess() ) {
				return null;
			}

			return $response->getResult()->getOrder();

		} catch ( ApiException $e ) {
			return null;
		}
	}

	/**
	 * Handle rate limiting with exponential backoff.
	 *
	 * @since 1.8.7
	 *
	 * @param callable $api_call The API call to make.
	 * @param int      $retry_count Current retry attempt.
	 * @return mixed The API response.
	 * @throws ApiException If the API call fails after all retries.
	 */
	private function handle_rate_limiting( $api_call, $retry_count = 0 ) {
		try {
			return $api_call();
		} catch ( ApiException $e ) {
			// Check if this is a rate limit error (429).
			if ( $e->getCode() === 429 && $retry_count < self::MAX_RETRIES ) {
				// Calculate delay using exponential backoff: base_delay * (2^retry_count).
				$delay = self::BASE_DELAY * pow( 2, $retry_count );

				if ( charitable_is_debug( 'square' ) ) {
					// phpcs:disable
					error_log(
						sprintf(
							'Square API rate limited. Retry %d of %d. Waiting %d seconds.',
							$retry_count + 1,
							self::MAX_RETRIES,
							$delay
						)
					);
					// phpcs:enable
				}

				// Sleep for the calculated delay.
				sleep( $delay );

				// Retry the API call.
				return $this->handle_rate_limiting( $api_call, $retry_count + 1 );
			}

			// If not a rate limit error or max retries reached, rethrow the exception.
			throw $e;
		}
	}

	/**
	 * Send a create payment request to API.
	 *
	 * @since 1.8.7
	 *
	 * @param CreatePaymentRequest $request Create payment request object.
	 *
	 * @return Payment|null
	 */
	private function send_create_payment_request( $request ) {
		try {
			$this->response = $this->handle_rate_limiting(
				function () use ( $request ) {
					return $this->client->payments->create( $request );
				}
			);

			if ( $this->response->getErrors() ) {
				$this->errors[] = esc_html__( 'Square fail: payment was not processed.', 'charitable' );
				$this->add_response_errors_message();
				return null;
			}

			return $this->response->getPayment();

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: payment was not processed.', 'charitable' );
			$this->add_response_errors_message( 'Exception' );
			return null;
		}
	}

	/**
	 * Perform a subscription transaction.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 */
	private function perform_subscription_transaction( array $args ) {

		// Create a customer.
		$customer_request = $this->prepare_create_customer_request( $args );
		$customer         = $this->send_create_customer_request( $customer_request );

		if ( ! $customer instanceof Customer ) {
			return;
		}

		// Create a customer card.
		$card_request = $this->prepare_create_customer_card_request( $customer->getId(), $args );
		$card         = $this->send_create_customer_card_request( $card_request );

		if ( ! $card instanceof Card ) {
			return;
		}

		// Get a subscription plan.
		$plan = $this->get_plan( $args );

		if ( ! $plan instanceof CatalogObject ) {
			return;
		}

		// Get a subscription plan variation.
		$plan_variation = $this->get_plan_variation( $args, $plan );

		if ( ! $plan_variation instanceof CatalogObject ) {
			return;
		}

		// Create a subscription.
		$subscription_request = $this->prepare_create_subscription_request(
			$plan_variation->getValue()->getId(),
			$customer->getId(),
			$card->getId(),
			$args
		);

		$result = $this->send_create_subscription_request( $subscription_request );

		return $result;
	}

	/**
	 * Check if all required subscription arguments are present.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Arguments to check.
	 */
	private function check_required_args_subscription( array $args ) {

		if ( empty( $args['subscription']['plan_name'] ) ) {
			$this->errors[] = esc_html__( 'Missing subscription plan name.', 'charitable' );
		}

		if ( empty( $args['subscription']['plan_variation_name'] ) ) {
			$this->errors[] = esc_html__( 'Missing subscription plan variation name.', 'charitable' );
		}

		if ( empty( $args['subscription']['phase_cadence'] ) ) {
			$this->errors[] = esc_html__( 'Missing subscription cadence.', 'charitable' );
		}

		if ( empty( $args['subscription']['customer']['first_name'] ) && empty( $args['subscription']['customer']['last_name'] ) ) {
			$this->errors[] = esc_html__( 'Missing customer name.', 'charitable' );
		}

		if ( empty( $args['subscription']['customer']['email'] ) ) {
			$this->errors[] = esc_html__( 'Missing customer email.', 'charitable' );
		}
	}

	/**
	 * Prepare a create customer request object for sending to API.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 *
	 * @return CreateCustomerRequest
	 */
	private function prepare_create_customer_request( array $args ): CreateCustomerRequest {

		$request = $this->get_create_customer_request_object();

		$request->setEmailAddress( $args['subscription']['customer']['email'] );

		if ( ! empty( $args['subscription']['customer']['first_name'] ) ) {
			$request->setGivenName( $args['subscription']['customer']['first_name'] );
		}

		if ( ! empty( $args['subscription']['customer']['last_name'] ) ) {
			$request->setFamilyName( $args['subscription']['customer']['last_name'] );
		}

		if ( ! empty( $args['subscription']['customer']['address'] ) ) {
			$address = $this->get_address_object( $args['subscription']['customer'] );

			$request->setAddress( $address );
		}

		return $request;
	}

	/**
	 * Prepare a create customer card request object for sending to API.
	 *
	 * @since 1.8.7
	 *
	 * @param string $customer_id Customer ID.
	 * @param array  $args        Payment arguments.
	 *
	 * @return CreateCardRequest
	 */
	private function prepare_create_customer_card_request( string $customer_id, array $args ): CreateCardRequest {

		$request = new CreateCardRequest(
			array(
				'idempotencyKey' => $this->get_idempotency_key(),
				'sourceId'       => $this->source_id,
				'card'           => new Card(),
			)
		);

		$request->getCard()->setCustomerId( $customer_id );

		if ( ! empty( $args['subscription']['customer']['address'] ) ) {
			$address = $this->get_address_object( $args['subscription']['customer'] );

			// For subscriptions: make sure that a postal code is not empty.
			// Otherwise, API error "The postal code doesn't match the one used for card nonce creation" is occur here.
			if ( ! empty( $address->getPostalCode() ) ) {
				$request->getCard()->setBillingAddress( $address );
			}
		}

		if ( ! empty( $args['subscription']['card_name'] ) ) {
			$request->getCard()->setCardholderName( $args['subscription']['card_name'] );
		}

		return $request;
	}

	/**
	 * Prepare a create subscription request object for sending to API.
	 *
	 * @since 1.8.7
	 *
	 * @param string $plan_id     Plan ID.
	 * @param string $customer_id Customer ID.
	 * @param string $card_id     Customer ID.
	 * @param array  $args        Payment arguments.
	 *
	 * @return CreateSubscriptionRequest
	 */
	private function prepare_create_subscription_request( string $plan_id, string $customer_id, string $card_id, array $args ) {

		$request = $this->get_create_subscription_request_object( $plan_id, $customer_id, $args );

		$request->setCardId( $card_id );
		$request->setSource( new SubscriptionSource() );
		$request->getSource()->setName( self::APP_NAME );

		return $request;
	}

	/**
	 * Retrieve a create customer request object.
	 *
	 * @since 1.8.7
	 *
	 * @return CreateCustomerRequest
	 */
	private function get_create_customer_request_object(): CreateCustomerRequest {

		$request = new CreateCustomerRequest();

		$request->setIdempotencyKey( $this->get_idempotency_key() );

		return $request;
	}

	/**
	 * Prepare an update order request object for sending to API.
	 *
	 * @since 1.8.7
	 *
	 * @param Order $order Order object.
	 *
	 * @return UpdateOrderRequest
	 */
	private function prepare_update_order_request( $order ): UpdateOrderRequest {

		$request = $this->get_update_order_request_object( $order );

		/**
		 * Filter an update order request object.
		 *
		 * @since 1.8.7
		 *
		 * @param UpdateOrderRequest $request Update order request object.
		 * @param Order              $order   Order object.
		 * @param Api                $api     Api class instance.
		 */
		return apply_filters( 'charitable_square_api_prepare_update_order_request', $request, $order, $this );
	}

	/**
	 * Send a create customer card request to API.
	 *
	 * @since 1.8.7
	 *
	 * @param CreateCardRequest $request Create card request object.
	 *
	 * @return Card|null
	 */
	private function send_create_customer_card_request( $request ) {
		try {
			$raw_client   = new RawClient(
				array(
					'baseUrl' => $this->test_mode ? \Square\Environments::Sandbox->value : \Square\Environments::Production->value,
					'headers' => array(
						'Authorization'  => 'Bearer ' . $this->access_token,
						'Square-Version' => self::SQUARE_VERSION,
						'Content-Type'   => 'application/json',
					),
				)
			);
			$cards_client = new CardsClient( $raw_client );

			$this->response = $this->handle_rate_limiting(
				function () use ( $cards_client, $request ) {
					return $cards_client->create( $request );
				}
			);

			// Get the card from the response.
			$card = $this->response->getCard();

			if ( ! $card ) {
				$this->errors[] = esc_html__( 'Square fail: customer card was not created.', 'charitable' );
				return null;
			}

			return $card;

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: customer card was not created.', 'charitable' );
			return null;
		}
	}

	/**
	 * Prepare a create payment request object for sending to API.
	 *
	 * @since 1.8.7
	 *
	 * @param Order $order Order object.
	 * @param array $args  Payment arguments.
	 *
	 * @return CreatePaymentRequest
	 */
	private function prepare_create_payment_request( $order, array $args ): CreatePaymentRequest {

		$request = $this->get_create_payment_request_object();
		$request = $this->create_payment_request_set_order( $request, $order );

		if ( ! empty( $args['billing'] ) ) {
			$address = $this->get_address_object( $args['billing'] );

			$request->setBillingAddress( $address );
		}

		if ( ! empty( $args['buyer_email'] ) ) {
			$request->setBuyerEmailAddress( $args['buyer_email'] );
		}

		if ( ! empty( $args['note'] ) ) {
			$request->setNote( $args['note'] );
		}

		/**
		 * Filter a create payment request object.
		 *
		 * @since 1.8.7
		 *
		 * @param CreateOrderRequest $request Create payment request object.
		 * @param array              $args    Payment arguments.
		 * @param Api                $api     Api class instance.
		 */
		return apply_filters( 'charitable_square_api_prepare_create_payment_request', $request, $args, $this );
	}

	/**
	 * Send a create customer request to API.
	 *
	 * @since 1.8.7
	 *
	 * @param CreateCustomerRequest $request Create customer request object.
	 *
	 * @return Customer|null
	 */
	private function send_create_customer_request( $request ) {
		try {
			$raw_client       = new RawClient(
				array(
					'baseUrl' => $this->test_mode ? \Square\Environments::Sandbox->value : \Square\Environments::Production->value,
					'headers' => array(
						'Authorization'  => 'Bearer ' . $this->access_token,
						'Square-Version' => self::SQUARE_VERSION,
						'Content-Type'   => 'application/json',
					),
				)
			);
			$customers_client = new CustomersClient( $raw_client );

			$this->response = $this->handle_rate_limiting(
				function () use ( $customers_client, $request ) {
					return $customers_client->create( $request );
				}
			);

			// Get the customer from the response.
			$customer = $this->response->getCustomer();

			if ( ! $customer ) {
				$this->errors[] = esc_html__( 'Square fail: customer was not created.', 'charitable' );
				return null;
			}

			return $customer;

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: customer was not created.', 'charitable' );
			return null;
		}
	}

	/**
	 * Get subscription plan.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 *
	 * @return CatalogObject|null
	 */
	private function get_plan( array $args ) {

		// Search a subscription plan.
		$search_plan_request = $this->get_search_catalog_request_object( 'SUBSCRIPTION_PLAN', $args['subscription']['plan_variation_name'] );
		$plan                = $this->send_search_catalog_request( $search_plan_request );

		// Create a subscription plan if it's not exists.
		if ( ! $plan ) {
			$create_plan_request = $this->get_create_plan_request_object( $args );
			$plan                = $this->send_create_plan_request( $create_plan_request );
		}

		return $plan;
	}

	/**
	 * Send a create subscription plan request to API.
	 *
	 * @since 1.8.7
	 *
	 * @param UpsertCatalogObjectRequest $request Create subscription plan request object.
	 *
	 * @return CatalogObject|null
	 */
	private function send_create_plan_request( $request ) {
		try {
			$this->response = $this->handle_rate_limiting(
				function () use ( $request ) {
					return $this->client->catalog->object->upsert( $request );
				}
			);

			if ( ! $this->response->getCatalogObject() ) {
				$this->errors[] = esc_html__( 'Square fail: subscription plan was not created.', 'charitable' );
				return null;
			}

			return $this->response->getCatalogObject();

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: subscription plan was not created.', 'charitable' );
			return null;
		}
	}

	/**
	 * Send a create subscription plan variations request to API.
	 *
	 * @since 1.8.7
	 *
	 * @param UpsertCatalogObjectRequest $request Create subscription plan request object.
	 *
	 * @return CatalogObject|null
	 */
	private function send_create_plan_variations_request( $request ) {
		try {
			$this->response = $this->handle_rate_limiting(
				function () use ( $request ) {
					return $this->client->catalog->object->upsert( $request );
				}
			);

			if ( ! $this->response->getCatalogObject() ) {
				$this->errors[] = esc_html__( 'Square fail: subscription plan variation was not created.', 'charitable' );
				return null;
			}

			return $this->response->getCatalogObject();

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: subscription plan variation was not created.', 'charitable' );
			return null;
		}
	}

	/**
	 * Send a create subscription request to API.
	 *
	 * @since 1.8.7
	 *
	 * @param CreateSubscriptionRequest $request Create subscription request object.
	 *
	 * @return Subscription|null
	 */
	private function send_create_subscription_request( $request ) {
		try {
			$this->response = $this->handle_rate_limiting(
				function () use ( $request ) {
					return $this->client->subscriptions->create( $request );
				}
			);

			if ( ! $this->response->getSubscription() ) {
				$this->errors[] = esc_html__( 'Square fail: something went wrong during subscription process.', 'charitable' );
				return null;
			}

			return $this->response->getSubscription();

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: something went wrong during subscription process.', 'charitable' );
			return null;
		}
	}

	/**
	 * Retrieve a create plan request object.
	 *
	 * @since 1.8.7
	 *
	 * @param array $args Payment arguments.
	 *
	 * @return UpsertCatalogObjectRequest
	 */
	private function get_create_plan_request_object( array $args ) {

		$request = new UpsertCatalogObjectRequest(
			array(
				'idempotencyKey' => $this->get_idempotency_key(),
				'object'         => CatalogObject::subscriptionPlan(
					new CatalogObjectSubscriptionPlan(
						array(
							'id'                   => '#plan_' . uniqid(), // Square requires IDs to start with # for new objects.
							'subscriptionPlanData' => new CatalogSubscriptionPlan(
								array(
									'name' => $args['subscription']['plan_name'],
								)
							),
						)
					)
				),
			)
		);

		$request->getObject()->asSubscriptionPlan()->getSubscriptionPlanData()->setAllItems( true );

		return $request;
	}

	/**
	 * Get subscription plan variation.
	 *
	 * @since 1.8.7
	 *
	 * @param array         $args Payment arguments.
	 * @param CatalogObject $plan Plan object.
	 *
	 * @return CatalogObject|null
	 */
	private function get_plan_variation( array $args, $plan ) {

		// Search a subscription plan.
		$search_plan_request = $this->get_search_catalog_request_object( 'SUBSCRIPTION_PLAN_VARIATION', $args['subscription']['plan_name'] );
		$plan_variation      = $this->send_search_catalog_request( $search_plan_request );

		// Create a subscription plan if it's not exists.
		if ( ! $plan_variation ) {
			$create_plan_variations_request = $this->get_create_plan_variations_request_object( $args, $plan );
			$plan_variation                 = $this->send_create_plan_variations_request( $create_plan_variations_request );
		}

		return $plan_variation;
	}

	/**
	 * Retrieve a create plan variation request object.
	 *
	 * @since 1.8.7
	 *
	 * @param array         $args Payment arguments.
	 * @param CatalogObject $plan Plan object.
	 *
	 * @return UpsertCatalogObjectRequest
	 */
	private function get_create_plan_variations_request_object( array $args, $plan ): UpsertCatalogObjectRequest {

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'get_create_plan_variations_request_object - args' );
			error_log( print_r( $args, true ) );
			error_log( 'get_create_plan_variations_request_object - plan' );
			error_log( print_r( $plan, true ) );
			// phpcs:enable
		}

		$request = new UpsertCatalogObjectRequest(
			array(
				'idempotencyKey' => $this->get_idempotency_key(),
				'object'         => CatalogObject::subscriptionPlanVariation(
					new CatalogObjectSubscriptionPlanVariation(
						array(
							'id' => '#variation_' . uniqid(),
							'subscriptionPlanVariationData' => new CatalogSubscriptionPlanVariation(
								array(
									'name'               => $args['subscription']['plan_variation_name'],
									'phases'             => array(
										new SubscriptionPhase(
											array(
												'cadence' => $args['subscription']['phase_cadence']['value'],
												'pricing' => new SubscriptionPricing(
													array(
														'type' => 'STATIC',
														'priceMoney' => new Money(
															array(
																'amount'   => $args['amount'],
																'currency' => $args['currency'],
															)
														),
													)
												),
											)
										),
									),
									'subscriptionPlanId' => $plan->asSubscriptionPlan()->getId(),
								)
							),
						)
					)
				),
			)
		);

		return $request;
	}

	/**
	 * Retrieve a create subscription request object.
	 *
	 * @since 1.8.7
	 *
	 * @param string $plan_id     Plan ID.
	 * @param string $customer_id Customer ID.
	 * @param array  $args        Payment arguments.
	 *
	 * @return CreateSubscriptionRequest
	 */
	private function get_create_subscription_request_object( string $plan_id, string $customer_id, array $args ): CreateSubscriptionRequest {
		$request = new CreateSubscriptionRequest(
			array(
				'locationId'      => $this->location_id,
				'customerId'      => $customer_id,
				'planVariationId' => $plan_id,
				'idempotencyKey'  => $this->get_idempotency_key(),
				'cardId'          => $args['card_id'] ?? null,
				'startDate'       => $args['start_date'] ?? null,
				'canceledDate'    => $args['canceled_date'] ?? null,
				'taxPercentage'   => $args['tax_percentage'] ?? null,
				'timezone'        => $args['timezone'] ?? null,
			)
		);

		return $request;
	}

	/**
	 * Retrieve a search catalog request object.
	 *
	 * @since 1.8.7
	 *
	 * @param string $type Object type.
	 * @param string $name Object name.
	 *
	 * @return SearchCatalogObjectsRequest
	 */
	private function get_search_catalog_request_object( string $type, string $name ): SearchCatalogObjectsRequest {

		$request = new SearchCatalogObjectsRequest();

		$request->setObjectTypes( array( $type ) );
		$request->setLimit( 1 );
		$request->setQuery( new CatalogQuery() );
		$request->getQuery()->setExactQuery(
			new CatalogQueryExact(
				array(
					'attributeName'  => 'name',
					'attributeValue' => $name,
				)
			)
		);

		return $request;
	}

	/**
	 * Send a search catalog request to API.
	 *
	 * @since 1.8.7
	 *
	 * @param SearchCatalogObjectsRequest $request Search subscription plan request object.
	 *
	 * @return CatalogObject|null
	 */
	private function send_search_catalog_request( $request ) {
		try {
			$this->response = $this->handle_rate_limiting(
				function () use ( $request ) {
					return $this->client->catalog->search( $request );
				}
			);

			$objects = $this->response->getObjects();
			if ( empty( $objects ) ) {
				$this->errors[] = esc_html__( 'Square fail: no catalog objects found.', 'charitable' );
				return null;
			}

			return $objects;

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: something went wrong during catalog search.', 'charitable' );
			return null;
		}
	}

	/**
	 * Process a subscription cancellation.
	 *
	 * @since 1.8.7
	 *
	 * @param string $subscription_id The ID of the subscription to cancel.
	 * @return bool True if the subscription was cancelled successfully, false otherwise.
	 */
	public function process_subscription_cancellation( string $subscription_id ) {
		$this->check_connection();

		if ( $this->errors ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'process_subscription_cancellation - errors' );
				error_log( print_r( $this->errors, true ) );
				// phpcs:enable
			}
			return false;
		}

		try {
			// Create cancel request.
			$request = new CancelSubscriptionsRequest(
				array(
					'subscriptionId' => $subscription_id,
				)
			);

			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'process_subscription_cancellation - request' );
				error_log( print_r( $request, true ) );
				// phpcs:enable
			}

			// Send the request to Square API.
			$response = $this->client->subscriptions->cancel( $request );

			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'process_subscription_cancellation - response' );
				error_log( print_r( $response, true ) );
				// phpcs:enable
			}

			// Check if cancellation was successful by looking for errors.
			if ( $response->getErrors() === null || empty( $response->getErrors() ) ) {
				// No errors means success.
				return $response;
			} else {
				// Log errors.
				$this->add_response_errors_message( 'SUBSCRIPTION_CANCELLATION' );
				return false;
			}
		} catch ( \Exception $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: subscription was not cancelled.', 'charitable' );
			return false;
		}
	}

	/**
	 * Refund payment.
	 *
	 * @since 1.8.7
	 *
	 * @param string $payment_id Payment ID.
	 * @param array  $args       Payment data.
	 */
	public function refund_payment( string $payment_id, array $args ) {

		try {
			$request = new RefundPaymentRequest(
				array(
					'idempotencyKey' => $this->get_idempotency_key(),
					'paymentId'      => $payment_id,
					'amountMoney'    => ( new Money() )->setAmount( $args['amount'] )->setCurrency( $args['currency'] ),
					'reason'         => $args['reason'],
				)
			);

			$this->response = $this->client->refunds->refundPayment( $request );

			if ( $this->response->getErrors() ) {
				$this->errors[] = esc_html__( 'Square fail: refund was not processed.', 'charitable' );
				return false;
			}

			return $this->response->getRefund();

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: refund was not processed.', 'charitable' );
			return false;
		}
	}


	/**
	 * Prepare and retrieve an address object.
	 *
	 * @since 1.8.7
	 *
	 * @param array $data Address data.
	 *
	 * @return Address
	 */
	private function get_address_object( array $data ): Address {

		$address = new Address();

		// The empty country value may occur API errors.
		if ( ! empty( $data['address']['country'] ) ) {
			$address->setAddressLine1( $data['address']['address1'] );
			$address->setAddressLine2( $data['address']['address2'] );
			$address->setLocality( $data['address']['city'] );
			$address->setAdministrativeDistrictLevel1( $data['address']['state'] );
			$address->setPostalCode( $data['address']['postal'] );
			$address->setCountry( $data['address']['country'] );
		}

		if ( ! empty( $data['first_name'] ) ) {
			$address->setFirstName( $data['first_name'] );
		}

		if ( ! empty( $data['last_name'] ) ) {
			$address->setLastName( $data['last_name'] );
		}

		return $address;
	}


	/**
	 * Retrieve information of all locations of a business.
	 *
	 * @since 1.8.7
	 *
	 * @return array|Location|null
	 */
	public function get_locations() {

		try {
			$this->response = $this->client->locations->list();

			if ( $this->response->getErrors() ) {
				if ( charitable_is_debug( 'square' ) ) {
					// phpcs:disable
					error_log( 'get_locations - errors' );
					error_log( $this->response->getErrors() );
					// phpcs:enable
				}
				return null;
			}

			return $this->response->getLocations();

		} catch ( \Square\Exceptions\SquareApiException $e ) {

			$this->exception = $e;
			return null;
		}
	}

	/**
	 * Return the Square business location ID
	 *
	 * @since 1.8.7
	 *
	 * @param  string $mode The test mode... either 'test' or 'live'.
	 * @return string|false
	 */
	public function get_location_id( $mode = 'test' ) {
		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'get_location_id - the mode is: ' . $mode );
			// phpcs:enable
		}

		if ( 'test' !== $mode && 'live' !== $mode ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'get_location_id - bad mode' . $mode );
				// phpcs:enable
			}
			return false;
		}

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'get_location_id - checking if square_legacy_settings is set' );
			// phpcs:enable
		}

		// Are we overridng with legacy settings?
		if ( charitable_square_legacy_mode() ) {
			$location_id = charitable_get_option( 'gateways_square', $mode . '_location_id' );
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'get_location_id - square_legacy_settings is set to true, so the location id is' );
				error_log( print_r( $location_id, true ) );
				// phpcs:enable
			}
			return $location_id;
		}

		$location_id = get_option( 'charitable_square_location_id_' . $mode );

		if ( empty( $location_id ) ) {
			return false;
		}

		return $location_id;
	}

	/**
	 * Set the location ID for a specific mode.
	 *
	 * @since 1.8.7
	 *
	 * @param string $location_id The location ID to set.
	 * @param string $mode        The mode to set the location ID for (test or live).
	 * @return bool True on success.
	 */
	public function set_location_id( $location_id = '', $mode = 'test' ) {

		update_option( 'charitable_square_location_id_' . $mode, $location_id );

		return true;
	}

	/**
	 * Set the location currency for a specific mode.
	 *
	 * @since 1.8.7
	 *
	 * @param string $currency The currency code to set.
	 * @param string $mode     The mode to set the currency for (test or live).
	 * @return bool True on success.
	 */
	public function set_location_currency( $currency = '', $mode = 'test' ) {
		update_option( 'charitable_square_location_currency_' . $mode, $currency );
		return true;
	}

	/**
	 * Retrieve last API call response and display error messages.
	 *
	 * @since 1.14.0
	 *
	 * @param string $type Type of error message.
	 */
	private function add_response_errors_message( string $type = 'API' ) {

		$errors = $this->get_response_errors();

		if ( empty( $errors ) ) {
			return;
		}

		$key = ( $type === 'Exception' ) ? 'message' : 'detail';

		foreach ( $errors as $error ) {
			$message = $error[ $key ] ?? '';

			if ( empty( $error['code'] ) || empty( $message ) ) {
				return;
			}

			$this->errors[] = esc_html( $type ) . ': (' . esc_html( $error['code'] ) . ')  ' . esc_html( $message );
		}
	}

	/**
	 * Retrieve API errors.
	 *
	 * @since 1.8.7
	 *
	 * @return array|null
	 */
	public function get_errors() {

		return $this->errors;
	}

	/**
	 * Retrieve last API call errors if are exist.
	 *
	 * @since 1.8.7
	 *
	 * @return array
	 */
	public function get_response_errors(): array {

		if ( $this->response instanceof \Square\Legacy\Http\ApiResponse && ! $this->response->isSuccess() ) {
			$errors = array();

			foreach ( (array) $this->response->getErrors() as $error ) {
				$errors[] = $error->jsonSerialize();
			}

			return $errors;
		}

		if ( $this->exception instanceof ApiException ) {
			return array(
				'code'    => $this->exception->getCode(),
				'message' => $this->exception->getMessage(),
			);
		}

		return array();
	}

	/**
	 * Retrieve an idempotency key.
	 *
	 * @since 1.8.7
	 *
	 * @link https://developer.squareup.com/docs/working-with-apis/idempotency
	 *
	 * @return string
	 */
	private function get_idempotency_key(): string {

		return uniqid( '', false );
	}

	/**
	 * Delete transients by mode.
	 *
	 * @since 1.8.7
	 *
	 * @param string $mode Square mode.
	 */
	public static function detete_transients( string $mode ) {

		delete_transient( 'charitable_square_account_' . $mode );
		delete_transient( 'charitable_square_active_locations_' . $mode );
	}

	/**
	 * Return the API key to use for the current mode.
	 * This function might be deprecated in the future.
	 *
	 * @since 1.8.7
	 * @param string $type live|test.
	 *
	 * @return string|false
	 */
	public function get_access_token( $type = 'test' ) {

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'starting to get the access token' );
			// phpcs:enable
		}

		if ( 'test' !== $type && 'live' !== $type ) {
			return false;
		}

		$settings = charitable_get_option( 'gateways_square' );

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'got charitablesquare settings, now get the access token' );
			// phpcs:enable
		}

		if ( empty( $settings ) ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'get_access_token - settings are empty' );
				// phpcs:enable
			}
			return false;
		}

		if ( empty( $settings[ $type ]['access_token'] ) ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'get_access_token - access token is empty' );
				// phpcs:enable
			}
			return false;
		}

		$access_token = trim( $settings[ $type ]['access_token'] );

		if ( empty( $access_token ) ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'get_access_token - access token is empty' );
				// phpcs:enable
			}
			return false;
		}

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'get_access_token - got the access token and it is ' . $access_token );
			// phpcs:enable
		}

		return $access_token;
	}

	/**
	 * Checks whether an Access Token is set.
	 *
	 * @since 1.8.7
	 *
	 * @return boolean
	 */
	public function has_valid_access_token() {
		$token_type = $this->test_mode ? 'test' : 'live';

		/* We're missing an Access Token. */
		if ( false === $this->{$token_type . '_access_token'} ) {
			return false;
		}

		/**
		 * If we have made an API call, we record whether there
		 * was an issue with using the Access Token. This will catch
		 * incorrect or invalid Access Tokens.
		 */
		if ( isset( $this->access_token_validated ) ) {
			return $this->access_token_validated;
		}

		return true;
	}

	/**
	 * Return the response to the most recent request.
	 *
	 * @since 1.8.7
	 *
	 * @return WP_Error|?
	 */
	public function get_last_response() {
		return $this->last_response;
	}

	/**
	 * Checks whether the API key is valid, based on API call response.
	 *
	 * @since 1.8.7
	 *
	 * @return boolean
	 */
	private function access_token_validated() {
		return '401' !== wp_remote_retrieve_response_code( $this->last_response );
	}

	/**
	 * Returns whether the most recent request failed.
	 *
	 * @since  1.8.7
	 *
	 * @return boolean
	 */
	private function is_failed_request() {
		return is_wp_error( $this->last_response ) || '2' !== substr( wp_remote_retrieve_response_code( $this->last_response ), 0, 1 );
	}

	/**
	 * Renew the access token using the refresh token.
	 *
	 * @since 1.8.7
	 *
	 * @param string $refresh_token The refresh token to use for renewal.
	 * @return array|false The response from Square API or false on failure.
	 */
	public function renew_token( $refresh_token ) {
		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'check for removal - starting renew_token' );
			// phpcs:enable
		}

		if ( empty( $refresh_token ) ) {
			return false;
		}

		$oauth_url = $this->test_mode ? 'https://connect.squareupsandbox.com/oauth2/token' : 'https://connect.squareup.com/oauth2/token';

		$args = array(
			'grant_type'    => 'refresh_token',
			'refresh_token' => $refresh_token,
		);

		$request_args = array(
			'method'      => 'POST',
			'timeout'     => 30,
			'redirection' => 5,
			'httpversion' => '1.0',
			'blocking'    => true,
			'headers'     => array(
				'Content-Type' => 'application/json',
			),
			'body'        => wp_json_encode( $args ),
		);

		$response = wp_remote_post( $oauth_url, $request_args );

		if ( charitable_is_debug( 'square' ) ) {
			// phpcs:disable
			error_log( 'renew_token - response' );
			error_log( print_r( $response, true ) );
			// phpcs:enable
		}

		if ( is_wp_error( $response ) ) {
			return false;
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( empty( $data ) || ! isset( $data['access_token'] ) ) {
			return false;
		}

		return $data;
	}

	/**
	 * Check the status of the access token.
	 *
	 * @since 1.8.7
	 *
	 * @return array {
	 *     Token status information.
	 *
	 *     @type bool   $is_valid Whether the token is valid.
	 *     @type string $message  Error message if token is invalid.
	 * }
	 */
	public function check_token_status() {
		try {
			$response = $this->client->oAuth->retrieveTokenStatus();

			if ( empty( $response->getErrors() ) ) {
				return array(
					'is_valid' => true,
					'message'  => '',
				);
			}

			$errors = $response->getErrors();
			$error  = reset( $errors );

			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Square token status check error: ' . print_r( $error, true ) );
				// phpcs:enable
			}

			return array(
				'is_valid' => false,
				'message'  => $error->getDetail() ?? __( 'Your Square connection has expired.', 'charitable' ),
			);

		} catch ( \Exception $e ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Square token status check exception: ' . $e->getMessage() );
				// phpcs:enable
			}

			return array(
				'is_valid' => false,
				'message'  => $e->getMessage(),
			);
		}
	}

	/**
	 * Revoke an access token.
	 *
	 * @param string $client_id The client ID.
	 * @param string $access_token The access token to revoke.
	 * @param string $merchant_id The merchant ID.
	 * @return array{success: bool, error?: string}
	 */
	public function revoke_token( string $client_id, string $access_token, string $merchant_id ): array {
		try {
			$request = new \Square\Models\RevokeTokenRequest(
				array(
					'clientId'    => $client_id,
					'accessToken' => $access_token,
					'merchantId'  => $merchant_id,
				)
			);

			// Get the application secret from settings.
			$settings           = charitable_get_option( 'square_settings', array() );
			$application_secret = isset( $settings['application_secret'] ) ? $settings['application_secret'] : '';

			if ( empty( $application_secret ) ) {
				return array(
					'success' => false,
					'error'   => __( 'Square application secret is not configured.', 'charitable' ),
				);
			}

			// Set the authorization header with the application secret.
			$options = array(
				'headers' => array(
					'Authorization' => 'Client ' . $application_secret,
				),
			);

			$response = $this->client->getOAuthApi()->revokeToken( $request, $options );

			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Square token revocation response: ' . print_r( $response, true ) );
				// phpcs:enable
			}

			return array( 'success' => true );
		} catch ( \Exception $e ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Square token revocation error: ' . $e->getMessage() );
				// phpcs:enable
			}

			return array(
				'success' => false,
				'error'   => $e->getMessage(),
			);
		}
	}

	/**
	 * Get merchant details from Square API.
	 *
	 * @since 1.8.7
	 *
	 * @return string|null The merchant name or null if not found.
	 */
	public function get_merchant_name() {
		// Get merchant ID from instance property or settings.
		$merchant_id = $this->merchant_id;

		if ( empty( $merchant_id ) ) {
			$square_settings = charitable_get_option( 'gateways_square', array() );
			$mode            = $this->test_mode ? 'test' : 'live';
			$merchant_id     = ! empty( $square_settings[ $mode ]['merchant_id'] ) ? $square_settings[ $mode ]['merchant_id'] : '';
		}

		if ( empty( $merchant_id ) ) {
			return null;
		}

		$merchant_name = get_transient( 'charitable_square_merchant_name_' . $merchant_id );

		if ( false !== $merchant_name ) {
			return $merchant_name;
		}

		try {
			$request  = new \Square\Merchants\Requests\GetMerchantsRequest(
				array(
					'merchantId' => $merchant_id,
				)
			);
			$response = $this->client->merchants->get( $request );

			if ( $response && $response->getMerchant() ) {
				$merchant      = $response->getMerchant();
				$merchant_name = $merchant->getBusinessName() ? $merchant->getBusinessName() : $merchant->getMainLocationName();

				if ( $merchant_name ) {
					set_transient( 'charitable_square_merchant_name_' . $merchant_id, $merchant_name, DAY_IN_SECONDS );
					return $merchant_name;
				}
			}
		} catch ( \Exception $e ) {
			if ( charitable_is_debug( 'square' ) ) {
				// phpcs:disable
				error_log( 'Square API Error: ' . $e->getMessage() );
				// phpcs:enable
			}
		}

		return null;
	}
}
