<?php
/**
 * Advocate controller for handling advocate functionality.
 *
 * @package Awesomemotive\Rewardswp\Controllers
 * @since 1.0.0
 */

namespace Awesomemotive\Rewardswp\Controllers;

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

use Awesomemotive\Rewardswp\Factories\RewardTypesFactory;
use Awesomemotive\Rewardswp\Interfaces\ControllerInterface;
use Awesomemotive\Rewardswp\Interfaces\IntegrationInterface;
use Awesomemotive\Rewardswp\Services\AntiSpamService;
use Awesomemotive\Rewardswp\Services\AssetsService;
use Awesomemotive\Rewardswp\Services\BirthdayService;
use Awesomemotive\Rewardswp\Services\PanelService;
use Awesomemotive\Rewardswp\Services\PointsService;
use Awesomemotive\Rewardswp\Services\ReferralCodeService;
use Awesomemotive\Rewardswp\Services\ReferralsService;
use Awesomemotive\Rewardswp\Services\RewardsService;
use Awesomemotive\Rewardswp\Settings\Options\Referrals\RewardsOption;
use Awesomemotive\Rewardswp\Settings\Options\Points\SpendActionsOption;
use Awesomemotive\Rewardswp\Factories\ViewsFactory;
use Awesomemotive\Rewardswp\Factories\MailerFactory;
use Awesomemotive\Rewardswp\Views\Panel\Advocate\RewardsWidgetView;
use Awesomemotive\Rewardswp\Views\Panel\Parts\EmptyRewardsView;
use Awesomemotive\Rewardswp\Models\Member;
use Awesomemotive\Rewardswp\Models\Referral;
use Exception;
use WP_User;
use function Awesomemotive\Rewardswp\Helpers\get_member_by;
use function Awesomemotive\Rewardswp\Helpers\get_advocate_rewards;
use function Awesomemotive\Rewardswp\Helpers\get_current_member;
use function Awesomemotive\Rewardswp\Helpers\get_option_value;
use function Awesomemotive\Rewardswp\Helpers\get_option_object;
use function Awesomemotive\Rewardswp\Helpers\is_current_user_a_member;
use function Awesomemotive\Rewardswp\Helpers\is_current_member_a_guest;
use function Awesomemotive\Rewardswp\Helpers\get_grouped_rewards;
use function Awesomemotive\Rewardswp\Helpers\is_wp_admin_page;
use function Awesomemotive\Rewardswp\Helpers\get_date_diff_in_days;
use function Awesomemotive\Rewardswp\Helpers\parse_name;
use function Awesomemotive\Rewardswp\Helpers\create_member;
use function Awesomemotive\Rewardswp\Helpers\login_user_programmatically;
use function Awesomemotive\Rewardswp\Helpers\get_points_entries;
use function Awesomemotive\Rewardswp\Helpers\get_points_currency_plural;
use function Awesomemotive\Rewardswp\Helpers\format_currency;
use Awesomemotive\Rewardswp\Settings\Options\Referrals\ReferralVariableOption;
use Awesomemotive\Rewardswp\Interfaces\ViewInterface;
use Awesomemotive\Rewardswp\Repositories\ActivityRepository;
use Awesomemotive\Rewardswp\Services\ActivityFormatterService;
use function Awesomemotive\Rewardswp\Helpers\rewardswp_log_error;
use function Awesomemotive\Rewardswp\Helpers\get_human_readable_date;
use function Awesomemotive\Rewardswp\Helpers\get_reward_display_title_from_values;
use function Awesomemotive\Rewardswp\Helpers\mask_email;
use function Awesomemotive\Rewardswp\Helpers\get_user_full_name;
use function Awesomemotive\Rewardswp\Helpers\get_referral;
use function Awesomemotive\Rewardswp\Helpers\get_member;
use function Awesomemotive\Rewardswp\Helpers\format_name_first_and_initial;


/**
 * Advocate Controller.
 *
 * @since 1.0.0
 */
class AdvocateController implements ControllerInterface {
	/**
	 * AJAX nonce action for advocate operations.
	 *
	 * @since 1.0.0
	 */
	const AJAX_NONCE = 'rewardswp_advocate_form_nonce';

	/**
	 * Maximum number of activity items per page.
	 *
	 * @since 1.2.0
	 */
	const MAX_ACTIVITY_ITEMS_PER_PAGE = 50;

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

	/**
	 * Reward types factory instance.
	 *
	 * @since 1.0.0
	 *
	 * @var RewardTypesFactory
	 */
	private RewardTypesFactory $reward_types_factory;

	/**
	 * Assets service instance.
	 *
	 * @since 1.0.0
	 *
	 * @var AssetsService
	 */
	private AssetsService $assets_service;

	/**
	 * Rewards service instance.
	 *
	 * @since 1.0.0
	 *
	 * @var RewardsService
	 */
	private RewardsService $rewards_service;

	/**
	 * The specific content view for the advocate panel (determined by DI).
	 *
	 * @since 1.1.0
	 * @var ViewInterface $advocate_panel_content_view
	 */
	private ViewInterface $advocate_panel_content_view;

	/**
	 * Mailer factory for sending emails.
	 *
	 * @since 1.1.0
	 * @var MailerFactory
	 */
	private MailerFactory $mailer_factory;

	/**
	 * Integration instance.
	 *
	 * @since 1.1.0
	 * @var IntegrationInterface
	 */
	private IntegrationInterface $integration;

	/**
	 * Panel service instance.
	 *
	 * @since 1.1.0
	 * @var PanelService
	 */
	private PanelService $panel_service;

	/**
	 * Anti-spam service instance.
	 *
	 * @since 1.2.0
	 * @var AntiSpamService
	 */
	private AntiSpamService $anti_spam_service;

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

	/**
	 * Referrals service instance.
	 *
	 * @since 1.2.0
	 * @var ReferralsService
	 */
	private ReferralsService $referrals_service;

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

	/**
	 * Activity repository instance.
	 *
	 * @since 1.2.0
	 * @var ActivityRepository
	 */
	private ActivityRepository $activity_repository;

	/**
	 * Activity formatter service instance.
	 *
	 * @since 1.2.0
	 * @var ActivityFormatterService
	 */
	private ActivityFormatterService $activity_formatter_service;

	/**
	 * Referral code service instance.
	 *
	 * @since 1.4.1
	 *
	 * @var ReferralCodeService
	 */
	private ReferralCodeService $referral_code_service;

	public function __construct(
		ViewsFactory $views_factory,
		AssetsService $assets_service,
		RewardTypesFactory $reward_types_factory,
		RewardsService $rewards_service,
		ViewInterface $advocate_panel_content_view,
		MailerFactory $mailer_factory,
		IntegrationInterface $integration,
		PanelService $panel_service,
		AntiSpamService $anti_spam_service,
		BirthdayService $birthday_service,
		ReferralsService $referrals_service,
		PointsService $points_service,
		ActivityRepository $activity_repository,
		ActivityFormatterService $activity_formatter_service,
		ReferralCodeService $referral_code_service
	) {
		$this->views_factory               = $views_factory;
		$this->assets_service              = $assets_service;
		$this->reward_types_factory        = $reward_types_factory;
		$this->rewards_service             = $rewards_service;
		$this->advocate_panel_content_view = $advocate_panel_content_view;
		$this->mailer_factory              = $mailer_factory;
		$this->integration                 = $integration;
		$this->panel_service               = $panel_service;
		$this->anti_spam_service           = $anti_spam_service;
		$this->birthday_service            = $birthday_service;
		$this->referrals_service           = $referrals_service;
		$this->points_service              = $points_service;
		$this->activity_repository         = $activity_repository;
		$this->activity_formatter_service  = $activity_formatter_service;
		$this->referral_code_service       = $referral_code_service;
	}

	/**
	 * Run integration hooks.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function init(): void {
		add_action( 'user_register', [ $this, 'connect_existing_advocate_to_new_user' ] );
		add_action( 'wp_login', [ $this, 'action_update_last_login' ], 10, 2 );

		// AJAX handlers for member creation (available for both logged-in and non-logged-in users).
		add_action( 'wp_ajax_rewardswp_create_member', [ $this, 'ajax_create_member' ] );
		add_action( 'wp_ajax_nopriv_rewardswp_create_member', [ $this, 'ajax_create_member' ] );

		// AJAX handlers for user login (available for both logged-in and non-logged-in users).
		add_action( 'wp_ajax_rewardswp_login_user', [ $this, 'ajax_login_user' ] );
		add_action( 'wp_ajax_nopriv_rewardswp_login_user', [ $this, 'ajax_login_user' ] );

		// AJAX handler for refreshing rewards data (logged-in users only).
		add_action( 'wp_ajax_rewardswp_refresh_rewards', [ $this, 'ajax_refresh_rewards' ] );

		// AJAX handler for clearing welcome flow meta (available for all users).
		add_action( 'wp_ajax_rewardswp_clear_welcome_flow', [ $this, 'ajax_clear_welcome_flow' ] );
		add_action( 'wp_ajax_nopriv_rewardswp_clear_welcome_flow', [ $this, 'ajax_clear_welcome_flow' ] );

		// AJAX handler for setting birthday (logged-in members only).
		add_action( 'wp_ajax_rewardswp_set_birthday', [ $this, 'ajax_set_birthday' ] );
		// AJAX handler for member activity logs (available for all users, including guests with tokens).
		add_action( 'wp_ajax_rewardswp_get_member_activity', [ $this, 'ajax_get_member_activity' ] );
		add_action( 'wp_ajax_nopriv_rewardswp_get_member_activity', [ $this, 'ajax_get_member_activity' ] );

		if ( is_wp_admin_page() ) {
			return; // Bail if we're in admin area.
		}

		// Delay frontend hooks until template_redirect when WC/EDD functions are available.
		add_action( 'template_redirect', [ $this, 'setup_frontend_hooks' ] );
	}

	/**
	 * Setup frontend hooks after checking visibility filter.
	 *
	 * This method runs on template_redirect to ensure WooCommerce and EDD
	 * conditional functions are available for checking page context.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function setup_frontend_hooks(): void {
		/**
		 * Allow plugins to control widget visibility.
		 *
		 * @since 1.1.0
		 *
		 * @param bool $is_visible Whether the widget should be visible or not.
		 */
		$is_visible = apply_filters( 'rewardswp_is_rewards_widget_visible', $this->integration->is_active() );

		if ( ! $is_visible ) {
			return; // Don't load widget at all.
		}

		$this->prevent_page_cache_for_identified_member();

		// These hooks will load only on pages where the widget should be visible.
		add_action( 'wp_footer', [ $this, 'render_template' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
	}

	/**
	 * Signal page-cache plugins to skip caching the current response when a
	 * RewardsWP member is identified.
	 *
	 * The rewards widget renders per-member data (member ID, points balance,
	 * name, email, referral link, available rewards) into every page via
	 * `wp_footer`. Identity is carried by either the WordPress auth cookie or
	 * the non-standard `rewardswp_member_access_token` guest cookie. Most
	 * page-cache plugins vary their cache only by a default set of cookies and
	 * do not recognize the guest token, so without this signal the first
	 * member's rendered HTML can be served to subsequent visitors of the same
	 * URL.
	 *
	 * Honored by all major WordPress page-cache plugins (LiteSpeed, WP Rocket,
	 * W3 Total Cache, WP Super Cache, Cloudflare APO with WP plugin) through the
	 * standard `DONOTCACHEPAGE` constant and `Cache-Control: no-cache` headers
	 * emitted by `nocache_headers()`.
	 *
	 * @since 1.4.3
	 *
	 * @return void
	 */
	private function prevent_page_cache_for_identified_member(): void {
		if ( get_current_member() === null ) {
			return; // Anonymous → response carries only the generic "join" widget and is cacheable.
		}

		/**
		 * Allow opting out of the page-cache skip signal when a member is identified.
		 *
		 * @since 1.4.3
		 *
		 * @param bool $should_prevent Whether to emit no-cache headers/constants.
		 */
		if ( ! apply_filters( 'rewardswp_prevent_page_cache_for_member', true ) ) {
			return;
		}

		nocache_headers();

		// Only DONOTCACHEPAGE: the leak is page-level (per-member HTML baked into the
		// full response). Object cache (Redis/Memcached) and database query cache are
		// keyed per query/group and not affected by this bug, so leave them on.
		if ( ! defined( 'DONOTCACHEPAGE' ) ) {
			define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Standard cache plugin contract.
		}

		// LiteSpeed Cache uses an action rather than the DONOTCACHEPAGE constant.
		do_action( 'litespeed_control_set_nocache', 'rewardswp: member identified, response varies per visitor' );
	}

	/**
	 * Enqueue necessary styles ands scripts.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function enqueue_assets(): void {
		$this->assets_service->enqueue_script( $this->assets_service::MAIN_SCRIPT_HANDLE );

		// Pass cookie config so JS uses the same domain/path as PHP.
		wp_add_inline_script(
			$this->assets_service::MAIN_SCRIPT_HANDLE,
			sprintf(
				'window.rewardswpCookieConfig={path:%s,domain:%s};',
				wp_json_encode( $this->referral_code_service->get_cookie_path() ),
				wp_json_encode( $this->referral_code_service->get_cookie_domain() )
			),
			'before'
		);

		// Check if we're in friend claim mode (from URL or persisted cookie).
		if ( $this->should_auto_open_panel_for_friend_claim() ) {
			$referral_slug     = get_option_value( ReferralVariableOption::class );
			$referral_code_raw = filter_input( INPUT_GET, $referral_slug );

			if ( $referral_code_raw ) {
				// Persist the referral code when detected from URL so it survives navigation.
				$this->referral_code_service->persist_referral_code( sanitize_text_field( $referral_code_raw ) );

				// Auto-open the panel only on the landing page (first visit via referral link).
				// On subsequent pages, the panel is available but respects the user's open/close state.
				wp_add_inline_script(
					$this->assets_service::MAIN_SCRIPT_HANDLE,
					"window.rewardswpOpenPanelOnPageLoad = typeof rewardswpOpenPanelOnPageLoad !== 'undefined' ? rewardswpOpenPanelOnPageLoad : true;",
					'before'
				);
			}
		}

		// Clear referral code on order confirmation page (friend has completed purchase).
		if ( $this->referral_code_service->get_persisted_referral_code() ) {
			$is_confirmation = false;

			if ( function_exists( 'is_order_received_page' ) && is_order_received_page() ) {
				$is_confirmation = true;
			} elseif ( function_exists( 'edd_is_success_page' ) && edd_is_success_page() ) {
				$is_confirmation = true;
			}

			if ( $is_confirmation ) {
				$this->referral_code_service->clear_persistence();
			}
		}
	}

	/**
	 * Render the advocate panel.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function render_template(): void {
		$advocate = get_current_member();
		/**
		 * Panel content view instance.
		 *
		 * @var RewardsWidgetView $panel_content_view
		 */
		$panel_content_view = $this->advocate_panel_content_view;

		// If user is not a member, still show the panel but with "Become a member" section.

		// Initialize variables.
		$rewards                 = [];
		$available_rewards_count = 0;
		$grouped_rewards         = [];
		$expanded                = false;

		// Only get rewards data if user is a member.
		if ( $advocate ) {
			// Filter by the advocate's rewards (for referral program).
			$rewards = get_advocate_rewards(
				$advocate->id,
				[
					'status' => [
						'issued',
						'redeemed',
					],
				]
			);

			// Get available rewards count using RewardsService.
			$available_rewards       = $this->rewards_service->get_advocate_rewards(
				$advocate->id,
				[
					'status' => 'issued',
				]
			);
			$available_rewards_count = count( $available_rewards );

			// Prepare available rewards data for Alpine.js (filter out expired rewards).
			$available_rewards_data = [];
			foreach ( $available_rewards as $reward ) {
				$reward_info = $this->prepare_reward_for_alpine( $reward );

				// Skip expired rewards - don't show them in the list.
				if ( ! empty( $reward_info['is_expired'] ) ) {
					continue;
				}

				$available_rewards_data[] = $reward_info;
			}

			$grouped_rewards = get_grouped_rewards( $rewards );
			$expanded        = count( $rewards ) === 1;
		} else {
			$available_rewards_data = [];
		}

		// Prepare args for the panel content view.
		$content_args = [
			'referrals' => [
				'link'             => $advocate ? $advocate->get_referral_url() : '',
				'friends_referred' => $advocate ? $advocate->completed_referrals ?? 0 : 0,
			],
			'points'    => [
				'balance' => $advocate ? $advocate->points_balance ?? 0 : 0,
			],
			'rewards'   => [
				'available'       => $available_rewards_data,
				'available_count' => $available_rewards_count,
				'grouped'         => $advocate ? call_user_func_array(
				'array_merge',
				array_map(
					function ( $rewards, $reward_type_name ) use ( $expanded ) {
						$reward_type = $this->reward_types_factory->get_reward_type( $reward_type_name );

						if ( is_null( $reward_type ) ) {
							return [];
						}

						return $reward_type->prepare_rewards_objects_for_view(
							$rewards,
							[
								'expanded' => $expanded,
							]
						);
					},
					$grouped_rewards,
					array_keys( $grouped_rewards )
				)
			) : [],
			],
			'member_id'     => $advocate ? $advocate->id ?? 0 : 0,
			'form_nonce'    => wp_create_nonce( self::AJAX_NONCE ),
			'empty_rewards' => $advocate ? $this->get_empty_rewards_content() : '',
		];

		// Render the panel with integrated launcher.
		$panel_content_view->render( $content_args );
	}

	/**
	 * Attach the newly created WP User to an advocate if one exists
	 *
	 * @since  1.0.1
	 *
	 * @param int $user_id The User ID that was created.
	 * @return void
	 * @throws Exception
	 */
	public function connect_existing_advocate_to_new_user( int $user_id ): void {
		$advocate = get_member_by( 'user_id', $user_id );

		if ( $advocate ) {
			return; // Advocate is connected to a WP User already.
		}

		$user = get_userdata( $user_id );

		if ( ! is_a( $user, WP_User::class ) ) {
			return; // Not a WP User.
		}

		$advocate = get_member_by( 'email', $user->user_email );

		if ( ! $advocate ) {
			return; // No advocate found with this email.
		}

		if ( is_a( $advocate->get_wp_user(), WP_User::class ) ) {
			return; // Customer is already a WP User.
		}

		$advocate->user_id = absint( $user_id );
		$advocate->token   = null;
		$advocate->update();

		// Activate pending rewards for the newly official member.
		$this->rewards_service->activate_pending_rewards_for_member( $advocate->id );
	}

	/**
	 * Retrieve the content when there are no rewards to display.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function get_empty_rewards_content(): string {
		$rewards_option = get_option_value( RewardsOption::class );

		if ( empty( $rewards_option['advocate']['type'] ) ) {
			return $this->views_factory->get_view( EmptyRewardsView::class )->get_contents();
		}

		$reward_type = $this->reward_types_factory->get_reward_type( $rewards_option['advocate']['type'] );

		if ( is_null( $reward_type ) ) {
			return $this->views_factory->get_view( EmptyRewardsView::class )->get_contents();
		}

		return $reward_type->get_empty_rewards_content();
	}

	/**
	 * Update the last login timestamp for the advocate.
	 *
	 * @since 1.0.0
	 *
	 * @param string  $user_login The user's login name.
	 * @param WP_User $user The user object.
	 * @return void
	 */
	public function action_update_last_login( string $user_login, WP_User $user ): void {
		$advocate = get_member_by( 'user_id', $user->ID );

		if ( $advocate ) {
			$advocate->update_meta( 'last_login_at', current_time( 'mysql' ) );
		}
	}

	/**
	 * AJAX handler for creating new members.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function ajax_create_member(): void {
		// Verify nonce.
		if ( ! check_ajax_referer( self::AJAX_NONCE, 'nonce', false ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Security check failed.', 'rewardswp' ),
				],
				403
			);
		}

		// Validate anti-spam protection.
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above.
		if ( ! $this->anti_spam_service->validate( $_POST ) ) {
			wp_send_json_error(
				[
					'message' => $this->anti_spam_service->get_last_error(),
				],
				400
			);
		}

		// Check if user is already a full member (not guest).
		if ( is_current_user_a_member() && ! is_current_member_a_guest() ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'You are already a member.', 'rewardswp' ),
				],
				400
			);
		}

		$is_guest_upgrade = is_current_member_a_guest();

		// Handle guest member upgrade to full member.
		if ( $is_guest_upgrade ) {
			$this->handle_guest_member_upgrade();
			return;
		}

		// Handle logged-in user registration.
		if ( is_user_logged_in() ) {
			$this->handle_logged_in_user_registration();
			return;
		}

		// Handle non-logged-in user registration.
		$this->handle_guest_user_registration();
	}

	/**
	 * Handle guest member upgrade to full WordPress member.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	private function handle_guest_member_upgrade(): void {
		// Guest member upgrading to full member - get data from existing member.
		$current_member = get_current_member();

		if ( ! $current_member ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Unable to find your member data.', 'rewardswp' ),
				],
				400
			);
		}

		// Check if email already has a WordPress user account.
		$existing_user = get_user_by( 'email', $current_member->email );
		if ( $existing_user ) {
			wp_send_json_error(
				[
					'message'     => esc_html__( 'An account with this email already exists. Please sign in to upgrade your membership.', 'rewardswp' ),
					'needs_login' => true,
				],
				400
			);
		}

		// Generate password and create WordPress user.
		$password   = wp_generate_password( 12, false );
		$wp_user_id = wp_create_user( $current_member->email, $password, $current_member->email );

		if ( is_wp_error( $wp_user_id ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Unable to create your account. Please try again.', 'rewardswp' ),
				],
				400
			);
		}

		// Update user first/last name.
		wp_update_user(
			[
				'ID'         => $wp_user_id,
				'first_name' => $current_member->first_name ?: '',
				'last_name'  => $current_member->last_name ?: '',
			]
		);

		// Update only specific fields to avoid overwriting points_balance
		// that may have been updated by hooks during wp_create_user().
		$current_member->update_fields(
			[
				'user_id' => $wp_user_id,
				'token'   => null,
				'status'  => 'active',
			]
		);

		// Activate pending rewards for the newly official member.
		$this->rewards_service->activate_pending_rewards_for_member( $current_member->id );

		// Note: Points are awarded automatically via user_register hook when wp_create_user() is called above.
		// Account credentials are handled by WordPress native wp_new_user_notification().

		// Log in the newly created user.
		login_user_programmatically( $wp_user_id );

		// Refresh member to get updated points balance.
		$current_member = get_member_by( 'id', $current_member->id );

		// Get signup points from points entries.
		$signup_points = 0;
		if ( $current_member ) {
			$signup_points_entries = get_points_entries(
				[
					'member_id'   => $current_member->id,
					'source_type' => 'signup',
				],
				1,
				1
			);

			if ( ! empty( $signup_points_entries ) ) {
				$signup_points = absint( $signup_points_entries[0]->points ?? 0 );
			}
		}

		// Success response for guest upgrade.
		wp_send_json_success(
			[
				'message'           => esc_html__( 'Account upgraded successfully!', 'rewardswp' ),
				'member_id'         => $current_member->id,
				'member_name'       => $current_member->get_first_name(),
				'upgraded'          => true,
				'points_balance'    => $current_member->points_balance ?? 0,
				'signup_points'     => $signup_points,
				'referral_link'     => $current_member->get_referral_url(),
				'fresh_nonce'       => wp_create_nonce( self::AJAX_NONCE ),
				'fresh_claim_nonce' => wp_create_nonce( ClaimController::AJAX_NONCE ),
			]
		);
	}

	/**
	 * Handle logged-in user registration as member.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	private function handle_logged_in_user_registration(): void {
		$current_user = wp_get_current_user();
		$member_data  = [
			'user_id'    => $current_user->ID,
			'email'      => $current_user->user_email,
			'first_name' => $current_user->first_name ?: '',
			'last_name'  => $current_user->last_name ?: '',
		];

		// Create the member.
		$member = create_member( $member_data );

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

		// Award signup points for logged-in users.
		do_action( 'rewardswp_award_points_on_signup', $current_user->ID );

		// Refresh member to get updated points balance.
		$member = get_member_by( 'id', $member->id );

		if ( ! $member ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Something went wrong. Please try again.', 'rewardswp' ),
				],
				500
			);
		}

		// Get signup points from points entries.
		$signup_points    = 0;
		$signup_points_entries = get_points_entries(
			[
				'member_id'   => $member->id,
				'source_type' => 'signup',
			],
			1,
			1
		);

		if ( ! empty( $signup_points_entries ) ) {
			$signup_points = absint( $signup_points_entries[0]->points ?? 0 );
		}

		// Success response.
		wp_send_json_success(
			[
				'message'           => esc_html__( 'Welcome! You are now a member.', 'rewardswp' ),
				'member_id'         => $member->id,
				'member_name'       => $member->get_first_name(),
				'points_balance'    => $member->points_balance ?? 0,
				'signup_points'     => $signup_points,
				'referral_link'     => $member->get_referral_url(),
				'fresh_nonce'       => wp_create_nonce( self::AJAX_NONCE ),
				'fresh_claim_nonce' => wp_create_nonce( ClaimController::AJAX_NONCE ),
			]
		);
	}

	/**
	 * Handle guest user registration (creates WordPress user + member).
	 *
	 * @since 1.1.0
	 * @return void
	 */
	private function handle_guest_user_registration(): void {
		// Get data from form submission.
		// Nonce verification is handled in the parent method ajax_create_member().
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$name  = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';

		// Validate required fields.
		if ( empty( $name ) || empty( $email ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Name and email are required.', 'rewardswp' ),
				],
				400
			);
		}

		// Validate email format.
		if ( ! is_email( $email ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Please enter a valid email address.', 'rewardswp' ),
				],
				400
			);
		}

		// Check if guest member exists with this email.
		$existing_guest_member = get_member_by( 'email', $email );
		$linked_guest_account  = false;

		// Check if email already has a WordPress user account.
		$existing_user = get_user_by( 'email', $email );
		if ( $existing_user ) {
			wp_send_json_error(
				[
					'message'            => __( 'We couldn\'t create your account.', 'rewardswp' ),
					'redirect_to_login' => true,
				],
				400
			);
		} else {
			// No WP user exists - create one.
			// Parse name into first and last name using helper function.
			$name_parts = parse_name( trim( $name ) );
			$first_name = $name_parts['first_name'] ?? '';
			$last_name  = $name_parts['last_name'] ?? '';

			// Generate password and create user.
			$password   = wp_generate_password( 12, false );
			$wp_user_id = wp_create_user( $email, $password, $email );

			if ( is_wp_error( $wp_user_id ) ) {
				wp_send_json_error(
					[
						'message' => esc_html__( 'Unable to create your account. Please try again.', 'rewardswp' ),
					],
					400
				);
			}

			// Update user first/last name.
			wp_update_user(
				[
					'ID'         => $wp_user_id,
					'first_name' => $first_name,
					'last_name'  => $last_name,
				]
			);

			// If guest member exists, link to new WP user.
			if ( $existing_guest_member && $existing_guest_member->is_guest() ) {
				// Update only specific fields to avoid overwriting points_balance
				// that may have been updated by hooks during wp_create_user().
				$existing_guest_member->update_fields(
					[
						'user_id'    => $wp_user_id,
						'first_name' => $first_name,
						'last_name'  => $last_name,
						'status'     => 'active',
						'token'      => null,
					]
				);
				$member               = $existing_guest_member;
				$linked_guest_account = true;

				// Activate pending rewards for the newly official member.
				$this->rewards_service->activate_pending_rewards_for_member( $member->id );
			} else {
				// Create new member.
				$member_data = [
					'user_id'    => $wp_user_id,
					'email'      => $email,
					'first_name' => $first_name,
					'last_name'  => $last_name,
				];

				$member = create_member( $member_data );

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

			// Award signup points. Account credentials are handled by WordPress native wp_new_user_notification().
			do_action( 'rewardswp_award_points_on_signup', $wp_user_id );

			// Log in the newly created user.
			login_user_programmatically( $wp_user_id );

			// Refresh member to get updated points balance.
			$member = get_member_by( 'id', $member->id );
		}

		// Get signup points from points entries.
		$signup_points = 0;
		if ( $member ) {
			$signup_points_entries = get_points_entries(
				[
					'member_id'   => $member->id,
					'source_type' => 'signup',
				],
				1,
				1
			);

			if ( ! empty( $signup_points_entries ) ) {
				$signup_points = absint( $signup_points_entries[0]->points ?? 0 );
			}
		}

		// Success response.
		wp_send_json_success(
			[
				'message'              => $linked_guest_account
					? esc_html__( 'Account created! Your points have been preserved.', 'rewardswp' )
					: esc_html__( 'Welcome! You are now a member.', 'rewardswp' ),
				'member_id'            => $member->id,
				'member_name'          => $member->get_first_name(),
				'points_balance'       => $member->points_balance ?? 0,
				'signup_points'        => $signup_points,
				'referral_link'        => $member->get_referral_url(),
				'fresh_nonce'          => wp_create_nonce( self::AJAX_NONCE ),
				'fresh_claim_nonce'    => wp_create_nonce( ClaimController::AJAX_NONCE ),
				'linked_guest_account' => $linked_guest_account,
				'member'               => [
					'id'             => $member->id,
					'email'          => $member->email,
					'name'           => $member->get_first_name(),
					'points_balance' => $member->points_balance ?? 0,
					'referral_link'  => $member->get_referral_url(),
				],
			]
		);
	}

	/**
	 * AJAX handler for user login.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function ajax_login_user(): void {
		// Verify nonce.
		if ( ! check_ajax_referer( self::AJAX_NONCE, 'nonce', false ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Security check failed.', 'rewardswp' ),
				],
				403
			);
		}

		// Check if user is already logged in.
		if ( is_user_logged_in() ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'You are already logged in.', 'rewardswp' ),
				],
				400
			);
		}

		// Get login data from form submission.
		$login    = isset( $_POST['email'] ) ? sanitize_text_field( wp_unslash( $_POST['email'] ) ) : '';
		// Passwords should not be sanitized to preserve special characters.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$password = isset( $_POST['password'] ) ? wp_unslash( $_POST['password'] ) : '';

		// Validate required fields.
		if ( empty( $login ) || empty( $password ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Email/username and password are required.', 'rewardswp' ),
				],
				400
			);
		}

		// Attempt to authenticate user (accepts both email and username).
		$user = wp_authenticate( $login, $password );

		if ( is_wp_error( $user ) ) {
			// Use generic message for security - prevents username enumeration attacks.
			wp_send_json_error(
				[
					'message' => esc_html__( 'Invalid email/username or password.', 'rewardswp' ),
				],
				401
			);
		}

		// Log the user in.
		wp_set_current_user( $user->ID );
		wp_set_auth_cookie( $user->ID, true );

		// Look up member record for in-place panel transition.
		$member = get_member_by( 'user_id', $user->ID );

		if ( $member ) {
			wp_send_json_success(
				[
					'message'           => esc_html__( 'Login successful!', 'rewardswp' ),
					'member_id'         => $member->id,
					'member_name'       => $member->get_first_name(),
					'points_balance'    => $member->points_balance ?? 0,
					'referral_link'     => $member->get_referral_url(),
					'fresh_nonce'       => wp_create_nonce( self::AJAX_NONCE ),
					'fresh_claim_nonce' => wp_create_nonce( ClaimController::AJAX_NONCE ),
					'is_new_member'     => false,
				]
			);
		}

		// No member record — create one.
		$member_data = [
			'user_id'    => $user->ID,
			'email'      => $user->user_email,
			'first_name' => $user->first_name ?: '',
			'last_name'  => $user->last_name ?: '',
		];

		$member = create_member( $member_data );

		if ( is_wp_error( $member ) ) {
			// Creation failed — fall back to page reload.
			wp_send_json_success(
				[
					'message'   => esc_html__( 'Login successful! Redirecting...', 'rewardswp' ),
					'user_id'   => $user->ID,
					'user_name' => $user->display_name,
				]
			);
		}

		// Award signup points.
		do_action( 'rewardswp_award_points_on_signup', $user->ID );

		// Refresh member to get updated points balance.
		$member = get_member_by( 'id', $member->id );

		if ( ! $member ) {
			// Refresh failed — fall back to page reload.
			wp_send_json_success(
				[
					'message'   => esc_html__( 'Login successful! Redirecting...', 'rewardswp' ),
					'user_id'   => $user->ID,
					'user_name' => $user->display_name,
				]
			);
		}

		wp_send_json_success(
			[
				'message'           => esc_html__( 'Login successful!', 'rewardswp' ),
				'member_id'         => $member->id,
				'member_name'       => $member->get_first_name(),
				'points_balance'    => $member->points_balance ?? 0,
				'referral_link'     => $member->get_referral_url(),
				'fresh_nonce'       => wp_create_nonce( self::AJAX_NONCE ),
				'fresh_claim_nonce' => wp_create_nonce( ClaimController::AJAX_NONCE ),
				'is_new_member'     => true,
			]
		);
	}

	/**
	 * Check if the panel should auto-open for friend claim mode.
	 * This happens when there's a referral code in the URL and the user is not a member.
	 *
	 * @since 1.1.0
	 * @return bool True if panel should auto-open for friend claim.
	 */
	private function should_auto_open_panel_for_friend_claim(): bool {
		// Use PanelService to check all conditions for friend claims:
		// - Referrals are enabled
		// - Pro license is active
		// - Friend reward is configured with a valid type.
		if ( ! $this->panel_service->can_friends_claim_rewards() ) {
			return false;
		}

		return ! empty( $this->referral_code_service->get_active_referral_code() );
	}

	/**
	 * AJAX handler for refreshing rewards data.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function ajax_refresh_rewards(): void {
		// Verify nonce.
		if ( ! check_ajax_referer( self::AJAX_NONCE, 'nonce', false ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Security check failed.', 'rewardswp' ),
				],
				403
			);
		}

		// Get current member.
		$advocate = get_current_member();

		if ( ! $advocate ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Member not found.', 'rewardswp' ),
				],
				400
			);
		}

		// Get updated rewards data.
		$available_rewards = $this->rewards_service->get_advocate_rewards(
			$advocate->id,
			[
				'status' => 'issued',
			]
		);

		// Prepare rewards data for Alpine.js.
		$rewards_data = [];
		foreach ( $available_rewards as $reward ) {
			$rewards_data[] = $this->prepare_reward_for_alpine( $reward );
		}

		// Success response.
		wp_send_json_success(
			[
				'rewards'        => $rewards_data,
				'rewards_count'  => count( $rewards_data ),
				'points_balance' => $advocate->points_balance ?? 0,
			]
		);
	}

	/**
	 * Prepare reward data for Alpine.js consumption.
	 *
	 * @since 1.1.0
	 * @param object $reward The reward object.
	 * @return array Formatted reward data.
	 */
	private function prepare_reward_for_alpine( $reward ): array {
		// Get reward info from RewardType classes.
		$reward_type_instance = $this->reward_types_factory->get_reward_type( $reward->reward_type );

		if ( ! $reward_type_instance ) {
			$reward_info = [
				'title'       => __( 'Reward', 'rewardswp' ),
				'description' => __( 'Reward available', 'rewardswp' ),
				'icon'        => 'reward-free-product',
			];
		} else {
			// Set the reward object in the reward type instance.
			$reward_type_instance->set_reward( $reward );

			$reward_info = [
				'title'       => $reward_type_instance->get_panel_title(),
				'description' => method_exists( $reward_type_instance, 'get_panel_instructions' )
					? $reward_type_instance->get_panel_instructions()
					: $reward_type_instance->get_panel_title(),
				'icon'        => $reward_type_instance->get_icon_name(),
			];
		}

		// Calculate if expired (used for filtering in the controller).
		$is_expired = false;
		if ( ! empty( $reward->expires_at ) ) {
			$days_until_expiry = get_date_diff_in_days( $reward->expires_at );
			$is_expired        = $days_until_expiry <= 0;
		}

		// Build simplified coupon terms array (matches points redemption format).
		// Note: Expired rewards are filtered out before display, so we always build terms.
		$coupon_terms = [];

		// 1. Minimum spend or "Applies to entire order" (except for free product).
		$minimum_order_amount = 0;
		if ( 'free_product' === $reward->reward_type ) {
			// First try to get from reward meta (saved during Points redemption).
			$minimum_order_amount = floatval( $reward->get_meta( 'minimum_order_amount' ) ?? 0 );

			// Fallback to settings for Referral rewards that don't have meta.
			if ( 0 === $minimum_order_amount ) {
				$rewards_settings = get_option_value( RewardsOption::class );
				$context          = $reward->recipient_type ?? 'advocate'; // advocate or friend.
				if ( ! empty( $rewards_settings[ $context ]['minimum_order_amount'] ) ) {
					$minimum_order_amount = floatval( $rewards_settings[ $context ]['minimum_order_amount'] );
				}
			}
		}

		if ( $minimum_order_amount > 0 ) {
			// Show "$X min spend" which implies it applies to the order.
			$coupon_terms[] = sprintf(
				/* translators: %s: minimum order amount with currency symbol */
				__( '%s min spend', 'rewardswp' ),
				format_currency( $minimum_order_amount )
			);
		} elseif ( 'free_product' !== $reward->reward_type ) {
			// Only show "Applies to entire order" for non-free-product rewards.
			$coupon_terms[] = __( 'Applies to entire order', 'rewardswp' );
		}

		// 2. Expiry - check configuration first, then show actual date if applicable.
		$expiry_text = '';
		$should_never_expire = false;

		// Identify reward source and get expiry configuration.
		if ( $reward->referral_id > 0 ) {
			// Referrals reward - check RewardsOption configuration.
			$rewards_settings = get_option_value( RewardsOption::class );
			$context          = $reward->recipient_type ?? 'advocate'; // advocate or friend.
			$reward_expiration = $rewards_settings[ $context ]['reward_expiration'] ?? '';

			// Check if configured to never expire.
			if ( empty( $reward_expiration ) || 'never' === $reward_expiration ) {
				$should_never_expire = true;
			}
		} else {
			// Points reward - check SpendActionsOption configuration.
			$spend_action_id = $reward->get_meta( 'spend_action_id' );
			if ( ! empty( $spend_action_id ) ) {
				$spend_actions_option = get_option_object( SpendActionsOption::class );
				if ( $spend_actions_option ) {
					$action_config = $spend_actions_option->find_action_config( $spend_action_id, $reward->reward_type );
					if ( $action_config ) {
						$coupon_duration = $action_config['coupon_duration'] ?? 'never';
						// Check if configured to never expire.
						if ( empty( $coupon_duration ) || 'never' === $coupon_duration ) {
							$should_never_expire = true;
						}
					}
				}
			}
		}

		// Display expiry based on configuration and expires_at value.
		if ( $should_never_expire ) {
			// Configuration says "Never expires" - show "No expiry" regardless of expires_at.
			$expiry_text    = __( 'No expiry', 'rewardswp' );
			$coupon_terms[] = $expiry_text;
		} elseif ( ! empty( $reward->expires_at ) ) {
			// Configuration allows expiry and expires_at is set - show expiry date.
			$expires_timestamp = strtotime( $reward->expires_at );
			$expiry_text       = sprintf(
				/* translators: %s: expiration date */
				__( 'Expires %s', 'rewardswp' ),
				date_i18n( 'M j, Y', $expires_timestamp )
			);
			$coupon_terms[]    = $expiry_text;
		} else {
			// No expires_at set - show "No expiry".
			$expiry_text    = __( 'No expiry', 'rewardswp' );
			$coupon_terms[] = $expiry_text;
		}

		// Join with bullet separator.
		$coupon_terms_text = implode( ' • ', $coupon_terms );

		// Generate icon HTML using IconHelper from the view.
		/**
		 * Rewards view instance.
		 *
		 * @var \Awesomemotive\Rewardswp\Views\Panel\Advocate\RewardsWidgetView $rewards_view
		 */
		$rewards_view = $this->advocate_panel_content_view;

		ob_start();
		$rewards_view->get_icon_helper()->render_icon( $reward_info['icon'], 'w-8 h-8' );
		$icon_html = ob_get_clean();

		$coupon_code       = $reward->get_meta( 'coupon_code' ) ?? '';
		$is_coupon_applied = false;

		// Check if coupon is applied (only if integration supports coupons).
		if ( ! empty( $coupon_code ) && method_exists( $this->integration, 'is_coupon_applied' ) ) {
			$is_coupon_applied = $this->integration->is_coupon_applied( $coupon_code );
		}

		// Get points cost from associated points entry.
		$points_cost    = 0;
		$points_entries = get_points_entries(
			[
				'source_type' => 'reward_redemption',
				'source_id'   => $reward->id,
				'type'        => 'spent',
			],
			1,
			1
		);

		if ( ! empty( $points_entries ) ) {
			// Points are stored as negative for spending, so use absolute value.
			$points_cost = absint( $points_entries[0]->points );
		}

		$points_label = get_points_currency_plural();

		// Check if free_product reward has out-of-stock product and get product name.
		$is_out_of_stock    = false;
		$product_name       = '';
		$reward_description = '';
		$free_product_price = 0;
		if ( 'free_product' === $reward->reward_type ) {
			$free_product_id    = $reward->get_meta( 'free_product_id' );
			$product_name       = $reward->get_meta( 'free_product_name' ) ?? '';
			$reward_description = $reward->get_meta( 'reward_description' ) ?? '';

			if ( $free_product_id && method_exists( $this->integration, 'is_product_in_stock' ) ) {
				$is_out_of_stock = ! $this->integration->is_product_in_stock( $free_product_id );
			}

			// Get product price for minimum spend calculation.
			if ( $free_product_id && method_exists( $this->integration, 'get_product' ) ) {
				$product_data = $this->integration->get_product( $free_product_id );
				if ( is_array( $product_data ) && isset( $product_data['price'] ) ) {
					$free_product_price = floatval( $product_data['price'] );
				}
			}
		}

		// Override title with product name for free_product rewards.
		// Priority: reward_description (custom name) > free_product_name (real name).
		// If product_value is set, show "$X off [Product]" instead of "Free [Product]".
		$display_title = $reward_info['title'];
		if ( 'free_product' === $reward->reward_type ) {
			// Use custom description if set, otherwise fall back to product name.
			$display_name = ! empty( $reward_description ) ? $reward_description : $product_name;

			if ( ! empty( $display_name ) ) {
				$product_value = $reward->get_meta( 'product_value' );
				if ( ! empty( $product_value ) && floatval( $product_value ) > 0 ) {
					$display_title = sprintf(
						/* translators: %1$s: formatted currency amount (e.g., "$500"), %2$s: product name */
						__( '%1$s off %2$s', 'rewardswp' ),
						format_currency( floatval( $product_value ) ),
						$display_name
					);
				} else {
					$display_title = sprintf(
						/* translators: %s: product name */
						__( 'Free %s', 'rewardswp' ),
						$display_name
					);
				}
			}
		}

		return [
			'id'                   => $reward->id,
			'title'                => $display_title,
			'description'          => $reward_info['description'],
			'coupon_code'          => $coupon_code,
			'is_applied'           => $is_coupon_applied,
			'coupon_terms'         => $coupon_terms_text,
			'expiry_text'          => $expiry_text,
			'is_expired'           => $is_expired,
			'is_out_of_stock'      => $is_out_of_stock,
			'icon_html'            => $icon_html,
			'points_cost'          => $points_cost,
			'points_label'         => $points_label,
			'product_name'         => $product_name,
			'reward_type'          => $reward->reward_type,
			'free_product_id'      => $free_product_id ?? null,
			'minimum_order_amount' => $minimum_order_amount,
			'free_product_price'   => $free_product_price,
		];
	}

	/**
	 * AJAX handler to clear the show_welcome_flow meta flag.
	 *
	 * This ensures the welcome flow only shows once for guest members after their first purchase.
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function ajax_clear_welcome_flow(): void {
		// Verify nonce.
		if ( ! check_ajax_referer( self::AJAX_NONCE, 'nonce', false ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid security token.', 'rewardswp' ) ], 403 );
		}

		// Get member ID from request.
		$member_id = isset( $_POST['member_id'] ) ? absint( $_POST['member_id'] ) : 0;

		if ( ! $member_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid member ID.', 'rewardswp' ) ], 400 );
		}

		// Get the member.
		$member = get_member_by( 'id', $member_id );

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

		// Clear the meta flag.
		$member->delete_meta( 'show_welcome_flow' );

		wp_send_json_success( [ 'message' => __( 'Welcome flow cleared.', 'rewardswp' ) ] );
	}

	/**
	 * AJAX handler to set a member's birthday.
	 *
	 * @since 1.3.0
	 * @return void
	 */
	public function ajax_set_birthday(): void {
		// Verify nonce.
		if ( ! check_ajax_referer( self::AJAX_NONCE, 'nonce', false ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Security check failed.', 'rewardswp' ),
				],
				403
			);
		}

		// Get current member.
		$member = get_current_member();

		if ( ! $member ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'You must be logged in to set your birthday.', 'rewardswp' ),
				],
				401
			);
		}

		// Get month and day from request.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with absint below.
		$month = isset( $_POST['month'] ) ? absint( wp_unslash( $_POST['month'] ) ) : 0;
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with absint below.
		$day   = isset( $_POST['day'] ) ? absint( wp_unslash( $_POST['day'] ) ) : 0;

		if ( ! $month || ! $day ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Please provide both month and day.', 'rewardswp' ),
				],
				400
			);
		}

		// Use the birthday service to set the birthday.
		$result = $this->birthday_service->set_birthday( $member, $month, $day );

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

		// Build success response with reward preview.
		$reward_preview = $this->birthday_service->get_reward_preview( $month, $day );

		$response = [
			'message'  => esc_html__( 'Birthday saved successfully!', 'rewardswp' ),
			'birthday' => $this->birthday_service->format_birthday_for_display(
				$this->birthday_service->get_birthday( $member )
			),
		];

		if ( $reward_preview ) {
			$response['reward_date']    = $reward_preview['reward_date'];
			$response['reward_points']  = $reward_preview['points'];
			$response['is_next_year']   = $reward_preview['is_next_year'];
		}

		$edit_notice = $this->birthday_service->get_birthday_edit_notice( $member );
		if ( $edit_notice ) {
			$response['notice'] = $edit_notice;
		}

		wp_send_json_success( $response );
	}

	/**
	 * AJAX handler for getting member activity logs.
	 *
	 * Returns paginated activity data for the frontend widget.
	 * Only available for logged-in members (not guests).
	 *
	 * @since 1.2.0
	 * @return void
	 */
	public function ajax_get_member_activity(): void {
		// Verify nonce.
		if ( ! check_ajax_referer( self::AJAX_NONCE, 'nonce', false ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Security check failed.', 'rewardswp' ),
				],
				403
			);
		}

		// Check if user is a member.
		if ( ! is_current_user_a_member() ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Activity logs are only available for members.', 'rewardswp' ),
				],
				403
			);
		}

		// Get current member.
		$member = get_current_member();
		if ( ! $member ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Unable to find your member data.', 'rewardswp' ),
				],
				400
			);
		}

		// Get request parameters.
		$tab      = isset( $_POST['tab'] ) ? sanitize_text_field( wp_unslash( $_POST['tab'] ) ) : 'points';
		$page     = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
		$per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;

		// Limit per_page to reasonable values.
		$per_page = min( max( $per_page, 1 ), self::MAX_ACTIVITY_ITEMS_PER_PAGE );
		$offset   = ( $page - 1 ) * $per_page;

		$items         = [];
		$total         = 0;
		$status_counts = null;

		switch ( $tab ) {
			case 'points':
				$raw_items = $this->activity_repository->get_points_activity( $member->id, $per_page, $offset );
				$total     = $this->activity_repository->count_points_activity( $member->id );
				$items     = $this->activity_formatter_service->format_points_activity( $raw_items );

				// Get status counts using PointsService (similar to ReferralsService pattern)
				$status_counts = [
					'total'    => $this->points_service->count_points_entries( [ 'member_id' => $member->id ] ),
					'approved' => $this->points_service->count_points_entries( [ 'member_id' => $member->id, 'status' => 'approved' ] ),
					'pending'  => $this->points_service->count_points_entries( [ 'member_id' => $member->id, 'status' => 'pending' ] ),
				];

				// Calculate sum of pending points
				$pending_points_sum = $this->points_service->get_pending_points_sum( $member->id );
				$status_counts['pending_points_sum'] = $pending_points_sum;
				break;

		case 'referrals':
			$raw_items = $this->activity_repository->get_referrals_activity( $member->id, $per_page, $offset );
			$total     = $this->activity_repository->count_referrals_activity( $member->id );
			$items     = $this->activity_formatter_service->format_referrals_activity( $raw_items );

			// Get status counts using ReferralsService
			$status_counts = [
				'total'     => $this->referrals_service->count_referrals( [ 'advocate_id' => $member->id, 'status' => [ 'completed', 'pending' ] ] ),
				'completed' => $this->referrals_service->count_referrals( [ 'advocate_id' => $member->id, 'status' => 'completed' ] ),
				'pending'   => $this->referrals_service->count_referrals( [ 'advocate_id' => $member->id, 'status' => 'pending' ] ),
			];
			break;

		default:
				wp_send_json_error(
					[
						'message' => esc_html__( 'Invalid tab specified.', 'rewardswp' ),
					],
					400
				);
		}

		$has_more = ( $offset + count( $items ) ) < $total;

		$response_data = [
			'items'    => $items,
			'total'    => $total,
			'page'     => $page,
			'per_page' => $per_page,
			'has_more' => $has_more,
		];

		// Include status_counts for both points and referrals tabs
		if ( isset( $status_counts ) ) {
			$response_data['status_counts'] = $status_counts;
		}

		wp_send_json_success( $response_data );
	}
}
