<?php

namespace apexl\Io;

use apexl\Config\Configuration as ConfigSingleton;
use apexl\Io\enums\HttpMethod;
use apexl\Io\exceptions\OptionsRequestException;
use apexl\Io\includes\Hook;
use apexl\Io\includes\System;
use apexl\Io\includes\Utils;
use apexl\Io\middleware\addCorsHeaders;
use apexl\Io\services\Output;
use Dotenv\Dotenv;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Factory\AppFactory;
use Throwable;

class Client
{
    public static ?Client $instance;
    protected string $configDir;
    protected string $configFile = '__PHP__';
    protected string $coreConfigPath;
    protected System $system;

    private function __construct()
    {
        //set a few default
        $this->configDir = realpath(__DIR__ . '/../config');
    }

    public static function reset(): void
    {
        self::$instance = null;
    }

    /**
     * Set the application base path
     * @param $basePath
     */
    public function setBasePath($basePath): void
    {
        System::setBasePath($basePath);
    }

    /**
     * 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.
     */
    public function start($configFile = '__PHP__'): void
    {
        $this->setConfigFile($configFile);
        //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->system->start();
        } catch (OptionsRequestException) {
            return;
        } catch (Throwable $e) {
            Output::addSystemError($e);
            trigger_error(
                sprintf(
                    '[IO Error :: Start] Error %s - %s | Found in file %s on line %s. Thrown',
                    $e->getCode(),
                    $e->getMessage(),
                    $e->getFile(),
                    $e->getLine()
                ),
                E_USER_ERROR
            );
        }

    }

    public function setConfigFile(string $configFile = '__PHP__'): void
    {
        $this->configFile = $configFile;
    }

    /**
     * Bootstrap IO CMF
     * @throws OptionsRequestException
     * @throws Exception
     */
    public function bootstrap(): void
    {
        Dotenv::createImmutable(System::getBasePath() . '/..')->load();
        if (!isset($this->coreConfigPath)) {

            $this->initConfig();
        }

        ini_set('display_errors', config('app.error.display'));
        ini_set('display_startup_errors', config('app.error.display'));
        error_reporting(config('app.error.reporting'));

        $this->shortCircuitOptionsRequest();

        // Attempt to initialise the System
        $this->system = System::getInstance();
        $this->system->startApplication();
        /** Add a boot hook so modules can set various defaults as soon as the start process is finished*/
        Hook::processHook("boot", ConfigSingleton::getInstance());
    }

    /**
     * @throws Exception
     */
    public function initConfig(): void
    {
        //Not loading unspecified PHP? Attempt to
        if ($this->configFile !== '__PHP__') {
            $this->initConfigPath($this->configFile);
            ConfigSingleton::load($this->coreConfigPath);
        } else {
            $this->initConfigPath();
            //we're batch loading PHP files. Loop over the given directory and try to load any / all PHP files in it.
            $files = Utils::getFilesInDir(null, $this->coreConfigPath, ['php'], ['.local.php', '.example.php']);
            ksort($files, SORT_NATURAL); //ensure we load based on file order, not alpha (in case this matters).
            foreach ($files as $file) {
                ConfigSingleton::load($file);
            }
        }
    }

    private function initConfigPath($configFile = null): void
    {
        $this->coreConfigPath = sprintf(
            '%s%s',
            $this->configDir,
            DIRECTORY_SEPARATOR,
        );
        if (!is_null($configFile)) {
            $this->coreConfigPath .= ltrim(
                $this->configFile,
                DIRECTORY_SEPARATOR
            );
        }
    }

    /**
     * @throws OptionsRequestException
     */
    private function shortCircuitOptionsRequest(): void
    {
        if (!System::isCli() && HttpMethod::OPTIONS->equals($_SERVER['REQUEST_METHOD'])) {
            $app = AppFactory::create();
            $app->add(addCorsHeaders::class);
            $app->options(
                '{routes:.+}',
                function (ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
                    return $response;
                }
            );

            $app->run();

            throw new OptionsRequestException();
        }
    }

    /**
     * Turn Io into a singleton.
     * @return Client
     */
    public static function getInstance(): Client
    {
        if (!isset(self::$instance)) {
            self::$instance = new Client();
        }

        return self::$instance;
    }

    public function getConfigDir(): string
    {
        return $this->configDir;
    }

    /**
     * Set the primary configuration Dir for IO
     * @param $dir
     * @return $this
     */
    public function setConfigDir($dir): static
    {
        $this->configDir = realpath(rtrim((string) $dir, DIRECTORY_SEPARATOR));
        //check if we have a config cacheDir - if not, create a default one.
        $cacheDir = ConfigSingleton::getCacheDirectory();
        if ($cacheDir) {
            $this->setConfigCacheDir($cacheDir);
        }

        return $this;
    }

    /**
     * Set the directory used to store the amalgamated config files / data as cache.
     * @param $dir
     * @return $this
     */
    public function setConfigCacheDir($dir): Client
    {
        ConfigSingleton::setCacheDirectory($dir);

        return $this;
    }
}
