<?php
/**
 * Fast Events Personalise controller.
 *
 * @since 2.1.0
 */

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

defined( 'ABSPATH' ) || exit;

use FeData\FastEvents\Helpers\PersonalisationHelper;
use FeData\FastEvents\Utilities\JsonUtil;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Fast Events Personalisation Controller class.
 */
class PersonaliseController {

	/**
	 * Fast Events PersonaliseController constructor.
	 *
	 * @since 2.1.0
	 *
	 * @param PersonalisationHelper $personalisation Fast Events Personalisation Helper.
	 */
	public function __construct( private readonly PersonalisationHelper $personalisation ) {}


	/**
	 * Register the routes for the objects of the controller.
	 *
	 * @since 2.1.0
	 */
	public function register_routes(): void {

		// Read or update personalised fields.
		register_rest_route(
			'fast-events',
			'/v1/personalise/(?P<id>\d+)/(?P<nonce>[a-f0-9]{10})/(?P<hash>[a-f0-9]{40})',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'personalise_get_input' ),
					'permission_callback' => '__return_true',
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'personalise_set_input' ),
					'permission_callback' => '__return_true',
				),
			)
		);

		// Personalise invoice data.
		register_rest_route(
			'fast-events',
			'/v1/personalise/invoice/(?P<nonce>[a-f0-9]{10})/(?P<uid>[\w]{40})',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'invoice_save_data' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'company'  => array(
						'required'          => true,
						'type'              => 'string',
						'maxLength'         => 50,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'vat'      => array(
						'required'          => true,
						'type'              => 'string',
						'maxLength'         => 50,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'name'     => array(
						'required'          => true,
						'type'              => 'string',
						'maxLength'         => 50,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'street'   => array(
						'required'          => true,
						'type'              => 'string',
						'maxLength'         => 50,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'postcode' => array(
						'required'          => true,
						'type'              => 'string',
						'maxLength'         => 50,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'city'     => array(
						'required'          => true,
						'type'              => 'string',
						'maxLength'         => 50,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			),
		);

		// Get a share link for a set of ticket ids to personalise.
		register_rest_route(
			'fast-events',
			'/v1/personalise/share/(?P<nonce>[a-f0-9]{10})/(?P<uid>[\w]{40})',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'share_get_link' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'ids'    => array(
						'required' => true,
						'type'     => 'string',
						'pattern'  => '^\d+(?:-\d+)*$',
					),
					'nonces' => array(
						'required' => true,
						'type'     => 'string',
						'pattern'  => '^[a-f0-9]{10}+(?:-[a-f0-9]{10}+)*$',
					),
					'keys'   => array(
						'required' => true,
						'type'     => 'string',
						'pattern'  => '^\d+(?:-\d+)*$',
					),
				),
			),
		);

		// Get a download share link for a set of ticket ids.
		register_rest_route(
			'fast-events',
			'/v1/personalise/share/download/(?P<nonce>[a-f0-9]{10})/(?P<uid>[\w]{40})',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'share_get_download_link' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'ids'    => array(
						'required' => true,
						'type'     => 'string',
						'pattern'  => '^\d+(?:-\d+)*$',
					),
					'hashes' => array(
						'required' => true,
						'type'     => 'string',
						'pattern'  => '^[a-f0-9]{40}+(?:-[a-f0-9]{40}+)*$',
					),
					'keys'   => array(
						'required' => true,
						'type'     => 'string',
						'pattern'  => '^\d+(?:-\d+)*$',
					),
				),
			),
		);
	}


	/**
	 * Retrieve personalised input from a specified qrcode_id.
	 *
	 * @param WP_REST_Request $data Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.1.0
	 */
	public function personalise_get_input( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;

		if ( ! $this->personalisation->validate_update_nonce_hash( $data['id'], $data['nonce'], $data['hash'] ) ) {
			return new WP_Error( 'invalid_hash_or_nonce', __( 'The link you followed is invalid or has expired', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Get the ticket.
		$etable = $wpdb->prefix . 'fe_events';
		$qtable = $wpdb->prefix . 'fe_event_qrcodes';
		$ticket = $wpdb->get_row( "SELECT $qtable.*, $etable.input_fields FROM $qtable INNER JOIN $etable ON $etable.id = $qtable.event_id WHERE $qtable.id = " . $data['id'] ); // phpcs:ignore
		if ( empty( $ticket ) ) {
			return new WP_Error( 'no_ticket', __( 'Ticket not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Get the personalised data and check the locked status.
		$response           = array();
		$personalised_input = $this->personalisation->get_input( $ticket->personalised_input );
		if ( isset( $personalised_input->locked ) && $personalised_input->locked ) {
			$response['locked'] = true;
		} else {
			$response['locked'] = false;
		}
		$response['data'] = (array) $personalised_input->data;

		// Get the personalise fields per ticket_type and the input fields definitions.
		list( $pers_fields, $inp ) = $this->personalisation->get_personalise_fields( $ticket->event_id, $ticket->order_id, $ticket->input_fields );

		// Is there anything to personalise?
		if ( empty( $pers_fields[ $ticket->ticket_type ] ) ) {
			$response['no_fields'] = true;
		} else {
			$response['no_fields'] = false;

			// Generate html for fields.
			$html = '';
			foreach ( $pers_fields[ $ticket->ticket_type ] as $field ) {
				$html .= '<div class="mb-3 row"><div class="col-12"><div class="form-floating">';
				$item  = $inp[ $field ];
				if ( 'select' === $item->type ) {
					$html         .= '<select class="form-select" id="' . $field . '" name="' . $field . '" aria-label="' . $field . '">';
					$select_values = explode( ',', $item->value );
					$default       = $personalised_input->data->{$field} ?? '';
					foreach ( $select_values as $key => $select_value ) {
						$html .= '<option value="' . $select_value . '"';
						$html .= ( ( 0 === $key && empty( $default ) ) || $default === $select_value ) ? ' selected>' : '>';
						$html .= $select_value . '</option>';
					}
					$html .= '</select><label for="' . $field . '">' . $field . '</label>';
				} else {
					$html .= '<input type="' . $item->type . '" class="form-control" id="' . $field . '" name="' . $field . '"';
					if ( 'text' === $item->type ) {
						$html .= empty( $item->min ) ? '' : ( ' minlength="' . $item->min . '"' );
						$html .= empty( $item->max ) ? ' maxlength="50"' : ( ' maxlength="' . $item->max . '"' );
					} else {
						$html .= empty( $item->min ) ? '' : ( ' min="' . $item->min . '"' );
						$html .= empty( $item->max ) ? '' : ( ' max="' . $item->max . '"' );
					}
					$html .= ' placeholder="' . ucfirst( $field ) . '"';
					$html .= ' value="' . ( $personalised_input->data->{$field} ?? '' ) . '">';
					$html .= '<label for="' . $field . '">' . ucfirst( $field ) . '</label>';
				}
				$html .= '</div></div></div>';
			}
			$response['html'] = $html;
		}

		return rest_ensure_response( $response );
	}


	/**
	 * Set the personalised input for the specified qrcode_id.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	public function personalise_set_input( WP_REST_Request $request ): WP_Error|WP_REST_Response {
		global $wpdb;

		if ( ! $this->personalisation->validate_update_nonce_hash( $request['id'], $request['nonce'], $request['hash'] ) ) {
			return new WP_Error( 'invalid_hash_or_nonce', __( 'The link you followed is invalid or has expired', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Get the ticket.
		$etable = $wpdb->prefix . 'fe_events';
		$qtable = $wpdb->prefix . 'fe_event_qrcodes';
		$ticket = $wpdb->get_row( "SELECT $qtable.*, $etable.input_fields FROM $qtable INNER JOIN $etable ON $etable.id = $qtable.event_id WHERE $qtable.id = " . $request['id'] ); // phpcs:ignore
		if ( empty( $ticket ) ) {
			return new WP_Error( 'no_ticket', __( 'Ticket not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Get the personalised data and check the locked status.
		$personalised_input = $this->personalisation->get_input( $ticket->personalised_input );
		if ( isset( $personalised_input->locked ) && $personalised_input->locked ) {
			/* translators: 1: ticket name  */
			return new WP_Error( 'ticket_locked', __( 'This ticket is locked and can no longer be edited.', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Get the personalise fields per ticket_type and the input fields definitions.
		list( $pers_fields, $inp ) = $this->personalisation->get_personalise_fields( $ticket->event_id, $ticket->order_id, $ticket->input_fields );

		// Is there anything to personalise?
		if ( empty( $pers_fields[ $ticket->ticket_type ] ) ) {
			/* translators: 1: ticket name  */
			return new WP_Error( 'no_personalisation', sprintf( __( 'Ticket type [%1$s] has no fields to personalise', 'fast-events' ), $ticket->ticket_type ), array( 'status' => 400 ) );
		}

		// Are all the fields present in the request and input field definitions (just to be sure)?
		foreach ( $pers_fields[ $ticket->ticket_type ] as $field ) {
			if ( ! isset( $request[ $field ] ) || ! isset( $inp[ $field ] ) ) {
				/* translators: 1: name of a personalization field that is missing */
				return new WP_Error( 'field_not_found', sprintf( __( 'Personalisation field [%1$s] not found', 'fast-events' ), $field ), array( 'status' => 400 ) );
			}
		}

		// Validate the input.
		foreach ( $pers_fields[ $ticket->ticket_type ] as $field ) {
			$item = $inp[ $field ];

			// Check if the field is required and has a value.
			if ( $item->required && empty( $request[ $field ] ) ) {
				/* translators: 1: name of a personalization field that is missing */
				return new WP_Error( 'field_required', sprintf( __( 'Personalisation field [%1$s] is empty', 'fast-events' ), $field ), array( 'status' => 400 ) );
			}

			// Check the 'select' type.
			if ( 'select' === $item->type && ! empty( $request[ $field ] ) ) {
				$select_values = explode( ',', $item->value );
				if ( ! in_array( sanitize_text_field( $request[ $field ] ), $select_values, true ) ) {
					/* translators: 1: name of a personalization field that is missing */
					return new WP_Error( 'field_illegal', sprintf( __( 'Personalisation field [%1$s] has an illegal value', 'fast-events' ), $field ), array( 'status' => 400 ) );
				}
			}

			// Check the length fields for text.
			if ( 'text' === $item->type ) {
				if ( ! empty( $item->min ) && strlen( $request[ $field ] ) < intval( $item->min ) ) {
					/* translators: %1$s is the input field name and %2$s is the minimum length */
					return new WP_Error( 'minimum_length_error', sprintf( __( 'The length of [%1$s] is below %2$s', 'fast-events' ), $field, $item->min ), array( 'staus' => 400 ) );
				}
				if ( ! empty( $item->max ) && strlen( $request[ $field ] ) > intval( $item->max ) ) {
					/* translators: %1$s is the input field name and %2$s is the maximum length */
					return new WP_Error( 'maximum_length_error', sprintf( __( 'The length of [%1$s] is greater then %2$s', 'fast-events' ), $field, $item->max ), array( 'staus' => 400 ) );
				}
			} elseif ( 'number' === $item->type ) {
				if ( ! empty( $request[ $field ] ) && ! is_numeric( $request[ $field ] ) ) {
					/* translators: %1$s is the input field name */
					return new WP_Error( 'format_error', sprintf( __( 'Non-numeric value for [%1$s]', 'fast-events' ), $field ), array( 'staus' => 400 ) );
				}
				if ( ! empty( $request[ $field ] ) ) {
					if ( ! empty( $item->min ) && intval( $request[ $field ] ) < intval( $item->min ) ) {
						/* translators: %1$s is the input name and %2$s is the minimum value */
						return new WP_Error( 'minimum_value_error', sprintf( __( 'The value of [%1$s] is below %2$s', 'fast-events' ), $field, $item->min ), array( 'staus' => 400 ) );
					}
					if ( ! empty( $item->max ) && intval( $request[ $field ] ) > intval( $item->max ) ) {
						/* translators: %1$s is the input field name and %2$s is the maximum length */
						return new WP_Error( 'maximum_value_error', sprintf( __( 'The value of [%1$s] is greater then %2$s', 'fast-events' ), $field, $item->max ), array( 'staus' => 400 ) );
					}
				}
			} elseif ( 'date' === $item->type ) {
				if ( ! empty( $request[ $field ] ) ) {
					$date = date_create_from_format( 'Y-m-d', $request[ $field ] );
					if ( false === $date ) {
						/* translators: %1$s is the input field name */
						return new WP_Error( 'invalid_date', sprintf( __( 'Invalid date field [%1$s], format must be "YYYY-MM-DD"', 'fast-events' ), $field ), array( 'staus' => 400 ) );
					}

					if ( ! empty( $item->min ) ) {
						$date_min = date_create_from_format( 'Y-m-d', $item->min );
						if ( $date < $date_min ) {
							/* translators: %1$s is the input field name and %2$s is the minimum date */
							return new WP_Error( 'minimum_date_error', sprintf( __( 'The date of [%1$s] must be later than %2$s', 'fast-events' ), $field, $item->min ), array( 'staus' => 400 ) );
						}
					}
					if ( ! empty( $item->max ) ) {
						$date_max = date_create_from_format( 'Y-m-d', $item->max );
						if ( $date > $date_max ) {
							/* translators: %1$s is the input field name and %2$s is the maximum date */
							return new WP_Error( 'maximum_date_error', sprintf( __( 'The date of [%1$s] must be before %2$s', 'fast-events' ), $field, $item->max ), array( 'staus' => 400 ) );
						}
					}
				}
			} elseif ( 'time' === $item->type ) {
				if ( ! empty( $request[ $field ] ) ) {
					$date = date_create_from_format( 'H:i', $request[ $field ] );
					if ( false === $date ) {
						/* translators: %1$s is the input field name */
						return new WP_Error( 'invalid_time', sprintf( __( 'Invalid time field [%1$s], format must be "HH:MM"', 'fast-events' ), $field ), array( 'staus' => 400 ) );
					}

					if ( ! empty( $item->min ) ) {
						$date_min = date_create_from_format( 'H:i', $item->min );
						if ( $date < $date_min ) {
							/* translators: %1$s is the input field name and %2$s is the minimum time */
							return new WP_Error( 'minimum_time_error', sprintf( __( 'The time of [%1$s] must be later than %2$s', 'fast-events' ), $field, $item->min ), array( 'staus' => 400 ) );
						}
					}
					if ( ! empty( $item->max ) ) {
						$date_max = date_create_from_format( 'H:i', $item->max );
						if ( $date > $date_max ) {
							/* translators: %1$s is the input field name and %2$s is the maximum time */
							return new WP_Error( 'maximum_time_error', sprintf( __( 'The time of [%1$s] must be before %2$s', 'fast-events' ), $field, $item->max ), array( 'staus' => 400 ) );
						}
					}
				}
			}
		}

		// Save the data.
		foreach ( $pers_fields[ $ticket->ticket_type ] as $field ) {
			$personalised_input->data->{$field} = sanitize_text_field( $request[ $field ] );
		}
		$personalised_input->personalised = true;
		$personalised_input->locked       = true; // For now, we don't use this separate feature.
		$resp                             = $wpdb->query(
			'UPDATE ' . $wpdb->prefix . 'fe_event_qrcodes SET personalised_input = \'' .
			JsonUtil::fast_events_json_encode( $personalised_input ) . '\' WHERE id = ' . $request['id'] // phpcs:ignore
		);
		if ( false === $resp ) {
			$error = $wpdb->last_error;

			return new WP_Error( 'failed', __( 'Update ticket personalisation failed: ', 'fast-events' ) . $error, array( 'status' => 400 ) );
		}
		if ( 0 === $resp ) {
			return new WP_Error( 'nothing_to_update', __( 'Ticket personalisation: nothing to update', 'fast-events' ), array( 'status' => 400 ) );
		}

		return rest_ensure_response( array( 'data' => (array) $personalised_input->data ) );
	}

	/**
	 * Save the invoice data.
	 *
	 * @param WP_REST_Request $data Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	public function invoice_save_data( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;

		if ( ! wp_verify_nonce( $data['nonce'], $data['uid'] ) ) {
			return new WP_Error( 'invalid_nonce', __( 'The link you followed is invalid or has expired', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Check for empty fields.
		if ( empty( $data['name'] ) || empty( $data['street'] ) || empty( $data['postcode'] ) || empty( $data['city'] ) ) {
			return new WP_Error( 'empty_fields', __( 'Empty fields are not allowed', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Check the locked checkbox.
		if ( ! isset( $data['locked'] ) ) {
			return new WP_Error( 'not_agreed', __( 'You have not agreed to the terms.', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Get the order.
		$order = $wpdb->get_row( 'SELECT id, event_id, input_fields FROM ' . $wpdb->prefix . 'fe_event_orders WHERE uid = \'' . $data['uid'] . '\' FOR UPDATE' ); // phpcs:ignore
		if ( empty( $order ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'no_order', __( 'Order not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Get the input data.
		$inp = $this->personalisation->get_order_input( $order->input_fields, $order->event_id, $order->id );
		if ( isset( $inp->invoice->locked ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'data_locked', __( 'The invoice data is locked and cannot be changed', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Save the data.
		$inp->invoice->company  = $data['company'];
		$inp->invoice->vat      = $data['vat'];
		$inp->invoice->name     = $data['name'];
		$inp->invoice->street   = $data['street'];
		$inp->invoice->postcode = $data['postcode'];
		$inp->invoice->city     = $data['city'];
		$inp->invoice->locked   = true;
		$resp                   = $wpdb->query(
			'UPDATE ' . $wpdb->prefix . 'fe_event_orders SET input_fields = \'' .
			JsonUtil::fast_events_json_encode( $inp ) . '\' WHERE id = ' . $order->id // phpcs:ignore
		);
		if ( false === $resp ) {
			$error = $wpdb->last_error;
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'failed', __( 'Update invoice data failed: ', 'fast-events' ) . $error, array( 'status' => 400 ) );
		}
		if ( 0 === $resp ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'nothing_to_update', __( 'Invoice data: nothing to update', 'fast-events' ), array( 'status' => 400 ) );
		}

		$wpdb->query( 'COMMIT' );

		return rest_ensure_response( true );
	}


	/**
	 * Retrieve the share link for a list ticket of ids (1 or more).
	 *
	 * @param WP_REST_Request $data Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	public function share_get_link( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;

		if ( ! wp_verify_nonce( $data['nonce'], 'share-' . $data['uid'] . $data['ids'] ) ) {
			return new WP_Error( 'invalid_nonce', __( 'The link you followed is invalid or has expired', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Validate nonces.
		$ids    = explode( '-', $data['ids'] );
		$nonces = explode( '-', $data['nonces'] );
		foreach ( $nonces as $key => $nonce ) {
			if ( ! wp_verify_nonce( $nonce, 'update' . $ids[ $key ] ) ) {
				return new WP_Error( 'invalid_nonce', __( 'Invalid nonce detected in parameters', 'fast-events' ), array( 'status' => 400 ) );
			}
		}

		// Check the keys.
		$keys = explode( '-', $data['keys'] );
		if ( count( $keys ) > count( $ids ) ) {
			return new WP_Error( 'invalid_keys', __( 'Wrong amount of keys', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Generate the list of ticket ids.
		$ticket_ids = array();
		foreach ( $keys as $key ) {
			if ( (int) $key > count( $ids ) ) {
				return new WP_Error( 'invalid_key', __( 'Invalid key value detected', 'fast-events' ), array( 'status' => 400 ) );
			}
			$ticket_ids[] = $ids[ (int) $key ];
		}

		// Get the tickets.
		$tickets = $wpdb->get_results(
			'SELECT id, event_id, order_id, personalised_input FROM ' . $wpdb->prefix . 'fe_event_qrcodes WHERE id IN (' .
			implode( ',', $ticket_ids ) . ') FOR UPDATE' // phpcs:ignore
		);
		if ( empty( $tickets ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'no_ticket', __( 'Ticket(s) not found', 'fast-events' ), array( 'status' => 404 ) );
		}
		if ( count( $ticket_ids ) !== count( $tickets ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'no_ticket', __( 'Not all tickets where found. Try reloading the page.', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Get the redirect url for this event.
		$url = $wpdb->get_var( 'SELECT redirect FROM ' . $wpdb->prefix . 'fe_events WHERE id = ' . $tickets[0]->event_id ); // phpcs:ignore
		if ( empty( $url ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'no_redirect', __( 'Cannot find redirect url', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Update the tickets.
		foreach ( $tickets as $ticket ) {
			$inp         = $this->personalisation->get_input( $ticket->personalised_input );
			$inp->shared = true;
			$resp        = $wpdb->query(
				'UPDATE ' . $wpdb->prefix . 'fe_event_qrcodes SET personalised_input = \'' .
				JsonUtil::fast_events_json_encode( $inp ) . '\' WHERE id = ' . $ticket->id // phpcs:ignore
			);
			if ( false === $resp ) {
				$error = $wpdb->last_error;
				$wpdb->query( 'ROLLBACK' );

				return new WP_Error( 'failed', __( 'Update ticket personalisation failed: ', 'fast-events' ) . $error, array( 'status' => 400 ) );
			}
		}

		$wpdb->query( 'COMMIT' );
		$t_ids = implode( '-', $ticket_ids );

		return rest_ensure_response(
			array(
				'link' => $url . '?ticket_ids=' . $t_ids . '&hash=' . hash_hmac( 'sha1', 'update' . $t_ids, wp_salt() ),
			)
		);
	}


	/**
	 * Retrieve the share download link for a list ticket of ids (1 or more).
	 *
	 * @param WP_REST_Request $data Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.1.0
	 */
	public function share_get_download_link( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;

		if ( ! wp_verify_nonce( $data['nonce'], 'share-' . $data['uid'] . $data['ids'] ) ) {
			return new WP_Error( 'invalid_nonce', __( 'The link you followed is invalid or has expired', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Validate individual hashes.
		$ids    = explode( '-', $data['ids'] );
		$hashes = explode( '-', $data['hashes'] );
		foreach ( $hashes as $key => $hash ) {
			if ( ! $this->personalisation->validate_download_hash( $ids[ $key ], $hash ) ) {
				return new WP_Error( 'invalid_hash', __( 'Invalid hash detected in parameters', 'fast-events' ), array( 'status' => 400 ) );
			}
		}

		// Check the keys.
		$keys = explode( '-', $data['keys'] );
		if ( count( $keys ) > count( $ids ) ) {
			return new WP_Error( 'invalid_keys', __( 'Wrong amount of keys', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Generate the list of ticket ids.
		$ticket_ids = array();
		foreach ( $keys as $key ) {
			if ( (int) $key > count( $ids ) ) {
				return new WP_Error( 'invalid_key', __( 'Invalid key value detected', 'fast-events' ), array( 'status' => 400 ) );
			}
			$ticket_ids[] = $ids[ (int) $key ];
		}

		// Get the tickets.
		$tickets = $wpdb->get_results(
			'SELECT id FROM ' . $wpdb->prefix . 'fe_event_qrcodes WHERE id IN (' . implode( ',', $ticket_ids ) . ')' // phpcs:ignore
		);
		if ( empty( $tickets ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'no_ticket', __( 'Ticket(s) not found', 'fast-events' ), array( 'status' => 404 ) );
		}
		if ( count( $ticket_ids ) !== count( $tickets ) ) {
			return new WP_Error( 'no_ticket', __( 'Not all tickets where found. Try reloading the page.', 'fast-events' ), array( 'status' => 404 ) );
		}

		$t_ids = implode( '-', $ticket_ids );

		return rest_ensure_response(
			array(
				'link' => rest_url( '/fast-events/v1/tickets/' . $t_ids . '/' . $this->personalisation->get_download_hash( $t_ids ) ),
			)
		);
	}
}
