<?php
/**
 * Model: Point.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\PointsTable;
use InvalidArgumentException;
use function Awesomemotive\Rewardswp\Helpers\get_current_time;
use function Awesomemotive\Rewardswp\Helpers\get_member;

/**
 * Class Point
 *
 * Represents the point entity and handles database interactions.
 *
 * @since 1.0.0
 */
class Point extends AbstractModelWithMeta {
	/**
	 * WordPress database handler.
	 *
	 * @since 1.0.0
	 * @var DatabaseHandler
	 */
	protected DatabaseHandler $db_handler;

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

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

	/**
	 * Member ID owning the points.
	 *
	 * @since 1.0.0
	 * @var int
	 */
	public int $member_id = 0;

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

	/**
	 * Point entry type (earned/spent).
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $type = '';

	/**
	 * Source entity type.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $source_type = '';

	/**
	 * Source entity ID.
	 *
	 * @since 1.0.0
	 * @var int|null
	 */
	public ?int $source_id = null;

	/**
	 * Point entry status.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public string $status = 'approved';

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

	/**
	 * Internal note.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $note_internal = null;

	/**
	 * Public note.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $note_public = null;

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

	/**
	 * Allowed point statuses.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	private array $allowed_statuses = [
		'pending',
		'approved',
		'expired',
		'revoked',
	];

	/**
	 * Static array to track member IDs that need points balance recalculation after deletion.
	 *
	 * @since 1.1.0
	 * @var array
	 */
	private static array $members_to_update = [];

	/**
	 * Constructor.
	 *
	 * @param DatabaseHandler $db_handler
	 * @param PointsTable     $db_table
	 * @since 1.0.0
	 */
	public function __construct(
		DatabaseHandler $db_handler,
		PointsTable $db_table
	) {
		$this->db_handler = $db_handler;
		$this->db_table   = $db_table;
	}

	/**
	 * Retrieve the labels for the model columns and metadata.
	 *
	 * @since 1.0.0
	 * @return array The 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->member_id     = absint( $this->member_id );
		$this->points        = absint( $this->points );
		$this->type          = sanitize_text_field( $this->type );
		$this->source_type   = sanitize_text_field( $this->source_type );
		$this->source_id     = ! empty( $this->source_id ) ? absint( $this->source_id ) : null;
		$this->status        = sanitize_text_field( $this->status );
		$this->expires_at    = ! empty( $this->expires_at ) ? sanitize_text_field( $this->expires_at ) : null;
		$this->note_internal = ! empty( $this->note_internal ) ? sanitize_text_field( $this->note_internal ) : null;
		$this->note_public   = ! empty( $this->note_public ) ? sanitize_text_field( $this->note_public ) : null;
		$this->created_at    = ! empty( $this->created_at ) ? sanitize_text_field( $this->created_at ) : '';
	}

	/**
	 * Validates the point 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->member_id ) ) {
			throw new InvalidArgumentException( esc_html__( 'Member ID is required.', 'rewardswp' ) );
		}

		if ( empty( $this->points ) ) {
			throw new InvalidArgumentException( esc_html__( 'Points amount is required.', 'rewardswp' ) );
		}

		if ( empty( $this->source_type ) ) {
			throw new InvalidArgumentException( esc_html__( 'Source type is required.', 'rewardswp' ) );
		}

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

		// Ensure spent is used for negative points.
		$this->type = 'spent' === $this->type || $this->points < 0
			? 'spent'
			: 'earned';

		// Ensure points are always positive - the type field determines earned/spent.
		$this->points = abs( $this->points );

		return true;
	}

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

		$this->validate_create();

		return true;
	}

	/**
	 * Check if the points have expired.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function is_expired(): bool {
		if ( empty( $this->expires_at ) ) {
			return false;
		}

		return strtotime( $this->expires_at ) < time();
	}

	/**
	 * Check if the points are approved.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function is_approved(): bool {
		return 'approved' === $this->status;
	}

	/**
	 * Check if the points are pending.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function is_pending(): bool {
		return 'pending' === $this->status;
	}

	/**
	 * Check if the points are revoked.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function is_revoked(): bool {
		return 'revoked' === $this->status;
	}

	/**
	 * Get formatted points value with + or - prefix.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_formatted_points(): string {
		return sprintf(
			'%s%s',
			'earned' === $this->type ? '+' : '-',
			number_format( $this->points )
		);
	}

	/**
	 * Run before the point entry is deleted.
	 *
	 * Tracks the member ID for points balance recalculation after deletion.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function before_delete(): void {
		if ( empty( $this->member_id ) ) {
			return;
		}

		// Only update balance if this point entry was approved and affects the balance.
		if ( 'approved' !== $this->status ) {
			return;
		}

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

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

	/**
	 * Batch update member points balance after point entries are deleted.
	 *
	 * This static method is called via shutdown hook to recalculate
	 * points balance for all members whose point entries were deleted in this request.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public static function update_members_points_balance_batch(): void {
		if ( empty( self::$members_to_update ) ) {
			return;
		}

		// Remove duplicates.
		$member_ids = array_unique( self::$members_to_update );

		// Get PointsTable instance to access table name.
		// We'll get it from the container via a temporary Point instance.
		$container = am_rewardswp()->get_container();
		if ( ! $container ) {
			return;
		}

		$points_table = $container->get( PointsTable::class );
		$points_table_name = $points_table->get_prefixed_table_name();

		// Get DatabaseHandler instance.
		$db_handler = $container->get( \Awesomemotive\Rewardswp\Database\DatabaseHandler::class );

		foreach ( $member_ids as $member_id ) {
			$member = get_member( $member_id );

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

			// Recalculate balance based on remaining approved point entries.
			// Calculate earned points.
			$earned_sql = "SELECT COALESCE(SUM(points), 0) FROM {$points_table_name}
					WHERE member_id = %d AND type = 'earned' AND status = 'approved'";
			$points_earned = (int) $db_handler->get_var( $earned_sql, [ $member_id ] );

			// Calculate spent points.
			$spent_sql = "SELECT COALESCE(SUM(points), 0) FROM {$points_table_name}
					WHERE member_id = %d AND type = 'spent' AND status = 'approved'";
			$points_spent = (int) $db_handler->get_var( $spent_sql, [ $member_id ] );

			// Update balance.
			$member->points_balance = max( 0, $points_earned - $points_spent );
			$member->update();
		}

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