<?php
/**
 * Render service provider.
 *
 * Wires the Phase-5 §8 / §9 / §11 / §12 rendering subsystem into the
 * affilun bootstrap pipeline. The provider closes the
 * §8.5 "Phase 5 完了基準" of
 * `specifications/rendering-and-template-spec.md` /
 * `specifications/implementation-roadmap.md` §8.5 by:
 *
 *   1. Loading the global `affilun_template()` template-loading helper
 *      defined in {@see template-helpers.php} so callers (themes,
 *      template overrides, downstream slices) can rely on the
 *      symbol being available immediately after this provider's
 *      register() returns.
 *
 *   2. Eagerly constructing the §6.5 {@see TemplateResolver}, the
 *      §8.2 / §8.4 {@see ViewModelArrayDumper}, the §11
 *      {@see LinkAttributeBuilder}, the §10.8
 *      {@see DisclaimerBuilder}, the §10.4 / §12
 *      {@see ImagePresentationBuilder}, the §9.1
 *      {@see ProductCardViewModelFactory}, and the
 *      {@see ProductCardRenderer} that ties them together. Each
 *      collaborator is bound into the
 *      {@see ServiceProviderRegistry} under a stable string key
 *      exposed as a `BINDING_*` class constant on this provider so
 *      downstream slices (REST controllers, the Gutenberg block,
 *      shortcode bridge, diagnostics surface) can resolve them
 *      without re-instantiating the chain.
 *
 *   3. Registering a single `affilun_template_resolver` filter callback
 *      during boot() that returns the bound {@see TemplateResolver}
 *      instance so the global {@see \affilun_template()} helper can
 *      locate the resolver via `apply_filters()`. The callback
 *      ignores the previous value, which lets a third-party register
 *      a higher-priority filter to chain a custom resolver if
 *      required. A diagnostic `affilun_template_helpers_loaded` filter
 *      is also registered so external code can verify the helper
 *      file has been loaded.
 *
 * Resolution policy:
 *
 *   - The plugin root directory is required at construction time.
 *     The renderer's plugin-template directory is computed as
 *     `${pluginRootDir}/templates`. The plugin root path is
 *     validated for absolute-path-ness so a misconfigured caller
 *     fails loudly at wiring time rather than at the first failed
 *     render.
 *
 *   - Every collaborator is optional; omitting one causes the
 *     provider to construct the production default during
 *     register(). Production wiring uses the public
 *     `BINDING_*` constants below; integration tests inject
 *     synthetic collaborators one axis at a time via the
 *     constructor overrides.
 *
 *   - Resolution is eager at register() time so a misconfiguration
 *     surfaces during {@see ServiceProviderRegistry::registerAll()}
 *     rather than on the first frontend render. The resolver
 *     instance is also cached on the provider so the boot()
 *     filter callback can capture it without re-resolving the
 *     registry binding.
 *
 * Phase-5 §8.5 completion criteria addressed by this provider:
 *
 *   - Wires `affilun_template_resolver` so the global helper finds a
 *     resolver.
 *   - Loads `template-helpers.php` so `affilun_template()` becomes
 *     available before any caller invokes it.
 *   - Composes the full §9.1 / §11 / §12 collaborator graph behind
 *     stable registry keys so the renderer can be driven from a
 *     saved snapshot without any external API call (see
 *     `tests/Unit/Application/Render/Integration/RenderingPipelineEndToEndTest.php`).
 *
 * @package Affilun\Application\Render
 */

declare(strict_types=1);

namespace Affilun\Application\Render;

use Affilun\Application\Render\Builder\DisclaimerBuilder;
use Affilun\Application\Render\Builder\ImagePresentationBuilder;
use Affilun\Application\Render\Builder\LinkAttributeBuilder;
use Affilun\Application\Render\Template\TemplateResolver;
use Affilun\Application\Render\ViewModel\ProductCardViewModelFactory;
use Affilun\Bootstrap\HookRegistrar;
use Affilun\Bootstrap\ServiceProvider;
use Affilun\Bootstrap\ServiceProviderRegistry;
use InvalidArgumentException;

/**
 * Binds the Phase-5 rendering collaborators and wires the
 * `affilun_template_resolver` filter so the global `affilun_template()`
 * helper can find the registered {@see TemplateResolver}.
 *
 * See the file-level docblock for the Phase-5 §8.5 completion-
 * criteria mapping and the resolution policy.
 */
final class RenderServiceProvider implements ServiceProvider {

	/**
	 * Registry key under which the §6.5 {@see TemplateResolver} is
	 * bound.
	 */
	public const BINDING_TEMPLATE_RESOLVER = 'affilun.render.template_resolver';

	/**
	 * Registry key under which the §8.2 / §8.4
	 * {@see ViewModelArrayDumper} is bound.
	 */
	public const BINDING_VIEW_MODEL_ARRAY_DUMPER = 'affilun.render.view_model_array_dumper';

	/**
	 * Registry key under which the §11 {@see LinkAttributeBuilder}
	 * is bound.
	 */
	public const BINDING_LINK_ATTRIBUTE_BUILDER = 'affilun.render.link_attribute_builder';

	/**
	 * Registry key under which the §10.8 {@see DisclaimerBuilder}
	 * is bound.
	 */
	public const BINDING_DISCLAIMER_BUILDER = 'affilun.render.disclaimer_builder';

	/**
	 * Registry key under which the §10.4 / §12
	 * {@see ImagePresentationBuilder} is bound.
	 */
	public const BINDING_IMAGE_PRESENTATION_BUILDER = 'affilun.render.image_presentation_builder';

	/**
	 * Registry key under which the §9.1
	 * {@see ProductCardViewModelFactory} is bound.
	 */
	public const BINDING_PRODUCT_CARD_VIEW_MODEL_FACTORY = 'affilun.render.product_card_view_model_factory';

	/**
	 * Registry key under which the {@see ProductCardRenderer} is
	 * bound.
	 */
	public const BINDING_PRODUCT_CARD_RENDERER = 'affilun.render.product_card_renderer';

	/**
	 * Absolute filesystem path of the plugin root directory.
	 *
	 * The renderer's plugin-template directory is derived as
	 * `${pluginRootDir}/templates`.
	 */
	private string $pluginRootDir;

	/**
	 * §6.5 template resolver override.
	 *
	 * When null, the provider constructs the production default in
	 * {@see self::resolveTemplateResolver()} using
	 * `${pluginRootDir}/templates` plus the WordPress
	 * `get_stylesheet_directory()` / `get_template_directory()`
	 * defaults.
	 */
	private ?TemplateResolver $templateResolverOverride;

	/**
	 * §8.2 / §8.4 view-model serializer override.
	 *
	 * When null, the provider constructs a fresh
	 * {@see ViewModelArrayDumper} during register().
	 */
	private ?ViewModelArrayDumper $arrayDumperOverride;

	/**
	 * Renderer override.
	 *
	 * When null, the provider constructs a {@see ProductCardRenderer}
	 * wired to the bound resolver and dumper using the renderer's
	 * default include strategy.
	 */
	private ?ProductCardRenderer $rendererOverride;

	/**
	 * §11 link-attribute builder override.
	 */
	private ?LinkAttributeBuilder $linkBuilderOverride;

	/**
	 * §10.8 disclaimer builder override.
	 */
	private ?DisclaimerBuilder $disclaimerBuilderOverride;

	/**
	 * §10.4 / §12 image-presentation builder override.
	 */
	private ?ImagePresentationBuilder $imageBuilderOverride;

	/**
	 * §9.1 ViewModel factory override.
	 */
	private ?ProductCardViewModelFactory $factoryOverride;

	/**
	 * Cached {@see TemplateResolver} bound into the registry.
	 *
	 * Populated during register() so the boot() filter callback can
	 * capture the same instance without re-resolving the binding.
	 */
	private ?TemplateResolver $resolvedTemplateResolver = null;

	/**
	 * Construct a render service provider with the supplied plugin
	 * root directory and (optionally overridden) collaborators.
	 *
	 * @param string                           $pluginRootDir             Absolute path to the plugin root directory.
	 * @param TemplateResolver|null            $templateResolverOverride  §6.5 resolver override.
	 * @param ViewModelArrayDumper|null        $arrayDumperOverride       §8.2 / §8.4 dumper override.
	 * @param ProductCardRenderer|null         $rendererOverride          Renderer override.
	 * @param LinkAttributeBuilder|null        $linkBuilderOverride       §11 builder override.
	 * @param DisclaimerBuilder|null           $disclaimerBuilderOverride §10.8 builder override.
	 * @param ImagePresentationBuilder|null    $imageBuilderOverride      §10.4 / §12 builder override.
	 * @param ProductCardViewModelFactory|null $factoryOverride          §9.1 factory override.
	 *
	 * @throws InvalidArgumentException When `$pluginRootDir` is empty
	 *                                  after trimming or not an
	 *                                  absolute path.
	 */
	public function __construct(
		string $pluginRootDir,
		?TemplateResolver $templateResolverOverride = null,
		?ViewModelArrayDumper $arrayDumperOverride = null,
		?ProductCardRenderer $rendererOverride = null,
		?LinkAttributeBuilder $linkBuilderOverride = null,
		?DisclaimerBuilder $disclaimerBuilderOverride = null,
		?ImagePresentationBuilder $imageBuilderOverride = null,
		?ProductCardViewModelFactory $factoryOverride = null
	) {
		$trimmed = trim( $pluginRootDir );
		if ( $trimmed === '' ) {
			$message = 'RenderServiceProvider 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(
				'RenderServiceProvider 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->templateResolverOverride  = $templateResolverOverride;
		$this->arrayDumperOverride       = $arrayDumperOverride;
		$this->rendererOverride          = $rendererOverride;
		$this->linkBuilderOverride       = $linkBuilderOverride;
		$this->disclaimerBuilderOverride = $disclaimerBuilderOverride;
		$this->imageBuilderOverride      = $imageBuilderOverride;
		$this->factoryOverride           = $factoryOverride;
	}

	/**
	 * Bind every Phase-5 rendering collaborator into the registry
	 * and pre-load the global `affilun_template()` helper file.
	 *
	 * Resolution is eager so a misconfiguration (for example a
	 * missing template directory under the supplied plugin root)
	 * surfaces during
	 * {@see ServiceProviderRegistry::registerAll()} rather than at
	 * the first frontend render.
	 *
	 * The template-helpers.php file is required exactly once so the
	 * global `affilun_template()` symbol is available before any caller
	 * invokes it. The file's `function_exists` guard means a stray
	 * second require during testing is harmless.
	 *
	 * @param ServiceProviderRegistry $registry Registry driving this
	 *                                          bootstrap pass.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		// The provider source file lives in the same directory as
		// template-helpers.php, so the relative path is unambiguous.
		require_once __DIR__ . '/template-helpers.php';

		$resolver      = $this->resolveTemplateResolver();
		$dumper        = $this->resolveArrayDumper();
		$linkBuilder   = $this->resolveLinkBuilder();
		$disclaimerBld = $this->resolveDisclaimerBuilder();
		$imageBuilder  = $this->resolveImageBuilder();
		$factory       = $this->resolveFactory( $linkBuilder, $disclaimerBld, $imageBuilder );
		$renderer      = $this->resolveRenderer( $resolver, $dumper );

		$this->resolvedTemplateResolver = $resolver;

		$registry->bind( self::BINDING_TEMPLATE_RESOLVER, $resolver );
		$registry->bind( self::BINDING_VIEW_MODEL_ARRAY_DUMPER, $dumper );
		$registry->bind( self::BINDING_LINK_ATTRIBUTE_BUILDER, $linkBuilder );
		$registry->bind( self::BINDING_DISCLAIMER_BUILDER, $disclaimerBld );
		$registry->bind( self::BINDING_IMAGE_PRESENTATION_BUILDER, $imageBuilder );
		$registry->bind( self::BINDING_PRODUCT_CARD_VIEW_MODEL_FACTORY, $factory );
		$registry->bind( self::BINDING_PRODUCT_CARD_RENDERER, $renderer );
	}

	/**
	 * Subscribe the bound {@see TemplateResolver} to the
	 * `affilun_template_resolver` filter and register a diagnostic
	 * `affilun_template_helpers_loaded` filter so external code can
	 * verify the helper file has been loaded.
	 *
	 * The resolver-returning callback captures the resolver instance
	 * cached on this provider during register(); it ignores the
	 * previous filter value so a third-party can chain a custom
	 * resolver via a higher-priority filter callback.
	 *
	 * @param HookRegistrar $hooks Shared hook registrar.
	 */
	public function boot( HookRegistrar $hooks ): void {
		$resolver = $this->resolvedTemplateResolver;

		$hooks->addFilter(
			'affilun_template_resolver',
			static function ( $previousValue = null ) use ( $resolver ): ?TemplateResolver {
				unset( $previousValue );
				return $resolver;
			},
			10,
			1
		);

		$hooks->addFilter(
			'affilun_template_helpers_loaded',
			static function ( $previousValue = null ): bool {
				unset( $previousValue );
				return true;
			},
			10,
			1
		);
	}

	/**
	 * Resolve the §6.5 {@see TemplateResolver} the registry binding
	 * exposes.
	 *
	 * Returns the constructor-injected override when present;
	 * otherwise constructs the production default wired against
	 * `${pluginRootDir}/templates` with the WordPress stylesheet /
	 * template directory probes the resolver provides by default.
	 */
	private function resolveTemplateResolver(): TemplateResolver {
		if ( $this->templateResolverOverride !== null ) {
			return $this->templateResolverOverride;
		}

		return new TemplateResolver( $this->pluginRootDir . DIRECTORY_SEPARATOR . 'templates' );
	}

	/**
	 * Resolve the §8.2 / §8.4 {@see ViewModelArrayDumper} the
	 * registry binding exposes.
	 */
	private function resolveArrayDumper(): ViewModelArrayDumper {
		if ( $this->arrayDumperOverride !== null ) {
			return $this->arrayDumperOverride;
		}

		return new ViewModelArrayDumper();
	}

	/**
	 * Resolve the §11 {@see LinkAttributeBuilder} the registry
	 * binding exposes.
	 */
	private function resolveLinkBuilder(): LinkAttributeBuilder {
		if ( $this->linkBuilderOverride !== null ) {
			return $this->linkBuilderOverride;
		}

		return new LinkAttributeBuilder();
	}

	/**
	 * Resolve the §10.8 {@see DisclaimerBuilder} the registry
	 * binding exposes.
	 */
	private function resolveDisclaimerBuilder(): DisclaimerBuilder {
		if ( $this->disclaimerBuilderOverride !== null ) {
			return $this->disclaimerBuilderOverride;
		}

		return new DisclaimerBuilder();
	}

	/**
	 * Resolve the §10.4 / §12 {@see ImagePresentationBuilder} the
	 * registry binding exposes.
	 */
	private function resolveImageBuilder(): ImagePresentationBuilder {
		if ( $this->imageBuilderOverride !== null ) {
			return $this->imageBuilderOverride;
		}

		return new ImagePresentationBuilder();
	}

	/**
	 * Resolve the §9.1 {@see ProductCardViewModelFactory} the
	 * registry binding exposes.
	 *
	 * @param LinkAttributeBuilder     $linkBuilder     §11 builder bound into the registry.
	 * @param DisclaimerBuilder        $disclaimerBld   §10.8 builder bound into the registry.
	 * @param ImagePresentationBuilder $imageBuilder    §10.4 / §12 builder bound into the registry.
	 */
	private function resolveFactory(
		LinkAttributeBuilder $linkBuilder,
		DisclaimerBuilder $disclaimerBld,
		ImagePresentationBuilder $imageBuilder
	): ProductCardViewModelFactory {
		if ( $this->factoryOverride !== null ) {
			return $this->factoryOverride;
		}

		return new ProductCardViewModelFactory(
			$linkBuilder,
			$disclaimerBld,
			$imageBuilder
		);
	}

	/**
	 * Resolve the {@see ProductCardRenderer} the registry binding
	 * exposes.
	 *
	 * @param TemplateResolver     $resolver Resolver bound into the registry.
	 * @param ViewModelArrayDumper $dumper   Dumper bound into the registry.
	 */
	private function resolveRenderer(
		TemplateResolver $resolver,
		ViewModelArrayDumper $dumper
	): ProductCardRenderer {
		if ( $this->rendererOverride !== null ) {
			return $this->rendererOverride;
		}

		return new ProductCardRenderer( $resolver, $dumper );
	}
}
