<?php
use TEC\Tickets\Commerce\Ticket;
use TEC\Tickets\Commerce\Module;


class Tribe__Tickets__REST__V1__Post_Repository
	extends Tribe__REST__Post_Repository
	implements Tribe__Tickets__REST__Interfaces__Post_Repository {
	const CONTEXT_PUBLIC = 'public';
	const CONTEXT_EDITOR = 'editor';

	/**
	 * A post type to get data request handler map.
	 *
	 * @var array
	 */
	protected $types_get_map = [];

	/**
	 * @var Tribe__REST__Messages_Interface
	 */
	protected $messages;

	/**
	 * @var string
	 */
	protected $global_id_key = '_tribe_global_id';

	/**
	 * @var string
	 */
	protected $global_id_lineage_key = '_tribe_global_id_lineage';

	/**
	 * @var int Cached current ticket id.
	 */
	protected $current_ticket_id;

	/**
	 * @var Tribe__Tickets__Ticket_Object Cached current ticket object;
	 */
	protected $current_ticket_object;

	/**
	 * @var Tribe__Tickets__Tickets Cached current ticket provider.
	 */
	protected $current_ticket_provider;

	/**
	 * @var WP_Post Cached current ticket post.
	 */
	protected $current_ticket_post;

	/**
	 * @var string The context the data will be shown in; defaults to `public`.
	 */
	protected $permission = 'public';

	/**
	 * Tribe__Tickets__REST__V1__Post_Repository constructor.
	 *
	 * @since 5.29.0 Made $messages explicitly nullable.
	 *
	 * @param Tribe__REST__Messages_Interface|null $messages The messages instance.
	 */
	public function __construct( ?Tribe__REST__Messages_Interface $messages = null ) {
		$this->types_get_map = [
			Tribe__Tickets__RSVP::ATTENDEE_OBJECT => [ $this, 'get_attendee_data' ],
		];

		$this->messages = $messages ? $messages : tribe( 'tickets.rest-v1.messages' );
	}

	/**
	 * Retrieves an array representation of the post.
	 *
	 * @since 4.7.5
	 *
	 * @param int    $id      The post ID.
	 * @param string $context Context of data.
	 *
	 * @return array An array representation of the post.
	 */
	public function get_data( $id, $context = '' ) {
		$post = get_post( $id );

		if ( empty( $post ) ) {
			return [];
		}

		if ( ! isset( $this->types_get_map[ $post->post_type ] ) ) {
			return (array) $post;
		}

		return call_user_func( $this->types_get_map[ $post->post_type ], $id, $context );
	}

	/**
	 * {@inheritdoc}
	 *
	 * @since 4.12.0 Returns 401 Unauthorized if Event Tickets Plus is not loaded.
	 */
	public function get_attendee_data( $attendee_id, $context = 'default' ) {
		$attendee_post = get_post( $attendee_id );

		if ( ! $attendee_post instanceof WP_Post ) {
			// the attendee post does not exist, user error.
			return new WP_Error( 'attendee-not-found', $this->messages->get_message( 'attendee-not-found' ), [ 'status' => 404 ] );
		}

		$attendee_id = $attendee_post->ID;

		/** @var Tribe__Tickets__Data_API $data_api */
		$data_api = tribe( 'tickets.data_api' );

		/** @var Tribe__Tickets__Tickets $provider */
		$provider = $data_api->get_ticket_provider( $attendee_id );

		if ( empty( $provider ) ) {
			// the attendee post does exist but it does not make sense on the server, server error.
			return new WP_Error( 'attendee-not-found', $this->messages->get_message( 'attendee-not-found' ), [ 'status' => 500 ] );
		}

		// The return value of this function will always be an array even if we only want one object.
		$attendee = $provider->get_all_attendees_by_attendee_id( $attendee_id );

		if ( empty( $attendee ) ) {
			// the attendee post does exist but it does not make sense on the server, server error.
			return new WP_Error( 'attendee-not-found', $this->messages->get_message( 'attendee-not-found' ), [ 'status' => 500 ] );
		}

		// See note above, this is an array with one element in it.
		$attendee = $attendee[0];

		return $this->build_attendee_data( $attendee );
	}

	/**
	 * {@inheritdoc}
	 */
	public function get_ticket_data( $ticket_id, $context = 'default' ) {
		if ( is_array( $ticket_id ) && ! empty( $ticket_id['id'] ) ) {
			// ticket data in array format.
			$ticket_id = $ticket_id['id'];
		}

		$ticket = $ticket_id instanceof Tribe__Tickets__Ticket_Object
			? $ticket_id
			: $this->get_ticket_object( $ticket_id );

		if ( $ticket instanceof WP_Error ) {
			return $ticket;
		}

		// make sure the data is a nested array.
		$data = json_decode( json_encode( $ticket ), true );

		$data['post_id']  = $ticket->get_event_id();
		$data['provider'] = $this->get_provider_slug( $ticket->provider_class );
		$data['id']       = (int) $data['ID'];
		$data['type']     = $ticket->type();

		try {
			$this->add_ticket_global_id_data( $data );
			$this->add_ticket_post_data( $data );
			$this->add_ticket_meta_data( $data );
			$this->add_ticket_attendees_data( $data );
			$this->add_ticket_rest_data( $data );
			$this->clean_ticket_data( $data );
		} catch ( Exception $e ) {
			if ( $e instanceof Tribe__REST__Exceptions__Exception ) {
				return new WP_Error( $e->getCode(), $e->getMessage() );
			}

			/** @var Tribe__REST__Exceptions__Exception $e */
			return new WP_Error(
				'error',
				__( 'An error happened while building the response: ', 'event-tickets' ) . $e->getMessage(),
				[ 'status' => $e->getMessage() ]
			);
		}

		/**
		 * Filters the data that will be returned for a ticket.
		 *
		 * @since 4.8
		 *
		 * @param array  $data The ticket data.
		 * @param int    $ticket_id The ticket post ID.
		 * @param string $context The context in which the data will show; this is about format,
		 *                        not permissions.
		 */
		$data = apply_filters( 'tribe_tickets_rest_api_ticket_data', $data, $ticket_id, $context );

		return $data;
	}

	/**
	 * Gets the ticket object from a ticket ID.
	 *
	 * @since 4.8
	 *
	 * @param int|WP_Post $ticket_id The ticket ID or post object.
	 *
	 * @return Tribe__Tickets__Ticket_Object|bool|WP_Error The ticket object, `false`, or WP_Error.
	 */
	protected function get_ticket_object( $ticket_id ) {
		if ( isset( $this->current_ticket_id ) && $ticket_id != $this->current_ticket_id ) {
			$this->reset_ticket_cache();
		}

		if (
			isset( $this->current_ticket_object )
			&& $this->current_ticket_object instanceof Tribe__Tickets__Ticket_Object
		) {
			return $this->current_ticket_object;
		}

		if ( $ticket_id instanceof WP_Post ) {
			$ticket_id = $ticket_id->ID;
		}

		/** @var Tribe__Tickets__Tickets $provider */
		$provider = tribe_tickets_get_ticket_provider( $ticket_id );

		if ( empty( $provider ) ) {
			return new WP_Error( 'ticket-provider-not-found', $this->messages->get_message( 'ticket-provider-not-found' ), [ 'status' => 500 ] );
		}

		$this->current_ticket_provider = $provider;

		$post = $provider->get_event_for_ticket( $ticket_id );

		if ( ! $post instanceof WP_Post ) {
			return new WP_Error( 'ticket-post-not-found', $this->messages->get_message( 'ticket-post-not-found' ), [ 'status' => 500 ] );
		}

		$this->current_ticket_post = $post;

		/** @var Tribe__Tickets__Ticket_Object $ticket */
		$ticket = $provider->get_ticket( $post->ID, $ticket_id );

		if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
			return new WP_Error( 'ticket-object-not-found', $this->messages->get_message( 'ticket-object-not-found' ), [ 'status' => 500 ] );
		}

		$this->current_ticket_id     = $ticket_id;
		$this->current_ticket_object = $ticket;

		return $ticket;
	}

	/**
	 * Resets the current ticket caches.
	 *
	 * @since 4.8
	 */
	public function reset_ticket_cache() {
		unset( $this->current_ticket_id, $this->current_ticket_provider, $this->current_ticket_post, $this->current_ticket_object );
	}

	/**
	 * Returns the slug for provider.
	 *
	 * @since 4.8
	 *
	 * @param string|object $provider_class The provider object or class.
	 *
	 * @return string
	 */
	public function get_provider_slug( $provider_class ) {
		if ( is_object( $provider_class ) ) {
			$provider_class = get_class( $provider_class );
		}

		$map = [
			'Tribe__Tickets__RSVP'                     => 'rsvp',
			'Tribe__Tickets__Commerce__PayPal__Main'   => 'tribe-commerce',
			'Tribe__Tickets_Plus__Commerce__WooCommerce__Main' => 'woo',
			'Tribe__Tickets_Plus__Commerce__EDD__Main' => 'edd',
			Module::class                              => \TEC\Tickets\Commerce::ABBR,
		];

		/**
		 * Filters the provider class to slug map.
		 *
		 * @since 4.8
		 *
		 * @param array $map A map in the shape [ <class> => <slug> ]
		 * @param string The provider class
		 */
		$map = apply_filters( 'tribe_tickets_rest_provider_slug_map', $map, $provider_class );

		$values  = array_values( $map );
		$default = $values[0];

		return Tribe__Utils__Array::get( $map, $provider_class, $default );
	}

	/**
	 * Adds the global ID information to the ticket data.
	 *
	 * @since 4.8
	 *
	 * @param array $data The ticket data.
	 *
	 * @throws Tribe__REST__Exceptions__Exception If the global ID generation fails.
	 */
	protected function add_ticket_global_id_data( array &$data ) {
		$provider_class = $data['provider_class'];
		$ticket_id      = $data['id'];

		$global_id = $this->get_ticket_global_id( $ticket_id, $provider_class );

		if ( false === $global_id ) {
			throw new Tribe__REST__Exceptions__Exception(
				$this->messages->get_message( 'error-global-id-generation' ),
				'error-global-id-generation',
				500
			);
		}

		$data['global_id']         = $global_id;
		$data['global_id_lineage'] = $this->get_ticket_global_id_lineage( $ticket_id, $global_id );
	}

	/**
	 * Returns a ticket global ID.
	 *
	 * If not set/updated for the attendee than the method will generate/update it.
	 *
	 * @since 4.8
	 *
	 * @param int    $ticket_id The ticket ID.
	 * @param string $provider_class The provider class.
	 *
	 * @return bool|string
	 */
	public function get_ticket_global_id( $ticket_id, $provider_class = null ) {
		$existing = get_post_meta( $ticket_id, $this->global_id_key, true );

		if ( ! empty( $existing ) ) {
			return $existing;
		}

		if ( empty( $provider_class ) ) {
			$provider = tribe_tickets_get_ticket_provider( $ticket_id );

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

			$provider_class = $provider->class_name;
		}

		$generator = new Tribe__Tickets__Global_ID();
		$generator->origin( home_url() );
		$type = $this->get_provider_slug( $provider_class );
		$generator->type( $type );

		$global_id = $generator->generate(
			[
				'type' => $type,
				'id'   => $ticket_id,
			]
		);

		update_post_meta( $ticket_id, $this->global_id_key, $global_id );

		return $global_id;
	}

	/**
	 * Returns a ticket Global ID lineage.
	 *
	 * If not set/updated for the attendee than the method will generate/update it.
	 *
	 * @since 4.8
	 *
	 * @param int    $ticket_id The ticket ID.
	 * @param string $global_id The global ID.
	 *
	 * @return array|bool
	 */
	public function get_ticket_global_id_lineage( $ticket_id, $global_id = null ) {
		if ( null === $global_id ) {
			$global_id = $this->get_ticket_global_id( $ticket_id );

			if ( false === $global_id ) {
				return false;
			}
		}

		$existing = get_post_meta( $ticket_id, $this->global_id_lineage_key, true );

		$new = ! empty( $existing )
			? array_unique( array_merge( (array) $existing, [ $global_id ] ) )
			: [ $global_id ];

		if ( $new !== $existing ) {
			update_post_meta( $ticket_id, $this->global_id_lineage_key, $new );
		}

		return $new;
	}

	/**
	 * Adds the ticket post information to the data.
	 *
	 * @since 4.8
	 *
	 * @param array $data The ticket data.
	 *
	 * @throws Tribe__REST__Exceptions__Exception If the post fetch or parsing fails.
	 */
	protected function add_ticket_post_data( &$data ) {
		$ticket_id   = $data['id'];
		$ticket_post = get_post( $ticket_id );
		$ticket      = $this->get_ticket_object( $ticket_id );

		if ( ! $ticket_post instanceof WP_Post || $ticket instanceof WP_Error ) {
			throw new Tribe__REST__Exceptions__Exception(
				$this->messages->get_message( 'error-ticket-post' ),
				'error-ticket-post',
				500
			);
		}

		/** @var Tribe__Tickets__Tickets_Handler $handler */
		$handler = tribe( 'tickets.handler' );

		$data['author']       = $ticket_post->post_author;
		$data['status']       = $ticket_post->post_status;
		$data['date']         = $ticket_post->post_date;
		$data['date_utc']     = $ticket_post->post_date_gmt;
		$data['modified']     = $ticket_post->post_modified;
		$data['modified_utc'] = $ticket_post->post_modified_gmt;
		$data['title']        = $ticket->name;
		$data['description']  = $ticket->description;
	}

	/**
	 * Adds the meta information to the ticket data.
	 *
	 * @since 4.8
	 *
	 * @param array $data The ticket data.
	 */
	protected function add_ticket_meta_data( &$data ) {
		$ticket_id = $data['id'];

		$data['image']                   = $this->get_ticket_header_image( $ticket_id );
		$data['available_from']          = $this->get_ticket_start_date( $ticket_id );
		$data['available_from_details']  = $this->get_ticket_start_date( $ticket_id, true );
		$data['available_until']         = $this->get_ticket_end_date( $ticket_id );
		$data['available_until_details'] = $this->get_ticket_end_date( $ticket_id, true );
		$data['capacity']                = $this->get_ticket_capacity( $ticket_id );
		$data['capacity_details']        = $this->get_ticket_capacity( $ticket_id, true );
		$data['is_available']            = $data['capacity_details']['available_percentage'] > 0;
		$data['cost']                    = $this->get_ticket_cost( $ticket_id );
		$data['cost_details']            = $this->get_ticket_cost( $ticket_id, true );
		$data['sale_price_data']         = $this->get_ticket_sale_price_data( $ticket_id );

		/**
		 * Since Attendee Information is a functionality provided by Event Tickets Plus
		 * we rely on Event Ticket Plus to filter the data to add attendee information
		 * to it.
		 */
		$data['supports_attendee_information'] = false;
	}

	/**
	 * Returns a ticket header image information if set.
	 *
	 * @since 4.8
	 *
	 * @param int $ticket_id The ticket ID.
	 *
	 * @return bool|array
	 */
	public function get_ticket_header_image( $ticket_id ) {
		$post = tribe_events_get_ticket_event( $ticket_id );

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

		/** @var Tribe__Tickets__Tickets_Handler $handler */
		$handler  = tribe( 'tickets.handler' );
		$image_id = (int) get_post_meta( $post->ID, $handler->key_image_header, true );

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

		$data = $this->get_image_data( $image_id );

		/**
		 * Filters the data that will returned for a ticket header image if set.
		 *
		 * @param array   $data      The ticket header image array representation.
		 * @param WP_Post $ticket_id The requested ticket.
		 * @param WP_Post $post      The post this ticket is related to.
		 */
		return apply_filters( 'tribe_rest_event_featured_image', $data, $ticket_id, $post );
	}

	/**
	 * Returns a ticket start date.
	 *
	 * @since 4.8
	 *
	 * @param int  $ticket_id The ticket ID.
	 * @param bool $get_details Whether to get the date in string format (`false`) or the full details (`true`).
	 *
	 * @return string|array
	 */
	public function get_ticket_start_date( $ticket_id, $get_details = false ) {
		/** @var Tribe__Tickets__Tickets_Handler $handler */
		$handler = tribe( 'tickets.handler' );

		$start_info   = [
			get_post_meta( $ticket_id, $handler->key_start_date, true ),
			get_post_meta( $ticket_id, $handler->key_start_time, true ),
		];
		$start_string = implode( ' ', array_filter( $start_info ) );

		return $get_details
			? $this->get_date_details( $start_string )
			: $start_string;
	}

	/**
	 * Returns a ticket end date.
	 *
	 * @since 4.8
	 *
	 * @param int  $ticket_id The ticket ID.
	 * @param bool $get_details Whether to get the date in string format (`false`) or the full details (`true`).
	 *
	 * @return string|array
	 */
	public function get_ticket_end_date( $ticket_id, $get_details = false ) {
		/** @var Tribe__Tickets__Tickets_Handler $handler */
		$handler = tribe( 'tickets.handler' );

		$end_info   = [
			get_post_meta( $ticket_id, $handler->key_end_date, true ),
			get_post_meta( $ticket_id, $handler->key_end_time, true ),
		];
		$end_string = implode( ' ', array_filter( $end_info ) );

		return $get_details
			? $this->get_date_details( $end_string )
			: $end_string;
	}

	/**
	 * Returns a ticket capacity or capacity details.
	 *
	 * @since 4.8
	 *
	 * @param int  $ticket_id The ticket ID.
	 * @param bool $get_details Whether to get capacity details or not. Defaults to `false`. If set to `true` returns an array.
	 *
	 * @return array|bool|int The ticket capacity, the details if `$get_details` is set to `true`
	 *                        or `false` on failure.
	 */
	public function get_ticket_capacity( $ticket_id, $get_details = false ) {
		$ticket = $this->get_ticket_object( $ticket_id );

		if ( $ticket instanceof WP_Error ) {
			return false;
		}

		$capacity = $ticket->capacity();

		if ( ! $get_details ) {
			return $capacity;
		}

		/**
		 * Here we use the `Tribe__Tickets__Ticket_Object::stock()` method in
		 * place of the `Tribe__Tickets__Ticket_Object::available()` one to make
		 * sure we get the value that users would see on the front-end in the
		 * ticket form.
		 */
		$available = $ticket->stock();

		$unlimited = -1 === $available;
		if ( $unlimited ) {
			$available_percentage = 100;
		} else {
			$available_percentage = $capacity <= 0 || $available == 0 ? 0 : (int) floor( $available / $capacity * 100 );
		}

		// @todo here we need to uniform the return values to indicate unlimited and oversold!

		$details = [
			'available_percentage' => $available_percentage,
			'available'            => (int) $ticket->stock(), // see note above about why we use this.
		];

		if ( tribe( 'tickets.rest-v1.main' )->request_has_manage_access() ) {
			$details['max']               = (int) $ticket->capacity();
			$details['sold']              = (int) $ticket->qty_sold();
			$details['pending']           = (int) $ticket->qty_pending();
			$details['global_stock_mode'] = (string) $ticket->global_stock_mode();
		}

		return $details;
	}

	/**
	 * Returns a ticket cost or details.
	 *
	 * @since 4.8
	 * @since 5.19.1 Use property regular_price if set.
	 *
	 * @param int  $ticket_id The ticket ID.
	 * @param bool $get_details Whether to get just the ticket cost (`false`) or
	 *                          the details too ('true').
	 *
	 * @return string|array|false The ticket formatted cost if `$get_details` is `false`, the
	 *                            ticket cost details otherwise; `false` on failure.
	 */
	public function get_ticket_cost( $ticket_id, $get_details = false ) {
		$ticket = $this->get_ticket_object( $ticket_id );

		if ( $ticket instanceof WP_Error ) {
			return false;
		}

		/** @var Tribe__Tickets__Commerce__Currency $currency */
		$currency = tribe( 'tickets.commerce.currency' );

		$provider = $ticket->provider_class;

		$price = $ticket->price;

		if ( ! is_numeric( $price ) ) {
			$price = 0; // free.
		}

		if ( Module::class === $provider ) {
			$price = tribe( Ticket::class )->get_regular_price( $ticket_id );
		} elseif ( ! empty( $ticket->regular_price ) ) {
			$price = $ticket->regular_price;

			if ( ! is_numeric( $price ) ) {
				$price = 0;
			}
		}

		$formatted_price = html_entity_decode( $currency->format_currency( $price, $ticket_id ), ENT_COMPAT );

		if ( ! $get_details ) {
			return $formatted_price;
		}

		return [
			'currency_symbol'             => html_entity_decode( $currency->get_provider_symbol( $provider, $ticket_id ), ENT_COMPAT ),
			'currency_position'           => $currency->get_provider_symbol_position( $provider, $ticket_id ),
			'values'                      => [ $price ],
			'suffix'                      => $ticket->price_suffix,
			'currency_decimal_separator'  => $currency->get_currency_decimal_point( $provider ),
			'currency_decimal_numbers'    => $currency->get_currency_number_of_decimals(),
			'currency_thousand_separator' => $currency->get_currency_thousands_sep( $provider ),
		];
	}

	/**
	 * Adds the attendees information to the ticket.
	 *
	 * @since 4.8
	 *
	 * @param array $data The ticket data.
	 */
	protected function add_ticket_attendees_data( array &$data ) {
		// Set as empty so it prevents errors with previous usage (no shortcode/block check).
		$data['attendees'] = [];

		$ticket_id = $data['id'];

		$ticket_object = $this->get_ticket_object( $ticket_id );

		$event = $ticket_object->get_event();

		$has_manage_access          = tribe( 'tickets.rest-v1.main' )->request_has_manage_access();
		$always_show_attendees_data = $has_manage_access;

		/**
		 * Allow filtering to always show attendees data on tickets in the REST API. This bypasses checks for Attendees
		 * shortcode or block in the associated event/post content for the ticket.
		 *
		 * @since 4.10.2
		 *
		 * @param bool $always_show_attendees_data Whether to always show attendees data. By default, Admin and Editor
		 *                                         can see this information.
		 * @param array $data                      Ticket REST data.
		 */
		$always_show_attendees_data = apply_filters( 'tribe_tickets_rest_api_always_show_attendee_data', $always_show_attendees_data, $data );

		// Check if we have an event or attendees block/shortcode.
		if ( ! $always_show_attendees_data ) {
			// Return if there's no event.
			if ( ! $event ) {
				return;
			}

			// Return if event is not showing attendees.
			if (
				(
					! function_exists( 'has_block' )
					|| ! has_block( 'tribe/attendees', $event )
				)
				&& ! has_shortcode( $event->post_content, 'tribe_attendees_list' )
				// In case has_shortcode does not work.
				&& false === strpos( $event->post_content, '[tribe_attendees_list]' )
			) {
				return;
			}
		}

		$data['attendees'] = $this->get_ticket_attendees( $ticket_id );

		if (
			$ticket_object instanceof Tribe__Tickets__Ticket_Object
			&& $has_manage_access
			&& false !== $data['attendees']
		) {
			$is_rsvp = 'Tribe__Tickets__RSVP' === $ticket_object->provider_class;

			$going        = 0;
			$not_going    = 0;
			$checked_in   = 0;
			$unchecked_in = 0;

			foreach ( $data['attendees'] as $attendee ) {
				if ( $is_rsvp ) {
					if ( true === $attendee['rsvp_going'] ) {
						++$going;
					} else {
						++$not_going;
					}
				}

				if ( ! empty( $attendee['checked_in'] ) ) {
					++$checked_in;
				} else {
					++$unchecked_in;
				}
			}


			if ( $is_rsvp ) {
				$data['rsvp'] = [
					'rsvp_going'     => $going,
					'rsvp_not_going' => $not_going,
				];
			}

			$attendees_count       = count( $data['attendees'] );
			$checked_in_percentage = $attendees_count > 0
				? ceil( 100 * $checked_in / $attendees_count )
				: 100;

			$data['checkin'] = [
				'checked_in'              => $checked_in,
				'unchecked_in'            => $unchecked_in,
				'checked_in_percentage'   => $checked_in_percentage,
				'unchecked_in_percentage' => 100 - $checked_in_percentage,
			];
		}
	}

	/**
	 * Returns a ticket attendees list.
	 *
	 * @param int $ticket_id The ticket ID.
	 *
	 * @return array|bool An array of ticket attendees or `false` on failure.
	 */
	public function get_ticket_attendees( $ticket_id ) {
		$ticket_object = $this->get_ticket_object( $ticket_id );

		if ( ! $ticket_object instanceof Tribe__Tickets__Ticket_Object ) {
			return false;
		}

		$has_manage_access = tribe( 'tickets.rest-v1.main' )->request_has_manage_access();
		$permission        = $has_manage_access ? 'editable' : 'readable';

		$query = tribe_attendees( 'restv1' )
			->permission( $permission )
			->where( 'ticket', $ticket_id );

		if ( ! $has_manage_access && 'Tribe__Tickets__RSVP' === $ticket_object->provider_class ) {
			// if we are dealing with an RSVP ticket then the attendee must be going to show.
			$query->where( 'meta_equals', Tribe__Tickets__RSVP::ATTENDEE_RSVP_KEY, 'yes' );
		}

		return $query->all();
	}

	/**
	 * Returns the sale price data for a ticket.
	 *
	 * @since 5.9.0
	 * @since 5.19.1 If the provider has method `get_sale_price_details`, return that.
	 *
	 * @param int $ticket_id The ticket ID.
	 *
	 * @return array<string,string> The sale price data.
	 */
	public function get_ticket_sale_price_data( int $ticket_id ): array {
		$provider = tribe_tickets_get_ticket_provider( $ticket_id );

		if ( method_exists( $provider, 'get_sale_price_details' ) ) {
			return $provider->get_sale_price_details( $ticket_id );
		}

		if ( ! $provider instanceof Module ) {
			return [];
		}

		return tribe( Ticket::class )->get_sale_price_details( $ticket_id );
	}

	/**
	 * Returns an attendee Global ID.
	 *
	 * If not set/updated for the attendee than the method will generate/update it.
	 *
	 * @since 4.8
	 *
	 * @param int $attendee_id The attendee ID.
	 *
	 * @return string
	 */
	public function get_attendee_global_id( $attendee_id ) {
		$existing = get_post_meta( $attendee_id, $this->global_id_key, true );

		if ( ! empty( $existing ) ) {
			return $existing;
		}

		$generator = new Tribe__Tickets__Global_ID();
		$generator->origin( home_url() );
		$generator->type( 'attendee' );

		$global_id = $generator->generate(
			[
				'type' => 'attendee',
				'id'   => $attendee_id,
			]
		);

		update_post_meta( $attendee_id, $this->global_id_key, $global_id );

		return $global_id;
	}

	/**
	 * Returns an attendee Global ID lineage.
	 *
	 * If not set/updated for the attendee than the method will generate/update it.
	 *
	 * @since 4.8
	 *
	 * @param int    $attendee_id The attendee ID.
	 * @param string $global_id The global ID.
	 *
	 * @return array|bool The attendee Global ID lineage or `false` on failure.
	 */
	public function get_attendee_global_id_lineage( $attendee_id, $global_id = null ) {
		if ( null === $global_id ) {
			$global_id = $this->get_attendee_global_id( $attendee_id );
		}

		$existing = get_post_meta( $attendee_id, $this->global_id_lineage_key, true );

		$new = ! empty( $existing )
			? array_unique( array_merge( (array) $existing, [ $global_id ] ) )
			: [ $global_id ];

		if ( $new !== $existing ) {
			update_post_meta( $attendee_id, $this->global_id_lineage_key, $new );
		}

		return $new;
	}

	/**
	 * Adds REST API related information to the returned data.
	 *
	 * @since 4.8
	 *
	 * @param array<string,mixed> $data The ticket data.
	 */
	protected function add_ticket_rest_data( &$data ) {
		/** @var Tribe__Tickets__REST__V1__Main $main */
		$main = tribe( 'tickets.rest-v1.main' );

		$data['rest_url'] = $main->get_url( '/tickets/' . $data['id'] );
	}

	/**
	 * Removes fields from the ticket data.
	 *
	 * @since 4.8
	 *
	 * @param array<string> $data The ticket data.
	 */
	protected function clean_ticket_data( array &$data ) {
		$unset_map = [
			'ID',
			'name',
			'show_description',
			'price',
			'regular_price',
			'admin_link',
			'report_link',
			'frontend_link',
			'provider_class',
			'menu_order',
			'start_date',
			'start_time',
			'end_date',
			'end_time',
			'purchase_limit',
			'sku',
		];

		$data = array_diff_key( $data, array_combine( $unset_map, $unset_map ) );
	}

	/**
	 * Builds an attendee data from the attendee information.
	 *
	 * @since 4.8
	 *
	 * @param array  $attendee The attendee information.
	 * @param string $context The context in which the data will be shown; this
	 *                        is about format, not permissions.
	 *
	 * @return array
	 */
	protected function build_attendee_data( array $attendee, $context = 'default' ) {
		$ticket = $this->get_ticket_object( $attendee['product_id'] );

		if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
			return [];
		}

		$attendee_id = $attendee['attendee_id'];
		/** @var Tribe__Tickets__Data_API $data_api */
		$data_api = tribe( 'tickets.data_api' );
		$provider = $data_api->get_ticket_provider( $attendee_id );
		if ( empty( $provider ) ) {
			return [];
		}

		/** @var Tribe__Tickets__REST__V1__Main $main */
		$main = tribe( 'tickets.rest-v1.main' );

		$attendee_post = get_post( $attendee_id );

		$checked_in      = (bool) $attendee['check_in'];
		$checkin_details = false;
		if ( $checked_in ) {
			$checkin_details = get_post_meta( $attendee_id, $this->current_ticket_provider->checkin_key . '_details', true );
			if ( isset( $checkin_details['date'], $checkin_details['source'], $checkin_details['author'] ) ) {
				$checkin_details = [
					'date'         => $checkin_details['date'],
					'date_details' => $this->get_date_details( $checkin_details['date'] ),
					'source'       => $checkin_details['source'],
					'author'       => $checkin_details['author'],
				];
			} else {
				$checkin_details = false;
			}
		}

		try {
			$attendee_order_id = $this->get_attendee_order_id( $attendee_id, $provider );
		} catch ( ReflectionException $e ) {
			return [];
		}

		$formatted_price = tribe( \Tribe__Tickets__Commerce__Currency::class )->get_formatted_currency( $ticket->price, $ticket->ID, $provider );
		$currency_config = tribe( \Tribe__Tickets__Commerce__Currency::class )->get_currency_by_provider( $ticket->price, $provider );

		$attendee_data = [
			'id'                => $attendee_id,
			'post_id'           => (int) $attendee['event_id'],
			'ticket_id'         => (int) $attendee['product_id'],
			'global_id'         => $this->get_attendee_global_id( $attendee_id ),
			'global_id_lineage' => $this->get_attendee_global_id_lineage( $attendee_id ),
			'author'            => $attendee_post->post_author,
			'status'            => $attendee_post->post_status,
			'date'              => $attendee_post->post_date,
			'date_utc'          => $attendee_post->post_date_gmt,
			'modified'          => $attendee_post->post_modified,
			'modified_utc'      => $attendee_post->post_modified_gmt,
			'rest_url'          => $main->get_url( '/attendees/' . $attendee_id ),
			'ticket'            => [
				'id'              => $ticket->ID,
				'title'           => $ticket->name,
				'description'     => $ticket->description,
				'raw_price'       => $ticket->price,
				'formatted_price' => $formatted_price,
				'currency_config' => $currency_config,
				'start_sale'      => $ticket->start_date,
				'end_sale'        => $ticket->end_date,
			],
		];

		$has_manage_access = tribe( 'tickets.rest-v1.main' )->request_has_manage_access();

		// Only show the attendee name if the attendee did not optout or the user can read private posts.
		if ( empty( $attendee['optout'] ) || $has_manage_access ) {
			$attendee_data['title']  = Tribe__Utils__Array::get( $attendee, 'holder_name', Tribe__Utils__Array::get( $attendee, 'purchaser_name', '' ) );
			$attendee_data['optout'] = tribe_is_truthy( $attendee['optout'] );
		} else {
			$attendee_data['optout'] = true;
		}

		// Sensible information should not be shown to everyone.
		if ( $has_manage_access ) {
			$attendee_data = array_merge(
				$attendee_data,
				[
					'provider'        => $this->get_provider_slug( $provider ),
					'order'           => $attendee_order_id,
					'sku'             => $this->get_attendee_sku( $attendee_id, $attendee_order_id, $provider ),
					'email'           => Tribe__Utils__Array::get( $attendee, 'holder_email', Tribe__Utils__Array::get( $attendee, 'purchaser_email', '' ) ),
					'checked_in'      => $checked_in,
					'checkin_details' => $checkin_details,

					// Show Attendee flags.
					// @todo Make these live in future IAC work.
					'is_subscribed'   => false,
					'is_purchaser'    => true,
				]
			);

			if ( $provider instanceof Tribe__Tickets__RSVP ) {
				$attendee_data['rsvp_going'] = tribe_is_truthy( $attendee['order_status'] );
			} else {
				$order_id   = $attendee['order_id'];
				$order_data = method_exists( $provider, 'get_order_data' )
					? $provider->get_order_data( $order_id )
					: false;

				if ( ! empty( $order_data ) ) {
					/** @var Tribe__Tickets__Commerce__Currency $currency */
					$currency      = tribe( 'tickets.commerce.currency' );
					$ticket_object = $this->get_ticket_object( $attendee['product_id'] );

					if ( ! is_wp_error( $ticket_object ) ) {
						$purchase_time            = Tribe__Utils__Array::get( $order_data, 'purchase_time', get_post_time( Tribe__Date_Utils::DBDATETIMEFORMAT, false, $attendee_id ) );
						$attendee_data['payment'] = [
							'provider'     => Tribe__Utils__Array::get( $order_data, 'provider_slug', $this->get_provider_slug( $provider ) ),
							'price'        => ! empty( $ticket_object->price ) ? $ticket_object->price : '',
							'currency'     => html_entity_decode( $currency->get_currency_symbol( $attendee['product_id'] ), ENT_COMPAT ),
							'date'         => $purchase_time,
							'date_details' => $this->get_date_details( $purchase_time ),
						];
					}
				}
			}
		}

		/**
		 * Filters the single attendee data.
		 *
		 * @since 4.8
		 *
		 * @param array $attendee_data
		 * @param string $context The context in which the data will show; this is about format,
		 *                        not permissions.
		 */
		$attendee_data = apply_filters( 'tribe_tickets_rest_api_attendee_data', $attendee_data, $context );

		return $attendee_data;
	}

	/**
	 * Retrieves the ID of the Order associated with an attendee depending on the provider.
	 *
	 * @since 4.8
	 *
	 * @param int                     $attendee_id The attendee ID.
	 * @param Tribe__Tickets__Tickets $provider    The ticket provider.
	 *
	 * @return int|mixed
	 *
	 * @throws ReflectionException If the provider class is not valid.
	 */
	protected function get_attendee_order_id( $attendee_id, Tribe__Tickets__Tickets $provider ) {
		if ( $attendee_id instanceof WP_Post ) {
			$attendee_id = $attendee_id->ID;
		}

		// The order is the the attendee ID itself for RSVP orders.
		if ( $provider instanceof Tribe__Tickets__RSVP ) {
			return (int) $attendee_id;
		}

		if ( ! empty( $provider->attendee_order_key ) ) {
			$key = $provider->attendee_order_key;
		} else {
			$reflection = new ReflectionClass( $provider );
			$key        = $reflection->getConstant( 'ATTENDEE_ORDER_KEY' );
		}

		return get_post_meta( $attendee_id, $key, true );
	}

	/**
	 * Retrieves an Attendee ticket SKU.
	 *
	 * @since 4.8
	 *
	 * @param int                     $attendee_id The attendee ID.
	 * @param int                     $order_id    The order ID.
	 * @param Tribe__Tickets__Tickets $provider    The ticket provider.
	 *
	 * @return string
	 */
	protected function get_attendee_sku( $attendee_id, $order_id, Tribe__Tickets__Tickets $provider ) {
		$sku = get_post_meta( $attendee_id, '_sku', true );

		if ( ! empty( $sku ) ) {
			return $sku;
		}

		if ( $provider instanceof Tribe__Tickets_Plus__Commerce__WooCommerce__Main ) {
			$sku = get_post_meta( $order_id, '_sku', true );
		}

		return $sku;
	}

	/**
	 * Returns an array representation of an attendee.
	 *
	 * @since 5.7.0
	 *
	 * @param int    $attendee_id A attendee post ID.
	 * @param string $context     Context of data.
	 *
	 * @return array|WP_Error Either the array representation of an attendee or an error object.
	 */
	public function get_qr_data( $attendee_id, $context = '' ) {
		/** @var Tribe__Tickets__Data_API $data_api */
		$data_api = tribe( 'tickets.data_api' );

		$attendee      = get_post( $attendee_id );
		$attendee_type = $data_api->detect_by_id( $attendee_id );

		if ( empty( $attendee ) || empty( $attendee_type['class'] ) ) {
			return new WP_Error( 'attendee-not-found', $this->messages->get_message( 'attendee-not-found' ) );
		}

		$service_provider = $data_api->get_ticket_provider( $attendee_id );
		if ( empty( $service_provider->checkin_key ) ) {
			return new WP_Error( 'attendee-check-in-not-found', $this->messages->get_message( 'attendee-check-in-not-found' ) );
		}

		$meta = array_map( 'reset', get_post_custom( $attendee_id ) );
		$data = [
			'id'         => $attendee_id,
			'checked_in' => $meta[ $service_provider->checkin_key ] ?? '',
		];

		/**
		 * Filters the data that will be returned for a single attendee.
		 *
		 * @since 4.7.5
		 * @deprecated 5.7.0 Use `tribe_tickets_rest_qr_data` instead.
		 *
		 * @param array   $data     The data that will be returned in the response.
		 * @param WP_Post $attendee The requested attendee.
		 */
		$data = apply_filters_deprecated( 'tribe_tickets_plus_rest_qr_data', [ $data, $attendee ], '5.7.0', 'tribe_tickets_rest_qr_data' );

		/**
		 * Filters the data that will be returned for a single attendee.
		 *
		 * @since 5.7.0
		 *
		 * @param array   $data     The data that will be returned in the response.
		 * @param WP_Post $attendee The requested attendee.
		 */
		$data = apply_filters( 'tribe_tickets_rest_qr_data', $data, $attendee );

		return $data;
	}
}
