<?php
/**
 * Rate-limit-exceeded provider exception.
 *
 * Concrete {@see ProviderException} subclass that signals the
 * upstream provider refused the request because the caller has hit
 * the provider's rate-limit window. The provider implementation
 * raises this exception when it observes a 429 / quota-exceeded
 * response from the upstream API, or when its own client-side
 * circuit breaker trips before issuing the call.
 *
 * Field semantics match `specifications/provider-api-contracts.md`
 * §10.2 / §12.3 exactly:
 *
 *   - {@see self::ERROR_CODE} is `rate_limit_exceeded`, 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.
 *
 *   - Per §12.3 ("再試行可能時刻が分かる場合は例外コンテキスト
 *     へ含める"), when the upstream API exposes a retry-after hint
 *     the provider implementation SHOULD populate the
 *     {@see ProviderException::context()} map with a `retry_after`
 *     entry. The shape of that entry is intentionally not fixed at
 *     this layer because providers expose the hint in different
 *     ways (Amazon: seconds; 楽天 / Yahoo: absolute timestamp); the
 *     observability layer normalizes when consuming.
 *
 *   - 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 429 response and the response
 *     body / headers indicate the failure is transient and tied to
 *     a rate-limit window.
 *   - Provider client-side throttle (token bucket, leaky bucket)
 *     refuses to dispatch the request because the per-window quota
 *     is exhausted; raising here lets the application layer back
 *     off without the round-trip to the upstream API.
 *
 * Distinct from {@see TemporaryUnavailableException}: that
 * exception fires for 5xx-style transient outages where retry is
 * possible but a specific retry-after window may not be known. The
 * {@see \Affilun\Domain\Provider\ProviderHealthStatus}
 * surfaces the same distinction as `STATUS_RATE_LIMITED` versus
 * a non-rate-limit `STATUS_DEGRADED` / `STATUS_UNREACHABLE`.
 *
 * @package Affilun\Domain\Provider\Exception
 */

declare(strict_types=1);

namespace Affilun\Domain\Provider\Exception;

use Throwable;

/**
 * Provider exception for rate-limit refusals from the upstream API
 * or from the provider's own client-side throttle.
 *
 * See the file-level docblock for the mapping to
 * `provider-api-contracts.md` §10.2 / §10.3 / §12.3, the
 * `retry_after` context-field convention, and the distinction
 * versus {@see TemporaryUnavailableException}.
 */
final class RateLimitExceededException 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 = 'rate_limit_exceeded';

	/**
	 * Construct a rate-limit-exceeded exception.
	 *
	 * Forwards {@see self::ERROR_CODE} to the
	 * {@see ProviderException} base so every rate-limit refusal
	 * carries the same §10.2 code regardless of which provider
	 * implementation raised 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 a `retry_after` entry when the upstream response carries
	 * a retry-after hint, per §12.3.
	 *
	 * @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 a `retry_after`
	 *                                       entry when the hint is known.
	 * @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 );
	}
}
