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

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

defined( 'ABSPATH' ) || exit;

use DateTime;
use FeData\FastEvents\Helpers\SeatsHelper;
use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\RestAPI\Public\QrcodeScansController;
use FeData\FastEvents\Utilities\JsonUtil;
use FeData\FastEvents\Utilities\PluginUtil;
use FeData\FastEvents\Utilities\TimeUtil;
use stdClass;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Tracking controller class.
 */
class TrackingController {


	/**
	 * Tracking constructor.
	 *
	 * @param LoggerFactory $logger Logger instance.
	 *
	 * @since 1.0
	 */
	public function __construct( private readonly LoggerFactory $logger ) {}


	/**
	 * Register the routes for the tracking.
	 *
	 * @since 1.0
	 */
	public function register_routes(): void {

		// Download tracking data for the FE Tracking App.
		register_rest_route(
			'fast-events',
			'/v1/tracking/register',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'fast_events_get_tracking_info' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'qrcode_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
					'signature' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'pattern'           => '^[0-9a-f]{16}$',
					),
				),
			)
		);

		// Normal scan via FE Scan App.
		register_rest_route(
			'fast-events',
			'/v1/tracking/scan',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'fast_events_get_tracking_scan' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'qrcode_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
					'qrcode'    => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'pattern'           => '^[\w]{16}$',
					),
					'signature' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'pattern'           => '^[0-9a-f]{16}$',
					),
				),
			)
		);

		// Automatic checkpoint upload from the FE Tracking App.
		register_rest_route(
			'fast-events',
			'/v1/tracking/checkpoint',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'fast_events_checkpoint_upload' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'location' => array(
						'required'   => true,
						'type'       => 'object',
						'properties' => array(
							'checkpoint'          => array(
								'required'          => true,
								'type'              => 'string',
								'sanitize_callback' => 'sanitize_text_field',
								'minLength'         => 1,
								'maxLength'         => 50,
							),
							'time'                => array(
								'required' => true,
								'type'     => 'string',
								'format'   => 'date-time',
							),
							'trackId'             => array(
								'required'          => true,
								'type'              => 'integer',
								'sanitize_callback' => 'absint',
							),
							'trackSignature'      => array(
								'required'          => true,
								'type'              => 'string',
								'sanitize_callback' => 'sanitize_text_field',
								'pattern'           => '^[0-9a-f]{16}$',
							),
							'qrcode'              => array(
								'required'          => true,
								'type'              => 'string',
								'sanitize_callback' => 'sanitize_text_field',
								'pattern'           => '^[\w]{16}$',
							),
							'checkpointId'        => array(
								'required'          => true,
								'type'              => 'integer',
								'sanitize_callback' => 'absint',
							),
							'checkpointSignature' => array(
								'required'          => true,
								'type'              => 'string',
								'sanitize_callback' => 'sanitize_text_field',
								'pattern'           => '^[0-9a-f]{32,64}$',
							),
						),
					),
				),
			)
		);

		// The Scan App submitted a finish track qrcode, presented by a participant.
		register_rest_route(
			'fast-events',
			'/v1/tracking/finish',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'fast_events_finish_tracking' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'qrcode'    => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'pattern'           => '^[\w]{16}$',
					),
					'signature' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'pattern'           => '^[0-9a-f]{32}$',
					),
				),
			)
		);
	}


	/**
	 * Generate the tracking info for the client
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	public function fast_events_get_tracking_info( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		// Get the qrcode.
		$event_table  = $wpdb->prefix . 'fe_events';
		$order_table  = $wpdb->prefix . 'fe_event_orders';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		// phpcs:disable
		$result       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.name, $order_table.email, $order_table.created_at, $event_table.event_name, " .
				"$event_table.event_date, $event_table.event_date_format, $event_table.tickets_needed, $event_table.seats_needed, " .
				"$event_table.webhooks_needed, $event_table.seats_needed,  $event_table.tracking_needed, $event_table.tracking_info, " .
				"$event_table.scan_keys, $qrcode_table.qrcode, $qrcode_table.ticket_type, $qrcode_table.event_id, " .
				"$qrcode_table.id as qrcode_id 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.id = %d",
				$data['qrcode_id']
			)
		);
		// phpcs:enable

		if ( empty( $result ) ) {
			http_response_code( 404 );
			die();
		}

		// Does the event support tickets and tracking?
		if ( 0 === (int) $result->tickets_needed || 0 === (int) $result->tracking_needed ) {
			http_response_code( 404 );
			die();
		}

		// Check the tracking info JSON structure.
		$tracking_info = json_decode( $result->tracking_info );
		if ( null === $tracking_info ) {  // Fatal error as we need this!
			http_response_code( 404 );
			die();
		}

		// Are we still before the closure of the tracking window?
		if ( TimeUtil::local_time() > TimeUtil::date_local_time( $tracking_info->stop_tracking ) ) {
			http_response_code( 404 );
			die();
		}

		// Is the signature valid?
		if ( PluginUtil::get_qrcode_signature( $result->qrcode, (int) $result->qrcode_id ) !== $data['signature'] ) {
			http_response_code( 404 );
			die();
		}

		// Check if seats are used and add them if needed.
		$result->ticket_seat = $result->ticket_type;
		if ( 1 === (int) $result->seats_needed ) {
			$result->ticket_seat .= $fevt_container->get( SeatsHelper::class )->get_seat( (int) $result->event_id, $result->qrcode );
		}

		// Create the unique tracking key for the signature once the client has passed all checkpoints,
		// and he can present the finish qrcode which is a md5 hash of the hash presented in this call and the signing token.
		// Mind you: if the tracking info is collected a second time, the previous request(s) will carry the old (and invalidated) signing token.
		// Organizations must assume that the last one is the rightful owner!
		$signing_token = PluginUtil::get_qrcode_signature( $result->qrcode . $data['signature'] . time(), (int) $result->qrcode_id );
		$wpdb->update(
			$qrcode_table,
			array( 'tracking_nonce' => $signing_token ),
			array( 'id' => (int) $result->qrcode_id )
		);

		// Create the scan-locations array.
		$scan_locations = array();
		$scans          = json_decode( $result->scan_keys );
		if ( null !== $scans ) {
			foreach ( $scans as $key => $value ) {
				$tmp              = new stdClass();
				$tmp->location    = $value->location;
				$tmp->level       = $value->entry;
				$tmp->signature   = hash_hmac( 'sha1', $key, $result->qrcode . $signing_token );
				$scan_locations[] = $tmp;
			}
		}

		// Create the return structure.
		$json                  = new stdClass();
		$json->home            = PluginUtil::get_home_domain();
		$json->event_name      = $result->event_name;
		$json->event_date      = mysql2date( $result->event_date_format, $result->event_date );
		$json->start_tracking  = $tracking_info->start_tracking;
		$json->stop_tracking   = $tracking_info->stop_tracking;
		$json->geofence_radius = (int) $tracking_info->geofence_radius;
		$json->distance_filter = (int) $tracking_info->distance_filter;
		$json->tr_help_url     = $tracking_info->tr_help_url;
		$json->tr_help_tel     = $tracking_info->tr_help_tel;
		$json->tr_help_txt     = $tracking_info->tr_help_txt;
		$json->ticket_type     = $result->ticket_type;
		$json->ticket_seat     = $result->ticket_seat;
		$json->qrcode          = $result->qrcode;
		$json->name            = $result->name;
		$json->email           = $result->email;
		$json->order_id        = (int) $result->id;
		$json->order_date      = $result->created_at;
		$json->geo_json        = JsonUtil::fast_events_json_encode( $tracking_info->tracks );
		$json->signing_token   = $signing_token;
		$json->scan_locations  = $scan_locations;
		$json->icon_url        = get_site_icon_url();

		return rest_ensure_response( $json );
	}


	/**
	 * Process scan request from the Scan App
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 1.0
	 */
	public function fast_events_get_tracking_scan( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		// Sanitize input.
		if ( is_null( $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( $data->get_header( 'x_fe_api_key' ) ) );

		// 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 ) );
		}

		// Get the qrcode.
		$event_table  = $wpdb->prefix . 'fe_events';
		$order_table  = $wpdb->prefix . 'fe_event_orders';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		// phpcs:disable
		$result       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.name, $order_table.email, $order_table.created_at, $event_table.event_name, " .
				"$event_table.event_date, $event_table.event_date_format, $event_table.tickets_needed, $event_table.seats_needed, " .
				"$event_table.webhooks_needed, $event_table.seats_needed,  $event_table.tracking_needed, $event_table.tracking_info, " .
				"$event_table.scan_keys, $qrcode_table.qrcode, $qrcode_table.ticket_type, $qrcode_table.event_id, $qrcode_table.tracking_nonce, " .
				"$qrcode_table.scan_date, $qrcode_table.scan_location, $qrcode_table.id as qrcode_id 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.id = %d",
				$data['qrcode_id']
			)
		);
		// phpcs:enable
		if ( empty( $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 ) );
		}

		// Does the event support tickets and tracking?
		$tracking = json_decode( $result->tracking_info );
		if ( 0 === (int) $result->tickets_needed || 0 === (int) $result->tracking_needed || null === $tracking ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'This event does not support tracking', 'fast-events' ), 5, $key );

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

		// We need a level 0 or 1 scan key!
		if ( true === $tracking->force_tracking_app ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'You cannot scan this checkpoint', 'fast-events' ), 12, $key );

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

		// Are we still in the tracking window?
		if ( TimeUtil::local_time() < TimeUtil::date_local_time( $tracking->start_tracking ) ||
			TimeUtil::local_time() > TimeUtil::date_local_time( $tracking->stop_tracking ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid tracking window', 'fast-events' ), 6, $key );

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

		// Is the signature valid?
		if ( PluginUtil::get_qrcode_signature( $result->qrcode, (int) $result->qrcode_id ) !== $data['signature'] || $result->qrcode !== $data['qrcode'] ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid track signature', 'fast-events' ), 10, $key );

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

		// Get the scan keys of the event and does our key exist?
		$scan = json_decode( $result->scan_keys );
		if ( empty( $scan->{$key} ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid scan key', 'fast-events' ), 8, $key );

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

		// It is possible that this is a level 1 scan and level 0 doesn't exist yet.
		// If so, and 'no_entry_scan' is 'true', we need to set the entry scan (date and location), just to be in sync with normal behaviour.
		if ( null === $result->scan_date && true === $tracking->no_entry_scan && ( 1 === $scan->{$key}->entry || 9 === $scan->{$key}->entry ) ) {
			$wpdb->update(
				$qrcode_table,
				array(
					'scan_date'     => current_time( 'mysql' ),
					'scan_location' => '*' . $scan->{$key}->location,
				),
				array( 'id' => $result->qrcode_id )
			);
		}

		// Now we have handled all the specific stuff for tracking.
		// Reroute the request to the normal scan REST API.
		$fevt_container->get( QrcodeScansController::class )->register_routes();
		$request = new WP_REST_Request( 'POST', '/fast-events/v1/scans' );
		$request->set_headers(
			array(
				'Content-Type'  => 'application/json',
				'X-FE-API-KEY'  => $key,
				'X-FE-TRACKING' => $result->tracking_nonce,
			)
		);
		$request->set_body( '{"id":"' . $data['qrcode'] . '"}' );
		$response = rest_do_request( $request );
		if ( $response->is_error() ) {
			return $response->as_error();
		} else {
			return rest_ensure_response( $response->get_data() );
		}
	}


	/**
	 * Automatic upload of passing a checkpoint in the FE Tracking App.
	 * For now, we always return HTTP 200 as the 'FE Tracking' can't coop with codes other than 2XX.
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 1.0
	 */
	public function fast_events_checkpoint_upload( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		$qrcode = $data['location']['qrcode'];

		$json                      = new stdClass();
		$json->result              = '';
		$json->trackId             = $data['location']['trackId']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		$json->checkpointId        = $data['location']['checkpointId']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		$json->checkpointSignature = $data['location']['checkpointSignature']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

		// Get the qrcode.
		$event_table  = $wpdb->prefix . 'fe_events';
		$order_table  = $wpdb->prefix . 'fe_event_orders';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		// phpcs:disable
		$result       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.name, $order_table.email, $order_table.created_at, $event_table.event_name, " .
				"$event_table.event_date, $event_table.event_date_format, $event_table.tickets_needed, $event_table.seats_needed, " .
				"$event_table.webhooks_needed, $event_table.seats_needed,  $event_table.tracking_needed, $event_table.tracking_info, " .
				"$event_table.scan_keys, $qrcode_table.qrcode, $qrcode_table.ticket_type, $qrcode_table.event_id, $qrcode_table.tracking_nonce, " .
				"$qrcode_table.id as qrcode_id, $qrcode_table.scan_date, $qrcode_table.scan_location, $qrcode_table.exit_scan " .
				"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",
				$qrcode
			)
		);
		// phpcs:enable
		if ( empty( $result ) ) {
			$this->logger->error(
				'Checkpoint upload, qrcode ' . $qrcode . ' not found',
				array(
					'type'   => 'Tracking',
					'qrcode' => $qrcode,
				)
			);
			$json->result = esc_html__( 'Qrcode not found', 'fast-events' );

			return rest_ensure_response( $json );
		}

		// Does the event support tickets and tracking?
		$tracking_info = json_decode( $result->tracking_info );
		if ( 0 === (int) $result->tickets_needed || 0 === (int) $result->tracking_needed || null === $tracking_info ) {
			$this->logger->error(
				'Checkpoint upload, qrcode: ' . $qrcode . ', event "' . $result->event_name . '" does not support tickets or tracking',
				array(
					'type'     => 'Tracking',
					'event_id' => (int) $result->event_id,
					'order_id' => (int) $result->id,
					'qrcode'   => $qrcode,
				)
			);
			$json->result = esc_html__( 'Event does not support tickets or tracking', 'fast-events' );

			return rest_ensure_response( $json );
		}

		// Are we still before the closure of the tracking window?
		if ( TimeUtil::local_time() > TimeUtil::date_local_time( $tracking_info->stop_tracking ) ) {
			$json->result = esc_html__( 'Request is outside tracking window', 'fast-events' );
			$this->logger->error(
				'Checkpoint upload, qrcode: ' . $qrcode . ', outside tracking window',
				array(
					'type'     => 'Tracking',
					'event_id' => (int) $result->event_id,
					'order_id' => (int) $result->id,
					'qrcode'   => $qrcode,
				)
			);
			return rest_ensure_response( $json );
		}

		// Is the track signature valid?
		if ( PluginUtil::get_qrcode_signature( $result->qrcode, (int) $result->qrcode_id ) !== $data['location']['trackSignature'] ) {
			$json->result = esc_html__( 'Invalid track signature', 'fast-events' );
			$this->logger->alert(
				'Checkpoint upload, qrcode: ' . $qrcode . ', invalid track signature',
				array(
					'type'     => 'Tracking',
					'event_id' => (int) $result->event_id,
					'order_id' => (int) $result->id,
					'qrcode'   => $qrcode,
				)
			);
			return rest_ensure_response( $json );
		}

		// Get the scan keys of the event and does our checkpoint (=Location) exist?
		$scans      = json_decode( $result->scan_keys );
		$key_found  = false;
		$scan_key   = '';
		$scan_level = 0;
		foreach ( $scans as $key => $value ) {
			if ( $value->location === $data['location']['checkpoint'] ) {
				$scan_key   = $key;
				$key_found  = true;
				$scan_level = $value->entry;
				break;
			}
		}
		if ( ! $key_found ) {
			$json->result = esc_html__( 'Checkpoint not found', 'fast-events' );
			$this->logger->alert(
				'Checkpoint upload, qrcode: ' . $qrcode . ', location "' . $data['location']['checkpoint'] . '" not found in scan_keys',
				array(
					'type'     => 'Tracking',
					'event_id' => (int) $result->event_id,
					'order_id' => (int) $result->id,
					'qrcode'   => $qrcode,
				)
			);
			return rest_ensure_response( $json );
		}

		// Is the checkpoint signature valid?
		if ( hash_hmac( 'sha1', $scan_key, $result->qrcode . $result->tracking_nonce ) !== $data['location']['checkpointSignature'] ) {
			$json->result = esc_html__( 'Invalid checkpoint signature', 'fast-events' );
			$this->logger->alert(
				'Checkpoint upload, qrcode: ' . $qrcode . ', location "' . $data['location']['checkpoint'] . '" invalid signature',
				array(
					'type'     => 'Tracking',
					'event_id' => (int) $result->event_id,
					'order_id' => (int) $result->id,
					'qrcode'   => $qrcode,
				)
			);
			return rest_ensure_response( $json );
		}

		// It is possible that this is a level 1 scan and level 0 doesn't exist yet.
		// If so, and 'no_entry_scan' is 'true', we need to set the entry scan (date and location), just to be in sync with normal behaviour.
		$scan_date = DateTime::createFromFormat( 'Y-m-d\TH:i:s.u\Z', $data['location']['time'] )->setTimezone( wp_timezone() )->format( 'Y-m-d H:i:s' );
		if ( null === $result->scan_date && true === $tracking_info->no_entry_scan && ( 1 === $scan_level || 9 === $scan_level ) ) {
			$wpdb->update(
				$qrcode_table,
				array(
					'scan_date'     => $scan_date,
					'scan_location' => '*' . $data['location']['checkpoint'],
				),
				array( 'id' => $result->qrcode_id )
			);
		}

		// Now we have handled all the specific stuff for tracking.
		// Reroute the request to the normal scan REST API.
		$fevt_container->get( QrcodeScansController::class )->register_routes();
		$request = new WP_REST_Request( 'POST', '/fast-events/v1/scans' );
		$request->set_headers(
			array(
				'Content-Type'   => 'application/json',
				'X-FE-API-KEY'   => $scan_key,
				'X-FE-SCAN-DATE' => $scan_date,
				'X-FE-TRACKING'  => $result->tracking_nonce,
			)
		);
		$request->set_body( '{"id":"' . $qrcode . '"}' );
		$response = rest_do_request( $request );
		if ( $response->is_error() ) {
			$error        = $response->as_error();
			$json->result = $error->get_error_message();

			return rest_ensure_response( $json );
		}

		// Pass the response to the app.
		$resp_json         = $response->get_data();
		$json->fe_event    = $resp_json['event'];
		$json->fe_name     = $resp_json['name'];
		$json->fe_email    = $resp_json['email'];
		$json->fe_level    = $resp_json['level'];
		$json->fe_type     = $resp_json['type'];
		$json->fe_status   = $resp_json['status'];
		$json->fe_date     = $resp_json['date'];
		$json->fe_location = $resp_json['location'];

		return rest_ensure_response( $json );
	}


	/**
	 * Request from Scan App to finish tracking, after the user presented the qrcode to the Scan App for scanning
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 1.0
	 */
	public function fast_events_finish_tracking( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		// Sanitize input.
		if ( is_null( $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( $data->get_header( 'x_fe_api_key' ) ) );

		// 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 ) );
		}

		// Get the qrcode.
		$event_table  = $wpdb->prefix . 'fe_events';
		$order_table  = $wpdb->prefix . 'fe_event_orders';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		// phpcs:disable
		$result       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.name, $order_table.email, $order_table.created_at, $event_table.event_name, " .
				"$event_table.event_date, $event_table.event_date_format, $event_table.tickets_needed, $event_table.seats_needed, " .
				"$event_table.webhooks_needed, $event_table.seats_needed,  $event_table.tracking_needed, $event_table.tracking_info, " .
				"$event_table.scan_keys, $qrcode_table.qrcode, $qrcode_table.ticket_type, $qrcode_table.event_id, $qrcode_table.tracking_nonce, " .
				"$qrcode_table.id as qrcode_id, $qrcode_table.scan_date, $qrcode_table.scan_location, $qrcode_table.exit_scan " .
				"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",
				$data['qrcode']
			)
		);
		// phpcs:enable
		if ( empty( $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 and does our key exist?
		$scan = json_decode( $result->scan_keys );
		if ( empty( $scan->{$key} ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid scan key', 'fast-events' ), 8, $key );

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

		// We need a level 9 scan key!
		if ( 9 !== (int) $scan->{$key}->entry ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid finish API key', 'fast-events' ), 11, $key );

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

		// Is tracking enabled on this event?
		$tracking = json_decode( $result->tracking_info );
		if ( 1 !== (int) $result->tracking_needed || null === $tracking ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'This event does not support tracking', 'fast-events' ), 5, $key );

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

		// Are we still in the tracking window?
		if ( TimeUtil::local_time() < TimeUtil::date_local_time( $tracking->start_tracking ) ||
			TimeUtil::local_time() > TimeUtil::date_local_time( $tracking->stop_tracking ) ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid tracking window', 'fast-events' ), 6, $key );

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

		// Check the signing key. First, generate the unique hash for this entry and
		// combine it with the stored tracking nonce and apply md5.
		$signature = PluginUtil::get_qrcode_signature( $result->qrcode, (int) $result->qrcode_id );
		$md5_hash  = md5( $signature . $result->tracking_nonce );
		if ( $md5_hash !== $data['signature'] ) {
			$error = apply_filters( 'fast_events_scan_error_text', esc_html__( 'Invalid signing key', 'fast-events' ), 7, $key );

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

		// 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' => 400 ) );
		}

		// 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' => 400 ) );
		}

		// It can happen that this level 9 scan from the FE Scan App was the only one for this ticket.
		// If so, and 'no_entry_scan` is 'true', we need to set the entry scan (date and location), just to be in sync with normal behaviour.
		if ( null === $result->scan_date && true === $tracking->no_entry_scan ) {
			$wpdb->update(
				$qrcode_table,
				array(
					'scan_date'     => current_time( 'mysql' ),
					'scan_location' => $scan->{$key}->location,
				),
				array( 'id' => $result->qrcode_id )
			);
		}

		// Now we have handled all the specific stuff for tracking.
		// Reroute the request to the normal scan REST API.
		$fevt_container->get( QrcodeScansController::class )->register_routes();
		$request = new WP_REST_Request( 'POST', '/fast-events/v1/scans' );
		$request->set_headers(
			array(
				'Content-Type'  => 'application/json',
				'X-FE-API-KEY'  => $key,
				'X-FE-TRACKING' => $result->tracking_nonce,
			)
		);
		$request->set_body( '{"id":"' . $data['qrcode'] . '"}' );
		$response = rest_do_request( $request );
		if ( $response->is_error() ) {
			return $response->as_error();
		} else {
			return rest_ensure_response( $response->get_data() );
		}
	}
}
