<?php
/**
 * SpendActionsOption.php.
 *
 * @package RewardsWP
 */

namespace Awesomemotive\Rewardswp\Settings\Options\Points;

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

use Awesomemotive\Rewardswp\Abstracts\AbstractOption;
use Awesomemotive\Rewardswp\Interfaces\OptionInterface;
use function Awesomemotive\Rewardswp\Helpers\can_access_rewardswp_feature;
use function Awesomemotive\Rewardswp\Helpers\get_lite_redemption_limit;
use function Awesomemotive\Rewardswp\Helpers\get_option_value;
use function Awesomemotive\Rewardswp\Helpers\get_points_currency_singular;
use function Awesomemotive\Rewardswp\Helpers\get_points_currency_plural;
use function Awesomemotive\Rewardswp\Helpers\get_rewardswp_plan_tier;
use function Awesomemotive\Rewardswp\Helpers\get_site_currency;
use function Awesomemotive\Rewardswp\Helpers\format_currency;

/**
 * Class SpendActionsOption
 *
 * @since 1.1.0
 */
/**
 * Class SpendActionsOption.
 */
class SpendActionsOption extends AbstractOption implements OptionInterface {
	/**
	 * Default coupon duration.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	const DEFAULT_COUPON_DURATION = 'never'; // Never expires for Lite.

	/**
	 * Get the setting key used in the option array.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'spend_actions';
	}

	/**
	 * Get the setting group.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public function get_group(): string {
		return 'points';
	}

	/**
	 * Get the label for the setting.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public function get_label(): string {
		return esc_html__( 'Spend Points Actions', 'rewardswp' );
	}

	/**
	 * Get the type of the setting (text, textarea, select, etc).
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public function get_type(): string {
		return 'array';
	}

	/**
	 * Get the current value for the option.
	 *
	 * Overrides parent to sanitize Pro-only fields when user lacks feature access.
	 * The DB value is preserved for future upgrades, but the returned value falls back to defaults.
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	public function get_value(): array {
		$value = parent::get_value();

		// If user has access to custom reward expiration, return value as-is.
		if ( can_access_rewardswp_feature( 'reward_expiration' ) ) {
			return $value;
		}

		// Support both stored shapes:
		// - list of actions: [ [ 'id' => ..., 'coupon_duration' => ... ], ... ]
		// - associative map: [ 'fixed_discount' => [ ... ], ... ]
		foreach ( $value as $key => $action ) {
			if ( is_array( $action ) ) {
				$value[ $key ]['coupon_duration'] = self::DEFAULT_COUPON_DURATION;
			}
		}

		return $value;
	}

	/**
	 * Get the action type definitions.
	 *
	 * @since 1.1.0
	 *
	 * @return array[]
	 */
	public function get_action_definitions(): array {
		return [
			'fixed_discount'      => [
				'title'           => __( 'Fixed amount discount', 'rewardswp' ),
				'description'     => __( 'Let customers exchange their points for a coupon with a fixed dollar discount', 'rewardswp' ),
				'custom_title'    => '',
				'points_required' => 500,  // $5 off at 100 pts = $1.
				'coupon_amount'   => 5,
				'minimum_spent'   => 0,
				'coupon_prefix'   => '',
				'coupon_duration' => 'never',  // Never expires.
				'icon'            => 'reward-amount-coupon',
				'feature_slug'    => 'redeem_fixed_discount',
			],
			'percentage_discount' => [
				'title'           => __( 'Percentage discount', 'rewardswp' ),
				'description'     => __( 'Let customers exchange their points for a coupon with a percentage discount', 'rewardswp' ),
				'custom_title'    => '',
				'points_required' => 1000,  // 10% off at 100 pts = $1.
				'coupon_amount'   => 10,
				'minimum_spent'   => 0,
				'coupon_prefix'   => '',
				'coupon_duration' => 'never',  // Never expires.
				'icon'            => 'reward-percentage-coupon',
				'feature_slug'    => 'redeem_percentage_discount',
			],
			'free_shipping'       => [
				'title'           => __( 'Free shipping', 'rewardswp' ),
				'description'     => __( 'Let customers exchange their points for a free shipping coupon', 'rewardswp' ),
				'custom_title'    => '',
				'points_required' => 1000,
				'minimum_spent'   => 0,
				'coupon_prefix'   => '',
				'coupon_duration' => 'never',  // Never expires.
				'icon'            => 'reward-free-shipping',
				'available_for'   => [ 'woocommerce' ],
				'feature_slug'    => 'redeem_free_shipping',
			],
			'free_product'        => [
				'title'               => __( 'Free product', 'rewardswp' ),
				'description'         => __( 'Let customers exchange their points for a free product', 'rewardswp' ),
				'custom_title'        => '',
				'points_required'     => 2000,
				'minimum_spent'       => 0,
				'coupon_prefix'       => '',
				'coupon_duration'     => 'never',  // Never expires.
				'products_included'   => [],
				'product_value'       => '',       // Maximum discount value (protection against price increases).
				'reward_description'  => '',       // Optional description shown to members.
				'redemption_limit'    => '1',      // Default: 1 redemption per member.
				'minimum_order_value' => '',       // Optional minimum cart subtotal.
				'icon'                => 'reward-free-product',
				'available_for'       => [ 'woocommerce', 'easy_digital_downloads' ],
				'feature_slug'        => 'redeem_free_product',
			],
		];
	}

	/**
	 * Get the default value for the setting.
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	public function get_default_value(): array {
		$definitions = $this->get_action_definitions();
		$defaults    = [];

		foreach ( $definitions as $key => $definition ) {
			$defaults[ $key ] = array_merge(
				$definition,
				[ 'enabled' => false ] // Add the 'enabled' key here.
			);
		}

		// Exclude free shipping and free product from the default *value* returned.
		// as per the original logic.
		unset( $defaults['free_shipping'], $defaults['free_product'] );

		return $defaults;
	}

	/**
	 * Sanitize the setting value.
	 *
	 * @since 1.1.0
	 *
	 * @param mixed $value The value to sanitize.
	 *
	 * @return array
	 */
	public function sanitize( $value ): array {
		if ( ! is_array( $value ) ) {
			return [];
		}

		$sanitized   = [];
		$definitions = $this->get_action_definitions();

		foreach ( $value as $action ) {
			if ( ! isset( $action['type'] ) || ! isset( $action['id'] ) ) {
				continue;
			}

			$sanitized[] = $this->sanitize_action( $action, $definitions );
		}

		return $sanitized;
	}

	/**
	 * Sanitize an individual action.
	 *
	 * @since 1.1.0
	 *
	 * @param array $action      The action data.
	 * @param array $definitions The action type definitions.
	 *
	 * @return array
	 */
	private function sanitize_action( array $action, array $definitions ): array {
		$type       = $action['type'];
		$definition = $definitions[ $type ] ?? [];
		$sanitized  = [];

		// Common fields.
		$sanitized['id']            = sanitize_text_field( $action['id'] );
		$sanitized['type']          = sanitize_text_field( $type );
		$sanitized['enabled']       = isset( $action['enabled'] ) && (bool) $action['enabled'];
		$sanitized['auto_disabled'] = isset( $action['auto_disabled'] ) && (bool) $action['auto_disabled'];
		$sanitized['custom_title']  = isset( $action['custom_title'] )
			? sanitize_text_field( $action['custom_title'] )
			: $definition['custom_title'];
		$sanitized['icon']         = isset( $definition['icon'] )
			? sanitize_text_field( $definition['icon'] )
			: 'gift-free';

		// Common coupon fields.
		$sanitized['minimum_spent'] = isset( $action['minimum_spent'] )
			? absint( $action['minimum_spent'] )
			: $definition['minimum_spent'];

		$sanitized['coupon_prefix'] = isset( $action['coupon_prefix'] )
			? sanitize_text_field( $action['coupon_prefix'] )
			: $definition['coupon_prefix'];

		// Handle both legacy numeric format and new string format.
		if ( isset( $action['coupon_duration'] ) ) {
			$duration = $action['coupon_duration'];

			// Convert legacy numeric format to string format.
			if ( is_numeric( $duration ) ) {
				$duration                     = absint( $duration );
				$sanitized['coupon_duration'] = ( 0 === $duration ) ? 'never' : '+' . $duration . ' days';
			} else {
				// Validate string format ('+30 days', '+3 months', etc. or 'never').
				$valid_durations              = [ '+30 days', '+3 months', '+6 months', '+1 year', '+2 years', 'never' ];
				$sanitized['coupon_duration'] = in_array( $duration, $valid_durations, true )
					? $duration
					: 'never';
			}
		} else {
			$sanitized['coupon_duration'] = $definition['coupon_duration'] ?? 'never';
		}

		// Type-specific fields.
		switch ( $type ) {
			case 'fixed_discount':
			case 'percentage_discount':
				$sanitized['points_required'] = isset( $action['points_required'] )
				? absint( $action['points_required'] )
				: $definition['points_required'];
				$sanitized['coupon_amount']   = isset( $action['coupon_amount'] )
				? absint( $action['coupon_amount'] )
				: $definition['coupon_amount'];
				break;

			case 'free_shipping':
				$sanitized['points_required'] = isset( $action['points_required'] )
				? absint( $action['points_required'] )
				: $definition['points_required'];
				break;

			case 'free_product':
				$sanitized['points_required'] = isset( $action['points_required'] )
				? absint( $action['points_required'] )
				: $definition['points_required'];

				// Handle products_included as an array or JSON string.
				$sanitized['products_included'] = [];
				if ( isset( $action['products_included'] ) ) {
					$products_data = $action['products_included'];

					// If it's a JSON string, decode it.
					if ( is_string( $products_data ) ) {
						$products_data = json_decode( $products_data, true );
					}

					if ( is_array( $products_data ) ) {
						foreach ( $products_data as $product ) {
							if ( is_array( $product ) && isset( $product['id'] ) ) {
								// Support both int IDs (WooCommerce) and string IDs (EDD "download_id:price_id").
								$product_id = $product['id'];
								if ( is_numeric( $product_id ) ) {
									$product_id = absint( $product_id );
								} else {
									$product_id = sanitize_text_field( $product_id );
								}

								$product_entry = [
									'id'     => $product_id,
									'name'   => isset( $product['name'] ) ? sanitize_text_field( $product['name'] ) : '',
									'source' => $this->integration->get_source(),
								];

								// Preserve in_stock if present (updated by stock sync hooks).
								if ( isset( $product['in_stock'] ) ) {
									$product_entry['in_stock'] = (bool) $product['in_stock'];
								}

								$sanitized['products_included'][] = $product_entry;
							}
						}
					}
				}

				// Product value: maximum discount value (protection against price increases).
				$sanitized['product_value'] = isset( $action['product_value'] ) && '' !== $action['product_value']
					? floatval( $action['product_value'] )
					: '';

				// Optional custom description shown to members in the rewards widget.
				$sanitized['reward_description'] = isset( $action['reward_description'] )
					? sanitize_textarea_field( $action['reward_description'] )
					: ( $definition['reward_description'] ?? '' );

				// Redemption limit: how many times each member can redeem this reward.
				$valid_limits                  = [ '1', 'unlimited' ];
				$sanitized['redemption_limit'] = isset( $action['redemption_limit'] ) && in_array( $action['redemption_limit'], $valid_limits, true )
					? $action['redemption_limit']
					: ( $definition['redemption_limit'] ?? '1' );

				// Minimum order value (optional).
				$sanitized['minimum_order_value'] = isset( $action['minimum_order_value'] ) && '' !== $action['minimum_order_value']
					? floatval( $action['minimum_order_value'] )
					: '';
				break;
		}

		return $sanitized;
	}

	/**
	 * Get the formatted description for a spend action.
	 *
	 * @since 1.1.0
	 * @param array $action The action data.
	 * @return string
	 */
	public function get_formatted_description( array $action, ?string $currency_singular = null, ?string $currency_plural = null ): string {
		// Use custom title if available.
		if ( ! empty( $action['custom_title'] ) && trim( $action['custom_title'] ) !== '' ) {
			return sanitize_text_field( $action['custom_title'] );
		}

		// Generate default description with correct values.
		$currency_singular = $currency_singular ?? get_points_currency_singular();
		$currency_plural   = $currency_plural ?? get_points_currency_plural();
		$description       = '';

		if ( ! isset( $action['type'] ) ) {
			return '';
		}

		switch ( $action['type'] ) {
			case 'fixed_discount':
				if ( ! empty( $action['points_required'] ) && ! empty( $action['coupon_amount'] ) ) {
					$points      = absint( $action['points_required'] );
					$amount      = absint( $action['coupon_amount'] );
					$description = sprintf(
						/* translators: %1$d: points, %2$s: points label, %3$s: discount amount with currency symbol. */
						esc_html__( '%1$d %2$s for %3$s off', 'rewardswp' ),
						$points,
						1 === $points ? $currency_singular : $currency_plural,
						format_currency( $amount )
					);
				}
				break;
			case 'percentage_discount':
				if ( ! empty( $action['points_required'] ) && ! empty( $action['coupon_amount'] ) ) {
					$points      = absint( $action['points_required'] );
					$percent     = absint( $action['coupon_amount'] );
					$description = sprintf(
					/* translators: %1$d points for percentage discount */
						esc_html__( '%1$d %2$s for %3$d%% off', 'rewardswp' ),
						$points,
						1 === $points ? $currency_singular : $currency_plural,
						$percent
					);
				}
				break;
			case 'free_shipping':
				if ( ! empty( $action['points_required'] ) ) {
					$points      = absint( $action['points_required'] );
					$description = sprintf(
					/* translators: %1$d points */
						esc_html__( '%1$d %2$s for free shipping', 'rewardswp' ),
						$points,
						1 === $points ? $currency_singular : $currency_plural
					);
				}
				break;
			case 'free_product':
				if ( ! empty( $action['points_required'] ) ) {
					$points      = absint( $action['points_required'] );
					$description = sprintf(
					/* translators: %1$d points */
						esc_html__( '%1$d %2$s for a free product', 'rewardswp' ),
						$points,
						1 === $points ? $currency_singular : $currency_plural
					);
				}
				break;
		}

		return $description;
	}

	/**
	 * Get reward label (e.g., "A$5 off coupon").
	 *
	 * @since 1.1.0
	 * @param array $action The action data.
	 * @return string
	 */
	public function get_reward_label( array $action ): string {
		switch ( $action['type'] ?? '' ) {
			case 'fixed_discount':
				return sprintf(
					/* translators: %s: discount amount with currency symbol. */
					__( '%s off coupon', 'rewardswp' ),
					format_currency( absint( $action['coupon_amount'] ?? 0 ) )
				);
			case 'percentage_discount':
				return sprintf(
					/* translators: %d: discount percentage */
					__( '%d%% off coupon', 'rewardswp' ),
					absint( $action['coupon_amount'] ?? 0 )
				);
			case 'free_shipping':
				return __( 'Free shipping', 'rewardswp' );
			case 'free_product':
				// Priority: 1. custom_title (future Plus/Pro), 2. reward_description, 3. product name, 4. fallback.
				if ( ! empty( $action['custom_title'] ) ) {
					return sanitize_text_field( $action['custom_title'] );
				}

				// Check if product_value is set (limits the discount amount).
				$product_value     = floatval( $action['product_value'] ?? 0 );
				$has_value_limit   = $product_value > 0;
				$products_included = $action['products_included'] ?? [];

				// Use reward_description if set (replaces product name in the label).
				if ( ! empty( $action['reward_description'] ) ) {
					$product_name = sanitize_text_field( $action['reward_description'] );
					if ( $has_value_limit ) {
						return sprintf(
							/* translators: %1$s: formatted currency amount (e.g., "$500"), %2$s: custom product description */
							__( '%1$s off %2$s', 'rewardswp' ),
							format_currency( $product_value ),
							$product_name
						);
					}
					return sprintf(
						/* translators: %s: custom product description */
						__( 'Free %s', 'rewardswp' ),
						$product_name
					);
				}

				// Try to get product name from products_included.
				if ( ! empty( $products_included[0]['name'] ) ) {
					$product_name = sanitize_text_field( $products_included[0]['name'] );
					if ( $has_value_limit ) {
						return sprintf(
							/* translators: %1$s: formatted currency amount (e.g., "$500"), %2$s: product name */
							__( '%1$s off %2$s', 'rewardswp' ),
							format_currency( $product_value ),
							$product_name
						);
					}
					return sprintf(
						/* translators: %s: product name */
						__( 'Free %s', 'rewardswp' ),
						$product_name
					);
				}

				// Fallback when no product name is available.
				if ( $has_value_limit ) {
					return sprintf(
						/* translators: %s: formatted currency amount (e.g., "$500") */
						__( '%s off product', 'rewardswp' ),
						format_currency( $product_value )
					);
				}

				return __( 'Free product', 'rewardswp' );
			default:
				return '';
		}
	}

	/**
	 * Get points cost label (e.g., "500 points" or "500 points for MacBook Pro").
	 *
	 * @since 1.1.0
	 * @param array $action The action data.
	 * @return string
	 */
	public function get_points_cost_label( array $action, ?string $currency_singular = null, ?string $currency_plural = null ): string {
		$points            = absint( $action['points_required'] ?? 0 );
		$currency_singular = $currency_singular ?? get_points_currency_singular();
		$currency_plural   = $currency_plural ?? get_points_currency_plural();

		$points_text = sprintf(
			/* translators: %1$d: points amount, %2$s: currency name */
			__( '%1$d %2$s', 'rewardswp' ),
			$points,
			1 === $points ? $currency_singular : $currency_plural
		);

		// For free_product, just return the points text.
		// The product name is already shown in the reward title (e.g., "Free Macbook"),
		// so we don't need to repeat it here. This also avoids grammatical issues
		// with articles (a/an) in English.
		// No special handling needed - just fall through to return $points_text.

		return $points_text;
	}

	/**
	 * Get the enabled actions.
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	public function get_enabled_actions(): array {
		$actions         = $this->get_value();
		$definitions     = $this->get_action_definitions();
		$enabled_actions = [];
		$plan_tier       = get_rewardswp_plan_tier();

		foreach ( $actions as $action ) {
			if ( isset( $action['enabled'] ) && $action['enabled'] ) {
				// Merge with definition to ensure icon and other fields are present.
				$type = $action['type'] ?? '';

				/*
				 * GRANDFATHERED FEATURE ACCESS (Added Dec 2025):
				 * We intentionally allow configured actions to work even if the site
				 * downgraded their plan. The feature gating only blocks ADDING new
				 * actions in the Admin UI (via upsell overlays), not using already-configured ones.
				 *
				 * This means: if a Plus user configured Free Shipping, then downgraded to Basic,
				 * Free Shipping will continue to work until they remove it from settings.
				 *
				 * @todo Review this decision in Dec 2026 - evaluate if this grandfathering
				 *       approach is working well or if we should enforce stricter gating.
				 */

				if ( isset( $definitions[ $type ] ) ) {
					// Ensure icon is set from definition if not already present.
					if ( ! isset( $action['icon'] ) && isset( $definitions[ $type ]['icon'] ) ) {
						$action['icon'] = $definitions[ $type ]['icon'];
					}
				}
				$enabled_actions[] = $action;

				// Limit enabled rewards for Lite users.
				if ( 'lite' === $plan_tier && count( $enabled_actions ) >= get_lite_redemption_limit() ) {
					break;
				}
			}
		}

		return $enabled_actions;
	}

	/**
	 * Get all valid spend action types.
	 *
	 * @since 1.1.0
	 *
	 * @return array Array of valid spend action types.
	 */
	public function get_valid_action_types(): array {
		return array_keys( $this->get_action_definitions() );
	}

	/**
	 * Check if a spend action type is valid.
	 *
	 * @since 1.1.0
	 *
	 * @param string $action_type The action type to validate.
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public function is_valid_action_type( string $action_type ): bool {
		return in_array( $action_type, $this->get_valid_action_types(), true );
	}

	/**
	 * Check if user can access a specific spend action type.
	 *
	 * Uses the feature_slug from action definitions to check feature access.
	 *
	 * @since 1.1.0
	 *
	 * @param string $action_type The action type to check (e.g., 'free_shipping').
	 *
	 * @return bool Whether the user has access to this action type.
	 */
	public function can_access_action_type( string $action_type ): bool {
		$definitions = $this->get_action_definitions();

		if ( ! isset( $definitions[ $action_type ] ) ) {
			return false;
		}

		$feature_slug = $definitions[ $action_type ]['feature_slug'] ?? null;

		// If no feature_slug defined, assume accessible.
		if ( empty( $feature_slug ) ) {
			return true;
		}

		return can_access_rewardswp_feature( $feature_slug );
	}

	/**
	 * Find a specific action configuration by ID and type.
	 *
	 * @since 1.1.0
	 *
	 * @param string $action_id   The action ID.
	 * @param string $action_type The action type.
	 *
	 * @return array|null The action configuration or null if not found.
	 */
	public function find_action_config( string $action_id, string $action_type ): ?array {
		$actions = $this->get_value();

		foreach ( $actions as $action ) {
			if ( isset( $action['id'] ) && $action['id'] === $action_id &&
				isset( $action['type'] ) && $action['type'] === $action_type ) {
				return $action;
			}
		}

		return null;
	}

	/**
	 * Calculate required points for a spend action.
	 *
	 * @since 1.1.0
	 *
	 * @param array $action_config The action configuration.
	 *
	 * @return int The required points.
	 */
	public function calculate_required_points( array $action_config ): int {
		return absint( $action_config['points_required'] ?? 0 );
	}

	/**
	 * Get the reward amount for a spend action.
	 *
	 * @since 1.1.0
	 *
	 * @param array $action_config The action configuration.
	 *
	 * @return float The reward amount.
	 */
	public function get_reward_amount( array $action_config ): float {
		if ( empty( $action_config['type'] ) ) {
			return 0.0;
		}

		switch ( $action_config['type'] ) {
			case 'fixed_discount':
			case 'percentage_discount':
				return isset( $action_config['coupon_amount'] ) && $action_config['coupon_amount'] > 0
					? (float) $action_config['coupon_amount']
					: 0.0;
			case 'free_shipping':
			default:
				return 0.0; // No amount for these types.
		}
	}
}
