<?php
/**
 * Rakuten provider credentials value object.
 *
 * Immutable value object that holds the credentials a
 * {@see \Affilun\Infrastructure\Provider\Rakuten\RakutenProvider}
 * implementation needs to call the Rakuten Web Service. The fields
 * mirror `specifications/provider-api-contracts.md` §9.2 verbatim:
 *
 *   - `applicationId` is the Rakuten Application ID (per the §9.2
 *     "必須" list). Required by every Rakuten Web Service request:
 *     the IchibaItem Search endpoint validates the value as the
 *     `applicationId` query parameter and rejects calls that omit
 *     it.
 *
 *   - `accessKey` is the Rakuten Access Key required by the
 *     current Rakuten Web Service authentication system. It is
 *     sent as an HTTP header so it does not leak through URLs.
 *
 *   - `affiliateId` is the Rakuten Affiliate ID. Required by
 *     affilun for affiliate-link minting and
 *     attached to search / lookup responses so the resulting
 *     detail URLs carry the affiliate-tracking parameters this
 *     site is registered under. Per §9.2 the field is mandatory at
 *     the credentials level even when a specific request would not
 *     need it; the provider implementation is responsible for
 *     conditionally forwarding it on the wire.
 *
 * Per `provider-api-contracts.md` §10.1 ("例外メッセージに秘密
 * 情報を含めない"), credentials MUST NOT leak through exception
 * messages, log lines, or `__toString()` output. This DTO
 * deliberately:
 *
 *   - Does NOT implement `__toString()` so a `(string) $credentials`
 *     coercion would raise rather than silently expose the values.
 *   - Does NOT expose a generic `toArray()` / `__debugInfo()`
 *     helper. The three named accessors are the only ways to read the values, and
 *     each one returns the field verbatim. Provider implementations
 *     read only the field needed for a specific query parameter or
 *     header rather than dumping the DTO.
 *   - Does NOT implement `Serializable` / PHP magic serialization
 *     hooks. Persistence of credentials is a Phase 8 admin-
 *     settings concern and goes through the dedicated WordPress
 *     options API, not through `serialize()` of this object.
 *
 * Construction validates each field is a non-empty trimmed string.
 * The DTO does not
 * verify the values against Rakuten's actual format requirements
 * (Application ID is a 19-digit number; Affiliate ID is a hex
 * string with dashes) because format drift is a deployment
 * concern: an admin who saved a malformed value should see a
 * loud authentication failure from the upstream API rather than
 * a silently-rejected save in Phase 8.
 *
 * Instances are immutable. There are no `with*()` mutation
 * helpers because credentials are configured once per
 * deployment and rotating them is a deliberate admin action
 * that produces a fresh DTO from a fresh settings save.
 *
 * @package Affilun\Infrastructure\Provider\Rakuten
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Provider\Rakuten;

use InvalidArgumentException;

/**
 * Immutable Rakuten Web Service credentials.
 *
 * See the file-level docblock for the mapping to
 * `provider-api-contracts.md` §9.2, the secret-material rule from
 * §10.1 that binds the type's accessor surface, and the rationale
 * for keeping format validation out of this DTO.
 */
final class RakutenCredentials {

	/**
	 * Rakuten Application ID per §9.2 "必須". Validated as a
	 * non-empty trimmed string at construction time.
	 */
	private string $applicationId;

	/**
	 * Rakuten Access Key for the current API authentication system.
	 */
	private string $accessKey;

	/**
	 * Rakuten Affiliate ID. Validated as a
	 * non-empty trimmed string at construction time.
	 */
	private string $affiliateId;

	/**
	 * Construct a Rakuten credentials snapshot.
	 *
	 * All required fields are trimmed at construction time and
	 * rejected when empty after trimming. The
	 * access key is deliberately modeled as required because the
	 * current API rejects requests that omit it.
	 *
	 * Format validation (Application ID must be a 19-digit number,
	 * Affiliate ID must match Rakuten's hex-with-dashes shape, and
	 * so on) is deliberately NOT performed here. See the file-level
	 * docblock for the rationale.
	 *
	 * @param string $applicationId Rakuten Application ID.
	 * @param string $accessKey     Rakuten Access Key.
	 * @param string $affiliateId   Rakuten Affiliate ID.
	 *
	 * @throws InvalidArgumentException When any credential is empty
	 *                                  after trimming.
	 */
	public function __construct(
		string $applicationId,
		string $accessKey,
		string $affiliateId
	) {
		$this->applicationId = self::normalizeRequired( 'applicationId', $applicationId );
		$this->accessKey     = self::normalizeRequired( 'accessKey', $accessKey );
		$this->affiliateId   = self::normalizeRequired( 'affiliateId', $affiliateId );
	}

	/**
	 * Rakuten Application ID per §9.2 "必須".
	 *
	 * Returned verbatim — the value is the canonical form supplied
	 * to the constructor, with surrounding whitespace already
	 * trimmed off. Provider implementations that build a request
	 * pass this directly to the `applicationId` query parameter
	 * via {@see \Affilun\Domain\Http\HttpRequest::withQueryParameter()}.
	 */
	public function applicationId(): string {
		return $this->applicationId;
	}

	/**
	 * Rakuten Access Key required by the current API.
	 */
	public function accessKey(): string {
		return $this->accessKey;
	}

	/**
	 * Rakuten Affiliate ID per §9.2 "必須".
	 *
	 * Returned verbatim. Provider implementations append this to
	 * affiliate-tracked URLs and to search-request query strings
	 * when the request shape supports it.
	 */
	public function affiliateId(): string {
		return $this->affiliateId;
	}

	/**
	 * Validate and normalize a required credential field at
	 * construction.
	 *
	 * Trims surrounding whitespace and rejects the result when it
	 * is the empty string. The error message names the field
	 * (`applicationId` / `accessKey` / `affiliateId`) so a misconfiguration
	 * surfaces with enough context to fix without dumping the
	 * raw value — per the §10.1 secret-material rule, the value
	 * itself MUST NOT appear in the message.
	 *
	 * @param string $field Field name for the error message.
	 * @param string $value Raw value as supplied by the caller.
	 *
	 * @throws InvalidArgumentException When `$value` is empty
	 *                                  after trimming.
	 */
	private static function normalizeRequired( string $field, string $value ): string {
		$trimmed = trim( $value );
		if ( $trimmed === '' ) {
			$message = sprintf(
				'RakutenCredentials %s must be a non-empty string.',
				$field
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		return $trimmed;
	}
}
