<?php
/**
 * Logs REST service provider.
 *
 * Phase 11 slice 11d Bootstrap-layer service provider that wires
 * the §12.2 / §12.3 logs REST endpoints documented in
 * `specifications/rest-api-spec.md`:
 *
 *   - §12.2 `GET /clb/v1/logs`         → {@see LogsController::list()}.
 *   - §12.3 `GET /clb/v1/logs/summary` → {@see LogsController::summary()}.
 *
 * Bindings exposed (registry keys):
 *
 *   - {@see self::BINDING_LIST_LOGS_USE_CASE}    — §12.2 use case.
 *   - {@see self::BINDING_LOGS_SUMMARY_USE_CASE} — §12.3 use case.
 *   - {@see self::BINDING_LOG_MASKER}            — masker shared by both.
 *   - {@see self::BINDING_CONTROLLER}            — composed controller.
 *
 * Provider ordering — register() resolves bindings published by
 * earlier providers, so the bootstrap pipeline in
 * `commerce-link-builder.php` MUST register the
 * {@see PersistenceServiceProvider} (publishes
 * {@see PersistenceServiceProvider::BINDING_LOG_REPOSITORY}) and
 * the {@see RestServiceProvider} (publishes
 * {@see RestServiceProvider::BINDING_ROUTE_REGISTRAR}) before
 * this one.
 *
 * @package CommerceLinkBuilder\Presentation\Rest
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Presentation\Rest;

use CommerceLinkBuilder\Application\Logs\LogMasker;
use CommerceLinkBuilder\Application\Logs\UseCase\GetLogsSummaryUseCase;
use CommerceLinkBuilder\Application\Logs\UseCase\ListLogsUseCase;
use CommerceLinkBuilder\Bootstrap\HookRegistrar;
use CommerceLinkBuilder\Bootstrap\ServiceProvider;
use CommerceLinkBuilder\Bootstrap\ServiceProviderRegistry;
use CommerceLinkBuilder\Infrastructure\Persistence\LogRepository;
use CommerceLinkBuilder\Infrastructure\Persistence\PersistenceServiceProvider;
use CommerceLinkBuilder\Presentation\Rest\Controller\LogsController;
use CommerceLinkBuilder\Presentation\Rest\Controller\RestRequestAdapter;
use CommerceLinkBuilder\Presentation\Rest\Permission\CapabilityPolicy;
use CommerceLinkBuilder\Presentation\Rest\Response\ProviderExceptionMapper;
use CommerceLinkBuilder\Presentation\Rest\Routing\RouteDefinition;
use CommerceLinkBuilder\Presentation\Rest\Routing\RouteRegistrar;
use CommerceLinkBuilder\Presentation\Rest\Validation\RequestValidator;
use Closure;
use LogicException;

/**
 * Binds the §12.2 / §12.3 use cases plus the composing
 * {@see LogsController}, then appends two
 * {@see RouteDefinition} values to the bound
 * {@see RouteRegistrar} so the §6a `rest_api_init` driver
 * forwards them to WordPress.
 *
 * See the file-level docblock for the bootstrap-pipeline ordering
 * requirement and the four registry keys this provider exposes.
 */
final class LogsServiceProvider implements ServiceProvider {

	/**
	 * Registry key under which the §12.2
	 * {@see ListLogsUseCase} is bound.
	 */
	public const BINDING_LIST_LOGS_USE_CASE = 'clb.rest.logs.list_use_case';

	/**
	 * Registry key under which the §12.3
	 * {@see GetLogsSummaryUseCase} is bound.
	 */
	public const BINDING_LOGS_SUMMARY_USE_CASE = 'clb.rest.logs.summary_use_case';

	/**
	 * Registry key under which the shared {@see LogMasker} is
	 * bound.
	 */
	public const BINDING_LOG_MASKER = 'clb.rest.logs.masker';

	/**
	 * Registry key under which the composed
	 * {@see LogsController} is bound.
	 */
	public const BINDING_CONTROLLER = 'clb.rest.logs_controller';

	/**
	 * §12.2 list use case override.
	 */
	private ?ListLogsUseCase $listUseCaseOverride;

	/**
	 * §12.3 summary use case override.
	 */
	private ?GetLogsSummaryUseCase $summaryUseCaseOverride;

	/**
	 * Shared masker override.
	 */
	private ?LogMasker $maskerOverride;

	/**
	 * Composed controller override.
	 */
	private ?LogsController $controllerOverride;

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

	/**
	 * Construct a logs service provider with optional overrides
	 * for each collaborator.
	 *
	 * Empty / no-arg construction is the production default; tests
	 * pass overrides one axis at a time so the controller graph
	 * can be exercised against synthetic use cases without standing
	 * up the persistence stack.
	 *
	 * @param ListLogsUseCase|null       $listUseCaseOverride    §12.2 use case override.
	 * @param GetLogsSummaryUseCase|null $summaryUseCaseOverride §12.3 use case override.
	 * @param LogMasker|null             $maskerOverride         Shared masker override.
	 * @param LogsController|null        $controllerOverride     Composed controller override.
	 */
	public function __construct(
		?ListLogsUseCase $listUseCaseOverride = null,
		?GetLogsSummaryUseCase $summaryUseCaseOverride = null,
		?LogMasker $maskerOverride = null,
		?LogsController $controllerOverride = null
	) {
		$this->listUseCaseOverride    = $listUseCaseOverride;
		$this->summaryUseCaseOverride = $summaryUseCaseOverride;
		$this->maskerOverride         = $maskerOverride;
		$this->controllerOverride     = $controllerOverride;
	}

	/**
	 * Bind the §12.2 / §12.3 use cases plus the composed
	 * {@see LogsController}, then append two
	 * {@see RouteDefinition} values to the bound
	 * {@see RouteRegistrar}.
	 *
	 * @param ServiceProviderRegistry $registry Registry driving
	 *                                          this bootstrap pass.
	 *
	 * @throws LogicException When the persistence or REST provider
	 *                        has not registered before this one.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		$logs   = $this->resolveLogRepository( $registry );
		$masker = $this->maskerOverride ?? new LogMasker();

		$listUseCase    = $this->listUseCaseOverride ?? new ListLogsUseCase( $logs, $masker );
		$summaryUseCase = $this->summaryUseCaseOverride ?? new GetLogsSummaryUseCase( $logs, $masker );

		$controller = $this->controllerOverride ?? new LogsController(
			$listUseCase,
			$summaryUseCase,
			new ProviderExceptionMapper(),
			new CapabilityPolicy(),
			new RequestValidator(),
			new RestRequestAdapter()
		);

		$this->resolvedController = $controller;

		$registry->bind( self::BINDING_LOG_MASKER, $masker );
		$registry->bind( self::BINDING_LIST_LOGS_USE_CASE, $listUseCase );
		$registry->bind( self::BINDING_LOGS_SUMMARY_USE_CASE, $summaryUseCase );
		$registry->bind( self::BINDING_CONTROLLER, $controller );

		$registrar = $this->resolveRouteRegistrar( $registry );
		$this->appendRoutes( $registrar );
	}

	/**
	 * No-op — routes are registered via
	 * {@see RestServiceProvider}'s `rest_api_init` driver.
	 *
	 * @param HookRegistrar $hooks Shared hook registrar.
	 */
	public function boot( HookRegistrar $hooks ): void {
		unset( $hooks );
	}

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

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

		return $logs;
	}

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

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

		return $registrar;
	}

	/**
	 * Append the two §12.2 / §12.3 {@see RouteDefinition} values
	 * to the supplied registrar.
	 *
	 * @param RouteRegistrar $registrar Bound registrar.
	 */
	private function appendRoutes( RouteRegistrar $registrar ): void {
		$permissionCallback = static function ( $request = null ): bool {
			unset( $request );
			return true;
		};

		$registrar->add(
			new RouteDefinition(
				RestNamespace::FULL,
				'/logs',
				RouteDefinition::METHOD_GET,
				LogsController::ROUTE_TOKEN_LIST,
				array(),
				$this->buildRouteCallback( 'list' ),
				$permissionCallback
			)
		);

		$registrar->add(
			new RouteDefinition(
				RestNamespace::FULL,
				'/logs/summary',
				RouteDefinition::METHOD_GET,
				LogsController::ROUTE_TOKEN_SUMMARY,
				array(),
				$this->buildRouteCallback( 'summary' ),
				$permissionCallback
			)
		);
	}

	/**
	 * Build a route-callback {@see Closure} that dispatches to the
	 * named controller method and adapts the returned envelope
	 * onto a {@see \WP_REST_Response} (or the pure-unit-mode array
	 * shim).
	 *
	 * @param string $method One of `'list'`, `'summary'`.
	 */
	private function buildRouteCallback( string $method ): Closure {
		return function ( mixed $request ) use ( $method ): mixed {
			$controller = $this->resolvedController;
			if ( $controller === null ) {
				$message = 'LogsServiceProvider route callback invoked before register() ran.';
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
				throw new LogicException( $message );
			}

			$envelope = match ( $method ) {
				'list'    => $controller->list( $request ),
				'summary' => $controller->summary( $request ),
				default   => $this->raiseUnknownMethod( $method ),
			};

			$status = isset( $envelope[ LogsController::META_HTTP_STATUS ] )
				&& is_int( $envelope[ LogsController::META_HTTP_STATUS ] )
					? $envelope[ LogsController::META_HTTP_STATUS ]
					: 200;

			unset( $envelope[ LogsController::META_HTTP_STATUS ] );

			if ( class_exists( '\\WP_REST_Response' ) ) {
				return new \WP_REST_Response( $envelope, $status );
			}

			return array(
				'data'   => $envelope,
				'status' => $status,
			);
		};
	}

	/**
	 * Raise a {@see LogicException} for an unknown route-callback
	 * method name.
	 *
	 * @param string $method Offending method name.
	 *
	 * @return never
	 *
	 * @throws LogicException Always.
	 */
	private function raiseUnknownMethod( string $method ): array {
		$message = sprintf(
			'LogsServiceProvider route callback received unknown method "%s".',
			$method
		);
		// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
		throw new LogicException( $message );
	}
}
