<?php
/**
 * Search provider contract.
 *
 * Specialization of {@see ProviderInterface} for provider
 * implementations that can satisfy keyword search requests. The
 * contract adds a single method ({@see self::searchProducts()}) on
 * top of the cross-cutting surface so consuming surfaces can branch
 * on `instanceof SearchProviderInterface` to discover whether a
 * given provider can drive the search UI without having to consult
 * {@see ProviderCapabilities::supportsSearch()} separately.
 *
 * Method semantics match `specifications/provider-api-contracts.md`
 * §6.2 exactly:
 *
 *   - {@see self::searchProducts()} accepts a normalized
 *     {@see SearchQuery} and returns a normalized
 *     {@see SearchResult}. 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.2 contract enumerates four binding rules the
 *     implementation MUST honor:
 *
 *       1. The result MUST be a {@see SearchResult} (not null, not
 *          a sentinel array). Per the broader §10.1 "失敗時に空配列
 *          を返してごまかす" prohibition, returning a non-`SearchResult`
 *          value to mask a failure is forbidden.
 *
 *       2. Zero matches is a normal-path outcome, NOT a failure.
 *          The implementation returns
 *          {@see SearchResult::empty()} (or an equivalent
 *          {@see SearchResult} whose `items` list is empty) and
 *          MUST NOT raise an exception for this case.
 *
 *       3. Genuine "search cannot complete" conditions surface as
 *          either an exception from the §10.2 closed set OR — when
 *          the provider's design supports a partial-success result
 *          shape — a {@see SearchResult} whose `rawMeta` carries
 *          the failure detail. The §6.2 wording ("検索不能は例外
 *          または失敗結果") deliberately leaves both options open
 *          so providers with retry-friendly error envelopes do not
 *          have to wrap-and-unwrap exceptions for the common case.
 *
 *       4. Provider-native search-input constraints that the
 *          cross-provider {@see SearchQuery} cannot express
 *          (per-provider sort tokens, region restrictions, minimum
 *          keyword length) MUST surface as
 *          {@see ProviderRequestException} (or a more specific
 *          §10.2 subclass) carrying enough context for the admin
 *          UI to explain the constraint to the operator.
 *
 *   - The returned {@see SearchResult} MUST satisfy its own
 *     constructor invariants:
 *
 *       * Every {@see ProductSnapshot} in `items` carries this
 *         provider's key (the §7.3 cross-check is enforced by
 *         {@see SearchResult} 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).
 *
 *       * `page` and `perPage` MUST equal the values from the
 *         supplied {@see SearchQuery} unless the provider had to
 *         clamp them to its own caps; in that case the provider
 *         SHOULD reflect the actual values it used so the consuming
 *         surface can drive correct pagination.
 *
 *       * `total` is null only when the upstream API does not
 *         expose a count; `hasMore` is the canonical "is there
 *         another page?" signal regardless of `total`.
 *
 * Failure mapping summary (per §6.2 + §10.2):
 *
 *   - Authentication failures: {@see AuthenticationException}.
 *   - Rate-limit refusals: {@see RateLimitExceededException}.
 *   - Transient outages: {@see TemporaryUnavailableException}.
 *   - Malformed search input the provider cannot accept:
 *     {@see ProviderRequestException}.
 *   - Upstream returned a response that fails the cross-provider
 *     contract: {@see ProviderResponseException}.
 *   - Provider does not support search at all: this is a
 *     programming error at dispatch time (the caller should have
 *     consulted {@see ProviderCapabilities::supportsSearch()}); 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::supportsSearch()} 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\Provider\Exception\AuthenticationException;
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;
use Affilun\Domain\Product\ProductSnapshot;
use Affilun\Domain\Search\SearchQuery;
use Affilun\Domain\Search\SearchResult;

/**
 * Specialization of {@see ProviderInterface} for providers that
 * can satisfy keyword search requests.
 *
 * See the file-level docblock for the four §6.2 binding rules
 * implementations must honor (no sentinel returns, zero matches is
 * normal, search-failure modes map to §10.2 exceptions or partial
 * results, provider-native input constraints surface as
 * {@see ProviderRequestException}) and the failure-mapping summary
 * that ties §6.2 to the §10.2 exception hierarchy.
 */
interface SearchProviderInterface extends ProviderInterface {

	/**
	 * Execute a keyword search and return the matched product
	 * snapshots together with the pagination context the caller
	 * needs to drive a follow-up request.
	 *
	 * The supplied {@see SearchQuery} is already validated at the
	 * cross-provider level — `keyword` is non-empty, `page` and
	 * `perPage` are >= 1, the price-range cross-field rule has been
	 * enforced, etc. — so the implementation does not need to
	 * re-check those invariants. It IS responsible for enforcing
	 * provider-specific constraints (per-provider page-size caps,
	 * supported sort tokens, locale restrictions) and raising
	 * {@see ProviderRequestException} when the supplied query
	 * violates them.
	 *
	 * The returned {@see SearchResult} MUST:
	 *
	 *   - Carry every snapshot tagged with this provider's key
	 *     ({@see ProviderInterface::key()}). The §7.3 cross-check
	 *     is enforced by the {@see SearchResult} constructor; a
	 *     mismatched snapshot is a programming error inside this
	 *     provider implementation.
	 *
	 *   - Reflect the {@see SearchQuery::page()} and
	 *     {@see SearchQuery::perPage()} values from the request
	 *     unless the provider had to clamp them — in which case the
	 *     {@see SearchResult} MUST reflect the actual values used.
	 *
	 *   - Provide a non-null `hasMore` answer regardless of whether
	 *     `total` is populated. Per §7.2 `hasMore` is the canonical
	 *     pagination signal; consuming surfaces MUST NOT infer it
	 *     from `count($items) < $perPage`.
	 *
	 *   - Forward provider-native diagnostic data (request id,
	 *     rate-limit headers, total-pages count) on `rawMeta` per
	 *     §7.2 — but consuming surfaces MUST NOT branch on its
	 *     contents.
	 *
	 * Zero matches is a normal-path outcome: the implementation
	 * returns a {@see SearchResult} whose `items` list is empty and
	 * whose `total` reflects the upstream's reported count (or null
	 * when the upstream does not expose one). The implementation
	 * MUST NOT raise an exception for this case.
	 *
	 * @param SearchQuery $query Normalized search query.
	 *
	 * @return SearchResult The matched snapshots together with the
	 *                      pagination context for follow-up calls.
	 *
	 * @throws ProviderRequestException        When `$query` violates
	 *                                         a provider-native input
	 *                                         constraint (per-provider
	 *                                         page-size cap exceeded,
	 *                                         unsupported sort token,
	 *                                         locale not served).
	 * @throws AuthenticationException         When the upstream
	 *                                         rejected the credentials
	 *                                         attached to the request.
	 * @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 search request
	 *                                         to a provider that does
	 *                                         not support search; in
	 *                                         practice the dispatch
	 *                                         layer should have
	 *                                         consulted
	 *                                         {@see ProviderCapabilities::supportsSearch()}
	 *                                         before reaching this
	 *                                         method.
	 * @throws ProviderException               When any other §10.2
	 *                                         error mode applies.
	 */
	public function searchProducts( SearchQuery $query ): SearchResult;
}
