<?php
/**
 * Model: Referral.php.
 *
 * @package RewardsWP
 */

namespace Awesomemotive\Rewardswp\Models;

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

use Awesomemotive\Rewardswp\Abstracts\AbstractModelWithMeta;
use Awesomemotive\Rewardswp\Database\DatabaseHandler;
use Awesomemotive\Rewardswp\Database\Tables\ReferralsTable;
use Awesomemotive\Rewardswp\Services\ReferralLifecycleService;
use Exception;
use InvalidArgumentException;
use function Awesomemotive\Rewardswp\Helpers\count_referrals;
use function Awesomemotive\Rewardswp\Helpers\get_member;
use function Awesomemotive\Rewardswp\Helpers\get_current_time;
use function Awesomemotive\Rewardswp\Helpers\get_rewards;

/**
 * Class Referral
 *
 * Represents the referral entity and handles database interactions.
 *
 * @since 1.0.0
 */
class Referral extends AbstractModelWithMeta {

	/**
	 * WordPress database handler.
	 *
	 * @since 1.0.0
	 * @var DatabaseHandler
	 */
	protected DatabaseHandler $db_handler;

	/**
	 * Table name.
	 *
	 * @since 1.0.0
	 * @var ReferralsTable
	 */
	protected ReferralsTable $db_table;

	/**
	 * Referral lifecycle service instance.
	 *
	 * @since 1.1.0
	 * @var ReferralLifecycleService
	 */
	protected ReferralLifecycleService $referral_lifecycle_service;

	/**
	 * Flag to track if referral has just transitioned to completed in this request.
	 *
	 * @since 1.1.0
	 * @var bool
	 */
	protected bool $just_completed = false;

	/**
	 * Static array to track advocate IDs that need statistics recalculation after deletion.
	 *
	 * @since 1.1.0
	 * @var array
	 */
	private static array $advocates_to_update = [];

	/**
	 * Properties representing referral fields.
	 *
	 * @since 1.0.0
	 * @var int
	 */
	public int $id = 0;

	/**
	 * Advocate member ID.
	 *
	 * @since 1.0.0
	 * @var int
	 */
	public int $advocate_id = 0;

	/**
	 * Origin event ID.
	 *
	 * @since 1.0.0
	 * @var int
	 */
	public int $event_id = 0;

	/**
	 * Referred WordPress user ID.
	 *
	 * @since 1.0.0
	 * @var int|null
	 */
	public ?int $referred_user_id = null;

	/**
	 * Referred email address.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $referred_email = '';

	/**
	 * Related order ID.
	 *
	 * @since 1.0.0
	 * @var int|null
	 */
	public ?int $order_id = null;

	/**
	 * Referral status.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $status = 'pending';

	/**
	 * Referral type.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $type = '';

	/**
	 * Referral source.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $source = 'unknown';

	/**
	 * Creation timestamp.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $created_at = null;

	/**
	 * Conversion timestamp.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $converted_at = null;

	/**
	 * Allowed referral statuses.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	public array $statuses;

	/**
	 * ReferralModel constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param DatabaseHandler          $db_handler                 Database handler instance.
	 * @param ReferralsTable           $db_table                   Referrals table instance.
	 * @param ReferralLifecycleService $referral_lifecycle_service Referral lifecycle service instance.
	 */
	public function __construct( DatabaseHandler $db_handler, ReferralsTable $db_table, ReferralLifecycleService $referral_lifecycle_service ) {
		$this->db_handler                 = $db_handler;
		$this->db_table                   = $db_table;
		$this->referral_lifecycle_service = $referral_lifecycle_service;

		// Set the referral statuses.
		$this->statuses = [
			'pending'   => esc_html__( 'Pending', 'rewardswp' ),
			'blocked'   => esc_html__( 'Blocked', 'rewardswp' ),
			'completed' => esc_html__( 'Completed', 'rewardswp' ),
			'canceled'  => esc_html__( 'Canceled', 'rewardswp' ),
		];
	}

	/**
	 * Retrieve the labels for the model columns and metadata.
	 *
	 * @since 1.0.0
	 * @return array The referral labels.
	 */
	public function get_labels(): array {
		return $this->db_table->get_columns();
	}

	/**
	 * Sanitize data.
	 *
	 * Must be used before saving data into the DB.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function sanitize(): void {
		$this->advocate_id      = absint( $this->advocate_id );
		$this->event_id         = absint( $this->event_id );
		$this->referred_user_id = absint( $this->referred_user_id );
		$this->referred_email   = sanitize_email( $this->referred_email );
		$this->status           = sanitize_text_field( $this->status );
		$this->type             = sanitize_text_field( $this->type );
		$this->source           = sanitize_text_field( $this->source );
		$this->created_at       = sanitize_text_field( $this->created_at );
		$this->converted_at     = sanitize_text_field( $this->converted_at );
	}

	/**
	 * Validates the referral data before creating.
	 *
	 * @since 1.0.0
	 *
	 * @return bool When validates successfully.
	 * @throws InvalidArgumentException If validation fails.
	 */
	public function validate_create(): bool {
		if ( empty( $this->advocate_id ) ) {
			throw new InvalidArgumentException( esc_html__( 'Advocate ID is required.', 'rewardswp' ) );
		}

		if ( empty( $this->event_id ) ) {
			throw new InvalidArgumentException( esc_html__( 'Event ID is required.', 'rewardswp' ) );
		}

		// Get the status keys to test against it.
		$allowed_statuses = array_keys( $this->statuses );

		if ( ! in_array( $this->status, $allowed_statuses, true ) ) {
			throw new InvalidArgumentException(
				sprintf(
					/* translators: %s is a placeholder for allowed statuses */
					esc_html__(
						'Wrong status found. Only %s are allowed.',
						'rewardswp'
					),
					esc_html( implode( ', ', $allowed_statuses ) )
				)
			);
		}

		// If it has an order ID, it means that this referral is a purchase.
		if ( empty( $this->type ) && ! empty( $this->order_id ) ) {
			$this->type = 'purchase';
		}

		return true;
	}

	/**
	 * Validates the referral data before updating.
	 *
	 * @since 1.0.0
	 * @return bool When validates successfully.
	 * @throws InvalidArgumentException If validation fails.
	 */
	public function validate_update(): bool {
		// Check if referral ID is provided.
		if ( empty( $this->id ) ) {
			throw new InvalidArgumentException( esc_html__( 'Referral ID is required for updates.', 'rewardswp' ) );
		}

		$this->validate_create();

		return true;
	}

	/**
	 * Complete a referral.
	 *
	 * @since 1.0.0
	 *
	 * @param string|null $source
	 * @param int|null    $order_id
	 *
	 * @return void
	 *
	 * @throws Exception
	 */
	public function set_as_completed( ?string $source = null, ?int $order_id = null ) {
		$this->status         = 'completed';
		$this->just_completed = true;

		if ( ! empty( $source ) ) {
			$this->source = sanitize_text_field( $source );
		}

		if ( ! empty( $order_id ) ) {
			$this->order_id = absint( $order_id );
		}

		$this->converted_at = get_current_time();

		// Save changes to the database.
		$this->update();
	}

	/**
	 * Run before the referral is deleted.
	 *
	 * Tracks the advocate ID for statistics recalculation after deletion.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function before_delete(): void {
		if ( empty( $this->advocate_id ) ) {
			return;
		}

		// Track this advocate ID for batch update after deletion.
		if ( ! in_array( $this->advocate_id, self::$advocates_to_update, true ) ) {
			self::$advocates_to_update[] = $this->advocate_id;
		}

		// Schedule batch update after all deletions in this request.
		if ( ! has_action( 'shutdown', [ self::class, 'update_advocates_statistics_batch' ] ) ) {
			add_action( 'shutdown', [ self::class, 'update_advocates_statistics_batch' ], 999 );
		}
	}

	/**
	 * Run after the referral is saved (created or updated)
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 *
	 * @throws Exception
	 */
	public function after_save(): void {
		$this->update_advocate_referral_counters();

		// Trigger lifecycle side-effects when referral has just completed.
		if ( $this->just_completed ) {

			/**
			 * Fires when a referral is completed.
			 *
			 * @since 1.3.0
			 *
			 * @param Referral $referral The completed referral.
			 */
			do_action( 'rewardswp_referral_completed', $this );

			$this->referral_lifecycle_service->on_referral_completed( $this );
			$this->just_completed = false;
		}
	}

	/**
	 * Update the advocate referral counters.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 *
	 * @throws Exception
	 */
	private function update_advocate_referral_counters() {
		$advocate = get_member( $this->advocate_id );

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

		$advocate->completed_referrals = count_referrals(
			[
				'advocate_id' => $advocate->id,
				'status'      => 'completed',
			]
		);

		$advocate->total_referrals = count_referrals(
			[
				'advocate_id' => $advocate->id,
				'status'      =>
					[
						'pending',
						'completed',
					],
			]
		);

		$advocate->update();
	}

	/**
	 * Batch update advocate statistics after referrals are deleted.
	 *
	 * This static method is called via shutdown hook to recalculate
	 * statistics for all advocates whose referrals were deleted in this request.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public static function update_advocates_statistics_batch(): void {
		if ( empty( self::$advocates_to_update ) ) {
			return;
		}

		// Remove duplicates.
		$advocate_ids = array_unique( self::$advocates_to_update );

		foreach ( $advocate_ids as $advocate_id ) {
			$advocate = get_member( $advocate_id );

			if ( is_null( $advocate ) ) {
				continue;
			}

			$advocate->completed_referrals = count_referrals(
				[
					'advocate_id' => $advocate->id,
					'status'      => 'completed',
				]
			);

			$advocate->total_referrals = count_referrals(
				[
					'advocate_id' => $advocate->id,
					'status'      =>
						[
							'pending',
							'completed',
						],
				]
			);

			$advocate->update();
		}

		// Clear the array after processing.
		self::$advocates_to_update = [];
	}

	/**
	 * Get the reward associated with this referral.
	 *
	 * @since 1.0.0
	 *
	 * @param string $recipient_type The type of recipient to get the reward for.
	 *
	 * @return Reward|null The reward object or null if not found.
	 */
	public function get_reward( string $recipient_type = 'advocate' ): ?Reward {
		if ( empty( $this->id ) ) {
			return null;
		}

		$rewards = get_rewards(
			[
				'referral_id'    => $this->id,
				'recipient_type' => $recipient_type,
			],
			1
		);

		return ! empty( $rewards ) ? $rewards[0] : null;
	}
}
