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

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing exception messages are never rendered as HTML.

use InvalidArgumentException;
use RuntimeException;

/**
 * Array-backed {@see ProductUrlRepository} implementation for tests.
 */
final class InMemoryProductUrlRepository implements ProductUrlRepository {

	/** @var array<int, ProductUrlRecord> */
	private array $records = array();

	/** @var array<string,int> */
	private array $productProviderUrlIndex = array();

	/**
	 * Next synthetic database id.
	 */
	private int $nextId = 1;

	/**
	 * @param list<ProductUrlRecord> $seed Seed records.
	 */
	public function __construct( array $seed = array() ) {
		foreach ( $seed as $record ) {
			$id = $record->id();
			if ( $id === null ) {
				throw new InvalidArgumentException( 'InMemoryProductUrlRepository seed records must carry an id.' );
			}
			if ( array_key_exists( $id, $this->records ) ) {
				throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository seed contains duplicate id %d.', $id ) );
			}
			$key = self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() );
			if ( array_key_exists( $key, $this->productProviderUrlIndex ) ) {
				throw new InvalidArgumentException( 'InMemoryProductUrlRepository seed contains duplicate product/provider/url.' );
			}
			$this->records[ $id ]                  = $record;
			$this->productProviderUrlIndex[ $key ] = $id;
			if ( $id >= $this->nextId ) {
				$this->nextId = $id + 1;
			}
		}
	}

	/**
	 * Persist a product URL record.
	 *
	 * @param ProductUrlRecord $record URL record.
	 */
	public function save( ProductUrlRecord $record ): ProductUrlRecord {
		$id = $record->id();
		return $id === null ? $this->insert( $record ) : $this->update( $id, $record );
	}

	/**
	 * Insert a new URL record.
	 *
	 * @param ProductUrlRecord $record Unsaved URL record.
	 */
	private function insert( ProductUrlRecord $record ): ProductUrlRecord {
		$key = self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() );
		if ( array_key_exists( $key, $this->productProviderUrlIndex ) ) {
			throw new RuntimeException( 'InMemoryProductUrlRepository: duplicate product/provider/url on insert.' );
		}
		$id                                    = $this->nextId++;
		$persisted                             = $record->withId( $id );
		$this->records[ $id ]                  = $persisted;
		$this->productProviderUrlIndex[ $key ] = $id;
		return $persisted;
	}

	/**
	 * Update an existing URL record.
	 *
	 * @param int              $id     Database row id.
	 * @param ProductUrlRecord $record URL record.
	 */
	private function update( int $id, ProductUrlRecord $record ): ProductUrlRecord {
		if ( ! array_key_exists( $id, $this->records ) ) {
			throw new RuntimeException( sprintf( 'InMemoryProductUrlRepository: cannot update missing id %d.', $id ) );
		}
		$old    = $this->records[ $id ];
		$oldKey = self::productProviderUrlKey( $old->productId(), $old->providerType(), $old->urlHash() );
		$newKey = self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() );
		if ( $oldKey !== $newKey && array_key_exists( $newKey, $this->productProviderUrlIndex ) ) {
			throw new RuntimeException( 'InMemoryProductUrlRepository: update would collide with an existing product/provider/url.' );
		}
		if ( $oldKey !== $newKey ) {
			unset( $this->productProviderUrlIndex[ $oldKey ] );
			$this->productProviderUrlIndex[ $newKey ] = $id;
		}
		$this->records[ $id ] = $record;
		return $record;
	}

	/**
	 * Find a URL record by database id.
	 *
	 * @param int $id Database row id.
	 */
	public function findById( int $id ): ?ProductUrlRecord {
		if ( $id <= 0 ) {
			throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::findById() requires a positive id, got %d.', $id ) );
		}
		return $this->records[ $id ] ?? null;
	}

	/**
	 * Find URL records attached to a product.
	 *
	 * @param int $productId Product id.
	 *
	 * @return list<ProductUrlRecord>
	 */
	public function findByProductId( int $productId ): array {
		if ( $productId <= 0 ) {
			throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::findByProductId() requires a positive productId, got %d.', $productId ) );
		}
		$result = array();
		foreach ( $this->records as $record ) {
			if ( $record->productId() === $productId ) {
				$result[] = $record;
			}
		}
		usort( $result, static fn ( ProductUrlRecord $a, ProductUrlRecord $b ): int => ( $a->id() ?? 0 ) <=> ( $b->id() ?? 0 ) );
		return $result;
	}

	/**
	 * Find a product URL row by product, provider, and URL hash.
	 *
	 * @param int    $productId    Product id.
	 * @param string $providerType Provider namespace.
	 * @param string $urlHash      Normalized URL hash.
	 */
	public function findByProductProviderUrl( int $productId, string $providerType, string $urlHash ): ?ProductUrlRecord {
		if ( $productId <= 0 ) {
			throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::findByProductProviderUrl() requires a positive productId, got %d.', $productId ) );
		}
		$key = self::productProviderUrlKey( $productId, $providerType, $urlHash );
		$id  = $this->productProviderUrlIndex[ $key ] ?? null;
		return $id === null ? null : ( $this->records[ $id ] ?? null );
	}

	/**
	 * Find a URL row by provider-side item id.
	 *
	 * @param string $providerType   Provider namespace.
	 * @param string $providerItemId Provider-side item identifier.
	 */
	public function findByProviderItem( string $providerType, string $providerItemId ): ?ProductUrlRecord {
		if ( trim( $providerType ) === '' || trim( $providerItemId ) === '' ) {
			throw new InvalidArgumentException( 'InMemoryProductUrlRepository::findByProviderItem() requires non-empty providerType and providerItemId.' );
		}
		foreach ( $this->records as $record ) {
			if ( $record->providerType() === $providerType && $record->providerItemId() === $providerItemId ) {
				return $record;
			}
		}
		return null;
	}

	/**
	 * Delete a URL row by database id.
	 *
	 * @param int $id Database row id.
	 */
	public function delete( int $id ): bool {
		if ( $id <= 0 ) {
			throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::delete() requires a positive id, got %d.', $id ) );
		}
		if ( ! array_key_exists( $id, $this->records ) ) {
			return false;
		}
		$record = $this->records[ $id ];
		unset( $this->records[ $id ] );
		unset( $this->productProviderUrlIndex[ self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() ) ] );
		return true;
	}

	/**
	 * Delete URL rows for a product/provider pair.
	 *
	 * @param int    $productId    Product id.
	 * @param string $providerType Provider namespace.
	 */
	public function deleteByProductAndProvider( int $productId, string $providerType ): int {
		if ( $productId <= 0 ) {
			throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::deleteByProductAndProvider() requires a positive productId, got %d.', $productId ) );
		}
		if ( trim( $providerType ) === '' ) {
			throw new InvalidArgumentException( 'InMemoryProductUrlRepository::deleteByProductAndProvider() requires a non-empty providerType.' );
		}
		$count = 0;
		foreach ( array_keys( $this->records ) as $id ) {
			$record = $this->records[ $id ];
			if ( $record->productId() === $productId && $record->providerType() === $providerType ) {
				$this->delete( $id );
				++$count;
			}
		}
		return $count;
	}

	/**
	 * Count stored URL rows.
	 */
	public function count(): int {
		return count( $this->records );
	}

	/**
	 * Build the in-memory unique-key index value.
	 *
	 * @param int    $productId    Product id.
	 * @param string $providerType Provider namespace.
	 * @param string $urlHash      Normalized URL hash.
	 */
	private static function productProviderUrlKey( int $productId, string $providerType, string $urlHash ): string {
		return $productId . "\0" . $providerType . "\0" . $urlHash;
	}
}
