<?php
/**
 * Product-not-found provider exception.
 *
 * Concrete {@see ProviderException} subclass that signals the
 * upstream provider could not find a product matching the supplied
 * {@see \Affilun\Domain\Provider\ProviderProductId}.
 * Typical examples are an Amazon ASIN that has been delisted, a
 * 楽天 itemCode whose shop has closed, or a Yahoo!ショッピング
 * itemId that the upstream API reports as no longer available.
 *
 * Field semantics match `specifications/provider-api-contracts.md`
 * §10.2 / §10.3 / §6.3 exactly:
 *
 *   - {@see self::ERROR_CODE} is `product_not_found`, the stable
 *     §10.2 code that REST error envelopes, log lines, and admin
 *     diagnostics views surface as the machine-readable identifier
 *     for this failure mode.
 *
 *   - The §10.3 admin-facing translation is "商品が見つかりません
 *     でした" — consuming surfaces are expected to map the error
 *     code to that human-readable string rather than rendering the
 *     raw exception message, which may carry provider-specific
 *     diagnostic detail unsuitable for end-users.
 *
 *   - Per §6.3 ("存在しない商品は `ProductNotFoundException`"),
 *     {@see \Affilun\Domain\Provider\ItemLookupProviderInterface}
 *     implementations MUST raise this exception (rather than
 *     returning null or an empty snapshot) when the upstream
 *     reports the product does not exist. Returning a sentinel
 *     would conflict with §10.1 ("失敗時に空配列を返してごまかす"
 *     forbidden).
 *
 *   - Per §10.1 the exception message MUST NOT carry secret
 *     material; the provider implementation that constructs the
 *     exception is responsible for redacting before passing the
 *     summary in.
 *
 * Typical raise sites:
 *
 *   - Provider HTTP client receives a 404 response (or a
 *     provider-native equivalent: Amazon's `ItemNotAccessible`,
 *     楽天's empty `Items` array on a single-item lookup, etc.) for
 *     a request whose `ProviderProductId` was syntactically valid.
 *
 *   - Upstream API returns a 2xx response whose body explicitly
 *     reports the product is unavailable / delisted / removed
 *     rather than the response being structurally malformed
 *     (which would be a {@see ProviderResponseException} instead).
 *
 *   - Sync layer probes a previously-cached product whose upstream
 *     listing has since been retired; the resulting exception lets
 *     the application layer transition the
 *     {@see \Affilun\Domain\Pricing\AvailabilityInfo}
 *     to {@see \Affilun\Domain\Pricing\AvailabilityInfo::STATUS_UNAVAILABLE}
 *     and stop scheduling further sync attempts.
 *
 * Distinct from {@see ProviderRequestException}: that exception
 * fires when the request itself is malformed (the supplied
 * identifier violates the provider's grammar) — there was nothing
 * for the upstream to look up. This exception fires when the
 * request was structurally valid but the upstream confirmed no
 * matching product exists.
 *
 * Distinct from {@see ProviderResponseException}: that exception
 * fires when the upstream returned a response whose contents
 * violate the cross-provider contract; this exception fires when
 * the upstream returned a well-formed "no such product" signal.
 *
 * @package Affilun\Domain\Provider\Exception
 */

declare(strict_types=1);

namespace Affilun\Domain\Provider\Exception;

use Throwable;

/**
 * Provider exception for lookup requests where the upstream
 * confirmed no product matches the supplied identifier.
 *
 * See the file-level docblock for the mapping to
 * `provider-api-contracts.md` §10.2 / §10.3 / §6.3, the binding
 * "do not return a sentinel" rule for ItemLookupProviderInterface
 * implementations, and the distinctions versus
 * {@see ProviderRequestException} and {@see ProviderResponseException}.
 */
final class ProductNotFoundException extends ProviderException {

	/**
	 * Stable §10.2 error code for this failure mode.
	 *
	 * 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 ERROR_CODE = 'product_not_found';

	/**
	 * Construct a product-not-found exception.
	 *
	 * Forwards {@see self::ERROR_CODE} to the
	 * {@see ProviderException} base so every "no such product"
	 * raise carries the same §10.2 code regardless of which
	 * provider implementation surfaced it. The remaining
	 * cross-cutting fields (`provider`, `message`, `context`,
	 * `previous`) follow the base's contract verbatim — see
	 * {@see ProviderException} for the per-field validation rules
	 * and the secret-material rule that binds the message body.
	 *
	 * Provider implementations SHOULD populate the `context` map
	 * with the `ProviderProductId` value that was looked up
	 * (typically as a `provider_product_id` entry) so log lines
	 * and admin diagnostics views can correlate the failure with
	 * the originating request without parsing the message body.
	 *
	 * @param string               $provider §4 provider key that raised the exception.
	 * @param string               $message  Human-readable, non-secret summary.
	 * @param array<string, mixed> $context  Structured diagnostic context;
	 *                                       SHOULD include the looked-up
	 *                                       provider product identifier.
	 * @param Throwable|null       $previous Underlying cause, or null.
	 */
	public function __construct(
		string $provider,
		string $message = '',
		array $context = array(),
		?Throwable $previous = null
	) {
		parent::__construct( self::ERROR_CODE, $provider, $message, $context, $previous );
	}
}
