<?php
/**
 * In-memory {@see RenderCache} adapter.
 *
 * Phase 9 in-memory implementation of the
 * {@see \CommerceLinkBuilder\Application\Cache\RenderCache} port,
 * composing a shared {@see InMemoryCacheStore} backend so the
 * eviction / flush / TTL machinery is implemented exactly once
 * across the four Phase 9 cache layers.
 *
 * Spec: `specifications/sync-and-cache-strategy.md` §4 (cache
 * layers), §6.3 (cache-key forms), and §14 (cache invalidation,
 * including the prefix-wildcard sweep that
 * {@see self::forgetByPrefix()} supports).
 *
 * The adapter stores each entry as a two-key associative array
 * `['view_model' => array, 'html' => string]` per the
 * {@see \CommerceLinkBuilder\Application\Cache\RenderCache::get()}
 * contract; on hit the same shape is returned verbatim.
 *
 * The adapter is layer-isolated: callers are expected to inject a
 * dedicated {@see InMemoryCacheStore} instance per layer so a
 * flush of the render layer does not also drop entries belonging
 * to the search, lookup, or provider-health layers. Sharing a
 * single store across multiple adapters is permitted but is the
 * caller's responsibility.
 *
 * @package CommerceLinkBuilder\Infrastructure\Cache
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Infrastructure\Cache;

use CommerceLinkBuilder\Application\Cache\RenderCache;

/**
 * In-memory {@see RenderCache} backed by {@see InMemoryCacheStore}.
 *
 * See the file-level docblock for the entry shape, the
 * layer-isolation rule, and the §14 prefix-wildcard invalidation
 * hook used by the `CacheInvalidator`.
 */
final class InMemoryRenderCache implements RenderCache {

	/**
	 * Backing generic store; carries the entries, the clock, and
	 * the TTL eviction logic shared across cache layers.
	 */
	private InMemoryCacheStore $store;

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

	/**
	 * Read the cached `['view_model' => array, 'html' => string]`
	 * entry, or null on miss / expired.
	 *
	 * @param string $key Cache key.
	 *
	 * @return array{view_model: array<string, mixed>, html: string}|null
	 */
	public function get( string $key ): ?array {
		$entry = $this->store->getRaw( $key );
		if ( null === $entry ) {
			return null;
		}

		$value = $entry->value();
		if ( ! is_array( $value ) ) {
			return null;
		}

		if ( ! array_key_exists( 'view_model', $value ) || ! array_key_exists( 'html', $value ) ) {
			return null;
		}

		$viewModel = $value['view_model'];
		$html      = $value['html'];

		if ( ! is_array( $viewModel ) || ! is_string( $html ) ) {
			return null;
		}

		/** @var array<string, mixed> $viewModel */
		return array(
			'view_model' => $viewModel,
			'html'       => $html,
		);
	}

	/**
	 * Write the rendered entry with the given TTL in seconds.
	 *
	 * The stored value is the two-key associative array
	 * `['view_model' => $viewModel, 'html' => $html]` per the
	 * {@see RenderCache::get()} contract. Delegates TTL
	 * validation to {@see InMemoryCacheStore::putRaw()}.
	 *
	 * @param string               $key        Cache key.
	 * @param array<string, mixed> $viewModel  View model snapshot.
	 * @param string               $html       Rendered HTML.
	 * @param int                  $ttlSeconds TTL in seconds; must be > 0.
	 */
	public function put( string $key, array $viewModel, string $html, int $ttlSeconds ): void {
		$this->store->putRaw(
			$key,
			array(
				'view_model' => $viewModel,
				'html'       => $html,
			),
			$ttlSeconds
		);
	}

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

	/**
	 * Drop every entry stored in this layer's backing store.
	 */
	public function flush(): void {
		$this->store->flush();
	}

	/**
	 * Drop every entry whose key begins with `$prefix` and return
	 * the count of removed entries.
	 *
	 * Delegates to {@see InMemoryCacheStore::forgetByPrefix()},
	 * which rejects an empty `$prefix` as a defensive guard
	 * against accidental layer-wide flushes.
	 *
	 * @param string $prefix Cache-key prefix.
	 *
	 * @return int Count of removed entries (>= 0).
	 */
	public function forgetByPrefix( string $prefix ): int {
		return $this->store->forgetByPrefix( $prefix );
	}
}
