<?php
/**
 * Block service provider.
 *
 * Wires the Phase-7 Gutenberg block registration foundation into
 * the Commerce Link Builder bootstrap pipeline. As of Phase-10
 * slice 10e the provider registers two blocks:
 *
 *   - `clb/product-card` (Phase-7 slice 7b) — driven by
 *     {@see ProductCardBlockRenderer} via the
 *     {@see ProductCardViewModelFactory} + {@see ProductCardRenderer}
 *     pipeline.
 *
 *   - `clb/product-grid` (Phase-10 slice 10e) — driven by
 *     {@see ProductGridBlockRenderer} via the slice-10c / 10d
 *     {@see ProductGridViewModelFactory} + {@see ProductGridRenderer}
 *     pipeline. The grid renderer's collaborators
 *     ({@see ProductGridViewModelFactory},
 *     {@see ProductGridRenderer}) are NOT registered by the
 *     {@see RenderServiceProvider}; this provider builds them
 *     inline at register() time from the registry-resolved
 *     {@see ProductCardViewModelFactory},
 *     {@see TemplateResolver}, and {@see ViewModelArrayDumper}
 *     bindings so the bootstrap pipeline is unchanged.
 *
 * The provider's responsibilities are:
 *
 *   1. Resolve the Phase-2 persistence port
 *      ({@see \CommerceLinkBuilder\Infrastructure\Persistence\ProductRepository})
 *      and the Phase-5 rendering collaborators
 *      ({@see \CommerceLinkBuilder\Application\Render\ViewModel\ProductCardViewModelFactory},
 *      {@see \CommerceLinkBuilder\Application\Render\ProductCardRenderer},
 *      {@see \CommerceLinkBuilder\Application\Render\Template\TemplateResolver},
 *      {@see \CommerceLinkBuilder\Application\Render\ViewModelArrayDumper})
 *      from the registry. Missing bindings raise
 *      {@see LogicException} so a misordered bootstrap fails
 *      loudly here rather than at the first `init` action firing
 *      or — worse — at the first frontend render.
 *
 *   2. Construct (or accept the test-injected overrides of) the
 *      {@see ProductCardBlockRenderer} and
 *      {@see ProductGridBlockRenderer} wired to those
 *      collaborators and bind them into the registry under
 *      {@see self::BINDING_PRODUCT_CARD_BLOCK_RENDERER} /
 *      {@see self::BINDING_PRODUCT_GRID_BLOCK_RENDERER} so
 *      diagnostics and integration tests can resolve the same
 *      instances the `render_callback`s close over.
 *
 *   3. Bind a fresh {@see BlockRegistrar} under
 *      {@see self::BINDING_BLOCK_REGISTRAR} so subsequent slices'
 *      service providers can resolve it from the registry and
 *      either append additional {@see BlockDefinition} values or
 *      replace existing definitions' `args` arrays.
 *
 *   4. Append two {@see BlockDefinition}s — one for
 *      `clb/product-card` (`${pluginRootDir}/blocks/product-card`)
 *      and one for `clb/product-grid`
 *      (`${pluginRootDir}/blocks/product-grid`) — each carrying a
 *      `'render_callback' => array($renderer, 'render')` entry.
 *      WordPress treats `render_callback` from `$args` as
 *      authoritative — taking precedence over any `render`
 *      `file:` entry in `block.json` — so the §11 dynamic
 *      renders flow through the Phase-5 / Phase-10 pipelines.
 *
 *   5. Subscribe a single `init` action callback that drives the
 *      bound registrar's {@see BlockRegistrar::register()}. Block
 *      registration MUST happen on `init` (not `plugins_loaded`)
 *      because WordPress's block-type registry is not initialized
 *      until then.
 *
 * Provider ordering — register() resolves bindings published by
 * earlier providers, so the bootstrap pipeline in
 * `commerce-link-builder.php` MUST register:
 *
 *   1. {@see \CommerceLinkBuilder\Infrastructure\Persistence\PersistenceServiceProvider}
 *      (publishes
 *      {@see \CommerceLinkBuilder\Infrastructure\Persistence\PersistenceServiceProvider::BINDING_PRODUCT_REPOSITORY}).
 *
 *   2. {@see \CommerceLinkBuilder\Application\Render\RenderServiceProvider}
 *      (publishes
 *      {@see \CommerceLinkBuilder\Application\Render\RenderServiceProvider::BINDING_PRODUCT_CARD_VIEW_MODEL_FACTORY},
 *      {@see \CommerceLinkBuilder\Application\Render\RenderServiceProvider::BINDING_PRODUCT_CARD_RENDERER},
 *      {@see \CommerceLinkBuilder\Application\Render\RenderServiceProvider::BINDING_TEMPLATE_RESOLVER},
 *      and
 *      {@see \CommerceLinkBuilder\Application\Render\RenderServiceProvider::BINDING_VIEW_MODEL_ARRAY_DUMPER}).
 *
 *   3. This provider (resolves the bindings, builds the two
 *      block renderers, and appends the `clb/product-card` and
 *      `clb/product-grid` definitions with the `render_callback`
 *      slots populated).
 *
 * Resolution policy:
 *
 *   - The plugin root directory is required at construction time
 *     and validated for absolute-path-ness so a misconfigured
 *     caller fails loudly at wiring time.
 *
 *   - The registrar and block-renderer overrides are optional;
 *     omitting them causes the provider to construct the
 *     production defaults during register() against the
 *     registry-resolved collaborators.
 *
 *   - Resolution is eager at register() time so a misconfiguration
 *     surfaces during
 *     {@see \CommerceLinkBuilder\Bootstrap\ServiceProviderRegistry::registerAll()}
 *     rather than at the first `init` action firing.
 *
 *   - The bound registrar is also cached on the provider so the
 *     boot() action callback can capture it without re-resolving
 *     the registry binding.
 *
 * See `specifications/gutenberg-block-spec.md` §3 (block-namespace
 * conventions), §4 (`clb/product-card` attributes), §5
 * (`clb/product-grid` attributes), §6 (`block.json` design), §11
 * (server-side rendering), and §17 (`block.json` implementation
 * guide).
 *
 * @package CommerceLinkBuilder\Presentation\Block
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Presentation\Block;

use CommerceLinkBuilder\Application\Cache\CacheKeyFactory;
use CommerceLinkBuilder\Application\Cache\CacheServiceProvider;
use CommerceLinkBuilder\Application\Cache\RenderCache;
use CommerceLinkBuilder\Application\Render\ProductCardRenderer;
use CommerceLinkBuilder\Application\Render\ProductGridRenderer;
use CommerceLinkBuilder\Application\Render\RenderServiceProvider;
use CommerceLinkBuilder\Application\Render\Template\TemplateResolver;
use CommerceLinkBuilder\Application\Render\ViewModel\ProductCardViewModelFactory;
use CommerceLinkBuilder\Application\Render\ViewModel\ProductGridViewModelFactory;
use CommerceLinkBuilder\Application\Render\ViewModelArrayDumper;
use CommerceLinkBuilder\Bootstrap\HookRegistrar;
use CommerceLinkBuilder\Bootstrap\ServiceProvider;
use CommerceLinkBuilder\Bootstrap\ServiceProviderRegistry;
use CommerceLinkBuilder\Infrastructure\Persistence\PersistenceServiceProvider;
use CommerceLinkBuilder\Infrastructure\Persistence\ProductRepository;
use InvalidArgumentException;
use LogicException;

/**
 * Binds the Phase-7 {@see BlockRegistrar} plus the slice-7b
 * {@see ProductCardBlockRenderer} and the Phase-10 slice 10e
 * {@see ProductGridBlockRenderer}, and subscribes the `init`
 * action that drives both blocks' registration through
 * WordPress's `register_block_type_from_metadata()`.
 *
 * See the file-level docblock for the bootstrap-pipeline ordering
 * requirement and the resolution policy.
 */
final class BlockServiceProvider implements ServiceProvider {

	/**
	 * Registry key under which the {@see BlockRegistrar} is bound.
	 */
	public const BINDING_BLOCK_REGISTRAR = 'clb.block.registrar';

	/**
	 * Registry key under which the slice-7b
	 * {@see ProductCardBlockRenderer} is bound.
	 */
	public const BINDING_PRODUCT_CARD_BLOCK_RENDERER = 'clb.block.product_card_renderer';

	/**
	 * Registry key under which the slice-10e
	 * {@see ProductGridBlockRenderer} is bound.
	 */
	public const BINDING_PRODUCT_GRID_BLOCK_RENDERER = 'clb.block.product_grid_renderer';

	/**
	 * Absolute filesystem path of the plugin root directory.
	 */
	private string $pluginRootDir;

	/**
	 * Block-registrar override.
	 */
	private ?BlockRegistrar $registrarOverride;

	/**
	 * Slice-7b block-renderer override.
	 */
	private ?ProductCardBlockRenderer $rendererOverride;

	/**
	 * Slice-10e grid-block-renderer override.
	 */
	private ?ProductGridBlockRenderer $gridRendererOverride;

	/**
	 * Cached {@see BlockRegistrar} bound into the registry.
	 */
	private ?BlockRegistrar $resolvedRegistrar = null;

	/**
	 * Construct a block service provider with the supplied plugin
	 * root directory and (optionally overridden) collaborators.
	 *
	 * @param string                        $pluginRootDir        Absolute path to the plugin root directory.
	 * @param BlockRegistrar|null           $registrarOverride    Registrar override.
	 * @param ProductCardBlockRenderer|null $rendererOverride     Card-block-renderer override.
	 * @param ProductGridBlockRenderer|null $gridRendererOverride Grid-block-renderer override.
	 *
	 * @throws InvalidArgumentException When `$pluginRootDir` is empty
	 *                                  after trimming or not an
	 *                                  absolute path.
	 */
	public function __construct(
		string $pluginRootDir,
		?BlockRegistrar $registrarOverride = null,
		?ProductCardBlockRenderer $rendererOverride = null,
		?ProductGridBlockRenderer $gridRendererOverride = null
	) {
		$trimmed = trim( $pluginRootDir );
		if ( $trimmed === '' ) {
			$message = 'BlockServiceProvider pluginRootDir must be a non-empty string.';
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		$isAbsolute = str_starts_with( $trimmed, DIRECTORY_SEPARATOR )
			|| str_starts_with( $trimmed, '/' )
			|| preg_match( '#^[A-Za-z]:[\\\\/]#', $trimmed ) === 1;
		if ( ! $isAbsolute ) {
			$message = sprintf(
				'BlockServiceProvider pluginRootDir must be an absolute path; got "%s".',
				$trimmed
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		$this->pluginRootDir        = rtrim( $trimmed, '/\\' );
		$this->registrarOverride    = $registrarOverride;
		$this->rendererOverride     = $rendererOverride;
		$this->gridRendererOverride = $gridRendererOverride;
	}

	/**
	 * Bind the {@see BlockRegistrar} and the two block renderers
	 * into the registry, then append the `clb/product-card` and
	 * `clb/product-grid` {@see BlockDefinition}s with their
	 * `render_callback`s populated.
	 *
	 * @param ServiceProviderRegistry $registry Registry driving this
	 *                                          bootstrap pass.
	 *
	 * @throws LogicException When the persistence or render
	 *                        provider has not registered before
	 *                        this one.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		$repository       = $this->resolveProductRepository( $registry );
		$cardFactory      = $this->resolveViewModelFactory( $registry );
		$cardRenderer     = $this->resolveProductCardRenderer( $registry );
		$templateResolver = $this->resolveTemplateResolver( $registry );
		$arrayDumper      = $this->resolveArrayDumper( $registry );

		// KI-007: optional cache wiring. When the cache provider has
		// been registered upstream the block renderers read from /
		// write to the §4 render cache layer; otherwise they fall
		// back on the Phase 9 MVP "render every request" behavior.
		$renderCache = $this->resolveOptionalRenderCache( $registry );
		$cacheKeys   = $this->resolveOptionalCacheKeyFactory( $registry );

		$cardBlockRenderer = $this->rendererOverride ?? new ProductCardBlockRenderer(
			$repository,
			$cardFactory,
			$cardRenderer,
			null,
			$renderCache,
			$cacheKeys,
			$arrayDumper
		);

		$gridFactory  = new ProductGridViewModelFactory( $cardFactory );
		$gridRenderer = new ProductGridRenderer( $templateResolver, $arrayDumper );

		$gridBlockRenderer = $this->gridRendererOverride ?? new ProductGridBlockRenderer(
			$repository,
			$gridFactory,
			$gridRenderer,
			null,
			$renderCache,
			$cacheKeys,
			$arrayDumper
		);

		$registrar = $this->registrarOverride ?? new BlockRegistrar();

		$this->resolvedRegistrar = $registrar;

		$registry->bind( self::BINDING_BLOCK_REGISTRAR, $registrar );
		$registry->bind( self::BINDING_PRODUCT_CARD_BLOCK_RENDERER, $cardBlockRenderer );
		$registry->bind( self::BINDING_PRODUCT_GRID_BLOCK_RENDERER, $gridBlockRenderer );

		$registrar->add(
			new BlockDefinition(
				$this->pluginRootDir . DIRECTORY_SEPARATOR . 'blocks' . DIRECTORY_SEPARATOR . 'product-card',
				'clb/product-card',
				array(
					'render_callback' => array( $cardBlockRenderer, 'render' ),
				)
			)
		);

		$registrar->add(
			new BlockDefinition(
				$this->pluginRootDir . DIRECTORY_SEPARATOR . 'blocks' . DIRECTORY_SEPARATOR . 'product-grid',
				'clb/product-grid',
				array(
					'render_callback' => array( $gridBlockRenderer, 'render' ),
				)
			)
		);
	}

	/**
	 * Subscribe an `init` action callback that drives the bound
	 * {@see BlockRegistrar::register()}.
	 *
	 * @param HookRegistrar $hooks Shared hook registrar.
	 */
	public function boot( HookRegistrar $hooks ): void {
		$hooks->addAction( 'init', array( $this, 'onInit' ) );
		// `enqueue_block_assets` fires both on the front-end and inside
		// the Gutenberg editor's iframe canvas, so the card CSS reaches
		// the in-editor preview HTML emitted by SelectedProductSummary.
		// `enqueue_block_editor_assets` would only load the stylesheet
		// into the wp-admin host page, not into the iframe where the
		// block's edit() output is actually rendered.
		$hooks->addAction( 'enqueue_block_assets', array( $this, 'onEnqueueScripts' ) );
	}

	/**
	 * `init` action callback.
	 *
	 * Drives {@see BlockRegistrar::register()} so every
	 * {@see BlockDefinition} appended during register() (and any
	 * appended by subsequent slices' service providers) is
	 * forwarded to WordPress at the canonical block-registration
	 * moment.
	 */
	public function onInit(): void {
		if ( $this->resolvedRegistrar === null ) {
			return;
		}

		$this->resolvedRegistrar->register();
	}

	/**
	 * `wp_enqueue_scripts` / `enqueue_block_editor_assets` callback
	 * (post-KI design refresh).
	 *
	 * Registers + enqueues the front-end card / grid stylesheet so
	 * a post that embeds a `clb/product-card` or `clb/product-grid`
	 * block renders the modern Rinker-inspired layout. The same
	 * stylesheet is enqueued in the block editor so the
	 * server-rendered preview (when wired in a future slice) and
	 * the front-end render share visuals.
	 *
	 * The asset URL is computed from the plugin entry file so the
	 * lookup is independent of the active theme. The handle is
	 * versioned by the plugin's own version constant when defined,
	 * otherwise by the file's mtime (the same fallback wp-scripts
	 * uses).
	 */
	public function onEnqueueScripts(): void {
		if ( ! function_exists( 'wp_enqueue_style' ) || ! function_exists( 'plugins_url' ) ) {
			return;
		}

		$entry_file = defined( 'CLB_PLUGIN_FILE' )
			? (string) constant( 'CLB_PLUGIN_FILE' )
			: dirname( __DIR__, 3 ) . '/commerce-link-builder.php';

		$rel_path = 'assets/css/clb-card.css';
		$abs_path = dirname( $entry_file ) . '/' . $rel_path;
		$src      = plugins_url( $rel_path, $entry_file );

		$version = defined( 'CLB_VERSION' )
			? (string) constant( 'CLB_VERSION' )
			: ( file_exists( $abs_path ) ? (string) filemtime( $abs_path ) : '0' );

		wp_enqueue_style(
			'clb-card-style',
			$src,
			array(),
			$version,
			'all'
		);
	}

	/**
	 * Resolve {@see ProductRepository} from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Bootstrap registry.
	 *
	 * @throws LogicException When the binding is missing or the
	 *                        bound value is not a
	 *                        {@see ProductRepository}.
	 */
	private function resolveProductRepository( ServiceProviderRegistry $registry ): ProductRepository {
		$key = PersistenceServiceProvider::BINDING_PRODUCT_REPOSITORY;
		if ( ! $registry->has( $key ) ) {
			$message = 'BlockServiceProvider requires PersistenceServiceProvider to register before it'
				. ' (missing ProductRepository binding).';
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		$repository = $registry->get( $key );
		if ( ! $repository instanceof ProductRepository ) {
			$message = sprintf(
				'BlockServiceProvider expected ProductRepository under "%s"; got %s.',
				$key,
				is_object( $repository ) ? get_class( $repository ) : gettype( $repository )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $repository;
	}

	/**
	 * Resolve {@see ProductCardViewModelFactory} from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Bootstrap registry.
	 *
	 * @throws LogicException When the binding is missing or the
	 *                        bound value is not a
	 *                        {@see ProductCardViewModelFactory}.
	 */
	private function resolveViewModelFactory(
		ServiceProviderRegistry $registry
	): ProductCardViewModelFactory {
		$key = RenderServiceProvider::BINDING_PRODUCT_CARD_VIEW_MODEL_FACTORY;
		if ( ! $registry->has( $key ) ) {
			$message = 'BlockServiceProvider requires RenderServiceProvider to register before it'
				. ' (missing ProductCardViewModelFactory binding).';
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		$factory = $registry->get( $key );
		if ( ! $factory instanceof ProductCardViewModelFactory ) {
			$message = sprintf(
				'BlockServiceProvider expected ProductCardViewModelFactory under "%s"; got %s.',
				$key,
				is_object( $factory ) ? get_class( $factory ) : gettype( $factory )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $factory;
	}

	/**
	 * Resolve {@see ProductCardRenderer} from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Bootstrap registry.
	 *
	 * @throws LogicException When the binding is missing or the
	 *                        bound value is not a
	 *                        {@see ProductCardRenderer}.
	 */
	private function resolveProductCardRenderer( ServiceProviderRegistry $registry ): ProductCardRenderer {
		$key = RenderServiceProvider::BINDING_PRODUCT_CARD_RENDERER;
		if ( ! $registry->has( $key ) ) {
			$message = 'BlockServiceProvider requires RenderServiceProvider to register before it'
				. ' (missing ProductCardRenderer binding).';
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		$renderer = $registry->get( $key );
		if ( ! $renderer instanceof ProductCardRenderer ) {
			$message = sprintf(
				'BlockServiceProvider expected ProductCardRenderer under "%s"; got %s.',
				$key,
				is_object( $renderer ) ? get_class( $renderer ) : gettype( $renderer )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $renderer;
	}

	/**
	 * Resolve {@see TemplateResolver} from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Bootstrap registry.
	 *
	 * @throws LogicException When the binding is missing or the
	 *                        bound value is not a
	 *                        {@see TemplateResolver}.
	 */
	private function resolveTemplateResolver( ServiceProviderRegistry $registry ): TemplateResolver {
		$key = RenderServiceProvider::BINDING_TEMPLATE_RESOLVER;
		if ( ! $registry->has( $key ) ) {
			$message = 'BlockServiceProvider requires RenderServiceProvider to register before it'
				. ' (missing TemplateResolver binding).';
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		$resolver = $registry->get( $key );
		if ( ! $resolver instanceof TemplateResolver ) {
			$message = sprintf(
				'BlockServiceProvider expected TemplateResolver under "%s"; got %s.',
				$key,
				is_object( $resolver ) ? get_class( $resolver ) : gettype( $resolver )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $resolver;
	}

	/**
	 * Resolve {@see ViewModelArrayDumper} from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Bootstrap registry.
	 *
	 * @throws LogicException When the binding is missing or the
	 *                        bound value is not a
	 *                        {@see ViewModelArrayDumper}.
	 */
	private function resolveArrayDumper( ServiceProviderRegistry $registry ): ViewModelArrayDumper {
		$key = RenderServiceProvider::BINDING_VIEW_MODEL_ARRAY_DUMPER;
		if ( ! $registry->has( $key ) ) {
			$message = 'BlockServiceProvider requires RenderServiceProvider to register before it'
				. ' (missing ViewModelArrayDumper binding).';
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		$dumper = $registry->get( $key );
		if ( ! $dumper instanceof ViewModelArrayDumper ) {
			$message = sprintf(
				'BlockServiceProvider expected ViewModelArrayDumper under "%s"; got %s.',
				$key,
				is_object( $dumper ) ? get_class( $dumper ) : gettype( $dumper )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $dumper;
	}

	/**
	 * Resolve the optional §4 {@see RenderCache} binding published
	 * by {@see CacheServiceProvider}, returning null when the
	 * cache provider has not been registered upstream so the block
	 * renderers fall back on the Phase 9 MVP "render every request"
	 * behavior (KI-007 fallback path).
	 *
	 * @param ServiceProviderRegistry $registry Bootstrap registry.
	 */
	private function resolveOptionalRenderCache( ServiceProviderRegistry $registry ): ?RenderCache {
		$key = CacheServiceProvider::BINDING_RENDER_CACHE;
		if ( ! $registry->has( $key ) ) {
			return null;
		}

		$value = $registry->get( $key );
		return $value instanceof RenderCache ? $value : null;
	}

	/**
	 * Resolve the optional §6.3 {@see CacheKeyFactory} binding
	 * published by {@see CacheServiceProvider}, returning null
	 * when the cache provider has not been registered upstream.
	 *
	 * @param ServiceProviderRegistry $registry Bootstrap registry.
	 */
	private function resolveOptionalCacheKeyFactory( ServiceProviderRegistry $registry ): ?CacheKeyFactory {
		$key = CacheServiceProvider::BINDING_CACHE_KEY_FACTORY;
		if ( ! $registry->has( $key ) ) {
			return null;
		}

		$value = $registry->get( $key );
		return $value instanceof CacheKeyFactory ? $value : null;
	}
}
