<?php
/**
 * LearnDash PayPal Payment Gateway.
 *
 * @since 4.25.0
 *
 * @package LearnDash\Core
 */

namespace LearnDash\Core\Modules\Payments\Gateways\Paypal;

use LDLMS_Post_Types;
use Learndash_Payment_Gateway;
use LearnDash_Settings_Section;
use LearnDash_Theme_Register_LD30;
use Learndash_Transaction_Meta_DTO;
use LearnDash\Core\Models\Product;
use LearnDash\Core\Models\Transaction;
use LearnDash\Core\Template\Template;
use LearnDash\Core\Utilities\Cast;
use LearnDash\Core\Utilities\Countries;
use Learndash_DTO_Validation_Exception;
use Learndash_Transaction_Gateway_Transaction_DTO;
use StellarWP\Learndash\StellarWP\Arrays\Arr;
use StellarWP\Learndash\StellarWP\DB\DB;
use StellarWP\Learndash\StellarWP\DB\QueryBuilder\JoinQueryBuilder;
use LearnDash\Core\App;
use LearnDash\Core\Enums\Commerce\Cancellation_Reason;
use LearnDash\Core\Models\Commerce\Charge;
use LearnDash\Core\Models\Commerce\Subscription;
use LearnDash\Core\Modules\Payments\Gateways\Paypal\Order_Status;
use LearnDash\Core\Modules\Payments\Subscriptions\Retry_Scheduler;
use LearnDash\Core\Modules\Payments\Subscriptions\Retry_Email_Trigger;
use WP_Error;
use WP_Post;
use WP_User;

/**
 * PayPal payment gateway class.
 *
 * @since 4.25.0
 */
class Payment_Gateway extends Learndash_Payment_Gateway {
	/**
	 * Suffix for the 'paypal' payment method button key.
	 *
	 * @since 4.25.0
	 *
	 * @var string
	 */
	private const PAYPAL_BUTTON_KEY_SUFFIX = '_paypal';

	/**
	 * Suffix for the 'card' payment method button key.
	 *
	 * @since 4.25.0
	 *
	 * @var string
	 */
	private const CARD_BUTTON_KEY_SUFFIX = '_paypal_card';

	/**
	 * Gateway name.
	 *
	 * @since 4.25.0
	 *
	 * @var string
	 */
	private static string $gateway_name = 'paypal_checkout';

	/**
	 * Partner attribution ID.
	 *
	 * @since 4.25.0
	 *
	 * @var string
	 */
	private static string $partner_attribution_id = 'LearnDash_SP_PPCP';

	/**
	 * Live partner ID.
	 *
	 * @since 4.25.0
	 *
	 * @var string
	 */
	private static string $live_partner_id = 'XLTK34Z6PNG4E'; // cspell:disable-line -- Generated by PayPal.

	/**
	 * Sandbox partner ID.
	 *
	 * @since 4.25.0
	 *
	 * @var string
	 */
	private static string $sandbox_partner_id = 'G8CQBUDE5QW72'; // cspell:disable-line -- Generated by PayPal.

	/**
	 * Settings Section Key used to configure this Payment Gateway.
	 *
	 * @since 4.25.0
	 *
	 * @var string
	 */
	protected string $settings_section_key = 'settings_paypal_checkout';

	/**
	 * Constructor.
	 *
	 * @since 4.25.0
	 *
	 * @return void
	 */
	public function __construct() {
		parent::__construct();

		// Load the settings.
		$this->configure();
	}

	/**
	 * Returns the gateway name.
	 *
	 * @since 4.25.0
	 *
	 * @return string
	 */
	public static function get_name(): string {
		return self::$gateway_name;
	}

	/**
	 * Returns the gateway label.
	 *
	 * @since 4.25.0
	 *
	 * @return string
	 */
	public static function get_label(): string {
		return esc_html__( 'PayPal Checkout', 'learndash' );
	}

	/**
	 * Checks if PayPal account is connected.
	 *
	 * @since 4.25.0
	 *
	 * @return bool True if PayPal account is connected. False otherwise.
	 */
	public static function account_is_connected(): bool {
		$settings = self::get_settings();

		return ! empty( $settings['merchant_id'] ?? '' )
			&& ! empty( $settings['account_id'] ?? '' )
			&& ! empty( $settings['client_id'] ?? '' )
			&& ! empty( $settings['client_secret'] ?? '' );
	}

	/**
	 * Returns the partner attribution ID.
	 *
	 * @since 4.25.0
	 *
	 * @return string
	 */
	public static function get_partner_attribution_id(): string {
		return self::$partner_attribution_id;
	}

	/**
	 * Returns whether the payment method 'PayPal' is active.
	 *
	 * @since 4.25.0
	 *
	 * @return bool
	 */
	public static function is_payment_method_paypal_active(): bool {
		return in_array(
			'paypal',
			(array) Arr::get( self::get_settings(), 'payment_methods', [] ),
			true
		);
	}

	/**
	 * Returns whether the payment method 'Credit Card' is active.
	 *
	 * @since 4.25.0
	 *
	 * @return bool
	 */
	public static function is_payment_method_card_active(): bool {
		return in_array(
			'card',
			(array) Arr::get( self::get_settings(), 'payment_methods', [] ),
			true
		);
	}

	/**
	 * Returns the partner ID.
	 *
	 * @since 4.25.0
	 *
	 * @return string
	 */
	public static function get_partner_id(): string {
		$settings = self::get_settings();

		if ( '1' === Arr::get( $settings, 'test_mode', '0' ) ) {
			return self::$sandbox_partner_id;
		}

		return self::$live_partner_id;
	}

	/**
	 * Returns the settings.
	 *
	 * @since 4.25.0
	 *
	 * @return array<string,mixed>
	 */
	public static function get_settings(): array {
		return Arr::wrap( LearnDash_Settings_Section::get_section_settings_all( 'LearnDash_Settings_Section_PayPal_Checkout' ) );
	}

	/**
	 * Creates an order reference ID.
	 *
	 * @since 4.25.0
	 *
	 * @param int   $user_id     User ID.
	 * @param int[] $product_ids Product IDs.
	 *
	 * @return string Reference ID. 11 characters long.
	 */
	public function create_order_reference_id( int $user_id, array $product_ids ): string {
		$data = [
			'user_id'     => $user_id,
			'product_ids' => $product_ids,
			'time'        => time(),
		];

		// TODO: Use wp_unique_id_from_values() in the future.
		return sprintf(
			'ld_%s',
			substr( md5( Cast::to_string( wp_json_encode( $data ) ) ), 0, 8 )
		);
	}

	/**
	 * Updates the user reference ID data.
	 *
	 * @since 4.25.0
	 *
	 * @param int                 $user_id      User ID.
	 * @param string              $reference_id Reference ID.
	 * @param array<string,mixed> $data         Data.
	 *
	 * @return void
	 */
	public function update_user_reference_id_data( int $user_id, string $reference_id, array $data ): void {
		update_user_meta( $user_id, 'ld_paypal_checkout_reference_id_' . $reference_id, $data );
	}

	/**
	 * Deletes the reference ID data.
	 *
	 * @since 4.25.0
	 *
	 * @param int    $user_id      User ID.
	 * @param string $reference_id Reference ID.
	 *
	 * @return void
	 */
	public function delete_reference_id_data( int $user_id, string $reference_id ): void {
		delete_user_meta( $user_id, 'ld_paypal_checkout_reference_id_' . $reference_id );
	}

	/**
	 * Returns whether the gateway is supported.
	 *
	 * @since 4.25.0
	 *
	 * @return bool
	 */
	public function is_supported(): bool {
		// It's supported only in the Modern Registration page.
		return learndash_registration_page_is_set()
			&& learndash_registration_variation() === LearnDash_Theme_Register_LD30::$variation_modern;
	}

	/**
	 * Returns the gateway info text for the 'PayPal' payment method.
	 *
	 * @since 4.25.0
	 *
	 * @param string $product_type Type of product being purchased.
	 *
	 * @return string
	 */
	public function get_checkout_info_text_for_paypal_payment_method( string $product_type ): string {
		if ( $product_type === LDLMS_Post_Types::COURSE ) {
			return sprintf(
				// translators: placeholders: Course label.
				_x(
					'A PayPal pop-up will open where you can complete your payment using your PayPal account or a saved debit or credit card. Once your payment is complete, the window will close and your %s access will update automatically.',
					'Message displayed when purchasing a course.',
					'learndash'
				),
				learndash_get_custom_label_lower( LDLMS_Post_Types::COURSE )
			);
		}

		if ( $product_type === LDLMS_Post_Types::GROUP ) {
			return sprintf(
				// translators: placeholders: Group label.
				_x(
					'A PayPal pop-up will open where you can complete your payment using your PayPal account or a saved debit or credit card. Once your payment is complete, the window will close and your %s access will update automatically.',
					'Message displayed when purchasing a group.',
					'learndash'
				),
				learndash_get_custom_label_lower( LDLMS_Post_Types::GROUP )
			);
		}

		return _x(
			'A PayPal pop-up will open where you can complete your payment using your PayPal account or a saved debit or credit card. Once your payment is complete, the window will close and your access will update automatically.',
			'Message displayed when purchasing a product that is neither a course nor a group.',
			'learndash'
		);
	}

	/**
	 * Returns the checkout data for a specific button key.
	 *
	 * @since 4.25.0
	 *
	 * @param string               $button_key Button key.
	 * @param array<string, mixed> $params     Payment params. Default empty array.
	 *
	 * @return array<string, mixed>
	 */
	public function get_checkout_data_for_button_key( string $button_key, array $params = [] ): array {
		$countries = [
			'' => sprintf(
				'&ndash; %s &ndash;',
				esc_html__( 'Select a country', 'learndash' )
			),
		] + Countries::get_all();

		if ( $button_key === static::get_name() . self::PAYPAL_BUTTON_KEY_SUFFIX ) {
			return [
				'label'                => __( 'Pay with PayPal', 'learndash' ),
				'info_text'            => $this->get_checkout_info_text_for_paypal_payment_method( Cast::to_string( $params['product_type'] ?? '' ) ),
				'meta_html'            => Template::get_template( 'components/logos/paypal.php' ),
				'gateway_details_html' => Template::get_template(
					'components/payments/paypal/checkout/paypal-details.php',
					[
						'gateway'   => $this,
						'countries' => $countries,
					]
				),
			];
		}

		if ( $button_key === static::get_name() . self::CARD_BUTTON_KEY_SUFFIX ) {
			return [
				'label'                => __( 'Pay with Credit/Debit Card', 'learndash' ),
				'info_text'            => '', // No info text for the card button.
				'meta_html'            => Template::get_template( 'components/icons/credit-card.php' ),
				'gateway_details_html' => Template::get_template(
					'components/payments/paypal/checkout/card-details.php',
					[
						'gateway'   => $this,
						'countries' => $countries,
					]
				),
			];
		}

		// No button key found.
		return [];
	}

	/**
	 * Returns the card manager form content.
	 *
	 * @since 4.25.0
	 *
	 * @return void
	 */
	public function render_card_manager_form_content(): void {
		$countries = [
			'' => sprintf(
				'&ndash; %s &ndash;',
				esc_html__( 'Select a country', 'learndash' )
			),
		] + Countries::get_all();

		Template::show_template(
			'components/payments/paypal/checkout/card-details/card-information',
			[
				'gateway' => $this,
			]
		);

		Template::show_template(
			'components/payments/paypal/checkout/card-details/billing-address',
			[
				'gateway'   => $this,
				'countries' => $countries,
			]
		);
	}

	/**
	 * Returns a flag to easily identify if the gateway supports transactions management.
	 *
	 * @since 4.25.0
	 *
	 * @return bool True if a gateway supports managing subscriptions/other transactions. False otherwise.
	 */
	public function supports_transactions_management(): bool {
		return true;
	}

	/**
	 * Cancels a subscription.
	 *
	 * @since 4.25.0
	 *
	 * @param string $subscription_id Subscription ID.
	 *
	 * @return bool|WP_Error True if cancelled. Otherwise, WP_Error.
	 */
	public function cancel_subscription( string $subscription_id ) {
		// We don't need to do anything here, as we don't create subscriptions in PayPal. We manage subscriptions manually with Action Scheduler.
		return true;
	}

	/**
	 * Adds hooks.
	 *
	 * @since 4.25.0
	 *
	 * @return void
	 */
	public function add_extra_hooks(): void {
		// No extra hooks.
	}

	/**
	 * Enqueues scripts.
	 *
	 * @since 4.25.0
	 *
	 * @return void
	 */
	public function enqueue_scripts(): void {
		/**
		 * It's implemented in the Assets class.
		 *
		 * @see src/Core/Modules/Payments/Gateways/Paypal/Assets.php
		 */
	}

	/**
	 * Creates a session/order/subscription or prepares payment options on backend.
	 *
	 * @since 4.25.0
	 *
	 * @return void Json response.
	 */
	public function setup_payment(): void {
		// PayPal does not use this method.
	}

	/**
	 * Returns true if everything is configured and payment gateway can be used, otherwise false.
	 *
	 * @since 4.25.0
	 *
	 * @return bool
	 */
	public function is_ready(): bool {
		return 'yes' === ( $this->settings['enabled'] ?? '' )
			&& self::account_is_connected();
	}

	/**
	 * Handles the webhook.
	 *
	 * @since 4.25.0
	 *
	 * @return void
	 */
	public function process_webhook(): void {
		/**
		 * It's implemented in the PayPal Webhook endpoint.
		 *
		 * @see src/Core/Modules/Payments/Gateways/Paypal/Endpoints/Payments/Webhook.php
		 */
	}

	/**
	 * Processes a successful single payment.
	 *
	 * @since 4.25.0
	 *
	 * @param int          $user_id User ID.
	 * @param array<mixed> $order   Order data.
	 *
	 * @return void
	 */
	public function process_successful_single_payment( int $user_id, array $order ): void {
		$this->log_info( 'Processing successful single payment for user ID[' . $user_id . '] and order[' . ( $order['id'] ?? 'unknown' ) . ']' );

		$reference_id = Cast::to_string( Arr::get( $order, 'purchase_units.0.reference_id' ) );

		if ( ! $reference_id ) {
			$this->log_error( 'No reference ID found in the order.' );

			return;
		}

		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			$this->log_error( 'User not found.' );

			return;
		}

		$products = $this->get_products_from_reference_id( $user_id, $reference_id );

		if ( empty( $products ) ) {
			$this->log_error( 'No products found for the reference ID.' );

			return;
		}

		$this->add_access_to_products( $products, $user );
		$this->log_info( 'Added access to products.' );

		if ( $this->has_transaction_from_reference_id( $user_id, $reference_id ) ) {
			$this->log_info( 'Transaction already recorded for the reference ID. Nothing to do.' );

			return;
		}

		// Get the payment token from the order for checkout with new payment method.
		$payment_token = Payment_Token::get_payment_token_from_order( $order );

		if ( $payment_token ) {
			$order['payment_token'] = $payment_token;
		}

		// Get the payment token from the reference ID for checkout with saved payment method.
		$saved_token = Payment_Token::get_payment_token_from_reference_id( $user_id, $reference_id );

		if ( $saved_token ) {
			$order['payment_token'] = $saved_token;
		}

		foreach ( $products as $product ) {
			try {
				$this->record_transaction(
					$this->map_transaction_meta( $order, $product )->to_array(),
					$product->get_post(),
					$user
				);

				$this->log_info( 'Recorded transaction for product ID: ' . $product->get_id() );
			} catch ( Learndash_DTO_Validation_Exception $e ) {
				$this->log_error( 'Error recording transaction: ' . $e->getMessage() );
			}
		}
	}

	/**
	 * Processes a failed payment.
	 *
	 * @since 4.25.0
	 *
	 * @param int    $user_id     User ID.
	 * @param int[]  $product_ids Product IDs.
	 * @param string $reason      The reason for the failed payment.
	 *
	 * @return void
	 */
	public function process_failed_payment( int $user_id, array $product_ids, string $reason ): void {
		$this->log_info( 'Processing failed payment for user ID[' . $user_id . '] and product IDs[' . implode( ', ', $product_ids ) . ']' );

		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			$this->log_error( 'User not found.' );

			return;
		}

		$products = Product::find_many( $product_ids );

		$this->remove_access_to_products( $products, $user );
		$this->log_info( 'Removed access to products.' );

		// Cancel the transactions.
		$this->log_info( 'Cancelling transactions.' );

		$this->cancel_transactions( $products, $user, $reason );

		$this->log_info( 'Transactions cancelled.' );
	}

	/**
	 * Processes a free trial start for a recurring product.
	 *
	 * Free trial is a special case where we add access to the product for the free trial and record a transaction for the free trial.
	 *
	 * @since 4.25.0
	 *
	 * @param int                 $user_id       User ID.
	 * @param Product             $product       Product instance.
	 * @param array<string,mixed> $payment_token Payment token data from PayPal.
	 *
	 * @return void
	 */
	public function process_free_trial_start( int $user_id, Product $product, array $payment_token ): void {
		$this->log_info( 'Processing free trial start for user ID[' . $user_id . '] and product ID[' . $product->get_id() . ']' );

		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			$this->log_error( 'User not found.' );

			return;
		}

		// Add access to the product for the free trial.

		$this->add_access_to_products( [ $product ], $user );
		$this->log_info( 'Added access to product for free trial.' );

		// Add the payment token to the transaction data if it's not already set.
		if ( empty( $payment_token['payment_token'] ) ) {
			$payment_token['payment_token'] = [
				'gateway'     => 'paypal_checkout',
				'token'       => Cast::to_string( Arr::get( $payment_token, 'id', '' ) ),
				'customer_id' => Cast::to_string( Arr::get( $payment_token, 'customer.id', '' ) ),
				'type'        => Cast::to_string(
					array_key_first(
						Arr::wrap(
							Arr::get( $payment_token, 'payment_source', [] )
						)
					)
				),
			];
		}

		// Record the transaction for the free trial.

		try {
			$this->record_transaction(
				$this->map_transaction_meta( $payment_token, $product )->to_array(),
				$product->get_post(),
				$user
			);

			$this->log_info( 'Recorded transaction for free trial. Product ID: ' . $product->get_id() );
		} catch ( Learndash_DTO_Validation_Exception $e ) {
			$this->log_error( 'Error recording transaction: ' . $e->getMessage() );
		}
	}

	/**
	 * Processes a subscription payment using the stored payment token.
	 *
	 * @since 4.25.0
	 *
	 * @param bool                  $result        The result of the payment processing.
	 * @param Subscription          $subscription  The subscription.
	 * @param WP_User               $user          The user.
	 * @param array<string, string> $payment_token The payment token.
	 *
	 * @return bool True if payment was successful, false otherwise.
	 */
	public function process_subscription_payment(
		bool $result,
		Subscription $subscription,
		WP_User $user,
		array $payment_token
	): bool {
		$this->log_info( 'Processing subscription payment for subscription ID[' . $subscription->get_id() . '] and user ID[' . $user->ID . ']' );

		$product = $subscription->get_product();

		if ( ! $product ) {
			$this->log_error( 'Product not found for subscription: ' . $subscription->get_id() );
			return false;
		}

		// Check if the payment token exists.
		$token_id = Cast::to_string( Arr::get( $payment_token, 'token', '' ) );

		if ( empty( $token_id ) ) {
			$this->log_error( 'Invalid payment token for subscription: ' . $subscription->get_id() );
			return false;
		}

		$data_builder = App::get( Order_Data::class );

		if ( ! $data_builder instanceof Order_Data ) {
			$this->log_error( 'Data builder not found.' );

			return false;
		}

		$order_data = $data_builder->build(
			[ $product->get_id() ],
			$user,
			'card' === Arr::get( $payment_token, 'type', '' ),
			false
		);

		// Mark if it's a card payment.
		if ( 'card' === Arr::get( $payment_token, 'type', '' ) ) {
			$order_data['use_card_fields'] = true;
		}

		// Add the payment token to the order data.
		$order_data['vault_id']    = $token_id;
		$order_data['customer_id'] = Cast::to_string( Arr::get( $payment_token, 'customer_id', '' ) );

		// Create the order.
		$client = App::get( Client::class );

		if ( ! $client instanceof Client ) {
			$this->log_error( 'Client not found.' );

			return false;
		}

		if ( $this->is_test_mode() ) {
			$client->use_sandbox();
		} else {
			$client->use_production();
		}

		$order = $client->create_order( $order_data );

		if ( is_wp_error( $order ) ) {
			$this->log_error( 'Error creating order: ' . $order->get_error_message() );

			return false;
		}

		$order_status = App::get( Order_Status::class );

		if ( ! $order_status instanceof Order_Status ) {
			$this->log_error( 'Order status helper not found.' );

			return false;
		}

		if ( ! $order_status->is_payment_completed( Cast::to_string( Arr::get( $order, 'status', '' ) ) ) ) {
			$this->log_error( 'Order status is not successful. Order ID: ' . Cast::to_string( Arr::get( $order, 'id', '' ) ) );

			// Do not cancel the subscription here. It will be cancelled in the process_subscription_failure() method.
			return false;
		}

		// Add a successful charge to the subscription.
		$subscription->add_charge( $product->get_price(), Charge::$status_success );

		$this->log_info( 'Added successful charge to subscription.' );

		// Capture the order.
		return true;
	}

	/**
	 * Processes a subscription failure.
	 *
	 * @since 4.25.0
	 *
	 * @param Subscription $subscription The subscription.
	 * @param WP_User      $user         The user.
	 *
	 * @return void
	 */
	public function process_subscription_failure(
		Subscription $subscription,
		WP_User $user
	): void {
		// Check if the subscription can be cancelled. Maybe it's already cancelled.
		if (
			$subscription->is_canceled()
			|| $subscription->is_expired()
		) {
			return;
		}

		$this->log_info( 'Processing subscription failure for subscription ID[' . $subscription->get_id() . '] and user ID[' . $user->ID . ']' );

		$product = $subscription->get_product();

		if ( $product ) {
			$subscription->add_charge( $product->get_price(), Charge::$status_failed );
		}

		// Check if the subscription can be retried.
		if ( ! $subscription->can_be_retried() ) {
			$this->log_info( 'Subscription ID[' . $subscription->get_id() . '] has reached maximum retry attempts. Canceling subscription.' );

			// Cancel the subscription after max retries.
			$subscription->cancel(
				Cancellation_Reason::FAILED_PAYMENT()->getValue(),
				true
			);

			// Send the payment failed access revoked email.
			Retry_Email_Trigger::send_payment_failed_access_revoked_email( $subscription, $user );

			return;
		}

		// Increment retry count and set last retry timestamp.
		$retry_count = $subscription->increment_retry_count();
		$subscription->set_last_retry_timestamp( time() );

		// Log the retry attempt.
		$this->log_info(
			sprintf(
				'Payment retry attempt %d of %d for subscription ID[%d].',
				$retry_count,
				$subscription->get_max_retries(),
				$subscription->get_id()
			)
		);

		$this->log_info(
			sprintf(
				'Payment failed for subscription ID[%d]. Retry attempt %d of %d. Next retry scheduled for %s.',
				$subscription->get_id(),
				$retry_count,
				$subscription->get_max_retries(),
				gmdate( 'Y-m-d H:i:s', $subscription->get_next_retry_timestamp() )
			)
		);

		// Schedule the next retry using the Retry Scheduler.
		$scheduled = Retry_Scheduler::schedule( $subscription );

		if ( $scheduled ) {
			// Send the retry email.
			Retry_Email_Trigger::send_retry_email( $subscription, $user );

			$this->log_info(
				sprintf(
					'Scheduled payment retry for subscription ID[%d] at %s.',
					$subscription->get_id(),
					gmdate( 'Y-m-d H:i:s', $subscription->get_next_retry_timestamp() )
				)
			);
		}
	}

	/**
	 * Processes a PayPal Standard (IPN) subscription migration to PayPal Checkout.
	 *
	 * This method is used to migrate a PayPal Standard (IPN) subscription to PayPal Checkout.
	 *
	 * @since 4.25.3
	 *
	 * @param Product             $product       The product (course or group).
	 * @param WP_User             $user          The user.
	 * @param array<string,mixed> $payment_token The payment token.
	 *
	 * @return bool True if payment was successful, false otherwise.
	 */
	public function process_ipn_subscription_migration(
		Product $product,
		WP_User $user,
		array $payment_token
	): bool {
		$this->log_info( 'Processing PayPal Standard (IPN) subscription migration for product ID[' . $product->get_id() . '] and user ID[' . $user->ID . ']' );

		// Check if the payment token exists.
		$token_id = Cast::to_string( Arr::get( $payment_token, 'id', '' ) );

		if ( empty( $token_id ) ) {
			$this->log_error( 'Invalid payment token for product: ' . $product->get_id() );

			return false;
		}

		$data_builder = App::get( Order_Data::class );

		if ( ! $data_builder instanceof Order_Data ) {
			$this->log_error( 'Data builder not found.' );

			return false;
		}

		$order_data = $data_builder->build(
			[ $product->get_id() ],
			$user,
			true,
			false
		);

		// Mark if it's a card payment.
		$order_data['use_card_fields'] = true;

		// Add the payment token to the order data.
		$order_data['vault_id']    = $token_id;
		$order_data['customer_id'] = Cast::to_string( Arr::get( $payment_token, 'customer.id', '' ) );

		// Create the order.
		$client = App::get( Client::class );

		if ( ! $client instanceof Client ) {
			$this->log_error( 'Client not found.' );

			return false;
		}

		if ( $this->is_test_mode() ) {
			$client->use_sandbox();
		} else {
			$client->use_production();
		}

		$order = $client->create_order( $order_data );

		if ( is_wp_error( $order ) ) {
			$this->log_error( 'Error creating order: ' . $order->get_error_message() );

			return false;
		}

		$order_status = App::get( Order_Status::class );

		if ( ! $order_status instanceof Order_Status ) {
			$this->log_error( 'Order status helper not found.' );

			return false;
		}

		if ( ! $order_status->is_payment_completed( Cast::to_string( Arr::get( $order, 'status', '' ) ) ) ) {
			$this->log_error( 'Order status is not successful. Order ID: ' . Cast::to_string( Arr::get( $order, 'id', '' ) ) );

			// Do not cancel the subscription here. It will be cancelled in the process_subscription_failure() method.
			return false;
		}

		try {
			// Create a transaction without a trial.
			$transaction_meta = Learndash_Transaction_Meta_DTO::create(
				[
					Transaction::$meta_key_gateway_name   => $this::get_name(),
					Transaction::$meta_key_is_test_mode   => $this->is_test_mode(),
					Transaction::$meta_key_price_type     => $product->is_price_type_subscribe()
						? LEARNDASH_PRICE_TYPE_SUBSCRIBE
						: LEARNDASH_PRICE_TYPE_PAYNOW,
					Transaction::$meta_key_pricing_info   => $product->get_pricing(),
					Transaction::$meta_key_has_trial      => false, // No trial for subscription migration.
					Transaction::$meta_key_has_free_trial => false, // No free trial for subscription migration.
					Transaction::$meta_key_gateway_transaction => Learndash_Transaction_Gateway_Transaction_DTO::create(
						[
							'id'          => Arr::get( $order, 'id', '' ),
							'customer_id' => Arr::get( $order, 'payer.payer_id', '' ),
							'event'       => [
								'payment_token' => [
									'gateway'     => 'paypal_checkout',
									'token'       => Cast::to_string( Arr::get( $payment_token, 'id', '' ) ),
									'customer_id' => Cast::to_string( Arr::get( $payment_token, 'customer.id', '' ) ),
									'type'        => 'card',
								],
							],
						]
					),
				]
			);

			$this->record_transaction(
				$transaction_meta->to_array(),
				$product->get_post(),
				$user
			);

			// Add access to the product.
			$this->add_access_to_products( [ $product ], $user );
			$this->log_info( 'Added access to product.' );

			$this->log_info( 'Recorded transaction for subscription migration. Product ID: ' . $product->get_id() );
		} catch ( Learndash_DTO_Validation_Exception $e ) {
			$this->log_error( 'Error recording transaction for subscription migration: ' . $e->getMessage() . '. Product ID: ' . $product->get_id() );
		}

		return true;
	}

	/**
	 * Returns the button keys handled by this gateway.
	 *
	 * @since 4.25.0
	 *
	 * @return string[] Button keys.
	 */
	public function get_button_keys(): array {
		return [
			static::get_name() . self::PAYPAL_BUTTON_KEY_SUFFIX,
			static::get_name() . self::CARD_BUTTON_KEY_SUFFIX,
		];
	}

	/**
	 * Returns the cancel URL from the reference ID.
	 *
	 * @since 4.25.0
	 *
	 * @param int    $user_id      User ID.
	 * @param string $reference_id Reference ID.
	 *
	 * @return string Cancel URL.
	 */
	public function get_cancel_url_from_reference_id( int $user_id, string $reference_id ): string {
		$settings = self::get_settings();

		return self::get_url_fail(
			$this->get_products_from_reference_id( $user_id, $reference_id ),
			Cast::to_string( Arr::get( $settings, 'return_url', '' ) )
		);
	}

	/**
	 * Returns the success URL from the reference ID.
	 *
	 * @since 4.25.0
	 *
	 * @param int    $user_id      User ID.
	 * @param string $reference_id Reference ID.
	 * @param string $order_id     Order ID.
	 *
	 * @return string Success URL.
	 */
	public function get_success_url_from_reference_id( int $user_id, string $reference_id, string $order_id = '' ): string {
		$settings = self::get_settings();

		return add_query_arg(
			[
				'ld_paypal_checkout_success' => 1,
				'pp_order_id'                => $order_id,
			],
			self::get_url_success(
				$this->get_products_from_reference_id( $user_id, $reference_id ),
				Cast::to_string( Arr::get( $settings, 'return_url', '' ) )
			)
		);
	}

	/**
	 * Returns the transactions (orders) from the reference ID.
	 *
	 * @since 4.25.0
	 *
	 * @param int    $user_id      User ID.
	 * @param string $reference_id Reference ID.
	 *
	 * @return bool True if there is a transaction, false otherwise.
	 */
	public function has_transaction_from_reference_id( int $user_id, string $reference_id ): bool {
		$products = $this->get_products_from_reference_id( $user_id, $reference_id );

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

		// Check if there are any transactions for the first product.

		$post_type  = LDLMS_Post_Types::get_post_type_slug( LDLMS_Post_Types::TRANSACTION );
		$product_id = $products[0]->get_id();

		return DB::table( 'posts' )
			->join(
				function ( JoinQueryBuilder $builder ) {
					$builder->innerJoin( 'postmeta', 'payment_processor_meta' )
						->on( 'payment_processor_meta.post_id', 'id' )
						->andOn( 'payment_processor_meta.meta_key', 'ld_payment_processor', true )
						->andOn( 'payment_processor_meta.meta_value', static::get_name(), true );
				}
			)
			->join(
				function ( JoinQueryBuilder $builder ) use ( $product_id ) {
					$builder->innerJoin( 'postmeta', 'product_id_meta' )
						->on( 'product_id_meta.post_id', 'id' )
						->andOn( 'product_id_meta.meta_key', 'post_id', true )
						->andOn( 'product_id_meta.meta_value', (string) $product_id, true );
				}
			)
			->where( 'post_type', $post_type )
			->where( 'post_author', $user_id )
			->where( 'post_status', 'publish' )
			->count() > 0;
	}

	/**
	 * Returns the products from the reference ID.
	 *
	 * @since 4.25.0
	 *
	 * @param int    $user_id      User ID.
	 * @param string $reference_id Reference ID.
	 *
	 * @return array<Product> Products.
	 */
	protected function get_products_from_reference_id( int $user_id, string $reference_id ): array {
		$data = Arr::wrap( get_user_meta( $user_id, 'ld_paypal_checkout_reference_id_' . $reference_id, true ) );

		return array_filter(
			array_map(
				[ Product::class, 'find' ],
				array_filter(
					Arr::wrap( Arr::get( $data, 'product_ids', [] ) ),
				)
			),
			function ( $product ) {
				return $product instanceof Product;
			}
		);
	}

	/**
	 * Configures gateway.
	 *
	 * @since 4.25.0
	 *
	 * @return void
	 */
	protected function configure(): void {
		$this->settings = self::get_settings();
	}

	/**
	 * Returns true it's a test mode, otherwise false.
	 *
	 * @since 4.25.0
	 *
	 * @return bool
	 */
	protected function is_test_mode(): bool {
		return 1 === Cast::to_int( Arr::get( $this->settings, 'test_mode', 0 ) );
	}

	/**
	 * Returns payment button HTML markup.
	 *
	 * @since 4.25.0
	 *
	 * @param array<mixed> $params Payment params.
	 * @param WP_Post      $post   Post being processing.
	 *
	 * @return string Payment button HTML markup.
	 */
	protected function map_payment_button_markup( array $params, WP_Post $post ): string {
		// We have multiple buttons, so we should override the get_payment_button_markup_for_button_key() method instead.
		return '';
	}

	/**
	 * Returns the payment button HTML markup for a specific button key.
	 *
	 * @since 4.25.0
	 *
	 * @param string       $button_key Button key.
	 * @param WP_Post      $post       Post being processing.
	 * @param array<mixed> $params     Payment params.
	 *
	 * @return string Payment button HTML markup.
	 */
	protected function get_payment_button_markup_for_button_key( string $button_key, WP_Post $post, array $params ): string {
		if ( ! $this->is_supported() ) {
			return '';
		}

		if (
			$button_key === static::get_name() . self::PAYPAL_BUTTON_KEY_SUFFIX
			&& $this->is_payment_method_paypal_active()
		) {
			return Template::get_template(
				'components/payments/paypal/checkout/paypal-button',
				[
					'button_label' => $this->map_payment_button_label(
						__( 'Pay with PayPal', 'learndash' ), // The label is not important here as it will be overridden later in the Modern Registration page.
						$post
					),
					'gateway'      => $this,
					'post'         => $post,
				]
			);
		}

		if (
			$button_key === static::get_name() . self::CARD_BUTTON_KEY_SUFFIX
			&& $this->is_payment_method_card_active()
		) {
			return Template::get_template(
				'components/payments/paypal/checkout/card-button',
				[
					'button_label' => $this->map_payment_button_label(
						__( 'Pay with Credit Card', 'learndash' ), // The label is not important here as it will be overridden later in the Modern Registration page.
						$post
					),
					'gateway'      => $this,
					'post'         => $post,
				]
			);
		}

		// No button found or payment method is not active.
		return '';
	}

	/**
	 * Maps transaction meta.
	 *
	 * @since 4.25.0
	 *
	 * @param array<string,mixed> $data    Data.
	 * @param Product             $product Product.
	 *
	 * @throws \Learndash_DTO_Validation_Exception Transaction data validation exception.
	 *
	 * @return Learndash_Transaction_Meta_DTO
	 */
	protected function map_transaction_meta( $data, Product $product ): Learndash_Transaction_Meta_DTO {
		// Check if this is a subscription with trial.
		$pricing        = $product->get_pricing();
		$has_trial      = ! empty( $pricing->trial_duration_value ) && ! empty( $pricing->trial_duration_length );
		$has_free_trial = $has_trial && $pricing->trial_price <= 0;

		$meta = [
			Transaction::$meta_key_gateway_name        => $this::get_name(),
			Transaction::$meta_key_is_test_mode        => $this->is_test_mode(),
			Transaction::$meta_key_price_type          => $product->is_price_type_subscribe()
				? LEARNDASH_PRICE_TYPE_SUBSCRIBE
				: LEARNDASH_PRICE_TYPE_PAYNOW,
			Transaction::$meta_key_pricing_info        => $pricing,
			Transaction::$meta_key_has_trial           => $has_trial,
			Transaction::$meta_key_has_free_trial      => $has_free_trial,
			Transaction::$meta_key_gateway_transaction => Learndash_Transaction_Gateway_Transaction_DTO::create(
				[
					'id'          => Arr::get( $data, 'id', '' ),
					'customer_id' => Arr::get( $data, 'payer.payer_id', '' ),
					'event'       => $data,
				]
			),
		];

		return Learndash_Transaction_Meta_DTO::create( $meta );
	}
}
