<?php
/**
 * Members admin controller.
 *
 * @package Awesomemotive\Rewardswp\Controllers\Admin
 * @since 1.0.0
 */

namespace Awesomemotive\Rewardswp\Controllers\Admin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Awesomemotive\Rewardswp\Factories\ViewsFactory;
use Awesomemotive\Rewardswp\Interfaces\ControllerInterface;
use Awesomemotive\Rewardswp\Views\Admin\Components\ToastView;
use Awesomemotive\Rewardswp\Views\Admin\ListTables\MembersListTableView;
use Awesomemotive\Rewardswp\Views\Admin\Modals\MemberDetailModalView;
use Awesomemotive\Rewardswp\Views\Admin\MemberDetailView;
use Exception;
use function Awesomemotive\Rewardswp\Helpers\get_member;
use function Awesomemotive\Rewardswp\Helpers\get_member_status_badge_html;
use function Awesomemotive\Rewardswp\Helpers\is_rewardswp_admin_page;
use function Awesomemotive\Rewardswp\Helpers\get_human_readable_date;
use function Awesomemotive\Rewardswp\Helpers\get_referrals;
use function Awesomemotive\Rewardswp\Helpers\get_site_currency_symbol;
use function Awesomemotive\Rewardswp\Helpers\update_member;
use Awesomemotive\Rewardswp\Models\Member;
use Awesomemotive\Rewardswp\Services\BirthdayService;
use Awesomemotive\Rewardswp\Services\MemberStatsService;
use Awesomemotive\Rewardswp\Services\PointsService;
use function Awesomemotive\Rewardswp\Helpers\parse_name;
use function Awesomemotive\Rewardswp\Helpers\create_member;
use function Awesomemotive\Rewardswp\Helpers\get_member_by;
use function Awesomemotive\Rewardswp\Helpers\get_user_first_name;
use function Awesomemotive\Rewardswp\Helpers\get_user_last_name;

class MembersAdminController implements ControllerInterface {
	/**
	 * Admin submenu position.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const MENU_POSITION = 10;

	/**
	 * Admin submenu slug.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const MENU_SLUG = 'rewardswp-members';

	/**
	 * Views factory instance.
	 *
	 * @since 1.0.0
	 *
	 * @var ViewsFactory
	 */
	private ViewsFactory $views_factory;

	/**
	 * Member stats service instance.
	 *
	 * @since 1.1.0
	 * @var MemberStatsService
	 */
	private MemberStatsService $member_stats_service;

	/**
	 * Points service instance.
	 *
	 * @since 1.1.0
	 * @var PointsService
	 */
	private PointsService $points_service;

	/**
	 * Birthday service instance.
	 *
	 * @since 1.4.0
	 * @var BirthdayService
	 */
	private BirthdayService $birthday_service;

	/**
	 * Nonce name for member modal actions (adjust points, update, delete, etc).
	 */
	const MEMBER_MODAL_ACTIONS_NONCE = 'rewardswp_member_modal_actions_nonce';

	/**
	 * Query parameter key used for confirmation nonce validation.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	const CONFIRMATION_NONCE_QUERY_KEY = 'rewardswp_action_nonce';

	/**
	 * Nonce action used when showing the delete confirmation screen.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	const DELETE_CONFIRMATION_NONCE_ACTION = 'rewardswp_delete_confirmation_display';

	/**
	 * Nonce action used when showing the exclude confirmation screen.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	const EXCLUDE_CONFIRMATION_NONCE_ACTION = 'rewardswp_exclude_confirmation_display';

	/**
	 * Construct.
	 *
	 * @since 1.0.0
	 *
	 * @param ViewsFactory       $views_factory
	 * @param MemberStatsService $member_stats_service
	 * @param PointsService      $points_service
	 * @param BirthdayService    $birthday_service
	 */
	public function __construct( ViewsFactory $views_factory, MemberStatsService $member_stats_service, PointsService $points_service, BirthdayService $birthday_service ) {
		$this->views_factory        = $views_factory;
		$this->member_stats_service = $member_stats_service;
		$this->points_service       = $points_service;
		$this->birthday_service     = $birthday_service;
	}

	/**
	 * Get page title.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_page_title(): string {
		return esc_html__( 'Members', 'rewardswp' );
	}

	/**
	 * Init.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'admin_menu', [ $this, 'add_admin_menu' ], self::MENU_POSITION );
		add_action( 'admin_head', [ $this, 'add_screen_options' ] );
		add_filter( 'set-screen-option', [ $this, 'action_members_set_screen_option' ], 10, 3 );

		// Process bulk actions early, before any output starts.
		// Use priority 1 to run before any other code that might cause output.
		// This handles all bulk actions (from both action and action2 params) because
		// by the time admin_action_* hooks fire, other plugins may have already started
		// outputting content (e.g., EDD enqueuing styles), causing "headers already sent" errors.
		add_action( 'admin_init', [ $this, 'maybe_process_bulk_actions_early' ], 1 );

		// Hook for processing the actual member deletion after confirmation.
		// This runs AFTER the confirmation page is shown and the user confirms.
		add_action( 'admin_action_rewardswp_process_member_deletion', [ $this, 'handle_process_member_deletion' ] );

		// Hook for processing the actual member exclusion after confirmation.
		add_action( 'admin_action_rewardswp_process_member_bulk_exclude', [ $this, 'handle_process_bulk_exclude' ] );

		// Hook for adjusting member points.
		add_action( 'wp_ajax_rewardswp_adjust_member_points', [ $this, 'handle_ajax_adjust_member_points' ] );

		// Hook for updating member referral code.
		add_action( 'wp_ajax_rewardswp_update_member_referral_code', [ $this, 'handle_ajax_update_member_referral_code' ] );

		// Hook for updating member details from Edit Member modal.
		add_action( 'wp_ajax_rewardswp_update_member_details', [ $this, 'handle_ajax_update_member_details' ] );

		// Hook for updating member status (exclude/include).
		add_action( 'wp_ajax_rewardswp_update_member_status', [ $this, 'handle_ajax_update_member_status' ] );

		// Hook for deleting a member from the detail page modal.
		add_action( 'wp_ajax_rewardswp_delete_member_from_detail', [ $this, 'handle_ajax_delete_member_from_detail' ] );

		// Hook for adding a new member.
		add_action( 'wp_ajax_rewardswp_add_member', [ $this, 'handle_ajax_add_member' ] );

		// Hook for searching WordPress users.
		add_action( 'wp_ajax_rewardswp_search_users', [ $this, 'handle_ajax_search_users' ] );
	}

	/**
	 * Register the menu.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function add_admin_menu() {
		add_submenu_page(
			MainAdminController::MAIN_MENU_SLUG,
			$this->get_page_title(),
			$this->get_page_title(),
			'manage_options',
			self::MENU_SLUG,
			[ $this, 'render_page' ],
			self::MENU_POSITION
		);
	}

	/**
	 * Register screen options.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function add_screen_options(): void {
		if ( ! is_rewardswp_admin_page( self::MENU_SLUG ) ) {
			return;
		}

		add_screen_option(
			'per_page',
			[
				'label'   => esc_html__( 'Number of members per page:', 'rewardswp' ),
				'option'  => 'members_per_page',
				'default' => 20,
			]
		);

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce not required for this.
		if ( empty( $_REQUEST['action'] ) || ( 'view_member' !== $_REQUEST['action'] ) ) {
			/*
			 * Instantiate the list table to make the columns array available to screen options.
			 *
			 * If the 'view_member' action is set, don't instantiate. Instantiating in sub-views
			 * creates conflicts in the screen option column controls if another list table is being
			 * displayed.
			 */
			$this->views_factory->get_view( MembersListTableView::class );
		}
	}

	/**
	 * Per page screen option value for the Member list table
	 *
	 * @since  1.0.0
	 *
	 * @param  bool|int $status
	 * @param  string   $option
	 * @param  mixed    $value
	 *
	 * @return mixed
	 */
	public function action_members_set_screen_option( $status, string $option, $value ) {

		if ( 'members_per_page' === $option ) {
			update_user_meta( get_current_user_id(), $option, $value );

			return $value;
		}

		return $status;
	}

	/**
	 * Render the page.
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function render_page(): void {
		if ( $this->maybe_handle_bulk_actions() ) {
			return;
		}

		// Check if we're viewing a specific member.
		$requested_action = filter_input( INPUT_GET, 'action', FILTER_UNSAFE_RAW );
		if ( is_string( $requested_action ) && sanitize_key( wp_unslash( $requested_action ) ) === 'view_member' ) {
			$this->render_member_detail_page();
			return;
		}

		$display_confirmation = '';
		$raw_confirmation     = filter_input( INPUT_GET, 'rewardswp_display_confirmation', FILTER_UNSAFE_RAW );
		if ( is_string( $raw_confirmation ) ) {
			$display_confirmation = sanitize_key( wp_unslash( $raw_confirmation ) );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Explicit nonce verification below.
		if ( 'delete_members' === $display_confirmation ) {
			$nonce     = '';
			$raw_nonce = filter_input( INPUT_GET, self::CONFIRMATION_NONCE_QUERY_KEY, FILTER_UNSAFE_RAW );
			if ( is_string( $raw_nonce ) ) {
				$nonce = sanitize_text_field( wp_unslash( $raw_nonce ) );
			}

			if ( ! $nonce || ! wp_verify_nonce( $nonce, self::DELETE_CONFIRMATION_NONCE_ACTION ) ) {
				delete_transient( 'rewardswp_member_delete_confirmation' );
				wp_safe_redirect(
					add_query_arg(
						[
							'page'            => self::MENU_SLUG,
							'rewardswp_error' => 'invalid_delete_confirmation_nonce',
						],
						admin_url( 'admin.php' )
					)
				);
				exit;
			}

			$confirmation_data = get_transient( 'rewardswp_member_delete_confirmation' );
			if ( $confirmation_data && isset( $confirmation_data['members_to_display'] ) && isset( $confirmation_data['member_ids_for_form'] ) ) {
				$list_table = $this->views_factory->get_view( MembersListTableView::class );
				if ( method_exists( $list_table, 'render_delete_confirmation_ui' ) ) {
					$list_table->render_delete_confirmation_ui(
						$confirmation_data['members_to_display'],
						$confirmation_data['member_ids_for_form']
					);

					delete_transient( 'rewardswp_member_delete_confirmation' );

					return; // Prevent rendering the default list table.
				}
			}
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Explicit nonce verification below.
		if ( 'exclude_members' === $display_confirmation ) {
			$nonce     = '';
			$raw_nonce = filter_input( INPUT_GET, self::CONFIRMATION_NONCE_QUERY_KEY, FILTER_UNSAFE_RAW );
			if ( is_string( $raw_nonce ) ) {
				$nonce = sanitize_text_field( wp_unslash( $raw_nonce ) );
			}

			if ( ! $nonce || ! wp_verify_nonce( $nonce, self::EXCLUDE_CONFIRMATION_NONCE_ACTION ) ) {
				delete_transient( 'rewardswp_member_exclude_confirmation' );
				wp_safe_redirect(
					add_query_arg(
						[
							'page'            => self::MENU_SLUG,
							'rewardswp_error' => 'invalid_exclude_confirmation_nonce',
						],
						admin_url( 'admin.php' )
					)
				);
				exit;
			}

			$confirmation_data = get_transient( 'rewardswp_member_exclude_confirmation' );
			if ( $confirmation_data && isset( $confirmation_data['members_to_display'] ) && isset( $confirmation_data['member_ids_for_form'] ) ) {
				$list_table = $this->views_factory->get_view( MembersListTableView::class );
				if ( method_exists( $list_table, 'render_exclude_confirmation_ui' ) ) {
					$list_table->render_exclude_confirmation_ui(
						$confirmation_data['members_to_display'],
						$confirmation_data['member_ids_for_form']
					);

					delete_transient( 'rewardswp_member_exclude_confirmation' );

					return; // Prevent rendering the default list table.
				}
			}
		}

		$list_table = $this->views_factory->get_view( MembersListTableView::class );

		$per_page = (int) get_user_meta( get_current_user_id(), 'members_per_page', true );
		if ( empty( $per_page ) || $per_page < 1 ) {
			$per_page = 20;
		}

		$list_table->prepare_items( $per_page );
		$list_table->render();
	}

	/**
	 * Render the member detail page.
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.0.0
	 */
	private function render_member_detail_page(): void {
		// Get member ID.
		$member_id_raw = filter_input( INPUT_GET, 'member_id', FILTER_UNSAFE_RAW );
		$member_id     = $member_id_raw ? absint( $member_id_raw ) : 0;

		if ( ! $member_id ) {
			wp_die( esc_html__( 'Member ID is required.', 'rewardswp' ) );
		}

		// Fetch member.
		$member = get_member( $member_id );

		if ( ! $member || ! $member->id ) {
			wp_die( esc_html__( 'Member not found.', 'rewardswp' ) );
		}

		// Get the member detail view and render it.
		try {
			$detail_view = $this->views_factory->get_view( MemberDetailView::class );
			$detail_view->render(
				[
					'member'      => $member,
					'member_data' => $this->get_member_data( $member ),
				]
			);
		} catch ( Exception $e ) {
			wp_die( esc_html__( 'Could not render member detail page.', 'rewardswp' ) . ' ' . esc_html( $e->getMessage() ) );
		}
	}

	/**
	 * Handles the first step of bulk member deletion: displaying the confirmation screen.
	 * This is hooked to `admin_action_rewardswp_bulk_delete`.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function handle_bulk_delete_confirmation_step(): void {
		check_admin_referer( 'bulk-members' );

		if ( ! current_user_can( 'manage_options' ) ) { // Replace 'manage_options' with a more specific capability if you have one.
			wp_die( esc_html__( 'You do not have sufficient permissions to delete members.', 'rewardswp' ) );
		}

		$member_ids = $this->get_member_ids_from_request();
		$member_ids = array_filter( $member_ids ); // Remove any zeros or invalids from absint.

		if ( empty( $member_ids ) ) {
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&rewardswp_message=no_members_selected' ) );
			exit;
		}

		$members_to_display = [];
		foreach ( $member_ids as $member_id ) {
			$member = get_member( $member_id );
			if ( ! is_null( $member ) ) {
				$members_to_display[] = [
					'id'   => $member->id,
					'name' => $member->get_full_name() ?: $member->email,
				];
			}
		}

		if ( empty( $members_to_display ) ) {
			// This might happen if all selected IDs were invalid.
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&rewardswp_message=invalid_members_selected' ) );
			exit;
		}

		$confirmation_data = [
			'members_to_display'  => $members_to_display,
			'member_ids_for_form' => $member_ids,
		];
		set_transient( 'rewardswp_member_delete_confirmation', $confirmation_data, HOUR_IN_SECONDS );

		wp_safe_redirect(
			add_query_arg(
				[
					'page'                             => self::MENU_SLUG,
					'rewardswp_display_confirmation'   => 'delete_members',
					self::CONFIRMATION_NONCE_QUERY_KEY => wp_create_nonce( self::DELETE_CONFIRMATION_NONCE_ACTION ),
				],
				admin_url( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Handles the actual processing of member deletion after confirmation.
	 * This is hooked to `admin_action_rewardswp_process_member_deletion`.
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.1.0
	 */
	public function handle_process_member_deletion(): void {
		check_admin_referer( 'rewardswp-confirm-delete-action', 'rewardswp_confirm_delete_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) { // Replace with a more specific capability if available.
			wp_die( esc_html__( 'You do not have sufficient permissions to delete members.', 'rewardswp' ) );
		}

		$member_ids_to_delete = [];
		if ( isset( $_POST['member_ids'] ) && is_array( $_POST['member_ids'] ) ) {
			$member_ids_to_delete = array_map( 'absint', (array) wp_unslash( $_POST['member_ids'] ) );
		}
		$member_ids_to_delete = array_filter( $member_ids_to_delete ); // Remove any zeros or invalids.

		$deleted_count = 0;
		$failed_count  = 0;

		if ( ! empty( $member_ids_to_delete ) ) {
			foreach ( $member_ids_to_delete as $member_id ) {
				$member = get_member( $member_id );
				if ( ! is_null( $member ) ) {
					try {
						if ( $member->delete() ) {
							++$deleted_count;
						} else {
							++$failed_count;
						}
					} catch ( Exception $e ) {
						++$failed_count;
					}
				} else {
					++$failed_count;
				}
			}
		}

		$redirect_url = admin_url( 'admin.php?page=' . self::MENU_SLUG );

		if ( $deleted_count > 0 ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'members_deleted', $redirect_url );
			$redirect_url = add_query_arg( 'deleted_count', $deleted_count, $redirect_url );
		}

		if ( $failed_count > 0 ) {
			$redirect_url = add_query_arg( 'rewardswp_error', 'members_deletion_failed', $redirect_url );
			$redirect_url = add_query_arg( 'failed_count', $failed_count, $redirect_url );
		}

		if ( $deleted_count === 0 && $failed_count === 0 && ! empty( $member_ids_to_delete ) ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'no_members_processed_for_deletion', $redirect_url );
		} elseif ( empty( $member_ids_to_delete ) ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'no_members_selected_for_deletion', $redirect_url );
		}

		wp_safe_redirect( $redirect_url );
		exit;
	}

	/**
	 * Handles the first step of bulk member exclusion: displaying the confirmation screen.
	 * This is hooked to `admin_action_rewardswp_bulk_exclude`.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function handle_bulk_exclude_confirmation_step(): void {
		check_admin_referer( 'bulk-members' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to exclude members.', 'rewardswp' ) );
		}

		$member_ids = $this->get_member_ids_from_request();
		$member_ids = array_filter( $member_ids );

		if ( empty( $member_ids ) ) {
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&rewardswp_message=no_members_selected' ) );
			exit;
		}

		$members_to_display = [];
		foreach ( $member_ids as $member_id ) {
			$member = get_member( $member_id );
			if ( ! is_null( $member ) ) {
				$members_to_display[] = [
					'id'   => $member->id,
					'name' => $member->get_full_name() ?: $member->email,
				];
			}
		}

		if ( empty( $members_to_display ) ) {
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&rewardswp_message=invalid_members_selected' ) );
			exit;
		}

		$confirmation_data = [
			'members_to_display'  => $members_to_display,
			'member_ids_for_form' => $member_ids,
		];
		set_transient( 'rewardswp_member_exclude_confirmation', $confirmation_data, HOUR_IN_SECONDS );

		wp_safe_redirect(
			add_query_arg(
				[
					'page'                             => self::MENU_SLUG,
					'rewardswp_display_confirmation'   => 'exclude_members',
					self::CONFIRMATION_NONCE_QUERY_KEY => wp_create_nonce( self::EXCLUDE_CONFIRMATION_NONCE_ACTION ),
				],
				admin_url( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Handles the actual processing of member bulk exclusion after confirmation.
	 * This is hooked to `admin_action_rewardswp_process_member_bulk_exclude`.
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.1.0
	 */
	public function handle_process_bulk_exclude(): void {
		check_admin_referer( 'rewardswp-confirm-bulk-exclude-action', 'rewardswp_confirm_bulk_exclude_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to exclude members.', 'rewardswp' ) );
		}

		$member_ids_to_exclude = [];
		if ( isset( $_POST['member_ids'] ) && is_array( $_POST['member_ids'] ) ) {
			$member_ids_to_exclude = array_map( 'absint', (array) wp_unslash( $_POST['member_ids'] ) );
		}
		$member_ids_to_exclude = array_filter( $member_ids_to_exclude );

		$updated_count = 0;
		$failed_count  = 0;

		if ( ! empty( $member_ids_to_exclude ) ) {
			foreach ( $member_ids_to_exclude as $member_id ) {
				$member = get_member( $member_id );
				if ( ! is_null( $member ) ) {
					try {
						$member->status = 'excluded';
						$result         = $member->update(); // Assuming update() saves the member
						if ( ! is_wp_error( $result ) ) {
							++$updated_count;
						} else {
							++$failed_count;
						}
					} catch ( Exception $e ) {
						++$failed_count;
					}
				} else {
					++$failed_count;
				}
			}
		}

		$redirect_url = admin_url( 'admin.php?page=' . self::MENU_SLUG );

		if ( $updated_count > 0 ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'members_excluded', $redirect_url );
			$redirect_url = add_query_arg( 'updated_count', $updated_count, $redirect_url );
		}

		if ( $failed_count > 0 ) {
			$redirect_url = add_query_arg( 'rewardswp_error', 'members_exclusion_failed', $redirect_url );
			$redirect_url = add_query_arg( 'failed_count', $failed_count, $redirect_url );
		}

		if ( $updated_count === 0 && $failed_count === 0 && ! empty( $member_ids_to_exclude ) ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'no_members_processed_for_exclusion', $redirect_url );
		} elseif ( empty( $member_ids_to_exclude ) ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'no_members_selected_for_exclusion', $redirect_url );
		}

		wp_safe_redirect( $redirect_url );
		exit;
	}

	/**
	 * Handles the processing of member bulk inclusion.
	 * This is hooked to `admin_action_rewardswp_bulk_include`.
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.1.0
	 */
	public function handle_bulk_include(): void {
		check_admin_referer( 'bulk-members' ); // Check nonce from the list table

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to include members.', 'rewardswp' ) );
		}

		$member_ids_to_include = $this->get_member_ids_from_request();
		$member_ids_to_include = array_filter( $member_ids_to_include );

		$updated_count = 0;
		$failed_count  = 0;

		if ( ! empty( $member_ids_to_include ) ) {
			foreach ( $member_ids_to_include as $member_id ) {
				$member = get_member( $member_id );
				if ( ! is_null( $member ) ) {
					try {
						$member->status = 'active';
						$result         = $member->update(); // Assuming update() saves the member
						if ( ! is_wp_error( $result ) ) {
							++$updated_count;
						} else {
							++$failed_count;
						}
					} catch ( Exception $e ) {
						++$failed_count;
					}
				} else {
					++$failed_count;
				}
			}
		}

		$redirect_url = admin_url( 'admin.php?page=' . self::MENU_SLUG );

		if ( $updated_count > 0 ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'members_included', $redirect_url );
			$redirect_url = add_query_arg( 'updated_count', $updated_count, $redirect_url );
		}

		if ( $failed_count > 0 ) {
			$redirect_url = add_query_arg( 'rewardswp_error', 'members_inclusion_failed', $redirect_url );
			$redirect_url = add_query_arg( 'failed_count', $failed_count, $redirect_url );
		}

		if ( $updated_count === 0 && $failed_count === 0 && ! empty( $member_ids_to_include ) ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'no_members_processed_for_inclusion', $redirect_url );
		} elseif ( empty( $member_ids_to_include ) ) {
			$redirect_url = add_query_arg( 'rewardswp_message', 'no_members_selected_for_inclusion', $redirect_url );
		}

		wp_safe_redirect( $redirect_url );
		exit;
	}

	/**
	 * Retrieve and sanitize the selected member IDs from the current request.
	 *
	 * Supports both the current `member_ids` parameter and the legacy `bulk-delete` name.
	 *
	 * @since 1.1.0
	 *
	 * @return array<int>
	 */
	private function get_member_ids_from_request(): array {
		$member_ids = [];

		$post_member_ids = filter_input( INPUT_POST, 'member_ids', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
		$get_member_ids  = filter_input( INPUT_GET, 'member_ids', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
		$post_bulk_ids   = filter_input( INPUT_POST, 'bulk-delete', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
		$get_bulk_ids    = filter_input( INPUT_GET, 'bulk-delete', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );

		if ( is_array( $post_member_ids ) ) {
			$member_ids = $post_member_ids;
		} elseif ( is_array( $get_member_ids ) ) {
			$member_ids = $get_member_ids;
		} elseif ( is_array( $post_bulk_ids ) ) {
			$member_ids = $post_bulk_ids;
		} elseif ( is_array( $get_bulk_ids ) ) {
			$member_ids = $get_bulk_ids;
		}

		return array_map( 'absint', $member_ids );
	}

	/**
	 * Process bulk actions early, before any page output starts.
	 *
	 * This method runs on admin_init with priority 1 to handle bulk actions before
	 * any other code can start outputting content. This is necessary because:
	 * 1. WordPress only fires admin_action_* hooks for the 'action' parameter
	 * 2. By the time admin_action_* fires, other plugins (e.g., EDD) may have already
	 *    started outputting content (styles, scripts), causing "headers already sent"
	 *    errors when we try to redirect.
	 *
	 * We process ALL bulk actions here (from both action and action2 params) to ensure
	 * redirects happen before any output.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function maybe_process_bulk_actions_early(): void {
		// Only process on our admin page.
		$page = filter_input( INPUT_GET, 'page', FILTER_UNSAFE_RAW );
		if ( ! is_string( $page ) || sanitize_key( $page ) !== self::MENU_SLUG ) {
			return;
		}

		// Get action values from both POST and GET, checking both action and action2.
		// WP_List_Table has two dropdowns that sync via JavaScript, so both may be set.
		$action_sources = [
			filter_input( INPUT_POST, 'action', FILTER_UNSAFE_RAW ),
			filter_input( INPUT_GET, 'action', FILTER_UNSAFE_RAW ),
			filter_input( INPUT_POST, 'action2', FILTER_UNSAFE_RAW ),
			filter_input( INPUT_GET, 'action2', FILTER_UNSAFE_RAW ),
		];

		$bulk_action = '';
		foreach ( $action_sources as $action_value ) {
			if ( is_string( $action_value ) && '-1' !== $action_value && ! empty( $action_value ) ) {
				$bulk_action = sanitize_text_field( wp_unslash( $action_value ) );
				break;
			}
		}

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

		// Only process our bulk actions that need early handling (ones that redirect).
		// Note: rewardswp_process_member_deletion and rewardswp_process_member_bulk_exclude
		// are handled by admin_action_* hooks because they run AFTER confirmation pages.
		$valid_actions = [
			'rewardswp_bulk_delete',
			'rewardswp_bulk_exclude',
			'rewardswp_bulk_include',
		];

		if ( ! in_array( $bulk_action, $valid_actions, true ) ) {
			return;
		}

		// Dispatch to the appropriate handler.
		switch ( $bulk_action ) {
			case 'rewardswp_bulk_delete':
				$this->handle_bulk_delete_confirmation_step();
				break;
			case 'rewardswp_bulk_exclude':
				$this->handle_bulk_exclude_confirmation_step();
				break;
			case 'rewardswp_bulk_include':
				$this->handle_bulk_include();
				break;
		}
	}

	/**
	 * Determine if a bulk action should be processed and dispatch the appropriate handler.
	 *
	 * Note: This is now a fallback. The primary bulk action processing happens in
	 * maybe_process_bulk_actions_early() which runs on admin_init.
	 *
	 * @since 1.1.0
	 *
	 * @return bool True when a handler was triggered (and likely exited).
	 */
	private function maybe_handle_bulk_actions(): bool {
		$action = $this->get_current_bulk_action();

		switch ( $action ) {
			case 'rewardswp_bulk_delete':
				$this->handle_bulk_delete_confirmation_step();
				return true;
			case 'rewardswp_bulk_exclude':
				$this->handle_bulk_exclude_confirmation_step();
				return true;
			case 'rewardswp_bulk_include':
				$this->handle_bulk_include();
				return true;
			default:
				return false;
		}
	}

	/**
	 * Get the current bulk action from the request.
	 *
	 * Mirrors WP_List_Table::current_action() behaviour so we can trigger our handlers even when
	 * WordPress core admin_action hooks do not fire (e.g., due to AJAX overrides or custom forms).
	 *
	 * @since 1.1.0
	 *
	 * @return string Bulk action slug or empty string.
	 */
	private function get_current_bulk_action(): string {
		$action_sources = [
			filter_input( INPUT_POST, 'action', FILTER_UNSAFE_RAW ),
			filter_input( INPUT_GET, 'action', FILTER_UNSAFE_RAW ),
			filter_input( INPUT_POST, 'action2', FILTER_UNSAFE_RAW ),
			filter_input( INPUT_GET, 'action2', FILTER_UNSAFE_RAW ),
		];

		foreach ( $action_sources as $action_value ) {
			if ( is_string( $action_value ) && $action_value !== '-1' ) {
				return sanitize_text_field( wp_unslash( $action_value ) );
			}
		}

		return '';
	}

	/**
	 * Handle AJAX request to adjust member points (add/deduct).
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function handle_ajax_adjust_member_points(): void {

		// 1. Security: Nonce and capability.
		$member_id = isset( $_POST['member_id'] ) ? absint( $_POST['member_id'] ) : 0;
		check_ajax_referer( 'rewardswp_adjust_points_nonce_' . $member_id, 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'You do not have sufficient permissions to adjust points.', 'rewardswp' ) ], 403 );
		}

		// 2. Get and sanitize inputs.
		$action = isset( $_POST['adjust_action'] ) ? sanitize_text_field( wp_unslash( $_POST['adjust_action'] ) ) : '';
		$amount = isset( $_POST['points_amount'] ) ? absint( $_POST['points_amount'] ) : 0;
		$reason = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : null;

		if ( ! $member_id ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Member ID is required.', 'rewardswp' ) ], 400 );
		}
		if ( $amount <= 0 ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Points amount must be greater than zero.', 'rewardswp' ) ], 400 );
		}
		if ( ! in_array( $action, [ 'add', 'deduct' ], true ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Invalid action.', 'rewardswp' ) ], 400 );
		}

		$member = get_member( $member_id );
		if ( ! $member || ! $member->id ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Member not found.', 'rewardswp' ) ], 404 );
		}

		// 3. Adjust points using the appropriate service
		$new_point_entry_object = null;
		try {
			// Determine entry type and note based on action
			if ( 'add' === $action ) {
				$entry_type = 'earned'; // Consistent with PointEntry model / display logic
				$entry_note = $reason ?: __( 'Manual adjustment (add)', 'rewardswp' );
			} else { // 'deduct'
				$entry_type = 'spent'; // Consistent with PointEntry model / display logic
				$entry_note = $reason ?: __( 'Manual adjustment (deduct)', 'rewardswp' );
			}

			// Create points entry - this will automatically update the member balance
			$new_point_entry_object = $this->points_service->create_points_entry(
				[
					'member_id'     => $member_id,
					'points'        => $amount,
					'type'          => $entry_type,
					'source_type'   => 'manual_adjustment',
					'status'        => 'approved',
					'note_internal' => $entry_note,
				],
				false // Don't send email for manual admin adjustments
			);

			if ( is_wp_error( $new_point_entry_object ) ) {
				wp_send_json_error( [ 'message' => $new_point_entry_object->get_error_message() ] );
			}

			// Re-fetch member to ensure all stats are current after adjustment and entry creation.
			// Or, if the $member object is updated by reference and PointsService doesn't change it further,
			// we can potentially use the existing $member. Re-fetching is safer.
			$updated_member = get_member( $member_id );
			if ( ! $updated_member || ! $updated_member->id ) {
				// This should ideally not happen if we just updated it.
				wp_send_json_error( [ 'message' => esc_html__( 'Could not retrieve updated member data.', 'rewardswp' ) ], 500 );
			}

			$member_stats = $this->member_stats_service->get_member_stats( $updated_member );

			$new_entry_data = null;
			if ( $new_point_entry_object && ! is_wp_error( $new_point_entry_object ) && isset( $new_point_entry_object->id ) ) {
				// Assuming $new_point_entry_object is the PointEntry model instance or an object with its data.
				$points_value = absint( $new_point_entry_object->points );
				$delta_val    = in_array( $new_point_entry_object->type, [ 'spent', 'remove' ], true ) ? -$points_value : $points_value;

				$new_entry_data = [
					'id'                   => $new_point_entry_object->id,
					'type'                 => $new_point_entry_object->type,
					'note_public'          => $new_point_entry_object->note_public ?: '',
					'note_internal'        => $new_point_entry_object->note_internal ?: '',
					'created_at_formatted' => get_date_from_gmt( $new_point_entry_object->created_at, 'F j, Y' ),
					'points_formatted'     => method_exists( $new_point_entry_object, 'get_formatted_points' ) ? $new_point_entry_object->get_formatted_points() . ' pts' : ( ( $new_point_entry_object->type === 'earned' || $new_point_entry_object->type === 'add' ? '+' : '-' ) . $new_point_entry_object->points . ' pts' ),
					'created_at_raw'       => $new_point_entry_object->created_at,
					'delta'                => $delta_val,
					'action_label'         => __( 'Adjusted by Admin', 'rewardswp' ),
					'target_label'         => '',
					'target_link'          => '',
					'status'               => 'approved',
					'date'                 => __( 'Today', 'rewardswp' ),
				];
			}

			wp_send_json_success(
				[
					'message'             => esc_html__( 'Points adjusted successfully.', 'rewardswp' ),
					'new_balance'         => $member_stats['points']['available'] ?? 0,
					'new_lifetime_points' => $member_stats['points']['lifetime'] ?? 0,
					'new_spent_points'    => $member_stats['points']['spent'] ?? 0,
					'new_redeemed_points' => $member_stats['points']['redeemed'] ?? 0,
					'new_entry'           => $new_entry_data,
				]
			);

		} catch ( Exception $e ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Error adjusting points: ', 'rewardswp' ) . $e->getMessage() ], 500 );
		}

		// Fallback, should have exited via wp_send_json_success or wp_send_json_error
		wp_send_json_error( [ 'message' => esc_html__( 'An unexpected error occurred while adjusting points.', 'rewardswp' ) ], 500 );
	}

	/**
	 * Handle AJAX request to update member referral code.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function handle_ajax_update_member_referral_code(): void {
		// 1. Security: Nonce and capability.
		// The nonce name 'rewardswp_update_referral_code_nonce_X' is created in MemberDetailView and passed in JS.
		// We check just the base part 'rewardswp_update_referral_code_nonce' here, as WordPress appends the ID automatically if created like that.
		// Or, ensure the JS sends a nonce that is exactly 'rewardswp_update_member_referral_code_nonce' if using a fixed nonce name.
		// For simplicity, we'll assume the JS sends the specific nonce for this action.
		check_ajax_referer( 'rewardswp_update_referral_code_nonce_' . ( isset( $_POST['member_id'] ) ? absint( $_POST['member_id'] ) : 0 ), 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) { // Or a more specific capability
			wp_send_json_error( [ 'message' => esc_html__( 'You do not have sufficient permissions to update referral codes.', 'rewardswp' ) ], 403 );
		}

		// 2. Get and sanitize inputs.
		$member_id         = isset( $_POST['member_id'] ) ? absint( $_POST['member_id'] ) : 0;
		$new_referral_code = isset( $_POST['new_referral_code'] ) ? sanitize_text_field( wp_unslash( $_POST['new_referral_code'] ) ) : '';

		if ( ! $member_id ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Member ID is required.', 'rewardswp' ) ], 400 );
		}

		if ( empty( $new_referral_code ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Referral code cannot be empty.', 'rewardswp' ) ], 400 );
		}

		// Potentially add more validation for the referral code format if needed.
		// For example, length, allowed characters, etc.

		// 3. Attempt to update the member's referral code.
		$result = update_member( $member_id, [ 'referral_code' => $new_referral_code ] );

		if ( is_wp_error( $result ) ) {
			// Check for specific error codes if update_advocate can return them (e.g., for existing code)
			if ( 'existing_referral_code' === $result->get_error_code() ) {
				wp_send_json_error( [ 'message' => $result->get_error_message() ] );
			} else {
				wp_send_json_error( [ 'message' => $result->get_error_message() ] );
			}
		} elseif ( $result instanceof Member ) {
			wp_send_json_success(
				[
					'message'           => esc_html__( 'Referral code updated successfully.', 'rewardswp' ),
					'new_referral_code' => $result->referral_code, // Send back the updated code
				]
			);
		} else {
			// Should not happen if update_advocate returns Member or WP_Error
			wp_send_json_error( [ 'message' => esc_html__( 'An unexpected error occurred while updating the referral code.', 'rewardswp' ) ] );
		}
	}

	/**
	 * Handle AJAX request to update member details (name, email).
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function handle_ajax_update_member_details(): void {
		// 1. Security: Nonce and capability.
		// The nonce name 'rewardswp_edit_member_nonce_X' is created in MemberDetailView and passed in JS.
		// We check just the base nonce name here.
		check_ajax_referer( 'rewardswp_edit_member_nonce_' . absint( $_POST['member_id'] ?? 0 ), 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) { // Or a more specific capability
			wp_send_json_error( [ 'message' => esc_html__( 'You do not have sufficient permissions to edit member details.', 'rewardswp' ) ], 403 );
		}

		// 2. Get and sanitize inputs.
		$member_id = isset( $_POST['member_id'] ) ? absint( $_POST['member_id'] ) : 0;
		$full_name = isset( $_POST['full_name'] ) ? sanitize_text_field( wp_unslash( $_POST['full_name'] ) ) : '';
		$email     = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';

		if ( ! $member_id ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Member ID is required.', 'rewardswp' ) ], 400 );
		}

		if ( empty( $full_name ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Name cannot be empty.', 'rewardswp' ),
					'errors'  => [ 'fullName' => esc_html__( 'Name cannot be empty.', 'rewardswp' ) ],
				],
				400
			);
		}

		if ( empty( $email ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Email cannot be empty.', 'rewardswp' ),
					'errors'  => [ 'email' => esc_html__( 'Email cannot be empty.', 'rewardswp' ) ],
				],
				400
			);
		} elseif ( ! is_email( $email ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Invalid email format.', 'rewardswp' ),
					'errors'  => [ 'email' => esc_html__( 'Invalid email format.', 'rewardswp' ) ],
				],
				400
			);
		}

		// Get optional birthday fields.
		$birthday_month = isset( $_POST['birthday_month'] ) ? absint( $_POST['birthday_month'] ) : 0;
		$birthday_day   = isset( $_POST['birthday_day'] ) ? absint( $_POST['birthday_day'] ) : 0;

		try {
			$member = get_member( $member_id );
			if ( ! $member ) {
				wp_send_json_error( [ 'message' => esc_html__( 'Member not found.', 'rewardswp' ) ], 404 );
			}

			$name_parts = parse_name( $full_name );

			$update_args = [
				'first_name' => $name_parts['first_name'],
				'last_name'  => $name_parts['last_name'],
				'email'      => $email,
			];

			// The update_advocate function will call $member->update() which includes $member->validate_update()
			$updated_member_or_error = update_member( $member_id, $update_args );

			if ( is_wp_error( $updated_member_or_error ) ) {
				// Try to get a more specific error if it relates to a field
				$error_data = [];
				if ( strpos( strtolower( $updated_member_or_error->get_error_message() ), 'email' ) !== false ) {
					$error_data['errors']['email'] = $updated_member_or_error->get_error_message();
				}
				wp_send_json_error( array_merge( [ 'message' => $updated_member_or_error->get_error_message() ], $error_data ), 400 );
			}

			/**
			 * Updated member instance.
			 *
			 * @var Member $updated_member
			 */
			$updated_member = $updated_member_or_error;

			/*
			 * Handle birthday update if provided. Admin edits intentionally bypass the
			 * 30-day fraud prevention timer — they're corrections, not member-initiated
			 * changes, so we don't touch BIRTHDAY_UPDATED_AT_META_KEY here.
			 */
			$birthday_display = '';
			if ( $birthday_month && $birthday_day ) {
				$validation = $this->birthday_service->validate_birthday_format( $birthday_month, $birthday_day );
				if ( is_wp_error( $validation ) ) {
					wp_send_json_error(
						[
							'message' => $validation->get_error_message(),
							'errors'  => [ 'birthday' => $validation->get_error_message() ],
						],
						400
					);
				}

				$birthday_value    = sprintf( '%02d-%02d', $birthday_month, $birthday_day );
				$existing_birthday = $updated_member->get_meta( BirthdayService::BIRTHDAY_META_KEY );

				$updated_member->update_meta( BirthdayService::BIRTHDAY_META_KEY, $birthday_value );

				if ( empty( $existing_birthday ) ) {
					// First time setting — record set_at.
					$updated_member->update_meta( BirthdayService::BIRTHDAY_SET_AT_META_KEY, current_time( 'mysql' ) );
				}
				$birthday_display = $this->birthday_service->format_birthday_for_display( $birthday_value );
			}

			wp_send_json_success(
				[
					'message'            => esc_html__( 'Member details updated successfully.', 'rewardswp' ),
					'full_name'          => $updated_member->get_full_name(),
					'email'              => $updated_member->email,
					'gravatar_url_large' => get_avatar_url( $updated_member->email, [ 'size' => 96 ] ),
					'birthday_display'   => $birthday_display,
				]
			);

		} catch ( Exception $e ) {
			wp_send_json_error( [ 'message' => $e->getMessage() ], 500 );
		}

		// Fallback
		wp_send_json_error( [ 'message' => esc_html__( 'An unexpected error occurred while updating member details.', 'rewardswp' ) ], 500 );
	}

	/**
	 * Handle AJAX request to update member status (exclude/include).
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function handle_ajax_update_member_status(): void {
		// 1. Security: Nonce and capability.
		check_ajax_referer( 'rewardswp_update_member_status_nonce_' . absint( $_POST['member_id'] ?? 0 ), 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) { // Or a more specific capability
			wp_send_json_error( [ 'message' => esc_html__( 'You do not have sufficient permissions to update member status.', 'rewardswp' ) ], 403 );
		}

		// 2. Get and sanitize inputs.
		$member_id  = isset( $_POST['member_id'] ) ? absint( $_POST['member_id'] ) : 0;
		$new_status = isset( $_POST['new_status'] ) ? sanitize_text_field( wp_unslash( $_POST['new_status'] ) ) : '';

		if ( ! $member_id ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Member ID is required.', 'rewardswp' ) ], 400 );
		}

		$allowed_statuses = [ 'active', 'excluded' ]; // Define what statuses can be set via this action
		if ( empty( $new_status ) || ! in_array( $new_status, $allowed_statuses, true ) ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Invalid status provided.', 'rewardswp' ) ], 400 );
		}

		try {
			$member = get_member( $member_id );
			if ( ! $member ) {
				wp_send_json_error( [ 'message' => esc_html__( 'Member not found.', 'rewardswp' ) ], 404 );
			}

			$member->status = $new_status;
			$update_result  = $member->update(); // update() should handle saving to DB

			if ( is_wp_error( $update_result ) ) {
				wp_send_json_error( [ 'message' => $update_result->get_error_message() ], 500 );
			}

			// Get updated member type HTML badge after status change
			$member_type_html = get_member_status_badge_html( $member );

			$message = 'active' === $new_status ? __( 'Member included in program successfully.', 'rewardswp' ) : __( 'Member excluded from program successfully.', 'rewardswp' );

			wp_send_json_success(
				[
					'message'          => $message,
					'new_status'       => $member->status, // Send back the confirmed status
					'member_type_html' => $member_type_html,
				]
			);

		} catch ( \Exception $e ) {
			wp_send_json_error( [ 'message' => $e->getMessage() ], 500 );
		}

		// Fallback
		wp_send_json_error( [ 'message' => esc_html__( 'An unexpected error occurred while updating member status.', 'rewardswp' ) ], 500 );
	}

	/**
	 * Handle AJAX request to delete a member from the detail page modal.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function handle_ajax_delete_member_from_detail(): void {
		// 1. Security: Nonce and capability.
		$member_id = isset( $_POST['member_id'] ) ? absint( $_POST['member_id'] ) : 0;
		check_ajax_referer( 'rewardswp_delete_member_nonce_' . $member_id, 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) { // Or a more specific capability for deleting members
			wp_send_json_error( [ 'message' => esc_html__( 'You do not have sufficient permissions to delete members.', 'rewardswp' ) ], 403 );
		}

		if ( ! $member_id ) {
			wp_send_json_error( [ 'message' => esc_html__( 'Member ID is required for deletion.', 'rewardswp' ) ], 400 );
		}

		try {
			$member = get_member( $member_id );
			if ( ! $member || ! $member->id ) {
				wp_send_json_error( [ 'message' => esc_html__( 'Member not found or could not be loaded.', 'rewardswp' ) ], 404 );
			}

			if ( $member->delete() ) {
				// Prepare redirect URL with success message for toast
				$redirect_url = add_query_arg(
					[
						'rewardswp_message' => 'member_deleted_successfully',
						// Optional: pass member name for more specific toast, but requires sanitization
						// 'deleted_member_name' => urlencode($member->get_full_name() ?: $member->email)
					],
					admin_url( 'admin.php?page=' . self::MENU_SLUG )
				);
				wp_send_json_success(
					[
						'message'      => esc_html__( 'Member deleted successfully. Redirecting...', 'rewardswp' ),
						'redirect_url' => $redirect_url,
					]
				);
			} else {
				wp_send_json_error( [ 'message' => esc_html__( 'Failed to delete member. The member might have already been removed or an error occurred.', 'rewardswp' ) ], 500 );
			}
		} catch ( \Exception $e ) {
			// Log the exception if needed, e.g., error_log( "Error deleting member $member_id: " . $e->getMessage() );
			wp_send_json_error( [ 'message' => esc_html__( 'An error occurred while trying to delete the member: ', 'rewardswp' ) . $e->getMessage() ], 500 );
		}
	}

	/**
	 * Get formatted member data for display.
	 *
	 * @since 1.0.0
	 *
	 * @param Member $member The member object.
	 *
	 * @throws Exception
	 *
	 * @return array
	 */
	private function get_member_data( Member $member ): array {
		return array_merge(
			[
				'gravatar_url_large'           => get_avatar_url( $member->email, [ 'size' => 96 ] ),
				'member_type_html'             => get_member_status_badge_html( $member ),
				'referred_by_display'          => $this->get_referred_by_display( $member ),
				'program_participation_status' => $member->status === 'active',
			],
			$this->member_stats_service->get_member_stats( $member )
		);
	}

	/**
	 * Get display string for who referred this member.
	 *
	 * @since 1.0.0
	 *
	 * @param Member $member The member object.
	 *
	 * @return string
	 */
	private function get_referred_by_display( Member $member ): string {
		$referred_by_name           = esc_html__( 'None', 'rewardswp' );
		$found_referral_advocate_id = null;

		if ( $member->user_id ) {
			$referrals_as_friend = get_referrals(
				[
					'referred_user_id' => $member->user_id,
					'orderby'          => 'created_at',
					'order'            => 'ASC',
				],
				1
			);
			if ( ! empty( $referrals_as_friend ) && isset( $referrals_as_friend[0]->advocate_id ) ) {
				$found_referral_advocate_id = absint( $referrals_as_friend[0]->advocate_id );
			}
		}

		if ( ! $found_referral_advocate_id && $member->email ) {
			$referrals_as_friend_by_email = get_referrals(
				[
					'referred_email' => $member->email,
					'orderby'        => 'created_at',
					'order'          => 'ASC',
				],
				1
			);
			if ( ! empty( $referrals_as_friend_by_email ) && isset( $referrals_as_friend_by_email[0]->advocate_id ) ) {
				$found_referral_advocate_id = absint( $referrals_as_friend_by_email[0]->advocate_id );
			}
		}

		if ( ! $found_referral_advocate_id ) {
			return $referred_by_name;
		}

		$advocate = get_member( $found_referral_advocate_id );

		if ( is_null( $advocate ) ) {
			return $referred_by_name;
		}

		$name_to_display = $advocate->get_full_name() ?: $advocate->email;

		if ( $name_to_display ) {
			return sprintf(
				'%s (#%d)',
				esc_html( $name_to_display ),
				$advocate->id
			);
		}

		return $referred_by_name;
	}

	/**
	 * Handle AJAX request to add a new member.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function handle_ajax_add_member(): void {
		try {
			// Verify nonce
			$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
			if ( ! $nonce || ! wp_verify_nonce( $nonce, 'rewardswp_add_member_nonce' ) ) {
				wp_send_json_error( [ 'message' => esc_html__( 'Invalid nonce.', 'rewardswp' ) ] );
				return;
			}

			// Check user permissions
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( [ 'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ) ] );
				return;
			}

			// Sanitize and validate input
			$user_id = filter_input( INPUT_POST, 'user_id', FILTER_VALIDATE_INT );
			$user_id = ( $user_id !== false ) ? $user_id : null;

			// Basic validation
			$errors = [];

			// Validate user ID and get user data
			if ( empty( $user_id ) ) {
				$errors['selectedUser'] = esc_html__( 'Please select a WordPress user.', 'rewardswp' );
			} else {
				// Check if user exists and get user data
				$user = get_user_by( 'ID', $user_id );
				if ( ! $user ) {
					$errors['selectedUser'] = esc_html__( 'Selected user does not exist.', 'rewardswp' );
				} else {
					// Check if user is already a member
					$existing_member = get_member_by( 'user_id', $user_id );
					if ( $existing_member ) {
						$errors['selectedUser'] = esc_html__( 'This user is already a member.', 'rewardswp' );
					}
				}
			}

			// Return validation errors if any
			if ( ! empty( $errors ) ) {
				wp_send_json_error(
					[
						'message'      => esc_html__( 'Please check the highlighted fields.', 'rewardswp' ),
						'field_errors' => $errors,
					]
				);
				return;
			}

			// Get user data from WordPress user
			$user = get_user_by( 'ID', $user_id );

			// Prepare member data using UserHelper functions (more robust with fallbacks)
			$member_data = [
				'user_id'    => $user_id,
				'email'      => $user->user_email,
				'first_name' => get_user_first_name( $user_id ) ?? '',
				'last_name'  => get_user_last_name( $user_id ) ?? '',
				'status'     => 'active',
			];

			// Create member using helper function
			$member = create_member( $member_data );

			// Check if creation was successful
			if ( is_wp_error( $member ) ) {
				wp_send_json_error(
					[
						'message'      => $member->get_error_message(),
						'field_errors' => strpos( $member->get_error_message(), 'email' ) !== false
							? [ 'email' => $member->get_error_message() ]
							: [],
					]
				);
				return;
			}

			// Send success response
			wp_send_json_success(
				[
					'message'   => esc_html__( 'Member created successfully.', 'rewardswp' ),
					'member_id' => $member->id,
				]
			);

		} catch ( Exception $e ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'An error occurred while creating the member.', 'rewardswp' ),
				]
			);
		}
	}

	/**
	 * Handle AJAX request to search WordPress users.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function handle_ajax_search_users(): void {
		try {
			// Verify nonce
			$ajax_nonce = isset( $_POST['_ajax_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ) : '';
			if ( ! $ajax_nonce || ! wp_verify_nonce( $ajax_nonce, 'rewardswp_searchable_nonce' ) ) {
				wp_send_json_error( [ 'message' => esc_html__( 'Invalid nonce.', 'rewardswp' ) ] );
				return;
			}

			// Check user permissions
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( [ 'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ) ] );
				return;
			}

			// Get search query
			$search = '';
			if ( isset( $_POST['query'] ) ) {
				$search = sanitize_text_field( wp_unslash( $_POST['query'] ) );
			}

			if ( strlen( $search ) < 3 ) {
				wp_send_json_success( [ 'items' => [] ] );
				return;
			}

			// Search users
			$users = get_users(
				[
					'search'         => '*' . $search . '*',
					'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
					'number'         => 20,
					'fields'         => [ 'ID', 'user_login', 'user_email', 'display_name', 'first_name', 'last_name' ],
				]
			);

			$results = [];
			foreach ( $users as $user ) {
				// Check if user is already a member
				$existing_member = get_member_by( 'user_id', $user->ID );
				if ( $existing_member ) {
					continue; // Skip users who are already members
				}

				$display_name = ! empty( $user->display_name ) ? $user->display_name : $user->user_login;
				$full_name    = '';

				if ( ! empty( $user->first_name ) && ! empty( $user->last_name ) ) {
					$full_name = $user->first_name . ' ' . $user->last_name;
				} elseif ( ! empty( $user->first_name ) ) {
					$full_name = $user->first_name;
				} elseif ( ! empty( $user->last_name ) ) {
					$full_name = $user->last_name;
				}

				$results[] = [
					'id'           => $user->ID,
					'name'         => sprintf( '%s (%s)', $display_name, $user->user_email ),
					'display_name' => $display_name,
					'email'        => $user->user_email,
					'first_name'   => $user->first_name,
					'last_name'    => $user->last_name,
					'full_name'    => $full_name,
				];
			}

			wp_send_json_success( [ 'items' => $results ] );

		} catch ( Exception $e ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'An error occurred while searching users.', 'rewardswp' ),
				]
			);
		}
	}
}
