<?php

namespace WC_Affiliate\Models;

defined( 'ABSPATH' ) || exit;

use WC_Affiliate\Abstracts\Model;
use WC_Affiliate\Models\Transaction as TransactionModel;
use WC_Affiliate\Traits\Validation;
use WP_Error;
use WP_User;

/**
 * Class Affiliate
 *
 * Represents an affiliate entity in the system. This model handles affiliate-related
 * data, metadata, and actions such as creation, updating, deletion, and statistics gathering.
 */
class Affiliate extends Model {

	use Validation;

	const STATUS_PENDING   = 'pending';
	const STATUS_ACTIVE    = 'active';
	const STATUS_REJECTED  = 'rejected';
	const STATUS_SUSPENDED = 'suspended';

	/**
	 * User ID
	 *
	 * @var int|null
	 */
	public ?int $user_id;

	/**
	 * User object
	 *
	 * @var WP_User|false|null
	 */
	protected $user;

	/**
	 * User model object
	 *
	 * @var User|null
	 */
	protected ?User $user_model;

	/**
	 * Affiliate metadata
	 *
	 * @var array
	 */
	protected array $meta;

	/**
	 * Table name for affiliates (user meta based)
	 *
	 * @var string|null
	 */
	protected static ?string $table_name = null;

	/**
	 * Initialize affiliate model
	 *
	 * Loads affiliate data from database and populates model properties
	 *
	 * @return void
	 */
	public function load(): void {
		if ( ! $this->id ) {
			return;
		}

		$this->user_id    = (int) $this->id;
		$this->user       = get_userdata( $this->user_id );
		$this->user_model = new User( $this->user_id );

		// Check if user has affiliate role..
		if ( ! $this->user instanceof \WP_User || 'affiliate' !== $this->user_model->get_role() ) {
			$this->exists = false;

			return;
		}

		if ( empty( $this->user_model->get_meta( '_wc_affiliate_status' ) ) ) {
			$this->exists = false;

			return;
		}

		// Load affiliate metadata..
		$this->load_meta();
		$this->load_user_data();

		// Set exists flag..
		$this->exists = true;

		wp_cache_flush();
	}

	/**
	 * Get affiliate user data
	 *
	 * @return void
	 */
	protected function load_user_data() {
		if ( ! $this->user instanceof \WP_User ) {
			return;
		}

		if ( empty( $this->meta ) ) {
			$this->load_meta();
		}

		$commission_settings = $this->get_commission_settings();

		$data = array(
			'id'                  => $this->user_id,
			'user_id'             => $this->user_id,
			'user_email'          => $this->user->user_email,
			'user_login'          => $this->user->user_login,
			'first_name'          => $this->user->first_name,
			'last_name'           => $this->user->last_name,
			'display_name'        => $this->user->display_name,
			'user_registered'     => get_date_from_gmt( $this->user->user_registered ),
			'time_applied'        => wp_date( 'Y-m-d H:i:s', $this->meta['time_applied'] ),
			'commission_type'     => $commission_settings['type'],
			'commission_label'    => $commission_settings['label'],
			'commission_amount'   => 'default' === $commission_settings['type'] ? '-' : $commission_settings['amount'],
			'payment_method'      => $this->meta['payout_method'],
			'payment_details'     => $this->meta['payment_details'],
			'auto_payout'         => $this->meta['auto_payout'],
			'website_url'         => $this->meta['website_url'],
			'promotion_method'    => $this->meta['promotion_method'],
			'payout_requested_at' => $this->meta['payout_requested_at'],
			'avatar_url'          => $this->meta['avatar_url'],
			'wp_avatar_url'       => get_avatar_url( $this->user_id, array( 'size' => 128 ) ),
			'status'              => $this->get_status(),
			'token'               => $this->get_token(),
			'balance'             => $this->get_balance(),
			'total_earnings'      => $this->get_total_earnings(),
			'total_referrals'     => $this->get_referral_count(),
			'total_visits'        => $this->get_visit_count(),
			'conversion_rate'     => $this->get_conversion_rate(),
			'referred_by'         => $this->get_referred_by(),
			'level'               => $this->get_level(),
			'level_label'         => $this->get_level_label(),
		);

		$this->data = apply_filters( 'wc_affiliate_current_affiliate_data', $data, $this->user_id );
	}

	/**
	 * Load affiliate metadata
	 *
	 * @return void
	 */
	protected function load_meta(): void {
		if ( ! $this->user || ! $this->user_model ) {
			return;
		}

		$this->meta = array(
			'referrer_user_id'    => $this->user_model->get_meta( '_wc_affiliate_referrer' ),
			'commission_type'     => $this->user_model->get_meta( 'commission_type' ),
			'commission_amount'   => $this->user_model->get_meta( 'commission_amount' ),
			'status'              => $this->user_model->get_meta( '_wc_affiliate_status' ),
			'time_applied'        => $this->user_model->get_meta( '_wc_affiliate_time_applied' ),
			'payout_method'       => $this->user_model->get_meta( '_wc_affiliate_payout_method' ),
			'payment_details'     => $this->user_model->get_meta( '_wc_affiliate_payment_details' ),
			'account_info'        => $this->user_model->get_meta( '_wc_affiliate_mannual_payment' ),
			'auto_payout'         => $this->user_model->get_meta( '_wc_affiliate_auto_payout' ),
			'token'               => $this->user_model->get_meta( '_wc_affiliate_token' ),
			'website_url'         => $this->user_model->get_meta( '_wc_affiliate_website_url' ),
			'promotion_method'    => $this->user_model->get_meta( '_wc_affiliate_promotion_method' ),
			'payout_requested_at' => $this->user_model->get_meta( '_wc-affiliate-applied_payout' ),
			'avatar_url'          => $this->user_model->get_meta( '_wc_affiliate_avatar' ),
		);
	}

	/**
	 * Check if affiliate exists
	 *
	 * @return bool True if affiliate exists, false otherwise.
	 */
	public function exists(): bool {
		return $this->user instanceof WP_User && ! empty( $this->meta['status'] );
	}

	/**
	 * Get affiliate status
	 *
	 * @return string Affiliate status (pending, active, rejected, suspended).
	 */
	public function get_status(): string {
		return $this->meta['status'] ?? '';
	}

	/**
	 * Check if affiliate is active
	 *
	 * @return bool True if affiliate status is active, false otherwise.
	 */
	public function is_active(): bool {
		return $this->get_status() === self::STATUS_ACTIVE;
	}

	/**
	 * Check if affiliate is pending
	 *
	 * @return bool True if affiliate status is pending, false otherwise.
	 */
	public function is_pending(): bool {
		return $this->get_status() === self::STATUS_PENDING;
	}

	/**
	 * Get affiliate display name
	 *
	 * @return string Display name of the affiliate user.
	 */
	public function get_display_name(): string {
		return $this->user ? $this->user->display_name : '';
	}

	/**
	 * Get affiliate user object
	 *
	 * @return WP_User The WordPress user object associated with this affiliate.
	 */
	public function get_user(): WP_User {
		return $this->user;
	}

	/**
	 * Get all affiliates with filters
	 *
	 * @param array $args    {
	 *                       Optional. Arguments to filter affiliates.
	 *
	 * @type string $status  Filter by status (pending, active, rejected, suspended).
	 * @type int    $limit   Number of affiliates to return. Default 50.
	 * @type int    $offset  Offset for pagination. Default 0.
	 * @type string $orderby Column to order by. Default 'ID'.
	 * @type string $order   Sort order (ASC or DESC). Default 'DESC'.
	 * @type string $search  Search term for user fields.
	 *                       }
	 *
	 * @return Affiliate[] List of affiliate objects.
	 */
	public function get_affiliates( array $args = array() ): array {
		$defaults = array(
			'status'  => '',
			'limit'   => 50,
			'offset'  => 0,
			'orderby' => 'ID',
			'order'   => 'DESC',
			'search'  => '',
		);

		$args = wp_parse_args( $args, $defaults );

		// Allow filtering query arguments.
		$args = apply_filters( 'wc_affiliate_get_affiliates_args', $args );

		// Use get_users with affiliate role..
		$user_args = array(
			'role'    => 'affiliate',
			'number'  => $args['limit'],
			'offset'  => $args['offset'],
			'orderby' => $args['orderby'],
			'order'   => $args['order'],
		);

		// Add search filter.
		$users = $this->get_users( $args, $user_args );

		// Convert to affiliate objects.
		$affiliates = array();
		foreach ( $users as $user ) {
			$affiliates[] = new self( $user->ID );
		}

		return apply_filters( 'wc_affiliate_get_affiliates', $affiliates, $args );
	}

	/**
	 * Get affiliate by user ID
	 *
	 * @param int $user_id User ID to fetch affiliate for.
	 *
	 * @return Affiliate|\WP_Error Affiliate object on success, WP_Error on failure.
	 */
	public function get_by_user_id( int $user_id ) {
		$user = get_userdata( $user_id );
		if ( ! $user instanceof WP_User ) {
			return new WP_Error( 'user_not_found', __( 'User not found.', 'wc-affiliate' ) );
		}

		$user_model = new User( $user_id );

		$affiliate             = new self( $user_id );
		$affiliate->id         = $user_id;
		$affiliate->user_id    = $user_id;
		$affiliate->user       = $user;
		$affiliate->user_model = $user_model;
		$affiliate->exists     = true;

		$affiliate->load_meta();
		$affiliate->load_user_data();

		return $affiliate;
	}

	/**
	 * Create a new affiliate
	 *
	 * @param array $data              {
	 *                                 Data for the new affiliate.
	 *
	 * @type int    $user_id           Required. User ID to create affiliate for.
	 * @type string $status            Optional. Affiliate status. Default 'pending'.
	 * @type string $commission_type   Optional. Commission type ('fixed' or 'percent'). Default 'percent'.
	 * @type float  $commission_amount Optional. Commission amount. Default 0.
	 *                                 }
	 *
	 * @return Affiliate|\WP_Error Affiliate object on success, WP_Error on failure.
	 */
	public function create( array $data ) {
		$user_id    = $data['user_id'] ?? 0;
		$user_model = new User( $user_id );

		// Allow filtering data before creation.
		$data = apply_filters( 'wc_affiliate_affiliate_create_data', $data, $user_id );

		// Allow plugins to prevent creation.
		$can_create = apply_filters( 'wc_affiliate_can_create_affiliate', true, $user_id, $data );
		if ( ! $can_create ) {
			return new WP_Error( 'creation_prevented', __( 'Affiliate creation prevented by filter', 'wc-affiliate' ) );
		}

		if ( ! empty( $user_model->get_meta( '_wc_affiliate_status' ) ) ) {
			return new \WP_Error( 'already_affiliate', \__( 'User is already an affiliate', 'wc-affiliate' ) );
		}

		// Fire action before affiliate creation.
		do_action( 'wc_affiliate_affiliate_before_create', $user_id, $data );

		// Assign affiliate role if active.
		$user = \get_userdata( $user_id );
		if ( $user instanceof WP_User && ! in_array( 'affiliate', $user->roles, true ) ) {
			$user->add_role( 'affiliate' );
		}

		$status            = $data['status'] ?? self::STATUS_PENDING;
		$commission_type   = $data['commission_type'] ?? 'default';
		$commission_amount = $data['commission_amount'] ?? '0.0';

		// Set affiliate meta using User model.
		$user_model->update_meta( 'commission_type', $commission_type );
		$user_model->update_meta( 'commission_amount', $commission_amount );
		$user_model->update_meta( '_wc_affiliate_status', $status );
		$user_model->update_meta( '_wc_affiliate_time_applied', time() );

		// Generate affiliate token if not exists.
		if ( ! $user_model->get_meta( '_wc_affiliate_token' ) ) {
			$user_model->update_meta( '_wc_affiliate_token', wca_generate_token( wca_get_token_type() ) );
		}

		$affiliate = $this->get_by_user_id( $user_id );

		// Fire action after affiliate creation.
		do_action( 'wc_affiliate_affiliate_after_create', $affiliate, $user_id, $data );

		return $affiliate;
	}

	/**
	 * Update affiliate data
	 *
	 * @param array $data              {
	 *                                 Associative array of data to update.
	 *
	 * @type string $status            Affiliate status.
	 * @type string $commission_type   Commission type ('fixed' or 'percent').
	 * @type float  $commission_amount Commission amount.
	 * @type string $payment_method    Payment method.
	 * @type string $payment_details   Payment details.
	 * @type bool   $auto_payout       Auto payout setting.
	 *                                 }
	 *
	 * @return bool|\WP_Error True on success, WP_Error on failure.
	 */
	public function update( array $data ) {
		if ( ! $this->exists() ) {
			return new WP_Error( 'affiliate_not_found', __( 'Affiliate not found', 'wc-affiliate' ) );
		}

		// Allow filtering data before update.
		$data = apply_filters( 'wc_affiliate_affiliate_update_data', $data, $this->user_id );

		// Allow plugins to prevent update.
		$can_update = apply_filters( 'wc_affiliate_can_update_affiliate', true, $this->user_id, $data );
		if ( ! $can_update ) {
			return new WP_Error( 'update_prevented', __( 'Affiliate update prevented by filter', 'wc-affiliate' ) );
		}

		// Fire action before affiliate update.
		do_action( 'wc_affiliate_affiliate_before_update', $this->user_id, $data );

		foreach ( $data as $key => $value ) {
			switch ( $key ) {
				case 'status':
					$this->user_model->update_meta( '_wc_affiliate_status', sanitize_text_field( $value ) );
					break;
				case 'commission_type':
					$this->user_model->update_meta( 'commission_type', sanitize_text_field( $value ) );
					break;
				case 'commission_amount':
					$this->user_model->update_meta( 'commission_amount', floatval( $value ) );
					break;
				case 'payment_method':
					$this->user_model->update_meta( '_wc_affiliate_payout_method', sanitize_text_field( $value ) );
					break;
				case 'payment_details':
					$this->user_model->update_meta( '_wc_affiliate_payment_details', wp_kses_post( $value ) );
					break;
				case 'auto_payout':
					$this->user_model->update_meta( '_wc_affiliate_auto_payout', (bool) $value );
					break;
			}
		}

		// Reload meta..
		$this->load_meta();

		// Fire action after affiliate update.
		do_action( 'wc_affiliate_affiliate_after_update', $this->user_id, $data );

		return true;
	}

	/**
	 * Delete affiliate
	 *
	 * @return bool|\WP_Error True on success, WP_Error on failure.
	 */
	public function delete() {
		if ( ! $this->exists() || ! $this->id ) {
			return new WP_Error( 'model_not_found', __( 'Model not found or does not exist.', 'wc-affiliate' ) );
		}

		// Allow plugins to prevent deletion.
		$can_delete = apply_filters( 'wc_affiliate_can_delete_affiliate', true, $this->user_id );
		if ( ! $can_delete ) {
			return new WP_Error( 'deletion_prevented', __( 'Affiliate deletion prevented by filter', 'wc-affiliate' ) );
		}

		// Fire action before affiliate deletion.
		do_action( 'wc_affiliate_affiliate_before_delete', $this->user_id );

		$result = $this->user_model->delete_meta( '_wc_affiliate_status' );

		if ( $result ) {
			$this->exists = false;
			$this->id     = null;
			$this->data   = array();

			// Fire action after affiliate deletion.
			do_action( 'wc_affiliate_affiliate_after_delete', $this->user_id );

			return true;
		}

		return new WP_Error( 'delete_failed', __( 'Failed to delete model.', 'wc-affiliate' ) );
	}

	/**
	 * Get affiliate metadata value
	 *
	 * Retrieves a specific metadata value for the affiliate from the user model
	 * and updates the local metadata cache.
	 *
	 * @param string $key     The metadata key to retrieve.
	 * @param mixed  $default Optional. Default value to return if metadata doesn't exist. Default empty string.
	 *
	 * @return mixed The metadata value if found, otherwise the default value.
	 */
	public function get_meta( string $key, $default = '' ) {
		// Get meta from user model.
		$value = $this->user_model->get_meta( $key );

		return $value ?? $default;
	}

	/**
	 * Update affiliate metadata
	 *
	 * Updates a specific metadata value for the affiliate in the database
	 * and refreshes the local metadata cache.
	 *
	 * @param string $key   The metadata key to update.
	 * @param mixed  $value The metadata value to set.
	 *
	 * @return void
	 */
	public function update_meta( string $key, $value ) {
		// Update affiliate meta in database.
		$this->user_model->update_meta( $key, $value );
	}

	/**
	 * Delete affiliate metadata
	 *
	 * Removes a specific metadata entry for the affiliate from the database
	 * by deleting it through the user model.
	 *
	 * @param string $key The metadata key to delete.
	 *
	 * @return void
	 */
	public function delete_meta( string $key ) {
		$this->user_model->delete_meta( $key );
	}

	/**
	 * Get referred affiliates.
	 *
	 * Retrieves an array of user IDs who were referred by the specified user.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $referrer_user_id The user ID of the referrer.
	 * @param array $args             Optional arguments for pagination (limit, offset).
	 *
	 * @return int[] Array of user IDs who were referred by the specified user.
	 */
	public static function get_referred_affiliates( int $referrer_user_id, array $args = array() ): array {
		global $wpdb;

		$limit  = isset( $args['limit'] ) ? intval( $args['limit'] ) : null;
		$offset = isset( $args['offset'] ) ? intval( $args['offset'] ) : 0;

		$query = $wpdb->prepare(
			"SELECT user_id FROM {$wpdb->usermeta}
  			 WHERE meta_key = '_wc_affiliate_referrer' AND meta_value = %d
  			 ORDER BY user_id DESC",
			$referrer_user_id
		);

		if ( $limit ) {
			$query .= $wpdb->prepare( ' LIMIT %d OFFSET %d', $limit, $offset );
		}

		return $wpdb->get_col( $query );
	}

	/**
	 * Get referred affiliates count.
	 *
	 * Retrieves the total count of users who were referred by the specified user.
	 *
	 * @since 1.0.0
	 *
	 * @param int $referrer_user_id The user ID of the referrer.
	 *
	 * @return int Total count of referred affiliates.
	 */
	public static function get_referred_affiliates_count( int $referrer_user_id ): int {
		global $wpdb;

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->usermeta}
  				 WHERE meta_key = '_wc_affiliate_referrer' AND meta_value = %d",
				$referrer_user_id
			)
		);
	}

	/**
	 * Add affiliate role to user
	 *
	 * Assigns the 'affiliate' role to the user associated with this affiliate model.
	 *
	 * @param string $role The role to assign. Defaults to 'affiliate'.
	 *
	 * @return void
	 */
	public function add_role( string $role = 'affiliate' ) {
		$this->user->add_role( $role );
	}

	/**
	 * Remove affiliate role from user
	 *
	 * Removes the 'affiliate' role from the user associated with this affiliate model.
	 *
	 * @param string $role The role to remove. Defaults to 'affiliate'.
	 *
	 * @return void
	 */
	public function remove_role( string $role = 'affiliate' ) {
		$this->user->remove_role( $role );
	}

	/**
	 * Get affiliate status counts
	 *
	 * @param array $args Optional filter arguments.
	 *
	 * @return array Array of statuses with counts.
	 */
	public function get_status_counts( array $args = array() ): array {
		$defaults = array(
			'start_date' => '',
			'end_date'   => '',
		);

		$args = wp_parse_args( $args, $defaults );

		// Get all available affiliate statuses via filter..
		$available_statuses = $this->get_available_statuses();

		// Get users with affiliate role.
		$user_args = array(
			'role'   => 'affiliate',
			'number' => -1, // Get all users.
		);

		// Date range filters (based on user registration date)..
		if ( $args['start_date'] ) {
			$user_args['date_query'] = array(
				array(
					'after'     => $args['start_date'] . ' 00:00:00',
					'inclusive' => true,
				),
			);
		}

		if ( $args['end_date'] ) {
			if ( isset( $user_args['date_query'] ) ) {
				$user_args['date_query'][0]['before']    = $args['end_date'] . ' 23:59:59';
				$user_args['date_query'][0]['inclusive'] = true;
			} else {
				$user_args['date_query'] = array(
					array(
						'before'    => $args['end_date'] . ' 23:59:59',
						'inclusive' => true,
					),
				);
			}
		}

		$users = get_users( $user_args );

		// Count statuses.
		$status_counts = array();
		foreach ( $users as $user ) {
			$user_model = new User( $user->ID );
			$status     = $user_model->get_meta( '_wc_affiliate_status' );

			if ( isset( $available_statuses[ $status ] ) ) {
				if ( ! isset( $status_counts[ $status ] ) ) {
					$status_counts[ $status ] = 0;
				}
				++$status_counts[ $status ];
			}
		}

		// Build status array with all available statuses, even if count is 0..
		$statuses = array();
		foreach ( $available_statuses as $status_key => $status_label ) {
			$statuses[ $status_key ] = array(
				'status' => $status_key,
				'label'  => $status_label,
				'count'  => $status_counts[ $status_key ] ?? 0,
			);
		}

		// Allow filtering of the final status counts..
		$statuses = apply_filters( 'wc_affiliate_all_affiliate_status_counts', $statuses, $args );

		// Calculate total..
		$total_count = 0;
		foreach ( $statuses as $status_data ) {
			$total_count += $status_data['count'];
		}

		return array(
			'statuses' => array_values( $statuses ),
			'totals'   => array(
				'count' => $total_count,
			),
		);
	}

	/**
	 * Generate affiliate link
	 *
	 * @param string $page_url The base URL to which the affiliate token will be appended.
	 * @param string $campaign Optional campaign name to track the source of the referral.
	 *
	 * @return string The generated affiliate link with the token and optional campaign parameters.
	 */
	public function generate_link( string $page_url, string $campaign = '' ): string {
		if ( ! $this->is_active() ) {
			return $page_url;
		}

		$ref_key    = wca_get_ref_key();
		$token_type = wca_get_token_type();
		$args       = array( $ref_key => wca_generate_token( $token_type ) );

		if ( ! empty( $campaign ) ) {
			$args['campaign'] = sanitize_text_field( $campaign );
		}

		// Allow filtering link arguments before generation.
		$args = apply_filters( 'wc_affiliate_link_args', $args, $this->user_id, $page_url, $campaign );

		$link = add_query_arg( $args, $page_url );

		// Allow filtering final generated link.
		return apply_filters( 'wc_affiliate_generated_link', $link, $this->user_id, $page_url, $campaign, $args );
	}

	/**
	 * Get affiliate statistics
	 *
	 * @param array $date_range Optional date range with 'start' and 'end' keys (Y-m-d format).
	 *
	 * @return array Associative array with total, active, pending, rejected, and suspended affiliate counts.
	 */
	public function get_statistics( array $date_range = array() ): array {
		$defaults = array(
			'start' => '',
			'end'   => '',
		);

		$date_range = wp_parse_args( $date_range, $defaults );

		// Allow filtering date range.
		$date_range = apply_filters( 'wc_affiliate_affiliate_statistics_date_range', $date_range );

		// Get users with affiliate role.
		$user_args = array(
			'role'   => 'affiliate',
			'number' => -1, // Get all users.
		);

		// Date range filters (based on user registration date)..
		if ( ! empty( $date_range['start'] ) ) {
			$user_args['date_query'] = array(
				array(
					'after'     => $date_range['start'] . ' 00:00:00',
					'inclusive' => true,
				),
			);
		}

		if ( ! empty( $date_range['end'] ) ) {
			if ( isset( $user_args['date_query'] ) ) {
				$user_args['date_query'][0]['before']    = $date_range['end'] . ' 23:59:59';
				$user_args['date_query'][0]['inclusive'] = true;
			} else {
				$user_args['date_query'] = array(
					array(
						'before'    => $date_range['end'] . ' 23:59:59',
						'inclusive' => true,
					),
				);
			}
		}

		$users = get_users( $user_args );

		// Count statuses.
		$stats = array(
			'total'     => 0,
			'active'    => 0,
			'pending'   => 0,
			'rejected'  => 0,
			'suspended' => 0,
		);

		foreach ( $users as $user ) {
			$user_model = new User( $user->ID );
			$status     = $user_model->get_meta( '_wc_affiliate_status' );

			if ( isset( $stats[ $status ] ) ) {
				++$stats[ $status ];
			}
			++$stats['total'];
		}

		return apply_filters( 'wc_affiliate_affiliate_statistics', $stats, $date_range, $user_args );
	}

	/**
	 * Get affiliate token
	 *
	 * @return string Affiliate token.
	 */
	public function get_token(): string {
		if ( empty( $this->meta['token'] ) ) {
			$this->generate_token();
		}

		return $this->meta['token'];
	}

	/**
	 * Get affiliate level in the referral tree
	 *
	 * Calculates the tier/level of an affiliate in the multi-level referral structure.
	 * Level 1 represents top-tier affiliates (no referrer), Level 2 represents first-level
	 * referrals, and so on.
	 *
	 * Uses caching to optimize performance for repeated calls.
	 *
	 * @since 1.0.0
	 *
	 * @param int $user_id   Optional. Affiliate user ID. Default is current affiliate's user_id.
	 * @param int $max_depth Optional. Maximum depth to prevent infinite loops. Default 100.
	 *
	 * @return int The level of the affiliate (1 = top tier, 2 = first level down, etc.)
	 */
	public function get_level( int $user_id = 0, int $max_depth = 100 ): int {
		// Use current affiliate's user_id if not specified.
		if ( 0 === $user_id ) {
			$user_id = $this->user_id;
		}

		// Start from level 1 (top tier).
		$level           = 1;
		$current_user_id = $user_id;
		$depth           = 0;
		$visited_users   = array(); // Prevent circular references.

		while ( $depth < $max_depth ) {
			// Prevent infinite loops from circular references.
			if ( isset( $visited_users[ $current_user_id ] ) ) {
				break;
			}
			$visited_users[ $current_user_id ] = true;

			// Get referrer ID.
			$referrer_id = get_user_meta( $current_user_id, '_wc_affiliate_referrer', true );

			// If no referrer is found, we've reached the top of the tree.
			if ( empty( $referrer_id ) ) {
				break;
			}

			// Move up to the next level.
			$current_user_id = (int) $referrer_id;
			++$level;
			++$depth;
		}

		/**
		 * Filter the calculated affiliate level.
		 *
		 * Allows modification of the affiliate level before caching.
		 *
		 * @since 1.0.0
		 *
		 * @param int $level   The calculated level.
		 * @param int $user_id The affiliate user ID.
		 * @param int $depth   The depth reached during calculation.
		 */
		return apply_filters( 'wc_affiliate_calculated_level', $level, $user_id, $depth );
	}

	/**
	 * Get affiliate level label
	 *
	 * Returns a human-readable label for the affiliate's level.
	 *
	 * @since 1.0.0
	 *
	 * @param int $level Optional. The level number. If not provided, uses current affiliate's level.
	 *
	 * @return string The level label (e.g., "Level 1", "Level 2", etc.)
	 */
	public function get_level_label( int $level = 0 ): string {
		if ( 0 === $level ) {
			$level = $this->get_level();
		}

		/**
		 * Filter the affiliate level labels.
		 *
		 * Allows customization of level labels for different tiers.
		 *
		 * @since 1.0.0
		 *
		 * @param array $labels Array of level labels keyed by level number.
		 */
		$labels = apply_filters(
			'wc_affiliate_level_labels',
			array(
				1 => __( 'Level 1 - Top Tier', 'wc-affiliate' ),
				2 => __( 'Level 2 - First Tier', 'wc-affiliate' ),
				3 => __( 'Level 3 - Second Tier', 'wc-affiliate' ),
				4 => __( 'Level 4 - Third Tier', 'wc-affiliate' ),
				5 => __( 'Level 5 - Fourth Tier', 'wc-affiliate' ),
			)
		);

		// Return custom label if exists, otherwise default format.
		if ( isset( $labels[ $level ] ) ) {
			return $labels[ $level ];
		}

		// translators: %d: Level number.
		return sprintf( __( 'Level %d', 'wc-affiliate' ), $level );
	}

	/**
	 * Generate unique affiliate token
	 *
	 * @return string Generated token.
	 */
	public function generate_token(): string {
		$token = $this->user ? wca_generate_token( wca_get_token_type() ) : '';

		$this->meta['token'] = $token;

		$this->user_model->update_meta( '_wc_affiliate_token', $token );

		return $token;
	}

	/**
	 * Get commission settings
	 *
	 * @return array Associative array with 'type' (fixed or percent) and 'amount' (float) of commission.
	 */
	public function get_commission_settings(): array {
		$commission_types          = wca_get_commission_types();
		$default_commission_type   = wca_get_commission_type();
		$default_commission_amount = wca_get_commission_amount();

		$commission_type       = ! empty( $this->meta['commission_type'] ) ? $this->meta['commission_type'] : 'default';
		$commission_amount     = ! empty( $this->meta['commission_amount'] ) ? $this->meta['commission_amount'] : $default_commission_amount;
		$commission_type_label = 'default' === $commission_type ? __( 'Default', 'wc-affiliate' ) : $commission_types[ $commission_type ];
		if ( 'default' === $commission_type ) {
			$commission_type   = $default_commission_type;
			$commission_amount = $default_commission_amount;
		}

		$commission_settings = array(
			'type'   => $commission_type,
			'label'  => $commission_type_label,
			'amount' => (float) $commission_amount,
		);

		/**
		 * Filter the affiliate's commission settings.
		 *
		 * Allows developers to modify the commission type and amount for a specific affiliate.
		 *
		 * @param array $commission_settings Array containing 'type' and 'amount' keys.
		 * @param int   $user_id             The affiliate's user ID.
		 */
		return apply_filters( 'wc_affiliate_current_affiliate_commission_settings', $commission_settings, $this->user_id );
	}

	/**
	 * Get total paid amount
	 *
	 * @return float Total amount paid to affiliate.
	 */
	public function get_total_paid(): float {
		global $wpdb;

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM(amount) FROM {$wpdb->prefix}wca_transactions WHERE affiliate = %d AND status = %s",
				$this->user_id,
				TransactionModel::STATUS_COMPLETED
			)
		);

		$total = (float) $total;

		return apply_filters( 'wc_affiliate_current_affiliate_total_paid', $total, $this->user_id );
	}

	/**
	 * Get affiliate balance
	 *
	 * @return float Current balance available for payout.
	 */
	public function get_balance(): float {
		global $wpdb;

		$balance = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM(commission) FROM {$wpdb->prefix}wca_referrals WHERE affiliate = %d AND payment_status = %s AND transaction_id = 0",
				$this->user_id,
				Referral::STATUS_APPROVED
			)
		);

		$balance = (float) $balance;

		return apply_filters( 'wc_affiliate_current_affiliate_balance', $balance, $this->user_id );
	}

	/**
	 * Get total earnings
	 *
	 * @return float Total earnings from all paid and processing referrals.
	 */
	public function get_total_earnings(): float {
		global $wpdb;

		$allowed_statuses = array( Referral::STATUS_APPROVED, Referral::STATUS_PAID );
		$allowed_statuses = apply_filters( 'wc_affiliate_current_affiliate_earnings_allowed_statuses', $allowed_statuses, $this->user_id );

		$join_allowed_statuses = implode( ',', array_map( fn( $status ) => "'$status'", $allowed_statuses ) );

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM(commission) FROM {$wpdb->prefix}wca_referrals WHERE affiliate = %d AND payment_status IN ( $join_allowed_statuses )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$this->user_id
			)
		);

		$total = (float) $total;

		return apply_filters( 'wc_affiliate_current_affiliate_total_earnings', $total, $this->user_id );
	}

	/**
	 * Get referral count
	 *
	 * @param string $status Optional status to filter referrals (e.g., 'paid', 'unpaid'). If null, counts all referrals.
	 *
	 * @return int Total number of referrals.
	 */
	public function get_referral_count( string $status = Referral::STATUS_PAID ): int {
		global $wpdb;

		$query  = "SELECT COUNT(*) FROM {$wpdb->prefix}wca_referrals WHERE affiliate = %d";
		$params = array( $this->user_id );

		if ( $status ) {
			$query   .= ' AND payment_status = %s';
			$params[] = $status;
		}

		$count = (int) $wpdb->get_var( $wpdb->prepare( $query, ...$params ) );

		return apply_filters( 'wc_affiliate_current_affiliate_referral_count', $count, $this->user_id, $status );
	}

	/**
	 * Get visit count
	 *
	 * @param int $days Optional number of days to limit the visit count. If null, counts all visits.
	 *
	 * @return int Total number of visits.
	 */
	public function get_visit_count( int $days = 30 ): int {
		global $wpdb;

		$query  = "SELECT COUNT(*) FROM {$wpdb->prefix}wca_visits WHERE affiliate = %d";
		$params = array( $this->user_id );

		if ( $days ) {
			$start_time = time() - ( $days * DAY_IN_SECONDS );
			$query     .= ' AND time > %d';
			$params[]   = $start_time;
		}

		$count = (int) $wpdb->get_var( $wpdb->prepare( $query, ...$params ) );

		return apply_filters( 'wc_affiliate_current_affiliate_visit_count', $count, $this->user_id, $days );
	}

	/**
	 * Get conversion rate
	 *
	 * @return float Conversion rate as percentage.
	 */
	public function get_conversion_rate(): float {
		$visits    = $this->get_visit_count();
		$referrals = $this->get_referral_count();

		$conversion_rate = $visits > 0 ? ( $referrals / $visits ) * 100 : 0;

		return apply_filters( 'wc_affiliate_current_affiliate_conversion_rate', $conversion_rate, $this->user_id, $visits, $referrals );
	}

	/**
	 * Get referred affiliates
	 */
	public function get_referred_by(): array {
		$referrer_id = $this->meta['referrer_user_id'];
		if ( empty( $referrer_id ) ) {
			return array(
				'display_name'   => __( 'Admin', 'wc-affiliate' ),
				'user_id'        => get_current_user_id(),
				'user_edit_link' => '',
			);
		}

		$referred_user_data    = get_user_by( 'id', $referrer_id );
		$referrer_profile_link = get_edit_user_link( $referrer_id );

		return array(
			'display_name'   => $referred_user_data->display_name,
			'user_id'        => $referrer_id,
			'user_edit_link' => $referrer_profile_link,
		);
	}

	/**
	 * Update affiliate status
	 *
	 * @param string $status        New status to set (pending, active, rejected, suspended).
	 * @param string $admin_message Optional admin message for status change notifications.
	 *
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public function update_status( string $status, string $admin_message = '' ) {
		$valid_statuses = array_keys( $this->get_available_statuses() );

		if ( ! in_array( $status, $valid_statuses, true ) ) {
			return new WP_Error( 'invalid_status', __( 'Invalid affiliate status', 'wc-affiliate' ) );
		}

		$old_status = $this->get_status();

		$this->meta['status'] = $status;

		$this->user_model->update_meta( '_wc_affiliate_status', $status );

		// Set application time for active status..
		if ( self::STATUS_ACTIVE === $status && self::STATUS_ACTIVE !== $old_status ) {
			$this->user_model->update_meta( '_wc_affiliate_time_applied', time() );
			$this->meta['time_applied'] = time();
		}

		// Fire hooks based on status change..
		if ( $old_status !== $status ) {
			switch ( $status ) {
				case self::STATUS_ACTIVE:
					do_action( 'wc_affiliate_application_approved', $this->user_id, $admin_message );
					break;
				case self::STATUS_REJECTED:
					do_action( 'wc_affiliate_application_rejected', $this->user_id, $admin_message );
					break;
			}
		}

		return true;
	}

	/**
	 * Update commission settings
	 *
	 * @param string $type   Commission type ('fixed' or 'percent').
	 * @param float  $amount Commission amount (non-negative number).
	 *
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public function update_commission_settings( string $type, float $amount ) {
		$valid_types = array( 'fixed', 'percent' );

		if ( ! in_array( $type, $valid_types, true ) ) {
			return new WP_Error( 'invalid_commission_type', __( 'Invalid commission type.', 'wc-affiliate' ) );
		}

		if ( $amount < 0 ) {
			return new WP_Error( 'invalid_commission_amount', __( 'Commission amount must be non-negative.', 'wc-affiliate' ) );
		}

		$settings = array(
			'type'   => $type,
			'amount' => $amount,
		);

		// Allow filtering commission settings before update.
		$settings = apply_filters( 'wc_affiliate_commission_settings_update', $settings, $this->user_id );

		$this->user_model->update_meta( '_wc_affiliate_commission_settings', $settings );

		$this->meta['commission_settings'] = $settings;

		do_action( 'wc_affiliate_commission_settings_updated', $this->user_id, $settings );

		return true;
	}

	/**
	 * Update payment settings
	 *
	 * @param string $method  Payment method (e.g., 'paypal', 'bank_transfer').
	 * @param array  $details Payment details specific to the method.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function update_payment_settings( string $method, array $details = array() ): bool {
		$this->meta['payment_method']  = $method;
		$this->meta['payment_details'] = $details;

		$this->user_model->update_meta( '_wc_affiliate_payout_method', $method );
		$this->user_model->update_meta( '_wc_affiliate_payment_details', $details );

		return true;
	}

	/**
	 * @return void
	 */
	public function remove_payout_request_badge() {
		$this->delete_meta( '_wc-affiliate-applied_payout' );
	}

	/**
	 * Get affiliate statistics for a specific period
	 *
	 * @param int $period_days Number of days for the statistics period. Default is 30 days.
	 *
	 * @return array Associative array containing statistics such as visits, referrals, commissions, sales, average commission, conversion rate, total balance, and total earnings.
	 */
	public function get_period_statistics( int $period_days = 30 ): array {
		$start_time = time() - ( $period_days * DAY_IN_SECONDS );

		global $wpdb;

		// Get period-specific stats from approved/paid referrals only.
		$valid_statuses      = array( Referral::STATUS_APPROVED, Referral::STATUS_PAID );
		$status_placeholders = implode( ',', array_map( fn( $status ) => "'$status'", $valid_statuses ) );

		$sql    = "SELECT COUNT(id) as referrals, SUM(commission) as commissions, SUM(order_total) as sales, AVG(commission) as avg_commission FROM {$wpdb->prefix}wca_referrals WHERE affiliate = %d AND payment_status IN ($status_placeholders)";
		$params = array( $this->user_id );
		if ( $period_days > 0 ) {
			$sql     .= ' AND time > %d';
			$params[] = $start_time;
		}

		$stats  = $wpdb->get_row( $wpdb->prepare( $sql, $params ), ARRAY_A );
		$visits = $this->get_visit_count( $period_days );

		$statistics = array(
			'visits'          => $visits,
			'referrals'       => (int) $stats['referrals'],
			'commissions'     => (float) $stats['commissions'],
			'sales'           => (float) $stats['sales'],
			'avg_commission'  => (float) $stats['avg_commission'],
			'conversion_rate' => $visits > 0 ? ( $stats['referrals'] / $visits ) * 100 : 0,
			'total_balance'   => $this->get_balance(),
			'total_earnings'  => $this->get_total_earnings(),
		);

		return apply_filters( 'wc_affiliate_current_affiliate_period_statistics', $statistics, $period_days );
	}

	/**
	 * Create a new affiliate application
	 *
	 * @param int   $user_id          User ID applying to be an affiliate.
	 * @param array $application_data Optional application data to store.
	 *
	 * @return Affiliate|WP_Error Affiliate object on success, WP_Error on failure.
	 */
	public function create_application( int $user_id, array $application_data = array() ) {
		$affiliate = new self( $user_id );

		if ( $affiliate->exists() ) {
			return new WP_Error( 'already_exists', __( 'User is already an affiliate', 'wc-affiliate' ) );
		}

		// Set initial status using User model..
		$user_model = new User( $user_id );
		$user_model->update_meta( '_wc_affiliate_status', self::STATUS_PENDING );
		$user_model->update_meta( '_wc_affiliate_time_applied', time() );

		// Store application data with sanitization..
		if ( ! empty( $application_data ) ) {
			$user_model->update_meta( '_wc_affiliate_application_data', $this->sanitize_application_data( $application_data ) );
		}

		// Generate token..
		$affiliate = new self( $user_id );
		$affiliate->generate_token();

		// Fire application hook..
		do_action( 'wc_affiliate_application_submitted', $user_id, $application_data );

		return $affiliate;
	}

	/**
	 * Get affiliate by token
	 *
	 * @param string $token Affiliate token to search for.
	 *
	 * @return Affiliate|null Affiliate object if found, null otherwise.
	 */
	public function get_by_token( string $token ): ?Affiliate {
		global $wpdb;

		$user_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT user_id FROM {$wpdb->usermeta} 
			 WHERE meta_key = '_wc_affiliate_token' AND meta_value = %s",
				$token
			)
		);

		return $user_id ? new self( $user_id ) : null;
	}

	/**
	 * Get affiliate count
	 *
	 * @param array $args Optional status to filter by.
	 *
	 * @return int Total number of affiliates.
	 */
	public function get_count( array $args = array() ): int {
		$defaults = array(
			'status' => '',
			'search' => '',
		);

		$args = wp_parse_args( $args, $defaults );

		// Use get_users with affiliate role to count.
		$user_args = array(
			'role'   => 'affiliate',
			'number' => -1, // Get all users, we'll count them.
			'fields' => 'ID', // Only get IDs for counting.
		);

		// Add search filter.
		$users = $this->get_users( $args, $user_args );

		return count( $users );
	}

	/**
	 * Get affiliate leaderboard
	 *
	 * Retrieves top-performing affiliates with their statistics for a given period.
	 * Collects referrals, sales, and commissions from wca_referrals table and
	 * visits count from wca_visits table using optimized LEFT JOIN.
	 *
	 * @param int    $limit       Number of top affiliates to return. Default 10.
	 * @param int    $period_days Number of days to consider for the leaderboard. Default 30.
	 * @param string $order_by    Field to order by: 'commissions', 'referrals', or 'visits'. Default 'commissions'.
	 *
	 * @return array List of top affiliates with their stats.
	 */
	public function get_leaderboard( int $limit = 10, int $period_days = 30, string $order_by = 'commissions' ): array {
		global $wpdb;

		// Calculate start time for the period.
		$start_time = time() - ( $period_days * DAY_IN_SECONDS );

		// Determine the ORDER BY field based on parameter.
		$order_field = 'visits' === $order_by ? 'visit_count' : ( 'referrals' === $order_by ? 'referral_count' : 'total_commission' );

		// Build optimized query with LEFT JOIN to get visits count.
		$referrals_table = $wpdb->prefix . 'wca_referrals';
		$visits_table    = $wpdb->prefix . 'wca_visits';

		$query = "
		SELECT 
			r.affiliate,
			COUNT(r.id) as referral_count,
			SUM(r.commission) as total_commission,
			SUM(r.order_total) as total_sales,
			COALESCE(v.visit_count, 0) as visit_count
		FROM {$referrals_table} r
		LEFT JOIN (
			SELECT 
				affiliate,
				COUNT(*) as visit_count
			FROM {$visits_table}
			WHERE time > %d
			GROUP BY affiliate
		) v ON r.affiliate = v.affiliate
		WHERE r.time > %d
		GROUP BY r.affiliate
		ORDER BY {$order_field} DESC
		LIMIT %d
	";

		// Execute the query with prepared statement.
		$results = $wpdb->get_results(
			$wpdb->prepare(
				$query,
				$start_time,
				$start_time,
				$limit
			)
		);

		// Build leaderboard array with affiliate data.
		$leaderboard = array();
		foreach ( $results as $result ) {
			$affiliate = new self( $result->affiliate );

			// Skip if affiliate does not exist.
			if ( ! $affiliate->exists() ) {
				continue;
			}

			// Get affiliate data.
			$data = $affiliate->to_array();

			// Add period-specific stats.
			$data['period_stats'] = array(
				'referrals'             => (int) $result->referral_count,
				'commissions'           => (float) $result->total_commission,
				'commissions_formatted' => wca_format_price( $result->total_commission ),
				'sales'                 => (float) $result->total_sales,
				'visits'                => (int) $result->visit_count,
			);

			$leaderboard[] = $data;
		}

		/**
		 * Filter the affiliate leaderboard data.
		 *
		 * @param array  $leaderboard Array of affiliate leaderboard data.
		 * @param int    $limit       Number of affiliates requested.
		 * @param int    $period_days Period in days.
		 * @param string $order_by    Field used for ordering.
		 */
		return apply_filters( 'wc_affiliate_affiliate_leaderboard', $leaderboard, $limit, $period_days, $order_by );
	}

	/**
	 * Get available affiliate statuses
	 *
	 * @return array Array of status keys and labels.
	 */
	protected function get_available_statuses(): array {
		return apply_filters(
			'wc_affiliate_all_available_statuses',
			array(
				self::STATUS_PENDING   => __( 'Pending', 'wc-affiliate' ),
				self::STATUS_ACTIVE    => __( 'Active', 'wc-affiliate' ),
				self::STATUS_REJECTED  => __( 'Rejected', 'wc-affiliate' ),
				self::STATUS_SUSPENDED => __( 'Suspended', 'wc-affiliate' ),
			)
		);
	}

	/**
	 * Save commission fields for affiliate.
	 *
	 * @param int    $user_id           User ID.
	 * @param string $status            Affiliate status.
	 * @param string $commission_type   Commission type.
	 * @param float  $commission_amount Commission amount.
	 *
	 * @return void
	 */
	public function save_commission_fields( int $user_id, string $status, string $commission_type, float $commission_amount ): void {
		$user_model = new User( $user_id );
		$user_model->update_meta( '_wc_affiliate_status', $status );
		$user_model->update_meta( 'commission_type', $commission_type );
		$user_model->update_meta( 'commission_amount', $commission_amount );
		$user_model->update_meta( '_wc_affiliate_time_applied', time() );

		if ( 'active' === $status ) {
			$user = new WP_User( $user_id );
			$user->add_role( 'affiliate' );
		}
	}

	/**
	 * Get affiliate activity (referrals and visits combined)
	 *
	 * @param int $affiliate_id Affiliate ID.
	 * @param int $limit        Number of items to return.
	 *
	 * @return array Combined activity data
	 */
	public function get_affiliate_activity( int $affiliate_id, int $limit = 20 ): array {
		// Get recent referrals.
		$referral_model = new Referral();
		$referrals      = $referral_model->get_referrals(
			array(
				'affiliate_id' => $affiliate_id,
				'limit'        => $limit,
				'orderby'      => 'time',
				'order'        => 'DESC',
			)
		);

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

		// Combine and sort by time.
		$activity = array();

		// Add referrals.
		foreach ( $referrals as $referral ) {
			$activity[] = array(
				'type'            => 'referral',
				'id'              => $referral->get_id(),
				'commission'      => (float) $referral->get( 'commission' ),
				'time'            => (int) $referral->get( 'time' ),
				'order_total'     => (float) $referral->get( 'order_total' ),
				'referral_status' => $referral->get( 'status' ),
				'page_url'        => '',
				'campaign'        => '',
			);
		}

		// Add visits.
		foreach ( $visits as $visit ) {
			$activity[] = array(
				'type'            => 'visit',
				'id'              => $visit->get_id(),
				'commission'      => 0,
				'time'            => (int) $visit->get( 'time' ),
				'order_total'     => 0,
				'referral_status' => '',
				'page_url'        => $visit->get( 'page_url' ),
				'campaign'        => $visit->get( 'campaign' ),
			);
		}

		// Sort by time descending and limit.
		usort(
			$activity,
			function ( $a, $b ) {
				return $b['time'] <=> $a['time'];
			}
		);

		return array_slice( $activity, 0, $limit );
	}

	/**
	 * Get users with affiliate filtering.
	 *
	 * @param array $args      Affiliate-specific arguments.
	 * @param array $user_args WordPress user query arguments.
	 *
	 * @return array
	 */
	public function get_users( array $args, array $user_args ): array {
		if ( ! empty( $args['search'] ) ) {
			$user_args['search']         = '*' . esc_attr( $args['search'] ) . '*';
			$user_args['search_columns'] = array( 'display_name', 'user_login', 'user_email' );
		}

		// Always include meta_query to ensure _wc_affiliate_status exists.
		$meta_query = array(
			array(
				'key'     => '_wc_affiliate_status',
				'compare' => 'EXISTS',
			),
		);

		// Add status filter using meta_query if specific status is requested.
		if ( ! empty( $args['status'] ) && 'all' !== $args['status'] ) {
			$meta_query[] = array(
				'key'     => '_wc_affiliate_status',
				'value'   => $args['status'],
				'compare' => '=',
			);
		}

		$user_args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query

		// Get users with affiliate role and status filter.
		return get_users( $user_args );
	}

	/**
	 * Validate affiliate data
	 *
	 * @param array $data Data to validate.
	 *
	 * @return true|array True if valid, array of errors if invalid.
	 */
	public function validate_data( array $data ) {
		$errors = array();

		// Use ValidationTrait methods..
		if ( isset( $data['commission_type'] ) ) {
			$this->validate_commission_type( $data['commission_type'], $errors );
		}

		if ( isset( $data['commission_amount'] ) ) {
			$this->validate_amount( $data['commission_amount'], $errors, true );
		}

		if ( isset( $data['status'] ) ) {
			$valid_statuses = array_keys( $this->get_available_statuses() );
			$this->validate_status( $data['status'], $valid_statuses, $errors );
		}

		$errors = apply_filters( 'wc_affiliate_validate_affiliate_data', $errors, $data, $this->user_id );

		return empty( $errors ) ? true : $errors;
	}

	/**
	 * Sanitize affiliate data
	 *
	 * @param array $data Raw data to sanitize.
	 *
	 * @return array Sanitized data.
	 */
	public function sanitize_data( array $data ): array {
		$sanitized = array();

		if ( isset( $data['commission_type'] ) ) {
			$sanitized['commission_type'] = sanitize_text_field( $data['commission_type'] );
		}

		if ( isset( $data['commission_amount'] ) ) {
			$sanitized['commission_amount'] = (float) $data['commission_amount'];
		}

		if ( isset( $data['status'] ) ) {
			$sanitized['status'] = sanitize_text_field( $data['status'] );
		}

		if ( isset( $data['payment_method'] ) ) {
			$sanitized['payment_method'] = sanitize_text_field( $data['payment_method'] );
		}

		if ( isset( $data['auto_payout'] ) ) {
			$sanitized['auto_payout'] = $data['auto_payout'] ? 'yes' : 'no';
		}

		return apply_filters( 'wc_affiliate_sanitize_affiliate_data', $sanitized, $data, $this->user_id );
	}

	/**
	 * Sanitize affiliate application data to prevent mass assignment
	 *
	 * @param array $data Raw application data.
	 *
	 * @return array Sanitized application data
	 */
	protected function sanitize_application_data( array $data ): array {
		$sanitized = array();

		// Define allowed fields and their sanitization types..
		$allowed_fields = array(
			'company'          => 'text',
			'website'          => 'url',
			'description'      => 'textarea',
			'experience'       => 'textarea',
			'promotion_method' => 'textarea',
			'social_media'     => 'url',
			'expected_traffic' => 'text',
			'payment_method'   => 'text',
			'tax_info'         => 'text',
			'additional_info'  => 'textarea',
		);

		foreach ( $data as $key => $value ) {
			$safe_key = sanitize_key( $key );

			// Only process allowed fields..
			if ( isset( $allowed_fields[ $safe_key ] ) ) {
				$sanitization_type = $allowed_fields[ $safe_key ];

				switch ( $sanitization_type ) {
					case 'url':
						$sanitized[ $safe_key ] = sanitize_text_field( $value );
						break;
					case 'textarea':
						$sanitized[ $safe_key ] = sanitize_text_field( $value, 'textarea' );
						break;
					case 'text':
					default:
						$sanitized[ $safe_key ] = sanitize_text_field( $value );
						break;
				}
			}
		}

		return $sanitized;
	}
}
