<?php

use Tribe__Utils__Array as Arr;

/**
 * Class Tribe__Tickets__Attendee_Repository
 *
 * The basic Attendee repository.
 *
 * @since 4.8
 */
class Tribe__Tickets__Attendee_Repository extends Tribe__Repository {

	/**
	 * The unique fragment that will be used to identify this repository filters.
	 *
	 * @var string
	 */
	protected $filter_name = 'attendees';

	/**
	 * Key name to use when limiting lists of keys.
	 *
	 * @since 5.1.0
	 *
	 * @var string
	 */
	protected $key_name = '';

	/**
	 * @var array An array of all the order statuses supported by the repository.
	 */
	protected static $order_statuses;

	/**
	 * The attendee provider object.
	 *
	 * @since 5.1.0
	 *
	 * @var Tribe__Tickets__Tickets
	 */
	protected $attendee_provider;

	/**
	 * @var array An array of all the public order statuses supported by the repository.
	 *            This list is hand compiled as reduced and easier to maintain.
	 */
	protected static $public_order_statuses = [
		'yes',     // RSVP
		'completed', // PayPal Legacy
		'wc-completed', // WooCommerce
		'publish', // Easy Digital Downloads, Legacy
		'complete', // Easy Digital Downloads
	];

	/**
	 * @var array An array of all the private order statuses supported by the repository.
	 */
	protected static $private_order_statuses;

	/**
	 * Tribe__Tickets__Attendee_Repository constructor.
	 */
	public function __construct() {
		parent::__construct();

		$this->create_args['post_type']   = current( $this->attendee_types() );
		$this->create_args['post_status'] = 'publish';
		$this->create_args['ping_status'] = 'closed';

		$this->default_args = array_merge( $this->default_args, [
			'post_type'   => $this->attendee_types(),
			'orderby'     => [ 'date', 'title', 'ID' ],
			'post_status' => 'any',
		] );

		// Add initial simple schema.
		$this->add_schema_entry( 'event', [ $this, 'filter_by_event' ] );
		$this->add_schema_entry( 'event__not_in', [ $this, 'filter_by_event_not_in' ] );
		$this->add_simple_meta_schema_entry( 'ticket', $this->attendee_to_ticket_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'ticket__not_in', $this->attendee_to_ticket_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'order', $this->attendee_to_order_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'order__not_in', $this->attendee_to_order_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'product_id', $this->attendee_to_ticket_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'product_id__not_in', $this->attendee_to_ticket_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'purchaser_name', $this->purchaser_name_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'purchaser_name__not_in', $this->purchaser_name_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'purchaser_name__like', $this->purchaser_name_keys(), 'meta_like' );
		$this->add_simple_meta_schema_entry( 'purchaser_email', $this->purchaser_email_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'purchaser_email__not_in', $this->purchaser_email_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'purchaser_email__like', $this->purchaser_email_keys(), 'meta_like' );
		$this->add_simple_meta_schema_entry( 'holder_name', $this->holder_name_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'holder_name__not_in', $this->holder_name_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'holder_name__like', $this->holder_name_keys(), 'meta_like' );
		$this->add_simple_meta_schema_entry( 'holder_email', $this->holder_email_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'holder_email__not_in', $this->holder_email_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'holder_email__like', $this->holder_email_keys(), 'meta_like' );
		$this->add_simple_meta_schema_entry( 'security_code', $this->security_code_keys(), 'meta_in' );
		$this->add_simple_meta_schema_entry( 'security_code__not_in', $this->security_code_keys(), 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'user', '_tribe_tickets_attendee_user_id', 'meta_in' );
		$this->add_simple_meta_schema_entry( 'user__not_in', '_tribe_tickets_attendee_user_id', 'meta_not_in' );
		$this->add_simple_meta_schema_entry( 'price', '_paid_price' );

		$this->schema = array_merge( $this->schema, [
			'checkedin'             => [ $this, 'filter_by_checkedin' ],
			'event__show_attendees' => [ $this, 'filter_by_show_attendees' ],
			'event_status'          => [ $this, 'filter_by_event_status' ],
			'has_attendee_meta'     => [ $this, 'filter_by_attendee_meta_existence' ],
			'optout'                => [ $this, 'filter_by_optout' ],
			'order_status__not_in'  => [ $this, 'filter_by_order_status_not_in' ],
			'order_status'          => [ $this, 'filter_by_order_status' ],
			'price_max'             => [ $this, 'filter_by_price_max' ],
			'price_min'             => [ $this, 'filter_by_price_min' ],
			'provider__not_in'      => [ $this, 'filter_by_provider_not_in' ],
			'provider'              => [ $this, 'filter_by_provider' ],
			'rsvp_status__or_none'  => [ $this, 'filter_by_rsvp_status_or_none' ],
			'rsvp_status'           => [ $this, 'filter_by_rsvp_status' ],
			'ticket_type'           => [ $this, 'filter_by_ticket_type' ],
			'ticket_type__not_in'   => [ $this, 'filter_by_ticket_type_not_in' ],
		] );

		// Add object default aliases.
		$this->update_fields_aliases = array_merge(
			$this->update_fields_aliases,
			[
				'ticket_id'      => '_tribe_tickets_ticket_id',
				'event_id'       => '_tribe_tickets_post_id',
				'post_id'        => '_tribe_tickets_post_id',
				'security_code'  => '_tribe_tickets_security_code',
				'order_id'       => '_tribe_tickets_order_id',
				'optout'         => '_tribe_tickets_optout',
				'user_id'        => '_tribe_tickets_user_id',
				'price_paid'     => '_tribe_tickets_price_paid',
				'price_currency' => '_tribe_tickets_price_currency_symbol',
				'full_name'      => '_tribe_tickets_full_name',
				'email'          => '_tribe_tickets_email',
				'check_in'       => current( $this->checked_in_keys() ),
			]
		);

		$this->init_order_statuses();
	}

	/**
	 * Returns an array of the attendee types handled by this repository.
	 *
	 * Extending repository classes should override this to add more attendee types.
	 *
	 * @since 4.8
	 *
	 * @return array
	 */
	public function attendee_types() {
		return [
			'rsvp'                          => 'tribe_rsvp_attendees',
			'tribe-commerce'                => 'tribe_tpp_attendees',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::POSTTYPE,
		];
	}

	/**
	 * Returns the list of meta keys relating an Attendee to a Post (Event).
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 4.8
	 *
	 * @return array
	 */
	public function attendee_to_event_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_event',
			'tribe-commerce'                => '_tribe_tpp_event',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$event_relation_meta_key,
		];
	}

	/**
	 * Returns the list of meta keys relating an Attendee to a Ticket.
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 4.8
	 *
	 * @return array
	 */
	public function attendee_to_ticket_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_product',
			'tribe-commerce'                => '_tribe_tpp_product',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$ticket_relation_meta_key,
		];
	}

	/**
	 * Returns a list of meta keys relating an attendee to the order
	 * that generated it.
	 *
	 * @since 4.8
	 *
	 * @return array
	 */
	protected function attendee_to_order_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_order',
			'tribe-commerce'                => '_tribe_tpp_order',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$order_relation_meta_key,
		];
	}

	/**
	 * Returns the list of meta keys relating an Attendee to a Post (Event).
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 4.10.6
	 *
	 * @return array
	 */
	public function purchaser_name_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_full_name',
			'tribe-commerce'                => '_tribe_tpp_full_name',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$full_name_meta_key,
		];
	}

	/**
	 * Returns the list of meta keys relating an Attendee to a Post (Event).
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 4.10.6
	 *
	 * @return array
	 */
	public function purchaser_email_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_email',
			'tribe-commerce'                => '_tribe_tpp_email',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$email_meta_key,
		];
	}

	/**
	 * Returns the list of meta keys relating an Attendee to a Post (Event).
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 5.2.1
	 *
	 * @return array
	 */
	public function holder_name_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_full_name',
			'tribe-commerce'                => '_tribe_tickets_full_name',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$full_name_meta_key,
		];
	}

	/**
	 * Returns the list of meta keys relating an Attendee to a Post (Event).
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 5.2.1
	 *
	 * @return array
	 */
	public function holder_email_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_email',
			'tribe-commerce'                => '_tribe_tickets_email',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$email_meta_key,
		];
	}

	/**
	 * Returns the list of meta keys relating an Attendee to a Post (Event).
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 4.10.6
	 *
	 * @return array
	 */
	public function security_code_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_security_code',
			'tribe-commerce'                => '_tribe_tpp_security_code',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$security_code_meta_key,
		];
	}

	/**
	 * Returns the list of meta keys denoting an Attendee optout choice.
	 *
	 * Extending repository classes should override this to add more keys.
	 *
	 * @since 4.8
	 *
	 * @return array
	 */
	public function attendee_optout_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_attendee_optout',
			'tribe-commerce'                => '_tribe_tpp_attendee_optout',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$optout_meta_key,
		];
	}

	/**
	 * Returns a list of meta keys indicating an attendee checkin status.
	 *
	 * @since 4.8
	 *
	 * @return array
	 */
	public function checked_in_keys() {
		return [
			'rsvp'                          => '_tribe_rsvp_checkedin',
			'tribe-commerce'                => '_tribe_tpp_checkedin',
			\TEC\Tickets\Commerce::PROVIDER => \TEC\Tickets\Commerce\Attendee::$checked_in_meta_key,
		];
	}

	/**
	 * Provides arguments to filter attendees by their optout status.
	 *
	 * @since 4.8
	 *
	 * @param string $optout An optout option, supported 'yes','no','any'.
	 *
	 * @return array|null
	 */
	public function filter_by_optout( $optout ) {
		global $wpdb;

		switch ( $optout ) {
			case 'any':
				return null;
				break;
			case 'no':
				$this->by( 'meta_not_in', $this->attendee_optout_keys(), [ 'yes', 1 ] );
				break;
			case 'yes':
				$this->by( 'meta_in', $this->attendee_optout_keys(), [ 'yes', 1 ] );
				break;
			case 'no_or_none':
				$optout_keys = $this->attendee_optout_keys();
				$optout_keys = array_map( [ $wpdb, '_real_escape' ], $optout_keys );
				$optout_keys = "'" . implode( "', '", $optout_keys ) . "'";

				$this->filter_query->join( "
					LEFT JOIN {$wpdb->postmeta} attendee_optout
					ON ( attendee_optout.post_id = {$wpdb->posts}.ID
						AND attendee_optout.meta_key IN ( {$optout_keys} ) )
				" );

				$this->filter_query->where( "(
					attendee_optout.post_id IS NULL
					OR attendee_optout.meta_value NOT IN ( 'yes', '1' )
				)" );

				break;
		}

		return null;
	}

	/**
	 * Provides arguments to filter attendees by a specific RSVP status.
	 *
	 * @since 4.8
	 *
	 * @param string $rsvp_status
	 *
	 * @return array
	 */
	public function filter_by_rsvp_status( $rsvp_status ) {
		return Tribe__Repository__Query_Filters::meta_in(
			Tribe__Tickets__RSVP::ATTENDEE_RSVP_KEY,
			$rsvp_status,
			'by-rsvp-status'
		);
	}

	/**
	 * Provides arguments to filter attendees by a specific RSVP status or no status at all.
	 *
	 * Mind that we allow tickets not to have an RSVP status at all and
	 * still match. This assumes that all RSVP tickets will have a status
	 * assigned (which is the default behaviour).
	 *
	 * @since 4.8
	 *
	 * @param string $rsvp_status
	 *
	 * @return array
	 */
	public function filter_by_rsvp_status_or_none( $rsvp_status ) {
		return Tribe__Repository__Query_Filters::meta_in_or_not_exists(
			Tribe__Tickets__RSVP::ATTENDEE_RSVP_KEY,
			$rsvp_status,
			'by-rsvp-status-or-none'
		);
	}

	/**
	 * Provides arguments to filter attendees by the ticket provider.
	 *
	 * To avoid lengthy queries we check if a provider specific meta
	 * key relating the Attendee to the event (a post) is set.
	 *
	 * @since 4.8
	 *
	 * @param string|array $provider A provider supported slug or an
	 *                               array of supported provider slugs.
	 *
	 * @return array
	 */
	public function filter_by_provider( $provider ) {
		$providers = Arr::list_to_array( $provider );
		$meta_keys = Arr::map_or_discard( (array) $providers, $this->attendee_to_event_keys() );

		$this->by( 'meta_exists', $meta_keys );
	}

	/**
	 * Provides arguments to exclude attendees by the ticket provider.
	 *
	 * To avoid lengthy queries we check if a provider specific meta
	 * key relating the Attendee to the event (a post) is not set.
	 *
	 * @since 4.8
	 *
	 * @param string|array $provider A provider supported slug or an
	 *                               array of supported provider slugs.
	 *
	 * @return array
	 */
	public function filter_by_provider_not_in( $provider ) {
		$providers = Arr::list_to_array( $provider );
		$meta_keys = Arr::map_or_discard( (array) $providers, $this->attendee_to_event_keys() );

		$this->by( 'meta_not_exists', $meta_keys );
	}

	/**
	 * Filters attendee to only get those related to posts with a specific status.
	 *
	 * @since 4.8
	 *
	 * @param string|array $event_status
	 *
	 * @throws Tribe__Repository__Void_Query_Exception If the requested statuses are not accessible by the user.
	 *
	 */
	public function filter_by_event_status( $event_status ) {
		$statuses = Arr::list_to_array( $event_status );

		$can_read_private_posts = current_user_can( 'read_private_posts' );

		// map the `any` meta-status
		if ( 1 === count( $statuses ) && 'any' === $statuses[0] ) {
			if ( ! $can_read_private_posts ) {
				$statuses = [ 'publish' ];
			} else {
				// no need to filter if the user can read all posts
				return;
			}
		}

		if ( ! $can_read_private_posts ) {
			$event_status = array_intersect( $statuses, [ 'publish' ] );
		}

		if ( empty( $event_status ) ) {
			throw Tribe__Repository__Void_Query_Exception::because_the_query_would_yield_no_results(
				'The user cannot read posts with the requested post statuses.'
			);
		}

		$this->where_meta_related_by(
			$this->attendee_to_event_keys(),
			'IN',
			'post_status',
			$statuses
		);
	}

	/**
	 * Filters attendee to only get those related to posts with "Show attendees list on event page" set to true.
	 *
	 *
	 * @since 4.11.1
	 */
	public function filter_by_show_attendees() {
		$this->where_meta_related_by_meta(
			$this->attendee_to_event_keys(),
			'=',
			'_tribe_hide_attendees_list',
			1,
			true
		);
	}

	/**
	 * Filters attendee to only get those related to orders with a specific ID.
	 *
	 * @since TVD
	 *
	 * @param string|array $order_id Order ID(s).
	 */
	public function filter_by_order( $order_id ) {
		$order_ids = Arr::list_to_array( $order_id );

		$this->by( 'meta_in', $this->attendee_to_order_keys(), $order_ids );
	}

	/**
	 * Applies the WHERE and JOIN clauses to filter Attendees by a specific order status.
	 *
	 * @since 5.6.4
	 *
	 * @param string $where_clause   The WHERE clause to apply.
	 * @param string $value_operator The operator to use for the value clause.
	 * @param string $value_clause   The value clause to use.
	 */
	protected function filter_by_order_status_where( string $where_clause, string $value_operator, string $value_clause ): void {
		$this->filter_query->where( $where_clause );
	}

	/**
	 * Filters attendee to only get those related to orders with a specific status.
	 *
	 * @since 4.8
	 * @since 5.6.4 Refactored the logic to remove Event Tickets Plus logic.
	 * @since 5.6.5 Added support to filter by TicketsCommerce order status.
	 *
	 * @param string       $type         Type of matching (in, not_in, like).
	 *
	 * @param string|array $order_status Order status.
	 *
	 * @throws Tribe__Repository__Void_Query_Exception If the requested statuses are not accessible by the user.
	 *
	 */
	public function filter_by_order_status( $order_status, $type = 'in' ) {
		$statuses = Arr::list_to_array( $order_status );

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

		// map the `any` meta-status
		if ( 1 === count( $statuses ) && 'any' === $statuses[0] ) {
			if ( ! $has_manage_access ) {
				$statuses = [ 'public' ];
			} else {
				// no need to filter if the user can read all posts
				return;
			}
		}

		// set a status for tc only, that fetches the wp_slug from the given slugs in $statuses array.

		// Allow the user to define singular statuses or the meta-status "public"
		if ( in_array( 'public', $statuses, true ) ) {
			$statuses = array_unique( array_merge( $statuses, self::$public_order_statuses ) );
		}

		// Allow the user to define singular statuses or the meta-status "private"
		if ( in_array( 'private', $statuses, true ) ) {
			$statuses = array_unique( array_merge( $statuses, self::$private_order_statuses ) );
		}

		// Remove any status the user cannot access
		if ( ! $has_manage_access ) {
			$statuses = array_intersect( $statuses, self::$public_order_statuses );
		}

		if ( empty( $statuses ) ) {
			throw Tribe__Repository__Void_Query_Exception::because_the_query_would_yield_no_results(
				'The user cannot access the requested attendee order statuses.'
			);
		}

		/** @var wpdb $wpdb */
		global $wpdb;

		$value_operator = 'IN';
		$value_clause   = "( '" . implode( "','", array_map( [ $wpdb, '_escape' ], $statuses ) ) . "' )";

		if ( 'not_in' === $type ) {
			$value_operator = 'NOT IN';
		}

		$this->filter_query->join( "
			LEFT JOIN {$wpdb->postmeta} order_status_meta
			ON order_status_meta.post_id = {$wpdb->posts}.ID
		", 'order-status-meta' );

		$et_where_clause = "
			(
				order_status_meta.meta_key IN ( '_tribe_rsvp_status', '_tribe_tpp_status' )
				AND order_status_meta.meta_value {$value_operator} {$value_clause}
			)
		";

		// filter $statuses to only get proper TC status slugs.
		$tc_order_statuses = array_filter( array_map( function ( $status ) {
			$status_obj = tribe( \TEC\Tickets\Commerce\Status\Status_Handler::class )->get_by_slug( $status );

			return $status_obj ? $status_obj->get_wp_slug() : '';
		}, $statuses ) );

		$tc_order_statuses  = "( '" . implode( "','", array_map( [ $wpdb, '_escape' ], $tc_order_statuses ) ) . "' )";
		$tc_order_post_type = \TEC\Tickets\Commerce\Order::POSTTYPE;

		$this->filter_query->join( "LEFT JOIN {$wpdb->posts} tc_order_status ON (
		 {$wpdb->posts}.post_parent = tc_order_status.ID
		  AND tc_order_status.post_type = '{$tc_order_post_type}'
		  AND tc_order_status.post_status IN {$tc_order_statuses} )" );

		$et_where_clause .= " OR {$wpdb->posts}.post_parent IN ( tc_order_status.ID )";

		// Allow extending classes to alter the JOIN and WHERE clauses.
		$this->filter_by_order_status_where( $et_where_clause, $value_operator, $value_clause );
	}

	/**
	 * Filters attendee to only get those not related to orders with a specific status.
	 *
	 * @since 4.10.6
	 *
	 * @param string|array $order_status
	 *
	 * @throws Tribe__Repository__Void_Query_Exception If the requested statuses are not accessible by the user.
	 *
	 */
	public function filter_by_order_status_not_in( $order_status ) {
		$this->filter_by_order_status( $order_status, 'not_in' );
	}

	/**
	 * Filters Attendees by a minimum paid price.
	 *
	 * @since 4.8
	 *
	 * @param int $price_min
	 */
	public function filter_by_price_min( $price_min ) {
		$this->by( 'meta_gte', '_paid_price', (int) $price_min );
	}

	/**
	 * Filters Attendees by a maximum paid price.
	 *
	 * @since 4.8
	 *
	 * @param int $price_max
	 */
	public function filter_by_price_max( $price_max ) {
		$this->by( 'meta_lte', '_paid_price', (int) $price_max );
	}

	/**
	 * Filters attendee depending on them having additional
	 * information or not.
	 *
	 * @since 4.8
	 *
	 * @param bool $exists
	 */
	public function filter_by_attendee_meta_existence( $exists ) {
		if ( $exists ) {
			$this->by( 'meta_exists', '_tribe_tickets_meta' );
		} else {
			$this->by( 'meta_not_exists', '_tribe_tickets_meta' );
		}
	}

	/**
	 * Filters attendees depending on their checkedin status.
	 *
	 * @since 4.8
	 * @since 5.6.4 Refactored the logic to use `Tribe__Repository__Query_Filters::meta_not` on `false`.
	 *
	 * @param bool $checkedin
	 *
	 * @return array|null Either the filtered query or `null` if the query filtering does not require arguments.
	 */
	public function filter_by_checkedin( $checkedin ) {
		$meta_keys = $this->checked_in_keys();

		if ( tribe_is_truthy( $checkedin ) ) {
			return Tribe__Repository__Query_Filters::meta_in( $meta_keys, '1', 'is-checked-in' );
		}

		$this->filter_query->meta_not( $meta_keys, '1', 'is-not-checked-in' );
	}

	/**
	 * Bootstrap method called once per request to compile the available
	 * order statuses.
	 *
	 * @since 4.8
	 *
	 * @return bool|string
	 */
	protected function init_order_statuses() {
		/** @var Tribe__Tickets__Status__Manager $status_mgr */
		$status_mgr = tribe( 'tickets.status' );

		if ( empty( self::$order_statuses ) ) {
			// For RSVP tickets the order status is the going status
			$statuses = [ 'yes', 'no' ];

			if ( tribe( 'tickets.commerce.paypal' )->is_active() ) {
				$statuses = array_merge( $statuses, $status_mgr->get_statuses_by_action( 'all', 'tpp' ) );
			}

			if (
				class_exists( 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main' )
				&& function_exists( 'wc_get_order_statuses' )
			) {
				$statuses = array_merge( $statuses, $status_mgr->get_statuses_by_action( 'all', 'woo' ) );
			}

			if (
				class_exists( 'Tribe__Tickets_Plus__Commerce__EDD__Main' )
				&& function_exists( 'edd_get_payment_statuses' )
			) {
				$edd_statuses = $status_mgr->get_statuses_by_action( 'all', 'edd' );

				// Remove complete status.
				$edd_statuses = array_diff( [ 'Complete' ], $edd_statuses );

				$statuses = array_merge( $statuses, $edd_statuses );
			}

			// Enforce lowercase for comparison purposes.
			$statuses = array_map( 'strtolower', $statuses );

			// Prevent unnecessary duplicates.
			$statuses = array_unique( $statuses );

			self::$order_statuses         = $statuses;
			self::$private_order_statuses = array_diff( $statuses, self::$public_order_statuses );
		}
	}

	/**
	 * Get key from list of keys if it exists and fallback to empty array.
	 *
	 * @since 4.10.5
	 *
	 * @param string $key  Key name.
	 * @param array  $list List of keys.
	 *
	 * @return array List of matching keys.
	 */
	protected function limit_list( $key, $list ) {
		if ( ! array_key_exists( $key, $list ) ) {
			return [];
		}

		return [
			$key => $list[ $key ],
		];
	}

	/**
	 * {@inheritDoc}
	 *
	 * @return WP_Post|false The new post object or false if unsuccessful.
	 */
	public function create() {
		/*
		 * Only create if we are using a specific attendee context. The post type used is
		 * entirely dependent on the provider-specific implementation for attendees.
		 */
		if ( ! $this->key_name ) {
			return false;
		}

		/*
		 * Only create if we have a ticket set.
		 */
		if ( ! isset( $this->updates['ticket_id'] ) ) {
			return false;
		}

		return parent::create();
	}

	/**
	 * Create an attendee object from ticket and attendee data.
	 *
	 * @since 5.1.0
	 *
	 * @param array                             $attendee_data List of additional attendee data.
	 *
	 * @param Tribe__Tickets__Ticket_Object|int $ticket        The ticket object or ID.
	 *
	 * @return WP_Post|false The new post object or false if unsuccessful.
	 *
	 * @throws Tribe__Repository__Usage_Error If the argument types are not set as expected.
	 *
	 */
	public function create_attendee_for_ticket( $ticket, $attendee_data ) {
		// Attempt to get the ticket object from the ticket ID.
		if ( is_numeric( $ticket ) && $this->attendee_provider ) {
			$ticket = $this->attendee_provider->get_ticket( null, $ticket );
		}

		// Require the ticket be a ticket object.
		if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
			throw new Tribe__Repository__Usage_Error( 'You must provide a valid ticket ID or object when creating an attendee from the Attendees Repository class' );
		}

		// Set the attendee arguments accordingly.
		$this->set_attendee_args( $attendee_data, $ticket );

		// Update the attendee data for referencing with what we handled in the set_attendee_args().
		$attendee_data = array_merge( $attendee_data, $this->updates );

		// Create the new attendee.
		$attendee = $this->create();

		if ( $attendee ) {
			// Handle any further attendee updates.
			$this->save_extra_attendee_data( $attendee, $attendee_data, $ticket );

			// Trigger creation actions.
			$this->trigger_create_actions( $attendee, $attendee_data, $ticket );
		}

		return $attendee;
	}

	/**
	 * Create an attendee object from ticket and attendee data.
	 *
	 * @since 5.1.0
	 *
	 * @param bool  $return_promise Whether to return a promise object or just the ids
	 *                              of the updated posts; if `true` then a promise will
	 *                              be returned whether the update is happening in background
	 *                              or not.
	 *
	 * @param array $attendee_data  List of attendee data to be saved.
	 *
	 * @return array|Tribe__Promise A list of the post IDs that have been (synchronous) or will
	 *                              be (asynchronous) updated if `$return_promise` is set to `false`;
	 *                              the Promise object if `$return_promise` is set to `true`.
	 *
	 * @throws Tribe__Repository__Usage_Error If the argument types are not set as expected.
	 *
	 */
	public function update_attendee( $attendee_data, $return_promise = false ) {
		if ( empty( $attendee_data['attendee_id'] ) ) {
			throw new Tribe__Repository__Usage_Error( 'You must provide the attendee_id when updating an attendee from the Attendees Repository class' );
		}

		$this->by( 'id', $attendee_data['attendee_id'] );

		/**
		 * Filter the attendee data before updating the attendee.
		 *
		 * @since 5.1.2
		 *
		 * @param array                               $attendee_data Attendee data that needs to be updated.
		 * @param Tribe__Tickets__Attendee_Repository $repository    The Tickets Attendee ORM object.
		 */
		$attendee_data = apply_filters( 'tribe_tickets_attendee_repository_update_attendee_data_args_before_update', $attendee_data, $this );

		// Set the attendee arguments accordingly.
		$this->set_attendee_args( $attendee_data );

		// Update the attendee.
		$saved = $this->save( $return_promise );

		if ( $return_promise ) {
			$repository = $this;

			return $saved->then(
				static function () use ( $repository, $attendee_data ) {
					// Trigger the update actions.
					$repository->trigger_update_actions( $attendee_data );
				}
			);
		}

		// Trigger the update actions.
		$this->trigger_update_actions( $attendee_data );

		return $saved;
	}

	/**
	 * Set arguments for attendee.
	 *
	 * @since 5.1.0
	 *
	 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object or null if not relying on it.
	 *
	 * @param array                         $attendee_data List of additional attendee data.
	 *
	 * @throws Tribe__Repository__Usage_Error If the argument types are not set as expected.
	 *
	 */
	public function set_attendee_args( $attendee_data, $ticket = null ) {
		$args = [
			'attendee_id'       => null,
			'title'             => null,
			'full_name'         => null,
			'email'             => null,
			'ticket_id'         => $ticket ? $ticket->ID : null,
			'post_id'           => $ticket ? $ticket->get_event_id() : null,
			'order_id'          => null,
			'order_attendee_id' => null,
			'user_id'           => null,
			'attendee_status'   => null,
			'price_paid'        => null,
			'optout'            => null,
			'check_in'          => null,
		];

		$args = array_merge( $args, $attendee_data );

		$attendee_id = null;

		$ignored = [
			'send_ticket_email',
			'send_ticket_email_args',
		];

		// Remove ignored arguments from being saved.
		foreach ( $ignored as $ignore ) {
			if ( isset( $args[ $ignore ] ) ) {
				unset( $args[ $ignore ] );
			}
		}

		// Unset the attendee ID if found.
		if ( isset( $args['attendee_id'] ) ) {
			$attendee_id = $args['attendee_id'];

			unset( $args['attendee_id'] );
		}

		// Do some extra set up if creating an attendee.
		if ( null === $attendee_id ) {
			// Default attendees to opted-out.
			if ( null === $args['optout'] ) {
				$args['optout'] = 1;
			}

			// Attempt to create order if none set.
			if ( empty( $args['order_id'] ) && $ticket ) {
				$order_id = $this->create_order_for_attendee( $args, $ticket );

				if ( $order_id ) {
					$args['order_id'] = $order_id;
				}
			}

			// If the title is empty, set the title from the full name.
			if ( empty( $args['title'] ) && $args['full_name'] ) {
				$args['title'] = $args['full_name'];

				// Maybe add the Order ID.
				if ( $args['order_id'] ) {
					$args['title'] = $args['order_id'] . ' | ' . $args['title'];
				}

				// Maybe add the Order Attendee ID.
				if ( null !== $args['order_attendee_id'] ) {
					$args['title'] .= ' | ' . $args['order_attendee_id'];
				}
			}

			// Maybe handle setting the User ID based on information we already have.
			if ( empty( $args['user_id'] ) && ! empty( $args['email'] ) && $this->attendee_provider ) {
				$user_id = $this->attendee_provider->maybe_setup_attendee_user_from_email( $args['email'], $args );

				if ( $user_id ) {
					$args['user_id'] = $user_id;
				}
			}

			if ( isset( $args['optout'] ) ) {
				// Enforce a 0/1 value for the optout value.
				$args['optout'] = (int) tribe_is_truthy( $args['optout'] );
			}

			if ( isset( $args['check_in'] ) ) {
				// Enforce a 0/1 value for the check_in value.
				$args['check_in'] = (int) tribe_is_truthy( $args['check_in'] );
			}
		}

		// Handle any customizations per provider for the attendee arguments.
		$args = $this->setup_attendee_args( $args, $attendee_data, $ticket );

		/**
		 * Allow filtering the arguments to set for the attendee.
		 *
		 * @since 5.1.0
		 *
		 * @param array                         $args          List of arguments to set for the attendee.
		 * @param array                         $attendee_data List of additional attendee data.
		 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object or null if not relying on it.
		 */
		$args = apply_filters( 'tribe_tickets_attendee_repository_set_attendee_args', $args, $attendee_data, $ticket );

		// Maybe run filter if using a provider key name.
		if ( $this->key_name ) {
			/**
			 * Allow filtering the arguments to set for the attendee by provider key name.
			 *
			 * @since 5.1.0
			 *
			 * @param array                         $args          List of arguments to set for the attendee.
			 * @param array                         $attendee_data List of additional attendee data.
			 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object or null if not relying on it.
			 */
			$args = apply_filters( 'tribe_tickets_attendee_repository_set_attendee_args_' . $this->key_name, $args, $attendee_data, $ticket );
		}

		// Remove arguments that are null.
		$args = array_filter(
			$args,
			static function ( $value ) {
				return ! is_null( $value );
			}
		);

		// Remove unused arguments from saving.
		if ( isset( $args['order_attendee_id'] ) ) {
			unset( $args['order_attendee_id'] );
		}

		$this->set_args( $args );
	}

	/**
	 * Set up the arguments to set for the attendee for this provider.
	 *
	 * @since 5.1.0
	 *
	 * @param array                              $args          List of arguments to set for the attendee.
	 * @param array                              $attendee_data List of additional attendee data.
	 * @param null|Tribe__Tickets__Ticket_Object $ticket        The ticket object or null if not relying on it.
	 *
	 * @return array List of arguments to set for the attendee.
	 */
	public function setup_attendee_args( $args, $attendee_data, $ticket = null ) {
		// Providers can override this.
		return $args;
	}

	/**
	 * Save extra attendee data after creation of attendee.
	 *
	 * @since 5.1.0
	 *
	 * @param WP_Post                       $attendee      The attendee object.
	 * @param array                         $attendee_data List of additional attendee data.
	 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object.
	 */
	public function save_extra_attendee_data( $attendee, $attendee_data, $ticket ) {
		$args = [];

		// Set up security code if it was not already customized.
		if ( empty( $attendee_data['security_code'] ) && $this->attendee_provider ) {
			$key = $attendee->ID;

			if ( ! empty( $attendee_data['order_id'] ) ) {
				$key = $attendee_data['order_id'] . '_' . $key;
			}

			$args['security_code'] = $this->attendee_provider->generate_security_code( $key );
		}

		/**
		 * Allow filtering the arguments to be used when saving extra attendee data.
		 *
		 * @since 5.1.0
		 *
		 * @param array                         $args          List of arguments to set for the attendee.
		 * @param WP_Post                       $attendee      The attendee object.
		 * @param array                         $attendee_data List of additional attendee data.
		 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object.
		 */
		$args = apply_filters( 'tribe_tickets_attendee_repository_save_extra_attendee_data_args', $args, $attendee, $attendee_data, $ticket );

		// Maybe run filter if using a provider key name.
		if ( $this->key_name ) {
			/**
			 * Allow filtering the arguments to be used when saving extra attendee data by provider key name.
			 *
			 * @since 5.1.0
			 *
			 * @param array                         $args          List of arguments to set for the attendee.
			 * @param WP_Post                       $attendee      The attendee object.
			 * @param array                         $attendee_data List of additional attendee data.
			 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object.
			 */
			$args = apply_filters( 'tribe_tickets_attendee_repository_save_extra_attendee_data_args_' . $this->key_name, $args, $attendee, $attendee_data, $ticket );
		}

		// If no args are set to be saved, bail.
		if ( empty( $args ) ) {
			return;
		}

		$query = tribe_attendees( $this->key_name );

		$query->by( 'id', $attendee->ID );

		try {
			$query->set_args( $args );
		} catch ( Tribe__Repository__Usage_Error $e ) {
			do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );

			return;
		}

		$query->save();
	}

	/**
	 * Trigger the creation actions needed based on the provider.
	 *
	 * @since 5.1.0
	 *
	 * @param WP_Post                       $attendee      The attendee object.
	 * @param array                         $attendee_data List of additional attendee data.
	 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object.
	 */
	public function trigger_create_actions( $attendee, $attendee_data, $ticket ) {
		/**
		 * Allow hooking into after the attendee has been created.
		 *
		 * @since 5.1.0
		 *
		 * @param WP_Post                             $attendee      The attendee object.
		 * @param array                               $attendee_data List of additional attendee data.
		 * @param Tribe__Tickets__Ticket_Object       $ticket        The ticket object.
		 * @param Tribe__Tickets__Attendee_Repository $repository    The current repository object.
		 */
		do_action( 'tribe_tickets_attendee_repository_create_attendee_for_ticket_after_create', $attendee, $attendee_data, $ticket, $this );

		// Maybe run filter if using a provider key name.
		if ( $this->key_name ) {
			/**
			 * Allow hooking into after the attendee has been created by provider key name.
			 *
			 * @since 5.1.0
			 *
			 * @param WP_Post                             $attendee      The attendee object.
			 * @param array                               $attendee_data List of additional attendee data.
			 * @param Tribe__Tickets__Ticket_Object       $ticket        The ticket object.
			 * @param Tribe__Tickets__Attendee_Repository $repository    The current repository object.
			 */
			do_action( 'tribe_tickets_attendee_repository_create_attendee_for_ticket_after_create_' . $this->key_name, $attendee, $attendee_data, $ticket, $this );
		}

		// Maybe send the attendee email.
		$this->maybe_send_attendee_email( $attendee->ID, $attendee_data );

		// Handle clearing the caches.
		if ( $this->attendee_provider ) {
			// Clear the attendee cache if post_id is provided.
			if ( ! empty( $this->updates['post_id'] ) ) {
				$this->attendee_provider->clear_attendees_cache( $this->updates['post_id'] );
			}

			// Clear the ticket cache if ticket is provided.
			if ( $ticket ) {
				$this->attendee_provider->clear_ticket_cache( $ticket->ID );
			}
		}
	}

	/**
	 * Trigger the update actions needed based on the provider.
	 *
	 * @since 5.1.0
	 *
	 * @param array $attendee_data List of attendee data to be saved.
	 */
	public function trigger_update_actions( $attendee_data ) {
		/**
		 * Allow hooking into after the attendee has been updated.
		 *
		 * @since 5.1.0
		 *
		 * @param array                               $attendee_data List of attendee data to be saved.
		 * @param Tribe__Tickets__Attendee_Repository $repository    The current repository object.
		 */
		do_action( 'tribe_tickets_attendee_repository_update_attendee_after_update', $attendee_data, $this );

		// Maybe run filter if using a provider key name.
		if ( $this->key_name ) {
			/**
			 * Allow hooking into after the attendee has been updated by provider key name.
			 *
			 * @since 5.1.0
			 *
			 * @param array                               $attendee_data List of attendee data to be saved.
			 * @param Tribe__Tickets__Attendee_Repository $repository    The current repository object.
			 */
			do_action( "tribe_tickets_attendee_repository_update_attendee_after_update_{$this->key_name}", $attendee_data, $this );
		}

		// Maybe send the attendee email.
		$this->maybe_send_attendee_email( $attendee_data['attendee_id'], $attendee_data );

		$this->maybe_handle_checkin( $attendee_data['attendee_id'], $attendee_data );

		// Clear the attendee cache if post_id is provided.
		if ( ! empty( $this->updates['post_id'] ) && $this->attendee_provider ) {
			$this->attendee_provider->clear_attendees_cache( $this->updates['post_id'] );
		}
	}


	/**
	 * Handle check in actions.
	 *
	 * @since 5.5.6
	 *
	 * @param int   $attendee_id   The attendee ID.
	 * @param array $attendee_data List of attendee data that was used for saving.
	 *
	 * @return void
	 */
	public function maybe_handle_checkin( $attendee_id, $attendee_data ): void {
		if ( ! isset( $attendee_data['check_in'] ) ) {
			return;
		}

		if ( $attendee_data['check_in'] ) {
			$this->attendee_provider->checkin( $attendee_id );
		} else {
			$this->attendee_provider->uncheckin( $attendee_id );
		}
	}

	/**
	 * Create an order for an attendee.
	 *
	 * @since 5.1.0
	 *
	 * @param array                                  $attendee_data List of attendee data to reference.
	 * @param null|int|Tribe__Tickets__Ticket_Object $ticket        The ticket object, ticket ID, or null if not
	 *                                                              relying on it.
	 *
	 * @return int|string|false The order ID or false if not created.
	 */
	public function create_order_for_attendee( $attendee_data, $ticket = null ) {
		// Bail if we already have an attendee or order.
		if ( ! empty( $attendee_data['attendee_id'] ) || ! empty( $attendee_data['order_id'] ) ) {
			return false;
		}

		// Attempt to generate a new order.
		$orders = tribe_tickets_orders( $this->key_name );

		// Bail if provider-specific order repository not found.
		if ( empty( $orders->key_name ) ) {
			return false;
		}

		$tickets = Arr::get( $attendee_data, 'tickets', [] );

		if ( empty( $tickets ) ) {
			$ticket_id = $ticket;

			if ( is_object( $ticket ) ) {
				// Detect ticket ID from the object.
				$ticket_id = $ticket->ID;
			} elseif ( empty( $ticket ) && isset( $attendee_data['ticket_id'] ) ) {
				// Detect the ticket ID from the attendee data.
				$ticket_id = $attendee_data['ticket_id'];
			}

			// Bail if no valid ticket ID.
			if ( $ticket_id < 1 ) {
				return false;
			}

			$tickets = [
				[
					'id'       => $ticket_id,
					'quantity' => 1,
				],
			];
		}

		$order_args = [
			'full_name'    => Arr::get( $attendee_data, 'full_name' ),
			'email'        => Arr::get( $attendee_data, 'email' ),
			'user_id'      => Arr::get( $attendee_data, 'user_id' ),
			'order_status' => Arr::get( $attendee_data, 'attendee_status' ),
			'tickets'      => $tickets,
		];

		/**
		 * Allow filtering the order data being used to create an order for the attendee.
		 *
		 * @since 5.1.0
		 *
		 * @param array                         $order_args    List of order data to be saved.
		 * @param array                         $attendee_data List of additional attendee data.
		 * @param Tribe__Tickets__Ticket_Object $ticket        The ticket object or null if not relying on it.
		 */
		$order_args = apply_filters( 'tribe_tickets_attendee_repository_create_order_for_attendee_order_args', $order_args, $attendee_data, $ticket );

		// Check if order creation is disabled.
		if ( empty( $order_args ) ) {
			return false;
		}

		try {
			$order = $orders->create_order_for_ticket( $order_args );
		} catch ( Tribe__Repository__Usage_Error $exception ) {
			return false;
		}

		return $order;
	}

	/**
	 * Maybe send the attendee email for an attendee.
	 *
	 * @since 5.1.0
	 *
	 * @param int   $attendee_id   The attendee ID.
	 * @param array $attendee_data List of attendee data that was used for saving.
	 */
	protected function maybe_send_attendee_email( $attendee_id, $attendee_data ) {
		$send_ticket_email      = (bool) Arr::get( $attendee_data, 'send_ticket_email', false );
		$send_ticket_email_args = (array) Arr::get( $attendee_data, 'send_ticket_email_args', [] );

		// Check if we need to send the ticket email.
		if ( ! $send_ticket_email ) {
			return;
		}

		// Check if we have an attendee provider object set.
		if ( ! $this->attendee_provider ) {
			return;
		}

		$attendee_tickets = [
			$attendee_id,
		];

		$this->attendee_provider->send_tickets_email_for_attendees( $attendee_tickets, $send_ticket_email_args );
	}

	/**
	 * Overrides the base method to correctly handle the `order_by` clauses before.
	 *
	 * The Event repository handles ordering with some non trivial logic and some query filtering.
	 * To avoid the "stacking" of `orderby` clauses and filters the query filters are added at the very last moment,
	 * right before building the query.
	 *
	 * @since 5.5.0
	 *
	 * @return WP_Query The built query object.
	 */
	protected function build_query_internally() {
		$order_by = Arr::get_in_any( [ $this->query_args, $this->default_args ], 'orderby' );
		unset( $this->query_args['orderby'], $this->default_args['order_by'] );

		$this->handle_order_by( $order_by );

		return parent::build_query_internally();
	}

	/**
	 * Handles the `order_by` clauses for events
	 *
	 * @since 5.5.0
	 *
	 * @param string $order_by The key used to order events; e.g. `event_date` to order events by start date.
	 */
	public function handle_order_by( $order_by ) {
		$check_orderby = $order_by;

		if ( ! is_array( $check_orderby ) ) {
			$check_orderby = explode( ' ', $check_orderby );
		}

		$after = false;
		$loop  = 0;

		foreach ( $check_orderby as $key => $value ) {
			$order_by      = is_numeric( $key ) ? $value : $key;
			$default_order = Arr::get_in_any( [ $this->query_args, $this->default_args ], 'order', 'ASC' );
			$order         = is_numeric( $key ) ? $default_order : $value;

			// Let the first applied ORDER BY clause override the existing ones, then stack the ORDER BY clauses.
			$override = $loop === 0;

			switch ( $order_by ) {
				case 'full_name':
					$this->order_by_full_name( $order, $after, $override );
					break;
				case 'security_code':
					$this->order_by_security_code( $order, $after, $override );
					break;
				case 'check_in':
					$this->order_by_check_in( $order, $after, $override );
					break;
				case 'rsvp_status':
					$this->order_by_rsvp_status( $order, $after, $override );
					break;
				case '__none':
					unset( $this->query_args['orderby'] );
					unset( $this->query_args['order'] );
					break;
				default:
					$after = $after || 1 === $loop;
					if ( empty( $this->query_args['orderby'] ) ) {
						// In some versions of WP, [ $order_by, $order ] doesn't work as expected. Using explict value setting instead.
						$this->query_args['orderby'] = $order_by;
						$this->query_args['order']   = $order;
					} else {
						$add = [ $order_by => $order ];
						// Make sure all `orderby` clauses have the shape `<orderby> => <order>`.
						$normalized = [];

						if ( ! is_array( $this->query_args['orderby'] ) ) {
							$this->query_args['orderby'] = [
								$this->query_args['orderby'] => $this->query_args['order']
							];
						}

						foreach ( $this->query_args['orderby'] as $k => $v ) {
							$the_order_by                = is_numeric( $k ) ? $v : $k;
							$the_order                   = is_numeric( $k ) ? $default_order : $v;
							$normalized[ $the_order_by ] = $the_order;
						}
						$this->query_args['orderby'] = $normalized;
						$this->query_args['orderby'] = array_merge( $this->query_args['orderby'], $add );
					}
			}
		}
	}

	/**
	 * Sets up the query filters to order attendees by the full name meta.
	 *
	 * @since 5.5.2
	 *
	 * @param string $order      The order direction, either `ASC` or `DESC`; defaults to `null` to use the order
	 *                           specified in the current query or default arguments.
	 * @param bool   $after      Whether to append the duration ORDER BY clause to the existing clauses or not;
	 *                           defaults to `false` to prepend the duration clause to the existing ORDER BY
	 *                           clauses.
	 * @param bool   $override   Whether to override existing ORDER BY clauses with this one or not; default to
	 *                           `true` to override existing ORDER BY clauses.
	 */
	protected function order_by_full_name( $order = null, $after = false, $override = true ) {
		global $wpdb;

		$meta_alias     = 'full_name';
		$meta_keys_in   = $this->prepare_interval( $this->holder_name_keys() );
		$postmeta_table = "orderby_{$meta_alias}_meta";
		$filter_id      = 'order_by_full_name';

		$this->filter_query->join(
			"
			LEFT JOIN {$wpdb->postmeta} AS {$postmeta_table}
				ON (
					{$postmeta_table}.post_id = {$wpdb->posts}.ID
					AND {$postmeta_table}.meta_key IN {$meta_keys_in}
				)
			",
			$filter_id,
			true
		);

		$order = $this->get_query_order_type( $order );

		$this->filter_query->orderby( [ $meta_alias => $order ], $filter_id, true, $after );
		$this->filter_query->fields( "{$postmeta_table}.meta_value AS {$meta_alias}", $filter_id, $override );
	}

	/**
	 * Sets up the query filters to order attendees by the security code meta.
	 *
	 * @since 5.5.0
	 *
	 * @param string $order      The order direction, either `ASC` or `DESC`; defaults to `null` to use the order
	 *                           specified in the current query or default arguments.
	 * @param bool   $after      Whether to append the duration ORDER BY clause to the existing clauses or not;
	 *                           defaults to `false` to prepend the duration clause to the existing ORDER BY
	 *                           clauses.
	 * @param bool   $override   Whether to override existing ORDER BY clauses with this one or not; default to
	 *                           `true` to override existing ORDER BY clauses.
	 */
	protected function order_by_security_code( $order = null, $after = false, $override = true ) {
		global $wpdb;

		$meta_alias     = 'security_code';
		$meta_keys_in   = $this->prepare_interval( $this->security_code_keys() );
		$postmeta_table = "orderby_{$meta_alias}_meta";
		$filter_id      = 'order_by_security_code';

		$this->filter_query->join(
			"
			LEFT JOIN {$wpdb->postmeta} AS {$postmeta_table}
				ON (
					{$postmeta_table}.post_id = {$wpdb->posts}.ID
					AND {$postmeta_table}.meta_key IN {$meta_keys_in}
				)
			"
			,
			$filter_id,
			true
		);

		$order = $this->get_query_order_type( $order );

		$this->filter_query->orderby( [ $meta_alias => $order ], $filter_id, true, $after );
		$this->filter_query->fields( "{$postmeta_table}.meta_value AS {$meta_alias}", $filter_id, $override );
	}

	/**
	 * Sets up the query filters to order attendees by the check-in status.
	 *
	 * @since 5.5.0
	 *
	 * @param string $order      The order direction, either `ASC` or `DESC`; defaults to `null` to use the order
	 *                           specified in the current query or default arguments.
	 * @param bool   $after      Whether to append the duration ORDER BY clause to the existing clauses or not;
	 *                           defaults to `false` to prepend the duration clause to the existing ORDER BY
	 *                           clauses.
	 * @param bool   $override   Whether to override existing ORDER BY clauses with this one or not; default to
	 *                           `true` to override existing ORDER BY clauses.
	 */
	protected function order_by_check_in( $order = null, $after = false, $override = true ) {
		global $wpdb;

		$meta_alias   = 'check_in';
		$meta_keys_in = $this->prepare_interval( $this->checked_in_keys() );

		$postmeta_table = "orderby_{$meta_alias}_meta";
		$filter_id      = "order_by_{$meta_alias}";

		$this->filter_query->join(
			"
			LEFT JOIN {$wpdb->postmeta} AS {$postmeta_table}
				ON (
					{$postmeta_table}.post_id = {$wpdb->posts}.ID
					AND {$postmeta_table}.meta_key IN {$meta_keys_in}
				)
			"
			,
			$filter_id,
			true
		);

		$order = $this->get_query_order_type( $order );

		$this->filter_query->orderby( [ $meta_alias => $order ], $filter_id, true, $after );
		$this->filter_query->fields( "{$postmeta_table}.meta_value AS {$meta_alias}", $filter_id, $override );
	}

	/**
	 * Sets up the query filters to order attendees by the order status.
	 *
	 * @since 5.5.0
	 *
	 * @param string $order      The order direction, either `ASC` or `DESC`; defaults to `null` to use the order
	 *                           specified in the current query or default arguments.
	 * @param bool   $after      Whether to append the duration ORDER BY clause to the existing clauses or not;
	 *                           defaults to `false` to prepend the duration clause to the existing ORDER BY
	 *                           clauses.
	 * @param bool   $override   Whether to override existing ORDER BY clauses with this one or not; default to
	 *                           `true` to override existing ORDER BY clauses.
	 */
	protected function order_by_rsvp_status( $order = null, $after = false, $override = true ) {
		global $wpdb;

		$meta_alias = 'rsvp_status';
		$meta_key   = Tribe__Tickets__RSVP::ATTENDEE_RSVP_KEY;

		$postmeta_table = "orderby_{$meta_alias}";
		$filter_id      = "order_by_{$meta_alias}";

		$this->filter_query->join(
			$wpdb->prepare(
				"
				LEFT JOIN {$wpdb->postmeta} AS {$postmeta_table}
					ON (
						{$postmeta_table}.post_id = {$wpdb->posts}.ID
						AND {$postmeta_table}.meta_key = %s
					)
				",
				$meta_key
			),
			$filter_id,
			true
		);

		$order = $this->get_query_order_type( $order );

		$this->filter_query->orderby( [ $meta_alias => $order ], $filter_id, $override, $after );
		$this->filter_query->fields( "{$postmeta_table}.meta_value AS {$meta_alias}", $filter_id, $override );
	}

	/**
	 * Get the order param for the current orderby clause.
	 *
	 * @since 5.5.0
	 *
	 * @param string|null $order order type value either 'ASC' or 'DESC'.
	 *
	 * @return string
	 */
	protected function get_query_order_type( $order = null ) {
		return $order === null
			? Arr::get_in_any( [ $this->query_args, $this->default_args ], 'order', 'ASC' )
			: $order;
	}

	/**
	 * Sets up the query filters to fetch Attendees by post they are attached to.
	 *
	 * @since 5.8.0
	 *
	 * @param int|array<int> $post_id Either a single post ID or an array of post IDs.
	 *
	 * @return void Query filters are set up to fetch Attendees by post they are attached to.
	 */
	public function filter_by_event( $post_id ): void {
		$post_ids = (array) $post_id;

		/**
		 * Filter the post IDs to be used when fetching Attendees by the related post.
		 *
		 * @since 5.8.0
		 *
		 * @param array<int> $post_ids The post IDs to be used when fetching Attendees by the related post.
		 */
		$post_ids = apply_filters( 'tec_tickets_attendees_filter_by_event', $post_ids, $this );

		$this->by( 'meta_in', $this->attendee_to_event_keys(), $post_ids );
	}

	/**
	 * Sets up the query filters to fetch Attendees not related to a post.
	 *
	 * @since 5.8.0
	 *
	 * @param int|array<int> $post_id Either a single post ID or an array of post IDs.
	 *
	 * @return void Query filters are set up to fetch Attendees not related to a post.
	 */
	public function filter_by_event_not_in( $post_id ): void {
		$post_ids = (array) $post_id;

		/**
		 * Filter the post IDs to be used when fetching Attendees not related to a post.
		 *
		 * @since 5.8.0
		 *
		 * @param array<int> $post_ids The post IDs to be used when fetching Attendees by the related post.
		 *
		 */
		$post_ids = apply_filters( 'tec_tickets_attendees_filter_by_event_not_in', $post_ids, $this );

		$this->by( 'meta_not_in', $this->attendee_to_event_keys(), $post_ids );
	}

	private function filter_by_ticket_type_operator( string $operator, $ticket_type ): void {
		$ticket_types = (array) $ticket_type;

		if ( empty( $ticket_types ) ) {
			// No ticket types means no Attendee of any kind will match.
			$this->void_query( true );

			return;
		}

		global $wpdb;
		$alias = 'attendee_by_ticket_type_' . substr( md5( microtime(), ), - 5 );
		$attendee_to_ticket_keys          = $this->attendee_to_ticket_keys();
		$prepared_attendee_to_ticket_keys = $wpdb->prepare(
			implode( ',', array_fill( 0, count( $attendee_to_ticket_keys ), '%s' ) ),
			$attendee_to_ticket_keys
		);

		// Join on the meta that holds the Attendee > Ticket relationship.
		$join = "LEFT JOIN {$wpdb->postmeta} {$alias} " .
		        "ON {$wpdb->posts}.ID = {$alias}.post_id " .
		        "AND {$alias}.meta_key IN ({$prepared_attendee_to_ticket_keys})";

		$ticket_post_types          = tribe_tickets()->ticket_types();
		$prepared_ticket_post_types = $wpdb->prepare(
			implode( ',', array_fill( 0, count( $ticket_post_types ), '%s' ) ),
			$ticket_post_types
		);
		$prepared_ticket_types             = $wpdb->prepare(
			implode( ',', array_fill( 0, count( $ticket_types ), '%s' ) ),
			$ticket_types
		);
		/*
		 * The value of the meta key that relates Attendees > Tickets will hold a Ticket ID.
		 * The Ticket IDs should be among those of a specific Ticket type, the `_type` meta key on
		 * the Ticket.
		 * The sub-query is used to avoid fetching from the database an unbound quantity of Ticket IDs
		 * to use them to filter the meta values.
		 */
		$where = "{$alias}.meta_value IN (
			SELECT tickets.ID FROM {$wpdb->posts} tickets
			LEFT JOIN {$wpdb->postmeta} type ON type.post_id = tickets.ID AND type.meta_key = '_type'
			WHERE tickets.post_type IN ({$prepared_ticket_post_types})
			AND COALESCE(type.meta_value, 'default') {$operator} ({$prepared_ticket_types})
		)";

		$this->join_clause( $join );
		$this->where_clause( $where );
	}

	/**
	 * Filters the Attendees by keeping only the ones for Tickets of a specific type.
	 *
	 * @since 5.8.2
	 *
	 * @param string|string[] $ticket_type The type of Ticket to keep Attendees for.
	 *
	 * @return void
	 */
	public function filter_by_ticket_type( $ticket_type ): void {
		$this->filter_by_ticket_type_operator( 'IN', $ticket_type );
	}

	/**
	 * Filters the Attendees by keeping only the ones for Tickets that are not of a specific type.
	 *
	 * @since 5.8.2
	 *
	 * @param string|string[] $ticket_type The type of Ticket to exclude Attendees for.
	 *
	 * @return void
	 */
	public function filter_by_ticket_type_not_in( $ticket_type ): void {
		$this->filter_by_ticket_type_operator( 'NOT IN', $ticket_type );
	}
}
