<?php
/**
 * Amazon discount shortcode renderer.
 *
 * @package Affilun\Presentation\Shortcode
 */

declare(strict_types=1);

namespace Affilun\Presentation\Shortcode;

use Affilun\Application\AmazonDiscount\AmazonDiscountBrand;
use Affilun\Application\AmazonDiscount\AmazonDiscountCategory;
use Affilun\Application\AmazonDiscount\AmazonDiscountProduct;
use Affilun\Application\AmazonDiscount\AmazonDiscountRepository;
use Affilun\Application\Render\Builder\ButtonConfigInput;
use Affilun\Application\Render\EmbedConfigInput;
use Affilun\Application\Render\ProductCardRenderer;
use Affilun\Application\Render\ViewModel\Parts\MetaFlags;
use Affilun\Application\Render\ViewModel\ProductCardViewModelFactory;
use Affilun\Application\Settings\PluginSettings;
use Affilun\Application\Settings\SettingsRepository;
use Affilun\Domain\Affiliate\AffiliateLinkResult;
use Affilun\Domain\Pricing\AvailabilityInfo;
use Affilun\Domain\Pricing\PriceInfo;
use Affilun\Domain\Product\ProductSnapshot;
use Affilun\Domain\Provider\ProviderProductId;
use Affilun\Domain\Render\RenderContext;
use Affilun\Presentation\Card\ProductCardVisualSettings;
use Throwable;

/**
 * Renders cached discounted products as grouped canonical affilun cards.
 */
final class AmazonDiscountShortcodeRenderer {

	/**
	 * @var array<string,string>
	 */
	public const SORT_LABELS = array(
		'discount_desc' => '割引率（高い順）',
		'discount_asc'  => '割引率（低い順）',
		'brand_asc'     => 'ブランド名（昇順）',
		'brand_desc'    => 'ブランド名（降順）',
		'title_asc'     => '商品名（昇順）',
		'title_desc'    => '商品名（降順）',
	);

	public const DEFAULT_SORT         = 'discount_desc';
	public const DEFAULT_PER_CATEGORY = 4;

	/**
	 * Create the grouped card renderer.
	 *
	 * @param AmazonDiscountRepository    $repository       Snapshot repository.
	 * @param ProductCardViewModelFactory $viewModelFactory Card view-model factory.
	 * @param ProductCardRenderer         $renderer         Standard card renderer.
	 * @param SettingsRepository|null     $settings         Saved plugin settings.
	 */
	public function __construct(
		private AmazonDiscountRepository $repository,
		private ProductCardViewModelFactory $viewModelFactory,
		private ProductCardRenderer $renderer,
		private ?SettingsRepository $settings = null
	) {
	}

	/**
	 * WordPress shortcode callback.
	 *
	 * @param mixed $attributes Raw shortcode attributes.
	 */
	public function renderShortcode( mixed $attributes ): string {
		return $this->render( is_array( $attributes ) ? $attributes : array(), false );
	}

	/**
	 * Render grouped products from normalized shortcode attributes.
	 *
	 * @param array<mixed> $attributes Shortcode-like attributes.
	 * @param bool         $preview    Whether to use editor render context.
	 */
	public function render( array $attributes, bool $preview ): string {
		$brands      = self::brandsFromAttributes( $attributes );
		$categories  = self::categoriesFromAttributes( $attributes );
		$sort        = self::sortFromAttributes( $attributes );
		$perCategory = self::perCategoryFromAttributes( $attributes );
		$byBrand     = $this->repository->findBrandsGrouped( $brands, $sort, $perCategory );
		$grouped     = $this->repository->findGrouped( $categories, $sort, $perCategory );
		$settings    = $this->settings !== null ? $this->settings->load() : PluginSettings::defaults();

		$html = '<section class="affilun-amazon-discounts">';
		$seen = 0;
		foreach ( $brands as $brand ) {
			$products = $byBrand[ $brand ] ?? array();
			if ( $products === array() ) {
				continue;
			}
			++$seen;
			$html .= $this->renderGroup(
				$brand,
				$products,
				'affilun-amazon-discounts__brand',
				$preview,
				$settings
			);
		}
		foreach ( $categories as $category ) {
			$products = $grouped[ $category ] ?? array();
			if ( $products === array() ) {
				continue;
			}
			++$seen;
			$html .= $this->renderGroup(
				AmazonDiscountCategory::label( $category ),
				$products,
				'affilun-amazon-discounts__category',
				$preview,
				$settings
			);
		}
		if ( $seen === 0 ) {
			$html .= '<p class="affilun-amazon-discounts__empty">'
				. self::escapeHtml( '現在表示できるAmazon割引商品はありません。日次取得後に表示されます。' )
				. '</p>';
		}
		$html .= '</section>';

		return $html;
	}

	/**
	 * @param array<mixed> $attributes Attributes.
	 *
	 * @return list<string>
	 */
	public static function brandsFromAttributes( array $attributes ): array {
		return AmazonDiscountBrand::parse( $attributes['brands'] ?? array() );
	}

	/**
	 * @param array<mixed> $attributes Attributes.
	 *
	 * @return list<string>
	 */
	public static function categoriesFromAttributes( array $attributes ): array {
		$value = $attributes['categories'] ?? null;
		if ( is_string( $value ) ) {
			$categories = AmazonDiscountCategory::normalize( array_map( 'trim', explode( ',', $value ) ) );
		} elseif ( is_array( $value ) ) {
			$categories = AmazonDiscountCategory::normalize( array_values( $value ) );
		} else {
			$categories = array();
		}
		return $categories === array() ? AmazonDiscountCategory::DEFAULT_SELECTION : $categories;
	}

	/**
	 * @param array<mixed> $attributes Attributes.
	 */
	public static function sortFromAttributes( array $attributes ): string {
		$sort = is_string( $attributes['sort'] ?? null ) ? trim( $attributes['sort'] ) : '';
		return array_key_exists( $sort, self::SORT_LABELS ) ? $sort : self::DEFAULT_SORT;
	}

	/**
	 * @param array<mixed> $attributes Attributes.
	 */
	public static function perCategoryFromAttributes( array $attributes ): int {
		$value = $attributes['per_category'] ?? $attributes['perCategory'] ?? self::DEFAULT_PER_CATEGORY;
		$count = is_numeric( $value ) ? (int) $value : self::DEFAULT_PER_CATEGORY;
		return max( 1, min( 20, $count ) );
	}

	/**
	 * Render one brand or category group.
	 *
	 * @param string                      $heading  Group heading.
	 * @param list<AmazonDiscountProduct> $products Stored products.
	 * @param string                      $groupClass Group CSS class.
	 * @param bool                        $preview  Whether to use editor context.
	 * @param PluginSettings              $settings Saved plugin settings.
	 */
	private function renderGroup(
		string $heading,
		array $products,
		string $groupClass,
		bool $preview,
		PluginSettings $settings
	): string {
		$html  = '<section class="' . self::escapeAttribute( $groupClass ) . '">';
		$html .= '<h2 class="affilun-amazon-discounts__heading">'
			. self::escapeHtml( $heading )
			. '</h2>';
		$html .= '<div class="affilun-amazon-discounts__grid">';
		foreach ( $products as $product ) {
			$html .= $this->renderProduct( $product, $preview, $settings );
		}
		return $html . '</div></section>';
	}

	/**
	 * Render one stored product through the canonical card pipeline.
	 *
	 * @param AmazonDiscountProduct $product Stored discount product.
	 * @param bool                  $preview Whether to use editor context.
	 * @param PluginSettings        $settings Saved plugin settings.
	 */
	private function renderProduct(
		AmazonDiscountProduct $product,
		bool $preview,
		PluginSettings $settings
	): string {
		try {
			$rendering = $settings->rendering();
			$template  = $rendering->defaultTemplate();
			$meta      = MetaFlags::defaults()
				->withShowBrand( true )
				->withShowDescription( true )
				->withShowPrice( true )
				->withShowProviderBadge( $rendering->showProviderBadgeByDefault() );
			$config    = new EmbedConfigInput(
				$template,
				array( new ButtonConfigInput( 'Amazonで見る', 'Amazon' ) ),
				$meta,
				ProductCardVisualSettings::rootClasses(
					$rendering,
					array( 'affilun-amazon-discount-card' )
				),
				false
			);
			$context   = $preview
				? RenderContext::editor( $template, 'ja-JP' )
				: RenderContext::frontend( $template, 'ja-JP' );
			$link      = new AffiliateLinkResult(
				$product->affiliateUrl(),
				array( 'nofollow', 'sponsored', 'noopener' ),
				'_blank',
				true,
				array()
			);
			$viewModel = $this->viewModelFactory->build(
				$this->snapshot( $product ),
				$config,
				$context,
				array( $link )
			);
			$html      = $this->renderer->render( $viewModel, $context );
			$html      = ProductCardVisualSettings::appendCardRootStyle(
				$html,
				ProductCardVisualSettings::providerButtonColorStyle( $settings, array( 'amazon' ) )
			);
			return $this->appendDiscountBadge( $html, $this->discountLabel( $product ) );
		} catch ( Throwable $error ) {
			if ( function_exists( 'error_log' ) ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Frontend render failure surface.
				error_log( 'AFFILUN Amazon discount card render failed: ' . $error->getMessage() );
			}
			return '<p class="affilun-amazon-discounts__card-error">'
				. self::escapeHtml( $product->title() )
				. '</p>';
		}
	}

	/**
	 * Add a fixed-style discount badge inside the canonical card body.
	 *
	 * @param string $html  Rendered card HTML.
	 * @param string $label Discount label.
	 */
	private function appendDiscountBadge( string $html, string $label ): string {
		$badge  = '<span class="affilun-amazon-discount-badge">'
			. self::escapeHtml( $label )
			. '</span>';
		$result = preg_replace(
			'/(<div\b[^>]*\bclass=("|\')[^"\']*\baffilun-card__body\b[^"\']*\2[^>]*>)/i',
			'$1' . $badge,
			$html,
			1
		);

		return is_string( $result ) ? $result : $html;
	}

	/**
	 * Format the stored discount percentage for the badge.
	 *
	 * @param AmazonDiscountProduct $product Stored discount product.
	 */
	private function discountLabel( AmazonDiscountProduct $product ): string {
		return $product->savingsPercentage() === null
			? sprintf( '%d%%以上 OFF', $product->minSavingPercent() )
			: sprintf( '%d%% OFF', $product->savingsPercentage() );
	}

	/**
	 * Convert a stored discount row to a standard product snapshot.
	 *
	 * @param AmazonDiscountProduct $product Stored discount product.
	 */
	private function snapshot( AmazonDiscountProduct $product ): ProductSnapshot {
		$price = $product->priceAmount() === null
			? null
			: new PriceInfo(
				$product->priceAmount(),
				$product->priceCurrency(),
				$product->priceLabel(),
				$product->fetchedAt(),
				false,
				false,
				null
			);
		return new ProductSnapshot(
			'amazon',
			new ProviderProductId( 'amazon', $product->asin() ),
			$product->title(),
			$product->brand(),
			null,
			$product->detailUrl(),
			$product->affiliateUrl(),
			$product->imageUrl(),
			array(),
			$price,
			AvailabilityInfo::unknown(),
			null,
			null,
			array( $product->categoryLabel() ),
			array( 'savings_percentage' => $product->savingsPercentage() ),
			$product->fetchedAt(),
			array(
				'search_index'       => $product->searchIndex(),
				'min_saving_percent' => $product->minSavingPercent(),
			)
		);
	}

	/**
	 * Escape visible text with a non-WordPress test fallback.
	 *
	 * @param string $value Raw text.
	 */
	private static function escapeHtml( string $value ): string {
		return function_exists( 'esc_html' )
			? esc_html( $value )
			: htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
	}

	/**
	 * Escape an HTML attribute with a non-WordPress test fallback.
	 *
	 * @param string $value Raw attribute.
	 */
	private static function escapeAttribute( string $value ): string {
		return function_exists( 'esc_attr' )
			? esc_attr( $value )
			: htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
	}
}
