<?php
/**
 * Link defaults value object.
 *
 * Immutable Phase-8 value object that carries the §13.6 Links
 * tab settings of `specifications/admin-ui-spec.md`. The fields
 * mirror the tab's UI controls one-to-one:
 *
 *   - `openInNewTabDefault` is the default value of the §13.6
 *     "Open in New Tab default" toggle. When true, freshly-
 *     created embeds emit `target="_blank"` on their primary
 *     link unless the embed config overrides the toggle.
 *
 *   - `addNofollow` / `addSponsored` / `addNoopener` /
 *     `addNoreferrer` are the four `rel`-attribute axes the
 *     §13.6 Links tab exposes. They are independent so an
 *     admin can opt out of any one without disturbing the
 *     others; the renderer concatenates the enabled tokens
 *     into the final `rel` attribute.
 *
 *   - `trackingEnabledDefault` is the default value of the
 *     §13.6 "Tracking Enabled default" toggle that drives the
 *     affiliate-tracking parameter injection at link-mint
 *     time.
 *
 * Instances are immutable; per-field withers return fresh
 * instances. Persistence shape is canonicalized by
 * {@see self::toArray()} / {@see self::fromArray()}.
 *
 * @package CommerceLinkBuilder\Application\Settings
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Application\Settings;

use InvalidArgumentException;

/**
 * Immutable §13.6 Links tab settings bundle.
 *
 * See the file-level docblock for the field-by-field mapping
 * to the §13.6 admin-ui controls.
 */
final class LinkDefaults {

	/**
	 * Default value of the "Open in New Tab" toggle.
	 */
	private bool $openInNewTabDefault;

	/**
	 * Whether `nofollow` is appended to the rel attribute.
	 */
	private bool $addNofollow;

	/**
	 * Whether `sponsored` is appended to the rel attribute.
	 */
	private bool $addSponsored;

	/**
	 * Whether `noopener` is appended to the rel attribute.
	 */
	private bool $addNoopener;

	/**
	 * Whether `noreferrer` is appended to the rel attribute.
	 */
	private bool $addNoreferrer;

	/**
	 * Default value of the "Tracking Enabled" toggle.
	 */
	private bool $trackingEnabledDefault;

	/**
	 * Construct a link-defaults snapshot with explicit values
	 * for every field.
	 *
	 * Every axis is a plain bool; no validation beyond PHP's
	 * native typing is performed because every combination is
	 * meaningful.
	 *
	 * @param bool $openInNewTabDefault    Default new-tab toggle.
	 * @param bool $addNofollow            Whether `nofollow` is appended.
	 * @param bool $addSponsored           Whether `sponsored` is appended.
	 * @param bool $addNoopener            Whether `noopener` is appended.
	 * @param bool $addNoreferrer          Whether `noreferrer` is appended.
	 * @param bool $trackingEnabledDefault Default tracking-enabled toggle.
	 */
	public function __construct(
		bool $openInNewTabDefault,
		bool $addNofollow,
		bool $addSponsored,
		bool $addNoopener,
		bool $addNoreferrer,
		bool $trackingEnabledDefault
	) {
		$this->openInNewTabDefault    = $openInNewTabDefault;
		$this->addNofollow            = $addNofollow;
		$this->addSponsored           = $addSponsored;
		$this->addNoopener            = $addNoopener;
		$this->addNoreferrer          = $addNoreferrer;
		$this->trackingEnabledDefault = $trackingEnabledDefault;
	}

	/**
	 * Construct a link-defaults snapshot with the canonical
	 * §13.6 baseline values: every flag enabled.
	 */
	public static function defaults(): self {
		return new self( true, true, true, true, true, true );
	}

	/**
	 * Default value of the "Open in New Tab" toggle.
	 */
	public function openInNewTabDefault(): bool {
		return $this->openInNewTabDefault;
	}

	/**
	 * Whether `nofollow` is appended to the rel attribute.
	 */
	public function addNofollow(): bool {
		return $this->addNofollow;
	}

	/**
	 * Whether `sponsored` is appended to the rel attribute.
	 */
	public function addSponsored(): bool {
		return $this->addSponsored;
	}

	/**
	 * Whether `noopener` is appended to the rel attribute.
	 */
	public function addNoopener(): bool {
		return $this->addNoopener;
	}

	/**
	 * Whether `noreferrer` is appended to the rel attribute.
	 */
	public function addNoreferrer(): bool {
		return $this->addNoreferrer;
	}

	/**
	 * Default value of the "Tracking Enabled" toggle.
	 */
	public function trackingEnabledDefault(): bool {
		return $this->trackingEnabledDefault;
	}

	/**
	 * Produce a copy with `openInNewTabDefault` replaced.
	 *
	 * @param bool $openInNewTabDefault New toggle.
	 */
	public function withOpenInNewTabDefault( bool $openInNewTabDefault ): self {
		$clone                      = clone $this;
		$clone->openInNewTabDefault = $openInNewTabDefault;
		return $clone;
	}

	/**
	 * Produce a copy with `addNofollow` replaced.
	 *
	 * @param bool $addNofollow New flag.
	 */
	public function withAddNofollow( bool $addNofollow ): self {
		$clone              = clone $this;
		$clone->addNofollow = $addNofollow;
		return $clone;
	}

	/**
	 * Produce a copy with `addSponsored` replaced.
	 *
	 * @param bool $addSponsored New flag.
	 */
	public function withAddSponsored( bool $addSponsored ): self {
		$clone               = clone $this;
		$clone->addSponsored = $addSponsored;
		return $clone;
	}

	/**
	 * Produce a copy with `addNoopener` replaced.
	 *
	 * @param bool $addNoopener New flag.
	 */
	public function withAddNoopener( bool $addNoopener ): self {
		$clone              = clone $this;
		$clone->addNoopener = $addNoopener;
		return $clone;
	}

	/**
	 * Produce a copy with `addNoreferrer` replaced.
	 *
	 * @param bool $addNoreferrer New flag.
	 */
	public function withAddNoreferrer( bool $addNoreferrer ): self {
		$clone                = clone $this;
		$clone->addNoreferrer = $addNoreferrer;
		return $clone;
	}

	/**
	 * Produce a copy with `trackingEnabledDefault` replaced.
	 *
	 * @param bool $trackingEnabledDefault New toggle.
	 */
	public function withTrackingEnabledDefault( bool $trackingEnabledDefault ): self {
		$clone                         = clone $this;
		$clone->trackingEnabledDefault = $trackingEnabledDefault;
		return $clone;
	}

	/**
	 * Report whether this snapshot equals the supplied one.
	 *
	 * Equality is structural: every flag must match under
	 * strict comparison.
	 *
	 * @param self $other Other snapshot to compare against.
	 */
	public function equals( self $other ): bool {
		return $this->openInNewTabDefault === $other->openInNewTabDefault
			&& $this->addNofollow === $other->addNofollow
			&& $this->addSponsored === $other->addSponsored
			&& $this->addNoopener === $other->addNoopener
			&& $this->addNoreferrer === $other->addNoreferrer
			&& $this->trackingEnabledDefault === $other->trackingEnabledDefault;
	}

	/**
	 * Serialize this snapshot for OptionStore persistence.
	 *
	 * @return array<string, mixed>
	 */
	public function toArray(): array {
		return array(
			'open_in_new_tab_default'  => $this->openInNewTabDefault,
			'add_nofollow'             => $this->addNofollow,
			'add_sponsored'            => $this->addSponsored,
			'add_noopener'             => $this->addNoopener,
			'add_noreferrer'           => $this->addNoreferrer,
			'tracking_enabled_default' => $this->trackingEnabledDefault,
		);
	}

	/**
	 * Construct a snapshot from a {@see self::toArray()} payload.
	 *
	 * @param array<string, mixed> $data Persisted payload.
	 *
	 * @throws InvalidArgumentException When `$data` is malformed.
	 */
	public static function fromArray( array $data ): self {
		return new self(
			self::requireBool( $data, 'open_in_new_tab_default' ),
			self::requireBool( $data, 'add_nofollow' ),
			self::requireBool( $data, 'add_sponsored' ),
			self::requireBool( $data, 'add_noopener' ),
			self::requireBool( $data, 'add_noreferrer' ),
			self::requireBool( $data, 'tracking_enabled_default' )
		);
	}

	/**
	 * Read a required bool field from a {@see self::fromArray()}
	 * payload.
	 *
	 * @param array<string, mixed> $data Persisted payload.
	 * @param string               $key  Field name.
	 *
	 * @throws InvalidArgumentException When the field is missing
	 *                                  or not a bool.
	 */
	private static function requireBool( array $data, string $key ): bool {
		if ( ! array_key_exists( $key, $data ) || ! is_bool( $data[ $key ] ) ) {
			$message = sprintf(
				'LinkDefaults payload must carry a bool `%s`.',
				$key
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		return $data[ $key ];
	}
}
