<?php
/**
 * Cart Recovery — Coupon Config DTO
 *
 * Immutable configuration for coupon generation. Populated by
 * `Niwa_AI_Cart_Recovery_Settings::get_coupon_config()` from the
 * admin-facing `OPT_CART_RECOVERY_SETTINGS` option (Phase 6 renders
 * the UI around the same keys) — Phase 4 holds the source of truth
 * defaults and sanitization.
 *
 * Keeping this as a pure readonly DTO (no WP dependency) means the
 * coupon service can be unit-tested without a WC bootstrap.
 *
 * @package Niwa_AI
 * @since   0.12.0
 */

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

final class Niwa_AI_Cart_Recovery_Coupon_Config {

	public const TYPE_PERCENT       = 'percent';
	public const TYPE_FIXED_CART    = 'fixed_cart';
	public const TYPE_FIXED_PRODUCT = 'fixed_product';

	public const MIN_SPEND_NONE        = 'none';
	public const MIN_SPEND_CART_TOTAL  = 'cart_subtotal';

	/**
	 * @param string $discount_type           One of the TYPE_* constants.
	 * @param float  $amount                  Discount amount (percent 0-100, or fixed amount in store currency).
	 * @param int    $ttl_hours               Coupon validity window from creation.
	 * @param string $prefix                  Code prefix (e.g. "NIWA-BACK-"). Used also for cleanup targeting.
	 * @param bool   $restrict_to_cart_products  If true, coupon only applies to the product ids from the snapshot.
	 * @param string $minimum_spend_mode      One of the MIN_SPEND_* constants.
	 * @param bool   $exclude_sale_items      If true, coupon excludes items already on sale.
	 * @param bool   $individual_use          If true, disallows stacking with other coupons.
	 * @param bool   $free_shipping           If true, coupon also grants free shipping.
	 */
	public function __construct(
		public readonly string $discount_type,
		public readonly float $amount,
		public readonly int $ttl_hours,
		public readonly string $prefix,
		public readonly bool $restrict_to_cart_products,
		public readonly string $minimum_spend_mode,
		public readonly bool $exclude_sale_items,
		public readonly bool $individual_use,
		public readonly bool $free_shipping
	) {}

	/**
	 * Validate + clamp to safe ranges. Callers should pass through this
	 * on construction from admin input, but it's also safe to call
	 * twice (idempotent).
	 */
	public static function from_raw( array $raw ): self {
		$type = isset( $raw['discount_type'] ) ? (string) $raw['discount_type'] : Niwa_AI_Constants::CART_RECOVERY_COUPON_TYPE;
		if ( ! in_array( $type, array( self::TYPE_PERCENT, self::TYPE_FIXED_CART, self::TYPE_FIXED_PRODUCT ), true ) ) {
			$type = self::TYPE_PERCENT;
		}

		$amount = isset( $raw['amount'] ) ? (float) $raw['amount'] : (float) Niwa_AI_Constants::CART_RECOVERY_COUPON_PERCENT;
		if ( self::TYPE_PERCENT === $type ) {
			$amount = max( 0.0, min( 100.0, $amount ) );
		} else {
			$amount = max( 0.0, $amount );
		}

		$ttl = isset( $raw['ttl_hours'] ) ? (int) $raw['ttl_hours'] : Niwa_AI_Constants::CART_RECOVERY_COUPON_TTL_HOURS;
		$ttl = max( 1, min( 24 * 30, $ttl ) ); // Clamp to [1 hour, 30 days].

		$prefix = isset( $raw['prefix'] ) ? self::sanitize_prefix( (string) $raw['prefix'] ) : Niwa_AI_Constants::CART_RECOVERY_COUPON_PREFIX;
		if ( '' === $prefix ) {
			$prefix = Niwa_AI_Constants::CART_RECOVERY_COUPON_PREFIX;
		}

		$min_mode = isset( $raw['minimum_spend_mode'] ) ? (string) $raw['minimum_spend_mode'] : self::MIN_SPEND_NONE;
		if ( ! in_array( $min_mode, array( self::MIN_SPEND_NONE, self::MIN_SPEND_CART_TOTAL ), true ) ) {
			$min_mode = self::MIN_SPEND_NONE;
		}

		return new self(
			$type,
			$amount,
			$ttl,
			$prefix,
			(bool) ( $raw['restrict_to_cart_products'] ?? false ),
			$min_mode,
			(bool) ( $raw['exclude_sale_items'] ?? false ),
			(bool) ( $raw['individual_use'] ?? true ),
			(bool) ( $raw['free_shipping'] ?? false )
		);
	}

	public static function defaults(): self {
		return self::from_raw( array() );
	}

	private static function sanitize_prefix( string $prefix ): string {
		// Keep alphanumerics, dashes, underscores; uppercase for visual consistency.
		$clean = preg_replace( '/[^A-Za-z0-9_\-]/', '', $prefix );
		$clean = is_string( $clean ) ? strtoupper( $clean ) : '';
		return substr( $clean, 0, 32 );
	}
}
