<?php

namespace apexl\Io;

use apexl\Config\Exceptions\InvalidConfigException;
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\middleware\AddRouteToHttpPaths;
use apexl\Io\modules\system\services\ModuleManager;
use apexl\Io\services\InstallChecker;
use apexl\Io\services\Output;
use apexl\Io\WhoopsHandlers\JsonRecursionErrorHandler;
use app\vendor\apexl\io\src\Io\middleware\AddBuildHeader;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
use Slim\Middleware\BodyParsingMiddleware;
use Slim\Middleware\MethodOverrideMiddleware;
use Whoops\Handler\JsonResponseHandler;
use Whoops\Handler\PrettyPageHandler;
use Zeuxisoo\Whoops\Slim\WhoopsMiddleware;

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 InstallChecker $installChecker,
        private Output $outputManager,
        private RouteManager $routeManager,
        private HookManager $hookManager,
    ) {}

    /**
     * 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();
            $this->addMiddleware();

            /**
             * 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->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);
    }

    private function setErrorOptions(): void
    {
        ini_set('display_errors', config('app.error.display'));
        ini_set('display_startup_errors', config('app.error.display'));
        error_reporting(config('app.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
    {
        //initialise the base containers.
        try {
            $this->preFlightChecks();
        } catch (Exception $e) {
            //We errored on preflight. Log the error, attempt to start the base app and return.
            $this->outputManager->addSystemError($e);

            throw $e;
        }

        //Then create the app
        //finally loop over the modules again so we can register routes.
        $this->moduleManager->init();
    }

    /**
     * @throws InvalidConfigException
     */
    protected function preFlightChecks(): void
    {
        //check if we have the required config set:
        $this->testAvailableConfig();

        //check if we have vault config. If not we need to install.
        if ($this->installChecker->installRequired && !$this->isInstalled()) {
            if (!$this->installChecker->onInstallPath) {
                $this->outputManager->addResponse(null, ['redirect' => $this->installChecker->installPath], false);
                $this->outputManager->addMessage(
                    'requires_install',
                    'error',
                    sprintf(
                        'This Application requires installation. Please set {app: {installRequired: FALSE}} in config, or install the application. Install Path: %s',
                        $this->installChecker->installPath
                    )
                );
            }
        }
    }

    /**
     * @Todo allow other modules to specify required configuration.
     * @Todo allow for other methods to easily test for any missing config, required or not.
     * @throws InvalidConfigException
     */
    protected function testAvailableConfig(): void
    {
        $missingConfig = [];
        $requiredConfig = [
            "app.environment",
            "app.allowed_domains",
        ];
        foreach ($requiredConfig as $key) {
            if (config($key) === null) {
                $missingConfig[] = $key;
            }
        }

        if ($missingConfig) {
            throw new InvalidConfigException(
                sprintf('Required application config is missing: (%s)', implode(',', $missingConfig))
            );
        }
    }

    public function isInstalled(): bool
    {
        return $this->installChecker->isInstalled;
    }

    public function addMiddleware(): void
    {
        $whoopsHandlers = $this->hookManager->processHook('whoopsHandlers', $this->whoopsHandlers());

        $this->application->add(AddBuildHeader::class);
        $this->application->add(AddRouteToHttpPaths::class);
        $this->application->addRoutingMiddleware();
        $this->application->add(MethodOverrideMiddleware::class);
        $this->application->add(BodyParsingMiddleware::class);
        $this->application->add(addCorsHeaders::class);
        $this->application->add(new WhoopsMiddleware(['enable' => true], $whoopsHandlers));
    }

    private function whoopsHandlers(): array
    {
        return [
            $this->application->getContainer()->get(JsonRecursionErrorHandler::class),
            config('app.environment') === 'local' ?
                $this->application->getContainer()->get(PrettyPageHandler::class) :
                $this->application->getContainer()->get(JsonResponseHandler::class),
        ];
    }

}
