<?php
/**
 * Model: Reward.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\RewardsTable;
use Exception;
use InvalidArgumentException;
use function Awesomemotive\Rewardswp\Helpers\get_member;
use function Awesomemotive\Rewardswp\Helpers\get_current_time;
use function Awesomemotive\Rewardswp\Helpers\get_referral;
use function Awesomemotive\Rewardswp\Helpers\get_reward_expiration_date;
use function Awesomemotive\Rewardswp\Helpers\get_points_entries;

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

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

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

	/**
	 * Reward entry ID.
	 *
	 * @since 1.0.0
	 * @var int
	 */
	public int $id = 0;

	/**
	 * Related referral ID.
	 *
	 * @since 1.0.0
	 * @var int
	 */
	public int $referral_id = 0;

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

	/**
	 * Reward type key.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $reward_type = '';

	/**
	 * Recipient type (advocate/friend).
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $recipient_type = '';

	/**
	 * Reward amount.
	 *
	 * @since 1.0.0
	 * @var int
	 */
	public int $amount = 0;

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

	/**
	 * Creation timestamp.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $created_at = '';

	/**
	 * Applied timestamp.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $applied_at = '';

	/**
	 * Expiration timestamp.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $expires_at = '';

	/**
	 * Allowed recipient types.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	private array $allowed_recipient_types = [
		'advocate',
		'friend',
	];

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

	/**
	 * RewardModel constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param DatabaseHandler $db_handler
	 * @param RewardsTable    $db_table
	 */
	public function __construct(
		DatabaseHandler $db_handler,
		RewardsTable $db_table
	) {
		$this->db_handler = $db_handler;
		$this->db_table   = $db_table;

		// Set the referral statuses.
		$this->statuses = [
			'pending'  => esc_html__( 'Pending', 'rewardswp' ),
			'issued'   => esc_html__( 'Issued', 'rewardswp' ),
			'canceled' => esc_html__( 'Canceled', 'rewardswp' ),
			'redeemed' => esc_html__( 'Used', 'rewardswp' ),
		];
	}

	/**
	 * Retrieve the labels for the model columns and metadata.
	 *
	 * @since 1.0.0
	 * @return array The reward 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->referral_id    = absint( $this->referral_id );
		$this->advocate_id    = absint( $this->advocate_id );
		$this->reward_type    = sanitize_text_field( $this->reward_type );
		$this->recipient_type = sanitize_text_field( $this->recipient_type );
		$this->amount         = absint( $this->amount );
		$this->status         = sanitize_text_field( $this->status );
		$this->created_at     = sanitize_text_field( $this->created_at );
		$this->applied_at     = sanitize_text_field( $this->applied_at );
		$this->expires_at     = sanitize_text_field( $this->expires_at );
	}

	/**
	 * Validates the reward data before creating.
	 *
	 * @since 1.0.0
	 *
	 * @return bool When validates successfully.
	 * @throws Exception|InvalidArgumentException If validation fails.
	 */
	public function validate_create(): bool {
		if ( ! in_array( $this->recipient_type, $this->allowed_recipient_types, true ) ) {
			throw new InvalidArgumentException(
				sprintf(
				/* translators: %s is a placeholder for allowed statuses */
					esc_html__(
						'Wrong recipient type found. Only %s are allowed.',
						'rewardswp'
					),
					esc_html( implode( ', ', $this->allowed_recipient_types ) )
				)
			);
		}

		// 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 ( empty( $this->created_at ) ) {
			$this->created_at = get_current_time();
		}

		if ( empty( $this->expires_at ) ) {
			$this->expires_at = get_reward_expiration_date( $this->recipient_type, $this->created_at );
		}

		return true;
	}

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

		// Runs the same validation used for creation.
		$this->validate_create();

		return true;
	}

	/**
	 * Return the referral from this reward.
	 *
	 * @since 1.0.0
	 *
	 * @return Referral|null
	 */
	public function get_referral(): ?Referral {
		if ( empty( $this->referral_id ) ) {
			return null;
		}

		return get_referral( $this->referral_id );
	}

	/**
	 * Return the advocate from this reward.
	 *
	 * @since 1.0.0
	 *
	 * @return Member|null
	 */
	public function get_advocate(): ?Member {
		if ( empty( $this->advocate_id ) ) {
			return null;
		}

		return get_member( $this->advocate_id );
	}

	/**
	 * Set the reward as redeemed.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 * @throws Exception If persisting the updated reward fails.
	 */
	public function set_as_redeemed() {
		$this->status     = 'redeemed';
		$this->applied_at = get_current_time();
		$this->update();
	}

	/**
	 * Set the reward as canceled.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 * @throws Exception If persisting the updated reward or referral fails.
	 */
	public function set_as_canceled() {
		$this->status = 'canceled';
		$this->update();

		$referral = $this->get_referral();

		if ( is_a( $referral, Referral::class ) ) {
			$referral->status = 'canceled';
			$referral->update();
		}
	}

	/**
	 * Get the point entry associated with this reward.
	 *
	 * This method finds the point entry that was created when points were redeemed
	 * to generate this reward. The point entry will have source_type = 'reward_redemption'
	 * and source_id = this reward's ID.
	 *
	 * @since 1.1.0
	 *
	 * @return Point|null The point entry if found, null otherwise.
	 */
	public function get_point_entry(): ?Point {
		if ( empty( $this->id ) ) {
			return null;
		}

		$point_entries = get_points_entries(
			[
				'source_type' => 'reward_redemption',
				'source_id'   => $this->id,
				'type'        => 'spent',
			],
			1, // Only need the first result.
			1
		);

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

	/**
	 * Get the points cost of this reward.
	 *
	 * This method returns the number of points that were spent/redeemed
	 * to generate this reward. It looks for the associated point entry
	 * and returns its points value.
	 *
	 * @since 1.1.0
	 *
	 * @return int The points cost, or 0 if no associated point entry is found.
	 */
	public function get_point_cost(): int {
		$point_entry = $this->get_point_entry();

		if ( ! $point_entry ) {
			return 0;
		}

		// Return the absolute value since spent points are stored as positive.
		// but represent a deduction from the member's balance.
		return (int) abs( $point_entry->points );
	}
}
