<?php
/**
 * Provider-response-invalid provider exception.
 *
 * Concrete {@see ProviderException} subclass that signals the
 * upstream provider returned a response that the provider
 * implementation could not parse into the cross-provider domain
 * model: required fields were missing, the JSON / XML envelope was
 * malformed, the response shape drifted from the documented
 * contract, or the upstream signalled a "policy violation" that
 * §10.2 expressly requires to be surfaced through this exception
 * rather than as a separate class.
 *
 * Field semantics match `specifications/provider-api-contracts.md`
 * §10.2 exactly:
 *
 *   - {@see self::ERROR_CODE} is `provider_response_invalid`, 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.
 *
 *   - Per §10.2 ("`PolicyViolation` の扱い"), provider-policy
 *     violations are NOT a separate exception class. They are
 *     surfaced through this exception with a
 *     {@see ProviderException::context()} entry of
 *     `reason: "policy_violation"`. The provider implementation
 *     that detects the violation is responsible for populating the
 *     `reason` key alongside any provider-specific supporting
 *     fields.
 *
 *   - 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. Provider-native diagnostic detail (raw response
 *     fragments, signature mismatches) belongs in the structured
 *     context map, not in the human-readable message.
 *
 * Typical raise sites:
 *
 *   - Provider HTTP client successfully receives a 2xx response but
 *     the body fails JSON / XML decoding entirely.
 *
 *   - Decoded response carries the wrong shape: a required field
 *     ({@see \CommerceLinkBuilder\Domain\Product\ProductSnapshot::title()},
 *     `detailUrl`, ...) is missing, an enum field carries an
 *     unknown token, or a nested object the cross-provider model
 *     expects is absent.
 *
 *   - Upstream API returns a structured "policy violation"
 *     envelope (provider terms of service breach, geo-restricted
 *     listing, brand-protection rejection) that the provider
 *     implementation classifies via the §10.2 `reason:
 *     "policy_violation"` context entry.
 *
 *   - Provider response signature / checksum fails verification
 *     against the request, indicating contract drift or a
 *     man-in-the-middle scenario the application layer cannot
 *     safely process.
 *
 * Distinct from {@see TemporaryUnavailableException}: that
 * exception fires when the upstream did not return a usable
 * response at all (5xx, network timeout, no body); this exception
 * fires when the upstream did return a response, but the response
 * itself violates the cross-provider contract.
 *
 * Distinct from {@see ProviderRequestException}: that exception
 * fires before a meaningful response is received, on the
 * request-construction side of the wire; this exception fires
 * after the response has been received and decoded.
 *
 * @package CommerceLinkBuilder\Domain\Provider\Exception
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Domain\Provider\Exception;

use Throwable;

/**
 * Provider exception for upstream responses that fail to parse or
 * that violate the cross-provider contract — including the §10.2
 * "policy violation" sub-reason.
 *
 * See the file-level docblock for the mapping to
 * `provider-api-contracts.md` §10.2, the `reason: "policy_violation"`
 * context-field convention, and the distinctions versus
 * {@see TemporaryUnavailableException} and
 * {@see ProviderRequestException}.
 */
final class ProviderResponseException 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 = 'provider_response_invalid';

	/**
	 * Canonical `reason` context-field value for the §10.2
	 * "policy violation" sub-case.
	 *
	 * Exposed as a class constant so provider implementations and
	 * consuming surfaces can reference the exact token without
	 * duplicating the literal across the codebase. Per §10.2 a
	 * policy violation is NOT a separate exception class — it is a
	 * {@see self::ERROR_CODE} exception with this `reason` entry on
	 * the {@see ProviderException::context()} map.
	 */
	public const REASON_POLICY_VIOLATION = 'policy_violation';

	/**
	 * Construct a provider-response-invalid exception.
	 *
	 * Forwards {@see self::ERROR_CODE} to the
	 * {@see ProviderException} base so every response-shape failure
	 * 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 that detect a §10.2 "policy
	 * violation" sub-case SHOULD populate the `context` map with
	 * `reason => self::REASON_POLICY_VIOLATION` (and any
	 * provider-specific supporting fields) so consuming surfaces
	 * can branch on the sub-reason 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 a `reason`
	 *                                       entry for the §10.2 policy-
	 *                                       violation sub-case.
	 * @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 );
	}
}
