<?php

namespace apexl\Io;

use apexl\Config\ConfigurationInterface;
use apexl\Config\Exceptions\MissingConfigException;
use apexl\Io\controllers\CatchAllController;
use apexl\Io\enums\HttpMethod;
use apexl\Io\exceptions\OptionsRequestException;
use apexl\Io\includes\HookManager;
use apexl\Io\includes\RouteManager;
use apexl\Io\includes\System;
use apexl\Io\middleware\AddCorsHeaders;
use apexl\Io\modules\system\services\MiddlewareCollector;
use apexl\Io\modules\system\services\ModuleManager;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;

final readonly class Client
{
    final public const string HOOK__BOOT = 'client.boot';

    protected System $system;

    /**
     * @throws Exception
     */
    public function __construct(
        public App $application,
        private ModuleManager $moduleManager,
        private RouteManager $routeManager,
        private HookManager $hookManager,
        private MiddlewareCollector $middlewareCollector,
        private ConfigurationInterface $config,
    ) {}

    /**
     * Bootstrap and run Io
     * UPDATED:: Now defaults to loading PHP files at the specified config directory. Passing anything other than __PHP__ here will result un an attempt to load the specific file, whatever the extension.
     * @throws Exception
     */
    public function start(): void
    {
        //In order to correctly pass cors and output something, we need to make sure we catch any bootstrap errors
        //store them in the systemError output, and continue to try and start the application.
        try {
            $this->bootstrap();

            /**
             * Catch-all route to serve a 404 Not Found page if none of the routes match
             * NOTE: make sure this route is defined last
             */
            $this->application->map(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
                '/{routes:.+}',
                CatchAllController::class);

            $this->application->run();

        } catch (OptionsRequestException) {
            return;
        }
    }

    /**
     * Bootstrap IO CMF
     * @throws OptionsRequestException
     * @throws Exception
     */
    public function bootstrap(): void
    {
        $this->setErrorOptions();
        $this->shortCircuitOptionsRequest();
        $this->processModules();
        $this->addMiddleware();
        $this->routeManager->buildRoutes();

        // Attempt to initialise the System
        /** Add a boot hook so modules can set various defaults as soon as the start process is finished*/
        $this->hookManager->processHook(self::HOOK__BOOT);
    }

    /**
     * @throws MissingConfigException
     */
    private function setErrorOptions(): void
    {
        ini_set('display_errors', $this->config->get('error.display'));
        ini_set('display_startup_errors', $this->config->get('error.display'));
        error_reporting($this->config->get('error.reporting'));

    }

    /**
     * If we are processing an OPTIONS request we short-circuit here to immediately return with CORS headers
     * This prevents further unnecessary processing of modules
     *
     * @throws OptionsRequestException
     * @todo Check to see if specific route exists in module and 404 if not
     *
     */
    private function shortCircuitOptionsRequest(): void
    {
        if (!$this->isCli() && HttpMethod::OPTIONS->equals($_SERVER['REQUEST_METHOD'])) {
            $this->application->add(AddCorsHeaders::class);
            $this->application->options(
                '{routes:.+}',
                fn(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface => $response
            );

            $this->application->run();

            throw new OptionsRequestException();
        }
    }

    public static function isCli(): bool
    {
        static $isCli;
        if (!isset($isCli)) {
            $isCli = php_sapi_name() === 'cli';
        }

        return $isCli;
    }

    /**
     * @throws Exception
     */
    private function processModules(): void
    {
        $this->moduleManager->init();
    }

    public function addMiddleware(): void
    {
        $weightedMiddlewares = $this->middlewareCollector->collect();

        foreach ($weightedMiddlewares as $middlewareCollection) {
            foreach ($middlewareCollection as $middleware) {
                $this->application->add($middleware);
            }
        }
    }
}
