<?php
/**
 * Affiliates Admin List Table
 *
 * @package     AffiliateWP
 * @subpackage  Admin/Affiliates
 * @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;

use function AffiliateWP\Functions\Affiliates\get_pending_affiliate_count;
use function AffiliateWP\Functions\Affiliates\get_next_pending_affiliate_id;

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

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

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

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

	/**
	 * Number of active affiliates found
	 *
	 * @var string
	 * @since 1.0
	 */
	public $active_count;

	/**
	 *  Number of inactive affiliates found
	 *
	 * @var string
	 * @since 1.0
	 */
	public $inactive_count;

	/**
	 * Number of pending affiliates found
	 *
	 * @var string
	 * @since 1.0
	 */
	public $pending_count;

	/**
	 * Number of rejected affiliates found
	 *
	 * @var string
	 * @since 1.0
	 */
	public $rejected_count;

	/**
	 * Number of flagged affiliates found (IP velocity).
	 *
	 * @var int
	 * @since 2.31.0
	 */
	public $flagged_count = 0;

	/**
	 * 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 = [] ) {
		$args = wp_parse_args(
			$args,
			[
				'singular' => 'affiliate',
				'plural'   => 'affiliates',
			]
		);

		parent::__construct( $args );

		$this->get_affiliate_counts();

		$this->load_review_with_ai_functionality();
	}

	/**
	 * Show the search field
	 *
	 * @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 ) {
		if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
			return;
		}

		$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, [ '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() {
		$base = affwp_admin_url( 'affiliates' );

		$current         = isset( $_GET['status'] ) ? $_GET['status'] : '';
		$is_flagged_view = isset( $_GET['flagged'] ) && '1' === $_GET['flagged'];
		$total_count     = '&nbsp;<span class="count">(' . $this->total_count . ')</span>';
		$active_count    = '&nbsp;<span class="count">(' . $this->active_count . ')</span>';
		$inactive_count  = '&nbsp;<span class="count">(' . $this->inactive_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_affiliate_statuses();

		// When viewing flagged affiliates, no status tab should be "current".
		$views = [
			'all'      => sprintf( '<a href="%s"%s>%s</a>', esc_url( remove_query_arg( [ 'status', 'flagged' ], $base ) ), ! $is_flagged_view && ( $current === 'all' || $current == '' ) ? ' class="current"' : '', __( 'All', 'affiliate-wp' ) . $total_count ),
			'active'   => sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'active', remove_query_arg( 'flagged', $base ) ) ), ! $is_flagged_view && $current === 'active' ? ' class="current"' : '', $labels['active'] . $active_count ),
			'inactive' => sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'inactive', remove_query_arg( 'flagged', $base ) ) ), ! $is_flagged_view && $current === 'inactive' ? ' class="current"' : '', $labels['inactive'] . $inactive_count ),
			'pending'  => sprintf( '<a href="%s"%s>%s</a>', esc_url( add_query_arg( 'status', 'pending', remove_query_arg( 'flagged', $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', $base ) ) ), ! $is_flagged_view && $current === 'rejected' ? ' class="current"' : '', $labels['rejected'] . $rejected_count ),
		];

		// Add Flagged view.
		$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', $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 = [
			'cb'              => '<input type="checkbox" />',
			'name'            => __( 'Name', 'affiliate-wp' ),
			'affiliate_id'    => __( 'Affiliate ID', 'affiliate-wp' ),
			'username'        => __( 'Username', 'affiliate-wp' ),
			'earnings'        => __( 'Paid Earnings', 'affiliate-wp' ),
			'unpaid_earnings' => __( 'Unpaid Earnings', 'affiliate-wp' ),
			'rate'            => __( 'Rate', 'affiliate-wp' ),
			'unpaid'          => __( 'Unpaid Referrals', 'affiliate-wp' ),
			'paid'            => __( 'Paid Referrals', 'affiliate-wp' ),
			'visits'          => __( 'Visits', 'affiliate-wp' ),
			'kyc_status'      => __( 'Identity Verification', 'affiliate-wp' ),
			'status'          => __( 'Status', 'affiliate-wp' ),
		];

		if ( ! affiliate_wp()->settings->get( 'enable_payouts_service' ) ) {
			unset( $columns['kyc_status'] );
		}

		/**
		 * Filters the affiliate list table columns.
		 *
		 * @since 1.0
		 *
		 * @param array                   $prepared_columns Prepared columns.
		 * @param array                   $columns          The columns for this list table.
		 * @param \AffWP_Affiliates_Table $this             List table instance.
		 */
		return apply_filters( 'affwp_affiliate_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 = [
			'username'        => [ 'username', false ],
			'name'            => [ 'name', false ],
			'affiliate_id'    => [ 'affiliate_id', false ],
			'earnings'        => [ 'earnings', false ],
			'unpaid_earnings' => [ 'unpaid_earnings', false ],
			'rate'            => [ 'rate', false ],
			'unpaid'          => [ 'unpaid', false ],
			'paid'            => [ 'paid', false ],
			'visits'          => [ 'visits', false ],
			'status'          => [ 'status', false ],
		];

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

	/**
	 * This function renders most of the columns in the list table.
	 *
	 * @access public
	 * @since 1.0
	 *
	 * @param \AffWP\Affiliate $affiliate   The current affiliate object.
	 * @param string           $column_name The name of the column
	 * @return string The column value.
	 */
	function column_default( $affiliate, $column_name ) {
		switch ( $column_name ) {

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

		/**
		 * Filters the default value for each affiliates list table column.
		 *
		 * This dynamic filter is appended with a suffix of the column name, for example:
		 *
		 *     `affwp_affiliate_table_referrals`
		 *
		 * @param string           $value     The column data.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object
		 */
		return apply_filters( 'affwp_affiliate_table_' . $column_name, $value, $affiliate );
	}

	/**
	 * Renders the "Name" column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Data shown in the Name column.
	 */
	public function column_name( $affiliate ) {

		$name = affiliate_wp()->affiliates->get_affiliate_name( $affiliate->affiliate_id );

		$base_query_args = [
			'page'         => 'affiliate-wp-affiliates',
			'affiliate_id' => $affiliate->ID,
		];

		$username = affwp_get_affiliate_username( $affiliate );

		$row_actions = array_merge(
			[
				'reports' => $this->get_row_action_link(
					__( 'Reports', 'affiliate-wp' ),
					[
						'page'            => 'affiliate-wp-reports',
						'tab'             => 'referral',
						'affiliate_login' => $username ? $username : __( '(user deleted)', 'affiliate-wp' ),
						'range'           => 'this_month',
					],
				),
			],
			affiliate_wp()->affiliates->get_affiliate_name( $affiliate->affiliate_id )
				? [
					'edit_user' => $this->get_row_action_link(
						__( 'Edit User', 'affiliate-wp' ),
						[],
						[ 'base_uri' => get_edit_user_link( $affiliate->user_id ) ]
					),
				]
				: [],
		);

		if ( strtolower( $affiliate->status ) === 'active' ) {

			$row_actions = array_merge(
				$row_actions,
				[
					'deactivate' => $this->get_row_action_link(
						__( 'Deactivate', 'affiliate-wp' ),
						array_merge(
							$base_query_args,
							[
								'affwp_notice' => 'affiliate_deactivated',
								'action'       => 'deactivate',
							]
						),
						[ 'nonce' => 'affiliate-nonce' ]
					),
				],
			);

		} elseif ( strtolower( $affiliate->status ) === 'pending' ) {

			$row_actions = array_merge(
				$row_actions,
				[
					'review' => $this->get_row_action_link(
						__( 'Review', 'affiliate-wp' ),
						array_merge(
							$base_query_args,
							[
								'affwp_notice' => false,
								'action'       => 'review_affiliate',
							],
						),
						[ 'nonce' => 'affiliate-nonce' ]
					),
					'accept' => $this->get_row_action_link(
						__( 'Accept', 'affiliate-wp' ),
						array_merge(
							$base_query_args,
							[
								'affwp_notice' => 'affiliate_accepted',
								'action'       => 'accept',
							],
						),
						[ 'nonce' => 'affiliate-nonce' ]
					),
					'reject' => $this->get_row_action_link(
						__( 'Reject', 'affiliate-wp' ),
						array_merge(
							$base_query_args,
							[
								'affwp_notice' => 'affiliate_rejected',
								'action'       => 'reject',
							],
						),
						[ 'nonce' => 'affiliate-nonce' ]
					),
				],
			);

		} else {

			$row_actions = array_merge(
				$row_actions,
				[
					'activate' => $this->get_row_action_link(
						__( 'Activate', 'affiliate-wp' ),
						array_merge(
							$base_query_args,
							[
								'affwp_notice' => 'affiliate_activated',
								'action'       => 'activate',
							],
						),
						[ 'nonce' => 'affiliate-nonce' ]
					),
				]
			);
		}

		$row_actions = array_merge(
			$row_actions,
			[
				'delete' => $this->get_row_action_link(
					__( 'Delete', 'affiliate-wp' ),
					array_merge(
						$base_query_args,
						[
							'affwp_notice' => false,
							'action'       => 'delete',
						],
					),
					[ 'nonce' => 'affiliate-nonce' ]
				),
			]
		);

		/**
		 * Filters the name column data for the affiliates list table.
		 *
		 * @since 1.0
		 *
		 * @param string           $value     Data shown in the Name column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters(
			'affwp_affiliate_table_name',
			sprintf(
				'
					<div class="info">
						<span class="gravatar">%1$s</span>
						<a class="name" href="%2$s">%3$s</a>
					</div>
					%4$s
				',
				get_avatar( $affiliate->user_id ?? 0, 32, '', $name ),
				esc_url(
					add_query_arg(
						array_merge(
							[
								'page'         => 'affiliate-wp-affiliates',
								'affiliate_id' => $affiliate->ID,
							],
							[
								'affwp_notice' => false,
								'action'       => 'edit_affiliate',
							]
						),
						admin_url( 'admin.php' )
					)
				),
				$name
					? $name
					: __( '(user deleted)', 'affiliate-wp' ),
				// Row Actions.
				sprintf(
					'<div class="row-actions">%1$s</div',
					$this->row_actions(
						/**
						 * Filters the row actions array for the Affiliates list table.
						 *
						 * @since 1.0
						 *
						 * @param array            $row_actions Row actions array.
						 * @param \AffWP\Affiliate $affiliate   Current affiliate.
						 */
						apply_filters( 'affwp_affiliate_row_actions', $row_actions, $affiliate ),
						true
					)
				)
			),
			$affiliate
		);
	}

	/**
	 * Renders the "Username" column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.8
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Data shown in the Username column.
	 */
	public function column_username( $affiliate ) {

		$row_actions = [];

		$username = affwp_get_affiliate_username( $affiliate );

		/**
		 * Filters the username column data for the affiliates list table.
		 *
		 * @since 1.8
		 *
		 * @param string           $username     Data shown in the Username column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters(
			'affwp_affiliate_table_username',
			sprintf(
				'<span class="username">%1$s</span>',
				$username ? $username : __( '(user deleted)', 'affiliate-wp' )
			),
			$affiliate
		);
	}

	/**
	 * Renders the checkbox column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Displays a checkbox.
	 */
	function column_cb( $affiliate ) {
		return '<input type="checkbox" name="affiliate_id[]" value="' . absint( $affiliate->affiliate_id ) . '" />';
	}

	/**
	 * Renders the earnings column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Affiliate paid earnings link.
	 */
	function column_earnings( $affiliate ) {
		$value = affwp_get_affiliate_earnings( $affiliate->affiliate_id, true );

		/**
		 * Filters the earnings column data for the affiliates list table.
		 *
		 * @since 1.0
		 *
		 * @param string           $value     Data shown in the Earnings column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters( 'affwp_affiliate_table_earnings', $value, $affiliate );
	}

	/**
	 * Renders the Unpaid Earnings column in the affiliates list table.
	 *
	 * @access public
	 * @since  2.0
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Affiliate paid earnings link.
	 */
	function column_unpaid_earnings( $affiliate ) {
		$value = affwp_get_affiliate_unpaid_earnings( $affiliate, true );

		/**
		 * Filters the Unpaid Earnings column data for the affiliates list table.
		 *
		 * @since 2.0
		 *
		 * @param string           $value     Data shown in the Unpaid Earnings column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters( 'affwp_affiliate_table_unpaid_earnings', $value, $affiliate );
	}

	/**
	 * Renders the "Rate" column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Affiliate rate.
	 */
	function column_rate( $affiliate ) {
		$value = affwp_get_affiliate_rate( $affiliate->affiliate_id, true );

		/**
		 * Filters the rate column data for the affiliates list table.
		 *
		 * @since 1.0
		 *
		 * @param string           $value     Data shown in the Rate column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters( 'affwp_affiliate_table_rate', $value, $affiliate );
	}


	/**
	 * Renders the unpaid referrals column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.7.5
	 *
	 * @param \AffWP\Affiliate $affiliate   The current affiliate object.
	 * @return string The unpaid referrals link.
	 */
	function column_unpaid( $affiliate ) {
		$unpaid_count = affiliate_wp()->referrals->unpaid_count( '', $affiliate->affiliate_id );

		$value = affwp_admin_link( 'referrals', $unpaid_count, [ 'affiliate_id' => $affiliate->affiliate_id, 'status' => 'unpaid' ] );

		/**
		 * Filters the unpaid referrals column data for the affiliates list table.
		 *
		 * @since 1.7.5
		 *
		 * @param string           $value     Data shown in the Unpaid column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters( 'affwp_affiliate_table_unpaid', $value, $affiliate );
	}


	/**
	 * Renders the paid referrals column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.0
	 * @since 2.9.5 Renamed to 'column_paid' from 'column_referrals'.
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string The affiliate referrals link.
	 */
	function column_paid( $affiliate ) {
		$paid_count = affiliate_wp()->referrals->paid_count( '', $affiliate->affiliate_id );

		$value = affwp_admin_link( 'referrals', $paid_count, [ 'affiliate_id' => $affiliate->affiliate_id, 'status' => 'paid' ] );

		/**
		 * Filters the paid referrals column data for the affiliates list table.
		 *
		 * @since 1.0 as 'affwp_affiliate_table_referrals'
		 * @since 2.9.5 Renamed to 'affwp_affiliate_table_paid'
		 *
		 * @param string $value     Data shown in the Paid Referrals column.
		 * @param array  $affiliate Contains all the data of the affiliate.
		 */
		return apply_filters( 'affwp_affiliate_table_paid', $value, $affiliate );
	}

	/**
	 * Renders the visits column in the affiliates list table.
	 *
	 * @access public
	 * @since  1.0
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string visits link
	 */
	function column_visits( $affiliate ) {
		$value = affwp_admin_link( 'visits', affwp_get_affiliate_visit_count( $affiliate->affiliate_id ), [ 'affiliate' => $affiliate->affiliate_id ] );

		/**
		 * Filters the username visits data for the affiliates list table.
		 *
		 * @since 1.0
		 *
		 * @param string           $value     Data shown in the Visits column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters( 'affwp_affiliate_table_visits', $value, $affiliate );
	}

	/**
	 * Renders the identity verification column in the affiliates list table.
	 *
	 * @since 2.4
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Visits link.
	 */
	public function column_kyc_status( $affiliate ) {

		$value                        = '';
		$badge_html                   = '';
		$payouts_service_account_meta = affwp_get_affiliate_meta( $affiliate->ID, 'payouts_service_account', true );

		if ( $payouts_service_account_meta && isset( $payouts_service_account_meta['kyc_status'] ) ) {
			$value = $payouts_service_account_meta['kyc_status'];

			// Only show a red badge when verification is required
			if ( 'required' === strtolower( $value ) ) {
				$badge_html = affwp_get_badge(
					[
						'text'    => __( 'Required', 'affiliate-wp' ),
						'variant' => 'danger',
						'size'    => 'xs',
					]
				);
			}
		}

		/**
		 * Filters the identity verification data for the affiliates list table.
		 *
		 * @since 2.4
		 *
		 * @param string           $value     Data shown in the Visits column.
		 * @param \AffWP\Affiliate $affiliate The current affiliate object.
		 */
		return apply_filters( 'affwp_affiliate_table_kyc_status', ! empty( $badge_html ) ? $badge_html : $value, $affiliate );
	}

	/**
	 * Renders the Status column in the affiliates list table.
	 *
	 * @since 2.7
	 * @since 2.16.0 Added html to color-code the status label.
	 *
	 * @param \AffWP\Affiliate $affiliate The current affiliate object.
	 * @return string Status label.
	 */
	public function column_status( $affiliate ) {
		return affwp_get_affiliate_status_badge( $affiliate->status );
	}

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

	/**
	 * Retrieve the bulk actions
	 *
	 * @access public
	 * @since 1.0
	 * @return array $actions Array of the bulk actions
	 */
	public function get_bulk_actions() {
		$actions = [
			'accept'     => __( 'Accept', 'affiliate-wp' ),
			'reject'     => __( 'Reject', 'affiliate-wp' ),
			'activate'   => __( 'Activate', 'affiliate-wp' ),
			'deactivate' => __( 'Deactivate', 'affiliate-wp' ),
			'delete'     => __( 'Delete', 'affiliate-wp' ),
		];

		/**
		 * Filters the bulk actions to return in the affiliates list table.
		 *
		 * @since 1.0
		 *
		 * @param array $actions Bulk actions.
		 */
		return apply_filters( 'affwp_affiliates_bulk_actions', $actions );
	}

	/**
	 * Process the bulk actions
	 *
	 * @access public
	 * @since 1.0
	 * @return void
	 */
	public function process_bulk_action() {

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

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

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

		if ( ! is_array( $ids ) ) {
			$ids = [ $ids ];
		}

		$ids = array_map( 'absint', $ids );

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

		if ( 'delete' === $this->current_action() ) {

			$redirect = affwp_admin_url(
				'affiliates',
				[
					'action'                        => 'delete',
					'affiliate_id'                  => $ids,
					'affwp_delete_affiliates_nonce' => wp_create_nonce( 'affwp_delete_affiliates_nonce' ),
				]
			);

			wp_safe_redirect( $redirect );
			exit;
		}

		$dynamic_coupons_enabled = affiliate_wp()->settings->get( 'dynamic_coupons' );

		foreach ( $ids as $id ) {

			if ( $dynamic_coupons_enabled && in_array( $this->current_action(), [ 'accept', 'activate' ] ) ) {
				$coupons = affwp_get_dynamic_affiliate_coupons( $id, false );

				if ( empty( $coupons ) ) {
					$coupon_added = affiliate_wp()->affiliates->coupons->add( [ 'affiliate_id' => $id ] );

					if ( false === $coupon_added ) {
						affiliate_wp()->utils->log( sprintf( 'Coupon could not be added for affiliate #%1$d.', $id ) );
					}
				}
			}

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

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

			if ( 'activate' === $this->current_action() ) {
				affwp_set_affiliate_status( $id, 'active' );
			}

			if ( 'deactivate' === $this->current_action() ) {
				affwp_set_affiliate_status( $id, 'inactive' );
			}

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

		}
	}

	/**
	 * Retrieve the discount code counts
	 *
	 * @access public
	 * @since 1.0
	 * @return void
	 */
	public function get_affiliate_counts() {

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

		$this->active_count = affiliate_wp()->affiliates->count(
			array_merge(
				$this->query_args,
				[
					'status' => 'active',
					'search' => $search,
				]
			)
		);

		$this->inactive_count = affiliate_wp()->affiliates->count(
			array_merge(
				$this->query_args,
				[
					'status' => 'inactive',
					'search' => $search,
				]
			)
		);

		$this->pending_count = affiliate_wp()->affiliates->count(
			array_merge(
				$this->query_args,
				[
					'status' => 'pending',
					'search' => $search,
				]
			)
		);

		$this->rejected_count = affiliate_wp()->affiliates->count(
			array_merge(
				$this->query_args,
				[
					'status' => 'rejected',
					'search' => $search,
				]
			)
		);

		$this->total_count = $this->active_count + $this->inactive_count + $this->pending_count + $this->rejected_count;

		// Count affiliates flagged by IP velocity (stored as affiliate meta).
		global $wpdb;

		$meta_table       = affiliate_wp()->affiliate_meta->table_name;
		$affiliates_table = affiliate_wp()->affiliates->table_name;

		$this->flagged_count = (int) $wpdb->get_var(
			"SELECT COUNT(DISTINCT a.affiliate_id)
			FROM {$affiliates_table} a
			INNER JOIN {$meta_table} am ON a.affiliate_id = am.affiliate_id
			WHERE am.meta_key = 'ip_velocity_flag'
			AND am.meta_value != ''"
		);
	}

	/**
	 * Retrieve all the data for all the Affiliates
	 *
	 * @access public
	 * @since 1.0
	 * @since 2.24.1 Improved for additional security (see comments).
	 *
	 * @return array $affiliate_data Array of all the data for the Affiliates
	 */
	public function affiliate_data() {

		$page = ( isset( $_GET['paged'] ) && is_numeric( $_GET['paged'] ) )
			? absint( $_GET['paged'] )
			: 1;

		// Note: Status validated in Affiliate_WP_DB_Affiliates::get_affiliates() against all valid status'.
		$status = ( isset( $_GET['status'] ) && is_string( $_GET['status'] ) )
			? $_GET['status']
			: '';

		// Note: Escaped with %%LIKE%% in Affiliate_WP_DB_Affiliates::get_affiliates().
		$search = ( isset( $_GET['s'] ) && is_string( $_GET['s'] ) )
			? $_GET['s']
			: '';

		// Note: Sanitized in Affiliate_WP_DB_Affiliates::get_affiliates(), can only be ASC or DESC.
		$order = ( isset( $_GET['order'] ) && is_string( $_GET['order'] ) )
			? $_GET['order']
			: 'DESC';

		// Note: Sanitized in Affiliate_WP_DB_Affiliates::get_affiliates(), accepts only specific columns in the affiliates database table.
		$orderby = ( isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) )
			? $_GET['orderby']
			: 'affiliate_id';

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

		// When filtering by flagged, fetch IDs from affiliate meta and scope with 'include'.
		$flagged      = isset( $_GET['flagged'] ) && '1' === $_GET['flagged'];
		$flagged_ids  = [];

		if ( $flagged ) {
			global $wpdb;

			$meta_table       = affiliate_wp()->affiliate_meta->table_name;
			$affiliates_table = affiliate_wp()->affiliates->table_name;

			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names are safe.
			$flagged_ids = $wpdb->get_col(
				"SELECT DISTINCT a.affiliate_id
				FROM {$affiliates_table} a
				INNER JOIN {$meta_table} am ON a.affiliate_id = am.affiliate_id
				WHERE am.meta_key = 'ip_velocity_flag'
				AND am.meta_value != ''"
			);

			$flagged_ids = array_map( 'intval', $flagged_ids );

			// If no flagged affiliates, return empty immediately.
			if ( empty( $flagged_ids ) ) {
				$this->current_count = 0;
				return [];
			}
		}

		$args = wp_parse_args(
			$this->query_args,
			[
				'number'  => $per_page,
				'offset'  => $per_page * ( $page - 1 ),
				'status'  => $status,
				'search'  => $search,
				'orderby' => sanitize_text_field( $orderby ),
				'order'   => sanitize_text_field( $order ),
			]
		);

		// Scope to flagged affiliate IDs when the Flagged filter is active.
		if ( $flagged && ! empty( $flagged_ids ) ) {
			$args['include'] = $flagged_ids;
		}

		/**
		 * Filters the arguments used to retrieve affiliates for the Affiliates list table.
		 *
		 * @since 2.4.4
		 *
		 * @param array                   $args Arguments passed to get_affiliates() to retrieve
		 *                                      the affiliate records for display.
		 * @param \AffWP_Affiliates_Table $this Affiliates list table instance.
		 */
		$args = apply_filters( 'affwp_affiliate_table_get_affiliates', $args, $this );

		$affiliates = affiliate_wp()->affiliates->get_affiliates( $args );

		// Retrieve the "current" total count for pagination purposes.
		$args['number'] = -1;

		$this->current_count = affiliate_wp()->affiliates->count( $args );

		return $affiliates;
	}

	/**
	 * Setup the final data for the table
	 *
	 * @access public
	 * @since 1.0
	 * @uses AffWP_Affiliates_Table::get_columns()
	 * @uses AffWP_Affiliates_Table::get_sortable_columns()
	 * @uses AffWP_Affiliates_Table::process_bulk_action()
	 * @uses AffWP_Affiliates_Table::affiliate_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_affiliates_per_page', $this->per_page );

		$columns = $this->get_columns();

		$hidden = [];

		$sortable = $this->get_sortable_columns();

		$this->get_column_info();

		$this->process_bulk_action();

		$data = $this->affiliate_data();

		$current_page = $this->get_pagenum();

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

		if ( $flagged ) {
			$total_items = $this->current_count;
		} else {
			switch ( $status ) {
				case 'active':
					$total_items = $this->active_count;
					break;
				case 'inactive':
					$total_items = $this->inactive_count;
					break;
				case 'pending':
					$total_items = $this->pending_count;
					break;
				case 'rejected':
					$total_items = $this->rejected_count;
					break;
				case 'any':
					$total_items = $this->current_count;
					break;
			}
		}

		$this->items = $data;

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