<?php

namespace WC_Affiliate\API;

defined( 'ABSPATH' ) || exit;

use WC_Affiliate\Models\Affiliate as AffiliateModel;
use WC_Affiliate\Models\User;
use WC_Affiliate\Traits\Auth;
use WC_Affiliate\Traits\Date;
use WC_Affiliate\Traits\Rest;
use WC_Affiliate\Traits\Validation;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_User;

/**
 * REST API Controller for Affiliate Management.
 *
 * Handles REST API requests related to affiliates, including CRUD operations, applications, links, leaderboards, and statistics.
 *
 * @since 1.0.0
 */
class Affiliate {

	use Auth;
	use Date;
	use Rest;
	use Validation;

	/**
	 * Affiliate model instance.
	 *
	 * @var AffiliateModel
	 */
	protected AffiliateModel $model;

	/**
	 * Affiliate API Controller.
	 *
	 * Handles REST API requests related to affiliates, including CRUD operations, applications, links, leaderboards, and statistics.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->model = new AffiliateModel();
	}

	/**
	 * Get affiliates list.
	 *
	 * Retrieves a paginated list of affiliates, optionally filtered by search term or status.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing optional search, status, page, and per_page parameters.
	 *
	 * @return WP_REST_Response The response with affiliates list and pagination headers.
	 */
	public function list( WP_REST_Request $request ): WP_REST_Response {
		$pagination = $this->get_pagination_params( $request );

		$search = sanitize_text_field( $request->get_param( 'search' ) );
		$status = sanitize_text_field( $request->get_param( 'status' ) );

		$args = array(
			'limit'   => $pagination['limit'],
			'offset'  => $pagination['offset'],
			'orderby' => 'ID',
			'order'   => 'DESC',
		);
		if ( $search ) {
			$args['search'] = $search;
		}
		if ( $status ) {
			$args['status'] = $status;
		}

		$affiliates       = $this->model->get_affiliates( $args );
		$total_affiliates = $this->model->get_count( $args );

		$data = array();
		foreach ( $affiliates as $affiliate ) {
			$data[] = $affiliate->to_array();
		}

		/**
		 * Filter affiliates list data before response.
		 *
		 * Allows modification of the affiliates list data.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $data    Array of affiliate data.
		 * @param array           $args    Query arguments.
		 * @param WP_REST_Request $request The original request object.
		 */
		$data = apply_filters( 'wc_affiliate_rest_api_affiliates_list_data', $data, $args, $request );

		$response = $this->response_success( $data );

		return $this->add_pagination_headers( $response, $total_affiliates, $pagination['page'], $pagination['per_page'] );
	}

	/**
	 * Create new affiliate.
	 *
	 * Creates a new affiliate for the specified user, with optional commission settings and status.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing user_id, commission_amount, commission_type, and status parameters.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with the created affiliate data or a WP_Error on failure.
	 */
	public function create( WP_REST_Request $request ) {
		$user_id           = (int) $request->get_param( 'user_id' );
		$commission_amount = (float) $request->get_param( 'commission_amount' );
		$commission_type   = sanitize_text_field( $request->get_param( 'commission_type' ) );
		$status            = sanitize_text_field( $request->get_param( 'status' ) );

		$commission_type = ! empty( $commission_type ) ? $commission_type : 'default';
		if ( 'default' === $commission_type ) {
			$commission_type   = wca_get_commission_type();
			$commission_amount = wca_get_commission_amount();
		}

		$affiliate_data = array(
			'user_id'           => $user_id,
			'status'            => ! empty( $status ) ? $status : 'pending',
			'commission_type'   => $commission_type,
			'commission_amount' => $commission_amount,
		);

		$result = $this->model->create( $affiliate_data );

		if ( is_wp_error( $result ) ) {
			return $this->response_error( $result->get_error_message() );
		}

		// Fire action after affiliate creation.
		do_action( 'wc_affiliate_affiliate_created', $result->get_id(), $affiliate_data, $request );

		$affiliate_data = $result->to_array();

		/**
		 * Filter created affiliate data before response.
		 *
		 * Allows modification of the newly created affiliate data.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $affiliate_data Created affiliate data.
		 * @param array           $input_data     Original input data.
		 * @param WP_REST_Request $request        The original request object.
		 */
		$affiliate_data = apply_filters( 'wc_affiliate_rest_api_affiliate_create_data', $affiliate_data, $affiliate_data, $request );

		return $this->response_success( $affiliate_data, __( 'Affiliate created successfully', 'wc-affiliate' ), 201 );
	}

	/**
	 * Get single affiliate.
	 *
	 * Retrieves detailed information for a specific affiliate by ID.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing id parameter.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with affiliate data or a WP_Error if not found.
	 */
	public function get( WP_REST_Request $request ) {
		$id        = (int) $request->get_param( 'affiliate_id' );
		$affiliate = new AffiliateModel( $id );

		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		$affiliate_data = $affiliate->to_array();

		/**
		 * Filter single affiliate data before response.
		 *
		 * Allows modification of the retrieved affiliate data.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $affiliate_data Affiliate data.
		 * @param int             $id             The affiliate ID.
		 * @param WP_REST_Request $request        The original request object.
		 */
		$affiliate_data = apply_filters( 'wc_affiliate_rest_api_affiliate_get_data', $affiliate_data, $id, $request );

		return $this->response_success( $affiliate_data );
	}

	/**
	 * Update affiliate.
	 *
	 * Updates specified fields for an existing affiliate.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing id and optional update parameters (status, commission_type, etc.).
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with updated affiliate data or a WP_Error on failure.
	 */
	public function update( WP_REST_Request $request ) {
		$id        = (int) $request->get_param( 'affiliate_id' );
		$affiliate = new AffiliateModel( $id );

		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		$old_status  = '';
		$update_data = array();
		if ( $request->get_param( 'status' ) ) {
			$old_status = $affiliate->get_status();

			// Store new status.
			$update_data['status'] = sanitize_text_field( $request->get_param( 'status' ) );
		}

		if ( null !== $request->get_param( 'commission_type' ) ) {
			$update_data['commission_type'] = sanitize_text_field( $request->get_param( 'commission_type' ) );
		}

		if ( null !== $request->get_param( 'commission_amount' ) ) {
			$update_data['commission_amount'] = (float) $request->get_param( 'commission_amount' );
		}

		if ( null !== $request->get_param( 'payment_method' ) ) {
			$update_data['payment_method'] = sanitize_text_field( $request->get_param( 'payment_method' ) );
		}

		if ( null !== $request->get_param( 'payment_details' ) ) {
			$update_data['payment_details'] = sanitize_text_field( $request->get_param( 'payment_details' ) );
		}

		if ( null !== $request->get_param( 'auto_payout' ) ) {
			$update_data['auto_payout'] = sanitize_text_field( $request->get_param( 'auto_payout' ) );
		}

		// Update user display_name if provided.
		if ( $request->get_param( 'display_name' ) ) {
			$user_id = $affiliate->user_id;
			if ( $user_id ) {
				wp_update_user(
					array(
						'ID'           => $user_id,
						'display_name' => sanitize_text_field( $request->get_param( 'display_name' ) ),
					)
				);
				// Reload the affiliate model to reflect the updated user data.
				$affiliate->load();
			}
		}

		if ( empty( $update_data ) && ! $request->get_param( 'display_name' ) ) {
			return $this->response_error( __( 'No data provided for update', 'wc-affiliate' ) );
		}

		$result = $affiliate->update( $update_data );

		if ( is_wp_error( $result ) ) {
			return $this->response_error( $result->get_error_message() );
		}

		if ( isset( $update_data['status'] ) ) {
			$update_data['old_status'] = $old_status;
		}

		// Fire action after affiliate update.
		do_action( 'wc_affiliate_affiliate_updated', $id, $update_data, $request );

		$affiliate_data = $affiliate->to_array();

		/**
		 * Filter updated affiliate data before response.
		 *
		 * Allows modification of the updated affiliate data.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $affiliate_data Updated affiliate data.
		 * @param array           $update_data    Data that was updated.
		 * @param int             $id             The affiliate ID.
		 * @param WP_REST_Request $request        The original request object.
		 */
		$affiliate_data = apply_filters( 'wc_affiliate_rest_api_affiliate_update_data', $affiliate_data, $update_data, $id, $request );

		return $this->response_success( $affiliate_data, __( 'Affiliate updated successfully', 'wc-affiliate' ) );
	}

	/**
	 * Delete affiliate.
	 *
	 * Removes an affiliate and associated metadata.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing id parameter.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response or a WP_Error on failure.
	 */
	public function delete( WP_REST_Request $request ) {
		$id        = (int) $request->get_param( 'affiliate_id' );
		$affiliate = new AffiliateModel( $id );

		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		/**
		 * Filter affiliate data before deletion.
		 *
		 * Allows modification or prevention of affiliate deletion.
		 *
		 * @since 1.0.0
		 *
		 * @param int             $id      Affiliate ID being deleted.
		 * @param WP_REST_Request $request The original request object.
		 */
		$id = apply_filters( 'wc_affiliate_rest_api_affiliate_delete_before', $id, $request );

		$result = $affiliate->delete();

		if ( is_wp_error( $result ) ) {
			return $this->response_error( $result->get_error_message() );
		}

		// Optionally, you can also delete related data like commissions, links, etc..
		$user_model = new User( $affiliate->user_id );
		$user_model->delete_meta( '_wc_affiliate_status' );

		/**
		 * Action fired after affiliate deletion.
		 *
		 * Allows performing additional actions after deleting an affiliate.
		 *
		 * @since 1.0.0
		 *
		 * @param int $id The deleted affiliate ID.
		 */
		do_action( 'wc_affiliate_rest_api_affiliate_deleted', $id );

		return $this->response_success(
			null,
			__( 'Affiliate deleted successfully', 'wc-affiliate' )
		);
	}

	/**
	 * Get affiliate statistics.
	 *
	 * Retrieves comprehensive performance statistics for a specific affiliate over a given period.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing id and period parameters.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with statistics or a WP_Error if not found.
	 */
	public function get_stats( WP_REST_Request $request ) {
		$id     = (int) $request->get_param( 'affiliate_id' );
		$period = sanitize_text_field( $request->get_param( 'period' ) );

		$affiliate = new AffiliateModel( $id );
		$user      = $affiliate->get_user();

		if ( ! $affiliate->exists() && ! user_can( $user, 'manage_options' ) ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		// Calculate date range based on period..
		$stats_params = $this->get_date_range( $period );

		// Get affiliate-specific statistics..
		$visit_model       = new \WC_Affiliate\Models\Visit();
		$referral_model    = new \WC_Affiliate\Models\Referral();
		$transaction_model = new \WC_Affiliate\Models\Transaction();

		$stats_params['affiliate_id'] = $id;

		$visit_stats       = $visit_model->get_statistics( $stats_params );
		$referral_stats    = $referral_model->get_statistics( $stats_params );
		$transaction_stats = $transaction_model->get_statistics( $stats_params );

		// Get period statistics using the affiliate model method..
		$period_stats = $affiliate->get_period_statistics( $this->get_period_days( $period ) );

		// Compile comprehensive dashboard data..
		$stats = array(
			'period'     => array(
				'type'       => $period,
				'start_date' => $stats_params['start_date'],
				'end_date'   => $stats_params['end_date'],
			),
			'overview'   => array(
				'affiliate_stats'   => array(
					'total_affiliates'  => 1, // Single affiliate.
					'active_affiliates' => $affiliate->get_status() === 'active' ? 1 : 0,
				),
				'visit_stats'       => $visit_stats,
				'referral_stats'    => $referral_stats,
				'transaction_stats' => $transaction_stats,
			),
			'conversion' => array(
				'conversion_rate' => (float) $visit_stats['conversion_rate'],
				'avg_commission'  => (float) $referral_stats['avg_commission'],
				'avg_order_value' => $referral_stats['total_referrals'] > 0 ? (float) ( $referral_stats['total_sales'] / $referral_stats['total_referrals'] ) : 0,
			),
			'stats'      => array(
				'total_visits'        => (int) $period_stats['visits'],
				'converted_visits'    => (int) $visit_stats['converted_visits'],
				'total_referrals'     => (int) $period_stats['referrals'],
				'total_sales'         => (float) $period_stats['sales'],
				'conversion_rate'     => (float) $period_stats['conversion_rate'],
				'avg_commission'      => (float) $period_stats['avg_commission'],
				'total_balance'       => (float) $period_stats['total_balance'],
				'total_earnings'      => (float) $period_stats['total_earnings'],
				'total_commissions'   => (float) $referral_stats['total_commissions'],
				'paid_commissions'    => (float) $referral_stats['paid_commissions'],
				'pending_commissions' => (float) $referral_stats['unpaid_commissions'],
			),
		);

		foreach ( $stats['stats'] as $stat_key => $stat_value ) {
			$blocked_stats_for_formatting = array( 'total_visits', 'converted_visits', 'conversion_rate', 'total_referrals' );
			$blocked_stats_for_formatting = apply_filters( 'wc_affiliate_rest_api_blocked_stats_for_formatting', $blocked_stats_for_formatting, $stats['stats'] );
			if ( in_array( $stat_key, $blocked_stats_for_formatting, true ) ) {
				continue;
			}

			$stats['stats'][ "{$stat_key}_formatted" ] = wca_format_price( (float) $stat_value );
		}

		/**
		 * Filter affiliate statistics data before response.
		 *
		 * Allows modification of the affiliate statistics.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $stats   Statistics data.
		 * @param int             $id      The affiliate ID.
		 * @param string          $period  The period.
		 * @param WP_REST_Request $request The original request object.
		 */
		$stats = apply_filters( 'wc_affiliate_rest_api_affiliate_stats_data', $stats, $id, $period, $request );

		return $this->response_success( $stats );
	}

	/**
	 * Get current user's affiliate data.
	 *
	 * Retrieves the affiliate information for the currently authenticated user.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with user affiliate data or a WP_Error if not an affiliate.
	 */
	public function me( WP_REST_Request $request ) {
		$user_id   = get_current_user_id();
		$affiliate = $this->model->get_by_user_id( $user_id );

		if ( is_wp_error( $affiliate ) ) {
			return $this->response_error( $affiliate->get_error_message(), 404 );
		}

		$affiliate_data = $affiliate->to_array();

		/**
		 * Filter current user's affiliate data before response.
		 *
		 * Allows modification of the user's affiliate data.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $affiliate_data Affiliate data.
		 * @param int             $user_id        The current user ID.
		 * @param WP_REST_Request $request        The original request object.
		 */
		$affiliate_data = apply_filters( 'wc_affiliate_rest_api_affiliate_me_data', $affiliate_data, $user_id, $request );

		return $this->response_success( $affiliate_data );
	}

	/**
	 * Apply to become an affiliate.
	 *
	 * Handles both user registration (if not logged in) and affiliate application submission.
	 * Includes reCAPTCHA verification, referral tracking, and meta field updates.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing registration/application data.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with application result or a WP_Error on failure.
	 */
	public function apply( WP_REST_Request $request ) {
		$user_id       = get_current_user_id();
		$referrer_user = wca_get_user_by_referral_token();

		$referer_user_id = $referrer_user instanceof WP_User ? $referrer_user->ID : 0;

		// Collect all data from request params.
		$data = array(
			'user_id'            => $user_id,
			'referrer_user_id'   => $referer_user_id,
			'is_logged_in'       => ! empty( $user_id ),
			'is_new_user'        => false,
			'user_email'         => $request->get_param( 'email' ),
			'first_name'         => $request->get_param( 'first_name' ),
			'last_name'          => $request->get_param( 'last_name' ),
			'user_login'         => $request->get_param( 'user_login' ),
			'user_password'      => $request->get_param( 'user_password' ),
			'user_password2'     => $request->get_param( 'user_password2' ),
			'website_url'        => $request->get_param( 'website_url' ),
			'terms_agree'        => $request->get_param( 'terms_agree' ),
			'recaptcha_response' => $request->get_param( 'recaptcha_response' ),
			'promotion_method'   => $request->get_param( 'promotion_method' ),
			'commission_type'    => $request->get_param( 'commission_type' ),
			'commission_amount'  => $request->get_param( 'commission_amount' ),
		);

		/**
		 * Filter affiliate application data before processing.
		 *
		 * Allows pro plugins to modify the collected data before any validation or processing occurs.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $data    Collected data from request params.
		 * @param WP_REST_Request $request The original request object.
		 */
		$data = apply_filters( 'wc_affiliate_rest_api_affiliate_apply_data', $data, $request );

		// Update flags based on filtered data.
		$user_id     = $data['user_id'];
		$is_new_user = $data['is_new_user'];

		// Handle user registration if not logged in.
		if ( ! $data['is_logged_in'] ) {
			$user_registration_data = array(
				'first_name'     => $data['first_name'],
				'last_name'      => $data['last_name'],
				'user_login'     => $data['user_login'],
				'user_email'     => $data['user_email'],
				'user_password'  => $data['user_password'],
				'user_password2' => $data['user_password2'],
			);

			$user_registration_result = $this->register_user( $user_registration_data );

			if ( is_wp_error( $user_registration_result ) ) {
				return $this->response_error( $user_registration_result->get_error_message() );
			}

			$user_id     = $user_registration_result;
			$is_new_user = true;

			// Sign in the newly registered user.
			$credentials = array(
				'user_login'    => $user_registration_data['user_login'],
				'user_password' => $user_registration_data['user_password'],
				'remember'      => true,
			);
			wp_signon( $credentials );
		}

		// Check if user is already an affiliate.
		$existing_affiliate = $this->model->get_by_user_id( $user_id );
		if ( is_wp_error( $existing_affiliate ) && in_array( 'user_not_found', $existing_affiliate->get_error_codes(), true ) ) {
			return $this->response_error( $existing_affiliate->get_error_message(), 409 );
		}

		if ( $existing_affiliate instanceof AffiliateModel && $existing_affiliate->exists() ) {
			return $this->response_error( __( 'You are already an affiliate', 'wc-affiliate' ), 409 );
		}

		// Validate terms agreement.
		if ( 'on' !== $data['terms_agree'] && ! empty( wca_get_terms_conditions_page_url() ) ) {
			return $this->response_error( __( 'You must agree to the terms and conditions', 'wc-affiliate' ) );
		}

		// reCAPTCHA verification (for both new and existing users).
		if ( wca_is_recaptcha_enabled() ) {
			if ( empty( $data['recaptcha_response'] ) ) {
				return $this->response_error( __( 'Please complete the reCAPTCHA verification', 'wc-affiliate' ) );
			}

			$secret_key = wca_get_recaptcha_site_key();
			if ( empty( $secret_key ) ) {
				return $this->response_error( __( 'reCAPTCHA configuration error', 'wc-affiliate' ), 500 );
			}

			$verify_response = wp_remote_post(
				'https://www.google.com/recaptcha/api/siteverify',
				array(
					'body' => array(
						'secret'   => $secret_key,
						'response' => $data['recaptcha_response'],
					),
				)
			);

			if ( is_wp_error( $verify_response ) ) {
				return $this->response_error( __( 'reCAPTCHA verification failed', 'wc-affiliate' ) );
			}

			$verify_data = json_decode( wp_remote_retrieve_body( $verify_response ), true );
			if ( ! $verify_data['success'] ) {
				return $this->response_error( __( 'reCAPTCHA verification failed', 'wc-affiliate' ) );
			}
		}

		// Get auto-approval setting.
		$status = wca_is_auto_approve_enabled() ? 'active' : 'pending';

		$commission_type   = ! empty( $data['commission_type'] ) ? $data['commission_type'] : 'default';
		$commission_amount = ! empty( $data['commission_amount'] ) ? $data['commission_amount'] : '0.0';

		$affiliate_data = array(
			'user_id'           => $user_id,
			'status'            => $status,
			'commission_type'   => $commission_type,
			'commission_amount' => $commission_amount,
		);

		$affiliate = $this->model->create( $affiliate_data );

		if ( is_wp_error( $affiliate ) ) {
			return $this->response_error( $affiliate->get_error_message() );
		}

		// Update additional meta fields.
		if ( ! empty( $data['website_url'] ) ) {
			$affiliate->update_meta( '_wc_affiliate_website_url', sanitize_text_field( $data['website_url'] ) );
		}

		if ( ! empty( $data['promotion_method'] ) ) {
			$affiliate->update_meta( '_wc_affiliate_promotion_method', sanitize_text_field( $data['promotion_method'] ) );
		}

		// Handle referral tracking.
		if ( ! empty( $data['referrer_user_id'] ) ) {
			$affiliate->update_meta( '_wc_affiliate_referrer', sanitize_text_field( $data['referrer_user_id'] ) );
		}

		// Refresh affiliate user data.
		$affiliate->load();

		/**
		 * Filter affiliate application data before response.
		 *
		 * Allows modification of the application result data.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $affiliate_data Affiliate data from application.
		 * @param int             $user_id        The applying user ID.
		 * @param string          $status         The assigned status.
		 * @param WP_REST_Request $request        The original request object.
		 * @param array           $data           The collected and filtered request data.
		 */
		$affiliate_data = apply_filters( 'wc_affiliate_rest_api_affiliate_applied_data', $affiliate->to_array(), $user_id, $status, $request, $data );

		if ( $is_new_user ) {
			$message = ( 'active' === $status
				? __( 'Registration and affiliate application successful! Your account is now active.', 'wc-affiliate' )
				: __( 'Registration successful! Your affiliate application has been submitted for review.', 'wc-affiliate' )
			);
		} else {
			$message = ( 'active' === $status
				? __( 'Your affiliate application has been approved automatically', 'wc-affiliate' )
				: __( 'Your affiliate application has been submitted for review', 'wc-affiliate' )
			);
		}

		do_action( 'wc_affiliate_affiliate_applied', $user_id, $affiliate_data );

		return $this->response_success( $affiliate_data, $message, 201 );
	}

	/**
	 * Generate new affiliate link.
	 *
	 * Creates a tracking link for the specified affiliate and page/campaign.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing affiliate_id (or id), page_url (or url), and campaign (or description) parameters.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with link details or a WP_Error on failure.
	 */
	public function generate_link( WP_REST_Request $request ) {
		$affiliate_id = (int) $request->get_param( 'affiliate_id' );
		$page_url     = sanitize_text_field( ! empty( $request->get_param( 'page_url' ) ) ? $request->get_param( 'page_url' ) : $request->get_param( 'url' ) );
		$campaign     = sanitize_text_field( ! empty( $request->get_param( 'campaign' ) ) ? $request->get_param( 'campaign' ) : $request->get_param( 'description' ) );

		$affiliate = new AffiliateModel( $affiliate_id );
		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		$current_user_id = get_current_user_id();
		if ( ! current_user_can( 'manage_options' ) && $current_user_id !== $affiliate->user_id ) {
			return $this->response_error( __( 'Permission denied', 'wc-affiliate' ), 403 );
		}

		// Check if external URL generation is allowed.
		if ( ! wca_is_external_url_generation_allowed() ) {
			// Validate if the URL belongs to the current site.
			$site_url   = site_url();
			$url_parts  = wp_parse_url( $page_url );
			$site_parts = wp_parse_url( $site_url );

			if ( isset( $url_parts['host'] ) && isset( $site_parts['host'] ) && $url_parts['host'] !== $site_parts['host'] ) {
				return $this->response_error( __( 'Please enter a URL from your own site.', 'wc-affiliate' ) );
			}
		}

		$link = $affiliate->generate_link( $page_url, $campaign );

		$response_data = array(
			'link'         => $link,
			'affiliate_id' => $affiliate_id,
			'page_url'     => $page_url,
			'campaign'     => $campaign,
		);

		/**
		 * Filter generated affiliate link data before response.
		 *
		 * Allows modification of the link generation response.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $response_data Link response data.
		 * @param WP_REST_Request $request       The original request object.
		 */
		$response_data = apply_filters( 'wc_affiliate_rest_api_affiliate_link_data', $response_data, $request );

		return $this->response_success( $response_data );
	}

	/**
	 * Get affiliate leaderboard.
	 *
	 * Retrieves a ranked list of top affiliates based on the specified metric and period.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing period, limit (or top_n), and order_by parameters.
	 *
	 * @return WP_REST_Response The response with leaderboard data.
	 */
	public function get_leaderboard( WP_REST_Request $request ): WP_REST_Response {
		$period_param    = sanitize_text_field( $request->get_param( 'period' ) );
		$limit           = (int) ( $request->get_param( 'limit' ) ? $request->get_param( 'limit' ) : ( $request->get_param( 'top_n' ) ? $request->get_param( 'top_n' ) : 10 ) );
		$limit           = min( 50, max( 5, $limit ) );
		$period_days_map = array(
			'today'    => 1,
			'week'     => 7,
			'month'    => 30,
			'quarter'  => 90,
			'year'     => 365,
			'all_time' => 365 * 10, // Arbitrary large window.
		);
		$days            = $period_days_map[ $period_param ] ?? 30;
		$order_by        = sanitize_text_field( $request->get_param( 'order_by' ) );
		$order_by        = in_array(
			$order_by,
			array(
				'commissions',
				'referrals',
				'visits',
			),
			true
		) ? $order_by : 'commissions';
		$leaderboard     = $this->model->get_leaderboard( $limit, $days, $order_by );

		$response_data = array(
			'period'      => $period_param,
			'period_days' => $days,
			'limit'       => $limit,
			'order_by'    => $order_by,
			'items'       => $leaderboard,
		);

		/**
		 * Filter affiliate leaderboard data before response.
		 *
		 * Allows modification of the leaderboard response.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $response_data Leaderboard response data.
		 * @param string          $period_param  The period parameter.
		 * @param int             $limit         The limit.
		 * @param string          $order_by      The order by field.
		 * @param WP_REST_Request $request       The original request object.
		 */
		$response_data = apply_filters( 'wc_affiliate_rest_api_affiliate_leaderboard_data', $response_data, $period_param, $limit, $order_by, $request );

		return $this->response_success( $response_data );
	}

	/**
	 * Bulk update affiliate statuses.
	 *
	 * Updates the status for multiple affiliates in a single request.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing ids array and status parameter.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with update summary or a WP_Error on failure.
	 */
	public function bulk_update_status( WP_REST_Request $request ) {
		$affiliate_ids = array_map( 'intval', $request->get_param( 'ids' ) );
		$status        = sanitize_text_field( $request->get_param( 'status' ) );

		if ( empty( $affiliate_ids ) ) {
			return $this->response_error( __( 'No affiliate IDs provided', 'wc-affiliate' ) );
		}

		$valid_statuses = array_keys( wca_get_affiliate_statuses() );
		if ( ! in_array( $status, $valid_statuses, true ) ) {
			return $this->response_error( __( 'Invalid status', 'wc-affiliate' ) );
		}

		$updated_count = 0;
		$errors        = array();
		foreach ( $affiliate_ids as $affiliate_id ) {
			$affiliate = new AffiliateModel( $affiliate_id );
			if ( ! $affiliate->exists() ) {
				// translators: %d: Affiliate ID..
				$errors[] = sprintf( __( 'Affiliate ID %d not found', 'wc-affiliate' ), $affiliate_id );
				continue;
			}

			// Prepare update data for sending a notification email.
			$update_data = array(
				'old_status' => $affiliate->get_status(),
			);

			$result = $affiliate->update_status( $status );
			if ( is_wp_error( $result ) ) {
				// translators: 1: Affiliate ID, 2: Error message..
				$errors[] = sprintf( __( 'Failed to update affiliate ID %1$d: %2$s', 'wc-affiliate' ), $affiliate_id, $result->get_error_message() );
				continue;
			}

			++$updated_count;

			// Prepare update data for sending a notification email.
			$update_data['status'] = $status;

			// Fire action after affiliate update.
			do_action( 'wc_affiliate_affiliate_updated', $affiliate_id, $update_data, $request );
		}

		$response_data = array(
			'updated_count' => $updated_count,
			'total_count'   => count( $affiliate_ids ),
		);

		if ( $errors ) {
			$response_data['errors'] = $errors;
		}

		// translators: %d: Number of affiliates updated..
		$message = sprintf( _n( '%d affiliate status updated successfully', '%d affiliate statuses updated successfully', $updated_count, 'wc-affiliate' ), $updated_count );

		/**
		 * Filter bulk update status response data.
		 *
		 * Allows modification of the bulk update response.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $response_data Response data including counts and errors.
		 * @param array           $affiliate_ids Array of affiliate IDs.
		 * @param string          $status        The new status.
		 * @param WP_REST_Request $request       The original request object.
		 */
		$response_data = apply_filters( 'wc_affiliate_rest_api_affiliate_bulk_update_data', $response_data, $affiliate_ids, $status, $request );

		return $this->response_success( $response_data, $message );
	}

	/**
	 * Get affiliate status counts (admin only).
	 *
	 * Retrieves counts of affiliates by status, optionally filtered by date range.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing optional start_date and end_date parameters.
	 *
	 * @return WP_REST_Response A successful response with status counts or a WP_Error on permission denial.
	 */
	public function get_status_counts( WP_REST_Request $request ): WP_REST_Response {
		$start_date = sanitize_text_field( $request->get_param( 'start_date' ) );
		$end_date   = sanitize_text_field( $request->get_param( 'end_date' ) );

		$args = array();

		if ( $start_date ) {
			$args['start_date'] = $start_date;
		}

		if ( $end_date ) {
			$args['end_date'] = $end_date;
		}

		$affiliate_model = new AffiliateModel();
		$status_counts   = $affiliate_model->get_status_counts( $args );

		/**
		 * Filter affiliate status counts data before response.
		 *
		 * Allows modification of the status counts.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $status_counts Status counts data.
		 * @param array           $args          Filter arguments.
		 * @param WP_REST_Request $request       The original request object.
		 */
		$status_counts = apply_filters( 'wc_affiliate_rest_api_affiliate_status_counts_data', $status_counts, $args, $request );

		return $this->response_success( $status_counts );
	}

	/**
	 * Remove affiliate status from user.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing affiliate ID.
	 *
	 * @return WP_REST_Response|WP_Error The response with removal result.
	 */
	public function remove_affiliate( WP_REST_Request $request ) {
		$affiliate_id = $request->get_param( 'affiliate_id' );

		if ( empty( $affiliate_id ) ) {
			return $this->response_error( __( 'Affiliate ID is required.', 'wc-affiliate' ) );
		}

		// Check if affiliate exists..
		$affiliate = $this->model->get_by_user_id( $affiliate_id );
		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found.', 'wc-affiliate' ), 404 );
		}

		// Remove affiliate status..
		$affiliate->delete_meta( '_wc_affiliate_status' );

		// Remove affiliate role..
		$affiliate->remove_role();

		return $this->response_success( null, __( 'Affiliate status removed successfully.', 'wc-affiliate' ) );
	}

	/**
	 * Review affiliate application (approve/reject)
	 *
	 * @param WP_REST_Request $request The REST request object.
	 *
	 * @return WP_REST_Response|WP_Error Response with review status or error.
	 */
	public function review_application( WP_REST_Request $request ) {
		$affiliate_id = $request->get_param( 'affiliate_id' );
		$action       = $request->get_param( 'action' );

		if ( empty( $affiliate_id ) || empty( $action ) ) {
			return $this->response_error( __( 'Affiliate ID and action are required.', 'wc-affiliate' ) );
		}

		if ( ! in_array( $action, array( 'approve', 'reject' ), true ) ) {
			return $this->response_error( __( 'Invalid action. Must be "approve" or "reject".', 'wc-affiliate' ) );
		}

		// Check if affiliate exists..
		$affiliate = $this->model->get_by_user_id( $affiliate_id );
		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found.', 'wc-affiliate' ), 404 );
		}

		if ( 'approve' === $action ) {
			// Approve affiliate..
			$affiliate->update_meta( '_wc_affiliate_status', 'active' );

			// Add affiliate role..
			$affiliate->remove_role( 'subscriber' );
			$affiliate->add_role();

			$status_message = __( 'Affiliate application approved.', 'wc-affiliate' );
		} else {
			// Reject affiliate..
			$affiliate->update_meta( '_wc_affiliate_status', 'rejected' );

			$status_message = __( 'Affiliate application rejected.', 'wc-affiliate' );
		}

		// Trigger action for email notifications, etc..
		do_action( 'wc_affiliate_account_reviewed', $affiliate_id, $action );

		return $this->response_success(
			array(
				'affiliate_id' => $affiliate_id,
				'action'       => $action,
				'status'       => 'approve' === $action ? 'active' : 'rejected',
			),
			$status_message
		);
	}

	/**
	 * Get recent referrals for affiliate.
	 *
	 * Retrieves the most recent referrals for a specific affiliate, limited to the specified number.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing id and limit parameters.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with recent referrals or a WP_Error if not found.
	 */
	public function get_recent_referrals( WP_REST_Request $request ) {
		$id    = (int) $request->get_param( 'affiliate_id' );
		$limit = min( 20, max( 1, (int) $request->get_param( 'limit' ) ) );

		$affiliate = new AffiliateModel( $id );
		$user      = $affiliate->get_user();

		if ( ! $affiliate->exists() && ! user_can( $user, 'manage_options' ) ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		// Check if current user can view this affiliate's data..
		$current_user_id = get_current_user_id();
		if ( ! current_user_can( 'manage_options' ) && $current_user_id !== $affiliate->user_id ) {
			return $this->response_error( __( 'Permission denied', 'wc-affiliate' ), 403 );
		}

		$referral_model = new \WC_Affiliate\Models\Referral();
		$referrals      = $referral_model->get_referrals(
			array(
				'affiliate_id' => $id,
				'limit'        => $limit,
				'orderby'      => 'time',
				'order'        => 'DESC',
			)
		);

		$data = array();
		foreach ( $referrals as $referral ) {
			$referral_data = $referral->to_array();

			$referral_data['type']                 = 'referral';
			$referral_data['escaped_name']         = esc_html( $referral_data['affiliate']['display_name'] );
			$referral_data['escaped_email']        = esc_html( $referral_data['affiliate']['user_email'] );
			$referral_data['formatted_commission'] = esc_html( $referral_data['commission_formatted'] );

			if ( ! empty( $referral_data['visit'] ) ) {
				$visit_model = new \WC_Affiliate\Models\Visit( $referral_data['visit'] );
				$visit_data  = $visit_model->to_array();

				$visit_data['type'] = 'visit';
				$data[]             = $visit_data;

				$referral_data['visit'] = $visit_data;

				unset( $referral_data['visit']['affiliate'] );
			}

			$data[] = $referral_data;
		}

		/**
		 * Filter recent referrals data for affiliate API.
		 *
		 * Allows modification of the recent referrals list.
		 *
		 * @since 1.0.0
		 *
		 * @param array $data  Array of recent referral data.
		 * @param int   $id    The affiliate ID.
		 * @param int   $limit The limit applied.
		 */
		$data = apply_filters( 'wc_affiliate_rest_api_affiliate_recent_referrals', $data, $id, $limit );

		return $this->response_success( $data );
	}

	/**
	 * Get recent visits for affiliate.
	 *
	 * Retrieves the most recent visits for a specific affiliate, limited to the specified number.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing id and limit parameters.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with recent visits or a WP_Error if not found.
	 */
	public function get_recent_visits( WP_REST_Request $request ) {
		$id    = (int) $request->get_param( 'affiliate_id' );
		$limit = min( 50, max( 1, (int) $request->get_param( 'limit' ) ) );

		$affiliate = new AffiliateModel( $id );

		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		// Check if current user can view this affiliate's data..
		$current_user_id = get_current_user_id();
		if ( ! current_user_can( 'manage_options' ) && $current_user_id !== $affiliate->user_id ) {
			return $this->response_error( __( 'Permission denied', 'wc-affiliate' ), 403 );
		}

		$visit_model = new \WC_Affiliate\Models\Visit();
		$visits      = $visit_model->get_visits(
			array(
				'affiliate_id' => $id,
				'limit'        => $limit,
				'orderby'      => 'time',
				'order'        => 'DESC',
			)
		);

		$data = array();
		foreach ( $visits as $visit ) {
			$visit_data = $visit->to_array();

			// Use Cleaner trait to escape output for security..
			$visit_data['escaped_page_url']     = esc_html( $visit_data['page_url'] );
			$visit_data['escaped_referrer_url'] = esc_html( $visit_data['referrer_url'] );
			$visit_data['escaped_campaign']     = esc_html( $visit_data['campaign'] );
			$visit_data['escaped_ip']           = esc_html( $visit_data['ip'] );

			$data[] = $visit_data;
		}

		/**
		 * Filter recent visits data for affiliate API.
		 *
		 * Allows modification of the recent visits list.
		 *
		 * @since 1.0.0
		 *
		 * @param array $data  Array of recent visit data.
		 * @param int   $id    The affiliate ID.
		 * @param int   $limit The limit applied.
		 */
		$data = apply_filters( 'wc_affiliate_rest_api_affiliate_recent_visits', $data, $id, $limit );

		return $this->response_success( $data );
	}

	/**
	 * Get recent activities for affiliate.
	 *
	 * Retrieves a combined list of recent referrals and visits for a specific affiliate, limited to the specified number.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing id and limit parameters.
	 *
	 * @return WP_REST_Response|WP_Error|null A successful response with recent activities or a WP_Error if not found.
	 */
	public function get_recent_activities( WP_REST_Request $request ) {
		$id    = (int) $request->get_param( 'affiliate_id' );
		$limit = min( 50, max( 1, (int) $request->get_param( 'limit' ) ) );

		$affiliate = new AffiliateModel( $id );

		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Affiliate not found', 'wc-affiliate' ), 404 );
		}

		// Check if current user can view this affiliate's data..
		$current_user_id = get_current_user_id();
		if ( ! current_user_can( 'manage_options' ) && $current_user_id !== $affiliate->user_id ) {
			return $this->response_error( __( 'Permission denied', 'wc-affiliate' ), 403 );
		}

		// Get affiliate activity using Affiliate model.
		$raw_activities = $affiliate->get_affiliate_activity( $id, 10 );

		$activities = array();
		foreach ( $raw_activities as $activity ) {
			$formatted_activity = array(
				'type'           => $activity['type'],
				'id'             => (int) $activity['id'],
				'affiliate_id'   => $id,
				'affiliate_name' => $affiliate->get_display_name(),
				'amount'         => (float) $activity['commission'],
				'timestamp'      => (int) $activity['time'],
				'time_formatted' => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $activity['time'] ),
			);

			if ( 'referral' === $activity['type'] ) {
				$formatted_activity['order_total']     = (float) $activity['order_total'];
				$formatted_activity['referral_status'] = $activity['referral_status'];
			} elseif ( 'visit' === $activity['type'] ) {
				$formatted_activity['visited_url'] = $activity['page_url'];
				$formatted_activity['campaign']    = $activity['campaign'];
			}

			$activities[] = $formatted_activity;
		}

		/**
		 * Filter recent activities data for affiliate API.
		 *
		 * Allows modification of the recent activities list.
		 *
		 * @since 1.0.0
		 *
		 * @param array $activities Array of recent activity data.
		 * @param int   $id         The affiliate ID.
		 * @param int   $limit      The limit applied.
		 */
		$activities = apply_filters( 'wc_affiliate_rest_api_affiliate_recent_activities', $activities, $id, $limit );

		return $this->response_success( $activities );
	}

	/**
	 * Get referred affiliates for current user.
	 *
	 * Retrieves a paginated list of affiliates who were referred by the current authenticated affiliate.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing optional page and per_page parameters.
	 *
	 * @return WP_REST_Response|WP_Error The response with referred affiliates data and pagination headers.
	 */
	public function get_referred_affiliates( WP_REST_Request $request ) {
		$current_user_id = get_current_user_id();

		if ( ! $current_user_id ) {
			return $this->response_error( __( 'User not authenticated', 'wc-affiliate' ), 401 );
		}

		// Check if current user is an affiliate.
		$current_affiliate = $this->model->get_by_user_id( $current_user_id );
		if ( ! $current_affiliate || ! $current_affiliate->exists() ) {
			return $this->response_error( __( 'Current user is not an affiliate', 'wc-affiliate' ), 403 );
		}

		$pagination = $this->get_pagination_params( $request );

		// Get users who have this user as referrer with pagination.
		$args              = array(
			'limit'  => $pagination['limit'],
			'offset' => $pagination['offset'],
		);
		$referred_user_ids = AffiliateModel::get_referred_affiliates( $current_user_id, $args );
		$total_referred    = AffiliateModel::get_referred_affiliates_count( $current_user_id );

		if ( empty( $referred_user_ids ) ) {
			$response = $this->response_success( array( 'referred_affiliates' => array() ) );

			return $this->add_pagination_headers( $response, $total_referred, $pagination['page'], $pagination['per_page'] );
		}

		// Get affiliate data for referred users.
		$referred_affiliates = array();
		foreach ( $referred_user_ids as $user_id ) {
			$affiliate = $this->model->get_by_user_id( $user_id );
			if ( $affiliate && $affiliate->exists() ) {
				$referred_affiliates[] = $affiliate->to_array();
			}
		}

		/**
		 * Filter referred affiliates data before response.
		 *
		 * Allows modification of the referred affiliates list.
		 *
		 * @since 1.0.0
		 *
		 * @param array           $referred_affiliates Array of referred affiliate data.
		 * @param int             $current_user_id     The current user ID.
		 * @param WP_REST_Request $request             The original request object.
		 */
		$referred_affiliates = apply_filters( 'wc_affiliate_rest_api_affiliate_referred_data', $referred_affiliates, $current_user_id, $request );

		$response = $this->response_success( array( 'referred_affiliates' => $referred_affiliates ) );

		return $this->add_pagination_headers( $response, $total_referred, $pagination['page'], $pagination['per_page'] );
	}

	/**
	 * Handle affiliation assignment.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing assignment data.
	 *
	 * @return WP_REST_Response|WP_Error The response with assignment result.
	 */
	public function handle_affiliation_assignment( WP_REST_Request $request ) {
		$status            = sanitize_text_field( $request->get_param( 'status' ) );
		$commission_type   = sanitize_text_field( $request->get_param( 'commission_type' ) );
		$commission_amount = (float) $request->get_param( 'commission_amount' );
		$affiliate_id      = (int) $request->get_param( 'affiliate' );

		// Verify request.
		if ( ! wp_verify_nonce( $request->get_param( 'nonce' ), 'wc-affiliate' ) ) {
			return $this->response_error( __( 'Unauthorized!', 'wc-affiliate' ), 403 );
		}

		if ( $affiliate_id > 0 ) {
			// Existing user logic.
			$user_id = $affiliate_id;

			// Save commission fields.
			$this->model->save_commission_fields( $user_id, $status, $commission_type, $commission_amount );

			return $this->response_success(
				array(
					'message'                    => __( 'Affiliate Data Saved Successfully!', 'wc-affiliate' ),
					'status'                     => 1,
					'user_id'                    => $user_id,
					'affiliate_status'           => $status,
					'commission_type'            => $commission_type,
					'commission_amount'          => $commission_amount,
					'_wc_affiliate_time_applied' => time(),
				)
			);
		} else {
			// New user logic.
			$first_name = sanitize_text_field( $request->get_param( 'first_name' ) );
			$last_name  = sanitize_text_field( $request->get_param( 'last_name' ) );
			$email      = sanitize_text_field( $request->get_param( 'email' ) );
			$password   = $request->get_param( 'password' );

			if ( empty( $email ) || empty( $password ) ) {
				return $this->response_error( __( 'Email and password are required.', 'wc-affiliate' ) );
			}

			if ( email_exists( $email ) ) {
				return $this->response_error( __( 'Email already registered.', 'wc-affiliate' ) );
			}

			// Prepare user data.
			$user_data = array(
				'user_login'    => $email,
				'user_pass'     => $password,
				'user_email'    => $email,
				'first_name'    => $first_name,
				'last_name'     => $last_name,
				'user_nicename' => $first_name,
				'display_name'  => trim( $first_name . ' ' . $last_name ),
				'role'          => 'customer',
			);

			$user_id = User::create_user( $user_data );

			if ( is_wp_error( $user_id ) ) {
				return $this->response_error( $user_id->get_error_message(), 500 );
			}

			// Save commission fields.
			$this->model->save_commission_fields( $user_id, $status, $commission_type, $commission_amount );

			return $this->response_success(
				array(
					'message'                    => __( 'New Affiliate Registered!', 'wc-affiliate' ),
					'status'                     => 1,
					'user_id'                    => $user_id,
					'affiliate_status'           => $status,
					'commission_type'            => $commission_type,
					'commission_amount'          => $commission_amount,
					'_wc_affiliate_time_applied' => time(),
				)
			);
		}
	}

	/**
	 * Request payout (for affiliates)
	 *
	 * @param WP_REST_Request $request The REST request object.
	 *
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function request_payout( WP_REST_Request $request ) {
		$affiliate_id = (int) $request->get_param( 'affiliate_id' );
		$affiliate    = new AffiliateModel( $affiliate_id );
		if ( ! $affiliate->exists() ) {
			return $this->response_error( __( 'Invalid affiliate ID', 'wc-affiliate' ), 400 );
		}

		// Check the minimum payout amount.
		$minimum_payout = wca_get_minimum_payout();
		$minimum_payout = apply_filters( 'wc_affiliate_minimum_payout_amount', $minimum_payout, $affiliate_id );

		// Check available balance.
		$available_balance = $affiliate->get_balance();
		$available_balance = apply_filters( 'wc_affiliate_available_balance', $available_balance, $affiliate_id );
		$payment_method    = $affiliate->get( 'payment_method' );

		if ( $minimum_payout > $available_balance ) {
			return $this->response_error(
				sprintf(
				// translators: %s: Available balance.
					__( 'Insufficient balance to process payout. Available balance: %s', 'wc-affiliate' ),
					wca_format_price( $available_balance )
				)
			);
		}

		update_user_meta( $affiliate_id, '_wc-affiliate-applied_payout', time() );

		$payout_result = apply_filters( 'wc_affiliate_handle_payout_request', null, $affiliate_id, $available_balance, $payment_method, $affiliate, $request );

		if ( null !== $payout_result ) {
			return $payout_result;
		}

		do_action( 'wc_affiliate_payout_request_created', $available_balance, $affiliate, $request );

		return $this->response_success(
			array(),
			__( 'Payout request submitted successfully', 'wc-affiliate' ),
			201
		);
	}

	/**
	 * Register new WordPress user.
	 *
	 * Helper method to handle WordPress user registration with validation.
	 *
	 * @since 1.0.0
	 *
	 * @param array $user_data User registration data.
	 *
	 * @return int|WP_Error User ID on success, WP_Error on failure.
	 */
	protected function register_user( array $user_data ) {
		// Extract and sanitize input data.
		$first_name       = sanitize_text_field( $user_data['first_name'] ?? '' );
		$last_name        = sanitize_text_field( $user_data['last_name'] ?? '' );
		$user_name        = sanitize_text_field( $user_data['user_login'] ?? '' );
		$email            = sanitize_email( $user_data['user_email'] ?? '' );
		$password         = $user_data['user_password'] ?? '';
		$password_confirm = $user_data['user_password2'] ?? '';

		// Validate required fields.
		if ( empty( $first_name ) || empty( $last_name ) || empty( $user_name ) || empty( $email ) || empty( $password ) || empty( $password_confirm ) ) {
			$missing_fields = array();

			if ( empty( $first_name ) ) {
				$missing_fields[] = __( 'First Name', 'wc-affiliate' );
			}
			if ( empty( $last_name ) ) {
				$missing_fields[] = __( 'Last Name', 'wc-affiliate' );
			}
			if ( empty( $user_name ) ) {
				$missing_fields[] = __( 'Username', 'wc-affiliate' );
			}
			if ( empty( $email ) ) {
				$missing_fields[] = __( 'Email', 'wc-affiliate' );
			}
			if ( empty( $password ) ) {
				$missing_fields[] = __( 'Password', 'wc-affiliate' );
			}
			if ( empty( $password_confirm ) ) {
				$missing_fields[] = __( 'Confirmation Password', 'wc-affiliate' );
			}

			return new WP_Error(
				'missing_fields',
				sprintf(
				// translators: %s: Comma-separated list of missing field names.
					__( 'The following required fields are missing: %s. Please fill in all required fields to continue.', 'wc-affiliate' ),
					implode( ', ', $missing_fields )
				)
			);
		}

		// Validate password match.
		if ( $password !== $password_confirm ) {
			return new WP_Error( 'password_mismatch', __( 'Passwords do not match', 'wc-affiliate' ) );
		}

		// Check email uniqueness.
		if ( email_exists( $email ) ) {
			return new WP_Error( 'email_exists', __( 'Email already exists. Please log in instead.', 'wc-affiliate' ) );
		}

		// Check username uniqueness.
		if ( username_exists( $user_name ) ) {
			return new WP_Error( 'username_exists', __( 'Username already exists. Please choose a different username.', 'wc-affiliate' ) );
		}

		// Prepare user data.
		$user_data = array(
			'user_login'   => $user_name,
			'user_pass'    => $password,
			'user_email'   => $email,
			'first_name'   => $first_name,
			'last_name'    => $last_name,
			'display_name' => $first_name . ' ' . $last_name,
		);

		return User::create_user( $user_data );
	}
}
