<?php
/**
 * Abandoned Cart Recovery admin list table.
 *
 * @package EDD\Pro\CartRecovery\Admin
 */

namespace EDD\Pro\CartRecovery\Admin\Tables;

use EDD\Admin\List_Table;
use EDD\Pro\CartRecovery\Carts\Repository;
use EDD\Pro\CartRecovery\Carts\Cart;
use EDD\Admin\Utils\NoItems;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

/**
 * List table for abandoned carts.
 */
final class Carts extends List_Table {
	use \EDD\Pro\CartRecovery\Traits\Status;

	/**
	 * Persistent cache group for customer data.
	 *
	 * @var string
	 */
	private const CUSTOMER_CACHE_GROUP = 'edd_acr_customers';

	/**
	 * Persistent cache group for cart data.
	 *
	 * @var string
	 */
	private const CART_DATA_CACHE_GROUP = 'edd_acr_cart_data';

	/**
	 * Number of items per page.
	 *
	 * @var int
	 */
	public $per_page = 20;

	/**
	 * Cart repository.
	 *
	 * @var Repository|null
	 */
	private $cart_repo;

	/**
	 * Constructor.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'edd_acr_cart',
				'plural'   => 'edd_acr_carts',
				'ajax'     => false,
			)
		);

		// Initialize cart repository with error handling to prevent fatal errors.
		try {
			$this->cart_repo = new Repository();
		} catch ( \Exception $e ) {
			$this->cart_repo = null;
			edd_debug_log( 'EDD ACR: Cart repository initialization failed - ' . $e->getMessage() );
		}
	}

	/**
	 * Get the base URL for the cart recovery list table.
	 *
	 * @since 3.6.5
	 * @return string Base URL for the cart list.
	 */
	public function get_base_url() {
		$base = remove_query_arg( edd_admin_removable_query_args(), admin_url( 'edit.php' ) );

		return add_query_arg(
			array(
				'post_type' => 'download',
				'page'      => 'edd-cart-recovery',
				'tab'       => 'carts',
			),
			$base
		);
	}

	/**
	 * Retrieves the list of columns.
	 *
	 * @since 3.6.5
	 */
	public function get_columns(): array {
		return array(
			'cb'          => '<input type="checkbox" />',
			'customer'    => __( 'Customer', 'easy-digital-downloads' ),
			'cart_total'  => __( 'Cart Total', 'easy-digital-downloads' ),
			'status'      => __( 'Status', 'easy-digital-downloads' ),
			'email_count' => __( 'Emails Sent', 'easy-digital-downloads' ),
			'last_email'  => __( 'Last Email', 'easy-digital-downloads' ),
		);
	}

	/**
	 * Columns that are sortable.
	 *
	 * @since 3.6.5
	 * @return array
	 */
	protected function get_sortable_columns(): array {
		return array(
			'last_email' => array( 'last_recovery_email', false ),
			'cart_total' => array( 'cart_total', false ),
		);
	}

	/**
	 * Primary column name.
	 *
	 * @since 3.6.5
	 * @return string
	 */
	protected function get_primary_column_name(): string {
		return 'customer';
	}

	/**
	 * Checkbox column.
	 *
	 * @since 3.6.5
	 * @param Cart $item The cart item.
	 * @return string
	 */
	protected function column_cb( $item ): string {
		$cart_id = $item instanceof Cart && isset( $item->id ) ? (int) $item->id : 0;
		if ( $cart_id <= 0 ) {
			return '';
		}

		return sprintf(
			'<input type="checkbox" name="cart_ids[]" value="%s" />',
			$cart_id
		);
	}

	/**
	 * Pre-processes cart data to avoid JSON decoding in rendering loops.
	 * Implements performance monitoring and persistent caching.
	 *
	 * @since 3.6.5
	 */
	private function pre_process_cart_data(): void {
		if ( empty( $this->items ) ) {
			return;
		}

		$start_time    = microtime( true );
		$total_items   = count( $this->items );
		$decode_errors = 0;
		$empty_carts   = 0;
		$cache_hits    = 0;

		foreach ( $this->items as $item ) {
			if ( ! $item instanceof Cart ) {
				continue;
			}

			$cart_id = isset( $item->id ) ? (int) $item->id : 0;
			if ( $cart_id <= 0 ) {
				continue;
			}

			// Check persistent cache first.
			$cached_data = wp_cache_get( $cart_id, self::CART_DATA_CACHE_GROUP );
			if ( false !== $cached_data ) {
				++$cache_hits;
				continue;
			}

			$raw_cart_contents = isset( $item->cart_contents ) ? $item->cart_contents : null;
			$cart_data         = $this->safe_json_decode( $raw_cart_contents );

			if ( null === $cart_data ) {
				++$decode_errors;
				$cart_data = array();
			} elseif ( empty( $cart_data['items'] ) ) {
				++$empty_carts;
			}

			// Store in persistent cache for future requests.
			wp_cache_set( $cart_id, $cart_data, self::CART_DATA_CACHE_GROUP, 300 );
		}

		$processing_time = microtime( true ) - $start_time;

		// Log performance metrics for monitoring.
		if ( $processing_time > 0.05 ) {
			edd_debug_log(
				sprintf(
					'[EDD ACR] Cart data preprocessing took %.3fs for %d carts',
					$processing_time,
					$total_items
				)
			);
		}

		if ( $cache_hits > 0 && $total_items > 10 ) {
			$cache_hit_rate = $cache_hits / $total_items * 100;
			edd_debug_log(
				sprintf(
					'[EDD ACR] Cart data cache hit rate: %.1f%% (%d/%d carts from cache)',
					$cache_hit_rate,
					$cache_hits,
					$total_items
				)
			);
		}

		if ( $decode_errors > 0 ) {
			edd_debug_log(
				sprintf(
					'[EDD ACR] JSON decode errors: %d out of %d carts',
					$decode_errors,
					$total_items
				)
			);
		}
	}

	/**
	 * Safely decodes JSON with proper error handling and validation.
	 *
	 * @since 3.6.5
	 * @param string $json_string JSON string to decode.
	 * @return array|null Decoded array or null on failure.
	 */
	private function safe_json_decode( ?string $json_string ): ?array {
		if ( null === $json_string ) {
			return array();
		}

		$json_string = (string) $json_string;
		if ( empty( $json_string ) ) {
			return array();
		}

		try {
			$result = json_decode( $json_string, true, 512, JSON_THROW_ON_ERROR );
			return is_array( $result ) ? $result : array();
		} catch ( \JsonException $e ) {
			edd_debug_log(
				sprintf(
					'[EDD ACR] JSON decode error: %s',
					$e->getMessage()
				)
			);

			return null;
		}
	}

	/**
	 * Renders the Customer column.
	 *
	 * @since 3.6.5
	 * @param Cart $item The cart item.
	 * @return string
	 */
	protected function column_customer( $item ): string {
		if ( ! $item instanceof Cart ) {
			return '<em>' . esc_html__( 'Invalid cart data', 'easy-digital-downloads' ) . '</em>';
		}

		$cart_id = isset( $item->id ) ? (int) $item->id : 0;

		// Customer name and email.
		$customer = $this->resolve_customer_name( $item );
		if ( ! empty( $item->email ) ) {
			$customer .= '<br><small>' . esc_html( $item->email ) . '</small>';
		}

		// Get cart data for simplified display.
		$cart_data   = wp_cache_get( $cart_id, self::CART_DATA_CACHE_GROUP );
		$items       = ( false !== $cart_data && is_array( $cart_data ) && ! empty( $cart_data['items'] ) ) ? $cart_data['items'] : array();
		$total_items = count( $items );

		// Build simplified cart display.
		$cart_display = '';
		if ( $total_items > 0 ) {
			$first_item = $items[0];
			$name       = ! empty( $first_item['name'] ) ? esc_html( $first_item['name'] ) : __( 'Item', 'easy-digital-downloads' );

			if ( $total_items > 1 ) {
				$cart_display = sprintf(
					'<div class="edd-acr-cart-summary"><small>%s <span class="edd-acr-more-items">+%d %s</span></small></div>',
					$name,
					$total_items - 1,
					_n( 'more', 'more', $total_items - 1, 'easy-digital-downloads' )
				);
			} else {
				$cart_display = sprintf(
					'<div class="edd-acr-cart-summary"><small>%s</small></div>',
					$name
				);
			}
		}

		// Build row actions.
		$actions = array(
			'view' => sprintf(
				'<button type="button" class="button-link edd-acr-view-cart" data-cart-id="%s">%s</button>',
				$cart_id,
				esc_html__( 'View Details', 'easy-digital-downloads' )
			),
		);

		if ( 'abandoned' === $item->status ) {
			$sequence   = new \EDD\Pro\CartRecovery\Emails\Sequence();
			$next_email = $sequence->get_next_email_in_sequence( $item );
			if ( $next_email ) {
				$send_url        = add_query_arg(
					array(
						'edd-action' => 'acr_send_recovery_email',
						'cart_id'    => $cart_id,
					),
					$this->get_base_url()
				);
				$actions['send'] = sprintf(
					'<a href="%s">%s</a>',
					wp_nonce_url( $send_url, 'edd_acr_send_recovery_email' ),
					esc_html__( 'Send Next Email', 'easy-digital-downloads' )
				);
			}

			$actions['recover'] = sprintf(
				'<button type="button" class="button-link edd-acr-mark-recovered" data-cart-id="%s">%s</button>',
				$cart_id,
				esc_html__( 'Mark Recovered', 'easy-digital-downloads' )
			);
		}

		return $customer . $cart_display . $this->row_actions( $actions );
	}

	/**
	 * Renders the Cart Total column using pre-processed cache data.
	 *
	 * @since 3.6.5
	 * @param Cart $item The cart item.
	 * @return string
	 */
	protected function column_cart_total( $item ): string {
		$cart_id = isset( $item->id ) ? (int) $item->id : 0;
		if ( $cart_id <= 0 ) {
			return '&mdash;';
		}

		if ( ! empty( $item->cart_total ) ) {
			$total = (float) $item->cart_total;
		} else {
			// Use persistent cache to avoid JSON decoding in rendering loop.
			$cart_data = wp_cache_get( $cart_id, self::CART_DATA_CACHE_GROUP );
			if ( false === $cart_data || ! is_array( $cart_data ) ) {
				$cart_data = array();
			}

			$total = 0;
			if ( ! empty( $cart_data['items'] ) && is_array( $cart_data['items'] ) ) {
				foreach ( $cart_data['items'] as $item_data ) {
					$price    = isset( $item_data['price'] ) ? (float) $item_data['price'] : 0;
					$quantity = isset( $item_data['quantity'] ) ? (int) $item_data['quantity'] : 1;
					$total   += $price * $quantity;
				}
			}
		}

		if ( $total <= 0 ) {
			return '&mdash;';
		}

		return esc_html( edd_currency_filter( edd_format_amount( $total ), $item->currency ) );
	}

	/**
	 * Renders the Status column.
	 *
	 * @since 3.6.5
	 * @param Cart $item The cart item.
	 * @return string
	 */
	protected function column_status( $item ): string {
		$status_value = '';
		if ( $item instanceof Cart && isset( $item->status ) && is_scalar( $item->status ) ) {
			$status_value = (string) $item->status;
		}

		if ( empty( $status_value ) ) {
			$status_value = 'unknown';
		}

		$status_config = $this->get_status_config( $status_value );
		$status_badge  = new \EDD\Utils\StatusBadge( $status_config );

		return $status_badge->get();
	}

	/**
	 * Gets status configuration with icon and tooltip.
	 *
	 * @since 3.6.5
	 * @param string $status Status value.
	 * @return array Status configuration.
	 */
	private function get_status_config( string $status ): array {
		$configs = array(
			'tracking'     => array(
				'icon'    => 'visibility',
				'tooltip' => __( 'Cart is being tracked but not yet abandoned.', 'easy-digital-downloads' ),
				'status'  => 'info',
			),
			'abandoned'    => array(
				'icon'    => 'warning',
				'tooltip' => __( 'Cart has been abandoned.', 'easy-digital-downloads' ),
				'status'  => 'warning',
			),
			'converted'    => array(
				'icon'    => 'yes-alt',
				'tooltip' => __( 'Cart was converted to a purchase without being abandoned.', 'easy-digital-downloads' ),
				'status'  => 'success',
			),
			'recovered'    => array(
				'icon'    => 'yes-alt',
				'tooltip' => __( 'Cart was abandoned and then successfully recovered.', 'easy-digital-downloads' ),
				'status'  => 'success',
			),
			'expired'      => array(
				'icon'    => 'clock',
				'tooltip' => __( 'Cart has expired and is no longer being tracked.', 'easy-digital-downloads' ),
			),
			'unsubscribed' => array(
				'icon'    => 'no-alt',
				'tooltip' => __( 'Cart was unsubscribed from recovery emails.', 'easy-digital-downloads' ),
			),
			'unknown'      => array(
				'icon'    => 'editor-help',
				'tooltip' => __( 'Status is unknown.', 'easy-digital-downloads' ),
			),
		);

		$config          = isset( $configs[ $status ] ) ? $configs[ $status ] : $configs['unknown'];
		$config['label'] = $this->get_status_label( $status );

		return $config;
	}

	/**
	 * Renders the Emails Sent column.
	 *
	 * @since 3.6.5
	 * @param Cart $item The cart item.
	 * @return string
	 */
	protected function column_email_count( $item ): string {
		$count = (int) $item->recovery_email_count;

		return $count ? (string) $count : '0';
	}

	/**
	 * Renders the Last Email column.
	 *
	 * @since 3.6.5
	 * @param Cart $item The cart item.
	 * @return string
	 */
	protected function column_last_email( $item ): string {
		return $this->render_date_column( $item->last_recovery_email );
	}

	/**
	 * Renders a date column with a human-readable time difference.
	 *
	 * @since 3.6.5
	 * @param string|null $datetime MySQL datetime string.
	 * @return string Formatted date string.
	 */
	private function render_date_column( ?string $datetime ): string {
		if ( empty( $datetime ) || '0000-00-00 00:00:00' === $datetime ) {
			return '&mdash;';
		}

		$time = strtotime( $datetime );

		return sprintf(
			'<abbr title="%s">%s</abbr>',
			esc_attr( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $time ) ),
			esc_html( sprintf( '%s ago', human_time_diff( $time ) ) )
		);
	}

	/**
	 * Message when no items.
	 *
	 * @since 3.6.5
	 */
	public function no_items() {
		$has_filters = ! empty( $_GET['status'] ) || ! empty( $_GET['s'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$actions     = array(
			array(
				'type'   => 'primary',
				'label'  => __( 'Learn More', 'easy-digital-downloads' ),
				'url'    => edd_link_helper(
					'https://easydigitaldownloads.com/docs/abandoned-cart-recovery/',
					array(
						'utm_medium'  => 'cart-recovery',
						'utm_content' => 'documentation',
					),
					false
				),
				'target' => '_blank',
				'rel'    => 'noopener noreferrer',
			),
		);
		if ( $has_filters ) {
			$actions[] = array(
				'type'  => 'secondary',
				'label' => __( 'Clear Filters', 'easy-digital-downloads' ),
				'url'   => edd_get_admin_url(
					array(
						'page' => 'edd-cart-recovery',
						'tab'  => 'carts',
					)
				),
			);
		}
		$no_items = new NoItems(
			__( 'No Abandoned Carts Found', 'easy-digital-downloads' ),
			__( 'Great news! There are no abandoned carts at the moment. This means customers are completing their purchases successfully.', 'easy-digital-downloads' ),
			$actions
		);
		$no_items->render();
	}

	/**
	 * Retrieves all the data for abandoned carts.
	 *
	 * @since 3.6.5
	 * @return array Cart items data.
	 */
	public function get_data() {
		// Check if cart repository is available.
		if ( ! $this->cart_repo ) {
			return array();
		}

		// Get pagination arguments.
		$args = $this->parse_pagination_args();

		// Parse common filters (search, status, dates, values).
		$args = $this->parse_common_filters( $args );

		// Translate limit/offset to page/per_page for the repository.
		$merged_args = array_merge(
			$args,
			array(
				'page'     => isset( $args['offset'] ) ? ( $args['offset'] / $this->per_page ) + 1 : 1,
				'per_page' => $args['number'] ?? $this->per_page,
			)
		);

		$result = $this->cart_repo->get_carts_for_admin( $merged_args );

		$items = array();

		if ( ! empty( $result['items'] ) && is_array( $result['items'] ) ) {
			foreach ( $result['items'] as $row ) {
				if ( $row instanceof Cart ) {
					$items[] = $row;
				}
			}
		}

		// Set counts for pagination.
		$total_count  = isset( $result['total'] ) ? (int) $result['total'] : 0;
		$this->counts = array(
			'total' => $total_count,
		);

		$status = $this->get_status();
		if ( $status ) {
			$this->counts[ $status ] = $total_count;
		}

		// Pre-process data to eliminate expensive operations in rendering loops.
		if ( ! empty( $items ) ) {
			$this->items = $items;
			$this->pre_process_cart_data();
			$this->prime_customer_cache( $items );
		}

		return $items;
	}

	/**
	 * Parses common GET parameters used by table display and CSV export.
	 *
	 * @since 3.6.5
	 * @param array $base_args Base arguments to merge with parsed parameters.
	 * @return array Parsed arguments.
	 */
	private function parse_common_filters( array $base_args = array() ): array {
		// Search filter.
		$search = $this->get_search();
		if ( $search ) {
			$base_args['search'] = $search;
		}

		// Status filter - use status for cart table.
		$status           = $this->get_status();
		$allowed_statuses = array_keys( $this->get_cart_statuses() );
		if ( $status && in_array( $status, $allowed_statuses, true ) ) {
			$base_args['status'] = $status;
		}

		// Date range filters.
		$date_from = $this->get_request_var( 'start-date' );
		if ( $date_from && $this->validate_date( $date_from ) ) {
			$base_args['date_from'] = sanitize_text_field( $date_from );
		}

		$date_to = $this->get_request_var( 'end-date' );
		if ( $date_to && $this->validate_date( $date_to ) ) {
			$base_args['date_to'] = sanitize_text_field( $date_to );
		}

		// Cart value range filters.
		$value_min = $this->get_request_var( 'value_min' );
		if ( ! empty( $value_min ) && is_numeric( $value_min ) ) {
			$base_args['value_min'] = (float) $value_min;
		}

		$value_max = $this->get_request_var( 'value_max' );
		if ( ! empty( $value_max ) && is_numeric( $value_max ) ) {
			$base_args['value_max'] = (float) $value_max;
		}

		$cart_id = $this->get_request_var( 'cart_id' );
		if ( ! empty( $cart_id ) ) {
			$base_args['cart_id'] = (int) $cart_id;
		}

		return $base_args;
	}

	/**
	 * Validates a date string in Y-m-d format.
	 *
	 * @since 3.6.5
	 * @param string $date Date string to validate.
	 * @return bool True if valid, false otherwise.
	 */
	private function validate_date( string $date ): bool {
		$d = \DateTime::createFromFormat( 'Y-m-d', $date );
		return $d && $d->format( 'Y-m-d' ) === $date;
	}

	/**
	 * Prepares the items for the table.
	 *
	 * @since 3.6.5
	 */
	public function prepare_items() {
		$columns  = $this->get_columns();
		$hidden   = array();
		$sortable = $this->get_sortable_columns();
		$status   = $this->get_status( 'total' );

		$this->_column_headers = array( $columns, $hidden, $sortable );
		$this->items           = $this->get_data();

		if ( empty( $this->counts[ $status ] ) ) {
			return;
		}

		$this->set_pagination_args(
			array(
				'total_pages' => ceil( $this->counts[ $status ] / $this->per_page ),
				'total_items' => $this->counts[ $status ],
				'per_page'    => $this->per_page,
			)
		);
	}

	/**
	 * Gets summary statistics for the current filtered view.
	 *
	 * @since 3.6.5
	 * @return array Summary data with count, total_value, avg_value.
	 */
	public function get_summary_stats(): array {
		if ( ! $this->cart_repo ) {
			return array(
				'count'       => 0,
				'total_value' => 0,
				'avg_value'   => 0,
			);
		}

		// Calculate from current items.
		$count       = count( $this->items );
		$total_value = 0;

		foreach ( $this->items as $item ) {
			$cart_id = isset( $item->id ) ? (int) $item->id : 0;
			if ( $cart_id <= 0 ) {
				continue;
			}

			// Get cached cart data.
			$cart_data = wp_cache_get( $cart_id, self::CART_DATA_CACHE_GROUP );
			if ( false === $cart_data || ! is_array( $cart_data ) ) {
				continue;
			}

			$total_value += $item->cart_total;
		}

		$avg_value = $count > 0 ? $total_value / $count : 0;

		return array(
			'count'       => $count,
			'total_value' => $total_value,
			'avg_value'   => $avg_value,
		);
	}

	/**
	 * Displays the advanced filters bar matching EDD core pattern.
	 *
	 * @since 3.6.5
	 */
	public function advanced_filters() {
		// Use parent helper methods for getting request vars.
		$date_from = sanitize_text_field( $this->get_request_var( 'date_from', '' ) );
		$date_to   = sanitize_text_field( $this->get_request_var( 'date_to', '' ) );
		$value_min = $this->get_request_var( 'value_min', '' );
		$value_max = $this->get_request_var( 'value_max', '' );
		?>
		<div id="edd-filters" class="wp-filter">
			<div class="filter-items">
				<span id="edd-cart-status-filter">
					<?php $this->status_dropdown(); ?>
				</span>
				<span id="edd-date-filters" class="edd-from-to-wrapper">
					<?php

					echo EDD()->html->date_field(
						array(
							'id'          => 'start-date',
							'name'        => 'start-date',
							'placeholder' => _x( 'From', 'date filter', 'easy-digital-downloads' ),
							'value'       => $date_from,
						)
					);

					echo EDD()->html->date_field(
						array(
							'id'          => 'end-date',
							'name'        => 'end-date',
							'placeholder' => _x( 'To', 'date filter', 'easy-digital-downloads' ),
							'value'       => $date_to,
						)
					);

					?>
				</span>
				<span id="edd-cart-value-filters">
					<span>
						<label for="cart-value-min"><?php esc_html_e( 'Min Value:', 'easy-digital-downloads' ); ?></label>
						<input type="number" id="cart-value-min" name="value_min" value="<?php echo esc_attr( $value_min ); ?>"
								placeholder="0" min="0" step="0.01" />
					</span>
					<span>
						<label for="cart-value-max"><?php esc_html_e( 'Max Value:', 'easy-digital-downloads' ); ?></label>
						<input type="number" id="cart-value-max" name="value_max" value="<?php echo esc_attr( $value_max ); ?>"
								placeholder="<?php esc_attr_e( 'No limit', 'easy-digital-downloads' ); ?>" min="0" step="0.01" />
					</span>
				</span>
				<span id="edd-cart-after-core-filters">
					<input type="submit" class="button-secondary" value="<?php esc_attr_e( 'Apply', 'easy-digital-downloads' ); ?>"/>
				</span>
				<?php if ( ! empty( $date_from ) || ! empty( $date_to ) || ! empty( $value_min ) || ! empty( $value_max ) ) : ?>
					<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=download&page=edd-cart-recovery&tab=carts' ) ); ?>" class="button-secondary">
						<?php esc_html_e( 'Clear Filter', 'easy-digital-downloads' ); ?>
					</a>
				<?php endif; ?>
				<?php
				/**
				 * Fires at the end of the advanced filters row.
				 *
				 * @since 3.6.5
				 */
				do_action( 'edd_cart_recovery_advanced_filters_row' );
				?>
			</div>
			<?php $this->search_box( __( 'Search', 'easy-digital-downloads' ), 'edd-carts' ); ?>
		</div>
		<?php
	}

	/**
	 * Renders the search box for the cart list table.
	 *
	 * @since 3.6.5
	 * @param string $text The text for the search box.
	 * @param string $input_id The ID for the search box.
	 * @return void
	 */
	public function search_box( $text, $input_id ): void {
		if ( ! $this->get_search() && ! $this->has_items() ) {
			return;
		}

		$input_id = $input_id . '-search-input';
		?>
		<p class="search-form">
			<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo esc_html( $text ); ?>:</label>
			<input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" placeholder="<?php esc_html_e( 'Search carts...', 'easy-digital-downloads' ); ?>" value="<?php _admin_search_query(); ?>" />
		</p>
		<?php
	}

	/**
	 * Retrieve the view types.
	 *
	 * Override parent to hide the views/counts tabs.
	 *
	 * @since 3.6.5
	 * @return array Empty array to hide views.
	 */
	public function get_views() {
		return array();
	}

	/**
	 * Returns an array of statuses to be used in the status dropdown.
	 *
	 * @since 3.6.5
	 * @return array
	 */
	private function get_status_options(): array {
		return array_merge(
			array(
				'' => __( 'All', 'easy-digital-downloads' ),
			),
			$this->get_cart_statuses()
		);
	}

	/**
	 * Renders a status filter dropdown.
	 *
	 * @since 3.6.5
	 */
	private function status_dropdown(): void {
		$current = filter_input( INPUT_GET, 'status', FILTER_SANITIZE_SPECIAL_CHARS );
		$options = $this->get_status_options();

		if ( empty( $options ) ) {
			return;
		}
		?>
		<select name="status">
			<?php foreach ( $options as $value => $label ) : ?>
				<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $current, $value ); ?>>
					<?php echo esc_html( $label ); ?>
				</option>
			<?php endforeach; ?>
		</select>
		<?php
	}

	/**
	 * Returns an array of additional hidden fields for the list table form.
	 *
	 * @since 3.6.5
	 * @return array Additional hidden fields.
	 */
	private function get_hidden_fields(): array {
		return array(
			'post_type' => 'download',
			'page'      => 'edd-cart-recovery',
			'tab'       => 'carts',
		);
	}

	/**
	 * Display the list table with form wrapper.
	 * Overrides parent to wrap table in a form with hidden fields and filters.
	 *
	 * @since 3.6.5
	 */
	public function display() {
		?>
		<form method="get">
			<?php
			foreach ( $this->get_hidden_fields() as $name => $value ) {
				printf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $name ), esc_attr( $value ) );
			}

			$this->views();
			$this->advanced_filters();
			parent::display();
			?>
		</form>
		<?php
	}

	/**
	 * Generate the table navigation above or below the table
	 *
	 * Removes the referrer nonce from parent class.
	 *
	 * @since 3.6.5
	 * @param string $which Position of the tablenav: 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		if ( 'top' === $which ) {
			wp_nonce_field( 'bulk-' . $this->_args['plural'], '_wpnonce', false );
		}
		?>
		<div class="tablenav <?php echo esc_attr( $which ); ?>">
			<?php if ( $this->has_items() ) : ?>
				<div class="alignleft actions bulkactions">
					<?php $this->bulk_actions( $which ); ?>
				</div>
				<?php
			endif;

			$this->extra_tablenav( $which );
			$this->pagination( $which );
			?>

			<br class="clear" />
		</div>
		<?php
	}

	/**
	 * Prime the persistent customer cache for the given items using batch processing.
	 *
	 * @since 3.6.5
	 * @param mixed $items Items returned from the repository; expected to be an array of records.
	 */
	private function prime_customer_cache( $items ): void {
		if ( ! is_array( $items ) || empty( $items ) ) {
			return;
		}

		// Process items in chunks directly to avoid large ID arrays.
		$item_chunks      = array_chunk( $items, 20 );
		$total_cache_hits = 0;
		$total_customers  = 0;

		foreach ( $item_chunks as $chunk ) {
			$ids = array();
			foreach ( $chunk as $item ) {
				if ( empty( $item->customer_id ) ) {
					continue;
				}

				$customer_id = (int) $item->customer_id;
				if ( $customer_id > 0 ) {
					$ids[] = $customer_id;
				}
			}

			if ( ! empty( $ids ) ) {
				$chunk_stats       = $this->load_customer_batch_with_cache( $ids );
				$total_cache_hits += $chunk_stats['cache_hits'];
				$total_customers  += $chunk_stats['total'];
			}
		}

		// Log cache performance metrics.
		if ( $total_customers > 10 ) {
			$cache_hit_rate = $total_cache_hits / $total_customers * 100;
			edd_debug_log(
				sprintf(
					'[EDD ACR] Customer cache hit rate: %.1f%% (%d/%d customers from cache)',
					$cache_hit_rate,
					$total_cache_hits,
					$total_customers
				)
			);
		}
	}

	/**
	 * Load a single batch of customer data with persistent caching.
	 *
	 * @since 3.6.5
	 * @param array $batch Array of customer IDs to load.
	 * @return array Statistics about cache performance.
	 */
	private function load_customer_batch_with_cache( array $batch ): array {
		$uncached_ids = array();
		$cache_hits   = 0;

		// Check persistent cache for each customer ID.
		foreach ( $batch as $customer_id ) {
			$cached_customer = wp_cache_get( $customer_id, self::CUSTOMER_CACHE_GROUP );
			if ( false !== $cached_customer ) {
				++$cache_hits;
			} else {
				$uncached_ids[] = $customer_id;
			}
		}

		// Load only uncached customers from database.
		if ( ! empty( $uncached_ids ) ) {
			$this->load_customer_batch_from_database( $uncached_ids );
		}

		return array(
			'cache_hits' => $cache_hits,
			'total'      => count( $batch ),
		);
	}

	/**
	 * Load customer batch from database and store in persistent cache.
	 *
	 * @since 3.6.5
	 * @param array $batch Array of customer IDs to load from database.
	 */
	private function load_customer_batch_from_database( array $batch ): void {
		global $wpdb;

		if ( ! ( $wpdb instanceof \wpdb ) ) {
			edd_debug_log( '[EDD ACR] Customer cache skipped: database handler unavailable.' );
			return;
		}

		$table        = $wpdb->prefix . 'edd_customers';
		$placeholders = implode( ',', array_fill( 0, count( $batch ), '%d' ) );

		// Add LIMIT clause to prevent excessive resource usage.
		// Use splat operator to unpack array arguments for PHP 7.4 compatibility.
		$query = $wpdb->prepare( "SELECT id, name, email FROM {$table} WHERE id IN ({$placeholders}) LIMIT " . count( $batch ), ...$batch );

		// Prepare failure is extremely rare but would leave the cache empty; log and abort.
		if ( false === $query ) {
			edd_debug_log( '[EDD ACR] Customer cache prepare failed.' );
			return;
		}

		$results = $wpdb->get_results( $query );

		// Store in persistent cache to avoid N+1 lookups during row rendering.
		foreach ( (array) $results as $row ) {
			$cid = (int) $row->id;
			if ( $cid > 0 ) {
				$customer_data = array(
					'name'  => isset( $row->name ) ? (string) $row->name : '',
					'email' => isset( $row->email ) ? (string) $row->email : '',
				);
				// Store in persistent cache for future requests.
				wp_cache_set( $cid, $customer_data, self::CUSTOMER_CACHE_GROUP, 600 );
			}
		}
	}

	/**
	 * Resolves customer display name from cart data using persistent cache.
	 *
	 * @since 3.6.5
	 * @param Cart $item The cart item.
	 * @return string
	 */
	private function resolve_customer_name( $item ): string {
		$customer_id = isset( $item->customer_id ) ? (int) $item->customer_id : 0;

		// Check persistent cache for performance (WordPress Object Cache provides in-memory caching).
		if ( $customer_id ) {
			$cached_customer = wp_cache_get( $customer_id, self::CUSTOMER_CACHE_GROUP );
			if ( false !== $cached_customer && ! empty( $cached_customer['name'] ) ) {
				return esc_html( $cached_customer['name'] );
			}
		}

		// Use Cart object's method for consistent name resolution.
		$name = $item->get_customer_name()['full_name'];

		// Cache the result for future requests if we have a customer_id.
		if ( $customer_id && ! empty( $name ) ) {
			wp_cache_set(
				$customer_id,
				array(
					'name'  => $name,
					'email' => ! empty( $item->email ) ? (string) $item->email : '',
				),
				self::CUSTOMER_CACHE_GROUP,
				600
			);
		}

		return esc_html( $name ?? __( 'Guest', 'easy-digital-downloads' ) );
	}

	/**
	 * Gets bulk actions.
	 *
	 * @since 3.6.5
	 * @return array Bulk actions.
	 */
	public function get_bulk_actions(): array {
		return array(
			'mark_recovered' => __( 'Mark as Recovered', 'easy-digital-downloads' ),
			'delete'         => __( 'Delete', 'easy-digital-downloads' ),
		);
	}
}
