<?php
/**
 * REST service provider.
 *
 * Wires the Phase-6 slice 6a REST API infrastructure foundation into
 * the affilun bootstrap pipeline. This provider does
 * NOT register any concrete REST endpoint — that is the responsibility
 * of subsequent slices (6b+) which append their own
 * {@see \Affilun\Presentation\Rest\Routing\RouteDefinition}
 * values to the bound {@see RouteRegistrar} from their own boot()
 * methods.
 *
 * The provider's responsibilities are:
 *
 *   1. Bind the §3.2 / §3.3
 *      {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy}
 *      under {@see self::BINDING_CAPABILITY_POLICY} so REST
 *      controllers and permission callbacks resolve their
 *      required-capability OR-list through one canonical seam.
 *
 *   2. Bind the §4.3 / §15.2
 *      {@see \Affilun\Presentation\Rest\Response\ProviderExceptionMapper}
 *      under {@see self::BINDING_PROVIDER_EXCEPTION_MAPPER} so REST
 *      controllers translate Phase-3 provider exceptions into the
 *      canonical error envelope through one canonical seam.
 *
 *   3. Bind a fresh {@see RouteRegistrar} under
 *      {@see self::BINDING_ROUTE_REGISTRAR} so subsequent slices'
 *      service providers can resolve it from the registry and
 *      append their concrete
 *      {@see \Affilun\Presentation\Rest\Routing\RouteDefinition}
 *      values from their own boot() methods.
 *
 *   4. Subscribe a single `rest_api_init` action callback that
 *      drives the bound registrar's
 *      {@see RouteRegistrar::register()} so every accumulated route
 *      is forwarded to WordPress at the canonical registration
 *      moment.
 *
 * Resolution policy:
 *
 *   - Each collaborator is optional; omitting one causes the
 *     provider to construct the production default during
 *     register(). Production wiring uses the `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 \Affilun\Bootstrap\ServiceProviderRegistry::registerAll()}
 *     rather than at the first REST request.
 *
 *   - The bound registrar is also cached on the provider so the
 *     boot() action callback can capture it without re-resolving
 *     the registry binding.
 *
 * @package Affilun\Presentation\Rest
 */

declare(strict_types=1);

namespace Affilun\Presentation\Rest;

use Affilun\Bootstrap\HookRegistrar;
use Affilun\Bootstrap\ServiceProvider;
use Affilun\Bootstrap\ServiceProviderRegistry;
use Affilun\Presentation\Rest\Permission\CapabilityPolicy;
use Affilun\Presentation\Rest\Response\ProviderExceptionMapper;
use Affilun\Presentation\Rest\Routing\RouteRegistrar;

/**
 * Binds the Phase-6 slice 6a REST infrastructure collaborators and
 * subscribes the `rest_api_init` action that drives the bound
 * {@see RouteRegistrar}.
 *
 * See the file-level docblock for the slice-6a scope (no concrete
 * endpoints) and the resolution policy.
 */
final class RestServiceProvider implements ServiceProvider {

	/**
	 * Registry key under which the §3.2 / §3.3
	 * {@see CapabilityPolicy} is bound.
	 */
	public const BINDING_CAPABILITY_POLICY = 'affilun.rest.capability_policy';

	/**
	 * Registry key under which the §4.3 / §15.2
	 * {@see ProviderExceptionMapper} is bound.
	 */
	public const BINDING_PROVIDER_EXCEPTION_MAPPER = 'affilun.rest.provider_exception_mapper';

	/**
	 * Registry key under which the {@see RouteRegistrar} is bound.
	 */
	public const BINDING_ROUTE_REGISTRAR = 'affilun.rest.route_registrar';

	/**
	 * §3.2 / §3.3 capability policy override.
	 *
	 * When null, the provider constructs a fresh
	 * {@see CapabilityPolicy} during register().
	 */
	private ?CapabilityPolicy $policyOverride;

	/**
	 * §4.3 / §15.2 provider-exception mapper override.
	 *
	 * When null, the provider constructs a fresh
	 * {@see ProviderExceptionMapper} during register().
	 */
	private ?ProviderExceptionMapper $exceptionMapperOverride;

	/**
	 * Route-registrar override.
	 *
	 * When null, the provider constructs a fresh
	 * {@see RouteRegistrar} during register() with the default
	 * WordPress-forwarding registration callable.
	 */
	private ?RouteRegistrar $registrarOverride;

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

	/**
	 * Construct a REST service provider with optional overrides for
	 * each collaborator.
	 *
	 * @param CapabilityPolicy|null        $policyOverride          §3.2 / §3.3 policy override.
	 * @param ProviderExceptionMapper|null $exceptionMapperOverride §4.3 / §15.2 mapper override.
	 * @param RouteRegistrar|null          $registrarOverride       Route-registrar override.
	 */
	public function __construct(
		?CapabilityPolicy $policyOverride = null,
		?ProviderExceptionMapper $exceptionMapperOverride = null,
		?RouteRegistrar $registrarOverride = null
	) {
		$this->policyOverride          = $policyOverride;
		$this->exceptionMapperOverride = $exceptionMapperOverride;
		$this->registrarOverride       = $registrarOverride;
	}

	/**
	 * Bind every Phase-6 slice-6a REST infrastructure collaborator
	 * into the registry.
	 *
	 * Resolution is eager so a misconfiguration surfaces during
	 * {@see ServiceProviderRegistry::registerAll()} rather than at
	 * the first REST request.
	 *
	 * @param ServiceProviderRegistry $registry Registry driving this
	 *                                          bootstrap pass.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		$policy    = $this->policyOverride ?? new CapabilityPolicy();
		$mapper    = $this->exceptionMapperOverride ?? new ProviderExceptionMapper();
		$registrar = $this->registrarOverride ?? new RouteRegistrar();

		$this->resolvedRegistrar = $registrar;

		$registry->bind( self::BINDING_CAPABILITY_POLICY, $policy );
		$registry->bind( self::BINDING_PROVIDER_EXCEPTION_MAPPER, $mapper );
		$registry->bind( self::BINDING_ROUTE_REGISTRAR, $registrar );
	}

	/**
	 * Subscribe a `rest_api_init` action callback that drives the
	 * bound {@see RouteRegistrar::register()}.
	 *
	 * The callback is registered as a public method on this provider
	 * (rather than as a closure) so the {@see HookRegistrar}'s
	 * introspection surface carries a stable, named callable that
	 * tests and diagnostics can identify without unwrapping a
	 * closure.
	 *
	 * @param HookRegistrar $hooks Shared hook registrar.
	 */
	public function boot( HookRegistrar $hooks ): void {
		$hooks->addAction( 'rest_api_init', array( $this, 'onRestApiInit' ) );
	}

	/**
	 * `rest_api_init` action callback.
	 *
	 * Drives {@see RouteRegistrar::register()} so every
	 * {@see \Affilun\Presentation\Rest\Routing\RouteDefinition}
	 * appended by subsequent slices' service providers (slice 6b+)
	 * is forwarded to WordPress at the canonical registration
	 * moment.
	 *
	 * Public because {@see HookRegistrar::addAction()} stores the
	 * callable as an `array($this, 'onRestApiInit')` tuple, which
	 * WordPress invokes with public-method visibility.
	 */
	public function onRestApiInit(): void {
		if ( $this->resolvedRegistrar === null ) {
			return;
		}

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