<?php
/**
 * Payments controller.
 *
 * @since 1.0
 */

declare( strict_types=1 );
namespace FeData\FastEvents\RestAPI;

defined( 'ABSPATH' ) || exit;

use FeData\FastEvents\Helpers\EventsHelper;
use FeData\FastEvents\Helpers\OrderHelper;
use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\Mail\MailerInterface;
use FeData\FastEvents\Payment\PaymentFactory;
use FeData\FastEvents\Storage\PluginSettings;
use FeData\FastEvents\Utilities\JsonUtil;
use FeData\FastEvents\Utilities\PluginUtil;
use FeData\FastEvents\Utilities\TimeUtil;
use JetBrains\PhpStorm\NoReturn;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;


/**
 * Payments controller class.
 */
class PaymentController {

	/**
	 * Constructor
	 *
	 * @param PluginSettings $settings      The plugin settings.
	 * @param LoggerFactory  $logger        The logger.
	 * @param EventsHelper   $events_helper The events helper.
	 * @param PaymentFactory $payment       The payment factory.
	 *
	 * @since 3.0.0
	 */
	public function __construct(
		private readonly PluginSettings $settings,
		private readonly LoggerFactory $logger,
		private readonly EventsHelper $events_helper,
		private readonly PaymentFactory $payment,
	) {
	}


	/**
	 * Register the routes for payments.
	 *
	 * @since 1.0
	 * @since 2.2.0 Added coupon check.
	 */
	public function register_routes(): void {
		register_rest_route(
			'fast-events',
			'/v1/payment',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_payment' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'name'        => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'email'       => array(
						'required' => true,
						'type'     => 'string',
						'format'   => 'email',
					),

					'event_info'  => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'event_nonce' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			),
		);

		register_rest_route(
			'fast-events',
			'/v1/payment/cancel/(?P<uid>[\w]{40})',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'payment_cancel' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			'fast-events',
			'/v1/payment/redirect/(?P<uid>[\w]{40})',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'payment_redirect' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			'fast-events',
			'/v1/payment/webhook(?:/(?P<uid>[\w]{40}))?',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'payment_webhook' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			'fast-events',
			'/v1/payment/email/confirm/(?P<uid>[\w]{40})',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'email_confirmation' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			'fast-events',
			'/v1/payment/coupon',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'check_coupon' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'name'        => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'email'       => array(
						'required'          => true,
						'type'              => 'string',
						'format'            => 'email',
						'sanitize_callback' => 'sanitize_email',
					),
					'event_info'  => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'event_nonce' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'event_id'    => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
				),
			),
		);
	}


	/**
	 * Process the order form
	 *
	 * @param WP_REST_Request $request_data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 1.0
	 * @since 1.4.0 Added sender_name and sender_email to query
	 * @since 1.7.1 COMMIT before payment-call to shorten transaction-time
	 * @since 1.8.4 Remove order after failure
	 * @since 2.3.0 Use fast_events_json_encode.
	 * @since 2.4.0 Get User-Agent from utility
	 * @since 3.0.0 Refactored to use the PaymentProviderInterface.
	 */
	public function create_payment( WP_REST_Request $request_data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';

		// Start the transaction.
		$wpdb->query( 'START TRANSACTION' );

		// Check the form input.
		$error = $this->events_helper->validate_form_input( $request_data );
		if ( is_wp_error( $error ) ) {
			$wpdb->query( 'ROLLBACK' );
			$this->events_helper->process_delayed_error();
			return $error;
		}

		// Everything went well -> commit the transaction.
		$wpdb->query( 'COMMIT' );

		// Is a payment needed?
		$total = $this->events_helper->get_amount();
		if ( 0.00 !== $total ) {

			// Create the payment. If successful, it will return an array with the payment_id ('id') and redirect url ('url').
			$payment = $this->payment->create_payment( $this->events_helper, $this->events_helper->get_test_payment_needed() );
			if ( is_wp_error( $payment ) ) {
				// The call failed -> remove all related orders.
				foreach ( $this->events_helper->get_order_ids() as $order_id ) {
					$fevt_container->get( OrderHelper::class )->order_delete( $order_id );
				}
				return $payment;
			}

			// Set the expiration timer if the expiry setting is not 0.
			if ( 0 !== $this->settings->expire() ) {
				as_schedule_single_action(
					TimeUtil::utc_time() + ( $this->settings->expire() * MINUTE_IN_SECONDS ),
					'fast_events_action_payment_expire',
					array(
						'order_id' => $this->events_helper->get_order_ids()[0],
					),
					'fast-events-payment-expire'
				);
			}

			// Update the order with the payment id from the provider and update the input_fields.
			foreach ( $this->events_helper->get_order_ids() as $order_id ) {
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->prefix}fe_event_orders SET payment_id = %s, input_fields = %s WHERE id=%d",
						$payment['id'],
						JsonUtil::fast_events_json_encode( $this->events_helper->get_json_input_fields( $order_id ) ),
						$order_id
					)
				);
			}

			// Redirect the client.
			return rest_ensure_response( array( 'redirect' => $payment['url'] ) );

		} else {
			// Update the order with the updated input_fields.
			foreach ( $this->events_helper->get_order_ids() as $order_id ) {
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = %s WHERE id=%d",
						JsonUtil::fast_events_json_encode( $this->events_helper->get_json_input_fields( $order_id ) ),
						$order_id
					)
				);
			}

			// No payment needed, so we send the email(s) now.
			foreach ( $this->events_helper->get_order_ids() as $order_id ) {
				// phpcs:disable
				$result = $wpdb->get_row(
					$wpdb->prepare(
						"SELECT $order_table.id, $order_table.uid, $order_table.payment_status, $order_table.event_id, " .
						"$order_table.input_fields, $order_table.name, $order_table.email, $order_table.created_at, " .
						"$event_table.sender_name, $event_table.sender_email, " .
						"$event_table.email_bcc, $event_table.email_subject, $event_table.email_body, " .
						"$event_table.confirm_bcc, $event_table.confirm_subject, $event_table.confirm_body, " .
						"$event_table.tickets_needed, $event_table.test_payments_needed, $event_table.seats_needed, $event_table.tracking_needed, " .
						"$event_table.emails_needed, $event_table.confirmation_emails_needed, $event_table.confirmation_timeout, $event_table.redirect, " .
						"$event_table.group_type, $event_table.event_group " .
						"FROM $order_table " .
						"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
						"WHERE $order_table.id = %d",
						$order_id
					)
				);
				// phpcs:enable

				// Does the key exist?
				if ( null === $result ) {
					return new WP_Error( 'not_found', esc_html__( 'Order not found', 'fast-events' ), array( 'status' => 404 ) );
				}

				// Create tickets and send email.
				$this->events_helper->fast_events_create_tickets_and_send_email( $this->events_helper->get_order_ids(), $this->events_helper->get_event_ids(), $result, MailerInterface::FAST_EVENTS_NORMAL_EMAIL, $result->payment_status );
			}

			// Redirect the client.
			return rest_ensure_response( array( 'redirect' => $result->redirect . '?redirect_uid=' . $result->uid ) );
		}
	}


	/**
	 * Process the Payment webhook
	 *
	 * @param WP_REST_Request $request_data Request instance.
	 *
	 * @since 1.0
	 * @since 1.4   Added sender_name and sender_email to query
	 * @since 2.4.0 Get User-Agent from utility
	 * @since 3.0.0 Refactored, now using the new Payment Interface
	 */
	public function payment_webhook( WP_REST_Request $request_data ): void {
		global $wpdb;

		$params = $request_data->get_params();

		// Check for bogus requests: Parameters 'id' and 'uid' must be returned.
		$info = $this->payment->get_uid( $request_data );
		if ( is_wp_error( $info ) ) {
			http_response_code( 200 ); // We don't give back the error!
			die();
		}

		// We have the payment_info, inform the Payment provider all is well.
		http_response_code( 200 );

		// Look up the uid and check if it matches the payment_id from the webhook.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';
		// phpcs:disable
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.uid, $order_table.payment_status, $order_table.event_id, " .
				"$order_table.input_fields, $order_table.name, $order_table.email, $order_table.created_at, $order_table.payment_id, " .
				"$event_table.event_type, $event_table.sender_name, $event_table.sender_email, " .
				"$event_table.email_bcc, $event_table.email_subject, $event_table.email_body, " .
				"$event_table.confirm_bcc, $event_table.confirm_subject, $event_table.confirm_body, " .
				"$event_table.tickets_needed, $event_table.test_payments_needed, $event_table.seats_needed, $event_table.webhooks_needed, $event_table.tracking_needed, " .
				"$event_table.emails_needed, $event_table.confirmation_emails_needed, $event_table.confirmation_timeout, $event_table.redirect, " .
				"$event_table.group_type, $event_table.event_group, $event_table.saas_user_id " .
				"FROM $order_table " .
				"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
				"WHERE $order_table.uid = %s",
				$info['uid']
			)
		);
		// phpcs:enable

		// Does the key exist?
		if ( null === $result ) {
			$this->logger->warning(
				'Cannot find the order with uid "' . $info['uid'] . '" in the database.',
				array(
					'type'           => 'Payment webhook',
					'ip'             => PluginUtil::get_ip(),
					'request_params' => $params,
				)
			);
			http_response_code( 200 );
			die();
		}

		// Check for bogus requests: Order UID is found, but the payment id does not match the one in the webhook.
		if ( ! empty( $info['id'] ) ) {
			$payment_id = sanitize_text_field( $info['id'] );
			if ( $payment_id !== $result->payment_id ) {
				$this->logger->warning(
					sprintf(
						'Order UID:%s, Payment transaction-id mismatch. Expected %s, got %s',
						$result->uid,
						$result->payment_id,
						sanitize_text_field( $info['id'] )
					),
					array(
						'type'           => 'Payment webhook',
						'ip'             => PluginUtil::get_ip(),
						'request_params' => $params,
					)
				);
				http_response_code( 200 );
				die();
			}
		} else {
			$payment_id = $result->payment_id;
		}

		// Most payment providers return the 'status' already in the 'get_uid' method.
		// But for those who don't, we need to get the status from the payment provider.
		if ( array_key_exists( 'status', $info ) ) {
			$json       = json_decode( $result->input_fields, true );
			$new_status = $info['status'];
		} else {
			$payment = $this->payment->get_payment( $payment_id, (int) $result->saas_user_id, $result, (bool) $result->test_payments_needed );
			if ( is_wp_error( $payment ) ) {
				http_response_code( 400 );
				die();
			}

			// The metadata may have been changed.
			$inp_fields = $wpdb->get_var( $wpdb->prepare( "SELECT input_fields FROM {$wpdb->prefix}fe_event_orders WHERE id=%d", $result->id ) );
			$json       = json_decode( $inp_fields, true );
			$new_status = $payment['status'];
		}
		$ids = empty( $json['orders'] ) ? array( (int) $result->id ) : array_map( 'intval', $json['orders'] );

		// Process the order status update.
		if ( 'authorized' === $new_status ) {
			$wpdb->query(
				"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status ='authorized' " .
				'WHERE id IN (' . implode( ',', $ids ) . ')', // phpcs:ignore
			);
			$result->payment_status = 'authorized';
			$this->payment->capture_payment( $payment_id, (int) $result->saas_user_id, $result, (bool) $result->test_payments_needed );
		} elseif ( 'paid' === $new_status && empty( $json['metadata']['has_refunds'] ) && empty( $json['metadata']['has_chargebacks'] ) &&
				empty( $json['metadata']['expired_at'] ) && empty( $json['metadata']['canceled_at'] ) && empty( $json['metadata']['failed_at'] ) ) {

			// Delete the expiration timer if cancellations are supported.
			if ( 0 !== $this->settings->expire() ) {
				foreach ( $ids as $id ) {
					as_unschedule_action( 'fast_events_action_payment_expire', array( 'order_id' => $id ), 'fast-events-payment-expire' );
				}
			}

			// If the order already has the 'paid' status, we return.
			if ( 'paid' === $result->payment_status ) {
				return;
			}

			$wpdb->query(
				"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status = 'paid' " .
				'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
			);

			// Create tickets and send email.
			$this->events_helper->fast_events_create_tickets_and_send_email(
				$json['orders'],
				$json['events'],
				$result,
				MailerInterface::FAST_EVENTS_NORMAL_EMAIL,
				$result->payment_status
			);

		} elseif ( ! empty( $json['metadata']['has_refunds'] ) ) {
			// In the case of multi-select events, we will set all orders to 'refunded'.
			// That's why the docs say it's way better to do a refund in the FE Admin as you can select individual orders in the multi-select event.

			// Delete the expiration timer if cancellations are supported.
			if ( 0 !== $this->settings->expire() ) {
				foreach ( $ids as $id ) {
					as_unschedule_action( 'fast_events_action_payment_expire', array( 'order_id' => $id ), 'fast-events-payment-expire' );
				}
			}

			// Restock the tickets if the status is 'paid'.
			if ( 'paid' === $result->payment_status ) {
				$wpdb->query(
					"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status ='refunded' " .
					'WHERE id IN (' . implode( ',', $ids ) . ')', // phpcs:ignore
				);
				$this->events_helper->fast_events_delete_tickets( $json['events'], $json['orders'] );
			}
		} elseif ( ! empty( $json['metadata']['has_chargebacks'] ) ) {

			// Delete the expiration timer if cancellations are supported.
			if ( 0 !== $this->settings->expire() ) {
				foreach ( $ids as $id ) {
					as_unschedule_action( 'fast_events_action_payment_expire', array( 'order_id' => $id ), 'fast-events-payment-expire' );
				}
			}

			// Set the status to 'chargeback'.
			$wpdb->query(
				"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status = 'chargeback' " .
				'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
			);

			// Only delete the tickets if not already done.
			if ( 'paid' === $result->payment_status ) {
				$this->events_helper->fast_events_delete_tickets( $json['events'], $json['orders'] );
			}
		} elseif ( in_array( $new_status, array( 'canceled', 'expired', 'failed' ), true ) ) {

			// Delete the expiration timer if cancellations are supported.
			if ( 0 !== $this->settings->expire() ) {
				foreach ( $ids as $id ) {
					as_unschedule_action( 'fast_events_action_payment_expire', array( 'order_id' => $id ), 'fast-events-payment-expire' );
				}
			}

			// Restock the tickets if the status is not in ('failed', 'canceled', 'expired').
			if ( ! in_array( $result->payment_status, array( 'canceled', 'expired', 'failed' ), true ) && empty( $json['metadata']['expired_at'] ) ) {
				$this->events_helper->fast_events_delete_tickets( $json['events'], $json['orders'] );
			}
			if ( ! in_array( $result->payment_status, array( 'canceled', 'expired', 'failed' ), true ) ) {
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status = %s " .
						'WHERE id IN (' . implode( ',', $ids ) . ')', // phpcs:ignore
						$new_status
					)
				);
			}
		}
	}


	/**
	 * Process the confirmation email
	 *
	 * @param WP_REST_Request $data Request instance.
	 *
	 * @since 1.0
	 * @since 1.4 Added sender_name and sender_email to query
	 */
	#[NoReturn]
	public function email_confirmation( WP_REST_Request $data ): void {
		global $wpdb;

		// Look up the uid in the orders table.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';
		// phpcs:disable
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.uid, $order_table.payment_status, $order_table.event_id, " .
				"$order_table.input_fields, $order_table.name, $order_table.email, $order_table.created_at, $order_table.payment_id, " .
				"$event_table.sender_name, $event_table.sender_email," .
				"$event_table.email_bcc, $event_table.email_subject, $event_table.email_body, " .
				"$event_table.confirm_bcc, $event_table.confirm_subject, $event_table.confirm_body, " .
				"$event_table.tickets_needed, $event_table.test_payments_needed, $event_table.seats_needed, $event_table.tracking_needed, " .
				"$event_table.emails_needed, $event_table.confirmation_emails_needed, $event_table.confirmation_timeout, $event_table.redirect, " .
				"$event_table.confirm_redirect, $event_table.group_type, $event_table.event_group " .
				"FROM $order_table " .
				"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
				"WHERE $order_table.uid = %s",
				$data['uid']
			)
		);
		// phpcs:enable
		if ( null === $result ) {
			http_response_code( 404 );
			die();
		}

		// Did we already confirm the booking?
		if ( str_ends_with( $result->payment_status, 'confirmed' ) ) {
			header( 'Location: ' . $result->confirm_redirect );
			die();
		}

		// Get the details.
		$new_status = $result->payment_status . ' - confirmed';
		$json       = json_decode( $result->input_fields );
		$order_ids  = $json->orders;
		$event_ids  = $json->events;

		// Update the order status.
		$res = $wpdb->update( $order_table, array( 'payment_status' => $new_status ), array( 'id' => $result->id ) );
		if ( false === $res || 0 === $res ) {
			$this->logger->warning(
				'Could not update payment status for order #' . $result->id,
				array(
					'order_id'   => (int) $result->id,
					'event_id'   => (int) $result->event_id,
					'old_status' => $result->payment_status,
					'new_status' => $new_status,
					'wpdb_error' => $wpdb->last_error,
				)
			);
		}

		// Create tickets and send email.
		$this->events_helper->fast_events_create_tickets_and_send_email( $order_ids, $event_ids, $result, MailerInterface::FAST_EVENTS_CONFIRM_EMAIL, $new_status );

		// Redirect.
		header( 'Location: ' . $result->confirm_redirect );
		die();
	}


	/**
	 * The client has canceled the payment.
	 * Check whether the ticket exists and is still open; if so, restock the ticket and redirect the user to the cancellation URL.
	 *
	 * @param WP_REST_Request $data Request instance.
	 *
	 * @since 3.0.0
	 */
	#[NoReturn]
	public function payment_cancel( WP_REST_Request $data ): void {
		global $wpdb;

		$info = $data->get_params();

		// Look up the uid and check if it matches the payment_id from the webhook.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';
		// phpcs:disable
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.uid, $order_table.payment_status, $order_table.input_fields, $order_table.event_id, " .
				"$order_table.created_at, $order_table.payment_id, $event_table.test_payments_needed, $event_table.saas_user_id " .
				"FROM $order_table " .
				"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
				"WHERE $order_table.uid = %s",
				$data['uid']
			)
		);
		// phpcs:enable

		// Does the key exist?
		if ( null === $result ) {
			$this->logger->warning(
				'Cancel payment -> cannot find the order in the database.',
				array(
					'type'           => 'Payment webhook',
					'uid'            => $info['uid'],
					'ip'             => PluginUtil::get_ip(),
					'request_params' => $info,
				)
			);
			wp_redirect( $this->settings->cancel_url() ); // phpcs:ignore
			exit();
		}

		// Check if the order is still open.
		if ( 'open' === $result->payment_status ) {
			$json = json_decode( $result->input_fields, true );
			$ids  = empty( $json['orders'] ) ? array( (int) $result->id ) : array_map( 'intval', $json['orders'] );

			// Save the cancellation time.
			$date = current_time( 'mysql' );

			// Delete the expiration timer.
			if ( 0 !== $this->settings->expire() ) {
				foreach ( $ids as $id ) {
					as_unschedule_action( 'fast_events_action_payment_expire', array( 'order_id' => $id ), 'fast-events-payment-expire' );
				}
			}

			// Set the status to 'canceled'.
			$wpdb->query(
				"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status = 'canceled', " .
				"input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.canceled_at', '$date' ) " . // phpcs:ignore
				'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
			);

			$this->events_helper->fast_events_delete_tickets( $json['events'], $json['orders'] );

			// Log the error for each order.
			foreach ( array_map( 'intval', $json['orders'] ) as $key => $order_id ) {
				$this->logger->warning(
					esc_html__( 'The user cancelled the order', 'fast-events' ),
					array(
						'type'     => 'Order cancelled',
						'order_id' => (int) $order_id,
						'event_id' => (int) $json['events'][ $key ],
					)
				);
			}
		}

		// Redirect to the cancel URL.
		wp_redirect( $this->settings->cancel_url() ); // phpcs:ignore
		exit();
	}

	/**
	 * Clients end here after the payment.
	 * Check if we have been informed on the payment status and redirect to either the errorpage or success-page
	 *
	 * @param WP_REST_Request $data Request instance.
	 *
	 * @since 1.0
	 * @since 2.3.0 Added sleep to give the payment provider a chance to update the payment status.
	 */
	#[NoReturn]
	public function payment_redirect( WP_REST_Request $data ): void {
		global $wpdb;

		// Don't cache.
		nocache_headers();

		// Prepare the query.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';
		$query       = $wpdb->prepare( // phpcs:disable
			"SELECT $order_table.id, $order_table.uid, $order_table.payment_id, $order_table.input_fields, " .
			"$order_table.payment_status, $event_table.test_payments_needed, $event_table.saas_user_id, $event_table.redirect " .
			"FROM $order_table " .
			"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
			"WHERE $order_table.uid = %s",
			$data['uid']
		); // phpcs:enable

		// Do we need to wait for the payment to be processed?
		if ( ! $this->settings->payment_wait() ) {
			$result = $wpdb->get_row( $query ); // phpcs:ignore
			if ( null === $result ) {
				wp_redirect( $this->settings->errorpage() ); // phpcs:ignore
				exit();
			}
			wp_redirect( $result->redirect . '?redirect_uid=' . $result->uid ); // phpcs:ignore
			exit();
		}

		// Loop for max 5 seconds to see if it's paid.
		$cnt = 0;
		do {
			usleep( 50000 );
			$result = $wpdb->get_row( $query ); // phpcs:ignore
			if ( null === $result ) {
				wp_redirect( $this->settings->errorpage() ); // phpcs:ignore
				exit();
			}
			++$cnt;
		} while ( 'paid' !== $result->payment_status && $cnt < 101 );

		// Is the order not paid?
		if ( 'paid' !== $result->payment_status ) {
			wp_redirect( $this->settings->errorpage()  . '?redirect_uid=' . $result->uid ); // phpcs:ignore
			exit();
		}

		// Order is paid -> success-page.
		wp_redirect( $result->redirect . '?redirect_uid=' . $result->uid ); // phpcs:ignore
		exit();
	}


	/**
	 * Validate coupon.
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 2.2.0
	 */
	public function check_coupon( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;

		// Check the basics (name and email).
		$params = $data->get_params();
		$values = $this->events_helper->validate_name_and_email( $params );
		if ( is_wp_error( $values ) ) {
			return $values;
		}
		$email = $values[1];

		// Check the nonce.
		$nonce = 'event_nonce_value' . $params['event_info'];
		if ( ! wp_verify_nonce( $params['event_nonce'], $nonce ) ) {
			return new WP_Error( 'missing_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'WordPress nonce error, please reload the orderform and try again', 'fast-events' ), 6 ) ), array( 'status' => 400 ) );
		}
		$event_info = json_decode( $params['event_info'] );

		// Check the event id.
		$event_id = (string) $params['event_id'];
		if ( empty( $event_info->{$event_id} ) ) {
			return new WP_Error( 'invalid_event_id', __( 'Invalid event id', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Check empty coupon code.
		$coupon = $params[ 'coupon-' . $params['event_id'] ];
		if ( empty( $coupon ) ) {
			return new WP_Error( 'empty_coupon_code', __( 'An empty coupon code is not allowed', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Get the event.
		$event = $wpdb->get_row( $wpdb->prepare( "SELECT id, input_fields, coupons_needed FROM {$wpdb->prefix}fe_events WHERE id = %d", $event_id ) );
		if ( empty( $event ) ) {
			/* translators: %d is the event_id */
			return new WP_Error( 'no_event', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Event %d does not exist', 'fast-events' ), 14 ) ), $params['event_id'] ), array( 'status' => 404 ) );
		}

		// Does the event support coupons?
		if ( 1 !== (int) $event->coupons_needed ) {
			return new WP_Error( 'no_coupon_support', __( 'This event does not support coupons', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Process the input fields.
		$json = $this->events_helper->create_input_fields( $params, $event );
		if ( is_wp_error( $json ) ) {
			return $json;
		}

		// Process the coupon.
		$amount = $this->events_helper->check_coupon( $json, $coupon, $event_id, $email );
		if ( is_wp_error( $amount ) ) {
			return $amount;
		}

		return rest_ensure_response( array( 'amount' => $amount ) );
	}
}
