<?php
/**
 * Sync priority value object.
 *
 * Immutable value object that represents the scheduling priority of
 * a sync job, per `specifications/sync-and-cache-strategy.md`
 * §13.2 (and the broader §10–§13 sync queue design). The type
 * unifies the closed set of priority tokens (`low`, `normal`,
 * `high`, `urgent`) and pairs each one with a numeric weight so
 * the sync dispatcher can sort the work queue without re-deriving
 * an ordering from the raw string.
 *
 * Per §13.2, priority is raised for usage-driven signals (frequent
 * use in articles, recent display, expired state, price-bearing
 * snapshots, providers with high update cadence) and lowered for
 * archived / long-unused / broken / admin-paused snapshots. The
 * concrete weight numbers (low=10, normal=50, high=80, urgent=100)
 * are an internal ordering convention and are not surfaced as a
 * spec constant; consumers MUST rely on
 * {@see self::isHigherThan()} / {@see self::isAtLeast()} / value
 * comparison rather than hard-coding the integers.
 *
 * @package Affilun\Domain\Sync
 */

declare(strict_types=1);

namespace Affilun\Domain\Sync;

use InvalidArgumentException;

/**
 * Immutable sync-queue priority token from
 * `sync-and-cache-strategy.md` §13.2. See the file-level docblock
 * for the rationale behind the four-level ordering and the
 * internal-only nature of the weight integers.
 */
final class SyncPriority {

	/**
	 * Lowest priority. Suitable for opportunistic background
	 * resync of long-untouched snapshots and for cleanup-style
	 * jobs that should never preempt user-driven work.
	 */
	public const LOW = 'low';

	/**
	 * Default priority. Suitable for routine scheduled resyncs of
	 * `stale` snapshots that are not under immediate display
	 * pressure.
	 */
	public const NORMAL = 'normal';

	/**
	 * Elevated priority. Suitable for `expired` snapshots, for
	 * snapshots used in articles with active traffic, and for
	 * post-configuration-change refreshes the administrator
	 * expects to see complete promptly.
	 */
	public const HIGH = 'high';

	/**
	 * Top priority. Suitable for admin-initiated single-item
	 * resyncs, for diagnostic refreshes blocking an admin surface,
	 * and for emergency invalidation flows.
	 */
	public const URGENT = 'urgent';

	/**
	 * Internal ordering weight for {@see self::LOW}. Held
	 * separately from the token so dispatcher ORDER BY clauses can
	 * sort numerically without parsing the string.
	 */
	private const WEIGHT_LOW = 10;

	/**
	 * Internal ordering weight for {@see self::NORMAL}.
	 */
	private const WEIGHT_NORMAL = 50;

	/**
	 * Internal ordering weight for {@see self::HIGH}.
	 */
	private const WEIGHT_HIGH = 80;

	/**
	 * Internal ordering weight for {@see self::URGENT}.
	 */
	private const WEIGHT_URGENT = 100;

	/**
	 * Closed set of accepted priority tokens, in ascending weight
	 * order. Maintained as a class-level constant so the validator
	 * and the test suite can iterate the same source of truth.
	 *
	 * @var list<string>
	 */
	private const VALID_PRIORITIES = array(
		self::LOW,
		self::NORMAL,
		self::HIGH,
		self::URGENT,
	);

	/**
	 * Mapping from priority token to internal ordering weight.
	 *
	 * @var array<string, int>
	 */
	private const WEIGHTS = array(
		self::LOW    => self::WEIGHT_LOW,
		self::NORMAL => self::WEIGHT_NORMAL,
		self::HIGH   => self::WEIGHT_HIGH,
		self::URGENT => self::WEIGHT_URGENT,
	);

	/**
	 * Canonical priority token.
	 */
	private string $value;

	/**
	 * Cached numeric ordering weight for {@see self::$value}.
	 */
	private int $weight;

	/**
	 * Private constructor — instances are obtained via the static
	 * named constructors {@see self::low()} / {@see self::normal()}
	 * / {@see self::high()} / {@see self::urgent()} or via
	 * {@see self::fromString()} for deserialization paths.
	 *
	 * @param string $value  Pre-validated priority token.
	 * @param int    $weight Pre-resolved ordering weight.
	 */
	private function __construct( string $value, int $weight ) {
		$this->value  = $value;
		$this->weight = $weight;
	}

	/**
	 * Construct a `low` priority instance.
	 */
	public static function low(): self {
		return new self( self::LOW, self::WEIGHT_LOW );
	}

	/**
	 * Construct a `normal` priority instance.
	 */
	public static function normal(): self {
		return new self( self::NORMAL, self::WEIGHT_NORMAL );
	}

	/**
	 * Construct a `high` priority instance.
	 */
	public static function high(): self {
		return new self( self::HIGH, self::WEIGHT_HIGH );
	}

	/**
	 * Construct an `urgent` priority instance.
	 */
	public static function urgent(): self {
		return new self( self::URGENT, self::WEIGHT_URGENT );
	}

	/**
	 * Construct a priority instance from a raw string token,
	 * validating against the closed §13.2 set.
	 *
	 * The token is trimmed first so a carelessly-decoded payload
	 * (trailing newline, padded space) is normalized to the same
	 * canonical form a freshly-constructed priority would carry.
	 *
	 * @param string $value Raw priority token as supplied by the caller.
	 *
	 * @throws InvalidArgumentException When `$value` is not a
	 *                                  member of the §13.2 closed
	 *                                  set.
	 */
	public static function fromString( string $value ): self {
		$trimmed = trim( $value );

		if ( ! array_key_exists( $trimmed, self::WEIGHTS ) ) {
			$message = sprintf(
				'SyncPriority must be one of [%s]; got "%s".',
				implode( ', ', self::VALID_PRIORITIES ),
				$value
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		return new self( $trimmed, self::WEIGHTS[ $trimmed ] );
	}

	/**
	 * Closed set of accepted priority tokens, in ascending weight
	 * order.
	 *
	 * @return list<string>
	 */
	public static function validPriorities(): array {
		return self::VALID_PRIORITIES;
	}

	/**
	 * Canonical priority token.
	 */
	public function value(): string {
		return $this->value;
	}

	/**
	 * Numeric ordering weight associated with this priority.
	 *
	 * Higher numbers mean "should run sooner". The concrete values
	 * (low=10, normal=50, high=80, urgent=100) are an internal
	 * ordering convention; consumers SHOULD prefer
	 * {@see self::isHigherThan()} / {@see self::isAtLeast()} over
	 * comparing the raw integers.
	 */
	public function weight(): int {
		return $this->weight;
	}

	/**
	 * Value-object equality: two priorities are equal iff their
	 * tokens match exactly.
	 *
	 * @param SyncPriority $other Other priority to compare against.
	 */
	public function equals( SyncPriority $other ): bool {
		return $this->value === $other->value;
	}

	/**
	 * Whether this priority outranks `$other` (strictly higher
	 * weight).
	 *
	 * @param SyncPriority $other Other priority to compare against.
	 */
	public function isHigherThan( SyncPriority $other ): bool {
		return $this->weight > $other->weight;
	}

	/**
	 * Whether this priority is at least as high as `$other` (equal
	 * or higher weight).
	 *
	 * @param SyncPriority $other Other priority to compare against.
	 */
	public function isAtLeast( SyncPriority $other ): bool {
		return $this->weight >= $other->weight;
	}
}
