<?php
/**
 * Provider capabilities value object.
 *
 * Immutable value object that enumerates which features a provider
 * implementation supports. The type unifies the cross-provider
 * capability surface `specifications/provider-api-contracts.md` §7.9
 * names so REST controllers, admin UIs, and the rendering layer can
 * branch on capability flags without knowing which concrete provider
 * is wired in — and so a future provider that supports a different
 * subset can be slotted in without touching the consumers.
 *
 * Field semantics match `provider-api-contracts.md` §7.9 exactly,
 * with the parallel feature-key surface from §8 exposed as
 * {@see self::supportsFeature()} for callers that prefer string keys
 * over named accessors:
 *
 *   - `supportsSearch` — provider implements
 *     {@see \Affilun\Domain\Provider\SearchProviderInterface}
 *     (Phase 3 slice 3g) and can satisfy keyword search requests.
 *
 *   - `supportsLookup` — provider implements
 *     {@see \Affilun\Domain\Provider\ItemLookupProviderInterface}
 *     and can satisfy single-item lookup by
 *     {@see \Affilun\Domain\Provider\ProviderProductId}.
 *
 *   - `supportsMultipleImages` — provider responses include a
 *     gallery image list in addition to the primary image. False
 *     means consuming surfaces should not render a gallery
 *     affordance.
 *
 *   - `supportsPrice` — provider responses include
 *     {@see \Affilun\Domain\Pricing\PriceInfo} on
 *     `ProductSnapshot::price()`.
 *
 *   - `supportsAvailability` — provider responses include a
 *     meaningful (non-`unknown`) availability signal. False means
 *     the rendering layer should treat every snapshot as
 *     `STATUS_UNKNOWN` for that provider.
 *
 *   - `supportsRating` — provider responses include
 *     `ProductSnapshot::rating()` and / or
 *     `ProductSnapshot::reviewCount()`. The §7.9 contract pairs the
 *     two axes under a single capability flag because providers
 *     that return one almost always return both, and decoupling
 *     would only invite four-way capability matrices the consumer
 *     has to enumerate.
 *
 *   - `supportsCategories` — provider responses include the
 *     `ProductSnapshot::categories()` path entries.
 *
 *   - `supportsSorts` — provider's search supports the §7.1
 *     `sort` discriminator. False means the search UI should hide
 *     the sort-order dropdown for this provider.
 *
 *   - `supportsAffiliateLinkBuild` — provider implementation can
 *     mint affiliate-tracked URLs from
 *     {@see \Affilun\Domain\Affiliate\AffiliateContext}.
 *     False means the application layer falls back to the
 *     provider's plain detail URL.
 *
 *   - `supportsHealthCheck` — provider implementation exposes a
 *     {@see \Affilun\Domain\Provider\ProviderHealthStatus}
 *     probe. False means the diagnostics surface should mark the
 *     provider's health as "unknown" rather than rendering a
 *     status badge.
 *
 *   - `supportsRateLimitInspection` — provider implementation
 *     exposes a structured rate-limit signal alongside its
 *     responses (per-window remaining / reset timestamp). False
 *     means consuming surfaces cannot show a rate-limit gauge for
 *     this provider.
 *
 * Per §7.9 ("UI は capabilities を見て機能を出し分ける" /
 * "false の機能に依存する UI は表示しない") consuming surfaces
 * MUST consult capabilities before rendering provider-specific
 * affordances; a UI that hard-codes a provider's feature set
 * defeats the entire point of the abstraction.
 *
 * The §8 feature-key surface is exposed in two ways:
 *
 *   - {@see self::supportsFeature()} accepts the §8 closed set of
 *     feature keys (`search`, `lookup`, `multiple_images`, ...) and
 *     returns the corresponding boolean. Useful for REST
 *     controllers that take a feature key from the request body
 *     and need to gate behavior on it.
 *
 *   - {@see self::supportedFeatures()} enumerates every supported
 *     feature key in §8 declaration order. Useful for admin UIs
 *     that want to render a "this provider supports..." badge
 *     list.
 *
 * §8 names twelve feature keys (`search`, `lookup`,
 * `multiple_images`, `price`, `availability`, `rating`,
 * `review_count`, `categories`, `sorts`, `affiliate_link`,
 * `health_check`, `rate_limit_inspection`) versus §7.9's eleven
 * boolean fields because §8 splits `rating` / `review_count` into
 * separate keys. The DTO maps both `rating` and `review_count` to
 * the same {@see self::$supportsRating} flag because §7.9
 * deliberately collapsed them into a single axis; the per-key
 * lookup honors §8's vocabulary while the underlying state stays
 * consistent with §7.9's surface.
 *
 * Instances are immutable. There are no `with*()` mutation helpers
 * because a capabilities snapshot represents a single provider's
 * static feature surface: any change should be modeled as a fresh
 * provider deployment producing a fresh
 * {@see ProviderCapabilities}, not as an in-place edit.
 *
 * @package Affilun\Domain\Provider
 */

declare(strict_types=1);

namespace Affilun\Domain\Provider;

use InvalidArgumentException;

/**
 * Immutable enumeration of a provider implementation's supported
 * features.
 *
 * See the file-level docblock for the mapping to
 * `provider-api-contracts.md` §7.9 / §8, the consuming-surface
 * contract that binds UIs to consult capabilities, and the rationale
 * for collapsing `rating` / `review_count` into a single
 * {@see self::$supportsRating} flag.
 */
final class ProviderCapabilities {

	/**
	 * §8 feature key for the keyword-search capability.
	 *
	 * Exposed as a class constant so call sites can reference the
	 * exact token without duplicating the literal, and so a future
	 * spec change has a single location to update.
	 */
	public const FEATURE_SEARCH = 'search';

	/**
	 * §8 feature key for single-item lookup by ProviderProductId.
	 */
	public const FEATURE_LOOKUP = 'lookup';

	/**
	 * §8 feature key for the gallery-image surface.
	 */
	public const FEATURE_MULTIPLE_IMAGES = 'multiple_images';

	/**
	 * §8 feature key for PriceInfo support.
	 */
	public const FEATURE_PRICE = 'price';

	/**
	 * §8 feature key for meaningful AvailabilityInfo signal.
	 */
	public const FEATURE_AVAILABILITY = 'availability';

	/**
	 * §8 feature key for `ProductSnapshot::rating()` support.
	 */
	public const FEATURE_RATING = 'rating';

	/**
	 * §8 feature key for `ProductSnapshot::reviewCount()` support.
	 *
	 * Maps to the same underlying {@see self::$supportsRating} flag
	 * as {@see self::FEATURE_RATING}; §7.9 collapses the two axes
	 * but §8's vocabulary names them separately.
	 */
	public const FEATURE_REVIEW_COUNT = 'review_count';

	/**
	 * §8 feature key for category-path support.
	 */
	public const FEATURE_CATEGORIES = 'categories';

	/**
	 * §8 feature key for the sort-order discriminator.
	 */
	public const FEATURE_SORTS = 'sorts';

	/**
	 * §8 feature key for affiliate-link minting.
	 */
	public const FEATURE_AFFILIATE_LINK = 'affiliate_link';

	/**
	 * §8 feature key for the health-check probe.
	 */
	public const FEATURE_HEALTH_CHECK = 'health_check';

	/**
	 * §8 feature key for structured rate-limit inspection.
	 */
	public const FEATURE_RATE_LIMIT_INSPECTION = 'rate_limit_inspection';

	/**
	 * Closed set of §8 feature keys, in declaration order.
	 *
	 * Maintained as a class-level constant so the validator and the
	 * enumeration helper can iterate the same source of truth.
	 *
	 * @var list<string>
	 */
	private const VALID_FEATURES = array(
		self::FEATURE_SEARCH,
		self::FEATURE_LOOKUP,
		self::FEATURE_MULTIPLE_IMAGES,
		self::FEATURE_PRICE,
		self::FEATURE_AVAILABILITY,
		self::FEATURE_RATING,
		self::FEATURE_REVIEW_COUNT,
		self::FEATURE_CATEGORIES,
		self::FEATURE_SORTS,
		self::FEATURE_AFFILIATE_LINK,
		self::FEATURE_HEALTH_CHECK,
		self::FEATURE_RATE_LIMIT_INSPECTION,
	);

	/**
	 * Whether the provider implementation can satisfy keyword
	 * search requests via SearchProviderInterface.
	 */
	private bool $supportsSearch;

	/**
	 * Whether the provider implementation can satisfy single-item
	 * lookup via ItemLookupProviderInterface.
	 */
	private bool $supportsLookup;

	/**
	 * Whether provider responses include a gallery image list in
	 * addition to the primary image.
	 */
	private bool $supportsMultipleImages;

	/**
	 * Whether provider responses include PriceInfo data.
	 */
	private bool $supportsPrice;

	/**
	 * Whether provider responses include a meaningful (non-unknown)
	 * availability signal.
	 */
	private bool $supportsAvailability;

	/**
	 * Whether provider responses include rating / review-count
	 * data. §7.9 collapses both axes under a single capability
	 * flag.
	 */
	private bool $supportsRating;

	/**
	 * Whether provider responses include category-path entries.
	 */
	private bool $supportsCategories;

	/**
	 * Whether the provider's search supports the §7.1 sort
	 * discriminator.
	 */
	private bool $supportsSorts;

	/**
	 * Whether the provider implementation can mint affiliate-
	 * tracked URLs.
	 */
	private bool $supportsAffiliateLinkBuild;

	/**
	 * Whether the provider implementation exposes a
	 * ProviderHealthStatus probe.
	 */
	private bool $supportsHealthCheck;

	/**
	 * Whether the provider implementation exposes a structured
	 * rate-limit signal.
	 */
	private bool $supportsRateLimitInspection;

	/**
	 * Construct a capabilities snapshot with explicit values for
	 * every flag.
	 *
	 * The constructor is exhaustive so a programming error
	 * (forgetting to declare a capability when adding a new
	 * provider implementation) surfaces at the type system
	 * boundary rather than as a runtime "feature silently
	 * disabled" surprise. Named-argument construction is the
	 * ergonomic entry point for most call sites.
	 *
	 * @param bool $supportsSearch              Keyword search.
	 * @param bool $supportsLookup              Single-item lookup.
	 * @param bool $supportsMultipleImages      Gallery image list.
	 * @param bool $supportsPrice               PriceInfo data.
	 * @param bool $supportsAvailability        Meaningful availability.
	 * @param bool $supportsRating              Rating / review count.
	 * @param bool $supportsCategories          Category-path entries.
	 * @param bool $supportsSorts               Sort discriminator.
	 * @param bool $supportsAffiliateLinkBuild  Affiliate URL minting.
	 * @param bool $supportsHealthCheck         Health-check probe.
	 * @param bool $supportsRateLimitInspection Rate-limit inspection.
	 */
	public function __construct(
		bool $supportsSearch,
		bool $supportsLookup,
		bool $supportsMultipleImages,
		bool $supportsPrice,
		bool $supportsAvailability,
		bool $supportsRating,
		bool $supportsCategories,
		bool $supportsSorts,
		bool $supportsAffiliateLinkBuild,
		bool $supportsHealthCheck,
		bool $supportsRateLimitInspection
	) {
		$this->supportsSearch              = $supportsSearch;
		$this->supportsLookup              = $supportsLookup;
		$this->supportsMultipleImages      = $supportsMultipleImages;
		$this->supportsPrice               = $supportsPrice;
		$this->supportsAvailability        = $supportsAvailability;
		$this->supportsRating              = $supportsRating;
		$this->supportsCategories          = $supportsCategories;
		$this->supportsSorts               = $supportsSorts;
		$this->supportsAffiliateLinkBuild  = $supportsAffiliateLinkBuild;
		$this->supportsHealthCheck         = $supportsHealthCheck;
		$this->supportsRateLimitInspection = $supportsRateLimitInspection;
	}

	/**
	 * Construct a capabilities snapshot with every flag set to
	 * false.
	 *
	 * Convenience factory for the canonical "minimal provider /
	 * not yet configured" snapshot so test setups and stub
	 * providers do not have to spell out eleven `false` arguments.
	 * Useful as a starting point for a provider that wants to
	 * gradually enable capabilities as its implementation matures.
	 */
	public static function none(): self {
		return new self(
			false,
			false,
			false,
			false,
			false,
			false,
			false,
			false,
			false,
			false,
			false
		);
	}

	/**
	 * Closed set of §8 feature keys, in declaration order.
	 *
	 * Exposed for tests and for diagnostics surfaces that want to
	 * enumerate the valid keys (for example, populating an admin
	 * filter dropdown) without duplicating the list.
	 *
	 * @return list<string>
	 */
	public static function validFeatures(): array {
		return self::VALID_FEATURES;
	}

	/**
	 * Whether the provider implementation can satisfy keyword
	 * search.
	 */
	public function supportsSearch(): bool {
		return $this->supportsSearch;
	}

	/**
	 * Whether the provider implementation can satisfy single-item
	 * lookup.
	 */
	public function supportsLookup(): bool {
		return $this->supportsLookup;
	}

	/**
	 * Whether provider responses include a gallery image list.
	 */
	public function supportsMultipleImages(): bool {
		return $this->supportsMultipleImages;
	}

	/**
	 * Whether provider responses include PriceInfo data.
	 */
	public function supportsPrice(): bool {
		return $this->supportsPrice;
	}

	/**
	 * Whether provider responses include a meaningful availability
	 * signal.
	 */
	public function supportsAvailability(): bool {
		return $this->supportsAvailability;
	}

	/**
	 * Whether provider responses include rating / review-count
	 * data. Per §7.9 the two axes share a single capability flag.
	 */
	public function supportsRating(): bool {
		return $this->supportsRating;
	}

	/**
	 * Whether provider responses include category-path entries.
	 */
	public function supportsCategories(): bool {
		return $this->supportsCategories;
	}

	/**
	 * Whether the provider's search supports the §7.1 sort
	 * discriminator.
	 */
	public function supportsSorts(): bool {
		return $this->supportsSorts;
	}

	/**
	 * Whether the provider implementation can mint affiliate-
	 * tracked URLs.
	 */
	public function supportsAffiliateLinkBuild(): bool {
		return $this->supportsAffiliateLinkBuild;
	}

	/**
	 * Whether the provider implementation exposes a
	 * ProviderHealthStatus probe.
	 */
	public function supportsHealthCheck(): bool {
		return $this->supportsHealthCheck;
	}

	/**
	 * Whether the provider implementation exposes a structured
	 * rate-limit signal.
	 */
	public function supportsRateLimitInspection(): bool {
		return $this->supportsRateLimitInspection;
	}

	/**
	 * Report whether the provider supports the named §8 feature.
	 *
	 * The closed set of accepted feature keys is enumerated by
	 * {@see self::validFeatures()}; an out-of-set token raises
	 * {@see InvalidArgumentException} carrying the offending value
	 * verbatim and a comma-separated list of the valid tokens for
	 * diagnosis.
	 *
	 * Per the §7.9 / §8 contract:
	 *
	 *   - `rating` and `review_count` both consult
	 *     {@see self::$supportsRating} because §7.9 collapses the
	 *     two axes; §8's vocabulary names them separately for
	 *     callers that want to render a per-axis label.
	 *
	 *   - The remaining ten keys map one-to-one to their named
	 *     accessors.
	 *
	 * @param string $feature §8 feature key.
	 *
	 * @throws InvalidArgumentException When `$feature` is not a
	 *                                  member of the §8 closed
	 *                                  set.
	 */
	public function supportsFeature( string $feature ): bool {
		switch ( $feature ) {
			case self::FEATURE_SEARCH:
				return $this->supportsSearch;
			case self::FEATURE_LOOKUP:
				return $this->supportsLookup;
			case self::FEATURE_MULTIPLE_IMAGES:
				return $this->supportsMultipleImages;
			case self::FEATURE_PRICE:
				return $this->supportsPrice;
			case self::FEATURE_AVAILABILITY:
				return $this->supportsAvailability;
			case self::FEATURE_RATING:
			case self::FEATURE_REVIEW_COUNT:
				return $this->supportsRating;
			case self::FEATURE_CATEGORIES:
				return $this->supportsCategories;
			case self::FEATURE_SORTS:
				return $this->supportsSorts;
			case self::FEATURE_AFFILIATE_LINK:
				return $this->supportsAffiliateLinkBuild;
			case self::FEATURE_HEALTH_CHECK:
				return $this->supportsHealthCheck;
			case self::FEATURE_RATE_LIMIT_INSPECTION:
				return $this->supportsRateLimitInspection;
		}

		$message = sprintf(
			'ProviderCapabilities feature must be one of [%s]; got "%s".',
			implode( ', ', self::VALID_FEATURES ),
			$feature
		);
		// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
		throw new InvalidArgumentException( $message );
	}

	/**
	 * Enumerate every §8 feature key the provider supports, in
	 * declaration order.
	 *
	 * Useful for admin UIs that want to render a "this provider
	 * supports..." badge list. The list is positionally keyed so
	 * JSON-encoding produces a JSON array, and entries appear in
	 * the same order {@see self::validFeatures()} returns them so
	 * the badge order is stable across providers.
	 *
	 * Note: `rating` and `review_count` both appear when
	 * {@see self::supportsRating()} is true, matching the §8
	 * vocabulary's per-axis split even though the underlying flag
	 * is shared.
	 *
	 * @return list<string>
	 */
	public function supportedFeatures(): array {
		$supported = array();
		foreach ( self::VALID_FEATURES as $feature ) {
			if ( $this->supportsFeature( $feature ) ) {
				$supported[] = $feature;
			}
		}
		return $supported;
	}
}
