<?php

namespace Planaday\Platform\WordpressConfig;

use Exception;
use Planaday\Platform\PlatformAbstraction\DataHandler;
use Planaday\Utility\Debug\Log\Logging;
use Planaday\Utility\Debug\PluginFeatureTimer;
use Planaday\Utility\DependencyInjection\Container;
use Planaday\Utility\DependencyInjection\InjectionImpossibleException;
use Planaday\Utility\Endpoints\ApiController;
use Planaday\Utility\Endpoints\ApiEndpoint;
use Planaday\Utility\Endpoints\ApiExceptionMapping;
use Planaday\Utility\Endpoints\BackofficeEndpoint;
use Planaday\Utility\Endpoints\RequestData;
use Planaday\Utility\PluginData;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionType;
use ReflectionUnionType;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

class WPRestInitializer {
	use Logging;

	public function __construct(
		protected PluginData $pluginData,
		protected Container $registry,
		protected PluginFeatureTimer $timer
	) {}

	public function registerEndpoints(): void {
		$stop = $this->timer->start("registering api endpoints");
		$this->registerDynamicRoutes();
		$stop();
	}

	public function registerDynamicRoutes(): void {
		$classesToRegister = ApiController::REGISTERED_CONTROLLERS;
		foreach ($classesToRegister as $class){
			$this->registerApiController($class);
		}
	}

	/**
	 * @param class-string $class
	 *
	 * @return void
	 */
	protected function registerApiController( string $class ) : void{
		try{
			$reflection = new ReflectionClass($class);
		} catch ( ReflectionException $e ) {
			$this->logError("failed to reflect on api controller $class, error:" . $e->getMessage());
			return;
		}

		$classAttributes = $reflection->getAttributes(ApiController::class);
		if ( count($classAttributes) === 0 ) {
			$this->logError("api controller $class does not have ApiController attribute");
			return;
		}
		if ( count($classAttributes) > 1 ) {
			$this->logError("api controller $class has multiple ApiController attributes");
			return;
		}
		/** @var ApiController $classAttribute */
		$classAttribute = $classAttributes[0]->newInstance();

		$routeNamespace = $this->pluginData->getFullPrefix() . '/v' . $classAttribute->version;

		$baseRoute = $classAttribute->baseRoute ?? '';
		if ( !empty($baseRoute)){
			if ( $baseRoute[0] !== '/') $baseRoute = '/' . $baseRoute;
			if ( $baseRoute[strlen($baseRoute) - 1] === '/') $baseRoute = substr($baseRoute, 0, -1);
		}

		$functions = $reflection->getMethods( ReflectionMethod::IS_PUBLIC );
		$countRegisteredRoutes = 0;

		/** @var ApiEndpoint $routeAttribute */
		foreach ($functions as $function) foreach ($function->getAttributes(ApiEndpoint::class) as $routeAttribute){
			if ($this->registerApiRoute(
				$routeNamespace,
				$baseRoute,
				$class,
				$routeAttribute->newInstance(),
				$function
			)) $countRegisteredRoutes++;
		}

		if ( $countRegisteredRoutes === 0 ){
			$this->logWarning("api controller $class has no registered routes");
		} else {
			$this->logInfo("registered $countRegisteredRoutes routes for api controller $class");
		}
	}

	protected function registerApiRoute(
		string $routeNameSpace,
		string $routeBase,
		string $class,
		ApiEndpoint $routeAttribute,
		ReflectionMethod $method
	) : bool {
		if ( empty($routeAttribute->route) ){
			$this->logError("api route for $class::$method->name has no route");
			return false;
		}

		if (!$this->methodCanBeEndpoint($method)){
			$this->logError("api route for $class::$method->name has an invalid signature");
			return false;
		}

		if ( $routeAttribute->route[0] !== '/' ) {
			$routeBase .= '/';
		}

		$route = $routeBase . $routeAttribute->route;

		$this->logInfo("registering api route $routeNameSpace$route for $class::$method->name");
		register_rest_route(
			$routeNameSpace,
			$route,
			array(
				'methods' => $routeAttribute->method->value,
				'callback' => function (WP_REST_Request $request) use ($class, $method) {
					return $this->doRequest($request, $class, $method);
				},
				'permission_callback' => __return_true(...)
			)
		);

		return true;
	}

	protected function convertWpRequestToRequestData( WP_REST_Request $request ): RequestData {
		return (new RequestData())
			->setBody($request->get_json_params() ?? [])
			->setUrlParams($request->get_url_params() ?? [])
			->setHeaders($request->get_headers() ?? []);
	}

	/**
	 * @template T
	 *
	 * @param WP_REST_Request $request
	 * @param class-string<T> $class
	 * @param ReflectionMethod $method
	 *
	 * @return mixed
	 */
	protected function doRequest( WP_REST_Request $request, string $class, ReflectionMethod $method) : mixed{
		$scope = $this->registry->getNewScope();
		try {
			$requestData = $this->convertWpRequestToRequestData($request);
			$isBackofficeEndpoint = count($method->getAttributes(BackofficeEndpoint::class)) > 0;
			if ($isBackofficeEndpoint) {
				$sentKey = $requestData->getHeaders()['x_planaday_backoffice_key'][0] ?? null;

				if ( empty($sentKey) )
					return new WP_Error(401, "Backoffice key not provided");

				try {
					$dataHandler = $this->registry->getInstance(DataHandler::class, $scope);
				} catch ( InjectionImpossibleException){
					return new WP_Error(500, "Failed to check backoffice key");
				}

				if (!$dataHandler->checkBackofficeKey($sentKey))
					return new WP_Error(403, "Backoffice key not valid");
			}

			$exceptionMappings = $method->getAttributes(ApiExceptionMapping::class);

			if ( !$method->isStatic()){
				try {
						/** @var T $controllerInstance */
						$controllerInstance = $this->registry->getInstance($class, $scope);
				} catch ( InjectionImpossibleException $e){
					return new WP_Error(500, "The class $class could not be created: " . $e->getMessage());
				}
			}

			try {
				return $method->invokeArgs($controllerInstance ?? null, [$requestData]);
			} catch ( ReflectionException $e ){
				return new WP_Error(500, "Invocation of function $class::$method->name failed: " . $e->getMessage());
			} catch ( Exception $e ){
				foreach ($exceptionMappings as $exceptionMapping){
					/** @var ApiExceptionMapping $mapping */
					$mapping = $exceptionMapping->newInstance();

					if ( is_a($e, $mapping->exceptionClass) ){
						return new WP_REST_Response(
							$mapping->message ?? $e->getMessage(),
							$mapping->httpCode,
						);
					}
				}

				return new WP_Error(500, "Invocation of function $class::$method->name failed: " . $e->getMessage());
			}
		} finally {
			$scope->destroy();
		}
	}

	protected function methodCanBeEndpoint(ReflectionMethod $method): bool {
		if ( $method->getNumberOfRequiredParameters() > 1) return false;
		if ( $method->getNumberOfParameters() === 0 ) return true;

		$firstParam = $method->getParameters()[0];
		return $this->typeIsRequestData( $firstParam->getType() );
	}

	protected function typeIsRequestData( ReflectionType $types ) : bool {
		if ($types instanceof ReflectionNamedType) return $types->getName() === RequestData::class;
		if ($types instanceof ReflectionUnionType) foreach ($types->getTypes() as $type){
			if ($type instanceof ReflectionNamedType && $type->getName() === RequestData::class) return true;
		}

		return false;
	}
}