<?php
/**
 * In-memory Amazon discount repository.
 *
 * @package Affilun\Infrastructure\Persistence
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence;

use Affilun\Application\AmazonDiscount\AmazonDiscountBrand;
use Affilun\Application\AmazonDiscount\AmazonDiscountProduct;
use Affilun\Application\AmazonDiscount\AmazonDiscountRepository;
use DateTimeImmutable;

/**
 * Test-safe repository used when WordPress database access is unavailable.
 */
final class InMemoryAmazonDiscountRepository implements AmazonDiscountRepository {

	/**
	 * @var array<string,list<AmazonDiscountProduct>>
	 */
	private array $rows = array();

	/**
	 * Replace one category snapshot.
	 *
	 * @param string                      $searchIndex Amazon SearchIndex.
	 * @param list<AmazonDiscountProduct> $products    Products in upstream order.
	 */
	public function replaceCategory( string $searchIndex, array $products ): int {
		$this->rows[ $searchIndex ] = array_values( $products );
		return count( $products );
	}

	/**
	 * Replace one brand snapshot.
	 *
	 * @param string                      $brand    Priority brand.
	 * @param list<AmazonDiscountProduct> $products Products in upstream order.
	 */
	#[\Override]
	public function replaceBrand( string $brand, array $products ): int {
		$key                = AmazonDiscountBrand::storageKey( $brand );
		$this->rows[ $key ] = array_values( $products );
		return count( $products );
	}

	/**
	 * Delete one brand snapshot.
	 *
	 * @param string $brand Priority brand.
	 */
	#[\Override]
	public function deleteBrand( string $brand ): void {
		unset( $this->rows[ AmazonDiscountBrand::storageKey( $brand ) ] );
	}

	/**
	 * Load sorted rows by category.
	 *
	 * @param list<string> $categories  Category order.
	 * @param string       $sort        Closed sort token.
	 * @param int          $perCategory Maximum rows per category.
	 *
	 * @return array<string,list<AmazonDiscountProduct>>
	 */
	public function findGrouped( array $categories, string $sort, int $perCategory ): array {
		return $this->findCollections( $categories, $sort, $perCategory );
	}

	/**
	 * Load sorted rows by brand.
	 *
	 * @param list<string> $brands   Brand order.
	 * @param string       $sort     Closed sort token.
	 * @param int          $perBrand Maximum rows per brand.
	 *
	 * @return array<string,list<AmazonDiscountProduct>>
	 */
	#[\Override]
	public function findBrandsGrouped( array $brands, string $sort, int $perBrand ): array {
		$grouped = array();
		foreach ( AmazonDiscountBrand::normalize( $brands ) as $brand ) {
			$rows = $this->rows[ AmazonDiscountBrand::storageKey( $brand ) ] ?? array();
			usort( $rows, self::comparator( $sort ) );
			$grouped[ $brand ] = array_slice( $rows, 0, $perBrand );
		}
		return $grouped;
	}

	/**
	 * Load sorted rows by stored collection key.
	 *
	 * @param list<string> $keys  Collection keys.
	 * @param string       $sort  Closed sort token.
	 * @param int          $limit Maximum rows per collection.
	 *
	 * @return array<string,list<AmazonDiscountProduct>>
	 */
	private function findCollections( array $keys, string $sort, int $limit ): array {
		$grouped = array();
		foreach ( $keys as $key ) {
			$rows = $this->rows[ $key ] ?? array();
			usort( $rows, self::comparator( $sort ) );
			$grouped[ $key ] = array_slice( $rows, 0, $limit );
		}
		return $grouped;
	}

	/**
	 * Count all stored rows.
	 */
	public function count(): int {
		return array_sum( array_map( 'count', $this->rows ) );
	}

	/**
	 * Return the newest fetch timestamp.
	 */
	public function latestFetchedAt(): ?DateTimeImmutable {
		$latest = null;
		foreach ( $this->rows as $rows ) {
			foreach ( $rows as $row ) {
				if ( $latest === null || $row->fetchedAt() > $latest ) {
					$latest = $row->fetchedAt();
				}
			}
		}
		return $latest;
	}

	/**
	 * Build the comparator for a closed sort token.
	 *
	 * @param string $sort Sort token.
	 *
	 * @return callable(AmazonDiscountProduct,AmazonDiscountProduct):int
	 */
	private static function comparator( string $sort ): callable {
		return static function ( AmazonDiscountProduct $left, AmazonDiscountProduct $right ) use ( $sort ): int {
			return match ( $sort ) {
				'discount_asc' => ( $left->savingsPercentage() ?? 101 ) <=> ( $right->savingsPercentage() ?? 101 ),
				'brand_asc' => self::compareNullableBrand( $left, $right, false ),
				'brand_desc' => self::compareNullableBrand( $left, $right, true ),
				'title_asc' => strnatcasecmp( $left->title(), $right->title() ),
				'title_desc' => strnatcasecmp( $right->title(), $left->title() ),
				default => ( $right->savingsPercentage() ?? -1 ) <=> ( $left->savingsPercentage() ?? -1 ),
			};
		};
	}

	/**
	 * Compare brands while keeping missing brands last.
	 *
	 * @param AmazonDiscountProduct $left       Left row.
	 * @param AmazonDiscountProduct $right      Right row.
	 * @param bool                  $descending Reverse non-null values.
	 */
	private static function compareNullableBrand(
		AmazonDiscountProduct $left,
		AmazonDiscountProduct $right,
		bool $descending
	): int {
		if ( $left->brand() === null || $right->brand() === null ) {
			return (int) ( $left->brand() === null ) <=> (int) ( $right->brand() === null );
		}
		return $descending
			? strnatcasecmp( $right->brand(), $left->brand() )
			: strnatcasecmp( $left->brand(), $right->brand() );
	}
}
