<?php
/**
 * Rewards service for managing rewards lifecycle and retrieval.
 *
 * @package RewardsWP
 */

/* phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- We have control over our query engine */

namespace Awesomemotive\Rewardswp\Services;

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

use Awesomemotive\Rewardswp\Abstracts\AbstractModelService;
use Awesomemotive\Rewardswp\Database\DatabaseHandler;
use Awesomemotive\Rewardswp\Database\Tables\ReferralsTable;
use Awesomemotive\Rewardswp\Database\Tables\RewardsTable;
use Awesomemotive\Rewardswp\Models\Referral;
use Awesomemotive\Rewardswp\Models\Reward;
use Awesomemotive\Rewardswp\Settings\Options\Points\SpendActionsOption;
use Awesomemotive\Rewardswp\Factories\MailerFactory;
use Awesomemotive\Rewardswp\Factories\RewardTypesFactory;
use Awesomemotive\Rewardswp\Services\PointsService;
use DI\Container;
use Exception;
use function Awesomemotive\Rewardswp\Helpers\get_option_object;
use function Awesomemotive\Rewardswp\Helpers\get_user_first_name;
use function Awesomemotive\Rewardswp\Helpers\get_user_full_name;

/**
 * Class RewardsService
 *
 * Service for managing rewards retrieval and metadata used across the UI.
 *
 * @since 1.0.0
 */
class RewardsService extends AbstractModelService {

	/**
	 * Referrals database table.
	 *
	 * @since 1.0.0
	 *
	 * @var ReferralsTable
	 */
	private ReferralsTable $referrals_db_table;

	/**
	 * Reward types factory.
	 *
	 * @since 1.1.0
	 * @var RewardTypesFactory
	 */
	private RewardTypesFactory $reward_types_factory;

	/**
	 * Points service.
	 *
	 * @since 1.0.6
	 * @var PointsService
	 */
	private PointsService $points_service;

	/**
	 * Mailer factory.
	 *
	 * @since 1.0.6
	 * @var MailerFactory
	 */
	private MailerFactory $mailer_factory;

	/**
	 * Default reward expiration period for referral rewards (Lite).
	 *
	 * @since 1.1.0
	 */
	public const DEFAULT_REFERRAL_REWARD_EXPIRATION = '+3 months';

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param DatabaseHandler      $db_handler            Database handler instance.
	 * @param ReferralsTable       $referrals_db_table    Referrals table instance.
	 * @param RewardsTable         $rewards_db_table      Rewards table instance.
	 * @param Container            $container             DI container instance.
	 * @param RewardTypesFactory   $reward_types_factory  Reward types factory instance.
	 * @param PointsService        $points_service        Points service instance.
	 * @param MailerFactory        $mailer_factory        Mailer factory instance.
	 */
	public function __construct(
		DatabaseHandler $db_handler,
		ReferralsTable $referrals_db_table,
		RewardsTable $rewards_db_table,
		Container $container,
		RewardTypesFactory $reward_types_factory,
		PointsService $points_service,
		MailerFactory $mailer_factory
	) {
		parent::__construct( $db_handler, $rewards_db_table, $container );

		$this->referrals_db_table   = $referrals_db_table;
		$this->reward_types_factory = $reward_types_factory;
		$this->points_service       = $points_service;
		$this->mailer_factory       = $mailer_factory;
	}

	/**
	 * Retrieve rewards based on the given parameters and pagination options.
	 *
	 * @since 1.0.0
	 *
	 * @param array $where An associative array of conditions to filter rewards.
	 *                     See {@see RewardsTable::get_columns()} for available parameters.
	 * @param int   $per_page Optional. The number of items per page for pagination. Defaults to 10.
	 * @param int   $page  Optional. The page number for pagination. Defaults to 1.
	 *
	 * @return Reward[] Array of Reward objects.
	 */
	public function get_rewards( array $where = [], int $per_page = 10, int $page = 1 ): array {
		return $this->get_objects( Reward::class, $where, $per_page, $page );
	}

	/**
	 * Get the number of rewards based on the given parameters.
	 *
	 * @since 1.0.0
	 *
	 * @param array $where An associative array of conditions to filter rewards.
	 *                     See {@see RewardsTable::get_columns()} for available parameters.
	 * @return int The number of entries found.
	 */
	public function count_rewards( array $where = [] ): int {
		return $this->count_objects( $where );
	}

	/**
	 * Retrieve a reward based on a coupon.
	 *
	 * @since 1.0.0
	 *
	 * @param string $coupon_code The coupon code to look for.
	 *
	 * @return Reward|null The Reward object or null if not found.
	 */
	public function get_reward_by_coupon_code( string $coupon_code ): ?Reward {
		$rewards = $this->get_objects(
			Reward::class,
			[
				'meta_query' => [
					[
						'key'   => 'coupon_code',
						'value' => sanitize_text_field( $coupon_code ),
					],
				],
			],
			1
		);

		return $rewards[0] ?? null;
	}

	/**
	 * Retrieves rewards associated with a specific advocate.
	 *
	 * @param int   $advocate_id The ID of the advocate.
	 * @param array $args Optional. Additional arguments to filter rewards. Default empty array.
	 * @param int   $per_page Optional. Number of rewards per page. Default 99999.
	 * @param int   $page Optional. The page number to retrieve. Default 1.
	 *
	 * @return Reward[] An array of rewards matching the given parameters.
	 */
	public function get_advocate_rewards( int $advocate_id, array $args = [], int $per_page = 99999, int $page = 1 ): array {
		return $this->get_rewards(
			array_filter(
				wp_parse_args(
					$args,
					[
						'advocate_id'    => $advocate_id,
						'recipient_type' => 'advocate',
						'status'         => [],
						'reward_type'    => '',
						'orderby'        => 'created_at',
						'order'          => 'DESC',
					]
				)
			),
			apply_filters( 'am_rewardswp_unlimited', $per_page ),
			$page
		);
	}

	/**
	 * Retrieve the unused friend rewards from a visit.
	 *
	 * Optionally filtered by the order ID.
	 *
	 * @since 1.0.0
	 *
	 * @param int $event_id The event ID.
	 * @param int $order_id Optional order ID.
	 *
	 * @return Reward[] Array of Reward objects.
	 */
	public function get_unused_friend_rewards( int $event_id, int $order_id = 0 ): array {
		if ( $event_id <= 0 ) {
			return []; // We need it at least the event ID.
		}

		return $this->get_objects(
			Reward::class,
			array_filter(
				[
					'recipient_type' => 'friend',
					sprintf( '%s.status', $this->db_table->get_prefixed_table_name() ) => 'issued',
					'join_query'     => [
						[
							'type'  => 'INNER',
							'table' => $this->referrals_db_table->get_prefixed_table_name(),
							'on'    => sprintf(
								'%s.referral_id = %s.id',
								$this->db_table->get_prefixed_table_name(),
								$this->referrals_db_table->get_prefixed_table_name()
							),
							'where' => array_filter(
								[
									'event_id' => $event_id,
									'order_id' => $order_id,
								]
							),
						],
					],
				]
			),
			99999,
			1,
			implode(
				', ',
				array_map(
					fn( $column ) => $this->db_table->get_prefixed_table_name() . '.' . $column,
					array_keys( $this->db_table->get_columns() )
				)
			)
		);
	}

	/**
	 * Get the next reward a member should unlock based on their points balance.
	 *
	 * This method analyzes the available spend actions and determines the next reward
	 * the member should work towards based on their current points balance.
	 *
	 * @since 1.1.0
	 *
	 * @param int $member_id      The member ID.
	 * @param int $points_balance The member's current points balance.
	 * @return array|null Array with reward data or null if no next reward found.
	 *                    Array structure: {
	 *                        'title' => string,
	 *                        'icon_key' => string,
	 *                        'points_cost' => int,
	 *                        'definition_key' => string,
	 *                        'can_afford' => bool
	 *                    }
	 */
	public function get_next_reward_for_member( int $member_id, int $points_balance ): ?array {
		if ( $member_id <= 0 ) {
			return null;
		}

		/**
		 * Spend actions option instance.
		 *
		 * @var SpendActionsOption|null $spend_actions_option
		 */
		$spend_actions_option = get_option_object( SpendActionsOption::class );
		if ( ! $spend_actions_option ) {
			return null;
		}

		$action_definitions = $spend_actions_option->get_action_definitions();
		$saved_actions      = $spend_actions_option->get_value();

		if ( ! is_array( $saved_actions ) ) {
			return null;
		}

		$unaffordable_rewards = [];
		$affordable_rewards   = [];

		foreach ( $saved_actions as $action_data ) {
			// Check if action is enabled (handle both boolean and string values).
			$is_enabled = isset( $action_data['enabled'] ) &&
					( true === $action_data['enabled'] || 1 === $action_data['enabled'] || '1' === $action_data['enabled'] );

			if ( ! $is_enabled || ! isset( $action_data['type'] ) || ! is_array( $action_data ) ) {
				continue;
			}

			$definition_key = $action_data['type'];
			$definition     = $action_definitions[ $definition_key ] ?? null;
			if ( ! $definition ) {
				continue;
			}

			// Determine points cost and user-visible title based on type.
			$points_cost   = 0;
			$display_title = $definition['title'] ?? ucfirst( str_replace( '_', ' ', $definition_key ) );

			switch ( $definition_key ) {
				case 'fixed_discount':
					$amount      = max( 1, absint( $action_data['coupon_amount'] ?? 1 ) );
					$points_cost = $spend_actions_option->calculate_required_points( $action_data );
					// Prefer RewardTypesFactory label when available.
					$display_title = '';
					$type_instance = $this->reward_types_factory->get_reward_type( 'fixed_discount' );
					if ( $type_instance && method_exists( $type_instance, 'get_reward_formatted_amount' ) ) {
						$display_title = $type_instance->get_reward_formatted_amount( (float) $amount );
					}
					break;

				case 'percentage_discount':
					$percent     = max( 1, absint( $action_data['coupon_amount'] ?? 1 ) );
					$points_cost = $spend_actions_option->calculate_required_points( $action_data );
					// Prefer RewardTypesFactory label when available.
					$display_title = '';
					$type_instance = $this->reward_types_factory->get_reward_type( 'percentage_discount' );
					if ( $type_instance && method_exists( $type_instance, 'get_reward_formatted_amount' ) ) {
						$display_title = $type_instance->get_reward_formatted_amount( (float) $percent );
					}
					break;

				case 'free_shipping':
					$points_cost = $spend_actions_option->calculate_required_points( $action_data );
					// Prefer RewardTypesFactory label when available.
					$display_title = '';
					$type_instance = $this->reward_types_factory->get_reward_type( 'free_shipping' );
					if ( $type_instance && method_exists( $type_instance, 'get_reward_formatted_amount' ) ) {
						$display_title = $type_instance->get_reward_formatted_amount();
					}
					break;

				default: // free_product and others.
					$points_cost = $spend_actions_option->calculate_required_points( $action_data );
					// Prefer RewardTypesFactory label when available.
					$display_title = '';
					$type_instance = $this->reward_types_factory->get_reward_type( 'free_product' );
					if ( $type_instance && method_exists( $type_instance, 'get_reward_formatted_amount' ) ) {
						$display_title = $type_instance->get_reward_formatted_amount();
					}
					break;
			}

			// Skip rewards with no cost.
			if ( $points_cost <= 0 ) {
				continue;
			}

			$reward_data = [
				'title'          => $definition['title'] ?? ucfirst( str_replace( '_', ' ', $definition_key ) ),
				'display_title'  => $display_title,
				'icon_key'       => $definition['icon'] ?? 'gift',
				'points_cost'    => $points_cost,
				'definition_key' => $definition_key,
				'can_afford'     => $points_balance >= $points_cost,
			];

			// Categorize rewards.
			if ( $points_cost > $points_balance ) {
				$unaffordable_rewards[] = $reward_data;
			} else {
				$affordable_rewards[] = $reward_data;
			}
		}

		// Prioritize affordable rewards first (what they can redeem now).
		if ( ! empty( $affordable_rewards ) ) {
			// Sort affordable rewards by points cost (ascending) and show the cheapest.
			usort(
				$affordable_rewards,
				function ( $a, $b ) {
					return $a['points_cost'] <=> $b['points_cost'];
				}
			);

			return $affordable_rewards[0]; // Show the reward they can redeem now.
		} elseif ( ! empty( $unaffordable_rewards ) ) {
			// If no affordable rewards, sort unaffordable ones and show the next to unlock.
			usort(
				$unaffordable_rewards,
				function ( $a, $b ) {
					return $a['points_cost'] <=> $b['points_cost'];
				}
			);

			return $unaffordable_rewards[0]; // Show the next reward to unlock.
		}

		// No rewards found.
		return null;
	}

	/**
	 * Delete all rewards and their associated metadata for a given member ID (advocate_id).
	 *
	 * @since 1.1.0
	 *
	 * @param int $member_id The ID of the member (advocate).
	 * @return bool True if all deletions were successful or if no rewards/metadata existed, false on any error.
	 */
	public function delete_by_member_id( int $member_id ): bool {
		if ( $member_id <= 0 ) {
			return false;
		}

		// Get all reward IDs associated with this member_id (advocate_id).
		$reward_ids = $this->db_handler->get_results(
			$this->db_table->get_prefixed_table_name(),
			[ 'advocate_id' => $member_id ],
			'id',
			apply_filters( 'am_rewardswp_unlimited', 99999 )
		);

		$all_successful = true;

		if ( ! empty( $reward_ids ) ) {
			$meta_table_name         = $this->db_table->get_prefixed_meta_table_name();
			$meta_relationship_field = $this->db_table->get_meta_table_relationship_field_name();

			foreach ( $reward_ids as $reward_row ) {
				$reward_id = $reward_row['id'] ?? null;
				if ( $reward_id && $meta_table_name && $meta_relationship_field ) {
					$meta_deleted = $this->db_handler->delete(
						$meta_table_name,
						[ $meta_relationship_field => $reward_id ]
					);
					if ( false === $meta_deleted ) {
						$all_successful = false;
						// Optionally log error for this specific reward_id's metadata deletion.
					}
				}
			}
		}

		// Now delete the main reward entries.
		$rewards_deleted = $this->db_handler->delete(
			$this->db_table->get_prefixed_table_name(),
			[ 'advocate_id' => $member_id ]
		);

		if ( false === $rewards_deleted ) {
			$all_successful = false;
		}

		return $all_successful;
	}

	/**
	 * Get reward expiration period for a recipient type.
	 *
	 * Lite stub: Returns default expiration period for Lite users.
	 * Pro override: Returns customized value from settings if user has access.
	 *
	 * @since 1.1.0
	 *
	 * @param string $recipient_type The recipient type ('advocate' or 'friend').
	 *
	 * @return string The expiration period (e.g., '+3 months' or 'never').
	 */
	public function get_reward_expiration( string $recipient_type ): string {
		return self::DEFAULT_REFERRAL_REWARD_EXPIRATION;
	}

	/**
	 * Activate a pending reward by changing its status to issued and sending notifications.
	 *
	 * This method is called when a guest member becomes official and their pending rewards
	 * need to be activated (made visible in widget and emails sent).
	 *
	 * @param Reward $reward The reward to activate.
	 *
	 * @return bool True if activation was successful, false otherwise.
	 * @throws Exception If an error occurs during reward activation.
	 * @since 1.0.6
	 */
	public function activate_pending_reward( Reward $reward ): bool {
		if ( 'pending' !== $reward->status ) {
			return false;
		}

		// Change status to issued.
		$reward->status = 'issued';
		$reward->update();

		// Get the reward type instance to handle type-specific activation.
		$reward_type_instance = $this->reward_types_factory->get_reward_type( $reward->reward_type );
		if ( ! $reward_type_instance ) {
			return false;
		}

		$reward_type_instance->set_reward( $reward );

		// For PointsRewardType: add points if they haven't been added yet.
		if ( 'points' === $reward->reward_type ) {
			$this->add_points_for_pending_reward( $reward );
		}

		// Get referral for email data and completion check.
		$referral = ! empty( $reward->referral_id )
			? \Awesomemotive\Rewardswp\Helpers\get_referral( $reward->referral_id )
			: null;

		// Send reward notification email.
		$advocate = \Awesomemotive\Rewardswp\Helpers\get_member_by( 'id', $reward->advocate_id );
		if ( ! $advocate ) {
			// Complete referral even if advocate not found (edge case).
			if ( $referral && 'pending' === $referral->status ) {
				$source = $referral->source ?? 'unknown';
				$referral->set_as_completed( $source, $referral->order_id ?? 0 );
			}
			return true;
		}

		$this->send_reward_notification_email( $reward, $reward_type_instance, $advocate, $referral );

		// Auto-redeem points rewards since points are credited immediately.
		if ( 'points' === $reward->reward_type && method_exists( $reward_type_instance, 'redeem_reward' ) ) {
			$reward_type_instance->redeem_reward( $reward );
		}

		// Complete the associated referral if it's still pending.
		if ( $referral && 'pending' === $referral->status ) {
			$source = $referral->source ?? 'unknown';
			$referral->set_as_completed( $source, $referral->order_id ?? 0 );
		}

		return true;
	}

	/**
	 * Add points for a pending reward if they haven't been added yet.
	 *
	 * @since 1.0.6
	 *
	 * @param Reward $reward The reward to add points for.
	 * @return void
	 */
	private function add_points_for_pending_reward( Reward $reward ): void {
		// Check if points already exist for this referral.
		$points_entries = \Awesomemotive\Rewardswp\Helpers\get_points_entries(
			[
				'member_id'   => $reward->advocate_id,
				'source_type' => 'referral',
				'source_id'   => $reward->referral_id,
			],
			1
		);

		if ( ! empty( $points_entries ) ) {
			return;
		}

		// Points haven't been added yet, add them now.
		$points_entry = $this->points_service->create_points_entry(
			[
				'member_id'   => $reward->advocate_id,
				'points'      => $reward->amount,
				'type'        => 'earned',
				'source_type' => 'referral',
				'source_id'   => $reward->referral_id,
				'status'      => 'approved',
				'note_public' => __( 'Points earned from referral', 'rewardswp' ),
			],
			false, // Don't send generic points notification, we'll send reward email instead.
			true   // Update member balance.
		);

		if ( is_wp_error( $points_entry ) ) {
			\Awesomemotive\Rewardswp\Helpers\rewardswp_log_error(
				'Failed to create points entry when activating pending reward: {message}',
				[
					'message'   => $points_entry->get_error_message(),
					'reward_id' => $reward->id,
				]
			);
		}
	}

	/**
	 * Send reward notification email to advocate.
	 *
	 * @since 1.X.X
	 *
	 * @param Reward        $reward The reward.
	 * @param object        $reward_type_instance The reward type instance.
	 * @param object        $advocate The advocate member.
	 * @param Referral|null $referral The referral object.
	 * @return void
	 */
	private function send_reward_notification_email( Reward $reward, object $reward_type_instance, object $advocate, ?Referral $referral ): void {
		$mailer = $this->mailer_factory->get_mailer( \Awesomemotive\Rewardswp\Notifications\Emails\AdvocateRewardEmail::class );

		$days_to_expire = \Awesomemotive\Rewardswp\Helpers\get_date_diff_in_days( $reward->expires_at );
		$friend_name    = '';

		if ( is_null( $referral ) ) {
			$friend_name = esc_html__( 'A friend', 'rewardswp' );
		} else {
			if ( empty( $friend_name ) && ! empty( $referral->referred_user_id ) ) {
				$friend_name = get_user_first_name( $referral->referred_user_id ) ?: get_user_full_name( $referral->referred_user_id );
			}

			if ( empty( $friend_name ) ) {
				$friend_name = esc_html__( 'A friend', 'rewardswp' );
			}
		}

		if ( 'points' === $reward->reward_type ) {
			$mailer
				->set_recipient( $advocate->email )
				->set_data(
					[
						'reward_coupon'          => '', // No coupon for points.
						'reward_amount'          => \Awesomemotive\Rewardswp\Helpers\format_points_with_label( $reward->amount ),
						'friend_name'            => $friend_name,
						'reward_expiration_time' => sprintf(
							/* translators: %s is the number of days to expire */
							_n( '%s day', '%s days', $days_to_expire, 'rewardswp' ),
							$days_to_expire
						),
					]
				)
				->send();
			return;
		}

		// Coupon reward email (for discount rewards).
		$coupon_code = $reward->get_meta( 'coupon_code' ) ?? '';
		$mailer
			->set_recipient( $advocate->email )
			->set_data(
				[
					'reward_coupon'          => $coupon_code,
					'reward_amount'          => $reward_type_instance->get_reward_formatted_amount( $reward->amount, 'value_only' ),
					'friend_name'            => $friend_name,
					'reward_expiration_time' => sprintf(
						/* translators: %s is the number of days to expire */
						_n( '%s day', '%s days', $days_to_expire, 'rewardswp' ),
						$days_to_expire
					),
				]
			)
			->send();
	}

	/**
	 * Activate all pending rewards for a member after they become official.
	 *
	 * This method is called when a guest member becomes official (registers/logs in).
	 * It finds all pending rewards for the member and activates them (changes status to issued,
	 * sends emails, adds points if applicable, and completes associated referrals).
	 *
	 * @since 1.X.X
	 *
	 * @param int $member_id The member ID.
	 *
	 * @return void
	 */
	public function activate_pending_rewards_for_member( int $member_id ): void {
		if ( $member_id <= 0 ) {
			return;
		}

		// Get all pending rewards for this member.
		$pending_rewards = $this->get_rewards(
			[
				'advocate_id'    => $member_id,
				'status'         => 'pending',
				'recipient_type' => 'advocate',
			],
			99999, // Get all pending rewards.
			1
		);

		if ( empty( $pending_rewards ) ) {
			return;
		}

		// Activate each pending reward.
		foreach ( $pending_rewards as $reward ) {
			try {
				$this->activate_pending_reward( $reward );
			} catch ( Exception $e ) {
				// Log error but continue processing other rewards.
				\Awesomemotive\Rewardswp\Helpers\rewardswp_log_error(
					'Failed to activate pending reward {reward_id} for member {member_id}: {message}',
					[
						'reward_id' => $reward->id ?? 0,
						'member_id' => $member_id,
						'message'   => $e->getMessage(),
					]
				);
			}
		}
	}
}
