<?php
// phpcs:ignoreFile -- Direct-access guard must stay at the top for Plugin Check; the file only defines the global template helper.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Global `affilun_template()` template-loading helper.
 *
 * Implements the §6.5 receiving-signature
 * `function affilun_template( string $key, array $viewModel ): void`
 * documented in `specifications/rendering-and-template-spec.md`
 * §6.5. Templates and theme overrides call this helper at the
 * top of a partial whenever they need to delegate sub-card
 * rendering ("render the inner card from the surrounding grid")
 * back through the plugin's whitelist resolver.
 *
 * Resolver registration:
 *
 *   - The helper looks up the active
 *     {@see \Affilun\Application\Render\Template\TemplateResolver}
 *     via the `affilun_template_resolver` filter. The plugin
 *     bootstrap (slice 5h) is responsible for registering a
 *     filter callback that returns the singleton resolver
 *     instance. Until the bootstrap runs the filter returns
 *     null and the helper degrades silently.
 *
 *   - When no resolver is registered, when the key fails
 *     §6.4 grammar / whitelist validation, or when the
 *     resolver throws
 *     {@see \Affilun\Application\Render\Template\TemplateResolutionException},
 *     the helper is a silent no-op per §13.3 graceful-
 *     degradation and §23.1 "エラー詳細を一般ユーザーに見せない".
 *     Renderer-side error reporting (admin notices, log lines)
 *     happens at the
 *     {@see \Affilun\Application\Render\ProductCardRenderer}
 *     boundary, not here, so a template-side helper failure
 *     never bubbles a raw exception to a public page.
 *
 * Read-only contract:
 *
 *   - `$viewModel` is passed by value (PHP array semantics) and
 *     re-introduced inside a static closure that receives only
 *     the resolved path and the array. Helper-internal locals
 *     (`$resolver`, `$templateKey`, `$resolvedPath`, the caught
 *     `$e`) are NOT visible to the included template, matching
 *     the §6.5 "テンプレート側での参照渡し変更を禁止" posture.
 *
 *   - The helper does not echo, return, or sanitize: rendering
 *     and escaping are entirely the template's responsibility.
 *
 * This file is loaded once by the plugin bootstrap. The
 * `function_exists` guard is intentional so a stray double-
 * require during testing does not redeclare the symbol.
 *
 * @package Affilun\Application\Render
 */

// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- "affilun" is a registered short prefix per phpcs.xml.dist; WPCS 3.x still flags sub-4-character prefixes on global function names regardless.
if ( ! function_exists( 'affilun_template' ) ) {
	/**
	 * §6.5 receiving-signature template-loading helper.
	 *
	 * Resolves `$key` against the registered
	 * {@see \Affilun\Application\Render\Template\TemplateResolver}
	 * (looked up via the `affilun_template_resolver` filter) and
	 * `require`-s the resolved file with `$viewModel` as a local
	 * read-only copy. Failure modes are silent no-ops per §13.3 /
	 * §23.1; see the file-level docblock for the rationale.
	 *
	 * @param string               $key       §6.4 template key.
	 * @param array<string, mixed> $viewModel Read-only view-model array.
	 */
	function affilun_template( string $key, array $viewModel ): void {
		$resolver = apply_filters( 'affilun_template_resolver', null );
		if ( ! $resolver instanceof \Affilun\Application\Render\Template\TemplateResolver ) {
			// No resolver registered — silent no-op per §13.3 graceful degradation.
			return;
		}

		try {
			$templateKey  = \Affilun\Application\Render\Template\TemplateKey::fromString( $key );
			$resolvedPath = $resolver->resolve( $templateKey );
		} catch ( \Throwable $e ) {
			// §23.1 一般ユーザー向け: never expose internal errors from a template helper.
			return;
		}

		// Read-only contract: $viewModel is passed by value from the caller; the
		// static closure isolates the include scope so renderer-internal locals
		// ($resolver, $templateKey, $resolvedPath, $e) cannot leak into the
		// template's variable namespace.
		(
			// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $viewModel is consumed by the required template file via local scope, not by the closure body.
			static function ( string $__aflPath, array $viewModel ): void {
				require $__aflPath;
			}
		)( $resolvedPath, $viewModel );
	}
}
// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
