<?php

namespace MPHB\Payments\Gateways;

use MPHB\Admin\Fields\FieldFactory;
use MPHB\Admin\Groups\SettingsGroup;
use MPHB\Core\StringEncryptHelper;
use MPHB\Entities\{ AuthorizedFunds, Booking, Payment };
use MPHB\Payments\Gateways\Stripe\{ StripeAPI, WebhookListener };
use MPHB\PostTypes\PaymentCPT\Statuses as PaymentStatuses;
use MPHB\Shortcodes\CheckoutShortcode;
use MPHB\Stripe\Exception\ApiErrorException;

/**
 * New code uses ConfirmationToken to confirm payment on the frontend.
 * https://docs.stripe.com/payments/finalize-payments-on-the-server?platform=web&type=payment
 *
 * How to test:
 * - https://docs.stripe.com/testing
 * - https://docs.stripe.com/payments/payment-intents/upgrade-to-handle-actions?platform=web#test-manual
 */
class StripeGateway extends Gateway {
	public const GATEWAY_ID = 'stripe';

	// https://docs.stripe.com/payments/klarna
	private const KLARNA_ALLOWED_CURRENCIES = array( 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'NOK', 'NZD', 'PLN', 'RON', 'SEK', 'USD' );

	protected ?StripeAPI $api = null;

	protected string $locale = 'auto';

	// See method parsePaymentFields()
	protected $paymentFields = array(
		'confirmation_token_id' => '',
		'payment_intent_id'     => '',
		'payment_method'        => 'payment', // Dynamic payments
		'redirect_url'          => '',
	);

	protected ?WebhookListener $webhookListener = null;

	public function __construct() {
		add_filter( 'mphb_gateway_has_instructions', array( $this, 'hideInstructions' ), 10, 2 );

		parent::__construct();

		add_action( 'init', array( $this, 'setupWebhooks' ), 9 );
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueueScripts' ) );

		// Show payment status after user returns from Stripe side. Must work
		// as well as webhook even if gateway is already not active.
		add_action( 'init', fn() => $this->hanglePaymentReceivedPageErrors() );
	}

	/**
	 * @throws \Exception if requests fail.
	 */
	public function captureAuthorizedFunds( Payment $payment ): bool {
		if ( ! $payment->hasPendingAuthedFunds() ) {
			return false;
		}

		$captureAmount = $payment->getAuthedFunds()->getAmount();
		$paymentIntent = $this->getApi()->retrievePaymentIntent( $payment->getTransactionId() );

		$isCaptured = (bool) $this->getApi()->capturePaymentIntent( $paymentIntent, $captureAmount );

		if ( $isCaptured ) {
			$payment->getAuthedFunds()->capture();

			MPHB()->getPaymentRepository()->save( $payment );
		}

		return $isCaptured;
	}

	/**
	 * @access private
	 */
	public function enqueueScripts() {
		if ( MPHB()->settings()->main()->isConfirmationUponPayment()
			&& $this->isActive()
			&& mphb_is_checkout_page()
		) {
			wp_enqueue_script( 'mphb-vendor-stripe-library' );
		}
	}

	/**
	 * @return strings
	 */
	public function getAdminDescription() {
		$adminDescription = parent::getAdminDescription();

		if ( ! is_null( $this->webhookListener ) ) {
			$adminDescription .= ' ' . sprintf(
				// Translators: %s: Webhooks destination URL.
				__( 'Webhooks Destination URL: %s', 'motopress-hotel-booking' ),
				'<code>' . esc_url( $this->webhookListener->getNotifyUrl() ) . '</code>'
			);

			$adminDescription = trim( $adminDescription );
		}

		return $adminDescription;
	}

	/**
	 * @return string[] Equal to getPaymentMethods() if the currency is euro,
	 *     ["card"] otherwise.
	 */
	public function getAllowedPaymentMethods(): array {
		$paymentMethods = $this->getPaymentMethods();
		$currency = MPHB()->settings()->currency()->getCurrencyCode();

		if ( $currency === 'EUR' ) {
			return ! empty( $paymentMethods ) ? $paymentMethods : array( 'card' );

		} elseif ( in_array( 'klarna', $paymentMethods ) && in_array( $currency, self::KLARNA_ALLOWED_CURRENCIES ) ) {
			return array( 'card', 'klarna' );

		} else {
			return array( 'card' );
		}
	}

	public function getApi(): StripeAPI {
		if ( $this->api === null ) {
			$this->api = new StripeAPI( $this );
		}

		return $this->api;
	}

	public function getCaptureMethod(): string {
		return $this->getOption( 'capture_method' );
	}

	/**
	 * @param Booking $booking
	 */
	public function getCheckoutData( $booking ) {
		$returnUrl = MPHB()->settings()->pages()->getReservationReceivedPageUrl(
			null,
			array(
				'mphb_action'         => 'handle_stripe_errors',
				'mphb_nonce'          => wp_create_nonce( 'handle_stripe_errors' ),
				'mphb_payment_status' => 'auto',
			)
		);

		// Put some basic customer info required for the StripeGateway.js
		// (important only for Payment Request and its checkout page)
		$customer = $booking->getCustomer();

		if ( ! is_null( $customer ) ) {
			$customerData = array(
				'email' => $customer->getEmail(),
				'name'  => $customer->getName(),
			);
		} else {
			$customerData = array();
		}

		$data = array(
			'amount'                  => $booking->calcDepositAmount(),
			'currency'                => MPHB()->settings()->currency()->getCurrencyCode(),
			'customer'                => $customerData,
			'defaultCountry'          => MPHB()->settings()->main()->getDefaultCountry(),
			'idempotencyKeyField'     => CheckoutShortcode::CHECKOUT_ID_FIELD,
			'isManualCapture'         => $this->isManualCapture(),
			'locale'                  => $this->locale,
			'paymentMethods'          => $this->getAllowedPaymentMethods(),
			'paymentMode'             => $this->getPaymentMode(),
			'paymentsConfigurationId' => $this->getPaymentsConfigurationId(),
			'publicKey'               => $this->getPublicKey(),
			'returnUrl'               => $returnUrl,
			// Styles for legacy methods:
			// - Docs: https://docs.stripe.com/js/elements_object/create_element?type=card#elements_create-options
			// - Example: https://github.com/stripe-archive/stripe-payments-demo/blob/master/public/javascripts/payments.js#L46
			'style'                   => apply_filters( 'mphb_stripe_elements_style', array( 'base' => array( 'fontSize' => '15px' ) ) ),
			'i18n'                    => array(
				// Payment methods (labels)
				'bancontact'       => __( 'Bancontact', 'motopress-hotel-booking' ),
				'card'             => __( 'Card', 'motopress-hotel-booking' ),
				'giropay'          => __( 'Giropay', 'motopress-hotel-booking' ),
				'ideal'            => __( 'iDEAL', 'motopress-hotel-booking' ),
				'klarna'           => __( 'Klarna', 'motopress-hotel-booking' ),
				'sepa_debit'       => __( 'SEPA Direct Debit', 'motopress-hotel-booking' ),

				// Additional labels
				'card_description' => __( 'Credit or debit card', 'motopress-hotel-booking' ),
				'iban'             => __( 'IBAN', 'motopress-hotel-booking' ),

				// Messages
				'iban_policy'      => __( 'By providing your IBAN and confirming this payment, you are authorizing this merchant and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.', 'motopress-hotel-booking' ), // From https://docs.stripe.com/sources/sepa-debit#prerequisite
				'redirect_notice'  => __( 'You will be redirected to a secure page to complete the payment.', 'motopress-hotel-booking' ),
			),
		);

		return array_merge( parent::getCheckoutData( $booking ), $data );
	}

	public function getCheckoutLocale(): string {
		return $this->locale;
	}

	/**
	 * @return string
	 */
	public function getDescription() {
		$description = parent::getDescription();

		if ( $this->isSandbox() ) {
			$description .= ' ' . sprintf(
				// Translators: 1: Test card number. 2: Test CVC.
				__( 'Use the card number %1$s with CVC %2$s, a valid expiration date and random 5-digit ZIP-code to test a payment.', 'motopress-hotel-booking' ),
				'4242424242424242',
				'123'
			);

			$description = trim( $description );
		}

		return $description;
	}

	/**
	 * @return string[] "card", "ideal", "sepa_debit" etc.
	 */
	public function getPaymentMethods(): array {
		$paymentMethods = $this->getOption( 'payment_methods' );

		if ( ! is_array( $paymentMethods ) ) {
			$paymentMethods = array( 'card' );
		} elseif ( ! in_array( 'card', $paymentMethods ) ) {
			// Add "card" to payment methods
			$paymentMethods = array_merge( array( 'card' ), $paymentMethods );
		}

		if ( $this->isManualCapture() ) {
			// https://docs.stripe.com/payments/payment-methods/payment-method-support#additional-api-supportability
			$paymentMethods = array_intersect( $paymentMethods, array( 'card', 'klarna' ) );

			// Reset keys, otherwise JS will throw an error
			$paymentMethods = array_values( $paymentMethods );
		}

		return $paymentMethods;
	}

	public function getPaymentMode(): string {
		if ( $this->getPaymentsConfigurationId() !== '' || empty( $this->getPaymentMethods() ) ) {
			return 'payment';
		} else {
			return 'legacy';
		}
	}

	public function getPaymentsConfigurationId(): string {
		return StringEncryptHelper::decryptString( $this->getOption( 'payments_configuration_id' ) );
	}

	protected function isPaymentFeeSupported(): bool {
		return true;
	}

	public function getPublicKey(): string {
		return StringEncryptHelper::decryptString( $this->getOption( 'public_key' ) );
	}

	public function getSecretKey(): string {
		return StringEncryptHelper::decryptString( $this->getOption( 'secret_key' ) );
	}

	public function getWebhookKey(): string {
		return StringEncryptHelper::decryptString( $this->getOption( 'webhook_key' ) );
	}

	/**
	 * @access private
	 *
	 * @param bool $show
	 *
	 * @since 3.6.1
	 */
	public function hideInstructions( $show, string $gatewayId ): bool {
		if ( $gatewayId == $this->id ) {
			$show = false;
		}

		return $show;
	}

	public function initPaymentFields() {
		$fields = array(
			'mphb_stripe_confirmation_token_id' => array(
				'type'     => 'hidden',
				'required' => false,
			),
			'mphb_stripe_payment_intent_id' => array(
				'type'     => 'hidden',
				'required' => false,
			),
			// "payment" or one of the legacy methods: "cart" etc.
			'mphb_stripe_payment_method' => array(
				'type'     => 'hidden',
				'required' => true,
			),
			'mphb_stripe_redirect_url' => array(
				'type'     => 'hidden',
				'required' => false,
			),
		);

		return $fields;
	}

	/**
	 * @return boolean
	 */
	public function isActive() {
		return parent::isActive()
			&& ! empty( $this->getPublicKey() )
			&& ! empty( $this->getSecretKey() )
			&& (
				! empty( $this->getPaymentsConfigurationId() )
				|| ! empty( $this->getPaymentMethods() )
			);
	}

	public function isAutoCapture(): bool {
		return $this->getCaptureMethod() === 'auto';
	}

	public function isDynamicPaymentsMode(): bool {
		return $this->getPaymentMode() === 'payment';
	}

	public function isLegacyMode(): bool {
		return $this->getPaymentMode() === 'legacy';
	}

	public function isManualCapture(): bool {
		return $this->getCaptureMethod() !== 'auto';
	}

	public function isSendReceiptEmail(): bool {
		return (bool) $this->getOption( 'send_receipt' );
	}

	public function parsePaymentFields( $input, &$errors ) {
		$isParsed = parent::parsePaymentFields( $input, $errors );

		if ( $isParsed ) {
			$parseFields = array(
				'confirmation_token_id',
				'payment_intent_id',
				'payment_method',
				'redirect_url',
			);

			foreach ( $parseFields as $param ) {
				$field = 'mphb_stripe_' . $param;

				if ( isset( $this->postedPaymentFields[ $field ] ) ) {
					$this->paymentFields[ $param ] = $this->postedPaymentFields[ $field ];

					unset( $this->postedPaymentFields[ $field ] );
				}
			}
		}

		return $isParsed;
	}

	public function processPayment( Booking $booking, Payment $payment ) {
		if ( $this->isDynamicPaymentsMode() ) {
			return $this->processByConfirmationToken( $booking, $payment );
		} else {
			return $this->processLegacyPaymentIntent( $booking, $payment );
		}
	}

	public function registerOptionsFields( &$subtab ) {
		parent::registerOptionsFields( $subtab );

		// Show warning if the SSL not enabled
		if ( ! MPHB()->isSiteSSL()
			&& ( ! MPHB()->settings()->payment()->isForceCheckoutSSL() && ! class_exists( 'WordPressHTTPS' ) )
		) {
			$enableField = $subtab->findField( "mphb_payment_gateway_{$this->id}_enable" );

			if ( ! is_null( $enableField ) ) {
				if ( $this->isActive() ) {
					// Translators: 1: Payment gateway title. 2. URL to SSL settings.
					$message = __( '%1$s is enabled, but the <a href="%2$s">Force Secure Checkout</a> option is disabled. Please enable SSL and ensure your server has a valid SSL certificate. Otherwise, %1$s will only work in Test Mode.', 'motopress-hotel-booking' );
				} else {
					// Translators: 1: Payment gateway title. 2. URL to SSL settings.
					$message = __( 'The <a href="%2$s">Force Secure Checkout</a> option is disabled. Please enable SSL and ensure your server has a valid SSL certificate. Otherwise, %1$s will only work in Test Mode.', 'motopress-hotel-booking' );
				}

				$message = sprintf(
					$message,
					__( 'Stripe', 'motopress-hotel-booking' ),
					esc_url( MPHB()->getSettingsMenuPage()->getUrl( array( 'tab' => 'payments' ) ) )
				);

				$enableField->setDescription( $message );
			}
		}

		$fields = $this->getFields();

		$group = new SettingsGroup( "mphb_payments_{$this->id}_group1", '', $subtab->getOptionGroupName() );
		$groupFields = array( 'public_key', 'secret_key', 'payments_configuration_id', 'payment_methods', 'webhook_key', 'capture_method', 'send_receipt', 'locale' );

		foreach ( $groupFields as $fieldName ) {
			if ( array_key_exists( $fieldName, $fields ) ) {
				$field = FieldFactory::create( "mphb_payment_gateway_{$this->id}_{$fieldName}", $fields[ $fieldName ] );

				$group->addField( $field );
			}
		}

		$subtab->addGroup( $group );
	}

	/**
	 * @throws \Exception if requests fail.
	 */
	public function releaseAuthorizedFunds( Payment $payment ): bool {
		if ( ! $payment->hasPendingAuthedFunds() ) {
			return false;
		}

		$paymentIntent = $this->getApi()->retrievePaymentIntent( $payment->getTransactionId() );
		$this->getApi()->cancelPaymentIntent( $paymentIntent );

		$payment->getAuthedFunds()->release();

		MPHB()->getPaymentRepository()->save( $payment );

		return true;
	}

	/**
	 * @access private
	 */
	public function setupWebhooks(): void {
		$this->webhookListener = new WebhookListener( $this );
	}

	protected function initDefaultOptions() {
		$defaults = array(
			'title'                     => __( 'Pay by Card (Stripe)', 'motopress-hotel-booking' ),
			'description'               => __( 'Pay with your credit card via Stripe.', 'motopress-hotel-booking' ),
			'enabled'                   => false,
			'is_sandbox'                => false,
			'public_key'                => '',
			'secret_key'                => '',
			'payment_methods'           => array(),
			'payments_configuration_id' => '',
			'webhook_key'               => '',
			'capture_method'            => 'auto',
			'send_receipt'              => false,
			'locale'                    => 'auto',
		);

		return array_merge( parent::initDefaultOptions(), $defaults );
	}

	protected function initOptionFields(): array {
		$secretKeyDescription =
			// Translators: %s: Key prefix.
			sprintf( __( 'Standard key must start with %s.', 'motopress-hotel-booking' ), '<code>sk_</code>' )
			. '<br>'
			// Translators: %s: Key prefix.
			. sprintf( __( 'Restricted key must start with %s.', 'motopress-hotel-booking' ), '<code>rk_</code>' );

		$webhookKeyDescription =
			__( 'Optional. You can provide your Stripe Webhook Secret Key to enhance the security of incoming webhooks. The key can be found in your Stripe account under the webhook settings as the "Signing secret".', 'motopress-hotel-booking' )
			. '<br>'
			// Translators: %s: API version.
			. sprintf( __( 'API version: %s', 'motopress-hotel-booking' ), '<code>' . StripeAPI::API_VERSION . '</code>' )
			. '<br>'
			// Translators: %s: Webhooks destination URL.
			. sprintf( __( 'Webhooks destination URL: %s', 'motopress-hotel-booking' ), '<code>' . esc_url( $this->webhookListener->getNotifyUrl() ) . '</code>' )
			. '<br>'
			// Translators: %s: list of supported event types separated by commas.
			. sprintf( __( 'Supported event types: %s', 'motopress-hotel-booking' ), '<code>charge.captured</code>, <code>charge.succeeded</code>, <code>charge.updated</code>, <code>payment_intent.canceled</code>, <code>payment_intent.payment_failed</code>, <code>payment_intent.succeeded</code>, <code>refund.created</code>, <code>refund.updated</code>' )
			. '<br>'
			. __( '<a href="https://docs.stripe.com/webhooks#configure-webhook-settings" target="_blank">Setting up webhook endpoints</a>', 'motopress-hotel-booking' );

		$currencySettingsUrl = get_admin_url( null, 'edit.php?post_type=mphb_room_type&page=mphb_settings#mphb_currency' );
		$paymentsWarning =
			'<a href="' . esc_url( $currencySettingsUrl ) . '" target="_blank">'
				. sprintf(
					// Translators: %s: Currency codes.
					__( 'The %s currency is selected in the main settings.', 'motopress-hotel-booking' ),
					MPHB()->settings()->currency()->getCurrencyCode()
				)
			. '</a> '
			. sprintf(
				// Translators: 1: Names of payment methods. 2: Currency codes.
				__( '%1$s support the following currencies: %2$s.', 'motopress-hotel-booking' ),
				__( 'Bancontact', 'motopress-hotel-booking' ) . ', '
					. __( 'iDEAL', 'motopress-hotel-booking' ) . ', '
					. __( 'Giropay', 'motopress-hotel-booking' ) . ' and '
					. __( 'SEPA Direct Debit', 'motopress-hotel-booking' ),
				'EUR'
			)
			. ' ' . sprintf(
				// Translators: 1: Name of payment method. 2: Currency codes.
				__( '%1$s supports: %2$s.', 'motopress-hotel-booking' ),
				__( 'Klarna', 'motopress-hotel-booking' ),
				implode( ', ', self::KLARNA_ALLOWED_CURRENCIES )
			)
			. ' <a href="https://stripe.com/docs/payments/klarna" target="_blank">'
				. sprintf(
					// Translators: %s: Payment method name.
					__( '%s special restrictions.', 'motopress-hotel-booking' ),
					__( 'Klarna', 'motopress-hotel-booking' )
				)
			. '</a>';

		$stripeFields = array(
			'public_key' => array(
				'type'        => 'text',
				'label'       => __( 'Public Key', 'motopress-hotel-booking' ),
				'description' => __( '<a href="https://dashboard.stripe.com/apikeys" target="_blank">Locate API keys in the Dashboard</a>', 'motopress-hotel-booking' ),
				'placeholder' => str_pad( 'pk_****_', 107, '*' ),
				'default'     => $this->getDefaultOption( 'public_key' ),
				'encoded'     => true,
			),
			'secret_key' => array(
				'type'        => 'text',
				'label'       => __( 'Secret Key', 'motopress-hotel-booking' ),
				'description' => $secretKeyDescription,
				'placeholder' => str_pad( 'sk_****_', 107, '*' ),
				'default'     => $this->getDefaultOption( 'secret_key' ),
				'encoded'     => true,
			),
			'payments_configuration_id' => array(
				'type'        => 'text',
				'label'       => __( 'Payment Methods', 'motopress-hotel-booking' ),
				'description' => __( 'Manage all payment methods through Stripe using your configuration ID.', 'motopress-hotel-booking' )
					. '<br>'
					. __( '<a href="https://dashboard.stripe.com/settings/payment_methods" target="_blank">Setting up the payment method configuration ID</a>', 'motopress-hotel-booking' ),
				'placeholder' => str_pad( 'pmc_', 28, '*' ),
				'default'     => $this->getDefaultOption( 'payments_configuration_id' ),
				'encoded'     => true,
			),
			'payment_methods' => array(
				'type'                => 'multiple-checkbox',
				'description_top'     => __( 'Alternatively, select from these popular Stripe payment methods:', 'motopress-hotel-booking' ),
				'description'         => __( 'We recommend managing payment methods through your Stripe dashboard since it will simplify checkout by automatically displaying only compatible payment methods based on currency, restrictions, and customer location.', 'motopress-hotel-booking' )
					. '<br>'
					. $paymentsWarning,
				'list'                => array(
					'bancontact' => __( 'Bancontact', 'motopress-hotel-booking' ),
					'ideal'      => __( 'iDEAL', 'motopress-hotel-booking' ),
					'giropay'    => __( 'Giropay', 'motopress-hotel-booking' ),
					'sepa_debit' => __( 'SEPA Direct Debit', 'motopress-hotel-booking' ),
					'klarna'     => __( 'Klarna', 'motopress-hotel-booking' ),
				),
				'always_enabled'      => array( 'card' => __( 'Card Payments', 'motopress-hotel-booking' ) ),
				'allow_group_actions' => false, // Disable "Select All" and "Unselect All"
				'default'             => $this->getDefaultOption( 'payment_methods' ),
			),
			'webhook_key' => array(
				'type'        => 'text',
				'label'       => __( 'Webhook Secret Key', 'motopress-hotel-booking' ),
				'description' => $webhookKeyDescription,
				'placeholder' => str_pad( 'whsec_', 38, '*' ),
				'default'     => $this->getDefaultOption( 'webhook_key' ),
				'encoded'     => true,
			),
			'capture_method' => array(
				'type'        => 'radio',
				// translators: Refers to Stripe payment behavior: charge immediately or authorize first and capture later.
				'label'       => __( 'Charging Method', 'motopress-hotel-booking' ),
				'list'        => array(
					'auto'   => __( 'Automatic (charge instantly) — Charge the customer immediately.', 'motopress-hotel-booking' ),
					// Automatically release, capture manually
					'manual' => __( 'Manual (charge later) — Authorize without charging. Charge later manually via Payments or in your Stripe dashboard. If you don’t charge the payment, it will be automatically released, usually within 7 days.', 'motopress-hotel-booking' ),
				),
				'default'     => $this->getDefaultOption( 'capture_method' ),
				'description' => __( 'Please note: Payment methods that do not support manual charging will not be shown to customers at checkout.', 'motopress-hotel-booking' ),
			),
			'send_receipt' => array(
				'type'        => 'checkbox',
				'label'       => __( 'Send Receipt', 'motopress-hotel-booking' ),
				'inner_label' => __( 'Automatically send a payment receipt via Stripe after a successful charge', 'motopress-hotel-booking' ),
				'default'     => $this->getDefaultOption( 'send_receipt' ),
			),
			'locale' => array(
				'type'        => 'select',
				'label'       => __( 'Checkout Locale', 'motopress-hotel-booking' ),
				'description' => __( 'Display Checkout in the user\'s preferred language, if available.', 'motopress-hotel-booking' ),
				'list'        => $this->getAvailableLocales(),
				'default'     => $this->getDefaultOption( 'locale' ),
			),
		);

		/**
		 * @param array $fields
		 */
		return apply_filters( 'mphb_stripe_fields', parent::initOptionFields() + $stripeFields );
	}

	protected function setupProperties() {
		parent::setupProperties();

		$this->adminTitle = __( 'Stripe', 'motopress-hotel-booking' );
		$this->locale = $this->getOption( 'locale' );
	}

	/**
	 * @since 5.2.4 Replaced translated language labels with native language
	 *     names.
	 *
	 * @see https://docs.stripe.com/js/appendix/supported_locales
	 *
	 * @return array [ Locale => Native name ]
	 */
	private function getAvailableLocales() {
		// https://api.wordpress.org/translations/core/1.0/
		// https://translation.io/docs/languages
		$locales = array(
			'auto'   => __( 'Auto', 'motopress-hotel-booking' ),
			'ar'     => '\u0627\u0644\u0639\u0631\u0628\u064a\u0629',
			'bg'     => '\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438',
			'cs'     => '\u010ce\u0161tina',
			'da'     => 'Dansk',
			'de'     => 'Deutsch',
			'el'     => '\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac',
			'en'     => 'English',
			'en-GB'  => 'English (United Kingdom)',
			'es'     => 'Espa\u00f1ol',
			'es-419' => 'Espa\u00f1ol (Latinoam\u00e9rica)',
			'et'     => 'Eesti',
			'fi'     => 'Suomi',
			'fil'    => 'Filipino',
			'fr'     => 'Fran\u00e7ais',
			'fr-CA'  => 'Fran\u00e7ais du Canada',
			'he'     => '\u05e2\u05b4\u05d1\u05b0\u05e8\u05b4\u05d9\u05ea',
			'hr'     => 'Hrvatski',
			'hu'     => 'Magyar',
			'id'     => 'Bahasa Indonesia',
			'it'     => 'Italiano',
			'ja'     => '\u65e5\u672c\u8a9e',
			'ko'     => '\ud55c\uad6d\uc5b4',
			'lt'     => 'Lietuvi\u0173 kalba',
			'lv'     => 'Latvie\u0161u valoda',
			'ms'     => 'Bahasa Melayu',
			'mt'     => 'Malti',
			'nb'     => 'Norsk bokm\u00e5l',
			'nl'     => 'Nederlands',
			'pl'     => 'Polski',
			'pt'     => 'Portugu\u00eas',
			'pt-BR'  => 'Portugu\u00eas do Brasil',
			'ro'     => 'Rom\u00e2n\u0103',
			'ru'     => '\u0420\u0443\u0441\u0441\u043a\u0438\u0439',
			'sk'     => 'Sloven\u010dina',
			'sl'     => 'Sloven\u0161\u010dina',
			'sv'     => 'Svenska',
			'th'     => '\u0e44\u0e17\u0e22',
			'tr'     => 'T\u00fcrk\u00e7e',
			'vi'     => 'Ti\u1ebfng Vi\u1ec7t',
			'zh'     => '\u7b80\u4f53\u4e2d\u6587',
			'zh-HK'  => '\u9999\u6e2f\u4e2d\u6587',
			'zh-TW'  => '\u7e41\u9ad4\u4e2d\u6587',
		);

		// Decode Unicode escape sequences (\uXXXX) into UTF-8 characters.
		$locales = array_map(
			function ( $locale ) {
				if ( strpos( $locale, '\u' ) !== false ) {
					return json_decode( '"' . $locale . '"' );
				} else {
					return $locale;
				}
			},
			$locales
		);

		return $locales;
	}

	/**
	 * Check Stripe error before showing "Payment Received Page".
	 */
	private function hanglePaymentReceivedPageErrors(): void {
		if ( ! isset( $_REQUEST['mphb_action'] ) || $_REQUEST['mphb_action'] !== 'handle_stripe_errors' ) {
			return;
		}

		if ( ! mphb_verify_nonce( 'handle_stripe_errors' ) ) {
			if ( is_admin() ) {
				wp_die(
					esc_html__( 'Nonce verification failed.', 'motopress-hotel-booking' ),
					esc_html__( 'Error', 'motopress-hotel-booking' ),
					array( 'response' => 403 )
				);
			}
		}

		if ( ! isset( $_REQUEST['payment_intent'] ) ) {
			if ( is_admin() ) {
				wp_die(
					esc_html__( 'PaymentIntent ID is missing.', 'motopress-hotel-booking' ),
					esc_html__( 'Error', 'motopress-hotel-booking' ),
					array( 'response' => 403 )
				);
			}
		}

		// phpcs:ignore
		$paymentIntentId = isset( $_REQUEST['payment_intent'] ) ? $_REQUEST['payment_intent'] : '';

		try {
			if ( ! empty( $paymentIntentId ) ) {
				$payment = MPHB()->getPaymentRepository()->findByTransactionId( $paymentIntentId );

				$notFailedStatuses = array(
					PaymentStatuses::STATUS_COMPLETED,
					PaymentStatuses::STATUS_ON_HOLD,
					PaymentStatuses::STATUS_PENDING,
				);

				if ( ! in_array( $payment->getStatus(), $notFailedStatuses ) ) {
					$isRedirecting = wp_redirect( MPHB()->settings()->pages()->getPaymentFailedPageUrl() );

					if ( $isRedirecting ) {
						exit;
					}
				}
			}

		} catch ( \Exception $e ) {
			if ( is_admin() ) {
				wp_die(
					$e->getMessage(), // phpcs:ignore
					esc_html__( 'Error', 'motopress-hotel-booking' ),
					array( 'response' => 403 )
				);
			}
		}
	}

	/**
	 * @throws \RuntimeException if confirmation token ID is not set.
	 * @throws ApiErrorException if the API request fails.
	 */
	private function processByConfirmationToken( Booking $booking, Payment $payment ): array {
		$confirmationTokenId = $this->paymentFields['confirmation_token_id'];

		if ( empty( $confirmationTokenId ) ) {
			throw new \RuntimeException( esc_html__( 'Confirmation token ID is not set.', 'motopress-hotel-booking' ) );
		}

		// Start payment processing
		update_post_meta( $payment->getId(), '_mphb_payment_type', 'payment' );
		update_post_meta( $payment->getId(), '_mphb_transaction_id', $confirmationTokenId );

		$payment->setTransactionId( $confirmationTokenId );

		if ( $this->isManualCapture() ) {
			$authedFunds = AuthorizedFunds::createForAmount( $payment->getId(), $payment->getAmount() + $payment->getPaymentFee() );
			$payment->setAuthedFunds( $authedFunds );

			MPHB()->getPaymentRepository()->save( $payment );
		}

		$receiptEmail = $this->isSendReceiptEmail() ? $booking->getCustomer()->getEmail() : '';

		$paymentIntent = $this->getApi()->createPaymentIntentByConfirmationToken(
			$confirmationTokenId,
			$payment->getAmount() + $payment->getPaymentFee(),
			$payment->getCurrency(),
			$this->generateItemName( $booking ),
			$receiptEmail
		);

		// Replace confirmation token ID with payment intent ID
		update_post_meta( $payment->getId(), '_mphb_transaction_id', $paymentIntent->id );

		$payment->setTransactionId( $paymentIntent->id );

		// Hold/complete payment
		switch ( $paymentIntent->status ) {
			case 'succeeded':
				// Translators: %s: Stripe PaymentIntent ID.
				$payment->addLog( sprintf( __( 'Payment for PaymentIntent %s succeeded.', 'motopress-hotel-booking' ), $paymentIntent->id ) );

				$this->paymentCompleted( $payment );
				break;

			case 'requires_action': // Redirect or 3D Secure
				// Translators: %s: Stripe PaymentIntent ID.
				$payment->addLog( sprintf( __( 'Payment for PaymentIntent %s requires additional action from the customer.', 'motopress-hotel-booking' ), $paymentIntent->id ) );
				break;

			case 'requires_capture':
				// Translators: %s: Stripe PaymentIntent ID.
				$payment->addLog( sprintf( __( 'You must manually charge the payment for PaymentIntent %s to complete the transaction.', 'motopress-hotel-booking' ), $paymentIntent->id ) );

				$this->paymentOnHold( $payment );
				break;

			default:
				// Translators: %s: PaymentIntent ID.
				$payment->addLog( sprintf( 'Payment for PaymentIntent %s is processing.', $paymentIntent->id ) );

				$this->paymentOnHold( $payment );
				break;
		}

		return array(
			'payment_intent' => array(
				'client_secret' => $paymentIntent->client_secret,
				'status'        => $paymentIntent->status,
			),
		);
	}

	private function processLegacyPaymentIntent( Booking $booking, Payment $payment ): void {
		$paymentMethod   = $this->paymentFields['payment_method'];
		$paymentIntentId = $this->paymentFields['payment_intent_id'];
		$redirectUrl     = $this->paymentFields['redirect_url'];

		if ( empty( $paymentIntentId ) ) {
			throw new \RuntimeException( esc_html__( 'PaymentIntent ID is not set.', 'motopress-hotel-booking' ) );
		}

		// Start payment processing
		update_post_meta( $payment->getId(), '_mphb_payment_type', $paymentMethod );
		update_post_meta( $payment->getId(), '_mphb_transaction_id', $paymentIntentId );

		$payment->setTransactionId( $paymentIntentId );

		if ( $this->isManualCapture() ) {
			$authedFunds = AuthorizedFunds::createForAmount( $payment->getId(), $payment->getAmount() + $payment->getPaymentFee() );
			$payment->setAuthedFunds( $authedFunds );

			MPHB()->getPaymentRepository()->save( $payment );
		}

		$paymentIntent = $this->getApi()->retrievePaymentIntent( $paymentIntentId );

		/*
		 * https://docs.stripe.com/payments/paymentintents/lifecycle#intent-statuses
		 *
		 * Stripe has many statuses, but we are using only 2 of them:
		 * "succeeded", "requires_capture" and "processing". "canceled" and
		 * other will not pass checks from stripe-gateway.js.
		 */
		switch ( $paymentIntent->status ) {
			case 'succeeded':
				// Translators: %s: Stripe PaymentIntent ID.
				$payment->addLog( sprintf( __( 'Payment for PaymentIntent %s succeeded.', 'motopress-hotel-booking' ), $paymentIntentId ) );

				$this->paymentCompleted( $payment );
				break;

			case 'requires_capture':
				// Translators: %s: Stripe PaymentIntent ID.
				$payment->addLog( sprintf( __( 'You must manually charge the payment for PaymentIntent %s to complete the transaction.', 'motopress-hotel-booking' ), $paymentIntentId ) );

				$this->paymentOnHold( $payment );
				break;

			default: // "processing"
				// Translators: %s: Stripe PaymentIntent ID.
				$payment->addLog( sprintf( __( 'Payment for PaymentIntent %s is processing.', 'motopress-hotel-booking' ), $paymentIntentId ) );

				$this->paymentOnHold( $payment );
				break;
		}

		// Set description to "Reservation #..." when we know the booking ID
		$description = $this->generateItemName( $booking );

		$this->getApi()->updateDescription( $paymentIntent, $description );

		if ( ! empty( $redirectUrl ) ) {
			wp_redirect( $redirectUrl );
		}
	}
}
