<?php
/**
 * Article AI suggestion result.
 *
 * @package Affilun\Application\Ai
 */

declare(strict_types=1);

namespace Affilun\Application\Ai;

/**
 * Normalized AI suggestions for editor product-link assistance.
 */
final class ArticleSuggestionResult {

	/**
	 * @param string               $searchKeywords          Primary search keywords.
	 * @param list<string>         $candidates               Keyword candidates.
	 * @param list<string>         $microcopySuggestions     Microcopy suggestions.
	 * @param array<string,string> $buttonLabelSuggestions   Button labels by provider.
	 * @param string               $source                   Source token.
	 */
	public function __construct(
		private string $searchKeywords,
		private array $candidates,
		private array $microcopySuggestions,
		private array $buttonLabelSuggestions,
		private string $source
	) {}

	/**
	 * Build from array response.
	 *
	 * @param array<string,mixed> $data   Data.
	 * @param string              $source Source token.
	 */
	public static function fromArray( array $data, string $source ): self {
		$keywords   = isset( $data['search_keywords'] ) && is_scalar( $data['search_keywords'] )
			? trim( (string) $data['search_keywords'] )
			: '';
		$candidates = self::stringList( $data['candidates'] ?? array(), 5, 120, $keywords );
		$microcopy  = self::stringList( $data['microcopy_suggestions'] ?? array(), 3, 80 );
		$labels     = self::buttonLabels( $data['button_label_suggestions'] ?? array() );

		return new self( $keywords, $candidates, $microcopy, $labels, $source );
	}

	/**
	 * Primary keywords.
	 */
	public function searchKeywords(): string {
		return $this->searchKeywords;
	}

	/**
	 * @return list<string>
	 */
	public function candidates(): array {
		return $this->candidates;
	}

	/**
	 * @return list<string>
	 */
	public function microcopySuggestions(): array {
		return $this->microcopySuggestions;
	}

	/**
	 * @return array<string,string>
	 */
	public function buttonLabelSuggestions(): array {
		return $this->buttonLabelSuggestions;
	}

	/**
	 * Source token.
	 */
	public function source(): string {
		return $this->source;
	}

	/**
	 * @return array<string,mixed>
	 */
	public function toResponseData(): array {
		return array(
			'search_keywords'          => $this->searchKeywords,
			'candidates'               => $this->candidates,
			'microcopy_suggestions'    => $this->microcopySuggestions,
			'button_label_suggestions' => $this->buttonLabelSuggestions,
			'source'                   => $this->source,
		);
	}

	/**
	 * Normalize a string list.
	 *
	 * @param mixed       $raw       Raw list.
	 * @param int         $limit     Maximum items.
	 * @param int         $maxLength Maximum item length.
	 * @param string|null $fallback  Optional fallback item.
	 *
	 * @return list<string>
	 */
	private static function stringList( mixed $raw, int $limit, int $maxLength, ?string $fallback = null ): array {
		$list = array();
		if ( is_array( $raw ) ) {
			foreach ( $raw as $value ) {
				if ( is_array( $value ) ) {
					$value = $value['keyword'] ?? $value['value'] ?? $value['label'] ?? null;
				}
				if ( ! is_scalar( $value ) ) {
					continue;
				}
				$item = self::truncate( trim( (string) $value ), $maxLength );
				if ( $item !== '' && ! in_array( $item, $list, true ) ) {
					$list[] = $item;
				}
				if ( count( $list ) >= $limit ) {
					break;
				}
			}
		}
		if ( $list === array() && $fallback !== null && trim( $fallback ) !== '' ) {
			$list[] = self::truncate( trim( $fallback ), $maxLength );
		}
		return $list;
	}

	/**
	 * Normalize button labels.
	 *
	 * @param mixed $raw Raw label map.
	 *
	 * @return array<string,string>
	 */
	private static function buttonLabels( mixed $raw ): array {
		$allowed = array( 'amazon', 'rakuten', 'yahoo_shopping', 'mercari' );
		$result  = array();
		if ( ! is_array( $raw ) ) {
			return $result;
		}
		foreach ( $allowed as $key ) {
			$value = $raw[ $key ] ?? null;
			if ( is_array( $value ) ) {
				$value = self::firstScalar( $value );
			}
			if ( is_scalar( $value ) ) {
				$label = self::truncate( trim( (string) $value ), 40 );
				if ( $label !== '' ) {
					$result[ $key ] = $label;
				}
			}
		}
		return $result;
	}

	/**
	 * Return the first scalar value in a list-like array.
	 *
	 * @param array<mixed> $values Candidate values.
	 */
	private static function firstScalar( array $values ): mixed {
		foreach ( $values as $value ) {
			if ( is_scalar( $value ) ) {
				return $value;
			}
		}
		return null;
	}

	/**
	 * Truncate text.
	 *
	 * @param string $value     Text.
	 * @param int    $maxLength Max length.
	 */
	private static function truncate( string $value, int $maxLength ): string {
		if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) {
			return mb_strlen( $value ) > $maxLength ? mb_substr( $value, 0, $maxLength ) : $value;
		}
		return strlen( $value ) > $maxLength ? substr( $value, 0, $maxLength ) : $value;
	}
}
