<?php
// don't load directly
if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

class Tribe__Tickets__Tickets_View {

	/**
	 * Get (and instantiate, if necessary) the instance of the class.
	 *
	 * @static
	 * @return self
	 */
	public static function instance() {
		return tribe( 'tickets.tickets-view' );
	}

	/**
	 * Hook the necessary filters and Actions!
	 *
	 * @static
	 * @return self
	 */
	public static function hook() {
		$myself = self::instance();

		add_action( 'template_redirect', [ $myself, 'authorization_redirect' ] );
		add_action( 'template_redirect', [ $myself, 'update_tickets' ] );

		// Generate Non TEC Permalink.
		add_action( 'generate_rewrite_rules', [ $myself, 'add_non_event_permalinks' ] );
		add_filter( 'query_vars', [ $myself, 'add_query_vars' ] );
		add_action( 'parse_request', [ $myself, 'prevent_page_redirect' ] );
		add_filter( 'the_content', [ $myself, 'intercept_content' ] );
		add_action( 'parse_request', [ $myself, 'maybe_regenerate_rewrite_rules' ] );
		add_filter( 'tribe_events_views_v2_bootstrap_should_display_single', [ $myself, 'intercept_views_v2_single_display' ], 15, 4 );

		// Prevent canonical redirect from stripping tribe-edit-orders parameter.
		add_filter( 'redirect_canonical', [ $myself, 'preserve_tickets_parameter_in_canonical_redirect' ] );

		// Handle tickets URLs consistently.
		add_filter( 'request', [ $myself, 'handle_tickets_request' ] );

		// Only Applies this to TEC users.
		if ( class_exists( 'Tribe__Events__Rewrite' ) ) {
			add_action( 'tribe_events_pre_rewrite', [ $myself, 'add_permalink' ] );
			add_filter( 'tribe_events_rewrite_base_slugs', [ $myself, 'add_rewrite_base_slug' ] );
		}

		// Intercept Template file for Tickets.
		add_action( 'pre_get_posts', [ $myself, 'modify_ticket_display_query' ] );
		add_filter( 'tribe_events_template_single-event.php', [ $myself, 'intercept_template' ], 20 );

		// We will inject on the Priority 4, to be happen before RSVP.
		add_action( 'tribe_events_single_event_after_the_meta', [ $myself, 'inject_link_template' ], 4 );
		add_filter( 'the_content', [ $myself, 'inject_link_template_the_content' ], 9 );

		return $myself;
	}

	/**
	 * By default WordPress has a nasty if query_var['p'] is a page then redirect to the page,
	 * so we will change the variables accordingly.
	 *
	 * @param WP_Query $query The current Query.
	 * @return void
	 */
	public function prevent_page_redirect( $query ) {
		$is_correct_page = isset( $query->query_vars['tribe-edit-orders'] ) && $query->query_vars['tribe-edit-orders'];

		if ( ! $is_correct_page || ! $this->is_edit_page() ) {
			return;
		}

		// This has no Performance problems, since get_post uses caching and we use this method later on.
		$post = isset( $query->query_vars['p'] ) ? get_post( absint( $query->query_vars['p'] ) ) : 0;
		if ( ! $post ) {
			return;
		}

		if ( ! tribe_tickets_post_type_enabled( $post->post_type ) ) {
			return;
		}

		$query->query_vars['post_type'] = $post->post_type;

		if ( 'page' === $post->post_type ) {
			// Set `page_id` for faster query.
			$query->query_vars['page_id'] = $post->ID;
		}

	}

	/**
	 * Tries to Flush the Rewrite rules.
	 *
	 * @return void
	 */
	public function maybe_regenerate_rewrite_rules() {
		// if they don't have any rewrite rules, do nothing
		// Don't try to run stuff for non-logged users (too time consuming).
		if ( ! is_array( $GLOBALS['wp_rewrite']->rules ) || ! is_user_logged_in() ) {
			return;
		}

		$rules = $this->rewrite_rules_array();

		$diff     = array_diff( $rules, $GLOBALS['wp_rewrite']->rules );
		$key_diff = array_diff_assoc( $rules, $GLOBALS['wp_rewrite']->rules );

		if ( empty( $diff ) && empty( $key_diff ) ) {
			return;
		}

		flush_rewrite_rules();
	}

	/**
	 * Gets the List of Rewrite rules we are using here.
	 *
	 * @since 5.8.2 Added filter to allow users to add additional rewrite rules for the My Tickets page.
	 *
	 * @return array<string>
	 */
	public function rewrite_rules_array() {
		$bases = $this->add_rewrite_base_slug();

		$rules = [
			sanitize_title_with_dashes( $bases['tickets'][0] ) . '/([0-9]{1,})/?' => 'index.php?p=$matches[1]&tribe-edit-orders=1',
		];

		/**
		 * Filter the rewrite rules for the My Tickets page.
		 *
		 * @param array<string> $rules The rewrite rules for the My Tickets page.
		 */
		return apply_filters( 'tec_tickets_my_tickets_page_rewrite_rules', $rules );
	}

	/**
	 * For non events the links will be a little bit weird, but it's the safest way.
	 *
	 * @param WP_Rewrite $wp_rewrite
	 */
	public function add_non_event_permalinks( WP_Rewrite $wp_rewrite  ) {
		$wp_rewrite->rules = $this->rewrite_rules_array() + $wp_rewrite->rules;
	}

	/**
	 * Register a new public (URL query parameters can use it) Query Var to allow tickets editing.
	 *
	 * @see \WP::parse_request()
	 *
	 * @param array $vars
	 *
	 * @return array
	 */
	public function add_query_vars( $vars ) {
		$vars[] = 'tribe-edit-orders';
		return $vars;
	}


	/**
	 * Sort Attendee by Order Status to Process Not Going First.
	 *
	 * @since 4.7.1
	 *
	 * @param array $a An array of ticket id and status.
	 * @param array $b An array of ticket id and status.
	 *
	 * @return int
	 */
	public function sort_attendees( $a, $b ) {
		return strcmp( $a['order_status'], $b['order_status'] );
	}

	/**
	 * Update the RSVP and Tickets values for each Attendee.
	 *
	 * @since 5.8.2 Removed optional param from get_tickets_page_url call.
	 */
	public function update_tickets() {
		$is_correct_page = $this->is_edit_page();

		// Now fetch the display and check it
		if (
			'tickets' !== get_query_var( 'eventDisplay', false )
			&& ! $is_correct_page
		) {
			return;
		}

		if (
			empty( $_POST['process-tickets'] )
			|| (
				empty( $_POST['tribe_tickets']['attendees'] )
				&& empty( $_POST['attendee'] )
				&& empty( $_POST['tribe-tickets-meta'] )
			)
		) {
			return;
		}

		$post_id = get_the_ID();

		$attendees = [];

		if ( isset( $_POST['tribe_tickets']['attendees'] ) ) {
			$attendees = $_POST['tribe_tickets']['attendees'];
		} elseif ( isset( $_POST['attendee'] ) ) {
			$attendees = $_POST['attendee'];
		}

		// Sort list to handle all not attending first.
		$attendees = wp_list_sort( $attendees, 'order_status', 'ASC', true );

		foreach ( $attendees as $attendee_id => $attendee_data ) {
			/**
			 * Allow Commerce providers to process updates for each attendee from the My Tickets page.
			 *
			 * @param array $attendee_data Information that we are trying to save.
			 * @param int   $attendee_id   The attendee ID.
			 * @param int   $post_id       The event/post ID.
			 */
			do_action( 'event_tickets_attendee_update', $attendee_data, (int) $attendee_id, $post_id );
		}

		/**
		 * Allow functionality to be hooked into after all of the attendees have been updated from the My Tickets page.
		 *
		 * @since 5.1.0 Added the $attendees value to the action for further integration.
		 *
		 * @param int   $post_id   The event/post ID.
		 * @param array $attendees List of attendees and their data that was saved.
		 */
		do_action( 'event_tickets_after_attendees_update', $post_id, $attendees );

		// After editing the values, we update the transient.
		Tribe__Post_Transient::instance()->delete( $post_id, Tribe__Tickets__Tickets::ATTENDEES_CACHE );

		$url = $this->get_tickets_page_url( $post_id );
		$url = add_query_arg( 'tribe_updated', 1, $url );
		wp_safe_redirect( esc_url_raw( $url ) );
		exit;
	}

	/**
	 * Helper function to generate the Link to the tickets page of an event.
	 *
	 * @since 4.7.1
	 * @since 5.8.2 Removed the $is_event_page param.
	 * @since 5.25.0 Added support for pretty permalinks.
	 *
	 * @param int $event_id event_id The Event ID we're checking.
	 *
	 * @return string      The URL to the tickets page.
	 */
	public function get_tickets_page_url( int $event_id ): string {
		$event_url = untrailingslashit( get_permalink( $event_id ) );

		// Bail early if there is no event URL.
		if ( empty( $event_url ) ) {
			return '';
		}

		$has_plain_permalink = '' === get_option( 'permalink_structure' );
		$post_type           = get_post_type( $event_id );
		$is_event_page       = 'tribe_events' === $post_type || 'tribe_event_series' === $post_type;

		// Handle event post types first and return early.
		if ( $is_event_page ) {
			return $has_plain_permalink
				? add_query_arg( 'eventDisplay', 'tickets', $event_url )
				: trailingslashit( $event_url ) . 'tickets';
		}

		// Handle plain permalinks for non-event posts.
		if ( $has_plain_permalink ) {
			return add_query_arg( 'tribe-edit-orders', 1, ( $event_url ) );
		}

		// Handle pretty permalinks for non-event posts.
		$bases        = $this->add_rewrite_base_slug();
		$tickets_slug = $bases['tickets'][0] ?? 'tickets';
		
		return home_url( untrailingslashit( "{$tickets_slug}/{$event_id}" ) );
	}

	/**
	 * Makes sure only logged users can See the Tickets page.
	 *
	 * @return void
	 */
	public function authorization_redirect() {
		/**
		 * @todo Remove this after we implement the Rewrites in Common
		 */
		$is_event_query = ! empty( $GLOBALS['wp_query']->tribe_is_event_query );

		// When it's not Events Query and we have TEC active we dont care
		if ( class_exists( 'Tribe__Events__Main' ) && ! $is_event_query ) {
			return;
		}

		// If we got here and it's a 404 + single
		if ( is_single() && is_404() ) {
			return;
		}

		// Now fetch the display and check it
		if ( 'tickets' !== get_query_var( 'eventDisplay', false ) && ! $this->is_edit_page() ) {
			return;
		}

		// Only goes to the Redirect if user is not logged in
		if ( is_user_logged_in() ) {
			return;
		}

		// Loop back to the Event, this page is only for Logged users
		wp_redirect( get_permalink() );
		exit;
	}

	/**
	 * To allow `tickets` to be translatable we need to add it as a base.
	 *
	 * @param array $bases The translatable bases.
	 * @return array
	 */
	public function add_rewrite_base_slug( $bases = [] ) {
		/**
		 * Allows users to filter and change the base for the order page
		 *
		 * @param string $slug
		 * @param array  $bases
		 */
		$bases['tickets'] = (array) apply_filters( 'event_tickets_rewrite_slug_orders_page', 'tickets', $bases );

		return $bases;
	}


	/**
	 * Checks if this is the ticket page based on the current query var.
	 *
	 * This only works after parse_query has run.
	 *
	 * @return bool
	 */
	public function is_edit_page() {
		return get_query_var( 'tribe-edit-orders', false )
		       || 'tickets' === get_query_var( 'eventDisplay', false );
	}

	/**
	 * Adds the Permalink for the tickets end point.
	 *
	 * @param Tribe__Events__Rewrite $rewrite
	 */
	public function add_permalink( Tribe__Events__Rewrite $rewrite ) {

		// Adds the 'tickets' endpoint for single event pages.
		$rewrite->single(
			[ '{{ tickets }}' ],
			[
				Tribe__Events__Main::POSTTYPE => '%1',
				'post_type' => Tribe__Events__Main::POSTTYPE,
				'eventDisplay' => 'tickets',
			]
		);

		// Adds the `tickets` endpoint for recurring events
		$rewrite->single(
			[ '(\d{4}-\d{2}-\d{2})', '{{ tickets }}' ],
			[
				Tribe__Events__Main::POSTTYPE => '%1',
				'eventDate' => '%2',
				'post_type' => Tribe__Events__Main::POSTTYPE,
				'eventDisplay' => 'tickets',
			]
		);

	}

	/**
	 * Filter to make sure Tickets eventDisplay properly displays the Tickets page.
	 *
	 * @since 4.11.2
	 *
	 * @param bool   $should_display_single If we should display single or not.
	 * @param string $view_slug             Which view slug we are working with.
	 *
	 * @return bool
	 */
	public function intercept_views_v2_single_display( $should_display_single, $view_slug ) {
		if ( 'tickets' === $view_slug ) {
			return true;
		}

		return $should_display_single;
	}

	/**
	 * Intercepts the_content from the posts to include the orders structure.
	 *
	 * @since 4.11.2 Avoid running when it shouldn't by bailing if not in main query loop on a single post.
	 * @since 5.25.0 Added filter to preserve tribe-edit-orders parameter in canonical redirect.
	 * 
	 * @param string $content Normally the_content of a post.
	 *
	 * @return string
	 */
	public function intercept_content( $content = '' ) {
		// Now fetch the display and check it
		$display = get_query_var( 'eventDisplay', false );

		// Prevents firing more than it needs to outside of the loop.
		if (
			! is_singular( Tribe__Tickets__Main::instance()->post_types() )
			|| ! in_the_loop()
			|| ! is_main_query()
			|| (
				'tickets' !== $display
				&& ! $this->is_edit_page()
			)
		) {
			return $content;
		}

		tribe_asset_enqueue_group( 'tribe-tickets-page-assets' );

		ob_start();

		include Tribe__Tickets__Templates::get_template_hierarchy( 'tickets/orders.php' );

		$content = ob_get_clean();

		return $content;
	}

	/**
	 * Modify the front end ticket list display for it to always display
	 * even when Hide From Event Listings is checked for an event.
	 *
	 * @since 4.7.3
	 *
	 * @param WP_Query $query Query object.
	 *
	 */
	public function modify_ticket_display_query( $query ) {
		if ( empty( $query->tribe_is_event_query ) ) {
			return;
		}

		if ( 'tickets' !== get_query_var( 'eventDisplay', false ) ) {
			return;
		}

		$query->set( 'post__not_in', '' );

		// Do not attempt to filter the query for this custom view.
		$query->set( 'tribe_suppress_query_filters', true );
	}

	/**
	 * We need to intercept the template loading and load the correct file.
	 *
	 * @param string $old_file Non important variable with the previous path.
	 *
	 * @return string          The correct File path for the tickets endpoint.
	 */
	public function intercept_template( $old_file ) {
		global $wp_query;

		/**
		 * @todo Remove this after we implement the Rewrites in Common
		 */
		$is_event_query = ! empty( $wp_query->tribe_is_event_query );

		// When it's not our query we don't care
		if ( ! $is_event_query ) {
			return $old_file;
		}

		// If we got here and it's a 404 + single
		if ( is_single() && is_404() ) {
			return $old_file;
		}

		// Now fetch the display and check it
		$display = get_query_var( 'eventDisplay', false );

		if ( 'tickets' !== $display && ! $this->is_edit_page() ) {
			return $old_file;
		}

		tribe_asset_enqueue_group( 'tribe-tickets-page-assets' );

		// Fetch the correct file using the Tickets Hierarchy
		$file = Tribe__Tickets__Templates::get_template_hierarchy( 'tickets/orders.php' );

		return $file;
	}

	/**
	 * Injects the Link to The front-end Tickets page normally
	 * at `tribe_events_single_event_after_the_meta`.
	 *
	 * @return void
	 */
	public function inject_link_template() {
		/**
		 * A flag we can set via filter, e.g. at the end of this method, to ensure this template only shows once.
		 *
		 * @since 4.5.6
		 *
		 * @param boolean $already_rendered Whether the order link template has already been rendered.
		 */
		$already_rendered = apply_filters( 'tribe_tickets_order_link_template_already_rendered', false );

		if ( $already_rendered ) {
			return;
		}

		$event_id = get_the_ID();
		$user_id  = get_current_user_id();

		if ( ! $this->has_rsvp_attendees( $event_id, $user_id ) && ! $this->has_ticket_attendees( $event_id, $user_id ) ) {
			return;
		}

		if ( $this->is_edit_page() ) {
			return;
		}

		if ( ! tribe_tickets_post_type_enabled( get_post_type() ) ) {
			return;
		}

		$file = Tribe__Tickets__Templates::get_template_hierarchy( 'tickets/orders-link.php' );

		/**
		 * @since 4.10.8 Attempt to load from old location to account for pre-existing theme overrides. If not found,
		 *            go through the motions with the new location.
		 */
		if ( empty( $file ) ) {
			$file = Tribe__Tickets__Templates::get_template_hierarchy( 'tickets/view-link.php' );
		}

		include $file;

		add_filter( 'tribe_tickets_order_link_template_already_rendered', '__return_true' );
	}

	/**
	 * Injects the Link to The front-end Tickets page to non Events.
	 *
	 * @param string $content  The content form the post.
	 * @return string $content
	 */
	public function inject_link_template_the_content( $content ) {
		// Prevents firing more then it needs too outside of the loop
		$in_the_loop = isset( $GLOBALS['wp_query']->in_the_loop ) && $GLOBALS['wp_query']->in_the_loop;

		$post_id = get_the_ID();
		$user_id = get_current_user_id();

		// if the current post type doesn't have tickets enabled for it, bail
		if ( ! tribe_tickets_post_type_enabled( get_post_type( $post_id ) ) ) {
			return $content;
		}

		/**
		 * @todo Remove this after we implement the Rewrites in Common
		 */
		$is_event_query = ! empty( $GLOBALS['wp_query']->tribe_is_event_query );

		// When it's not our query we don't care
		if ( ( class_exists( 'Tribe__Events__Main' ) && $is_event_query ) || ! $in_the_loop ) {
			return $content;
		}

		// If we have this we are already on the tickets page
		if ( $this->is_edit_page() ) {
			return $content;
		}

		if ( ! $this->has_rsvp_attendees( $post_id, $user_id ) && ! $this->has_ticket_attendees( $post_id, $user_id ) ) {
			return $content;
		}

		ob_start();

		$file = Tribe__Tickets__Templates::get_template_hierarchy( 'tickets/orders-link.php' );

		/**
		 * @since 4.10.8 Attempt to load from old location to account for pre-existing theme overrides. If not found,
		 *            go through the motions with the new location.
		 */
		if ( empty( $file ) ) {
			$file = Tribe__Tickets__Templates::get_template_hierarchy( 'tickets/view-link.php' );
		}

		include $file;

		$content .= ob_get_clean();

		add_filter( 'tribe_tickets_order_link_template_already_rendered', '__return_true' );

		return $content;
	}

	/**
	 * Fetches from the Cached attendees list the ones that are relevant for this user and event.
	 * Important to note that this method will return the attendees organized by order id.
	 *
	 * @param int       $event_id      The Event ID we're checking.
	 * @param int|null  $user_id       An Optional User ID.
	 * @param boolean   $include_rsvp  If this should include RSVP, default is false.
	 * @return array                    List of Attendees grouped by order id.
	 */
	public function get_event_attendees_by_order( $event_id, $user_id = null, $include_rsvp = false ) {
		if ( ! $user_id ) {
			$attendees = Tribe__Tickets__Tickets::get_event_attendees( $event_id );
		} else {
			// If we have a user_id then limit by that.
			$args = [
				'by' => [
					'user' => $user_id,
				],
			];

			$attendee_data = Tribe__Tickets__Tickets::get_event_attendees_by_args( $event_id, $args );

			$attendees = $attendee_data['attendees'];
		}

		$orders = [];

		foreach ( $attendees as $key => $attendee ) {
			// Ignore RSVP if we don't tell it specifically
			if ( 'rsvp' === $attendee['provider_slug'] && ! $include_rsvp ) {
				continue;
			}

			$orders[ (int) $attendee['order_id'] ][] = $attendee;
		}

		return $orders;
	}

	/**
	 * Fetches from the Cached attendees list the ones that are relevant for this user and event.
	 * Important to note that this method will return the attendees from RSVP.
	 *
	 * @param int       $event_id     The Event ID we're checking.
	 * @param int|null  $user_id      An Optional User ID.
	 * @return array                   Array with the RSVP attendees.
	 */
	public function get_event_rsvp_attendees( $event_id, $user_id = null ) {
		$attendees = [];

		/** @var Tribe__Tickets__RSVP $rsvp */
		$rsvp = tribe( 'tickets.rsvp' );

		if ( null === $user_id ) {
			return $rsvp->get_attendees_by_id( $event_id );
		}

		return $rsvp->get_attendees_by_user_id( $user_id, $event_id );
	}

	/**
	 * Groups RSVP attendees by purchaser name/email.
	 *
	 * @param int $event_id The Event ID we're checking.
	 * @param int|null $user_id An optional user ID.
	 * @return array Array with the RSVP attendees grouped by purchaser name/email.
	 */
	public function get_event_rsvp_attendees_by_purchaser( $event_id, $user_id = null ) {
		$attendees = $this->get_event_rsvp_attendees( $event_id, $user_id );

		if ( ! $attendees ) {
			return [];
		}

		$attendee_groups = [];
		foreach ( $attendees as $attendee ) {
			$key = $attendee['purchaser_name'] . '::' . $attendee['purchaser_email'];

			if ( ! isset( $attendee_groups[ $key ] ) ) {
				$attendee_groups[ $key ] = [];
			}

			$attendee_groups[ $key ][] = $attendee;
		}

		return $attendee_groups;
	}

	/**
	 * Gets a List of Possible RSVP answers.
	 *
	 * @param string $selected    Allows users to check if an option exists or get it's label.
	 * @param bool   $just_labels Whether just the options labels should be returned.
	 *
	 * @return array|bool An array containing the RSVP states, an array containing the selected
	 *                    option data or `false` if the selected option does not exist.
	 */
	public function get_rsvp_options( $selected = null, $just_labels = true ) {
		/** @var Tribe__Tickets__Status__Manager $status_mgr */
		$status_mgr = tribe( 'tickets.status' );

		$options = $status_mgr->get_status_options( 'rsvp' );

		/**
		 * Allow users to add more RSVP options.
		 *
		 * Additional RSVP options should be specified in the following formats:
		 *
		 *      [
		 *          'slug' => 'Option 1 label',
		 *          'slug' => [ 'label' => 'Option 3 label' ],
		 *          'slug' => [ 'label' => 'Option 2 label', 'decrease_stock_by' => 1 ],
		 *      ]
		 *
		 * The `decrease_stock_by` key can be omitted and will default to `1`.
		 *
		 * @param array $options
		 * @param string $selected
		 */
		$options = apply_filters( 'event_tickets_rsvp_options', $options, $selected );

		$options = array_filter( $options, [ $this, 'has_rsvp_format' ] );
		array_walk( $options, [ $this, 'normalize_rsvp_option' ] );

		// If an option was passed return it's label, but if doesn't exist return false
		if ( null !== $selected ) {
			return isset( $options[ $selected  ] ) ?
                $options[ $selected  ]['label'] : false;
		}

		return $just_labels ?
			array_combine( array_keys( $options ), wp_list_pluck( $options, 'label' ) )
			: $options;
	}

	/**
	 * Check if the RSVP option is a valid one.
	 *
	 * @param string  $option Which rsvp option to check.
	 * @return boolean
	 */
	public function is_valid_rsvp_option( $option ) {
		return in_array( $option, array_keys( $this->get_rsvp_options() ) );
	}

	/**
	 * Counts the amount of RSVP attendees.
	 *
	 * @param int      $event_id The Event ID we're checking.
	 * @param int|null $user_id  An Optional User ID.
	 *
	 * @return int
	 */
	public function count_rsvp_attendees( $event_id, $user_id = null ) {
		if ( ! $user_id && null !== $user_id ) {
			// No attendees for this user.
			return 0;
		}

		/** @var Tribe__Tickets__RSVP $rsvp */
		$rsvp = tribe( 'tickets.rsvp' );

		// Get total attendees count for all users.
		if ( ! $user_id ) {
			return $rsvp->get_attendees_count( $event_id );
		}

		// Get total attendees count for this user.
		return $rsvp->get_attendees_count_by_user( $event_id, $user_id );
	}

	/**
	 * Counts the Amount of Tickets attendees.
	 *
	 * @param int       $event_id     The Event ID we're checking.
	 * @param int|null  $user_id      An Optional User ID.
	 * @return int
	 */
	public function count_ticket_attendees( $event_id, $user_id = null ) {
		if ( ! $user_id && null !== $user_id ) {
			// No attendees for this user.
			return 0;
		}

		$args = [
			'by' => [
				'provider__not_in' => 'rsvp',
				'status'           => 'publish',
			],
		];

		// Get total attendees count for this user.
		if ( $user_id ) {
			$args['by']['user'] = $user_id;
		}

		return Tribe__Tickets__Tickets::get_event_attendees_count( $event_id, $args );
	}

	/**
	 * Verifies if we have RSVP attendees for this user and event.
	 *
	 * @param int       $event_id     The Event ID we're checking.
	 * @param int|null  $user_id      An Optional User ID.
	 * @return int
	 */
	public function has_rsvp_attendees( $event_id, $user_id = null ) {
		$rsvp_orders = $this->count_rsvp_attendees( $event_id, $user_id );
		return ! empty( $rsvp_orders );
	}

	/**
	 * Verifies if we have Tickets attendees for this user and event
	 *
	 * @param int       $event_id     The Event ID we're checking.
	 * @param int|null  $user_id      An Optional User ID.
	 * @return int
	 */
	public function has_ticket_attendees( $event_id, $user_id = null ) {
		$ticket_orders = $this->count_ticket_attendees( $event_id, $user_id );
		return ! empty( $ticket_orders );
	}

	/**
	 * Gets the name(s) of the type(s) of ticket(s) the specified user (optional) has for the specified event.
	 *
	 * @since 4.2
	 * @since 4.10.8 Deprecated the 3rd parameter (whether or not to use 'plurals') in favor of figuring it out per type.
	 *
	 * @param int      $event_id   The Event ID we're checking.
	 * @param int|null $user_id    An optional User ID.
	 * @param null     $deprecated Deprecated argument.
	 *
	 * @return string
	 */
	public function get_description_rsvp_ticket( $event_id, $user_id = null, $deprecated = null ) {
		$descriptions = [];

		$rsvp_count = $this->count_rsvp_attendees( $event_id, $user_id );

		$ticket_count = $this->count_ticket_attendees( $event_id, $user_id );

		if ( 1 === $rsvp_count ) {
			$descriptions[] = tribe_get_rsvp_label_singular( 'tickets_view_description' );
		} elseif ( 1 < $rsvp_count ) {
			$descriptions[] = tribe_get_rsvp_label_plural( 'tickets_view_description' );
		}

		if ( 1 === $ticket_count ) {
			$descriptions[] = tribe_get_ticket_label_singular( 'tickets_view_description' );
		} elseif ( 1 < $ticket_count ) {
			$descriptions[] = tribe_get_ticket_label_plural( 'tickets_view_description' );
		}

		// Just return false if array is empty
		if ( empty( $descriptions ) ) {
			return '';
		}

		return esc_html( implode( _x( ' and ', 'separator if there are both RSVPs and Tickets', 'event-tickets' ), $descriptions ) );
	}

	/**
	 * Creates the HTML for the Select Element for RSVP options.
	 *
	 * @param string $name     The Name of the Field.
	 * @param string $selected The Current selected option.
	 * @param int  $event_id   The Event/Post ID (optional).
	 * @param int  $ticket_id  The Ticket/RSVP ID (optional).
	 * @return void
	 */
	public function render_rsvp_selector( $name, $selected, $event_id = null, $ticket_id = null ) {
		$options = $this->get_rsvp_options();

		?>
		<select class="tribe-answer-select" <?php echo $this->get_restriction_attr( $event_id, $ticket_id ); ?> name="<?php echo esc_attr( $name ); ?>">
		<?php foreach ( $options as $value => $label ): ?>
			<option <?php selected( $selected, $value ); ?> value="<?php echo esc_attr( $value ); ?>"><?php echo esc_html( $label ); ?></option>
		<?php endforeach; ?>
		</select>
		<?php
	}

	/**
	 * Verifies if the Given Event has RSVP restricted.
	 *
	 * @param int  $event_id   The Event/Post ID (optional).
	 * @param int  $ticket_id  The Ticket/RSVP ID (optional).
	 * @param int  $user_id    A User ID (optional).
	 * @return boolean
	 */
	public function is_rsvp_restricted( $event_id = null, $ticket_id = null, $user_id = null ) {
		// By default we always pass the current User
		if ( is_null( $user_id ) ) {
			$user_id = get_current_user_id();
		}

		/**
		 * Allow users to filter if this Event or Ticket has Restricted RSVP
		 *
		 * @param boolean  $restricted Is this Event or Ticket Restricted?
		 * @param int      $event_id   The Event/Post ID (optional)
		 * @param int      $ticket_id  The Ticket/RSVP ID (optional)
		 * @param int      $user_id    An User ID (optional)
		 */
		return apply_filters( 'event_tickets_is_rsvp_restricted', false, $event_id, $ticket_id, $user_id );
	}

	/**
	 * Gets a HTML Attribute for input/select/textarea to be disabled.
	 *
	 * @param int  $event_id   The Event/Post ID (optional).
	 * @param int  $ticket_id  The Ticket/RSVP ID (optional).
	 * @return boolean
	 */
	public function get_restriction_attr( $event_id = null, $ticket_id = null ) {
		$is_disabled = '';

		if ( $this->is_rsvp_restricted( $event_id, $ticket_id ) ) {
			$is_disabled = 'disabled title="' . esc_attr( sprintf( __( 'This %s is no longer active.', 'event-tickets' ), tribe_get_rsvp_label_singular( 'rsvp_restricted_title_text' ) ) ) . '"';
		}

		return $is_disabled;
	}

	/**
	 * Creates the HTML for the status of the  RSVP choice.
	 *
	 * @param string $name     The Name of the Field.
	 * @param string $selected The Current selected option.
	 * @param int  $event_id   The Event/Post ID (optional).
	 * @param int  $ticket_id  The Ticket/RSVP ID (optional).
	 * @return void
	 */
	public function render_rsvp_status( $name, $selected, $event_id = null, $ticket_id = null ) {
		$options = $this->get_rsvp_options();
		echo sprintf( '<span>%s</span>', esc_html( $options[ $selected ] ) );
	}

	/**
	 * Prunes RSVP options that are arrays and are not defining a label.
	 *
	 * @param array|string $option
	 *
	 * @return bool
	 */
	protected function has_rsvp_format( $option ) {
		if ( ! is_array( $option ) ) {
			return true;
		}

		// label is the bare minimum
		if ( ! isset( $option['label'] ) ) {
			return false;
		}

		return empty( $option['decrease_stock_by'] )
		       || (
					is_numeric( $option['decrease_stock_by'] )
		            && intval( $option['decrease_stock_by'] ) == $option['decrease_stock_by']
		            && intval( $option['decrease_stock_by'] ) >= 0
		       );
	}

	/**
	 * Normalizes the RSVP option conforming it to the array format.
	 *
	 * @param array|string $option
	 */
	protected function normalize_rsvp_option( &$option ) {
		$label_only_format = ! is_array( $option );
		if ( $label_only_format ) {
			$option = [ 'label' => $option, 'decrease_stock_by' => 1 ];
		} else {
			$option['decrease_stock_by'] = isset( $option['decrease_stock_by'] ) ? $option['decrease_stock_by'] : 1;
		}
	}

	/**
	 * Gets the block template "out of context" and makes it usable for non-Block Editor views.
	 *
	 * @since 4.11.0
	 * @since 4.12.3 Update usage of get_event_ticket_provider().
	 *
	 * @param WP_Post|int $post The post object or ID.
	 * @param boolean     $echo Whether to echo the output or not.
	 *
	 * @return string The block HTML.
	 */
	public function get_tickets_block( $post, $echo = true ) {
		if ( empty( $post ) ) {
			return '';
		}

		if ( is_numeric( $post ) ) {
			$post = get_post( $post );
		}

		if ( ! $post instanceof WP_Post ) {
			return '';
		}

		// If password protected, do not display content.
		if ( post_password_required() ) {
			return '';
		}

		$post_id = $post->ID;

		$provider = Tribe__Tickets__Tickets::get_event_ticket_provider_object( $post_id );

		// Protect against ticket that exists but is of a type that is not enabled.
		if ( empty( $provider ) ) {
			return '';
		}

		// No need to handle RSVPs here.
		if ( 'Tribe__Tickets__RSVP' === $provider->class_name ) {
			return '';
		}

		/** @var Tribe__Tickets__Editor__Template $template */
		$template = tribe( 'tickets.editor.template' );

		/** @var Tribe__Tickets__Editor__Blocks__Tickets $blocks_tickets */
		$blocks_tickets = tribe( 'tickets.editor.blocks.tickets' );

		/** @var Tribe__Settings_Manager $settings_manager */
		$settings_manager = tribe( 'settings.manager' );

		$threshold = $settings_manager::get_option( 'ticket-display-tickets-left-threshold', null );

		/**
		 * Overwrites the threshold to display "# tickets left".
		 *
		 * @since 4.11.1
		 *
		 * @param int   $threshold Stock threshold to trigger display of "# tickets left"
		 * @param int   $post_id   WP_Post/Event ID.
		 */
		$threshold = absint( apply_filters( 'tribe_display_tickets_block_tickets_left_threshold', $threshold, $post_id ) );

		/**
		 * Allow filtering of the button name for the tickets block.
		 *
		 * @since 4.11.0
		 *
		 * @param string $button_name The button name. Set to cart-button to send to cart on submit, or set to checkout-button to send to checkout on submit.
		 */
		$submit_button_name = apply_filters( 'tribe_tickets_ticket_block_submit', 'cart-button' );

		/**
		 * Show original price on sale.
		 *
		 * @param bool Whether the original price should be shown on sale or not. Default is true.
		 *
		 * @return bool Whether the original price should be shown on sale or not.
		 */
		$show_original_price_on_sale = apply_filters( 'tribe_tickets_show_original_price_on_sale', true );

		// Load assets manually.
		$blocks_tickets->assets();

		$tickets = $provider->get_tickets( $post_id );

		$args = [
			'post_id'                     => $post_id,
			'provider'                    => $provider,
			'provider_id'                 => $provider->class_name,
			'tickets'                     => $tickets,
			'cart_classes'                => [ 'tribe-block', 'tribe-tickets' ], // @todo: deprecate with V1.
			'tickets_on_sale'             => $blocks_tickets->get_tickets_on_sale( $tickets ),
			'has_tickets_on_sale'         => tribe_events_has_tickets_on_sale( $post_id ),
			'is_sale_past'                => $blocks_tickets->get_is_sale_past( $tickets ),
			'is_sale_future'              => $blocks_tickets->get_is_sale_future( $tickets ),
			'currency'                    => tribe( 'tickets.commerce.currency' ),
			'handler'                     => tribe( 'tickets.handler' ),
			'privacy'                     => tribe( 'tickets.privacy' ),
			'threshold'                   => $threshold,
			'must_login'                  => ! is_user_logged_in() && $provider->login_required(),
			'show_original_price_on_sale' => $show_original_price_on_sale,
			'is_mini'                     => null,
			'is_modal'                    => null,
			'submit_button_name'          => $submit_button_name,
			'cart_url'                    => method_exists( $provider, 'get_cart_url' ) ? $provider->get_cart_url() : '',
			'checkout_url'                => method_exists( $provider, 'get_checkout_url' ) ? $provider->get_checkout_url() : '',
			'attendees'                   => null,
			'attendees_total'             => null,
		];


		// Handle Event Tickets logic.
		$hide_attendee_list_optout = \Tribe\Tickets\Events\Attendees_List::is_hidden_on( $post_id );

		/**
		 * Filters whether to hide the attendee list opt-out option.
		 *
		 * @since 5.6.3
		 *
		 * @param bool        $hide_attendee_list_optout Whether to hide the attendee list opt-out option.
		 * @param int|WP_Post $post                      The post object or ID.
		 */
		$hide_attendee_list_optout = apply_filters( 'tec_tickets_hide_attendee_list_optout', $hide_attendee_list_optout, $post_id );

		// If we are not hiding the attendees output, than grab the data.
		if ( ! tribe_is_truthy( $hide_attendee_list_optout ) ) {
			$attendees_list           = tribe( 'tickets.events.attendees-list' );
			$args['attendees']       = $attendees_list->get_attendees_for_post( $post_id );
			$args['attendees_total'] = $attendees_list->get_attendance_counts( $post_id );
		}

		/**
		 * Add the rendering attributes into global context.
		 *
		 * Start with the following for template files loading this global context.
		 * Keep all templates with this starter block of comments updated if these global args update.
		 *
		 * @var Tribe__Tickets__Editor__Template   $this                        [Global] Template object.
		 * @var int                                $post_id                     [Global] The current Post ID to which tickets are attached.
		 * @var Tribe__Tickets__Tickets            $provider                    [Global] The tickets provider class.
		 * @var string                             $provider_id                 [Global] The tickets provider class name.
		 * @var Tribe__Tickets__Ticket_Object[]    $tickets                     [Global] List of tickets.
		 * @var array                              $cart_classes                [Global] CSS classes.
		 * @var Tribe__Tickets__Ticket_Object[]    $tickets_on_sale             [Global] List of tickets on sale.
		 * @var bool                               $has_tickets_on_sale         [Global] True if the event has any tickets on sale.
		 * @var bool                               $is_sale_past                [Global] True if tickets' sale dates are all in the past.
		 * @var bool                               $is_sale_future              [Global] True if no ticket sale dates have started yet.
		 * @var Tribe__Tickets__Commerce__Currency $currency                    [Global] Tribe Currency object.
		 * @var Tribe__Tickets__Tickets_Handler    $handler                     [Global] Tribe Tickets Handler object.
		 * @var Tribe__Tickets__Privacy            $privacy                     [Global] Tribe Tickets Privacy object.
		 * @var int                                $threshold                   [Global] The count at which "number of tickets left" message appears.
		 * @var bool                               $show_original_price_on_sale [Global] Show original price on sale.
		 * @var null|bool                          $is_mini                     [Global] If in "mini cart" context.
		 * @var null|bool                          $is_modal                    [Global] Whether the modal is enabled.
		 * @var string                             $submit_button_name          [Global] The button name for the tickets block.
		 * @var string                             $cart_url                    [Global] Link to Cart (could be empty).
		 * @var string                             $checkout_url                [Global] Link to Checkout (could be empty).
		 * @var array                              $attendees                   [Global]  Array List of public attendees for display.
		 * @var int                                $attendees_total             [Global] Total number of attendees attending the event.
		 */
		$template->add_template_globals( $args );

		// Add local vars to ensure that the data is passed properly within WP_Query Loop.
		$template->set_values( $args, true );

		// Enqueue assets.
		tribe_asset_enqueue_group( 'tribe-tickets-block-assets' );

		if ( tribe_tickets_new_views_is_enabled() ) {
			 ob_start();

			/**
			 * Allow for the addition of content (namely the "Who's Attending?" list) above the ticket form.
			 *
			 * @since 5.5.0
			 * @since 5.8.2 Added the `$post_id` and `$post` parameters.
			 *
			 * @param int     $post_id The ID of the post the tickets block is being rendered for.
			 * @param WP_Post $post    The post object the tickets block is being rendered for.
			 *
			 */
			do_action( 'tribe_tickets_before_front_end_ticket_form', $post_id, $post );

			$before_content = (string) ob_get_clean();
			if ( $echo ) {
				echo $before_content;
				$before_content = '';
			}

			/**
			 * A flag we can set via filter, e.g. at the end of this method, to ensure this template only shows once.
			 *
			 * @since 4.5.6
			 *
			 * @param boolean $already_rendered Whether the order link template has already been rendered.
			 *
			 * @see Tribe__Tickets__Tickets_View::inject_link_template()
			 */
			$already_rendered = apply_filters( 'tribe_tickets_order_link_template_already_rendered', false );

			// Output order links / view link if we haven't already (for RSVPs).
			if ( ! $already_rendered ) {
				$before_content .= $template->template( 'blocks/attendees/order-links', [], $echo );

				if ( empty( $before_content ) ) {
					$before_content = $template->template( 'blocks/attendees/view-link', [], $echo );
				}

				add_filter( 'tribe_tickets_order_link_template_already_rendered', '__return_true' );
			}

			$rendered_content  = $before_content;
			$rendered_content .= $template->template( 'v2/tickets', [], $echo );

			// Only append the attendees section if they did not hide the attendee list.
			if ( ! tribe_is_truthy( $hide_attendee_list_optout ) ) {
				$rendered_content .= $template->template( 'blocks/attendees', [], $echo );
			}
			return $rendered_content;
		}

		return $template->template( 'blocks/tickets', [], $echo );
	}

	/**
	 * Gets the RSVP block template "out of context" and makes it usable for Classic views.
	 *
	 * @since 4.12.3
	 *
	 * @param WP_Post|int $post The post object or ID.
	 * @param boolean     $echo Whether to echo the output or not.
	 * @param int[]       $include_tickets Array of ticket IDs to include, excluding all others.
	 *
	 * @return string The block HTML.
	 */
	public function get_rsvp_block( $post, $echo = true, $include_tickets = [] ) {
		if ( empty( $post ) ) {
			return '';
		}

		if ( is_numeric( $post ) ) {
			$post = get_post( $post );
		}

		if (
			empty( $post )
			|| ! ( $post instanceof WP_Post )
		) {
			return '';
		}

		// If password protected then do not display content.
		if ( post_password_required( $post ) ) {
			return '';
		}

		$post_id = $post->ID;

		/** @var \Tribe__Tickets__Editor__Template $template */
		$template = tribe( 'tickets.editor.template' );

		/** @var \Tribe__Tickets__Editor__Blocks__Rsvp $blocks_rsvp */
		$blocks_rsvp = tribe( 'tickets.editor.blocks.rsvp' );

		/** @var Tribe__Tickets__RSVP $rsvp */
		$rsvp = tribe( 'tickets.rsvp' );

		// Check if the call is coming from a shortcode.
		$doing_shortcode = tribe_doing_shortcode( 'tribe_tickets_rsvp' );

		// Get the RSVP block HTML ID.
		$block_html_id  = 'rsvp-now';
		$block_html_id .= $doing_shortcode ? '-' . uniqid() : '';

		// Load assets manually.
		$blocks_rsvp->assets();

		$tickets        = $blocks_rsvp->get_tickets( $post_id );
		$active_tickets = $blocks_rsvp->get_active_tickets( $tickets );
		$past_tickets   = $blocks_rsvp->get_all_tickets_past( $tickets );

		if( class_exists( 'Tribe__Tickets_Plus__Main' ) && ! empty( $include_tickets ) ) {
			$tickets        = Tribe__Tickets_Plus__Tickets::filter_tickets_by_ids( $tickets, $include_tickets );
			$active_tickets = Tribe__Tickets_Plus__Tickets::filter_tickets_by_ids( $active_tickets, $include_tickets );
			$past_tickets   = Tribe__Tickets_Plus__Tickets::filter_tickets_by_ids( $past_tickets, $include_tickets );
		}

		$args = [
			'post_id'             => $post_id,
			'attributes'          => $blocks_rsvp->attributes(),
			'active_rsvps'        => $active_tickets,
			'all_past'            => $past_tickets,
			'has_rsvps'           => ! empty( $tickets ),
			'has_active_rsvps'    => ! empty( $active_tickets ),
			'must_login'          => ! is_user_logged_in() && $rsvp->login_required(),
			'login_url'           => Tribe__Tickets__Tickets::get_login_url( $post_id ),
			'threshold'           => $blocks_rsvp->get_threshold( $post_id ),
			'step'                => null,
			'opt_in_checked'      => false,
			'opt_in_attendee_ids' => '',
			'opt_in_nonce'        => '',
			'doing_shortcode'     => $doing_shortcode,
			'block_html_id'       => $block_html_id,
			'going'               => tribe_get_request_var( 'going', '' ),
			'attendees'           => [],
		];

		/**
		 * Add the rendering attributes into global context.
		 *
		 * Start with the following for template files loading this global context.
		 * Keep all templates with this starter block of comments updated if these global args update.
		 *
		 * @var Tribe__Tickets__Editor__Template $this                Template object.
		 * @var int                              $post_id             [Global] The current Post ID to which RSVPs are attached.
		 * @var array                            $attributes          [Global] RSVP attributes (could be empty).
		 * @var Tribe__Tickets__Ticket_Object[]  $active_rsvps        [Global] List of RSVPs.
		 * @var bool                             $all_past            [Global] True if RSVPs availability dates are all in the past.
		 * @var bool                             $has_rsvps           [Global] True if the event has any RSVPs.
		 * @var bool                             $has_active_rsvps    [Global] True if the event has any RSVPs available.
		 * @var bool                             $must_login          [Global] True if login is required and user is not logged in..
		 * @var string                           $login_url           [Global] The site's login URL.
		 * @var int                              $threshold           [Global] The count at which "number of tickets left" message appears.
		 * @var null|string                      $step                [Global] The point we're at in the loading process.
		 * @var bool                             $opt_in_checked      [Global] Whether appearing in Attendee List was checked.
		 * @var string                           $opt_in_attendee_ids [Global] The list of attendee IDs to send in the form submission.
		 * @var string                           $opt_in_nonce        [Global] The nonce for opt-in AJAX requests.
		 * @var bool                             $doing_shortcode     [Global] True if detected within context of shortcode output.
		 * @var bool                             $block_html_id       [Global] The RSVP block HTML ID. $doing_shortcode may alter it.
		 */
		$template->add_template_globals( $args );

		ob_start();

		/**
		 * Allow for the addition of content (namely the "Who's Attending?" list) above the ticket form.
		 *
		 * @since 4.5.5
		 * @since 5.8.2 Added the `$post_id` and `$post` parameters.
		 *
		 * @param int     $post_id The ID of the post the RSVP block is being rendered for.
		 * @param WP_Post $post    The post object the RSVP block is being rendered for.
		 */
		do_action( 'tribe_tickets_before_front_end_ticket_form', $post_id, $post );

		/**
		 * A flag we can set via filter, e.g. at the end of this method, to ensure this template only shows once.
		 *
		 * @since 4.5.6
		 *
		 * @param boolean $already_rendered Whether the order link template has already been rendered.
		 *
		 * @see Tribe__Tickets__Tickets_View::inject_link_template()
		 */
		$already_rendered = apply_filters( 'tribe_tickets_order_link_template_already_rendered', false );

		// Output order links / view link if we haven't already (for RSVPs).
		if ( ! $already_rendered ) {
			$template->template( 'tickets/view-link' );

			add_filter( 'tribe_tickets_order_link_template_already_rendered', '__return_true' );
		}

		$before_content = ob_get_clean();

		// Maybe echo the content from the action.
		if ( $echo ) {
			echo $before_content;

			$before_content = '';
		}

		// Maybe render the new views.
		if ( tribe_tickets_rsvp_new_views_is_enabled() ) {
			// Enqueue new assets.
			tribe_asset_enqueue_group( 'tribe-tickets-rsvp' );
			tribe_asset_enqueue( 'tribe-tickets-rsvp-style' );
			tribe_asset_enqueue( 'tribe-tickets-forms-style' );
			// @todo: Remove this once we solve the common breakpoints vs container based.
			tribe_asset_enqueue( 'tribe-common-responsive' );

			return $before_content . $template->template( 'v2/rsvp', $args, $echo );
		}

		// Enqueue assets.
		tribe_asset_enqueue( 'tribe-tickets-gutenberg-rsvp' );
		tribe_asset_enqueue( 'tribe-tickets-gutenberg-block-rsvp-style' );

		return $before_content . $template->template( 'blocks/rsvp', $args, $echo );
	}

	/**
	 * Generate the required data for the "My Tickets" link.
	 *
	 * @since 5.8.0
	 * @since 5.8.2 Removed the optional parameter from `get_tickets_page_url` call.
	 *
	 * @param int $event_id The event ID.
	 * @param int $user_id The user ID.
	 *
	 * @return array<string, mixed> The data for the "My Tickets" link.
	 */
	public function get_my_tickets_link_data( int $event_id, int $user_id ): array {
		$event              = get_post( $event_id );
		$post_type          = get_post_type_object( $event->post_type );
		$post_type_singular = $post_type ? $post_type->labels->singular_name : _x( 'Post', 'fallback post type singular name', 'event-tickets' );
		$counters           = [];
		$rsvp_count         = $this->count_rsvp_attendees( $event_id, $user_id );
		$ticket_count       = $this->count_ticket_attendees( $event_id, $user_id );

		$count_by_type = [
			'rsvp'   => [
				'count'    => $rsvp_count,
				'singular' => tribe_get_rsvp_label_singular( 'my-tickets-view-link' ),
				'plural'   => tribe_get_rsvp_label_plural( 'my-tickets-view-link' ),
			],
			'ticket' => [
				'count'    => $ticket_count,
				'singular' => tribe_get_ticket_label_singular( 'my-tickets-view-link' ),
				'plural'   => tribe_get_ticket_label_plural( 'my-tickets-view-link' ),
			],
		];

		/**
		 * Filters the data for the "My Tickets" link.
		 *
		 * @since 5.8.0
		 *
		 * @param array<string, array> $data The data for the "My Tickets" link.
		 * @param int   $event_id The event ID.
		 * @param int   $user_id  The user ID.
		 */
		$count_by_type = apply_filters( 'tec_tickets_my_tickets_link_ticket_count_by_type', $count_by_type, $event_id, $user_id );

		$total_count = 0;
		$type_count  = 0;
		$type_label  = '';

		foreach ( $count_by_type as $type => $item ) {
			if ( empty( $item['count'] ) ) {
				continue;
			}

			// Translators: 1: number of tickets, 2: ticket type label singular, 3: ticket type label plural.
			$counters[] = sprintf(
				_n( '%1$d %2$s', '%1$d %3$s', $item['count'], 'event-tickets' ),
				$item['count'],
				$item['singular'],
				$item['plural']
			);

			$type_label   = $item['count'] > 1 ? $item['plural'] : $item['singular'];
			$total_count += $item['count'];

			$type_count ++;
		}

		// Translators: 1: singular or plural label for ticket type on My Tickets page link label.
		$type_label = sprintf( __( 'View %s', 'event-tickets' ), $type_label );
		$link_label = $type_count > 1 ? __( 'View all', 'event-tickets' ) : $type_label;

		// Translators: 1: number of RSVPs and/or Tickets with accompanying ticket type text, 2: post type label
		$message = esc_html( sprintf( __( 'You have %1$s for this %2$s.', 'event-tickets' ), implode( _x( ' and ', 'separator if there are more multiple type of tickets.', 'event-tickets' ), $counters ), $post_type_singular ) );

		return [
			'total_count' => $total_count,
			'message'     => $message,
			'link_label'  => $link_label,
			'link'        => $this->get_tickets_page_url( $event_id ),
		];
	}

	/**
	 * Preserve pretty URL format in canonical redirect for tickets pages.
	 *
	 * @since 5.25.0
	 *
	 * @param string $redirect_url The URL to redirect to.
	 * @return string The URL to redirect to.
	 */
	public function preserve_tickets_parameter_in_canonical_redirect( $redirect_url ) {
		// Bail early if no redirect URL or no tribe-edit-orders parameter.
		if ( empty( $redirect_url ) || ! get_query_var( 'tribe-edit-orders' ) ) {
			return $redirect_url;
		}

		$post_id = get_query_var( 'p' );

		// Bail early if no post ID.
		if ( ! $post_id ) {
			// Fallback: preserve the parameter in query string format.
			return add_query_arg( 'tribe-edit-orders', 1, $redirect_url );
		}

		// Check if we have pretty permalinks enabled.
		$has_plain_permalink = '' === get_option( 'permalink_structure' );

		// Bail early if using plain permalinks - use query string format.
		if ( $has_plain_permalink ) {
			return add_query_arg( 'tribe-edit-orders', 1, $redirect_url );
		}

		// Use WordPress rewrite API to generate the proper URL.
		return $this->get_tickets_page_url( $post_id );
	}

	/**
	 * Handle tickets page requests to prevent canonical redirect issues.
	 *
	 * @since 5.25.0
	 *
	 * @param array $query_vars The query variables.
	 * @return array The modified query variables.
	 */
	public function handle_tickets_request( $query_vars ) {
		// Bail early if this is not a tickets page request.
		if ( empty( $query_vars['tribe-edit-orders'] ) ) {
			return $query_vars;
		}

		// Bail early if there's no post ID.
		if ( ! isset( $query_vars['p'] ) || ! $query_vars['p'] ) {
			return $query_vars;
		}

		$post = get_post( $query_vars['p'] );

		// Bail early if no post is found.
		if ( ! $post ) {
			return $query_vars;
		}

		// Force the query to be treated as a single post.
		$query_vars['post_type'] = $post->post_type;

		if ( 'page' === $post->post_type ) {
			$query_vars['page_id'] = $post->ID;
			unset( $query_vars['p'] );
		}
		
		return $query_vars;
	}
}
