<?php
/**
 * Top-level provider contract.
 *
 * Every CLB provider implementation MUST implement this interface.
 * The contract collects the cross-cutting operations every provider
 * exposes regardless of which capabilities it supports — identity
 * (`key()`, `label()`), introspection
 * ({@see self::capabilities()}, {@see self::healthCheck()},
 * {@see self::supportsFeature()}), and the affiliate-link build
 * surface {@see self::buildAffiliateLink()} that every provider
 * must answer (even if its answer is a passthrough).
 *
 * Method semantics match `specifications/provider-api-contracts.md`
 * §6.1 exactly:
 *
 *   - {@see self::key()} returns the stable §4 provider key
 *     (`amazon`, `rakuten`, `yahoo_shopping`, ...). The value MUST
 *     be deterministic for the lifetime of the implementation —
 *     the persistence layer, REST envelopes, and log lines all
 *     correlate failures back to this key, and a key that drifts
 *     between calls would invalidate every prior record.
 *
 *   - {@see self::label()} returns the human-readable display name
 *     for the admin UI ("Amazon", "楽天市場",
 *     "Yahoo!ショッピング"). Localization is the consuming
 *     surface's responsibility; the value returned here is the
 *     provider's canonical label in the locale the implementation
 *     was constructed for.
 *
 *   - {@see self::capabilities()} returns the
 *     {@see ProviderCapabilities} snapshot describing which
 *     features the provider supports. Per §7.9 ("UI は capabilities
 *     を見て機能を出し分ける") consuming surfaces MUST consult this
 *     before issuing requests so an
 *     {@see \CommerceLinkBuilder\Domain\Provider\Exception\UnsupportedFeatureException}
 *     never has to fire as the first signal.
 *
 *   - {@see self::healthCheck()} returns the
 *     {@see ProviderHealthStatus} snapshot describing the
 *     provider's current operational state. Implementations that
 *     declare {@see ProviderCapabilities::supportsHealthCheck()}
 *     MUST return a meaningful probe result; implementations that
 *     do not declare the capability SHOULD still return a
 *     deterministic snapshot (typically
 *     {@see ProviderHealthStatus::unconfigured()} or
 *     {@see ProviderHealthStatus::healthy()}) rather than throwing.
 *
 *   - {@see self::supportsFeature()} reports whether the named §8
 *     feature key is supported. Implementations SHOULD delegate to
 *     {@see ProviderCapabilities::supportsFeature()} on their own
 *     {@see self::capabilities()} result so the two surfaces stay
 *     consistent without duplication.
 *
 *   - {@see self::buildAffiliateLink()} mints an
 *     {@see AffiliateLinkResult} for the supplied
 *     {@see ProductSnapshot} given the {@see AffiliateContext}.
 *     Every provider answers this call — implementations whose
 *     {@see ProviderCapabilities::supportsAffiliateLinkBuild()}
 *     reports false MUST return
 *     {@see AffiliateLinkResult::passthrough()} on the snapshot's
 *     detail URL rather than throwing, so consuming surfaces can
 *     always render a clickable link.
 *
 * Failure semantics:
 *
 *   - Operations that cannot be honored MUST raise the matching
 *     exception from the §10.2 closed set
 *     ({@see \CommerceLinkBuilder\Domain\Provider\Exception\ProviderException}
 *     hierarchy). Returning sentinels / null / empty arrays to mask
 *     a failure is forbidden by §10.1.
 *
 *   - {@see self::healthCheck()} is the one method that intentionally
 *     does NOT throw on transient upstream failures: the whole
 *     point of the probe is to surface the failure as a structured
 *     {@see ProviderHealthStatus} snapshot the diagnostics surface
 *     can render. Authentication failures, rate-limit refusals, and
 *     network outages all map to the corresponding `STATUS_*` token
 *     rather than to an exception.
 *
 *   - {@see self::buildAffiliateLink()} MUST NOT throw for the
 *     "no tracking configured" / "fallback to plain detail URL"
 *     case; that case is encoded as
 *     {@see AffiliateLinkResult::passthrough()} with
 *     `trackingApplied = false`. Throwing is reserved for genuine
 *     errors (the supplied snapshot's detail URL is invalid, the
 *     provider's affiliate-program endpoint is unreachable when
 *     tracking IS configured).
 *
 * Implementations are expected to live in `src/Infrastructure/Provider/`
 * per the `coding-standards.md` §2.2 / `provider-api-contracts.md`
 * §16.1 layout — the Domain layer holds only the contract, not the
 * concrete provider classes that talk to upstream HTTP APIs.
 *
 * @package CommerceLinkBuilder\Domain\Provider
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Domain\Provider;

use CommerceLinkBuilder\Domain\Affiliate\AffiliateContext;
use CommerceLinkBuilder\Domain\Affiliate\AffiliateLinkResult;
use CommerceLinkBuilder\Domain\Product\ProductSnapshot;
use CommerceLinkBuilder\Domain\Provider\Exception\ProviderConfigurationException;
use CommerceLinkBuilder\Domain\Provider\Exception\ProviderException;
use CommerceLinkBuilder\Domain\Provider\Exception\ProviderRequestException;

/**
 * Top-level contract every provider implementation must satisfy.
 *
 * See the file-level docblock for the per-method contract, the
 * "no sentinels on failure" rule that binds every method except
 * {@see self::healthCheck()} and {@see self::buildAffiliateLink()},
 * and the placement convention for concrete implementations.
 */
interface ProviderInterface {

	/**
	 * Stable §4 provider key identifying this implementation.
	 *
	 * The value MUST be lowercase ASCII (no whitespace, no
	 * surrounding punctuation) and MUST be deterministic for the
	 * lifetime of the implementation — REST envelopes, log lines,
	 * and the persistence layer all correlate records back to this
	 * key, and a key that drifts between calls would invalidate
	 * every prior record.
	 */
	public function key(): string;

	/**
	 * Human-readable display name for the admin UI.
	 *
	 * Returned in the implementation's canonical locale; consuming
	 * surfaces handle further localization. The value MUST be a
	 * non-empty string.
	 */
	public function label(): string;

	/**
	 * Capabilities snapshot describing which §8 features this
	 * provider supports.
	 *
	 * Per §7.9 ("UI は capabilities を見て機能を出し分ける")
	 * consuming surfaces MUST consult this before issuing requests
	 * so an unsupported-feature failure does not have to surface
	 * as an exception.
	 */
	public function capabilities(): ProviderCapabilities;

	/**
	 * Current health-probe snapshot for this provider.
	 *
	 * Implementations that declare
	 * {@see ProviderCapabilities::supportsHealthCheck()} MUST return
	 * a meaningful probe result. Implementations that do not declare
	 * the capability SHOULD still return a deterministic snapshot
	 * (typically {@see ProviderHealthStatus::unconfigured()} or a
	 * cached {@see ProviderHealthStatus::healthy()}) rather than
	 * throwing — the §7.10 status surface is itself the
	 * "something is wrong" signal.
	 *
	 * Authentication failures, rate-limit refusals, and network
	 * outages observed during the probe MUST be encoded as the
	 * matching `STATUS_*` token on the returned snapshot, NOT as
	 * an exception. Throwing is reserved for genuinely-unexpected
	 * conditions (programming error, malformed configuration object
	 * that the provider cannot decode at all).
	 */
	public function healthCheck(): ProviderHealthStatus;

	/**
	 * Report whether the named §8 feature key is supported.
	 *
	 * Implementations SHOULD delegate to
	 * {@see ProviderCapabilities::supportsFeature()} on their own
	 * {@see self::capabilities()} result so the two surfaces stay
	 * consistent without duplication.
	 *
	 * The closed set of accepted feature keys is enumerated by
	 * {@see ProviderCapabilities::validFeatures()}; out-of-set
	 * tokens MUST raise {@see \InvalidArgumentException} (typically
	 * via the delegated {@see ProviderCapabilities::supportsFeature()}
	 * call).
	 *
	 * @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;

	/**
	 * Mint an affiliate-tracked URL for the given product snapshot.
	 *
	 * Every provider answers this call — implementations whose
	 * {@see ProviderCapabilities::supportsAffiliateLinkBuild()}
	 * reports false MUST return
	 * {@see AffiliateLinkResult::passthrough()} on the snapshot's
	 * detail URL rather than throwing, so consuming surfaces can
	 * always render a clickable link.
	 *
	 * Provider mismatch is a programming error: the supplied
	 * `$snapshot` MUST belong to this provider (per §7.3
	 * "provider 不一致の ID を他 provider に渡してはならない").
	 * Implementations MUST raise
	 * {@see ProviderRequestException} when the snapshot's
	 * {@see ProductSnapshot::provider()} does not match
	 * {@see self::key()}.
	 *
	 * Genuine failures (the snapshot's detail URL is invalid, the
	 * provider's affiliate endpoint is unreachable when tracking IS
	 * configured, the supplied {@see AffiliateContext} is missing
	 * required-for-this-provider fields) MUST raise the matching
	 * {@see ProviderException} subclass from the §10.2 closed set.
	 *
	 * The "no tracking configured / fallback to plain detail URL"
	 * case is NOT a failure: it is encoded as
	 * {@see AffiliateLinkResult::passthrough()} with
	 * `trackingApplied = false` and any administrator-facing
	 * warnings on {@see AffiliateLinkResult::warnings()}.
	 *
	 * @param ProductSnapshot  $snapshot Snapshot whose detail URL
	 *                                   should be tracked.
	 * @param AffiliateContext $context  Per-request affiliate
	 *                                   context (associate tag,
	 *                                   campaign, UTM tags, ...).
	 *
	 * @throws ProviderRequestException        When `$snapshot`
	 *                                         belongs to a different
	 *                                         provider, or when its
	 *                                         detail URL is invalid.
	 * @throws ProviderConfigurationException  When the affiliate
	 *                                         configuration the
	 *                                         provider needs is
	 *                                         partially supplied
	 *                                         (e.g. associate tag
	 *                                         missing while tracking
	 *                                         IS expected).
	 * @throws ProviderException               When any other §10.2
	 *                                         error mode applies.
	 */
	public function buildAffiliateLink(
		ProductSnapshot $snapshot,
		AffiliateContext $context
	): AffiliateLinkResult;
}
