<?php
/**
 * Click tracking REST service provider.
 *
 * @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\Infrastructure\Persistence\ClickEventRepository;
use Affilun\Infrastructure\Persistence\OptionStore;
use Affilun\Infrastructure\Persistence\PersistenceServiceProvider;
use Affilun\Presentation\Rest\Controller\ClicksController;
use Affilun\Presentation\Rest\Controller\RestRequestAdapter;
use Affilun\Presentation\Rest\Permission\CapabilityPolicy;
use Affilun\Presentation\Rest\Routing\RouteDefinition;
use Affilun\Presentation\Rest\Routing\RouteRegistrar;
use Closure;
use LogicException;

/**
 * Registers the public click-recording endpoint.
 */
final class ClicksServiceProvider implements ServiceProvider {

	/**
	 * Controller binding.
	 */
	public const BINDING_CONTROLLER = 'affilun.rest.clicks_controller';

	/**
	 * Optional controller override.
	 */
	private ?ClicksController $controllerOverride;

	/**
	 * Resolved controller.
	 */
	private ?ClicksController $resolvedController = null;

	/**
	 * @param ClicksController|null $controllerOverride Controller override.
	 */
	public function __construct( ?ClicksController $controllerOverride = null ) {
		$this->controllerOverride = $controllerOverride;
	}

	/**
	 * Register route and controller.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		$options   = $this->resolveOptionStore( $registry );
		$policy    = $this->resolveCapabilityPolicy( $registry );
		$registrar = $this->resolveRouteRegistrar( $registry );
		$clicks    = $this->resolveClickEventRepository( $registry );

		$controller = $this->controllerOverride ?? new ClicksController(
			$options,
			$policy,
			new RestRequestAdapter(),
			null,
			null,
			$clicks
		);

		$this->resolvedController = $controller;
		$registry->bind( self::BINDING_CONTROLLER, $controller );

		$permissionCallback = static function ( $request = null ): bool {
			unset( $request );
			return true;
		};

		$registrar->add(
			new RouteDefinition(
				RestNamespace::FULL,
				'/clicks',
				RouteDefinition::METHOD_POST,
				ClicksController::ROUTE_TOKEN_WRITE,
				array(),
				$this->buildRouteCallback(),
				$permissionCallback
			)
		);
	}

	/**
	 * No hooks; routes are appended at register time.
	 *
	 * @param HookRegistrar $hooks Hooks.
	 */
	public function boot( HookRegistrar $hooks ): void {
		unset( $hooks );
	}

	/**
	 * Build route callback.
	 *
	 * @phpstan-return Closure(mixed): mixed
	 */
	private function buildRouteCallback(): Closure {
		return function ( mixed $request ): mixed {
			if ( $this->resolvedController === null ) {
				throw new LogicException( 'ClicksServiceProvider route callback invoked before register() ran.' );
			}

			$envelope = $this->resolvedController->record( $request );
			$status   = isset( $envelope[ ClicksController::META_HTTP_STATUS ] )
				&& is_int( $envelope[ ClicksController::META_HTTP_STATUS ] )
					? $envelope[ ClicksController::META_HTTP_STATUS ]
					: 200;

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

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

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

	/**
	 * Resolve option store.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 *
	 * @throws LogicException When the binding is missing or invalid.
	 */
	private function resolveOptionStore( ServiceProviderRegistry $registry ): OptionStore {
		$key = PersistenceServiceProvider::BINDING_OPTION_STORE;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'ClicksServiceProvider requires PersistenceServiceProvider option store.' );
		}
		$options = $registry->get( $key );
		if ( ! $options instanceof OptionStore ) {
			throw new LogicException( 'ClicksServiceProvider expected OptionStore.' );
		}
		return $options;
	}

	/**
	 * Resolve optional click event repository.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 */
	private function resolveClickEventRepository( ServiceProviderRegistry $registry ): ?ClickEventRepository {
		$key = PersistenceServiceProvider::BINDING_CLICK_EVENT_REPOSITORY;
		if ( ! $registry->has( $key ) ) {
			return null;
		}
		$repository = $registry->get( $key );
		return $repository instanceof ClickEventRepository ? $repository : null;
	}

	/**
	 * Resolve capability policy.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 *
	 * @throws LogicException When the binding is missing or invalid.
	 */
	private function resolveCapabilityPolicy( ServiceProviderRegistry $registry ): CapabilityPolicy {
		$key = RestServiceProvider::BINDING_CAPABILITY_POLICY;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'ClicksServiceProvider requires RestServiceProvider capability policy.' );
		}
		$policy = $registry->get( $key );
		if ( ! $policy instanceof CapabilityPolicy ) {
			throw new LogicException( 'ClicksServiceProvider expected CapabilityPolicy.' );
		}
		return $policy;
	}

	/**
	 * Resolve route registrar.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 *
	 * @throws LogicException When the binding is missing or invalid.
	 */
	private function resolveRouteRegistrar( ServiceProviderRegistry $registry ): RouteRegistrar {
		$key = RestServiceProvider::BINDING_ROUTE_REGISTRAR;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'ClicksServiceProvider requires RestServiceProvider route registrar.' );
		}
		$registrar = $registry->get( $key );
		if ( ! $registrar instanceof RouteRegistrar ) {
			throw new LogicException( 'ClicksServiceProvider expected RouteRegistrar.' );
		}
		return $registrar;
	}
}
