<?php
/**
 * REST mapper usage-hints value object.
 *
 * Immutable value object that carries optional, pre-resolved usage
 * statistics — embed count plus the list of post ids that embed a
 * given product — that REST controllers may compute upfront and
 * forward to the
 * {@see \CommerceLinkBuilder\Presentation\Rest\Mapper\ProductDetailMapper}
 * so the mapper itself stays a pure VO→array translator that never
 * reaches into infrastructure.
 *
 * The shape is the §6.2 `usage` sub-object of
 * `specifications/rest-api-spec.md`:
 *
 *   - `embedCount` is the §6.2 `usage.embed_count` value: a
 *     non-negative integer count of embed definitions that
 *     reference the product. Validated as `>= 0` at construction
 *     time so a programming error (signed-overflow, negative
 *     subtraction) surfaces here rather than at the REST boundary.
 *
 *   - `postIds` is the §6.2 `usage.post_ids` value: a sequential
 *     `list<int>` of WordPress post ids that embed the product, in
 *     the order the controller resolved them. Each entry is
 *     enforced as `>= 1` because a `0` post id is the WordPress
 *     "no post" sentinel, never a valid foreign key.
 *
 * Phase-6 callers do not yet populate this VO — usage tracking is
 * wired in Phase 9 — but the mapper accepts a nullable hint so the
 * Phase 6 controller can pass {@see self::none()} (or simply null)
 * and the array shape stays §6.2-compliant from day one.
 *
 * @package CommerceLinkBuilder\Presentation\Rest\Mapper
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Presentation\Rest\Mapper;

use InvalidArgumentException;

/**
 * Immutable usage-hints carrier consumed by the §6.2
 * {@see ProductDetailMapper}.
 *
 * See the file-level docblock for the mapping to
 * `rest-api-spec.md` §6.2 and the per-field validation rules.
 */
final class UsageHints {

	/**
	 * §6.2 `usage.embed_count` value. Non-negative.
	 */
	private int $embedCount;

	/**
	 * §6.2 `usage.post_ids` value. Sequential list of post ids,
	 * each `>= 1`.
	 *
	 * @var list<int>
	 */
	private array $postIds;

	/**
	 * Construct a usage-hints VO with explicit values for every
	 * field.
	 *
	 * @param int       $embedCount Embed-definition count, `>= 0`.
	 * @param list<int> $postIds    Sequential list of post ids,
	 *                              each `>= 1`.
	 *
	 * @throws InvalidArgumentException When `$embedCount` is
	 *                                  negative, when `$postIds` is
	 *                                  not a sequential list, or
	 *                                  when any entry is not an
	 *                                  integer `>= 1`.
	 */
	public function __construct( int $embedCount, array $postIds = array() ) {
		if ( $embedCount < 0 ) {
			$message = sprintf(
				'UsageHints embedCount must be non-negative, got %d.',
				$embedCount
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		$expectedIndex = 0;
		foreach ( $postIds as $key => $value ) {
			if ( $key !== $expectedIndex ) {
				$message = 'UsageHints postIds must be a sequential list (0-indexed, contiguous).';
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
				throw new InvalidArgumentException( $message );
			}
			if ( ! is_int( $value ) ) {
				$message = sprintf(
					'UsageHints postIds entry at index %d must be an integer.',
					$expectedIndex
				);
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
				throw new InvalidArgumentException( $message );
			}
			if ( $value < 1 ) {
				$message = sprintf(
					'UsageHints postIds entry at index %d must be a positive integer, got %d.',
					$expectedIndex,
					$value
				);
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
				throw new InvalidArgumentException( $message );
			}
			++$expectedIndex;
		}

		$this->embedCount = $embedCount;
		/** @var list<int> $postIds */
		$this->postIds = $postIds;
	}

	/**
	 * Construct an empty usage-hints VO with `embedCount = 0` and
	 * an empty post-id list. Used by Phase-6 controllers that have
	 * not yet wired usage tracking (Phase 9).
	 */
	public static function none(): self {
		return new self( 0, array() );
	}

	/**
	 * §6.2 `usage.embed_count` value.
	 */
	public function embedCount(): int {
		return $this->embedCount;
	}

	/**
	 * §6.2 `usage.post_ids` value.
	 *
	 * @return list<int>
	 */
	public function postIds(): array {
		return $this->postIds;
	}

	/**
	 * Structural equality: two hints are equal when both the embed
	 * count and the post-id list match exactly (including order).
	 *
	 * @param self $other Other hints to compare with.
	 */
	public function equals( self $other ): bool {
		return $this->embedCount === $other->embedCount
			&& $this->postIds === $other->postIds;
	}
}
