<?php
/**
 * Transients-backed {@see RenderCache} adapter.
 *
 * Phase 9 MVP implementation of the
 * {@see \CommerceLinkBuilder\Application\Cache\RenderCache} 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), §6.3
 * (cache-key forms), and §14 (cache invalidation, including the
 * prefix-wildcard sweep that {@see self::forgetByPrefix()}
 * supports on adapters whose backend can enumerate entries).
 *
 * 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.
 *
 * MVP limitation: {@see self::forgetByPrefix()} delegates to
 * {@see TransientsCacheStore::forgetByPrefix()}, which is a
 * documented Phase-9-MVP no-op returning 0. The §14
 * `CacheInvalidator` MVP overshoot already routes broad-flush
 * callers through whole-cache flush behavior on this backend.
 *
 * @package CommerceLinkBuilder\Infrastructure\Cache
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Infrastructure\Cache;

use CommerceLinkBuilder\Application\Cache\RenderCache;

/**
 * Transients-backed {@see RenderCache} adapter.
 *
 * See the file-level docblock for the entry shape, the spec
 * citations, and the documented Phase-9-MVP limitation on
 * {@see self::forgetByPrefix()}.
 */
final class TransientsRenderCache implements RenderCache {

	/**
	 * 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 `['view_model' => array, 'html' => string]`
	 * entry, or null on miss / expired / malformed envelope.
	 *
	 * @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 TransientsCacheStore::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 );
	}

	/**
	 * 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();
	}

	/**
	 * Phase-9-MVP no-op returning 0 (delegates to
	 * {@see TransientsCacheStore::forgetByPrefix()}). Targeted
	 * prefix sweeps require options-table enumeration that this
	 * slice does not implement.
	 *
	 * @param string $prefix Cache-key prefix (ignored).
	 *
	 * @return int Always 0 for the Phase-9-MVP backend.
	 */
	public function forgetByPrefix( string $prefix ): int {
		return $this->store->forgetByPrefix( $prefix );
	}
}
