<?php
/**
 * UI Variant Service.
 *
 * Resolves the server-side UI variant preference for the redesigned bulk
 * generator rollout without consulting client-only sources such as URL
 * parameters or localStorage.
 *
 * @package AIPT\Core\Services
 * @since   2.6.1
 */

declare(strict_types=1);

namespace AIPT\Core\Services;

use InvalidArgumentException;

defined( 'ABSPATH' ) || exit;

/**
 * Handles UI variant preferences and rollout metrics.
 *
 * @since 2.6.1
 */
final class UiVariantService {

	public const META_KEY                     = 'aipt_ui_variant';
	public const SITE_OPTION_KEY              = 'aipt_ui_variant';
	public const METRICS_OPTION               = 'aipt_ui_variant_metrics';
	public const META_MODAL_SNOOZED_UNTIL_KEY = 'aipt_ui_modal_snoozed_until';

	/**
	 * Duration of a single "I'll try later" snooze, in seconds (3 days).
	 *
	 * @since 2.6.2
	 */
	public const MODAL_SNOOZE_DURATION = 3 * 24 * 60 * 60;

	/**
	 * Valid UI variant values.
	 *
	 * @since 2.6.1
	 * @var string[]
	 */
	private const VALID_VARIANTS = array( 'legacy', 'next', 'auto' );

	/**
	 * WordPress function callbacks.
	 *
	 * @since 2.6.1
	 * @var array<string, callable>
	 */
	private $callbacks;

	/**
	 * Constructor.
	 *
	 * @since 2.6.1
	 *
	 * @param array<string, callable> $callbacks Optional WordPress function callbacks.
	 */
	public function __construct( array $callbacks = array() ) {
		$this->callbacks = array_replace(
			array(
				'get_option'       => 'get_option',
				'update_option'    => 'update_option',
				'get_user_meta'    => 'get_user_meta',
				'update_user_meta' => 'update_user_meta',
				'delete_user_meta' => 'delete_user_meta',
			),
			$callbacks
		);
	}

	/**
	 * Resolve the effective variant for a user.
	 *
	 * Precedence: user meta > site option > install-aware default. Returns a
	 * concrete variant (legacy or next) — mirrors the client-side
	 * `resolveUiVariant()` resolver so server-rendered + client-rendered
	 * decisions match:
	 *
	 * - User meta `auto`  → `legacy` (conservative; user explicitly stored
	 *   the default sentinel rather than picking a side).
	 * - Site option `auto` → `next` (fresh-install signal — see
	 *   {@see UiVariantService::getSiteVariant()}; matches client
	 *   `resolveConcreteVariant('auto', 'site_option')`).
	 *
	 * @since 2.6.1
	 *
	 * @param int $user_id User ID.
	 * @return string Concrete resolved variant: 'legacy' or 'next'.
	 */
	public function resolveForUser( int $user_id ): string {
		$user_variant = $this->getUserVariant( $user_id );

		if ( null !== $user_variant && $this->isValidVariant( $user_variant ) ) {
			return 'auto' === $user_variant ? 'legacy' : $user_variant;
		}

		$site_variant = $this->getSiteVariant();

		return 'auto' === $site_variant ? 'next' : $site_variant;
	}

	/**
	 * Get the raw user-level variant preference.
	 *
	 * @since 2.6.1
	 *
	 * @param int $user_id User ID.
	 * @return string|null Stored variant, or null when unset.
	 */
	public function getUserVariant( int $user_id ): ?string {
		$variant = $this->callWordPressFunction( 'get_user_meta', array( $user_id, self::META_KEY, true ) );

		if ( ! is_string( $variant ) || '' === $variant ) {
			return null;
		}

		return $variant;
	}

	/**
	 * Set the user-level variant preference.
	 *
	 * @since 2.6.1
	 *
	 * @param int    $user_id User ID.
	 * @param string $variant UI variant.
	 * @return void
	 * @throws \InvalidArgumentException When the variant is invalid.
	 */
	public function setUserVariant( int $user_id, string $variant ): void {
		$this->assertValidVariant( $variant );

		$this->callWordPressFunction( 'update_user_meta', array( $user_id, self::META_KEY, $variant ) );
	}

	/**
	 * Clear the user-level variant preference.
	 *
	 * @since 2.6.1
	 *
	 * @param int $user_id User ID.
	 * @return void
	 */
	public function clearUserVariant( int $user_id ): void {
		$this->callWordPressFunction( 'delete_user_meta', array( $user_id, self::META_KEY ) );
	}

	/**
	 * Whether the "Try new UI" modal is currently snoozed for a user.
	 *
	 * The "I'll try later" CTA stores a future Unix timestamp; this returns
	 * true while the current time is below that timestamp. Explicit opt-in
	 * or switch-back via SwitchBackPill writes the userVariant meta, which
	 * short-circuits the modal predicate independently of this snooze flag.
	 *
	 * @since 2.6.2
	 *
	 * @param int $user_id User ID.
	 * @return bool True when the modal is currently snoozed.
	 */
	public function isModalSnoozed( int $user_id ): bool {
		$value = $this->callWordPressFunction( 'get_user_meta', array( $user_id, self::META_MODAL_SNOOZED_UNTIL_KEY, true ) );

		if ( '' === $value || false === $value || null === $value ) {
			return false;
		}

		$until = (int) $value;

		return $until > time();
	}

	/**
	 * Snooze the "Try new UI" modal for {@see self::MODAL_SNOOZE_DURATION}
	 * seconds (3 days) from now. After the timestamp elapses the modal
	 * re-shows on the next eligible page load.
	 *
	 * @since 2.6.2
	 *
	 * @param int $user_id User ID.
	 * @return int The unix timestamp the snooze expires at.
	 */
	public function snoozeModal( int $user_id ): int {
		$until = time() + self::MODAL_SNOOZE_DURATION;
		$this->callWordPressFunction(
			'update_user_meta',
			array( $user_id, self::META_MODAL_SNOOZED_UNTIL_KEY, (string) $until )
		);

		return $until;
	}

	/**
	 * Get the site-level variant preference.
	 *
	 * Existing installs default to legacy. Fresh installs, identified by the
	 * absence of the aipt_first_run_completed option, default to auto.
	 *
	 * @since 2.6.1
	 *
	 * @return string One of legacy, next, or auto.
	 */
	public function getSiteVariant(): string {
		$variant = $this->callWordPressFunction( 'get_option', array( self::SITE_OPTION_KEY, false ) );

		if ( is_string( $variant ) && $this->isValidVariant( $variant ) ) {
			return $variant;
		}

		$first_run_completed = $this->callWordPressFunction( 'get_option', array( 'aipt_first_run_completed', null ) );

		return null === $first_run_completed ? 'auto' : 'legacy';
	}

	/**
	 * Set the site-level variant preference.
	 *
	 * @since 2.6.1
	 *
	 * @param string $variant UI variant.
	 * @return void
	 * @throws \InvalidArgumentException When the variant is invalid.
	 */
	public function setSiteVariant( string $variant ): void {
		$this->assertValidVariant( $variant );

		$this->callWordPressFunction( 'update_option', array( self::SITE_OPTION_KEY, $variant, false ) );
	}

	/**
	 * Get normalized rollout metrics.
	 *
	 * @since 2.6.1
	 *
	 * @return array{opt_ins:int,switch_backs:int,last_used_at:int|null}
	 */
	public function getMetrics(): array {
		$metrics = $this->callWordPressFunction( 'get_option', array( self::METRICS_OPTION, array() ) );

		if ( ! is_array( $metrics ) ) {
			$metrics = array();
		}

		$last_used_at = null;

		if ( array_key_exists( 'last_used_at', $metrics ) ) {
			$last_used_at = max( 0, (int) $metrics['last_used_at'] );
		}

		return array(
			'opt_ins'      => isset( $metrics['opt_ins'] ) ? max( 0, (int) $metrics['opt_ins'] ) : 0,
			'switch_backs' => isset( $metrics['switch_backs'] ) ? max( 0, (int) $metrics['switch_backs'] ) : 0,
			'last_used_at' => $last_used_at,
		);
	}

	/**
	 * Increment the opt-in metric.
	 *
	 * @since 2.6.1
	 *
	 * @return void
	 */
	public function incrementOptIn(): void {
		$metrics             = $this->getMetrics();
		++$metrics['opt_ins'];

		$this->updateMetrics( $metrics );
	}

	/**
	 * Increment the switch-back metric.
	 *
	 * @since 2.6.1
	 *
	 * @return void
	 */
	public function incrementSwitchBack(): void {
		$metrics                  = $this->getMetrics();
		++$metrics['switch_backs'];

		$this->updateMetrics( $metrics );
	}

	/**
	 * Update the last-used timestamp metric.
	 *
	 * @since 2.6.1
	 *
	 * @return void
	 */
	public function touchLastUsed(): void {
		$metrics                 = $this->getMetrics();
		$metrics['last_used_at'] = time();

		$this->updateMetrics( $metrics );
	}

	/**
	 * Persist normalized metrics.
	 *
	 * @since 2.6.1
	 *
	 * @param array $metrics Metrics payload.
	 * @return void
	 */
	private function updateMetrics( array $metrics ): void {
		$this->callWordPressFunction( 'update_option', array( self::METRICS_OPTION, $metrics, false ) );
	}

	/**
	 * Call a configured WordPress function callback.
	 *
	 * @since 2.6.1
	 *
	 * @param string $name Callback key.
	 * @param array  $args Callback arguments.
	 * @return mixed Callback result.
	 */
	private function callWordPressFunction( string $name, array $args = array() ) {
		return \call_user_func_array( $this->callbacks[ $name ], $args );
	}

	/**
	 * Validate a variant value.
	 *
	 * @since 2.6.1
	 *
	 * @param string $variant UI variant.
	 * @return void
	 * @throws \InvalidArgumentException When the variant is invalid.
	 */
	private function assertValidVariant( string $variant ): void {
		if ( ! $this->isValidVariant( $variant ) ) {
			throw new InvalidArgumentException( 'Invalid UI variant.' );
		}
	}

	/**
	 * Check whether a variant value is supported.
	 *
	 * @since 2.6.1
	 *
	 * @param string $variant UI variant.
	 * @return bool True when supported.
	 */
	private function isValidVariant( string $variant ): bool {
		return in_array( $variant, self::VALID_VARIANTS, true );
	}
}
