<?php
/**
 * Transients-backed {@see SearchCache} adapter.
 *
 * Phase 9 MVP implementation of the
 * {@see \Affilun\Application\Cache\SearchCache} port,
 * composing a {@see TransientsCacheStore} so the WordPress
 * transient-name hashing, envelope serialization, and TTL
 * eviction live in exactly one place across the four Phase 9
 * cache layers.
 *
 * Spec: `specifications/sync-and-cache-strategy.md` §4 (cache
 * layers), §4.2 (Object Cache / Transients as L2), and §6.3
 * (cache-key forms).
 *
 * Layer isolation is preserved by injecting a dedicated
 * {@see TransientsCacheStore} instance per layer; the wiring in
 * {@see \Affilun\Application\Cache\CacheServiceProvider}
 * follows that convention for symmetry with the in-memory
 * variants even though the transient backend has no per-layer
 * flush state to partition.
 *
 * @package Affilun\Infrastructure\Cache
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Cache;

use Affilun\Application\Cache\SearchCache;

/**
 * Transients-backed {@see SearchCache} adapter.
 *
 * See the file-level docblock for the spec citations and the
 * layer-isolation convention.
 */
final class TransientsSearchCache implements SearchCache {

	/**
	 * Backing generic transient store; carries the transient-name
	 * hashing, the envelope serialization, the clock, and the
	 * TTL eviction logic shared across cache layers.
	 */
	private TransientsCacheStore $store;

	/**
	 * Construct the adapter.
	 *
	 * @param TransientsCacheStore $store Backing transient store.
	 */
	public function __construct( TransientsCacheStore $store ) {
		$this->store = $store;
	}

	/**
	 * Read the cached search-result payload, or null on miss /
	 * expired / malformed envelope.
	 *
	 * @param string $key Cache key.
	 *
	 * @return array<string, mixed>|null
	 */
	public function get( string $key ): ?array {
		$entry = $this->store->getRaw( $key );
		if ( null === $entry ) {
			return null;
		}

		$value = $entry->value();
		if ( ! is_array( $value ) ) {
			// Defensive: a non-array value would only occur if the
			// underlying transient envelope were tampered with; treat
			// it as a miss rather than surfacing a typed payload that
			// violates the SearchCache::get() contract.
			return null;
		}

		/** @var array<string, mixed> $value */
		return $value;
	}

	/**
	 * Write the search-result payload with the given TTL in
	 * seconds. Delegates TTL validation to
	 * {@see TransientsCacheStore::putRaw()}.
	 *
	 * @param string               $key        Cache key.
	 * @param array<string, mixed> $payload    Normalized search result.
	 * @param int                  $ttlSeconds TTL in seconds; must be > 0.
	 */
	public function put( string $key, array $payload, int $ttlSeconds ): void {
		$this->store->putRaw( $key, $payload, $ttlSeconds );
	}

	/**
	 * Remove the entry stored under `$key`. Idempotent.
	 *
	 * @param string $key Cache key.
	 */
	public function forget( string $key ): void {
		$this->store->forget( $key );
	}

	/**
	 * Phase-9-MVP no-op (delegates to
	 * {@see TransientsCacheStore::flush()}, which documents why
	 * Transients cannot be enumerated without a `$wpdb`-backed
	 * sweep that this slice does not introduce).
	 */
	public function flush(): void {
		$this->store->flush();
	}
}
