<?php
/**
 * Item-lookup provider contract.
 *
 * Specialization of {@see ProviderInterface} for provider
 * implementations that can satisfy single-item lookup requests by
 * a {@see ProviderProductId}. The contract adds a single method
 * ({@see self::getProduct()}) on top of the cross-cutting surface
 * so consuming surfaces can branch on
 * `instanceof ItemLookupProviderInterface` to discover whether a
 * given provider can drive the "fetch one item" code path without
 * having to consult
 * {@see ProviderCapabilities::supportsLookup()} separately.
 *
 * Method semantics match `specifications/provider-api-contracts.md`
 * §6.3 exactly:
 *
 *   - {@see self::getProduct()} accepts a normalized
 *     {@see ProviderProductId} and returns a normalized
 *     {@see ProductSnapshot}. Both ends of the call are validated
 *     domain values, so the provider implementation receives
 *     well-formed input and is responsible for producing
 *     well-formed output that satisfies the cross-provider
 *     contract.
 *
 *   - The §6.3 contract enumerates four binding rules the
 *     implementation MUST honor:
 *
 *       1. The supplied {@see ProviderProductId} MUST belong to
 *          this provider — the §7.3 "provider 不一致の ID を他
 *          provider に渡してはならない" rule lifted to the lookup
 *          dispatch boundary. Implementations MUST raise
 *          {@see ProviderRequestException} when the id's
 *          {@see ProviderProductId::provider()} does not match
 *          {@see ProviderInterface::key()} on this provider.
 *
 *       2. "Product does not exist" is NOT encoded as a sentinel
 *          return (null, empty snapshot) — it surfaces as
 *          {@see ProductNotFoundException}. The §10.1 "失敗時に
 *          空配列を返してごまかす" prohibition binds especially
 *          tightly here because a returned-but-empty snapshot
 *          would lie about the upstream's answer.
 *
 *       3. Transient upstream failures map to
 *          {@see TemporaryUnavailableException}; the application
 *          layer is expected to back off and retry per the §12
 *          retry contract.
 *
 *       4. Authentication failures map to
 *          {@see AuthenticationException} and MUST NOT be folded
 *          into a "not found" result, even though the upstream may
 *          return an indistinguishable 4xx envelope for both
 *          cases. The provider implementation is responsible for
 *          telling the two apart based on the upstream's error
 *          taxonomy.
 *
 *   - The returned {@see ProductSnapshot} MUST satisfy its own
 *     constructor invariants:
 *
 *       * The snapshot's {@see ProductSnapshot::provider()} key
 *         and the supplied {@see ProviderProductId::provider()}
 *         MUST match (the §7.3 cross-check is enforced by
 *         {@see ProductSnapshot} itself, but the provider
 *         implementation is the canonical raise site for the
 *         mismatch — a snapshot tagged with the wrong key is a
 *         programming error inside the provider).
 *
 *       * The snapshot's
 *         {@see ProductSnapshot::providerProductId()} SHOULD be
 *         the same id the caller supplied, modulo any
 *         canonicalization the provider applies (case folding,
 *         legacy-id rewriting). Consuming surfaces correlate the
 *         response back to the request via this id, so a
 *         silently-rewritten id is a debugging hazard.
 *
 *       * The snapshot's {@see ProductSnapshot::fetchedAt()} MUST
 *         reflect the actual time the provider produced the
 *         snapshot, not a cached / stub timestamp from a
 *         pre-existing record. The sync layer uses this timestamp
 *         to decide whether subsequent stale-detection passes
 *         apply.
 *
 * Failure mapping summary (per §6.3 + §10.2):
 *
 *   - Product does not exist: {@see ProductNotFoundException}.
 *   - Authentication failures: {@see AuthenticationException}.
 *   - Transient outages / network timeouts:
 *     {@see TemporaryUnavailableException}.
 *   - Rate-limit refusals: {@see RateLimitExceededException}.
 *   - Supplied id belongs to another provider, or the id's value
 *     half violates the provider's native grammar:
 *     {@see ProviderRequestException}.
 *   - Upstream returned a response that fails the cross-provider
 *     contract: {@see ProviderResponseException}.
 *   - Provider does not support lookup at all: this is a
 *     programming error at dispatch time (the caller should have
 *     consulted {@see ProviderCapabilities::supportsLookup()});
 *     the dispatch layer raises
 *     {@see UnsupportedFeatureException} BEFORE the call reaches
 *     this interface, so implementations of this interface SHOULD
 *     NOT need to raise it themselves.
 *
 * Implementations that satisfy this interface SHOULD also report
 * {@see ProviderCapabilities::supportsLookup()} as `true` from
 * their {@see ProviderInterface::capabilities()} result; the two
 * surfaces are independent (one is type-system, the other is
 * runtime introspection) but MUST stay consistent.
 *
 * @package Affilun\Domain\Provider
 */

declare(strict_types=1);

namespace Affilun\Domain\Provider;

use Affilun\Domain\Product\ProductSnapshot;
use Affilun\Domain\Provider\Exception\AuthenticationException;
use Affilun\Domain\Provider\Exception\ProductNotFoundException;
use Affilun\Domain\Provider\Exception\ProviderException;
use Affilun\Domain\Provider\Exception\ProviderRequestException;
use Affilun\Domain\Provider\Exception\ProviderResponseException;
use Affilun\Domain\Provider\Exception\RateLimitExceededException;
use Affilun\Domain\Provider\Exception\TemporaryUnavailableException;
use Affilun\Domain\Provider\Exception\UnsupportedFeatureException;

/**
 * Specialization of {@see ProviderInterface} for providers that
 * can satisfy single-item lookup requests by
 * {@see ProviderProductId}.
 *
 * See the file-level docblock for the four §6.3 binding rules
 * implementations must honor (provider key cross-check on the
 * supplied id, "not found" surfaces as
 * {@see ProductNotFoundException} not as a sentinel, transient
 * failures map to {@see TemporaryUnavailableException}, auth
 * failures stay distinct from "not found") and the failure-mapping
 * summary that ties §6.3 to the §10.2 exception hierarchy.
 */
interface ItemLookupProviderInterface extends ProviderInterface {

	/**
	 * Fetch the {@see ProductSnapshot} for the supplied
	 * {@see ProviderProductId}.
	 *
	 * The supplied id is already validated at the cross-provider
	 * level — both halves are non-empty and trimmed — but the
	 * implementation MUST verify the provider half matches its own
	 * {@see ProviderInterface::key()} and raise
	 * {@see ProviderRequestException} when it does not. The id's
	 * value half may still violate the provider's native grammar
	 * (Amazon ASIN length / charset, 楽天 itemCode shape); that
	 * also surfaces as {@see ProviderRequestException} carrying
	 * enough context for the admin UI to explain the constraint.
	 *
	 * The returned {@see ProductSnapshot} MUST:
	 *
	 *   - Carry this provider's key (the §7.3 cross-check is
	 *     enforced by {@see ProductSnapshot} itself, but the
	 *     provider implementation is the canonical raise site for
	 *     the mismatch).
	 *
	 *   - Reflect the supplied id on
	 *     {@see ProductSnapshot::providerProductId()} modulo any
	 *     canonicalization the provider applies. Consuming surfaces
	 *     correlate the response back to the request via this id,
	 *     so silently rewriting it is a debugging hazard.
	 *
	 *   - Stamp {@see ProductSnapshot::fetchedAt()} with the
	 *     current time at which the provider produced the snapshot,
	 *     NOT a cached / stub timestamp from a pre-existing record.
	 *     The sync layer's stale-detection passes consult this
	 *     timestamp.
	 *
	 *   - Encode the upstream's availability signal on
	 *     {@see ProductSnapshot::availability()}; per §7.6 the
	 *     "no signal" case is encoded as
	 *     {@see \Affilun\Domain\Pricing\AvailabilityInfo::unknown()}
	 *     rather than as null.
	 *
	 * "Product does not exist" — the upstream confirmed no product
	 * matches the supplied id — is NOT a normal-path return.
	 * Implementations MUST raise {@see ProductNotFoundException}
	 * with the looked-up id on the exception's
	 * {@see \Affilun\Domain\Provider\Exception\ProviderException::context()}
	 * map (typically as a `provider_product_id` entry) so
	 * downstream observability can correlate the failure with the
	 * originating request.
	 *
	 * @param ProviderProductId $id Identifier whose detail snapshot
	 *                              should be fetched.
	 *
	 * @return ProductSnapshot Normalized snapshot for the supplied id.
	 *
	 * @throws ProviderRequestException        When `$id` belongs to
	 *                                         a different provider,
	 *                                         or when its value half
	 *                                         violates the provider's
	 *                                         native identifier
	 *                                         grammar.
	 * @throws ProductNotFoundException        When the upstream
	 *                                         confirmed no product
	 *                                         matches the supplied
	 *                                         id.
	 * @throws AuthenticationException         When the upstream
	 *                                         rejected the credentials
	 *                                         attached to the request.
	 *                                         MUST NOT be folded into
	 *                                         a {@see ProductNotFoundException}
	 *                                         even when the upstream
	 *                                         returns an
	 *                                         indistinguishable 4xx
	 *                                         envelope for both cases.
	 * @throws RateLimitExceededException      When the upstream is
	 *                                         currently refusing
	 *                                         requests due to a
	 *                                         rate-limit window.
	 * @throws TemporaryUnavailableException   When the upstream
	 *                                         returned a transient
	 *                                         5xx-style failure or
	 *                                         the request timed out
	 *                                         at the network layer.
	 * @throws ProviderResponseException       When the upstream
	 *                                         response failed to
	 *                                         parse into the
	 *                                         cross-provider model
	 *                                         or violated the §7.4
	 *                                         contract.
	 * @throws UnsupportedFeatureException     When the dispatch layer
	 *                                         routes a lookup request
	 *                                         to a provider that does
	 *                                         not support lookup; in
	 *                                         practice the dispatch
	 *                                         layer should have
	 *                                         consulted
	 *                                         {@see ProviderCapabilities::supportsLookup()}
	 *                                         before reaching this
	 *                                         method.
	 * @throws ProviderException               When any other §10.2
	 *                                         error mode applies.
	 */
	public function getProduct( ProviderProductId $id ): ProductSnapshot;
}
