<?php
/**
 * Authentication-failed provider exception.
 *
 * Concrete {@see ProviderException} subclass that signals the
 * upstream provider rejected the request because the supplied
 * credentials were missing, malformed, or no longer valid (expired
 * tokens, revoked associate tags, rotated secrets that have not yet
 * been refreshed in admin settings).
 *
 * Field semantics match `specifications/provider-api-contracts.md`
 * §10.2 exactly:
 *
 *   - {@see self::ERROR_CODE} is `authentication_failed`, 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, which may carry provider-specific
 *     diagnostic detail unsuitable for end-users.
 *
 *   - Per §10.1, the exception message MUST NOT carry secret
 *     material (credentials, signing keys, unredacted authorization
 *     headers); the provider implementation that constructs the
 *     exception is responsible for redacting before passing the
 *     summary in.
 *
 * Typical raise sites:
 *
 *   - Provider HTTP client receives a 401 / 403 response from the
 *     upstream API and the response body indicates a credential
 *     rejection rather than a per-request authorization decision.
 *   - Provider configuration loader detects a malformed credential
 *     payload (truncated PEM, invalid base64 access key) at request
 *     construction time, before the request is dispatched.
 *
 * Distinct from {@see ProviderConfigurationException}: that
 * exception fires when credentials are entirely absent or the
 * provider has not been wired up yet, while this exception fires
 * when credentials are present but rejected. The
 * {@see \Affilun\Domain\Provider\ProviderHealthStatus}
 * surfaces the same distinction as `STATUS_INVALID_CREDENTIALS`
 * versus `STATUS_UNCONFIGURED`.
 *
 * Distinct from {@see ProviderRequestException}: that exception
 * fires when the request itself is malformed (bad parameters,
 * missing required fields), while this exception fires when the
 * request is structurally valid but the credentials attached to it
 * are not accepted.
 *
 * @package Affilun\Domain\Provider\Exception
 */

declare(strict_types=1);

namespace Affilun\Domain\Provider\Exception;

use Throwable;

/**
 * Provider exception for credentials missing, malformed, expired,
 * or otherwise rejected by the upstream API.
 *
 * See the file-level docblock for the mapping to
 * `provider-api-contracts.md` §10.2 / §10.3, the secret-material
 * rule that binds raise sites, and the distinctions versus
 * {@see ProviderConfigurationException} and
 * {@see ProviderRequestException}.
 */
final class AuthenticationException 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 = 'authentication_failed';

	/**
	 * Construct an authentication-failed exception.
	 *
	 * Forwards {@see self::ERROR_CODE} to the
	 * {@see ProviderException} base so every authentication failure
	 * 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.
	 *
	 * @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.
	 * @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 );
	}
}
