<?php

namespace WPForms\Integrations\Square\Api;

use stdClass;
use WPForms\Integrations\Square\Helpers;
use WPForms\Vendor\Square\Models\Invoice;
use WPForms\Vendor\Square\Exceptions\ApiException;
use WPForms\Vendor\Square\Http\ApiResponse;
use WPForms\Vendor\Square\Models\Address;
use WPForms\Vendor\Square\Models\Card;
use WPForms\Vendor\Square\Models\CatalogObject;
use WPForms\Vendor\Square\Models\CatalogObjectType;
use WPForms\Vendor\Square\Models\CatalogQuery;
use WPForms\Vendor\Square\Models\CatalogQueryExact;
use WPForms\Vendor\Square\Models\CatalogSubscriptionPlan;
use WPForms\Vendor\Square\Models\UpdateSubscriptionResponse;
use WPForms\Vendor\Square\Models\CatalogSubscriptionPlanVariation;
use WPForms\Vendor\Square\Models\CreateCardRequest;
use WPForms\Vendor\Square\Models\CreateCustomerRequest;
use WPForms\Vendor\Square\Models\CreateOrderRequest;
use WPForms\Vendor\Square\Models\CreatePaymentRequest;
use WPForms\Vendor\Square\Models\CreatePaymentResponse;
use WPForms\Vendor\Square\Models\CreateSubscriptionRequest;
use WPForms\Vendor\Square\Models\CreateSubscriptionResponse;
use WPForms\Vendor\Square\Models\Money;
use WPForms\Vendor\Square\Models\Order;
use WPForms\Vendor\Square\Models\OrderLineItem;
use WPForms\Vendor\Square\Models\OrderLineItemDiscount;
use WPForms\Vendor\Square\Models\OrderSource;
use WPForms\Vendor\Square\Models\OrderState;
use WPForms\Vendor\Square\Models\Payment;
use WPForms\Vendor\Square\Models\RefundPaymentRequest;
use WPForms\Vendor\Square\Models\SearchCatalogObjectsRequest;
use WPForms\Vendor\Square\Models\Subscription;
use WPForms\Vendor\Square\Models\SubscriptionPhase;
use WPForms\Vendor\Square\Models\SubscriptionPricing;
use WPForms\Vendor\Square\Models\SubscriptionSource;
use WPForms\Vendor\Square\Models\UpdateOrderRequest;
use WPForms\Vendor\Square\Models\UpdateSubscriptionRequest;
use WPForms\Vendor\Square\Models\UpsertCatalogObjectRequest;
use WPForms\Vendor\Square\Models\Customer;
use WPForms\Vendor\Square\SquareClient;
use WPForms\Integrations\Square\Connection;
use WPForms\Integrations\Square\Square;

/**
 * WPForms Square API class.
 *
 * @since 1.9.5
 */
class Api {

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

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

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

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

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

	/**
	 * Constructs the main Square API wrapper class.
	 *
	 * @since 1.9.5
	 *
	 * @param Connection $connection Connection object.
	 */
	public function __construct( $connection ) {

		$this->client = new SquareClient(
			[
				'accessToken' => $connection->get_access_token(),
				'environment' => $connection->get_mode(),
			]
		);
	}

	/**
	 * Set tokens from a submitted form data.
	 *
	 * @since 1.9.5
	 *
	 * @param array $entry Copy of original $_POST.
	 */
	public function set_payment_tokens( array $entry ) {

		if ( ! empty( $entry['square']['source_id'] ) ) {
			$this->source_id = $entry['square']['source_id'];
		}
	}

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

		$connection = Connection::get();

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

			return;
		}

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

			return;
		}

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

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

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

	/**
	 * Check if all required general arguments are present.
	 *
	 * @since 1.9.5
	 *
	 * @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.', 'wpforms-lite' );
		}

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

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

	/**
	 * Check if all required single payment arguments are present.
	 *
	 * @since 1.9.5
	 *
	 * @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.', 'wpforms-lite' );
		}
	}

	/**
	 * Process single transaction.
	 *
	 * @since 1.9.5
	 *
	 * @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 ) {
			return;
		}

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

		/**
		 * Fire when a single transaction is performed.
		 *
		 * @since 1.9.5
		 *
		 * @param array $result Single transaction result.
		 * @param array $args   Payment arguments.
		 * @param Api   $api    Api class instance.
		 */
		do_action( 'wpforms_square_api_process_single_transaction_after', $result, $args, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Process subscription transaction.
	 *
	 * @since 1.9.5
	 *
	 * @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;
		}

		$this->perform_subscription_transaction( $args );
	}

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

		try {
			$request = new RefundPaymentRequest( $this->get_idempotency_key(), new Money() );

			$request->setPaymentId( $payment_id );
			$request->setReason( $args['reason'] );
			$request->getAmountMoney()->setAmount( $args['amount'] );
			$request->getAmountMoney()->setCurrency( $args['currency'] );

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

			if ( ! $this->response->isSuccess() ) {
				return false;
			}
		} catch ( ApiException $e ) {
			$this->exception = $e;

			return false;
		}

		return true;
	}

	/**
	 * Update subscription.
	 *
	 * @since 1.9.5
	 *
	 * @param array $args Subscription arguments.
	 */
	public function update_subscription( array $args ) {

		$subscription = $this->retrieve_subscription( $args['id'] );

		if ( ! $subscription instanceof Subscription ) {
			return;
		}

		$request = $this->get_update_subscription_request_object( $subscription, $args );

		$this->send_update_subscription_request( $args['id'], $request );
	}

	/**
	 * Cancel subscription.
	 *
	 * @since 1.9.5
	 *
	 * @param string $subscription_id Subscription id.
	 *
	 * @return bool
	 */
	public function cancel_subscription( string $subscription_id ): bool {

		try {
			$this->response = $this->client->getSubscriptionsApi()->cancelSubscription( $subscription_id );

			if ( ! $this->response->isSuccess() ) {
				return false;
			}
		} catch ( ApiException $e ) {
			$this->exception = $e;

			return false;
		}

		return true;
	}

	/**
	 * Retrieve a Card object.
	 *
	 * @since 1.9.5
	 *
	 * @param Subscription $subscription Subscription object.
	 *
	 * @return Card|null
	 */
	public function get_subscription_card( $subscription ) {

		if ( ! $subscription instanceof Subscription ) {
			return null;
		}

		$card_id = $subscription->getCardId();

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

		// Get a customer.
		$card = $this->send_retrieve_card_request( $card_id );

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

		return $card;
	}

	/**
	 * Send a retrieve subscription request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param string $id The ID of the subscription to retrieve.
	 *
	 * @return Subscription|null
	 */
	public function retrieve_subscription( string $id ) {

		try {
			$response = $this->client->getSubscriptionsApi()->retrieveSubscription( $id );

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

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

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

	/**
	 * Retrieve a Card object.
	 *
	 * @since 1.9.5
	 *
	 * @param Subscription $subscription Subscription object.
	 *
	 * @return Invoice|null
	 */
	public function get_latest_subscription_invoice( $subscription ) {

		if ( ! $subscription instanceof Subscription ) {
			return null;
		}

		try {
			$invoices = $subscription->getInvoiceIds();

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

			// Get a customer.
			$response = $this->client->getInvoicesApi()->getInvoice( reset( $invoices ) ); // Get the latest invoice by using the first ID in the array as the subscription's invoice IDs are sorted by date in ascending order.

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

			return $response->getResult()->getInvoice();
		} catch ( ApiException $e ) {
			return null;
		}
	}

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

		$result = [];

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

		if ( ! $order instanceof Order ) {
			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 );

		// 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 ? [ 'order' => $updated_order ] : $result;
		}

		$result['payment'] = $payment;

		return $result;
	}

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

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

		/**
		 * Filter a create order request object.
		 *
		 * @since 1.9.5
		 *
		 * @param CreateOrderRequest $request Create order request object.
		 * @param array              $args    Payment arguments.
		 * @param Api                $api     Api class instance.
		 */
		return apply_filters( 'wpforms_square_api_prepare_create_order_request', $request, $args, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Prepare an update order request object for sending to API.
	 *
	 * @since 1.9.5
	 *
	 * @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.9.5
		 *
		 * @param UpdateOrderRequest $request Update order request object.
		 * @param Order              $order   Order object.
		 * @param Api                $api     Api class instance.
		 */
		return apply_filters( 'wpforms_square_api_prepare_update_order_request', $request, $order, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Prepare a create payment request object for sending to API.
	 *
	 * @since 1.9.5
	 *
	 * @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.9.5
		 *
		 * @param CreateOrderRequest $request Create payment request object.
		 * @param array              $args    Payment arguments.
		 * @param Api                $api     Api class instance.
		 */
		return apply_filters( 'wpforms_square_api_prepare_create_payment_request', $request, $args, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

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

		$request = new CreateOrderRequest();

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

		$request->setOrder( new Order( $args['location_id'] ) );
		$request->getOrder()->setSource( new OrderSource() );
		$request->getOrder()->getSource()->setName( Square::APP_NAME );

		return $request;
	}

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

		$request = new UpdateOrderRequest();

		$request->setIdempotencyKey( $this->get_idempotency_key() );
		$request->setOrder( $order );
		$request->getOrder()->setState( OrderState::CANCELED );

		return $request;
	}

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

		$request = new CreatePaymentRequest( $this->source_id, $this->get_idempotency_key() );

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

		return $request;
	}

	/**
	 * Send a create order request object to API.
	 *
	 * @since 1.9.5
	 *
	 * @param CreateOrderRequest $request Create order request object.
	 *
	 * @return Order|null
	 */
	private function send_create_order_request( $request ) {

		try {
			$this->response = $this->client->getOrdersApi()->createOrder( $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: order was not created.', 'wpforms-lite' );

				return null;
			}

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

		} catch ( ApiException $e ) {
			$this->exception = $e;
			$this->errors[]  = esc_html__( 'Square fail: order was not created.', 'wpforms-lite' );

			return null;
		}
	}

	/**
	 * Send an update order request object to API.
	 *
	 * @since 1.9.5
	 *
	 * @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->client->getOrdersApi()->updateOrder( $order_id, $request );

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

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

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

	/**
	 * Send a create payment request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param CreatePaymentRequest $request Create payment request object.
	 *
	 * @return Payment|null
	 */
	private function send_create_payment_request( $request ) {

		try {
			$this->response = $this->client->getPaymentsApi()->createPayment( $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: payment was not processed.', 'wpforms-lite' );

				$this->add_response_errors_message();

				return null;
			}

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

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

			$this->add_response_errors_message( 'Exception' );

			return null;
		}
	}

	/**
	 * Set line items to a create order request object.
	 *
	 * @since 1.9.5
	 *
	 * @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 = [];

		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;
	}

	/**
	 * Set discounts to a create order request object.
	 *
	 * @since 1.9.5
	 *
	 * @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 = [];

		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.9.5
	 *
	 * @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 ( ! Helpers::is_license_ok() && 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 single order line item object.
	 *
	 * @since 1.9.5
	 *
	 * @param array $item Order item data.
	 * @param array $args Payment arguments.
	 *
	 * @return OrderLineItem
	 */
	private function get_order_line_item_object( array $item, array $args ): OrderLineItem {

		$order_line_items = new OrderLineItem( $item['quantity'] );

		$order_line_items->setName( $item['name'] );
		$order_line_items->setBasePriceMoney( new Money() );
		// Round to the nearest whole number because $item['amount'] can contain a number close to,
		// but slightly under it, due to how it is stored in the memory.
		$order_line_items->getBasePriceMoney()->setAmount( round( $item['amount'] ) );
		$order_line_items->getBasePriceMoney()->setCurrency( $args['currency'] );

		return $order_line_items;
	}

	/**
	 * Retrieve a single order discount object.
	 *
	 * @since 1.9.5
	 *
	 * @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;
	}

	/**
	 * Prepare and retrieve an address object.
	 *
	 * @since 1.9.5
	 *
	 * @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.9.5
	 *
	 * @return array|Location|null
	 */
	public function get_locations() {

		try {
			$this->response = $this->client->getLocationsApi()->listLocations();

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

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

		} catch ( ApiException $e ) {
			$this->exception = $e;

			return null;
		}
	}

	/**
	 * Retrieve a Merchant object with details.
	 *
	 * @since 1.9.5
	 *
	 * @param string $id The Merchant ID.
	 *
	 * @return Merchant|null
	 */
	public function get_merchant( string $id ) {

		try {
			$this->response = $this->client->getMerchantsApi()->retrieveMerchant( $id );

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

			return $this->response->getResult()->getMerchant();

		} catch ( ApiException $e ) {
			$this->exception = $e;

			return null;
		}
	}

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

		return uniqid( '', false );
	}

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

		return $this->errors;
	}

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

		if ( $this->response instanceof ApiResponse && ! $this->response->isSuccess() ) {
			$errors = [];

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

			return $errors;
		}

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

		return [];
	}

	/**
	 * Retrieve last API call response resource.
	 *
	 * @since 1.9.5
	 *
	 * @return array
	 */
	public function get_response_resource(): array {

		if ( ! $this->response instanceof ApiResponse ) {
			return [];
		}

		$result = $this->response->getResult();

		if ( $result instanceof CreatePaymentResponse ) {
			return [ 'payment' => $this->response->getResult()->getPayment() ];
		}

		if ( $result instanceof CreateSubscriptionResponse || $result instanceof UpdateSubscriptionResponse ) {
			return [ 'subscription' => $this->response->getResult()->getSubscription() ];
		}

		return [];
	}

	/**
	 * 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 );
		}
	}

	/**
	 * Perform a subscription transaction.
	 *
	 * @since 1.9.5
	 *
	 * @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->getId(), $customer->getId(), $card->getId(), $args );

		$this->send_create_subscription_request( $subscription_request );
	}

	/**
	 * Check if all required subscription arguments are present.
	 *
	 * @since 1.9.5
	 *
	 * @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.', 'wpforms-lite' );
		}

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

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

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

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

	/**
	 * Prepare a create customer request object for sending to API.
	 *
	 * @since 1.9.5
	 *
	 * @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.9.5
	 *
	 * @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( $this->get_idempotency_key(), $this->source_id, 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.9.5
	 *
	 * @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 ): CreateSubscriptionRequest {

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

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

		return $request;
	}

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

		$request = new CreateCustomerRequest();

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

		return $request;
	}

	/**
	 * Retrieve a search catalog request object.
	 *
	 * @since 1.9.5
	 *
	 * @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( [ $type ] );
		$request->setLimit( 1 );
		$request->setQuery( new CatalogQuery() );
		$request->getQuery()->setExactQuery( new CatalogQueryExact( 'name', $name ) );

		return $request;
	}

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

		$request = new UpsertCatalogObjectRequest( $this->get_idempotency_key(), new CatalogObject( CatalogObjectType::SUBSCRIPTION_PLAN, '#plan' ) );

		$plan_data = new CatalogSubscriptionPlan( $args['subscription']['plan_name'] );

		$plan_data->setAllItems( true );

		$request->getObject()->setSubscriptionPlanData( $plan_data );

		return $request;
	}

	/**
	 * Get subscription plan variation.
	 *
	 * @since 1.9.5
	 *
	 * @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( CatalogObjectType::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 !== null ) {
			return $plan_variation;
		}

		$plan_variations_request = $this->get_create_plan_variations_request_object( $args, $plan );
		$plan_variation          = $this->send_create_plan_variations_request( $plan_variations_request );

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

		return $plan_variation;
	}

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

		$request = new UpsertCatalogObjectRequest( $this->get_idempotency_key(), new CatalogObject( CatalogObjectType::SUBSCRIPTION_PLAN_VARIATION, '#plan_variation' ) );

		$phase = new SubscriptionPhase( $args['subscription']['phase_cadence']['value'] );

		$phase->setPricing( new SubscriptionPricing() );
		$phase->getPricing()->setType( 'STATIC' );
		$phase->getPricing()->setPriceMoney( new Money() );
		$phase->getPricing()->getPriceMoney()->setAmount( $args['amount'] );
		$phase->getPricing()->getPriceMoney()->setCurrency( $args['currency'] );

		$request->getObject()->setSubscriptionPlanVariationData( new CatalogSubscriptionPlanVariation( $args['subscription']['plan_variation_name'], [ $phase ] ) );
		$request->getObject()->getSubscriptionPlanVariationData()->setSubscriptionPlanId( $plan->getId() );

		return $request;
	}

	/**
	 * Retrieve a create subscription request object.
	 *
	 * @since 1.9.5
	 *
	 * @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( $args['location_id'], $customer_id );

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

		return $request;
	}

	/**
	 * Send a create customer request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param CreateCustomerRequest $request Create customer request object.
	 *
	 * @return Customer|null
	 */
	private function send_create_customer_request( $request ) {

		try {
			$this->response = $this->client->getCustomersApi()->createCustomer( $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: customer was not created.', 'wpforms-lite' );

				return null;
			}

			return $this->response->getResult()->getCustomer();

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

			return null;
		}
	}

	/**
	 * Send a retrieve card request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param string $id The ID of the customer to retrieve.
	 *
	 * @return Card|null
	 */
	private function send_retrieve_card_request( string $id ) {

		try {
			$response = $this->client->getCardsApi()->retrieveCard( $id );

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

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

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

	/**
	 * Send a create customer card request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param CreateCardRequest $request Create card request object.
	 *
	 * @return Card|null
	 */
	private function send_create_customer_card_request( $request ) {

		try {
			$this->response = $this->client->getCardsApi()->createCard( $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: customer card was not created.', 'wpforms-lite' );

				return null;
			}

			return $this->response->getResult()->getCard();

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

			return null;
		}
	}

	/**
	 * Send a search catalog request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param SearchCatalogObjectsRequest $request Search subscription plan request object.
	 *
	 * @return CatalogObject|null
	 */
	private function send_search_catalog_request( $request ) {

		try {
			$this->response = $this->client->getCatalogApi()->searchCatalogObjects( $request );

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

			$objects = $this->response->getResult()->getObjects();

			if ( ! is_array( $objects ) || empty( $objects[0] ) ) {
				return null;
			}

			return $objects[0];

		} catch ( ApiException $e ) {
			$this->exception = $e;

			return null;
		}
	}

	/**
	 * Get subscription plan.
	 *
	 * @since 1.9.5
	 *
	 * @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( CatalogObjectType::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 === null ) {
			$create_plan_request = $this->get_create_plan_request_object( $args );
			$plan                = $this->send_create_plan_request( $create_plan_request );
		}

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

		return $plan;
	}

	/**
	 * Send a create subscription plan request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param UpsertCatalogObjectRequest $request Create subscription plan request object.
	 *
	 * @return CatalogObject|null
	 */
	private function send_create_plan_request( $request ) {

		try {
			$this->response = $this->client->getCatalogApi()->upsertCatalogObject( $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: subscription plan was not created.', 'wpforms-lite' );

				return null;
			}

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

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

			return null;
		}
	}

	/**
	 * Send a create subscription plan variations request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param UpsertCatalogObjectRequest $request Create subscription plan request object.
	 *
	 * @return CatalogObject|null
	 */
	private function send_create_plan_variations_request( $request ) {

		try {
			$this->response = $this->client->getCatalogApi()->upsertCatalogObject( $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: subscription plan variation was not created.', 'wpforms-lite' );

				return null;
			}

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

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

			return null;
		}
	}

	/**
	 * Send a create subscription request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param CreateSubscriptionRequest $request Create subscription request object.
	 *
	 * @return Subscription|null
	 */
	private function send_create_subscription_request( $request ) {

		try {
			$this->response = $this->client->getSubscriptionsApi()->createSubscription( $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: something went wrong during subscription process.', 'wpforms-lite' );

				return null;
			}

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

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

			return null;
		}
	}

	/**
	 * Retrieve a update subscription request object.
	 *
	 * @since 1.9.5
	 *
	 * @param Subscription $subscription Subscription object.
	 * @param array        $args         Subscription arguments.
	 *
	 * @return UpdateSubscriptionRequest|null
	 */
	private function get_update_subscription_request_object( $subscription, array $args ) {

		if ( ! $subscription instanceof Subscription ) {
			return null;
		}

		$subscription->setSource( new SubscriptionSource() );
		$subscription->getSource()->setName( Square::APP_NAME . ' Payment #' . $args['payment_id'] );

		$request = new UpdateSubscriptionRequest();

		$request->setSubscription( $subscription );

		return $request;
	}

	/**
	 * Send a create subscription request to API.
	 *
	 * @since 1.9.5
	 *
	 * @param string                    $subscription_id Subscription id.
	 * @param UpdateSubscriptionRequest $request         Update subscription request object.
	 *
	 * @return Subscription|null
	 */
	private function send_update_subscription_request( string $subscription_id, $request ) {

		try {
			$this->response = $this->client->getSubscriptionsApi()->updateSubscription( $subscription_id, $request );

			if ( ! $this->response->isSuccess() ) {
				$this->errors[] = esc_html__( 'Square fail: something went wrong during subscription update.', 'wpforms-lite' );

				return null;
			}

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

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

			return null;
		}
	}

	/**
	 * Retrieve card details from specific transaction in object format.
	 *
	 * @since 1.9.5
	 *
	 * @param string $transaction_id The ID of the order to retrieve card details from.
	 *
	 * @return stdClass|void
	 */
	public function get_card_details_from_transaction_id( string $transaction_id ) {

		$response = $this->client->getPaymentsApi()->getPayment( $transaction_id );

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

		$payment      = $response->getResult()->getPayment();
		$card_details = $payment->getCardDetails();

		if ( ! $card_details ) {
			return;
		}

		$card = $card_details->getCard();

		if ( ! $card ) {
			return;
		}

		// Create a temporary object to mimic Square's payment_method_details structure.
		$details                                 = new stdClass();
		$details->source_type                    = 'card';
		$details->card_details                   = new stdClass();
		$details->card_details->card             = new stdClass();
		$details->card_details->card->last_4     = $card->getLast4();
		$details->card_details->card->card_brand = $card->getCardBrand();
		$details->card_details->card->exp_month  = $card->getExpMonth();
		$details->card_details->card->exp_year   = $card->getExpYear();

		return $details;
	}

	/**
	 * Retrieve the latest invoice transaction_id.
	 *
	 * @since 1.9.5
	 *
	 * @param Invoice $invoice Invoice object.
	 *
	 * @return string
	 */
	public function get_latest_invoice_transaction_id( $invoice ): string {

		$order_id = $invoice->getOrderId();

		if ( ! $order_id ) {
			return '';
		}

		$order_response = $this->client->getOrdersApi()->retrieveOrder( $order_id );

		if ( ! $order_response->isSuccess() ) {
			return '';
		}

		$order   = $order_response->getResult()->getOrder();
		$tenders = $order->getTenders();

		if ( empty( $tenders ) ) {
			return '';
		}

		// Assuming the last tender represents the final transaction.
		return end( $tenders )->getPaymentId();
	}

	/**
	 * Retrieve the invoice by order ID.
	 *
	 * @since 1.9.5
	 *
	 * @param string $order_id Order ID.
	 *
	 * @return Invoice|null
	 */
	public function get_invoice_by_order_id( string $order_id ) {

		$invoices_api = $this->client->getInvoicesApi();

		try {
			$response = $invoices_api->listInvoices( Helpers::get_location_id() );

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

			$invoices = $response->getResult()->getInvoices();

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

			foreach ( $invoices as $invoice ) {
				if ( $invoice->getOrderId() === $order_id ) {
					return $invoice;
				}
			}
		} catch ( ApiException $e ) {
			return null;
		}

		return null;
	}
}
