<?php
/**
 * Button configuration input value object.
 *
 * Immutable input DTO consumed by
 * {@see \Affilun\Application\Render\Builder\LinkAttributeBuilder}
 * to translate domain-layer inputs (a ProductSnapshot, an
 * AffiliateLinkResult, a RenderContext) into a §10.7 ButtonView.
 *
 * Phase 5 deliberately does not yet model the complete §15
 * `ButtonConfig` shape: that type carries a much wider surface
 * (preset id, provider-specific copy, override CSS variables, ...)
 * than the link-attribute builder needs and lives one slice ahead
 * of this one. This DTO carries the minimal closed set of fields
 * the builder consumes today so the builder's signature does not
 * have to change when the full ButtonConfig lands:
 *
 *   - `label` is the §10.7 "ボタン属性 / label" pre-formatted
 *     button label ("Amazon", "楽天市場で見る"). Required,
 *     non-empty after trimming.
 *
 *   - `providerLabel` is the human-readable provider name the
 *     §10.7 "aria-label 例" template substitutes ("Amazon",
 *     "楽天市場"). Required, non-empty after trimming. Held
 *     separately from the lowercase provider key on the
 *     ProductSnapshot so the localization template can render
 *     "Sample を 楽天市場 で見る" rather than "Sample を rakuten
 *     で見る".
 *
 *   - `presetClassName` is the optional caller-supplied extra CSS
 *     class string the link-attribute builder appends after the
 *     canonical `affilun-button affilun-button-{provider}` tokens.
 *     Nullable because most call sites do not need it; non-null
 *     values are trimmed and rejected when the trim leaves an
 *     empty string — "present but empty" is treated as a
 *     programming error.
 *
 *   - `prefersTrackedUrl` is the boolean toggle that drives the
 *     §11 URL-selection branch: when true (the default), the
 *     link-attribute builder prefers the affiliate-tracked URL
 *     when the AffiliateLinkResult reports tracking was applied;
 *     when false, the builder always uses the snapshot's detail
 *     URL. The flag exists so REST mappers and admin previews can
 *     render the un-tracked detail URL without having to forge a
 *     fake AffiliateLinkResult.
 *
 * Instances are immutable and have no `with*()` mutation helpers
 * because a button config is composed once per render at the
 * ViewModel-factory boundary.
 *
 * @package Affilun\Application\Render\Builder
 */

declare(strict_types=1);

namespace Affilun\Application\Render\Builder;

use InvalidArgumentException;

/**
 * Immutable input DTO for {@see LinkAttributeBuilder}.
 *
 * See the file-level docblock for the rationale for carrying only
 * the minimal closed set of fields the §10.7 / §11
 * link-attribute builder actually consumes today.
 */
final class ButtonConfigInput {

	/**
	 * Pre-formatted button label.
	 */
	private string $label;

	/**
	 * Human-readable provider name.
	 */
	private string $providerLabel;

	/**
	 * Caller-supplied extra CSS class string, or null.
	 */
	private ?string $presetClassName;

	/**
	 * Whether the link-attribute builder should prefer the
	 * affiliate-tracked URL when tracking was applied.
	 */
	private bool $prefersTrackedUrl;

	/**
	 * Construct a config input with explicit values for every
	 * field.
	 *
	 * Validation summary:
	 *
	 *   - `label` and `providerLabel` are trimmed and rejected
	 *     when empty after trimming.
	 *   - `presetClassName`, when non-null, is trimmed and
	 *     rejected when the trim leaves an empty string.
	 *
	 * @param string      $label             Pre-formatted button label.
	 * @param string      $providerLabel     Human-readable provider name.
	 * @param string|null $presetClassName   Extra CSS class string, or null.
	 * @param bool        $prefersTrackedUrl Whether to prefer the tracked URL.
	 *
	 * @throws InvalidArgumentException When any field violates the
	 *                                  per-field rules summarized
	 *                                  above.
	 */
	public function __construct(
		string $label,
		string $providerLabel,
		?string $presetClassName = null,
		bool $prefersTrackedUrl = true
	) {
		$this->label             = self::normalizeRequiredString( 'label', $label );
		$this->providerLabel     = self::normalizeRequiredString( 'providerLabel', $providerLabel );
		$this->presetClassName   = self::normalizeOptionalString( 'presetClassName', $presetClassName );
		$this->prefersTrackedUrl = $prefersTrackedUrl;
	}

	/**
	 * Pre-formatted button label.
	 */
	public function label(): string {
		return $this->label;
	}

	/**
	 * Human-readable provider name.
	 */
	public function providerLabel(): string {
		return $this->providerLabel;
	}

	/**
	 * Caller-supplied extra CSS class string, or null.
	 */
	public function presetClassName(): ?string {
		return $this->presetClassName;
	}

	/**
	 * Whether the link-attribute builder should prefer the
	 * affiliate-tracked URL when tracking was applied.
	 */
	public function prefersTrackedUrl(): bool {
		return $this->prefersTrackedUrl;
	}

	/**
	 * Report whether this input equals the supplied one.
	 *
	 * Equality is structural: every field must match with strict
	 * comparison.
	 *
	 * @param self $other Other input to compare against.
	 */
	public function equals( self $other ): bool {
		return $this->label === $other->label
			&& $this->providerLabel === $other->providerLabel
			&& $this->presetClassName === $other->presetClassName
			&& $this->prefersTrackedUrl === $other->prefersTrackedUrl;
	}

	/**
	 * Validate and normalize a required string field at
	 * construction.
	 *
	 * @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 normalizeRequiredString( string $field, string $value ): string {
		$trimmed = trim( $value );
		if ( $trimmed === '' ) {
			$message = sprintf(
				'ButtonConfigInput %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;
	}

	/**
	 * Validate and normalize a nullable string field that, when
	 * non-null, must carry non-empty content.
	 *
	 * @param string      $field Field name for the error message.
	 * @param string|null $value Raw value, or null.
	 *
	 * @throws InvalidArgumentException When `$value` is the empty
	 *                                  string after trimming.
	 */
	private static function normalizeOptionalString( string $field, ?string $value ): ?string {
		if ( $value === null ) {
			return null;
		}

		$trimmed = trim( $value );
		if ( $trimmed === '' ) {
			$message = sprintf(
				'ButtonConfigInput %s must be null or a non-empty string.',
				$field
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		return $trimmed;
	}
}
