<?php
/**
 * Qrcode scan controller.
 *
 * @since 1.0
 */

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

use FeData\FastEvents\Helpers\PersonalisationHelper;
use FeData\FastEvents\Helpers\SeatsHelper;
use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\Utilities\JsonUtil;
use FeData\FastEvents\Utilities\WebhookUtil;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

defined( 'ABSPATH' ) || exit;

/**
 * Qrcode scan class.
 */
class QrcodeScansController {

	/**
	 * Namespace of this package.
	 *
	 * @since 1.0
	 * @var string
	 */
	private string $namespace = 'fast-events/v1';

	/**
	 * REST base object.
	 *
	 * @since 1.0
	 * @var string
	 */
	private string $rest_base = 'scans';


	/**
	 * Qrcode_Scan constructor.
	 *
	 * @since 1.0
	 *
	 * @param LoggerFactory         $logger          Logger instance.
	 * @param PersonalisationHelper $personalisation Personalisation helper.
	 */
	public function __construct(
		private readonly LoggerFactory $logger,
		private readonly PersonalisationHelper $personalisation
	) {}


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

		// New scan.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'app_scan_request' ),
					'permission_callback' => '__return_true',
					'args'                => array(
						'id' => array(
							'description'       => esc_html__( 'The unique id of the qrcode', 'fast-events' ),
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
							'pattern'           => '^[\w]{16}$',
						),
					),
				),
				'schema' => array( $this, 'get_new_scan_schema' ),
			)
		);

		// Get all scans from a ticket.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\w]{16})',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_all_scans' ),
					'permission_callback' => '__return_true',
					'args'                => array(
						'id' => array(
							'description'       => esc_html__( 'The unique id of the qrcode', 'fast-events' ),
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
							'pattern'           => '^[\w]{16}$',
						),
					),
				),
				'schema' => array( $this, 'get_all_scans_schema' ),
			)
		);
	}


	/**
	 * Find all scans of a ticket.
	 *
	 * @param int    $qrcode_id Unique qrcode id.
	 * @param string $format    See: https://www.php.net/manual/en/function.date.php#refsect1-function.date-parameters.
	 *
	 * @return array|null   Array of scans
	 * @since 2.4.0. Sorting: date has priority over level.
	 *
	 * @since 1.0
	 */
	private function fe_get_all_scans( int $qrcode_id, string $format ): ?array {
		global $wpdb;

		// Look up the scans.
		$scans = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT 0 as scan_level, scan_date, scan_location FROM {$wpdb->prefix}fe_event_qrcodes WHERE id = %d AND scan_date IS NOT NULL " .
				'UNION ' .
				"SELECT scan_level, scan_date, scan_location FROM {$wpdb->prefix}fe_event_entries WHERE qrcode_id = %d " .
				'ORDER BY scan_date, scan_level',
				$qrcode_id,
				$qrcode_id
			)
		);

		// Set the date format and typecast scan_level.
		foreach ( $scans as $index => $value ) {
			$scans[ $index ]->scan_level = (int) $value->scan_level;
			$scans[ $index ]->scan_date  = mysql2date( $format, $scans[ $index ]->scan_date );
		}

		return $scans;
	}


	/**
	 * Find all level 7 (temporarily leave) or level 8 (re-enter) scans of a ticket.
	 *
	 * @since 2.4.0
	 *
	 * @param int $qrcode_id Unique qrcode id.
	 * @return bool Temporarily left or not
	 */
	private function fe_has_temporarily_left( int $qrcode_id ): bool {
		global $wpdb;

		// Look up the scans.
		$level_count = $wpdb->get_row(
			$wpdb->prepare(
				'SELECT SUM(CASE WHEN scan_level = 7 THEN 1 ELSE 0 END) AS cnt_7, ' .
				'SUM(CASE WHEN scan_level = 8 THEN 1 ELSE 0 END) AS cnt_8 FROM ' .
				"{$wpdb->prefix}fe_event_entries WHERE qrcode_id = %d ",
				$qrcode_id
			)
		);

		// None found?
		if ( empty( $level_count ) ) {
			return false;
		}

		return (int) $level_count->cnt_7 > (int) $level_count->cnt_8;
	}


	/**
	 * Somebody left the event -> increase stock by 1, so you can sell another ticket.
	 *
	 * @param int    $event_id Unique id of the event.
	 * @param string $ticket_type Name of the ticket.
	 *
	 * @since 1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 *
	 * @return void
	 */
	private function increase_stock( int $event_id, string $ticket_type ): void {
		global $wpdb;

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

		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT stock, stock_link, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
				$event_id
			)
		);
		if ( empty( $row ) ) {
			$tmp_error = $wpdb->last_error;
			$wpdb->query( 'ROLLBACK' );
			$this->logger->warning(
				'Reload on exit for event #' . $event_id . '(' . $ticket_type . '), read-error: ' . $tmp_error,
				array(
					'type'        => 'SCAN',
					'event_id'    => $event_id,
					'ticket_type' => $ticket_type,
				)
			);
			return;
		}

		// Increase the stock ticket type by 1 if there is stock control.
		$ev_fields = json_decode( $row->input_fields );
		if ( ! empty( $ev_fields->tickets ) ) {
			foreach ( $ev_fields->tickets as $key => $value ) {
				if ( $ticket_type === $value->name && '' !== $value->stock ) {
					++$ev_fields->tickets[ $key ]->stock;
				}
			}
		}

		// Increase event stock by 1 if there is stock control.
		$stock = (int) $row->stock;
		if ( 0 !== $stock ) {
			++$stock;
		}

		// Update event.
		$resp = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->prefix}fe_events SET stock=%d,input_fields=%s WHERE id=%d",
				array(
					$stock,
					JsonUtil::fast_events_json_encode( $ev_fields ),
					$event_id,
				)
			)
		);
		if ( false === $resp ) { // 0 returns are not invalid! Event may not have any stock-control.
			$tmp_error = $wpdb->last_error;
			$wpdb->query( 'ROLLBACK' );
			$this->logger->warning(
				'Reload on exit for event #' . $event_id . '(' . $ticket_type . '), update-error: ' . $tmp_error,
				array(
					'type'        => 'SCAN',
					'event_id'    => $event_id,
					'ticket_type' => $ticket_type,
				)
			);
			return;
		}

		// Check if the stock control is linked to another event.
		if ( 0 !== (int) $row->stock_link ) {
			$linked_event = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT stock, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
					(int) $row->stock_link
				)
			);
			if ( empty( $linked_event ) ) {
				$tmp_error = $wpdb->last_error;
				$wpdb->query( 'ROLLBACK' );
				$this->logger->warning(
					'Reload on exit for linked event #' . $row->stock_link . '(' . $ticket_type . '), read-error: ' . $tmp_error,
					array(
						'type'         => 'SCAN',
						'event_id'     => $event_id,
						'ticket_type'  => $ticket_type,
						'linked_event' => (int) $row->stock_link,
					)
				);
				return;
			}

			// Increase the stock ticket type by 1 if there is stock control.
			$l_fields = json_decode( $linked_event->input_fields );
			if ( ! empty( $l_fields->tickets ) ) {
				foreach ( $l_fields->tickets as $key => $value ) {
					if ( $ticket_type === $value->name && '' !== $value->stock ) {
						++$l_fields->tickets[ $key ]->stock;
					}
				}
			}

			// Increase event stock by 1 if there is stock control.
			$stock = (int) $linked_event->stock;
			if ( 0 !== $stock ) {
				++$stock;
			}

			$resp = $wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}fe_events SET stock=%d,input_fields=%s WHERE id=%d",
					array(
						$stock,
						JsonUtil::fast_events_json_encode( $l_fields ),
						(int) $row->stock_link,
					)
				)
			);
			if ( false === $resp ) { // 0 returns are not invalid! Event may not have any stock-control.
				$tmp_error = $wpdb->last_error;
				$wpdb->query( 'ROLLBACK' );
				$this->logger->warning(
					'Reload on exit for linked event #' . $row->stock_link . '(' . $ticket_type . '), update-error: ' . $tmp_error,
					array(
						'type'         => 'SCAN',
						'event_id'     => $event_id,
						'ticket_type'  => $ticket_type,
						'linked_event' => (int) $row->stock_link,
					)
				);
				return;
			}
		}

		// Commit the transaction.
		$wpdb->query( 'COMMIT' );
	}


	/**
	 * Return all scans of a ticket including the basic order info.
	 *
	 * @since 1.0
	 * @since 1.9.3 Added order_id and qrcode to response.
	 *
	 * @param WP_REST_Request $request_data Request instance.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_all_scans( WP_REST_Request $request_data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		// Sanitize input.
		if ( is_null( $request_data->get_header( 'x_fe_api_key' ) ) ) {
			return new WP_Error( 'not_found', esc_html__( 'Scan App API key not found', 'fast-events' ), array( 'status' => 400 ) );
		}
		$key  = sanitize_text_field( wp_unslash( $request_data->get_header( 'x_fe_api_key' ) ) );
		$code = $request_data['id'];

		// Check key length (must be 16).
		if ( 16 !== strlen( $key ) ) {
			return new WP_Error( 'failed', esc_html__( 'Invalid Scan App API key', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Look up the qrcode.
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$event_table  = $wpdb->prefix . 'fe_events';
		$order_table  = $wpdb->prefix . 'fe_event_orders';

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $qrcode_table.id, $qrcode_table.order_id, $qrcode_table.ticket_type, $qrcode_table.scan_date, $qrcode_table.created_at, " .
				"$qrcode_table.scan_location, $qrcode_table.qrcode, $qrcode_table.event_id, $qrcode_table.personalised_input, $order_table.name, " .
				"$order_table.email, $order_table.input_fields, $event_table.event_name, $event_table.scan_keys, $event_table.seats_needed " .
				"FROM $qrcode_table " .
				"INNER JOIN $order_table ON $order_table.id = $qrcode_table.order_id " .
				"INNER JOIN $event_table ON $event_table.id = $qrcode_table.event_id " .
				"WHERE $qrcode_table.qrcode = %s",
				$code
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

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

		// Get the scan keys of the event.
		$scan = json_decode( $result->scan_keys );

		// Does the key exist?
		if ( empty( $scan->{$key} ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid ticket for this entrance', 'fast-events' ), 1, $key );
			return new WP_Error( 'invalid_key', sanitize_text_field( $error ), array( 'status' => 404 ) );
		}

		// Do we use seats?
		$ticket_type = $result->ticket_type;
		if ( 1 === (int) $result->seats_needed ) {
			$ticket_type .= $fevt_container->get( SeatsHelper::class )->get_seat( (int) $result->event_id, $code );
		}

		/**
		 * Filter the output of the scan.
		 *
		 * @param int $order_id The unique order id.
		 * @param string $qrcode The unique qrcode of the ticket.
		 * @param string $type The ticket type.
		 * @param string $name The name of the qrcode holder.
		 * @param string $email The emailaddress of the qrcode holder.
		 * @param string $date The datetime of the scan.
		 * @param string $location The of the scan.
		 *
		 * @since 1.9.3 Added order_id qrcode (read-only).
		 *
		 * @since 1.0
		 */
		if ( has_filter( 'fast_events_scan_filter_output' ) ) {
			$attr = apply_filters(
				'fast_events_scan_filter_output',
				array(
					'order_id' => (int) $result->order_id,
					'qrcode'   => $result->qrcode,
					'event'    => $result->event_name,
					'type'     => $ticket_type,
					'name'     => $result->name,
					'email'    => $result->email,
					'date'     => $result->scan_date,
					'location' => $result->scan_location,
				)
			);

			return rest_ensure_response(
				array(
					'order_id'      => (int) $result->order_id,
					'qrcode'        => $result->qrcode,
					'event_name'    => sanitize_text_field( $attr['event'] ),
					'ticket_type'   => sanitize_text_field( $attr['type'] ),
					'name'          => sanitize_text_field( $attr['name'] ),
					'email'         => sanitize_text_field( $attr['email'] ),
					'created_at'    => $result->created_at,
					'order_input'   => $this->personalisation->get_order_input( $result->input_fields, $result->event_id, $result->order_id )->fields,
					'order_tickets' => $this->personalisation->get_order_input( $result->input_fields, $result->event_id, $result->order_id )->tickets,
					'personalised'  => $this->personalisation->get_input( $result->personalised_input )->data,
					'scans'         => $this->fe_get_all_scans( (int) $result->id, $scan->{$key}->date_format ),
				)
			);
		} else {
			return rest_ensure_response(
				array(
					'order_id'      => (int) $result->order_id,
					'qrcode'        => $result->qrcode,
					'event_name'    => $result->event_name,
					'ticket_type'   => $ticket_type,
					'name'          => $result->name,
					'email'         => $result->email,
					'created_at'    => $result->created_at,
					'order_input'   => $this->personalisation->get_order_input( $result->input_fields, $result->event_id, $result->order_id )->fields,
					'order_tickets' => $this->personalisation->get_order_input( $result->input_fields, $result->event_id, $result->order_id )->tickets,
					'personalised'  => $this->personalisation->get_input( $result->personalised_input )->data,
					'scans'         => $this->fe_get_all_scans( (int) $result->id, $scan->{$key}->date_format ),
				)
			);
		}
	}


	/**
	 * Scan ticket via Android/IOS app
	 *
	 * @since 1.0
	 * @since 1.9.3 Added order_id and qrcode to response.
	 *
	 * @param WP_REST_Request $request_data Request instance.
	 * @return WP_Error|WP_REST_Response
	 */
	public function app_scan_request( WP_REST_Request $request_data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		// Sanitize input.
		if ( is_null( $request_data->get_header( 'x_fe_api_key' ) ) ) {
			return new WP_Error( 'not_found', esc_html__( 'Scan App API key not found', 'fast-events' ), array( 'status' => 400 ) );
		}
		$key  = sanitize_text_field( wp_unslash( $request_data->get_header( 'x_fe_api_key' ) ) );
		$code = $request_data['id'];

		// Check key length (must be 16).
		if ( 16 !== strlen( $key ) ) {
			return new WP_Error( 'failed', esc_html__( 'Invalid Scan App API key', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Look up the qrcode.
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$event_table  = $wpdb->prefix . 'fe_events';
		$order_table  = $wpdb->prefix . 'fe_event_orders';
		$entry_table  = $wpdb->prefix . 'fe_event_entries';

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $qrcode_table.id, $qrcode_table.ticket_type, $qrcode_table.event_id, $qrcode_table.order_id, $qrcode_table.scan_date, " .
				"$qrcode_table.scan_location, $qrcode_table.qrcode, $qrcode_table.exit_scan, $qrcode_table.personalised_input, $order_table.name, " .
				"$order_table.email, $order_table.input_fields, $event_table.event_name, $event_table.seats_needed, $event_table.webhooks_needed, " .
				"$event_table.tracking_needed, $event_table.reload_on_exit_needed, $event_table.scan_keys, $qrcode_table.tracking_nonce " .
				"FROM $qrcode_table " .
				"INNER JOIN $order_table ON $order_table.id = $qrcode_table.order_id " .
				"INNER JOIN $event_table ON $event_table.id = $qrcode_table.event_id " .
				"WHERE $qrcode_table.qrcode = %s",
				$code
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// Does the ticket exist?
		if ( null === $result ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Ticket not found', 'fast-events' ), 0, $key );

			return new WP_Error( 'not_found', sanitize_text_field( $error ), array( 'status' => 404 ) );
		}

		// If this is a tracking event, the request must come from the internal 'class-fast-events-tracking' (=x_fe_tracking header).
		if ( 1 === (int) $result->tracking_needed && ( is_null( $request_data->get_header( 'x_fe_tracking' ) ) || $result->tracking_nonce !== $request_data->get_header( 'x_fe_tracking' ) ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Wrong type of qrcode', 'fast-events' ), 9, $key );

			return new WP_Error( 'wrong_qrcode', sanitize_text_field( $error ), array( 'status' => 404 ) );
		}

		// Did the ticket already leave the event?
		if ( (int) $result->exit_scan ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Ticket already left the event', 'fast-events' ), 3, $key );

			return new WP_Error( 'left_event', sanitize_text_field( $error ), array( 'status' => 404 ) );
		}

		// Get the scan keys of the event.
		$scan = json_decode( $result->scan_keys );

		// Does the key exist?
		if ( empty( $scan->{$key} ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid ticket for this entrance', 'fast-events' ), 1, $key );

			return new WP_Error( 'invalid_key', sanitize_text_field( $error ), array( 'status' => 404 ) );
		}

		// This key can scan which ticket types?
		// Empty field implies every ticket type can be scanned.
		$types = explode( ',', $scan->{$key}->tickets );
		if ( '' !== $scan->{$key}->tickets && ! in_array( $result->ticket_type, $types, true ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid ticket for this entrance', 'fast-events' ), 1, $key );

			return new WP_Error( 'invalid_key', sanitize_text_field( $error ), array( 'status' => 404 ) );
		}

		// Type 0 = Main entry scan, 1 = E.g. Backstage, 6 = Reset, 7 = Temporarily leave, 8 = Re-enter and 9 = Exit scan.
		// 1, 6, 7, 8 and 9 are only valid if the main entry scan has been done.
		$etype = (int) $scan->{$key}->entry;
		if ( 0 !== $etype && empty( $result->scan_date ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'No entrance scan performed', 'fast-events' ), 2, $key );

			return new WP_Error( 'no_entry', sanitize_text_field( $error ), array( 'status' => 404 ) );
		}

		// Set the scan time. Normally this is 'now', but we may receive an older time from the
		// FE Tracking App via a tracking REST call with a 'checkpoint upload'. In the latter case we try to find the header 'x_fe_scan_date'.
		$scan_date = current_time( 'mysql' );
		if ( ! is_null( $request_data->get_header( 'x_fe_scan_date' ) ) ) {
			$scan_date = sanitize_text_field( wp_unslash( $request_data->get_header( 'x_fe_scan_date' ) ) );
		}

		$json['order_id']     = (int) $result->order_id;
		$json['qrcode']       = $result->qrcode;
		$json['event']        = $result->event_name;
		$json['name']         = $result->name;
		$json['email']        = $result->email;
		$json['level']        = $etype;
		$json['type']         = $result->ticket_type;
		$json['order_input']  = $this->personalisation->get_order_input( $result->input_fields, $result->event_id, $result->order_id )->fields;
		$json['personalised'] = $this->personalisation->get_input( $result->personalised_input )->data;
		$inserted_id          = 0;

		// Do we use seats?
		if ( 1 === (int) $result->seats_needed ) {
			$json['type'] .= $fevt_container->get( SeatsHelper::class )->get_seat( (int) $result->event_id, $code );
		}

		// Is the ticket already scanned?
		if ( ! empty( $result->scan_date ) ) { // The level 0 scan has been done.
			$json['status']   = false;
			$json['date']     = $result->scan_date;
			$json['location'] = $result->scan_location;

			// Are we dealing with level 7 (temporarily leave) or level 8 (re-enter)?
			$has_temporarily_left = false;
			if ( 1 === $etype || 6 === $etype || 7 === $etype || 8 === $etype || 9 === $etype ) {
				$has_temporarily_left = $this->fe_has_temporarily_left( (int) $result->id );
			}

			if ( $has_temporarily_left && ( 1 === $etype || 6 === $etype || 7 === $etype || 9 === $etype ) ) {
				$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Ticket already temporarily left the event', 'fast-events' ), 13, $key );

				return new WP_Error( 'temporarily_left_event', sanitize_text_field( $error ), array( 'status' => 404 ) );
			}

			if ( ! $has_temporarily_left && 8 === $etype ) {
				$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Ticket already re-entered the event', 'fast-events' ), 14, $key );

				return new WP_Error( 'already_re_entered', sanitize_text_field( $error ), array( 'status' => 404 ) );
			}

			// Levels 7 (Temporarily leave) and 8 (Re-enter) can happen multiple times.
			if ( 7 === $etype || 8 === $etype ) {
				$json['date']     = $scan_date;
				$json['location'] = $scan->{$key}->location;
				// phpcs:disable
				$entry            = $wpdb->query(
					$wpdb->prepare(
						'INSERT INTO ' . $entry_table . ' SET ' .
						'qrcode_id = %d, scancode = %s, scan_date = %s, scan_location = %s, scan_level = %d, tracking_lap = ' .
						'(SELECT cnt + 1 FROM (SELECT COALESCE(MAX(tracking_lap), 0) as cnt FROM ' . $entry_table . ' WHERE qrcode_id = %d AND scan_level = %d) as tmp)',
						(int) $result->id,
						$key,
						$json['date'],
						$json['location'],
						$etype,
						(int) $result->id,
						$etype
					)
				);
				// phpcs:enable

				// Did we fail to insert this entry? -> Should never happen, but there is a small chance.
				if ( false === $entry || 0 === $entry ) {
					$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'This scan has already been done', 'fast-events' ), 4, $key );

					return new WP_Error( 'no_entry', sanitize_text_field( $error ), array( 'status' => 404 ) );
				}

				// Save the inserted id.
				$inserted_id = $wpdb->insert_id;

				// Set return value.
				$json['status'] = true;
			}

			// Reset scan (type = 6). All scan entries will be removed.
			if ( 6 === $etype ) {
				$json['status']   = true;
				$json['date']     = $scan_date;
				$json['location'] = $scan->{$key}->location;

				if ( has_filter( 'fast_events_scan_reset_filter' ) ) {
					/**
					 * Filter the level 6 scan and possibly return WP_Error or a (int) 9, anything else returned will be ignored.
					 *
					 * @since 2.5.0
					 *
					 * @param int $order_id The unique order id.
					 * @param string $qrcode The unique qrcode of the ticket.
					 * @param bool $status The scan result is true or false.
					 * @param string $event The name of the event.
					 * @param string $type The ticket type.
					 * @param string $name The name of the qrcode holder.
					 * @param string $email The emailaddress of the qrcode holder.
					 * @param int $level The scan level (0, 1, 6, 7, 8, 9).
					 * @param string $date The datetime of the scan.
					 * @param string $location The location of the scan.
					 */
					$attr = apply_filters(
						'fast_events_scan_reset_filter',
						$json,
						$this->fe_get_all_scans( (int) $result->id, $scan->{$key}->date_format )
					);
					if ( is_wp_error( $attr ) ) {
						return $attr;
					}
					if ( 9 === $attr ) {
						$etype         = 9;
						$json['level'] = 9;
					} else {
						$wpdb->query( $wpdb->prepare( 'UPDATE ' . $qrcode_table . ' SET scan_date=NULL,scan_location="",exit_scan=0 WHERE id=%d', $result->id ) ); // phpcs:ignore
						$wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $entry_table . ' WHERE qrcode_id=%d', $result->id ) ); // phpcs:ignore
					}
				}
			}

			// 'Backstage' scan (type = 1), 'Exit' scan (type = 9).
			// Both levels (1 and 9) can only be inserted once.
			if ( ! $has_temporarily_left && ( 1 === $etype || 9 === $etype ) ) {
				$json['date']     = $scan_date;
				$json['location'] = $scan->{$key}->location;
				$entry            = $wpdb->query(
					$wpdb->prepare(
						'INSERT IGNORE INTO ' . $entry_table . '(qrcode_id, scancode, scan_date, scan_location, scan_level) ' . // phpcs:ignore
						'VALUES(%d, %s, %s, %s, %d)',
						(int) $result->id,
						$key,
						$json['date'],
						$json['location'],
						$etype
					)
				);

				// Did we fail to insert this entry? -> It was already scanned!
				if ( false === $entry || 0 === $entry ) {
					$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'This scan has already been done', 'fast-events' ), 4, $key );

					return new WP_Error( 'no_entry', sanitize_text_field( $error ), array( 'status' => 404 ) );
				}

				// Save the inserted id.
				$inserted_id = $wpdb->insert_id;

				// Set return value.
				$json['status'] = true;

				// If it was an exit scan, set it to true in the qrcode table.
				if ( 9 === $etype ) {
					$wpdb->query(
						$wpdb->prepare(
							'UPDATE ' . $qrcode_table . ' SET exit_scan=1 WHERE id=%d', // phpcs:ignore
							$result->id
						)
					);

					// Reload on exit?
					if ( 1 === (int) $result->reload_on_exit_needed ) {
						$this->increase_stock( (int) $result->event_id, $result->ticket_type );
					}
				}
			}
		} else {

			$json['status']   = true;
			$json['date']     = $scan_date;
			$json['location'] = $scan->{$key}->location;

			$res = $wpdb->query(
				$wpdb->prepare(
					'UPDATE ' . $qrcode_table . ' SET scan_date=%s,scan_location=%s WHERE id=%d AND scan_date IS NULL', // phpcs:ignore
					array(
						$json['date'],
						$json['location'],
						$result->id,
					)
				)
			);
			if ( false === $res || 0 === $res ) {
				// Should not happen though, but in theory it can (fraud!!!).
				$json['status']   = false;
				$json['location'] = '';
			}
		}

		// If the scan is valid -> fire the webhook.
		if ( false !== $json['status'] && 1 === (int) $result->webhooks_needed ) {
			WebhookUtil::create_webhook(
				'scan.created',
				array(
					(int) $result->id,
					$etype,
					$inserted_id,
				)
			);
		}

		// Filter the data.
		$json['date'] = mysql2date( $scan->{$key}->date_format, $json['date'] );

		/**
		 * Filter the output of the scan.
		 *
		 * @since 1.0
		 * @since 1.9.3 Added order_id and qrcode.
		 *
		 * @param int $order_id The unique order id.
		 * @param string $qrcode The unique qrcode of the ticket.
		 * @param string $event The name of the event.
		 * @param string $type The ticket type.
		 * @param string $name The name of the qrcode holder.
		 * @param string $email The emailaddress of the qrcode holder.
		 * @param string $date The datetime of the scan.
		 * @param string $location The location of the scan.
		 */
		if ( has_filter( 'fast_events_scan_filter_output' ) ) {
			$attr = apply_filters(
				'fast_events_scan_filter_output',
				array(
					'order_id' => $json['order_id'],
					'qrcode'   => $json['qrcode'],
					'event'    => $json['event'],
					'type'     => $json['type'],
					'name'     => $json['name'],
					'email'    => $json['email'],
					'date'     => $json['date'],
					'location' => $json['location'],
				)
			);

			$json['order_id'] = (int) $result->order_id;
			$json['qrcode  '] = $result->qrcode;
			$json['event']    = sanitize_text_field( $attr['event'] );
			$json['type']     = sanitize_text_field( $attr['type'] );
			$json['name']     = sanitize_text_field( $attr['name'] );
			$json['email']    = sanitize_text_field( $attr['email'] );
			$json['date']     = sanitize_text_field( $attr['date'] );
			$json['location'] = sanitize_text_field( $attr['location'] );
		}

		// Trigger scan action event.
		if ( has_action( 'fast_events_scan_ticket' ) ) {
			/**
			 * Action after ticket scan.
			 *
			 * @since 1.0
			 * @since 1.9.3 Added order_id and qrcode.
			 *
			 * @param int $order_id The unique order id.
			 * @param string $qrcode The unique qrcode of the ticket.
			 * @param bool $status The scan result is true or false.
			 * @param string $event The name of the event.
			 * @param string $type The ticket type.
			 * @param string $name The name of the qrcode holder.
			 * @param string $email The emailaddress of the qrcode holder.
			 * @param int $level The scan level (0, 1, 6, 7, 8, 9).
			 * @param string $date The datetime of the scan.
			 * @param string $location The location of the scan.
			 */
			do_action( 'fast_events_scan_ticket', $json, $this->fe_get_all_scans( (int) $result->id, $scan->{$key}->date_format ) );
		}

		return rest_ensure_response( $json );
	}


	/**
	 * Get schema for a new scan request.
	 *
	 * @since 1.0
	 * @since 1.9.3 Added order id.
	 * @since 2.4.0 Added level 7 (Temporarily leave) and level 8 (Re-enter).
	 * @since 2.5.0 Added level 6 (Reset).
	 */
	public function get_all_scans_schema(): array {
		return array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'scans',
			'type'       => 'object',
			'properties' => array(
				'order_id'    => array(
					'description' => esc_html__( 'The unique id of the order.', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'integer',
					'readonly'    => true,
				),
				'event_name'  => array(
					'description' => esc_html__( 'The name of the event', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'ticket_type' => array(
					'description' => esc_html__( 'The ticket name appended with the seat info', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'name'        => array(
					'description' => esc_html__( 'The name of the user', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'email'       => array(
					'description' => esc_html__( 'The emailaddress of the user', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'format'      => 'email',
					'readonly'    => true,
				),
				'scans'       => array(
					'description' => esc_html__( 'All scans of the ticket', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'array',
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'scan_level'    => array(
								'description' => esc_html__( 'The scan level (0=Entry, 1=Staged, 6=Reset, 7=Temporarily leave, 8= Re-enter, 9=Exit)', 'fast-events' ),
								'context'     => array( 'view' ),
								'type'        => 'integer',
								'enum'        => array( 0, 1, 6, 7, 8, 9 ),
								'readonly'    => true,
							),
							'scan_date'     => array(
								'description' => esc_html__( 'The date of the scan', 'fast-events' ),
								'context'     => array( 'view' ),
								'type'        => 'string',
								'readonly'    => true,
							),
							'scan_location' => array(
								'description' => esc_html__( 'The location of the scan', 'fast-events' ),
								'context'     => array( 'view' ),
								'type'        => 'string',
								'format'      => 'email',
								'readonly'    => true,
							),
						),
					),
				),
			),
		);
	}


	/**
	 * Get schema for a new scan request.
	 *
	 * @since 1.0
	 * @since 1.9.3 Added order id.
	 */
	public function get_new_scan_schema(): array {
		return array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'scans',
			'type'       => 'object',
			'properties' => array(
				'order_id' => array(
					'description' => esc_html__( 'The unique id of the order.', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'integer',
					'readonly'    => true,
				),
				'status'   => array(
					'description' => esc_html__( 'Was the scan successful', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'boolean',
					'readonly'    => true,
				),
				'event'    => array(
					'description' => esc_html__( 'The name of the event', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'type'     => array(
					'description' => esc_html__( 'The ticket name', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'name'     => array(
					'description' => esc_html__( 'The name of the user', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'email'    => array(
					'description' => esc_html__( 'The emailaddress of the user', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'format'      => 'email',
					'readonly'    => true,
				),
				'date'     => array(
					'description' => esc_html__( 'The date of the scan', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'readonly'    => true,
				),
				'location' => array(
					'description' => esc_html__( 'The location of the scan', 'fast-events' ),
					'context'     => array( 'view' ),
					'type'        => 'string',
					'format'      => 'email',
					'readonly'    => true,
				),
			),
		);
	}
}
