<?php
/**
 * Disclaimer builder.
 *
 * Pure-function builder that produces the pre-formatted
 * `list<string>` of disclaimer messages a §8.2
 * ProductCardViewModel surfaces under its `disclaimers` slot. The
 * mapping follows `specifications/rendering-and-template-spec.md`
 * §10.8 (price disclaimer obligation), §13.3 (graceful
 * degradation: missing-data branches MUST NOT raise), and §23
 * (administrator vs. end-user-facing messaging boundary).
 *
 * Ordering and content rules:
 *
 *   - When a {@see PriceInfo} is supplied and reports
 *     `disclaimerRequired() === true`, a price-disclaimer line is
 *     prepended. The provider-supplied
 *     {@see PriceInfo::disclaimerText()} is preferred verbatim;
 *     when null, a CLB-canonical message is selected by locale:
 *
 *       * `ja-*` (and the unspecified-locale default) →
 *         "価格および在庫情報は取得時点の情報です。"
 *       * any non-`ja-*` locale →
 *         "Price and availability are accurate as of the
 *         observation time."
 *
 *   - When the {@see RenderContext::trackingEnabled()} flag is
 *     true (i.e. the surface actually emits affiliate-tracked
 *     URLs), an affiliate-disclosure line is appended after the
 *     price line so the order on screen reads
 *     [price disclosure, sponsored-link disclosure]:
 *
 *       * `ja-*` → "※本リンクはアフィリエイトです。Created by clb.rio.st"
 *       * non-`ja-*` → "This page contains affiliate links. Created by clb.rio.st"
 *
 *   - Entries are deduplicated case-sensitively (preserving
 *     first-seen order) so a caller that supplies a price
 *     disclaimer text identical to the sponsored-link disclosure
 *     does not surface the same line twice.
 *
 *   - Each entry is trimmed; entries that become empty after
 *     trimming are silently skipped per §13.3.
 *
 *   - The result is always a `list<string>` (sequential int keys
 *     starting at 0). The empty list is a valid outcome (no price
 *     disclaimer required, tracking disabled).
 *
 * Instances carry no state and the {@see self::build()} method is
 * pure: same input, same output, no I/O, no globals.
 *
 * @package CommerceLinkBuilder\Application\Render\Builder
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Application\Render\Builder;

use CommerceLinkBuilder\Domain\Pricing\PriceInfo;
use CommerceLinkBuilder\Domain\Product\ProductSnapshot;
use CommerceLinkBuilder\Domain\Render\RenderContext;

/**
 * Pure §10.8 / §23 disclaimer-line builder.
 *
 * See the file-level docblock for the mapping to
 * `rendering-and-template-spec.md` §10.8 / §13.3 / §23 and the
 * locale-driven canonical-message selection.
 */
final class DisclaimerBuilder {

	/**
	 * §10.8 CLB-canonical price disclaimer for Japanese locales.
	 */
	private const PRICE_FALLBACK_JA = '価格および在庫情報は取得時点の情報です。';

	/**
	 * §10.8 CLB-canonical price disclaimer for non-Japanese
	 * locales.
	 */
	private const PRICE_FALLBACK_EN = 'Price and availability are accurate as of the observation time.';

	/**
	 * §23 sponsored-link disclosure for Japanese locales.
	 */
	private const SPONSORED_JA = '※本リンクはアフィリエイトです。Created by clb.rio.st';

	/**
	 * §23 sponsored-link disclosure for non-Japanese locales.
	 */
	private const SPONSORED_EN = 'This page contains affiliate links. Created by clb.rio.st';

	/**
	 * Build the §8.2 `disclaimers` slot value for a single card.
	 *
	 * @param ProductSnapshot $snapshot Domain product snapshot.
	 * @param PriceInfo|null  $price    Price snapshot, or null.
	 * @param RenderContext   $context  Active render context.
	 *
	 * @return list<string>
	 *
	 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
	 */
	public function build(
		ProductSnapshot $snapshot,
		?PriceInfo $price,
		RenderContext $context
	): array {
		unset( $snapshot );

		$lines = array();

		if ( $price !== null && $price->disclaimerRequired() ) {
			$provided = $price->disclaimerText();
			if ( $provided !== null ) {
				$lines[] = $provided;
			} else {
				$lines[] = self::isJapaneseLocale( $context->locale() )
					? self::PRICE_FALLBACK_JA
					: self::PRICE_FALLBACK_EN;
			}
		}

		if ( $context->trackingEnabled() ) {
			$lines[] = self::isJapaneseLocale( $context->locale() )
				? self::SPONSORED_JA
				: self::SPONSORED_EN;
		}

		$seen   = array();
		$result = array();
		foreach ( $lines as $line ) {
			$trimmed = trim( $line );
			if ( $trimmed === '' ) {
				continue;
			}

			if ( isset( $seen[ $trimmed ] ) ) {
				continue;
			}

			$seen[ $trimmed ] = true;
			$result[]         = $trimmed;
		}

		return $result;
	}

	/**
	 * Test whether the supplied locale tag selects the Japanese
	 * canonical-disclaimer template.
	 *
	 * Null and empty locale tags select Japanese (the plugin's
	 * default audience); explicit `ja` / `ja-*` / `ja_*` tags
	 * also select Japanese; everything else selects English.
	 *
	 * @param string|null $locale BCP-47 locale tag, or null.
	 */
	private static function isJapaneseLocale( ?string $locale ): bool {
		if ( $locale === null ) {
			return true;
		}

		$normalized = strtolower( trim( $locale ) );
		if ( $normalized === '' ) {
			return true;
		}

		if ( $normalized === 'ja' ) {
			return true;
		}

		return strpos( $normalized, 'ja-' ) === 0
			|| strpos( $normalized, 'ja_' ) === 0;
	}
}
