<?php

use TEC\Events\Custom_Tables\V1\Tables\Occurrences;
use TEC\Events\Custom_Tables\V1\WP_Query\Custom_Tables_Query;
use Tribe__Events__Pro__Main as ECP;
use Tribe__Events__Main as TEC;
use TEC\Tickets\Admin\Attendees\Page as Attendees_Page;
use TEC\Tickets\Commerce\Ticket as Commerce_Ticket;

/**
 * Handles moving attendees from a post to another.
 */
class Tribe__Tickets__Admin__Move_Tickets {
	protected $dialog_name = 'move_tickets';
	protected $ticket_history;
	protected $has_multiple_providers = false;
	protected $ticket_provider = '';

	/**
	 * The attendees currently being operated on.
	 *
	 * Structure is an array indexed by attendee ID, with each value being
	 * the attendee array itself.
	 *
	 * @var array
	 */
	protected $attendees = [];

	public function setup() {
		$this->ticket_history();

		add_action( 'admin_init', [ $this, 'dialog' ] );
		add_filter( 'tribe_events_tickets_attendees_table_bulk_actions', [ $this, 'bulk_actions' ] );
		add_action( 'wp_ajax_move_tickets', [ $this, 'move_tickets_request' ] );
		add_action( 'tribe_tickets_ticket_type_moved', [ $this, 'move_all_tickets_for_type' ], 10, 4 );
		add_action( 'wp_ajax_move_tickets_get_post_types', [ $this, 'get_post_types' ] );
		add_action( 'wp_ajax_move_tickets_get_post_choices', [ $this, 'get_post_choices' ] );
		add_action( 'wp_ajax_move_tickets_get_ticket_types', [ $this, 'get_ticket_types' ] );
		add_action( 'tribe_tickets_all_tickets_moved', [ $this, 'notify_attendees' ], 10, 4 );
	}

	/**
	 * @return Tribe__Tickets__Admin__Ticket_History
	 */
	public function ticket_history() {
		if ( ! isset( $this->ticket_history ) ) {
			$this->ticket_history = new Tribe__Tickets__Admin__Ticket_History;
		};

		return $this->ticket_history;
	}

	/**
	 * Sets up the move tickets dialog.
	 */
	public function dialog() {
		if ( ! $this->is_move_tickets_dialog() ) {
			return;
		}

		if ( ! wp_verify_nonce( $_GET['check'], 'move_tickets' ) ) {
			return;
		}

		$event_id = absint( tribe_get_request_var( 'event_id', tribe_get_request_var( 'post', 0 ) ) );

		// Bail when we don't have the event.
		if ( 0 === $event_id ) {
			return;
		}

		$attendee_ids = tribe_get_request_var( 'ticket_ids', '' );
		$attendee_ids = array_map( 'intval', explode( '|', $attendee_ids ) );
		$attendee_ids = array_filter( $attendee_ids );

		$this->build_attendee_list( $attendee_ids, $event_id );

		/**
		 * Provides an opportunity to modify the template variables used in the
		 * move tickets dialog.
		 *
		 * @param array $template_vars
		 */
		$template_vars = (array) apply_filters( 'tribe_tickets_move_tickets_template_vars', array(
			'title'              => __( 'Move Attendees', 'event-tickets' ),
			'mode'               => 'move_tickets',
			'check'              => wp_create_nonce( 'move_tickets' ),
			'event_name'         => get_the_title( $event_id ),
			'attendees'          => $this->attendees,
			'multiple_providers' => $this->has_multiple_providers,
		) );

		set_current_screen();
		define( 'IFRAME_REQUEST', true );
		$this->dialog_assets();
		iframe_header( $template_vars['title'] );

		extract( $template_vars );
		include EVENT_TICKETS_DIR . '/src/admin-views/move-tickets.php';

		iframe_footer();
		exit();
	}

	/**
	 * Enqueues all assets and data required by the move tickets dialog.
	 */
	protected function dialog_assets() {
		// Ensure common admin CSS is enqueued within this screen
		add_filter( 'tribe_asset_enqueue_tribe-common-admin', '__return_true', 20 );

		/**
		 * Provides an opportunity to modify the variables passed to the move
		 * tickets JS code.
		 *
		 * @param array $script_data
		 */
		$data = apply_filters( 'tribe_tickets_move_tickets_script_data', array(
			'check'                    => wp_create_nonce( 'move_tickets' ),
			'unexpected_failure'       => '<p>' . __( 'Woops! We could not complete the requested operation due to an unforeseen problem.', 'event-tickets' ) . '</p>',
			'update_post_list_failure' => __( 'Unable to update the post list. Please refresh the page and try again.', 'event-tickets' ),
			'no_posts_found'           => __( 'No results found - you may need to widen your search criteria.', 'event-tickets' ),
			'no_ticket_types_found'    => __( 'No tickets were found for this post.', 'event-tickets' ),
			'loading_msg'              => __( 'Loading, please wait&hellip;', 'event-tickets' ),
			'src_post_id'              => absint( tribe_get_request_var( 'event_id', tribe_get_request_var( 'post', 0 ) ) ),
			'ticket_ids'               => array_keys( $this->attendees ),
			'provider'                 => $this->ticket_provider,
			'mode'                     => 'move_tickets',
		) );

		tec_asset(
			Tribe__Tickets__Main::instance(),
			'tribe-move-tickets-dialog',
			'move-tickets-dialog.js',
			array( 'jquery' ),
			'admin_enqueue_scripts',
			array(
				'localize' => array(
					'name' => 'tribe_move_tickets_data',
					'data' => $data,
				),
			)
		);
	}

	/**
	 * Indicates if the current request is for the "move tickets type"
	 * dialog or not.
	 *
	 * @return bool
	 */
	protected function is_move_tickets_dialog() {
		return ( isset( $_GET['dialog'] ) && $this->dialog_name === $_GET['dialog'] );
	}

	/**
	 * @return string
	 */
	public function dialog_name() {
		return $this->dialog_name;
	}

	/**
	 * Takes the provided array of attendee IDs and uses it to populate
	 * $this->attendees, to determine which ticket provider we're using
	 * or if the range of tickets includes more than one provider.
	 *
	 * @param array $attendee_ids
	 * @param int   $event_id
	 */
	protected function build_attendee_list( array $attendee_ids, $event_id ) {
		$this->attendees              = [];
		$this->ticket_provider        = '';
		$this->has_multiple_providers = false;

		$args = [
			'in' => $attendee_ids,
		];

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

		foreach ( $attendee_data['attendees'] as $attendee ) {

			if ( empty( $attendee['attendee_id'] ) ) {
				$attendee['attendee_id'] = $attendee['ID'];
			}

			$attendee_id = (int) $attendee['attendee_id'];

			$this->attendees[ $attendee_id ] = $attendee;

			$provider = (string) $this->get_ticket_provider( $attendee );

			if (
				! empty( $this->ticket_provider )
				&& ! empty( $provider )
				&& $this->ticket_provider !== $provider
			) {
				$this->has_multiple_providers = true;
			}

			$this->ticket_provider = empty( $provider ) && isset( $attendee['provider'] ) ? $attendee['provider'] : $provider;
		}
	}

	/**
	 * Given an attendee array, attempts to determine which ticket provider owns it (Woo, RSVP, etc).
	 *
	 * @param array $attendee
	 *
	 * @return string
	 */
	protected function get_ticket_provider( array $attendee ) {
		if ( ! isset( $attendee['product_id'] ) ) {
			return '';
		}

		$ticket_type = Tribe__Tickets__Tickets::load_ticket_object( $attendee['product_id'] );

		if ( ! $ticket_type ) {
			return '';
		}

		if ( property_exists( $ticket_type, 'provider_class' ) ) {
			return $ticket_type->provider_class;
		}

		return '';
	}

	/**
	 * Adds a "Move" option to the attendee screen bulk action selector.
	 *
	 * There is not a corresponding bulk action handler, as such, because when this
	 * is selected further handling will be managed via JS (and interaction will be
	 * through a modal interface).
	 *
	 * @param array $actions
	 *
	 * @return array
	 */
	public function bulk_actions( array $actions ) {
		if (
			tribe( 'tickets.attendees' )->user_can_manage_attendees()
			&& is_admin()
			&& ! tribe( Attendees_Page::class )->is_on_page()
		) {
			$actions['move'] = _x( 'Move', 'attendee screen bulk actions', 'event-tickets' );
		}

		return $actions;
	}

	/**
	 * Responds to ajax requests for a list of supported post types.
	 */
	public function get_post_types() {
		if ( ! wp_verify_nonce( $_POST['check'], 'move_tickets' ) ) {
			wp_send_json_error();
		}

		wp_send_json_success( array( 'posts' => $this->get_post_types_list() ) );
	}

	/**
	 * Returns a list of post types for which tickets are currently enabled.
	 *
	 * The list is expressed as an array in the following format:
	 *
	 *     [ 'slug' => 'name', ... ]
	 *
	 * @return array
	 */
	protected function get_post_types_list() {
		$types_list = array( 'all' => __( 'All supported types', 'tribe-tickets' ) );

		foreach ( Tribe__Tickets__Main::instance()->post_types() as $type ) {
			$pto                 = get_post_type_object( $type );
			$types_list[ $type ] = $pto->label;
		}

		return $types_list;
	}

	/**
	 * Responds to requests for a list of possible destination posts.
	 */
	public function get_post_choices() {
		if ( ! wp_verify_nonce( $_POST['check'], 'move_tickets' ) ) {
			wp_send_json_error();
		}

		$args = wp_parse_args( $_POST, array(
			'post_type'    => '',
			'search_terms' => '',
			'ignore'       => '',
		) );

		wp_send_json_success(
			array(
				'posts' => $this->get_possible_matches( $args ),
			)
		);
	}

	/**
	 * Returns a list of posts that could be possible homes for a ticket
	 * type, given the constraints in optional array $request (if not set,
	 * looks in $_POST for the corresponding values):
	 *
	 * - 'post_type': string or array of post types
	 * - 'search_term': string used for searching posts to narrow the field
	 *
	 * @param array|null $request post parameters (or looks at $_POST if not set)
	 *
	 * @return array
	 */
	protected function get_possible_matches( array $request = null ) {
		// Take the params from $request if set, else look at $_POST
		$params = wp_parse_args( is_null( $request ) ? $_POST : $request, array(
			'post_type'    => array(),
			'search_terms' => '',
			'ignore'       => '',
		) );

		// The post_type argument should be an array (of all possible types, if not specified)
		$post_types = array_filter( (array) $params['post_type'] );

		if ( empty( $post_types ) || 'all' === $params['post_type'] ) {
			$post_types = array_keys( $this->get_post_types_list() );
		}

		/**
		 * Controls the number of posts returned when searching for posts that
		 * can serve as ticket hosts.
		 *
		 * @param int $limit
		 */
		$limit = (int) apply_filters( 'tribe_tickets_find_ticket_type_host_posts_limit', 100 );

		$ignore_ids = (array) $params['ignore'];
		$ignore_ids = array_map( 'absint', $ignore_ids );
		$ignore_ids = array_filter( $ignore_ids );

		$query_args = [
			'post_type'           => $post_types,
			'posts_per_page'      => $limit,
			'eventDisplay'        => 'custom',
			'orderby'             => 'title',
			'order'               => 'ASC',
			's'                   => $params['search_terms'],
			'post__not_in'        => $ignore_ids,
			'post__not_recurring' => true, // Supported only in CT1.
		];

		/**
		 * Filters the query arguments used to find posts that can serve as ticket hosts.
		 *
		 * @since 5.8.0
		 *
		 * @param array<string,mixed> $query_args The query arguments that will be used to find posts that can serve
		 *                                        as ticket hosts.
		 * @param array<string,mixed> $request    The request parameters that were used to generate the query arguments.
		 */
		$query_args = apply_filters( 'tec_tickets_find_ticket_type_host_posts_query_args', $query_args, $request );
		$query = new \WP_Query( $query_args );

		return $this->format_post_list( $query );
	}

	/**
	 * Given an array of WP_Post objects, returns an array containing the post title
	 * of each (with the post ID as the index).
	 *
	 * @param array $query_results
	 *
	 * @return array
	 */
	protected function format_post_list( \WP_Query $query ) {
		global $post;
		$posts = array();

		while ( $query->have_posts() ) {
			$query->the_post();
			/*y This filter is documented in wp-includes/post-template.php */
			$title = apply_filters( 'the_title', $post->post_title, $post->ID );

			// Append the event start date if there is one, ie for events.
			if ( $post->_EventStartDate ) {
				$title .= ' (' . tribe_get_start_date( $post->ID ) . ')';
			}

			$posts[ $title ] = $post->ID;
		}

		wp_reset_postdata();

		// Ensure the array is sorted by keys (titles) to maintain consistent ordering.
		ksort( $posts );

		return $posts;
	}

	/**
	 * Returns a list of ticket types available in a specific post
	 * (belonging to a specific provider).
	 */
	public function get_ticket_types() {
		if ( ! wp_verify_nonce( $_POST['check'], 'move_tickets' ) ) {
			wp_send_json_error();
		}

		$args = wp_parse_args( $_POST, array(
			'post_id'  => '',
			'provider' => '',
		) );

		wp_send_json_success(
			array(
				'posts' => $this->get_ticket_type_matches( $args['post_id'], $args['provider'], $args['ticket_ids'] ),
			)
		);
	}

	/**
	 * Builds a list of ticket types in the designated post and belonging to
	 * the specified provider.
	 *
	 * @param int    $target_post_id
	 * @param string $provider
	 * @param array  $ticket_ids
	 *
	 * @return array
	 */
	protected function get_ticket_type_matches( $target_post_id, $provider, $ticket_ids = array() ) {
		$ticket_types = array();
		$ticket_ids   = array_map( 'absint', array_filter( $ticket_ids, 'is_numeric' ) );

		foreach ( Tribe__Tickets__Tickets::get_event_tickets( $target_post_id ) as $ticket ) {
			if ( stripslashes( $provider ) !== $ticket->provider_class ) {
				continue;
			}

			if ( in_array( $ticket->ID, $ticket_ids ) ) {
				continue;
			}

			$ticket_types[ absint( $ticket->ID ) ] = esc_html( $ticket->name );
		}

		return $ticket_types;
	}

	/**
	 * Listens for and handles requests to reassign tickets from one ticket type to another.
	 *
	 * @since 4.10.9 Use customizable ticket name functions.
	 */
	public function move_tickets_request() {
		if ( ! wp_verify_nonce( $_POST['check'], 'move_tickets' ) ) {
			wp_send_json_error();
		}

		$args = wp_parse_args( $_POST, array(
			'ticket_ids'     => '',
			'target_type_id' => '',
			'src_post_id'    => '',
			'target_post_id' => '',
		) );

		$src_post_id    = absint( $args['src_post_id'] );
		$ticket_ids     = array_map( 'intval', (array) $args['ticket_ids'] );
		$target_type_id = absint( $args['target_type_id'] );
		$target_post_id = absint( $args['target_post_id'] );

		if ( ! $ticket_ids || ! $target_type_id ) {
			wp_send_json_error( array(
				'message' => esc_html(
					sprintf(
						__( '%1$s could not be moved: valid %2$s IDs or a destination ID were not provided.', 'event-tickets' ),
						tribe_get_ticket_label_plural( 'move_tickets_request_error' ),
						tribe_get_ticket_label_singular( 'move_tickets_request_error' )
					)
				)
			) );
		}

		$moved_tickets = $this->move_tickets( $ticket_ids, $target_type_id, $src_post_id, $target_post_id );

		if ( ! $moved_tickets ) {
			wp_send_json_error( array(
				'message' => esc_html( sprintf( __( '%s could not be moved: there was an unexpected failure during reassignment.', 'event-tickets' ), tribe_get_ticket_label_plural( 'move_tickets_request_error' )
				) )
			) );
		}

		$remove_tickets = ( $src_post_id != $target_post_id ) ? $ticket_ids : null;

		// Include details of the new ticket type the tickets were reassigned to
		$moved_to = sprintf(
			_x( 'assigned to %s', 'moved tickets success message fragment', 'event-tickets' ),
			'<a href="' . esc_url( get_admin_url( null, '/post.php?post=' . $target_type_id . '&action=edit' ) ) . '" target="_blank">' . esc_html( get_the_title( $target_type_id ) ) . '</a>'
		);

		// If that ticket type is hosted by a different event post, prepend details of that also
		if ( $src_post_id !== $target_post_id ) {
			$moved_to = sprintf(
				            _x( 'moved to %s and', 'moved tickets success message fragment', 'event-tickets' ),
				            '<a href="' . esc_url( get_admin_url( null, '/post.php?post=' . $target_post_id . '&action=edit' ) ) . '" target="_blank">' . esc_html( get_the_title( $target_post_id ) ) . '</a>'
			            ) . ' ' . $moved_to;
		}

		wp_send_json_success( array(
			'message'        => sprintf(
				_n(
					'%1$d attendee for %2$s was successfully %3$s. By default, we adjust capacity and stock, however, we recommend reviewing each as needed to ensure numbers are correct. This attendee will receive an email notifying them of the change.',
					'%1$d attendees for %2$s were successfully %3$s. By default, we adjust capacity and stock, however, we recommend reviewing each as needed to ensure numbers are correct. These attendees will receive an email notifying them of the change.',
					$moved_tickets,
					'event-tickets'
				),
				$moved_tickets,
				'<a href="' . esc_url( get_admin_url( null, '/post.php?post=' . $src_post_id . '&action=edit' ) ) . '" target="_blank">' . esc_html( get_the_title( $src_post_id ) ) . '</a>',
				$moved_to
			),
			'remove_tickets' => $remove_tickets,
		) );
	}

	/**
	 * Moves tickets to a new ticket type.
	 *
	 * The target ticket type *must* belong to the same provider as the tickets being
	 * moved (ie, you cannot move RSVP tickets to a WooCommerce ticket type, nor can
	 * a mix of RSVP and WooCommerce tickets be moved to a new ticket type).
	 *
	 * @param array $ticket_ids
	 * @param int   $tgt_ticket_type_id
	 * @param int   $src_event_id
	 * @param int   $tgt_event_id
	 *
	 * @return int number of successfully moved tickets (zero upon failure to move any)
	 */
	public function move_tickets( array $ticket_ids, $tgt_ticket_type_id, $src_event_id, $tgt_event_id ) {
		$ticket_ids = array_map( 'intval', $ticket_ids );

		/**
		 * Filters the set of Attendee IDs that will be moved following a user request to move Attendees.
		 *
		 * A note about misnomers coming from legacy code: 1 .this method moves Attendees, not Tickets, from a Ticket
		 * on a ticket-able post to another Ticket on another ticketable post 2. the term Event is used to refer to any
		 * ticket-able post, not just Events.
		 *
		 * @since 5.8.3
		 *
		 * @parram int[] $ticket_ids The set of Attendee IDs that will be moved.
		 *
		 * @param int $tgt_ticket_type_id The ID of the Ticket the Attendees will be moved to.
		 * @param int $src_event_id       The ID of the ticket-able Post the Attendees are currently associated with.
		 * @param int $tgt_event_id       The ID of the ticket-able Post the Attendees will be moved to.
		 */
		$ticket_ids = apply_filters( 'tec_tickets_move_attendees_ids', $ticket_ids, $tgt_ticket_type_id, $src_event_id, $tgt_event_id );

		$instigator_id    = get_current_user_id();
		$ticket_type      = Tribe__Tickets__Tickets::load_ticket_object( $tgt_ticket_type_id );
		$successful_moves = 0;

		if ( ! $ticket_type ) {
			return 0;
		}

		$ticket_objects = [];
		$providers      = [];

		$args = [
			'in' => $ticket_ids,
		];

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

		foreach ( $attendee_data['attendees'] as $issued_ticket ) {
			$ticket_objects[] = $issued_ticket;

			$providers[ $issued_ticket['provider'] ] = call_user_func( array(
				$issued_ticket['provider'],
				'get_instance'
			) );
		}

		// We expect to have found as many tickets as were specified.
		if ( count( $ticket_objects ) !== count( $ticket_ids ) ) {
			return 0;
		}

		// Check that the tickets are homogeneous in relation to the ticket provider.
		if ( 1 !== count( $providers ) ) {
			return 0;
		}

		$provider_class   = key( $providers );
		$ticket_type_key  = constant( $provider_class . '::ATTENDEE_PRODUCT_KEY' );
		$ticket_event_key = constant( $provider_class . '::ATTENDEE_EVENT_KEY' );

		if ( empty( $ticket_type_key ) || empty( $ticket_event_key ) ) {
			return 0;
		}

		foreach ( $ticket_objects as $ticket ) {
			$ticket_id          = $ticket['attendee_id'];
			$product_id         = $ticket['product_id'];
			$src_ticket_type_id = get_post_meta( $ticket_id, $ticket_type_key, true );

			/**
			 * Fires immediately before a ticket is moved.
			 *
			 * @param int $ticket_type_id
			 * @param int $tgt_ticket_type_id
			 * @param int $tgt_event_id
			 * @param int $instigator_id
			 */
			do_action( 'tribe_tickets_ticket_before_move', $ticket_id, $tgt_ticket_type_id, $tgt_event_id, $instigator_id );

			// Update the global stock level if necessary.
			( new Tribe__Tickets__Global_Stock( $product_id ) )->update_stock_level_on_move( $src_event_id, $tgt_event_id );

			// Move the Attendee to the new Ticket Type and Event.
			update_post_meta( $ticket_id, $ticket_type_key, $tgt_ticket_type_id );
			update_post_meta( $ticket_id, $ticket_event_key, $tgt_event_id );

			// adjust stock numbers for RSVP and Commerce Tickets.
			if ( 'Tribe__Tickets__RSVP' === $ticket['provider'] ) {
				$rsvp = new Tribe__Tickets__RSVP();
				$rsvp->decrease_ticket_sales_by( $src_ticket_type_id );
				$rsvp->increase_ticket_sales_by( $tgt_ticket_type_id );
			} else {
				$c_ticket = new Commerce_Ticket();
				
				// Check if the ticket type uses shared capacity.
				$shared_capacity = $ticket_type->global_stock_mode() === 'global' || $ticket_type->global_stock_mode() === 'capped';
				
				// Create separate global stock objects for source and target events.
				$src_global_stock = new Tribe__Tickets__Global_Stock( $src_event_id );
				$tgt_global_stock = new Tribe__Tickets__Global_Stock( $tgt_event_id );
				
				// Adjust the stock level for the source and target events.
				$c_ticket->decrease_ticket_sales_by( $src_ticket_type_id, 1, $shared_capacity, $src_global_stock );
				$c_ticket->increase_ticket_sales_by( $tgt_ticket_type_id, 1, $shared_capacity, $tgt_global_stock );
			}

			$history_message = sprintf(
				__( 'This ticket was moved to %1$s %2$s from %3$s %4$s', 'event-tickets' ),
				'<a href="' . esc_url( get_the_permalink( $tgt_event_id ) ) . '" target="_blank">' . esc_html( get_the_title( $tgt_event_id ) ) . '</a>',
				'<a href="' . esc_url( get_the_permalink( $tgt_ticket_type_id ) ) . '" target="_blank">(' . esc_html( get_the_title( $tgt_ticket_type_id ) ) . ')</a>',
				'<a href="' . esc_url( get_the_permalink( $src_event_id ) ) . '" target="_blank">' . esc_html( get_the_title( $src_event_id ) ) . '</a>',
				'<a href="' . esc_url( get_the_permalink( $src_ticket_type_id ) ) . '" target="_blank">(' . esc_html( get_the_title( $src_ticket_type_id ) ) . ')</a>'
			);

			$history_data = array(
				'ticket_ids'         => $ticket_ids,
				'src_event_id'       => $src_event_id,
				'tgt_event_id'       => $tgt_event_id,
				'tgt_ticket_type_id' => $tgt_ticket_type_id,
			);

			Tribe__Post_History::load( $ticket_id )->add_entry( $history_message, $history_data );

			/**
			 * Fires when a ticket is relocated from ticket type to another, which may be in
			 * a different post altogether.
			 *
			 * @param int $ticket_id          the ticket which has been moved
			 * @param int $src_ticket_type_id the ticket type it belonged to originally
			 * @param int $tgt_ticket_type_id the ticket type it now belongs to
			 * @param int $src_event_id       the event/post which the ticket originally belonged to
			 * @param int $tgt_event_id       the event/post which the ticket now belongs to
			 * @param int $instigator_id      the user who initiated the change
			 */
			do_action( 'tribe_tickets_ticket_moved', $ticket_id, $src_ticket_type_id, $tgt_ticket_type_id, $src_event_id, $tgt_event_id, $instigator_id );

			$successful_moves ++;
		}

		// Clear attendee cache now that the attendees have moved.
		foreach ( $providers as $provider ) {
			$provider->clear_attendees_cache( $src_event_id );
			$provider->clear_attendees_cache( $tgt_event_id );
		}

		/**
		 * Fires when all of the specified ticket IDs have been moved
		 *
		 * @param array $ticket_ids         each ticket ID
		 * @param int   $tgt_ticket_type_id the ticket type they were moved to
		 * @param int   $src_event_id       the event they belonged to prior to the move
		 * @param int   $tgt_event_id       the event they belong to after the move
		 */
		do_action( 'tribe_tickets_all_tickets_moved', $ticket_ids, $tgt_ticket_type_id, $src_event_id, $tgt_event_id );

		return $successful_moves;
	}

	/**
	 * Notifies the ticket owners that their tickets have been moved to a new ticket
	 * type.
	 *
	 * @param array $ticket_ids
	 * @param int   $tgt_ticket_type_id
	 * @param int   $src_event_id
	 * @param int   $tgt_event_id
	 */
	public function notify_attendees( $ticket_ids, $tgt_ticket_type_id, $src_event_id, $tgt_event_id ) {
		$to_notify = array();

		$args = [
			'in' => $ticket_ids,
			'by' => [
				'ticket' => $tgt_ticket_type_id,
			],
		];

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

		// Build a list of email addresses we want to send notifications of the change to
		foreach ( $attendee_data['attendees'] as $attendee ) {
			// Skip if an email address isn't available
			if ( ! isset( $attendee['purchaser_email'] ) ) {
				continue;
			}

			if ( ! isset( $to_notify[ $attendee['purchaser_email'] ] ) ) {
				$to_notify[ $attendee['purchaser_email'] ] = array( $attendee );
			} else {
				$to_notify[ $attendee['purchaser_email'] ][] = $attendee;
			}
		}

		foreach ( $to_notify as $email_addr => $affected_tickets ) {
			/**
			 * Sets the moved ticket email address.
			 *
			 * @param string $email_addr
			 */
			$to = apply_filters( 'tribe_tickets_ticket_moved_email_recipient', $email_addr );

			/**
			 * Sets any attachments for the moved ticket email address.
			 *
			 * @param array $attachments
			 */
			$attachments = apply_filters( 'tribe_tickets_ticket_moved_email_attachments', array() );

			/**
			 * Sets the HTML for the moved ticket email.
			 *
			 * @param string $html
			 */
			$content = apply_filters( 'tribe_tickets_ticket_moved_email_content',
				$this->generate_email_content( $tgt_ticket_type_id, $src_event_id, $tgt_event_id, $affected_tickets )
			);

			/**
			 * Sets any headers for the moved tickets email.
			 *
			 * @param array $headers
			 */
			$headers = apply_filters( 'tribe_tickets_ticket_moved_email_headers',
				array( 'Content-type: text/html' )
			);

			/**
			 * Sets the subject line for the moved tickets email.
			 *
			 * @param string $subject
			 */
			$subject = apply_filters( 'tribe_tickets_ticket_moved_email_subject',
				sprintf( __( 'Changes to your tickets from %s', 'event-tickets' ), get_bloginfo( 'name' ) )
			);

			wp_mail( $to, $subject, $content, $headers, $attachments );
		}
	}

	/**
	 * @param int   $tgt_ticket_type_id
	 * @param int   $src_event_id
	 * @param int   $tgt_event_id
	 * @param array $affected_tickets
	 *
	 * @return string
	 */
	protected function generate_email_content( $tgt_ticket_type_id, $src_event_id, $tgt_event_id, $affected_tickets ) {
		$vars = array(
			'original_event_id'   => $src_event_id,
			'original_event_name' => get_the_title( $src_event_id ),
			'new_event_id'        => $tgt_event_id,
			'new_event_name'      => get_the_title( $tgt_event_id ),
			'ticket_type_id'      => $tgt_ticket_type_id,
			'ticket_type_name'    => get_the_title( $tgt_ticket_type_id ),
			'affected_tickets'    => $affected_tickets,
		);

		return tribe_tickets_get_template_part( 'tickets/email-tickets-moved', null, $vars, false );
	}

	/**
	 * When a ticket type is moved, the tickets need to move with it. This callback takes
	 * care of that process.
	 *
	 * @param int $ticket_type_id
	 * @param int $destination_post_id
	 * @param int $src_post_id
	 * @param int $instigator_id
	 *
	 * @see Tribe__Tickets__Admin__Move_Ticket_Types::move_ticket_type()
	 *
	 */
	public function move_all_tickets_for_type( $ticket_type_id, $destination_post_id, $src_post_id, $instigator_id ) {
		$args = [
			'by' => [
				'ticket' => $ticket_type_id,
			],
		];

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

		foreach ( $attendee_data['attendees'] as $issued_ticket ) {
			if ( ! class_exists( $issued_ticket['provider'] ) ) {
				continue;
			}

			$issued_ticket_id = $issued_ticket['attendee_id'];

			// Move the ticket to the destination post
			$event_key = constant( $issued_ticket['provider'] . '::ATTENDEE_EVENT_KEY' );
			update_post_meta( $issued_ticket_id, $event_key, $destination_post_id );

			// Maintain an audit trail
			$history_message = sprintf(
				__( 'This ticket was moved to %1$s from %2$s', 'event-tickets' ),
				'<a href="' . esc_url( get_the_permalink( $destination_post_id ) ) . '" target="_blank">' . esc_html( get_the_title( $destination_post_id ) ) . '</a>',
				'<a href="' . esc_url( get_the_permalink( $src_post_id ) ) . '" target="_blank">' . esc_html( get_the_title( $src_post_id ) ) . '</a>'
			);

			$history_data = array(
				'src_event_id' => $src_post_id,
				'tgt_event_id' => $destination_post_id,
			);

			Tribe__Post_History::load( $issued_ticket_id )->add_entry( $history_message, $history_data );
		}
	}
}
