<?php
/**
 * Referrals Admin List Table
 *
 * @package     AffiliateWP
 * @subpackage  Admin/Referrals
 * @copyright   Copyright (c) 2014, Sandhills Development, LLC
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.9
 */

use AffWP\Admin\List_Table;

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) exit;

#[\AllowDynamicProperties]

/**
 * AffWP_Referrals_Table Class
 *
 * Renders the Affiliates table on the Affiliates page
 *
 * @since 1.0
 *
 * @see \AffWP\Admin\List_Table
 */
class AffWP_Referrals_Table extends List_Table {

	/**
	 * Default number of items to show per page
	 *
	 * @var int
	 * @since 1.0
	 */
	public $per_page = 30;

	/**
	 * Total number of referrals found
	 *
	 * @var int
	 * @since 1.0
	 */
	public $total_count;

	/**
	 * Number of paid referrals found
	 *
	 * @var int
	 * @since 1.0
	 */
	public $paid_count;

	/**
	 * Number of unpaid referrals found
	 *
	 * @var int
	 * @since 1.0
	 */
	public $unpaid_count;

	/**
	 * Number of pending referrals found
	 *
	 * @var int
	 * @since 1.0
	 */
	public $pending_count;

	/**
	 * Number of rejected referrals found
	 *
	 * @var int
	 * @since 1.0
	 */
	public $rejected_count;

	/**
	 * Number of failed referrals found.
	 *
	 * @var   int
	 * @since 2.8.1
	 */
	public $failed_count;

	/**
	 * Draft Count
	 *
	 * @since Unknown
	 */
	private $draft_count;

	/**
	 * Number of flagged referrals found.
	 *
	 * @var   int
	 * @since 2.31.0
	 */
	public $flagged_count;

	/**
	 * Get things started
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @see WP_List_Table::__construct()
	 *
	 * @param array $args Optional. Arbitrary display and query arguments to pass through
	 *                    the list table. Default empty array.
	 */
	public function __construct( $args = array() ) {
		$args = wp_parse_args( $args, array(
			'singular' => 'referral',
			'plural'   => 'referrals',
		) );

		parent::__construct( $args );

		$this->get_referral_counts();
	}

	/**
	 * Drops WP's `striped` class so rows render on a uniform white ground with
	 * hairline dividers + a hover tint (see {@see get_scroller_classes()}).
	 *
	 * @since 2.33.0
	 *
	 * @return array
	 */
	protected function get_table_classes() {
		return $this->get_unstriped_table_classes();
	}

	/**
	 * Generates content for a single row of the referrals table.
	 *
	 * The `group` class drives the hover-revealed action rail (`group-hover:`)
	 * and `data-edit-url` lets affwp-list-row-actions.js open the edit-referral
	 * screen on whole-row click.
	 *
	 * @since 2.9.6
	 * @since 2.33.0 Added the `group` class and `data-referral-id`/`data-edit-url`.
	 *
	 * @param \AffWP\Referral $referral The current referral object.
	 */
	public function single_row( $referral ) {
		/**
		 * Filters the CSS class for a single row of the referrals table.
		 *
		 * @since 2.9.6
		 *
		 * @param string          $class   CSS class.
		 * @param \AffWP\Referral $referral Referral object.
		 */
		$class = trim( 'group ' . apply_filters( 'affwp_referral_table_single_row_class', '', $referral ) );

		$referral_id = is_object( $referral ) ? (int) $referral->ID : 0;

		$edit_url = add_query_arg(
			array(
				'action'      => 'edit_referral',
				'referral_id' => $referral_id,
			),
			affwp_admin_url( 'referrals' )
		);

		printf(
			'<tr id="referral-%1$d" class="%2$s" data-referral-id="%1$d" data-edit-url="%3$s">',
			$referral_id,
			esc_attr( $class ),
			esc_url( $edit_url )
		);
		$this->single_row_columns( $referral );
		echo '</tr>';
	}

	/**
	 * Show the search field.
	 *
	 * Override parent to always show search box even when no items exist.
	 * This ensures search remains available when filtering returns no results.
	 *
	 * @access public
	 * @since 1.0
	 *
	 * @param string $text     Label for the search box.
	 * @param string $input_id ID of the search box.
	 *
	 * @return void
	 */
	public function search_box( $text, $input_id ) {
		$input_id = $input_id . '-search-input';

		if ( ! empty( $_REQUEST['orderby'] ) )
			echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
		if ( ! empty( $_REQUEST['order'] ) )
			echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
		?>
		<p class="search-box">
			<label class="screen-reader-text" for="<?php echo $input_id ?>"><?php echo $text; ?>:</label>
			<input type="search" id="<?php echo $input_id ?>" name="s" value="<?php _admin_search_query(); ?>" />
			<?php submit_button( $text, 'button', false, false, array( 'ID' => 'search-submit' ) ); ?>
		</p>
	<?php
	}

	/**
	 * Retrieve the view types
	 *
	 * @access public
	 * @since 1.0
	 * @return array $views All the views available
	 */
	public function get_views() {

		$affiliate_id    = isset( $_GET['affiliate_id'] ) ? absint( $_GET['affiliate_id'] ) : '';
		$base            = affwp_admin_url( 'referrals' );
		$base            = $affiliate_id ? add_query_arg( 'affiliate_id', $affiliate_id, $base ) : $base;
		$current         = isset( $_GET['status'] ) ? sanitize_key( $_GET['status'] ) : '';
		$is_flagged_view = ( isset( $_GET['flagged'] ) && '1' === $_GET['flagged'] )
			|| ( isset( $_REQUEST['flagged'] ) && '1' === $_REQUEST['flagged'] );
		$total_count     = '&nbsp;<span class="count">(' . $this->total_count    . ')</span>';
		$paid_count      = '&nbsp;<span class="count">(' . $this->paid_count . ')</span>';
		$unpaid_count    = '&nbsp;<span class="count">(' . $this->unpaid_count . ')</span>';
		$pending_count   = '&nbsp;<span class="count">(' . $this->pending_count . ')</span>';
		$rejected_count  = '&nbsp;<span class="count">(' . $this->rejected_count . ')</span>';

		$labels = affwp_get_referral_statuses( true );

		// When viewing flagged referrals, no status tab should be "current".
		$views = array(
			'all'      => sprintf( '<a href="%s"%s>%s</a>', esc_url( remove_query_arg( [ 'status', 'flagged', 'flag_type' ], $base ) ), ! $is_flagged_view && ( $current === 'all' || $current == '' ) ? ' class="current"' : '', __( 'All', 'affiliate-wp' ) . $total_count ),
			'paid'     => sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'paid', remove_query_arg( [ 'flagged', 'flag_type' ], $base ) ) ), ! $is_flagged_view && $current === 'paid' ? ' class="current"' : '', $labels['paid'] . $paid_count ),
			'unpaid'   => sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'unpaid', remove_query_arg( [ 'flagged', 'flag_type' ], $base ) ) ), ! $is_flagged_view && $current === 'unpaid' ? ' class="current"' : '', $labels['unpaid'] . $unpaid_count ),
			'pending'  => sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'pending', remove_query_arg( [ 'flagged', 'flag_type' ], $base ) ) ), ! $is_flagged_view && $current === 'pending' ? ' class="current"' : '', $labels['pending'] . $pending_count ),
			'rejected' => sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'rejected', remove_query_arg( [ 'flagged', 'flag_type' ], $base ) ) ), ! $is_flagged_view && $current === 'rejected' ? ' class="current"' : '', $labels['rejected'] . $rejected_count ),
		);

		// Only display the Failed view if currently filtering by that status.
		if ( isset( $_REQUEST['status'] ) && 'failed' === $_REQUEST['status'] ) {
			$failed_count   = '&nbsp;<span class="count">(' . $this->failed_count . ')</span>';

			$views['failed'] = sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'failed', remove_query_arg( [ 'flagged', 'flag_type' ], $base ) ) ), ! $is_flagged_view && $current === 'failed' ? ' class="current"' : '', $labels['failed'] . $failed_count );
		}

		// Add Flagged view - always show for discoverability.
		$flagged_count   = '&nbsp;<span class="count">(' . $this->flagged_count . ')</span>';
		$flagged_current = $is_flagged_view ? ' class="current"' : '';
		$flagged_url     = add_query_arg( 'flagged', '1', remove_query_arg( [ 'status', 'flag_type' ], $base ) );

		$views['flagged'] = sprintf(
			'<a href="%s"%s>%s</a>',
			esc_url( $flagged_url ),
			$flagged_current,
			__( 'Flagged', 'affiliate-wp' ) . $flagged_count
		);

		return $views;
	}

	/**
	 * Retrieve the table columns
	 *
	 * @access public
	 * @since 1.0
	 * @return array $columns Array of all the list table columns
	 */
	public function get_columns() {
		$columns = array(
			'cb'          => '<input type="checkbox" />',
			'referral_id' => __( 'Referral ID', 'affiliate-wp' ),
			'affiliate'   => __( 'Affiliate', 'affiliate-wp' ),
			'amount'      => __( 'Amount', 'affiliate-wp' ),
			'status'      => __( 'Status', 'affiliate-wp' ),
			'type'        => __( 'Type', 'affiliate-wp' ),
			'reference'   => __( 'Reference', 'affiliate-wp' ),
			'description' => __( 'Description', 'affiliate-wp' ),
			'date'        => __( 'Date', 'affiliate-wp' ),
			'actions'     => '<span class="screen-reader-text">' . esc_html__( 'Actions', 'affiliate-wp' ) . '</span>',
		);

		/**
		 * Filters the referrals list table columns.
		 *
		 * @since 1.0
		 *
		 * @param array                  $prepared_columns Prepared columns.
		 * @param array                  $columns          The columns for this list table.
		 * @param \AffWP_Referrals_Table $this             List table instance.
		 */
		return apply_filters( 'affwp_referral_table_columns', $this->prepare_columns( $columns ), $columns, $this );
	}

	/**
	 * Retrieve the table's sortable columns
	 *
	 * @access public
	 * @since 1.0
	 * @return array Array of all the sortable columns
	 */
	public function get_sortable_columns() {
		$columns = array(
			'referral_id' => array( 'referral_id', false ),
			'amount'      => array( 'amount', false ),
			'affiliate'   => array( 'affiliate_id', false ),
			'type'        => array( 'type', false ),
			'date'        => array( 'date', false ),
			'status'      => array( 'status', false ),
		);

		/**
		 * Filters the referrals list table sortable columns.
		 *
		 * @since 1.0
		 *
		 * @param array                  $columns          The sortable columns for this list table.
		 * @param \AffWP_Referrals_Table $this             List table instance.
		 */
		return apply_filters( 'affwp_referral_table_sortable_columns', $columns, $this );
	}

	/**
	 * This function renders most of the columns in the list table.
	 *
	 * @access public
	 * @since 1.0
	 *
	 * @param \AffWP\Referral $referral    Contains all the data of the affiliate
	 * @param string          $column_name The name of the column
	 *
	 * @return string Column Name
	 */
	public function column_default( $referral, $column_name ) {
		switch( $column_name ) {

			case 'date' :
				$value = $referral ? $referral->date_i18n() : '—';
				break;

			case 'description' :
				$value       = $referral ? wp_trim_words( $referral->description, 10 ) : '—';
				$description = $referral ? $referral->description : '—';

				/**
				 * Filters the referral description column data in the referrals list table.
				 *
				 * @since 1.0
				 *
				 * @param string $value       Data shown in the Description column.
				 * @param array  $description The referral description.
				 */
				$value = (string) apply_filters( 'affwp_referral_description_column', $value, $description );
				break;

			case 'type' :
				$value = $referral ? $referral->type() : '—';
				$type  = $referral ? $referral->type : '—';

				/**
				 * Filters the referral type column data in the referrals list table.
				 *
				 * @since 1.0
				 *
				 * @param string $value Data shown in the type column.
				 * @param array  $type  The referral type.
				 */
				$value = (string) apply_filters( 'affwp_referral_type_column', $value, $type );
				break;

			default:
				$value = isset( $referral->$column_name ) ? $referral->$column_name : '—';
				break;
		}

		/**
		 * Filters the default value for each column in the referrals list table.
		 *
		 * This dynamic filter is appended with a suffix of the column name, for example:
		 *
		 *     `affwp_referral_table_description`
		 *
		 * @since 1.0
		 *
		 * @param string $value    Column data to show.
		 * @param array  $referral Referral data.
		 *
		 */
		return apply_filters( 'affwp_referral_table_' . $column_name, $value, $referral );
	}

	/**
	 * Render the checkbox column
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Referral $referral Contains all the data for the checkbox column
	 * @return string Displays a checkbox
	 */
	public function column_cb( $referral ) {
		if ( ! $referral ) {
			return '';
		}

		return '<input type="checkbox" name="referral_id[]" value="' . absint( $referral->referral_id ) . '" />';
	}

	/**
	 * Renders the amount column.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Referral $referral Contains all the data for the checkbox column
	 * @return string Displays the referral amount
	 */
	public function column_amount( $referral ) {
		if ( ! $referral ) {
			return affwp_currency_filter( affwp_format_amount( 0.00 ) );
		}

		$value = sprintf(
			'<span class="block! min-[783px]:text-right! tabular-nums!">%s</span>',
			affwp_currency_filter( affwp_format_amount( $referral->amount ) )
		);

		/**
		 * The referral amount column data.
		 *
		 * @since 1.0
		 *
		 * @param string          $value    Data shown in the Amount column.
		 * @param \AffWP\Referral $referral The referral data.
		 */
		return apply_filters( 'affwp_referral_table_amount', $value, $referral );
	}

	/**
	 * Renders the status column.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Referral $referral Contains all the data for the checkbox column
	 * @return string Displays the referral status
	 */
	public function column_status( $referral ) {
		if ( ! $referral ) {
			return '—';
		}

		// Use the new referral status badge helper
		$value = affwp_get_referral_status_badge( $referral->status );

		/**
		 * Filters the referral status column data in the referrals list table.
		 *
		 * @since 1.0
		 *
		 * @param string          $value    Data shown in the Status column.
		 * @param \AffWP\Referral $referral The referral data.
		 */
		return apply_filters( 'affwp_referral_table_status', $value, $referral );
	}

	/**
	 * Renders the affiliate column.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Referral $referral Contains all the data for the checkbox column
	 * @return string The affiliate
	 */
	public function column_affiliate( $referral ) {
		if ( ! $referral ) {
			return '—';
		}

		$value = affwp_admin_link(
			'referrals',
			esc_html( affiliate_wp()->affiliates->get_affiliate_name( $referral->affiliate_id ) ),
			array( 'affiliate_id' => $referral->affiliate_id )
		);

		/**
		 * Filters the referring affiliate column data in the referrals list table.
		 *
		 * You'll also need to specify the wrapping html for this value (defaults to
		 * an anchor to the referral admin screen for this referral).
		 *
		 * @since 1.0
		 *
		 * @param string          $value    Data shown in the Affiliate column.
		 * @param \AffWP\Referral $referral The referral data.
		 */
		$value = apply_filters( 'affwp_referral_affiliate_column', $value, $referral );

		/**
		 * Filters the referring affiliate column data in the referrals list table.
		 *
		 * @since 1.0
		 *
		 * @param string          $value    Data shown in the Affiliate column.
		 * @param \AffWP\Referral $referral The referral data.
		 */
		return apply_filters( 'affwp_referral_table_affiliate', $value, $referral );
	}

	/**
	 * Renders the reference column.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Referral $referral Contains all the data for the checkbox column.
	 * @return string The reference.
	 */
	public function column_reference( $referral ) {
		if ( ! $referral ) {
			return '—';
		}

		/**
		 * Filters the referral reference column value in the referrals list table.
		 *
		 * @since 1.0
		 *
		 * @param string          $reference Data shown in the Reference column.
		 * @param \AffWP\Referral $referral  The current referral.
		 */
		$value = apply_filters( 'affwp_referral_reference_column', $referral->reference, $referral );

		/**
		 * Filters the referral reference column data in the referrals list table.
		 *
		 * @since 1.0
		 *
		 * @param string           $value    Data shown in the Reference column.
		 * @param \AffWP\Referral  $referral The referral data.
		 */
		return apply_filters( 'affwp_referral_table_reference', $value, $referral );
	}

	/**
	 * Render the actions column
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Referral $referral Contains all the data for the actions column.
	 * @return string The actions HTML.
	 */
	public function column_actions( $referral ) {
		if ( ! $referral ) {
			return '—';
		}

		$row_actions = array();

		$base_query_args = array(
			'referral_id' => $referral->ID
		);

		if( 'paid' == $referral->status ) {

			// Primary: View the payout this referral was paid in. Opens the payout drawer
			// in-context (the global drawer JS intercepts action=view_payout links).
			if ( ! empty( $referral->payout_id ) && current_user_can( 'manage_payouts' ) ) {
				$row_actions['view-payout'] = sprintf(
					'<a href="%1$s" class="view-payout">%2$s</a>',
					esc_url( add_query_arg( array( 'action' => 'view_payout', 'payout_id' => $referral->payout_id ), affwp_admin_url( 'payouts' ) ) ),
					esc_html__( 'View payout', 'affiliate-wp' )
				);
			}

			// Mark as Unpaid (reversal/correction) — demoted from the rail to the kebab.
			$row_actions['mark-as-unpaid'] = $this->get_row_action_link(
				__( 'Mark as Unpaid', 'affiliate-wp' ),
				array_merge( $base_query_args, array(
					'action' => 'mark_as_unpaid'
				) ),
				array(
					'nonce'    => 'referral-nonce',
					'class'    => 'mark-as-unpaid'
				)
			);

		} else {

			if( 'unpaid' == $referral->status && current_user_can( 'manage_payouts' ) ) {

				$payout_method  = affwp_get_affiliate_payout_method( $referral->affiliate_id );
				$can_pay_single = affwp_payout_method_supports_single_referral( $payout_method );

				if ( $can_pay_single ) {

					// Primary: Pay using the affiliate's effective payout method. Rendered as
					// the rail text action; JS opens the confirm modal. No-JS fallback opens
					// the referral so it can be paid there.
					$row_actions['pay'] = $this->get_row_action_link(
						__( 'Pay', 'affiliate-wp' ),
						array_merge( $base_query_args, array(
							'action' => 'edit_referral'
						) ),
						array(
							'base_uri' => affwp_admin_url( 'referrals' ),
							'class'    => 'pay'
						)
					);
				}

				// Reconciliation: record as paid (creates a manual payout; no money moves
				// through the store). Suppressed only when "Pay" already does exactly this
				// (Manual method), to avoid a duplicate.
				if ( ! $can_pay_single || 'manual' !== $payout_method ) {
					$row_actions['record-as-paid'] = $this->get_row_action_link(
						__( 'Record as paid', 'affiliate-wp' ),
						array_merge( $base_query_args, array(
							'action' => 'mark_as_paid'
						) ),
						array(
							'nonce'    => 'referral-nonce',
							'class'    => 'record-as-paid'
						)
					);
				}

			}

			if( 'rejected' == $referral->status || 'pending' == $referral->status ) {

				// Accept.
				$row_actions['accept'] = $this->get_row_action_link(
					__( 'Accept referral', 'affiliate-wp' ),
					array_merge( $base_query_args, array(
						'action' => 'accept'
					) ),
					array(
						'nonce'    => 'referral-nonce',
						'class'    => 'accept'
					)
				);

			}

			if( 'rejected' !== $referral->status && 'failed' !== $referral->status ) {

				// Reject.
				$row_actions['reject'] = $this->get_row_action_link(
					__( 'Reject referral', 'affiliate-wp' ),
					array_merge( $base_query_args, array(
						'action' => 'reject'
					) ),
					array(
						'nonce'    => 'referral-nonce',
						'class'    => 'reject'
					)
				);
			}

		}

		// Edit.
		$row_actions['edit'] = $this->get_row_action_link(
			__( 'Edit referral', 'affiliate-wp' ),
			array_merge( $base_query_args, array(
				'action' => 'edit_referral'
			) ),
			array(
				'base_uri' => affwp_admin_url( 'referrals' ),
				'class'    => 'edit'
			)
		);

		// Delete.
		$row_actions['delete'] = $this->get_row_action_link(
			__( 'Delete referral', 'affiliate-wp' ),
			array_merge( $base_query_args, array(
				'affwp_action' => 'process_delete_referral'
			) ),
			array(
				'base_uri' => affwp_admin_url( 'referrals' ),
				'nonce'    => 'affwp_delete_referral_nonce',
				'class'    => 'delete'
			)
		);
		$row_actions['delete'] = '<span class="trash">' . $row_actions['delete'] . '</span>';

		/**
		 * Filters the row actions array for the Referrals list table.
		 *
		 * Retained only for back-compat. Use {@see 'affwp_referral_row_actions'} instead.
		 *
		 * @since 1.2
		 *
		 * @param array           $row_actions Row actions array.
		 * @param \AffWP\Referral $referral    Current referral.
		 */
		$row_actions = apply_filters( 'affwp_referral_action_links', $row_actions, $referral );

		/**
		 * Filters the row actions array for the Referrals list table.
		 *
		 * @since 1.9
		 *
		 * @param array           $row_actions Row actions array.
		 * @param \AffWP\Referral $referral    Current referral.
		 */
		$row_actions = apply_filters( 'affwp_referral_row_actions', $row_actions, $referral );

		return $this->render_actions_cell( $referral, $row_actions );
	}

	/**
	 * Renders the Actions column as the payouts-style pill: a hover-revealed
	 * icon rail for the two high-frequency status toggles (Mark Paid / Mark
	 * Unpaid) plus a native-popover kebab holding every action as a text item.
	 *
	 * Mirrors AffWP_Payouts_Table::column_actions(). Rail icons are real nonced
	 * links (work without JS); affwp-list-row-actions.js upgrades them to
	 * in-place AJAX. The column's right-alignment and sticky behavior come from
	 * the arbitrary-variant utilities applied in {@see display()}.
	 *
	 * @since 2.33.0
	 *
	 * @param \AffWP\Referral $referral    Referral object.
	 * @param array           $row_actions Key => `<a>` markup, already filtered.
	 * @return string Actions cell HTML.
	 */
	protected function render_actions_cell( $referral, $row_actions ) {
		$referral_id = (int) $referral->ID;

		$inline_html = '';

		// Primary action: "Pay" as a text action in the hover rail (unpaid rows). JS
		// upgrades the click to the confirm modal + AJAX; the href is the no-JS fallback.
		if ( ! empty( $row_actions['pay'] ) && preg_match( '/href=(["\'])(.*?)\1/i', $row_actions['pay'], $m ) ) {
			$inline_html .= $this->render_rail_text_action(
				html_entity_decode( $m[2], ENT_QUOTES ),
				__( 'Pay', 'affiliate-wp' ),
				sprintf( ' data-affwp-pay="referral" data-affwp-id="%d"', $referral_id )
			);
		}

		// "Pay" lives only in the rail (bespoke modal behavior); keep it out of the kebab.
		unset( $row_actions['pay'] );

		// Accept (approve) — primary rail icon on pending/rejected rows. Reuses the in-place
		// AJAX status toggle (-> unpaid). Reject stays a kebab item (negative action; one-click
		// reject doesn't belong on the prominent rail).
		if ( ! empty( $row_actions['accept'] ) && preg_match( '/href=(["\'])(.*?)\1/i', $row_actions['accept'], $m ) ) {
			$inline_html .= $this->render_rail_action(
				html_entity_decode( $m[2], ENT_QUOTES ),
				'check',
				__( 'Accept referral', 'affiliate-wp' ),
				sprintf( ' data-affwp-ajax="referral" data-affwp-id="%d" data-affwp-status="%s"', $referral_id, 'unpaid' )
			);
		}
		unset( $row_actions['accept'] );

		// "View payout" is a secondary cross-reference (it jumps to a different record), so
		// it stays a text item in the kebab rather than a rail icon. The rail is reserved
		// for the row's primary action (Pay, on unpaid rows); the whole-row click owns
		// "view the referral". The global payout-drawer JS opens the payout in-context from
		// the menu link, so no rail treatment is needed here.

		return $this->render_row_actions_pill(
			'affwp-referral-actions-' . $referral_id,
			$inline_html,
			$row_actions,
			__( 'Delete this referral? This cannot be undone.', 'affiliate-wp' )
		);
	}

	/**
	 * Renders the message to be displayed when there are no referrals.
	 *
	 * @since 1.7.2
	 * @access public
	 */
	public function no_items() {
		_e( 'No referrals found.', 'affiliate-wp' );
	}

	/**
	 * Wraps only the <table> element in a horizontally-scrollable container
	 * so narrow viewports can pan instead of forcing column headers to wrap
	 * ("Referr / al", "Affil / iate").
	 *
	 * The tablenavs (bulk actions, pagination, filters) deliberately sit
	 * OUTSIDE the scroll container so they stay anchored to the viewport
	 * when the user scrolls the table horizontally. Inlines the structure
	 * from \WP_List_Table::display() rather than wrapping parent::display()
	 * for that reason.
	 *
	 * The `affwpScrollShadow` Alpine component on the outer wrapper toggles
	 * edge-fade gradients depending on scroll position, hinting that there
	 * is more content off-screen.
	 *
	 * @since 2.33.0
	 */
	public function display() {
		$singular = $this->_args['singular'];

		$this->display_tablenav( 'top' );

		$this->screen->render_screen_reader_content( 'heading_list' );

		/*
		 * Keep sortable column headers on one line. WP core renders the label
		 * and the `.sorting-indicators` arrows as stacked block/grid boxes, so
		 * `white-space:nowrap` can't hold them together — they wrap to a second
		 * line in narrow columns. Flex lays the label + arrows out inline; the
		 * column then claims its full single-line width (the table scrolls if
		 * needed). Emitted once per render via static guard.
		 */
		static $sortable_style_printed = false;
		if ( ! $sortable_style_printed ) {
			echo '<style>.affwp-rowactions-scroller th.sortable a,.affwp-rowactions-scroller th.sorted a{display:flex;align-items:center;gap:4px;}.affwp-rowactions-scroller th.column-amount a{justify-content:flex-end;}</style>';
			$sortable_style_printed = true;
		}

		?>
		<div x-data="affwpScrollShadow" class="relative clear-both">
			<div x-ref="scroller" class="affwp-rowactions-scroller <?php echo esc_attr( $this->get_scroller_classes( '', true ) ); ?>">
				<table class="wp-list-table <?php echo esc_attr( implode( ' ', $this->get_table_classes() ) ); ?>">
					<thead>
						<tr>
							<?php $this->print_column_headers(); ?>
						</tr>
					</thead>

					<tbody id="the-list"<?php echo $singular ? " data-wp-lists='list:" . esc_attr( $singular ) . "'" : ''; ?>>
						<?php $this->display_rows_or_placeholder(); ?>
					</tbody>

					<tfoot>
						<tr>
							<?php $this->print_column_headers( false ); ?>
						</tr>
					</tfoot>
				</table>
			</div>
			<?php $this->render_scroll_shadow_overlays(); ?>
		</div>
		<?php

		$this->render_pay_modal();

		// Bulk "Pay" opens the wide drawer (the shared Pay Affiliates preview body, scoped to
		// the selection), not a modal. Single-referral "Pay" still uses the lightweight modal above.
		if ( function_exists( 'affwp_render_bulk_pay_drawer' ) ) {
			affwp_render_bulk_pay_drawer();
		}

		// Payout detail drawer, opened in-context by the "View payout" action on paid
		// referrals. Render functions are arg-less singletons (guarded against double output).
		if ( function_exists( 'affwp_render_payout_drawer' ) ) {
			affwp_render_payout_drawer();
		}
		if ( function_exists( 'affwp_render_batch_drawer' ) ) {
			affwp_render_batch_drawer();
		}

		$this->display_tablenav( 'bottom' );
	}

	/**
	 * Renders the shared "Pay" confirm modal once per list view.
	 *
	 * Uses the Alpine modal store (focus trap / Escape / backdrop / ARIA). The rail
	 * "Pay" action opens it via affwp-list-row-actions.js, which fetches the
	 * method-resolved details (affwp_pay_referral_preview) into the store data and
	 * pays via affwp_pay_referral on confirm.
	 *
	 * @since 2.33.0
	 */
	protected function render_pay_modal() {

		affwp_render_pay_referral_modal();
	}

	/**
	 * Outputs the reporting views.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param string $which Optional. Whether the bulk actions are being displayed at
	 *                      the top or bottom of the list table. Accepts either 'top'
	 *                      or bottom. Default empty.
	 */
	public function bulk_actions( $which = '' ) {

		if ( is_null( $this->_actions ) ) {
			$no_new_actions = $this->_actions = $this->get_bulk_actions();
			$this->_actions = array_intersect_assoc( $this->_actions, $no_new_actions );
			$two = '';
		} else {
			$two = '2';
		}

		if ( empty( $this->_actions ) ) {
			return;
		}

		// Only allow bulk delete when filtered by failed status.
		if ( isset( $_GET['status'] ) && 'failed' === $_GET['status'] ) {
			$this->_actions = array( 'delete' => __( 'Delete', 'affiliate-wp' ) );
		}

		echo "<select name='action$two'>\n";
		echo "<option value='-1' selected='selected'>" . __( 'Bulk Actions', 'affiliate-wp' ) . "</option>\n";

		foreach ( $this->_actions as $name => $title ) {
			$class = 'edit' == $name ? ' class="hide-if-no-js"' : '';

			echo "\t<option value='$name'$class>$title</option>\n";
		}

		echo "</select>\n";

		/**
		 * Fires at the top and bottom of the referral bulk-actions admin screen
		 * (inside the form element).
		 *
		 * @since 1.0
		 *
		 * @param string $which Indicator for whether the bulk actions were rendered at the 'top'
		 *                      or 'bottom' of the referrals list table.
		 */
		do_action( 'affwp_referral_bulk_actions', $which );

		submit_button( __( 'Apply', 'affiliate-wp' ), 'action', false, false, array( 'id' => "doaction$two" ) );
		echo "\n";

		// Makes the filters only get output at the top of the page
		if( ! did_action( 'affwp_referral_filters' ) ) {
			$affiliate = isset( $_GET['affiliate_id'] ) ? $_GET['affiliate_id'] : false;

			if ( $affiliate && $affiliate = affwp_get_affiliate( $affiliate ) ) {
				$affiliate_name = affwp_get_affiliate_username( $affiliate );
			} else {
				$affiliate_name = '';
			}
			?>
			<span class="affwp-ajax-search-wrap">
				<input type="text" name="affiliate_id" id="user_name" class="affwp-user-search" value="<?php echo esc_attr( $affiliate_name ); ?>" data-affwp-status="any" autocomplete="off" placeholder="<?php _e( 'Affiliate name', 'affiliate-wp' ); ?>" />
			</span>
			<?php
			$from     = ! empty( $_REQUEST['filter_from'] ) ? $_REQUEST['filter_from'] : '';
			$to       = ! empty( $_REQUEST['filter_to'] )   ? $_REQUEST['filter_to']   : '';
			$set_type = ! empty( $_REQUEST['type'] )        ? $_REQUEST['type']        : '';

			echo "<input type='text' class='affwp-datepicker' autocomplete='off' name='filter_from' placeholder='" . __( 'From - mm/dd/yyyy', 'affiliate-wp' ) . "' value='" . esc_attr( $from ) . "'/>";
			echo "<input type='text' class='affwp-datepicker' autocomplete='off' name='filter_to' placeholder='" . __( 'To - mm/dd/yyyy', 'affiliate-wp' ) . "' value='" . esc_attr( $to ) . "'/>&nbsp;";

			?>
			<select name="type" class="affwp-referral-type-select">
				<option value=""><?php _e( 'All Types', 'affiliate-wp' ); ?></option>
				<?php foreach( affwp_get_referral_types() as $type_id => $type ) : ?>
					<option value="<?php echo esc_attr( $type_id ); ?>"<?php selected( $type_id, $set_type ); ?>><?php echo esc_html( $type['label'] ); ?></option>
				<?php endforeach; ?>
			</select>
			<?php
			// Show flag type dropdown only when viewing flagged referrals (progressive disclosure).
			// Check both GET and REQUEST to handle form submissions properly.
			$is_flagged_filter = ( isset( $_GET['flagged'] ) && '1' === $_GET['flagged'] )
				|| ( isset( $_REQUEST['flagged'] ) && '1' === $_REQUEST['flagged'] );

			if ( $is_flagged_filter ) :
				$set_flag_type = ! empty( $_REQUEST['flag_type'] ) ? sanitize_text_field( $_REQUEST['flag_type'] ) : '';
				$flag_types    = [
					'self_referral'   => __( 'Self-Referral', 'affiliate-wp' ),
					'referring_site'  => __( 'Referring Site', 'affiliate-wp' ),
					'conversion_rate' => __( 'Conversion Rate', 'affiliate-wp' ),
					'ppc_traffic'     => __( 'PPC Traffic', 'affiliate-wp' ),
				];
				?>
				<select name="flag_type" class="affwp-flag-type-select">
					<option value=""><?php _e( 'All Flag Types', 'affiliate-wp' ); ?></option>
					<?php foreach ( $flag_types as $flag_id => $flag_label ) : ?>
						<option value="<?php echo esc_attr( $flag_id ); ?>"<?php selected( $flag_id, $set_flag_type ); ?>><?php echo esc_html( $flag_label ); ?></option>
					<?php endforeach; ?>
				</select>
				<?php
				// Preserve the flagged parameter when form is submitted.
				echo '<input type="hidden" name="flagged" value="1" />';
			endif;

			/**
			 * Fires in the admin referrals screen, inside the search filters form area, prior to the submit button.
			 *
			 * @since 1.0
			 */
			do_action( 'affwp_referral_filters' );

			submit_button( __( 'Filter', 'affiliate-wp' ), 'action', false, false );
			echo "\n";

		}
	}

	/**
	 * Retrieves the bulk actions.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @return array $actions The array of bulk actions.
	 */
	public function get_bulk_actions() {
		$actions = array(
			'accept'         => __( 'Accept', 'affiliate-wp' ),
			'reject'         => __( 'Reject', 'affiliate-wp' ),
			'pay'            => __( 'Pay', 'affiliate-wp' ),
			'mark_as_paid'   => __( 'Record as paid', 'affiliate-wp' ),
			'mark_as_unpaid' => __( 'Mark as Unpaid', 'affiliate-wp' ),
			'delete'         => __( 'Delete', 'affiliate-wp' ),
		);

		if ( ! current_user_can( 'manage_payouts' ) ) {
			unset( $actions['mark_as_paid'] );
			unset( $actions['pay'] );
		}

		/**
		 * Filters the bulk actions array for the referrals list table.
		 *
		 * @since 1.0
		 *
		 * @param array $actions List of bulk actions.
		 */
		return apply_filters( 'affwp_referrals_bulk_actions', $actions );
	}

	/**
	 * Processes bulk actions for the referrals list table.
	 *
	 * @access public
	 * @since  1.0
	 * @since  2.27.4 Improved so status' are not change unless they are
	 *                different (https://github.com/awesomemotive/affiliate-wp/pull/5289/files).
	 */
	public function process_bulk_action() {

		if( empty( $_REQUEST['_wpnonce'] ) ) {
			return;
		}

		if( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-referrals' ) && ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'referral-nonce' ) ) {
			return;
		}

		$ids = isset( $_GET['referral_id'] ) ? $_GET['referral_id'] : array();

		if ( ! is_array( $ids ) ) {
			$ids = array( $ids );
		}

		$ids    = array_map( 'absint', $ids );
		$action = ! empty( $_REQUEST['action'] ) ? $_REQUEST['action'] : false;

		if( empty( $ids ) || empty( $action ) ) {
			return;
		}

		foreach ( $ids as $id ) {

			if ( 'delete' === $this->current_action() ) {
				affwp_delete_referral( $id );
			}

			if ( 'reject' === $this->current_action() ) {
				affwp_set_referral_status( $id, 'rejected' );
			}

			if ( 'accept' === $this->current_action() ) {
				affwp_set_referral_status( $id, 'unpaid' );
			}

			// Mark as paid...
			if ( 'mark_as_paid' === $this->current_action() ) {

				$referral = affwp_get_referral( $id );

				if (
					is_a( $referral, '\AffWP\Referral' )
						&& 'paid' !== $referral->status // Only create a payout if it's not already marked as paid.
				) {

					affwp_set_referral_status( $referral, 'paid' ); // Mark as paid.

					// Try and generate any payouts we need for it.
					affwp_add_payout(
						array(
							'affiliate_id'  => $referral->affiliate_id,
							'referrals'     => $id,
							'payout_method' => 'manual',
						)
					);
				}
			}

			// Mark as un-paid...
			if ( 'mark_as_unpaid' === $this->current_action() ) {
				$referral = affwp_get_referral( $id );

				if ( 'unpaid' !== $referral->status ) { // Only mark as unpaid if it's not already unpaid.
					affwp_set_referral_status( $id, 'unpaid' );
				}
			}

			/**
			 * Fires after a referral bulk action is performed.
			 *
			 * The dynamic portion of the hook name, `$this->current_action()` refers
			 * to the current bulk action being performed.
			 *
			 * @since 1.0
			 *
			 * @param int $id The ID of the object.
			 */
			do_action( 'affwp_referrals_do_bulk_action_' . $this->current_action(), $id );
		}

		// Refresh referral counts.
		$this->get_referral_counts();
	}

	/**
	 * Retrieves the discount code counts.
	 *
	 * @access public
	 * @since  1.0
	 */
	public function get_referral_counts() {

		$affiliate_id = isset( $_GET['affiliate_id'] ) ? $_GET['affiliate_id'] : '';

		if( is_array( $affiliate_id ) ) {
			$affiliate_id = array_map( 'absint', $affiliate_id );
		} else {
			$affiliate_id = absint( $affiliate_id );
		}

		$this->draft_count = affiliate_wp()->referrals->count(
			array_merge( $this->query_args, array(
				'affiliate_id' => $affiliate_id,
				'status'       => 'draft',
			) )
		);

		$this->paid_count = affiliate_wp()->referrals->count(
			array_merge( $this->query_args, array(
				'affiliate_id' => $affiliate_id,
				'status'       => 'paid',
			) )
		);

		$this->unpaid_count = affiliate_wp()->referrals->count(
			array_merge( $this->query_args, array(
				'affiliate_id' => $affiliate_id,
				'status'       => 'unpaid',
			) )
		);
		$this->pending_count = affiliate_wp()->referrals->count(
			array_merge( $this->query_args, array(
				'affiliate_id' => $affiliate_id,
				'status'       => 'pending',
			) )
		);

		$this->rejected_count = affiliate_wp()->referrals->count(
			array_merge( $this->query_args, array(
				'affiliate_id' => $affiliate_id,
				'status'       => 'rejected',
			) )
		);

		$this->failed_count = affiliate_wp()->referrals->count(
			array_merge( $this->query_args, array(
				'affiliate_id' => $affiliate_id,
				'status'       => 'failed',
			) )
		);

		// Count flagged referrals (pending with any flag type — excludes rejected).
		$this->flagged_count = affiliate_wp()->referrals->count(
			array_merge( $this->query_args, array(
				'affiliate_id' => $affiliate_id,
				'status'       => 'pending',
				'flag'         => [ 'self_referral', 'referring_site', 'conversion_rate', 'ppc_traffic' ],
			) )
		);

		// Failed referrals count is not included in the total by design.
		$this->total_count = $this->paid_count + $this->unpaid_count + $this->pending_count + $this->rejected_count;
	}

	/**
	 * Retrieve all the data for all the Affiliates
	 *
	 * @access public
	 * @since 1.0
	 * @return array $referrals_data Array of all the data for the Affiliates
	 */
	public function referrals_data() {

		$page        = isset( $_GET['paged'] )        ? absint( $_GET['paged'] ) : 1;
		$status      = isset( $_GET['status'] )       ? $_GET['status']          : '';
		$affiliate   = isset( $_GET['affiliate_id'] ) ? $_GET['affiliate_id']    : '';
		$reference   = isset( $_GET['reference'] )    ? $_GET['reference']       : '';
		$context     = isset( $_GET['context'] )      ? $_GET['context']         : '';
		$campaign    = isset( $_GET['campaign'] )     ? $_GET['campaign']        : '';
		$type        = isset( $_GET['type'] )         ? $_GET['type']            : '';
		$from        = isset( $_GET['filter_from'] )  ? $_GET['filter_from']     : '';
		$to          = isset( $_GET['filter_to'] )    ? $_GET['filter_to']       : '';
		$order       = isset( $_GET['order'] )        ? $_GET['order']           : 'DESC';
		$orderby     = isset( $_GET['orderby'] )      ? $_GET['orderby']         : 'referral_id';
		$flagged     = isset( $_REQUEST['flagged'] )   ? $_REQUEST['flagged']     : '';
		$flag_type   = isset( $_REQUEST['flag_type'] ) ? sanitize_text_field( $_REQUEST['flag_type'] ) : '';
		$referral    = '';
		$description = '';
		$is_search   = false;

		$amount = isset( $_GET['amount'] ) ? sanitize_text_field( $_GET['amount'] ) : 0;

		if ( $affiliate && $affiliate = affwp_get_affiliate( $affiliate ) ) {
			$affiliate = $affiliate->ID;
		} else {
			// Switch back to empty for the benefit of get_referrals().
			$affiliate = '';
		}

		$date = array();
		if( ! empty( $from ) ) {
			$date['start'] = $from;
		}
		if( ! empty( $to ) ) {
			$date['end'] = $to;
		}

		if( ! empty( $_GET['s'] ) ) {

			$is_search = true;

			$search = sanitize_text_field( $_GET['s'] );

			if( is_numeric( $search ) ) {
				// This is an referral ID search
				$referral = absint( $search );
			} elseif ( strpos( $search, 'ref:' ) !== false ) {
				$reference = trim( str_replace( 'ref:', '', $search ) );
			} elseif ( strpos( $search, 'context:' ) !== false ) {
				$context = trim( str_replace( 'context:', '', $search ) );
			} elseif ( strpos( $search, 'affiliate:' ) !== false ) {
				$affiliate = absint( trim( str_replace( 'affiliate:', '', $search ) ) );
			} elseif ( strpos( $search, 'campaign:' ) !== false ) {
				$campaign = trim( str_replace( 'campaign:', '', $search ) );
			} elseif ( strpos( $search, 'amount:' ) !== false ) {
				$amount = trim( str_replace( 'amount:', '', $search ) );
			} elseif ( strpos( $search, 'desc:' ) !== false ) {
				$description = trim( str_replace( 'desc:', '', $search ) );
			}

		}

		$per_page = $this->get_items_per_page( 'affwp_edit_referrals_per_page', $this->per_page );

		// Determine flag filter based on flagged view and flag_type dropdown.
		// Flagged = pending referrals with a flag (excludes rejected referrals).
		$flag_filter = '';
		if ( '1' === $flagged ) {
			$status = 'pending';

			// If filtering by flagged, use specific type or all flag types.
			if ( ! empty( $flag_type ) ) {
				$flag_filter = sanitize_text_field( $flag_type );
			} else {
				$flag_filter = [ 'self_referral', 'referring_site', 'conversion_rate', 'ppc_traffic' ];
			}
		}

		$args = wp_parse_args( $this->query_args, array(
			'number'       => $per_page,
			'offset'       => $per_page * ( $page - 1 ),
			'status'       => $status,
			'referral_id'  => $referral,
			'affiliate_id' => $affiliate,
			'reference'    => $reference,
			'context'      => $context,
			'campaign'     => $campaign,
			'type'         => $type,
			'amount'       => $amount,
			'description'  => $description,
			'date'         => $date,
			'flag'         => $flag_filter,
			'search'       => $is_search,
			'orderby'      => sanitize_text_field( $orderby ),
			'order'        => sanitize_text_field( $order )
		) );

		$referrals = affiliate_wp()->referrals->get_referrals( $args );

		// Retrieve the "current" total count for pagination purposes.
		$args['number']      = -1;
		$this->current_count = affiliate_wp()->referrals->count( $args );

		return $referrals;
	}

	/**
	 * Setup the final data for the table
	 *
	 * @access public
	 * @since 1.0
	 * @uses AffWP_Referrals_Table::get_columns()
	 * @uses AffWP_Referrals_Table::get_sortable_columns()
	 * @uses AffWP_Referrals_Table::process_bulk_action()
	 * @uses AffWP_Referrals_Table::referrals_data()
	 * @uses WP_List_Table::get_pagenum()
	 * @uses WP_List_Table::set_pagination_args()
	 * @return void
	 */
	public function prepare_items() {
		$per_page = $this->get_items_per_page( 'affwp_edit_referrals_per_page', $this->per_page );

		$this->get_column_info();

		$this->process_bulk_action();

		$data = $this->referrals_data();

		$status  = isset( $_GET['status'] ) ? sanitize_key( $_GET['status'] ) : 'any';
		$flagged = ( isset( $_GET['flagged'] ) && '1' === $_GET['flagged'] )
			|| ( isset( $_REQUEST['flagged'] ) && '1' === $_REQUEST['flagged'] );

		// If filtering by flagged, use current_count (which respects the flag filter).
		if ( $flagged ) {
			$total_items = $this->current_count;
		} else {
			switch( $status ) {
				case 'draft':
					$total_items = $this->draft_count;
					break;
				case 'paid':
					$total_items = $this->paid_count;
					break;
				case 'pending':
					$total_items = $this->pending_count;
					break;
				case 'unpaid':
					$total_items = $this->unpaid_count;
					break;
				case 'rejected':
					$total_items = $this->rejected_count;
					break;
				case 'failed':
					$total_items = $this->failed_count;
					break;
				case 'any':
					$total_items = $this->current_count;
					break;
			}
		}

		$this->items = $data;

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

	/**
	 * Display the table navigation.
	 *
	 * Override parent to always show bulk actions/filters even when no items exist.
	 * This ensures filter dropdowns remain visible when filtering returns no results.
	 *
	 * @since 2.31.0
	 *
	 * @param string $which Either 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		if ( 'top' === $which ) {
			wp_nonce_field( 'bulk-' . $this->_args['plural'] );
		}

		// When no pages exist, .tablenav-pages.no-pages is hidden and loses its 9px bottom margin.
		// Add equivalent margin to tablenav to maintain consistent spacing.
		$total_pages = $this->_pagination_args['total_pages'] ?? 0;
		$extra_style = ( 'top' === $which && $total_pages < 1 ) ? ' style="margin-bottom: 13px;"' : '';
		?>
	<div class="tablenav <?php echo esc_attr( $which ); ?>"<?php echo $extra_style; ?>>

		<div class="alignleft actions bulkactions">
			<?php $this->bulk_actions( $which ); ?>
		</div>
		<?php
		$this->extra_tablenav( $which );
		$this->pagination( $which );
		?>

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