<?php

namespace apexl\Io\includes;

use apexl\Config\Configuration;
use apexl\Io\middleware\accessRequest\accessRequest;
use apexl\Io\middleware\addCorsHeaders;
use apexl\Io\middleware\bodyParser\bodyParserController;
use apexl\Io\modules\install\services\databaseTools;
use apexl\Io\modules\system\services\modules;
use apexl\Io\services\globalData;
use apexl\Io\services\Mailer;
use apexl\Io\services\Output;
use apexl\Io\services\pathUtility;
use apexl\Utils\Json\Json;
use apexl\Vault\Vault;
use app\vendor\apexl\io\src\Io\middleware\addBuildHeader;
use DI\Container;
use DI\ContainerBuilder;
use Exception;
use ForceUTF8\Encoding;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\App;
use Slim\Exception\HttpNotFoundException;
use Slim\Factory\AppFactory;
use Slim\Middleware\MethodOverrideMiddleware;
use Whoops\Handler\JsonResponseHandler;
use Zeuxisoo\Whoops\Slim\WhoopsMiddleware;

/**
 * Class System
 * @package apexl\Io\includes
 */
class System
{
    public const HOOK__GET_VARIABLE = 'system.getVariable';

    /** @var App */
    public static $app;

    /** @var self */
    public static $instance;

    /** @var modules */
    public static $module;
    public static $installing = false;
    /** @var Singleton */
    protected static $config;
    /** @var $pathUtility */
    protected static $pathUtility;
    /** @var Routes */
    protected static $routes;
    protected static $basePath;

    protected static $isInstalled = false;

    /**
     * System constructor.
     * @throws Exception
     */
    private function __construct()
    {
        self::$config = Configuration::getInstance();
        self::$pathUtility = pathUtility::getInstance();
        self::$module = modules::getInstance();
        $this->defineConstants();
    }

    /**
     * Allows us to keep the system class as a singleton.
     * @return System
     */
    public static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new System();
        }

        return self::$instance;
    }

    protected function defineConstants(): void
    {
        foreach (['VERSION' => self::$config->app->version ?? 1] as $key => $value) {
            if (!defined($key)) {
                define($key, $value);
            }
        }
    }

    public static function reset(): void
    {
        self::$app = null;
        self::$instance = null;
        self::$module = null;
        self::$installing = null;
        self::$config = null;
        self::$pathUtility = null;
        self::$routes = null;
        self::$basePath = null;
        self::$isInstalled = null;
    }

    public static function switchDatabase($name)
    {
        $database = Vault::getInstance();
        $database->setDefaultVault($name);
    }

    /**
     * Load and decode a json schema file.
     * @param $schemaPath
     * @return mixed
     * @todo add checks, defaults etc.
     */
    public static function loadSchema($schemaPath)
    {
        $schema = file_get_contents($schemaPath);

        return json_decode($schema);
    }

    public static function start()
    {
        //App not available? something went wrong when starting. In that case, we need to manually add CORS and drop out.
        if (!self::$app) {
            //No app? Something went very wrong, just return.
            return;
        }
        self::$app->add(bodyParserController::class);
        self::$app->add(accessRequest::class);
        self::$app->addRoutingMiddleware();
        self::$app->add(new MethodOverrideMiddleware());
        $whoopsHandlers = Hook::processHook('whoopsHandlers', [new JsonResponseHandler()]);
        self::$app->add(new WhoopsMiddleware(['enable' => true], $whoopsHandlers));
        self::$app->add(addCorsHeaders::class);
        self::$app->add(addBuildHeader::class);

        /**
         * Catch-all route to serve a 404 Not Found page if none of the routes match
         * NOTE: make sure this route is defined last
         */
        self::$app->map(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], '/{routes:.+}', function ($request, $response) {
            try {
                throw new HttpNotFoundException($request);
            } catch (Exception $e) {
                error_log(
                    "[IO Exception :: Routing] (Line " . $e->getLine() . " | " . $e->getFile() . ") - " . $e->getCode(
                    ) . " | " . $e->getMessage()
                );
                //did we thorw a true 404? or is there an exisitng system error?
                if (!Output::getSystemErrors()) {
                    //no errors? throw the 404, otherwise return the system error code..
                    return System::asJson($response, [
                        'title' => '404 Not Found',
                        'code' => 404,
                        'message' => 'The requested resource could not be found. Please verify the URI and try again.'
                    ], 404);

                } else {
                    //we got atleast one system error. return the code for the first one we got.
                    $existingErrors = Output::getSystemErrors() ?? [];
                    $error = array_shift($existingErrors);

                    return System::asJson($response, [], ($error['status'] ?? 500));
                }
            }
        });

        self::$app->run();
    }

    public static function asJson(Response $response, $data = [], $status = 200): Response
    {
        //grab the messages class, or the any overridden version of it.
        $output = self::getRegisteredService(Output::class) ?? Output::getInstance();
        $data = array_merge($data, $output::getOutput());
        $response->getBody()->write(json_encode($data));

        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus($status);
    }

    /**
     * Get actual service by base service FQDN (allows us to collect overridden services)
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public static function getRegisteredService($service): mixed
    {
        /** @var ?ContainerInterface $container */
        $container = self::$app->getContainer();

        return $container?->get($service);
    }

    public static function get($name)
    {
        return self::$app->$name;
    }

    public static function redirect($location, $status = 302): never
    {
        header('Location: ' . $location, true, $status);
        exit();
    }

    public static function writeConfig($key, $config)
    {
        Singleton::updateConfigFileByKey($key, $config);
        self::$config->reloadConfig();
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public static function makeRegisteredService(string $service, array $params = []): mixed
    {
        /** @var ?ContainerInterface $container */
        $container = self::$app->getContainer();

        return $container?->make($service, $params);
    }

    public static function output($messages, $route = '', $type = 'redirect'): array
    {
        $success = true;
        switch ($type) {
            case 'redirect':
                $event = 'redirectTo';
                break;
            case 'external':
                $event = 'redirectToExternal';
                break;
            case 'refresh':
                $event = 'refresh';
                break;
            case 'success':
                //do nothing
                break;
            case 'error':
                $success = false;
                break;
            default:
                $event = $type;
                break;
        }
        //build messages;
        $messagesData = [];
        if ($content = $messages['messages'] ?? false) {
            $messagesData = $content;
        } elseif ($content = $messages['content'] ?? false) {
            $messagesData[] = [
                'content' => $messages['content'],
                'type' => $success ? 'success' : 'error',
            ];
        } else {
            $messagesData = null;
        }
        $output['success'] = $success ?? 'success';
        $output['messages'] = $messagesData;
        $output['data'] = isset($messages['data']) ? $messages['data'] : '';
        if ($event) {
            $output[$event] = $route;
        }

        return $output;
    }

    public static function makeUTF8Safe($data)
    {
        $object = (object) [];
        foreach ($data as $key => $value) {
            $object->$key = Encoding::fixUTF8($value);
        }

        return $object;
    }

    public static function getBasePath()
    {
        return self::$basePath;
    }

    public static function setBasePath($basePath)
    {
        self::$basePath = $basePath;
    }

    public static function moduleIsInstalled($module): bool
    {
        //if we're installing the system then return false. Otherwise we can check in our system table.
        //@todo add system table check
        return !self::$installing;
    }

    public static function getInstalledModuleByName($name)
    {
        $module = self::getModuleByName($name);
        if ($module['installed'] ?? false) {
            return $module;
        }

        return false;
    }

    public static function getModuleByName($name)
    {
        $vault = Vault::getInstance();

        return $vault->select('modules')->fields()->where('name', $name)->fetchAssoc()->execute();
    }

    /**
     * @param $name
     * @param $data
     * @param int $site
     * @return mixed
     */
    public static function setVariable(string $name, $data, int $site = 1)
    {
        $vault = Vault::getInstance();
        $cleanedData = match (gettype($data)) {
            'array', 'object', 'boolean' => json_encode($data),
            default => $data,
        };
        //insert or update?
        if (self::hasVariable($name, $site)) {
            $vault->update('variables')->fields([
                'value' => $cleanedData,
                'site' => $site,
            ])->where('site', $site)
                ->where('name', $name)
                ->execute();

            return $data;
        }

        $vault->insert('variables')->fields([
            'name' => $name,
            'value' => $cleanedData,
            'site' => $site,
        ])->execute();

        return $data;
    }

    public static function hasVariable(string $name, int $site = 1): bool
    {
        //DB isn't available when it's not installed, so skip the query if we're running core install.
        if (System::$isInstalled) {
            $vault = Vault::getInstance();

            $var = $vault->
            select('variables')
                ->fields('count(*) as count')
                ->where('site', $site)
                ->where('name', $name)
                ->execute()
                ->fetchAssoc();

            return (int) $var['count'] !== 0;
        }

        return false;
    }

    /**
     * @return mixed
     */
    public static function getVariable(string $name, int $site = 1)
    {
        $result = null;
        //DB isn't available when it's not installed, so skip the query if we're running core installation.
        if (System::$isInstalled) {
            $vault = Vault::getInstance();
            $var = $vault->select('variables')
                ->fields()
                ->where('site', $site)
                ->where('name', $name)
                ->execute()
                ->fetchAssoc();

            if (($var['value'] ?? false) !== false) {
                $result = Json::isJson($var['value']) ?? $var['value'];
            }
        }

        $result = Hook::processHook(self::HOOK__GET_VARIABLE, $result, $name, $site);

        return Hook::processHook(sprintf('%s:%s', self::HOOK__GET_VARIABLE, $name), $result, $site);
    }

    public static function getVariables($site = 1): array
    {
        //DB isn't available when it's not installed, so skip the query if we're running core install.
        $vars = [];
        if (System::$isInstalled) {
            $vault = Vault::getInstance();
            $vars = $vault->select('variables')
                ->fields()
                ->where('site', $site)
                ->execute()
                ->fetchAll();

            foreach ($vars as $var) {
                if (($var->value ?? false) !== false) {
                    $vars[$var->name] = Json::isJson($var->value) ?? $var->value;
                }
            }
        }

        return $vars;
    }

    /**
     * @param $name
     * @param $site
     * @return void
     */
    public static function deleteVariable($name, $site = 1)
    {
        $vault = Vault::getInstance();
        $vault->delete('variables')->fields()->where('site', $site)->where('name', $name)->execute();
    }

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

        return $isCli;
    }

    public function startApplication()
    {
        //initialise the base containers.
        self::$module->getAvailableModules();

        try {
            $this->preFlightChecks();
        } catch (Exception $e) {
            error_log(
                "[IO Exception :: Start Application] (Line " . $e->getLine() . " | " . $e->getFile(
                ) . ") - " . $e->getCode() . " | " . $e->getMessage()
            );
            //We errored on preflight. Log the error, attempt to start the base app and return.
            Output::addSystemError($e);
            self::$app = AppFactory::create();

            return;
        }

        $services = self::$module->initialiseActiveModuleServices();


        //Then create the app
        AppFactory::setContainer($this->addCoreServices($services));
        self::$app = AppFactory::create();
        //finally loop over the modules again so we can register routes.
        self::$module->initialiseActiveModules();
        Routes::buildRoutes();
    }

    /**
     * Function to perform a number of initial checks prior to launching.
     */
    protected function preFlightChecks()
    {
        //check if we have the required config set:
        self::testAvailableConfig();

        //check if we have vault config. If not we need to install.
        $requireInstall = $this::$config->app->installRequired ?? true;
        if (!$this->isInstalled() && $requireInstall) {
            $installPath = self::getInstallPath();
            if (self::$pathUtility->getPath(0) != $installPath) {
                Output::addResponse(null, ['redirect' => System::getInstallPath()], false);
                Output::addMessage(
                    'requires_install',
                    "error",
                    "This Application requires installation. Please set {app: {installRequired: FALSE}} in config, or install the application. Install Path: " . $installPath
                );
            }

            //force the loading of any provided install display module.
            if (self::$config->app->install->display ?? false) {
                self::$module->setModuleFirst(self::$config->app->install->display);
            }

            //add the installer to the modules list.
            self::$module->setModuleFirst("install");
            self::$installing = true;
        }
    }

    protected static function testAvailableConfig()
    {
        $missingConfig = [];
        $requiredAppConfig = [
            "app" => [
                "environment",
                "allowed_domains",
            ],
        ];
        //TODO allow other modules to specify required configuration.
        //@TODO allow for other methods to easily test for any missing config, required or not.
        foreach ($requiredAppConfig as $key => $configItem) {
            if (is_array($configItem)) {
                foreach ($configItem as $subItem) {
                    if (!self::isConfigSet($key, $subItem)) {
                        $missingConfig[] = $key . ': ' . $subItem;
                    }
                }
            } elseif (!self::isConfigSet($key, $configItem)) {
                $missingConfig[] = $key . ': ' . $configItem;
            }
        }

        if ($missingConfig !== []) {
            throw new Exception("Required application config is missing: (" . implode(',', $missingConfig) . ")");
        }
    }

    protected static function isConfigSet($key, $item)
    {
        return (isset(self::$config->$key->$item) && !empty(self::$config->$key->$item)) ?? false;
    }

    /**
     * Method to check if the System is installed, or if it needs to go through the install process.
     */
    public function isInstalled()
    {
        if (self::$installing) {
            return false;
        }
        if (self::$isInstalled) {
            return true;
        }
        //first check if the database exists:
        self::$isInstalled = (new databaseTools())->checkDBExists();
        //next check if a core table exists
        if (self::$isInstalled) {
            try {
                $database = Vault::getInstance();
                if ($database->isInitialised()) {
                    $row = $database->select('sites')->fields()->limit(1)->execute()->fetchAssoc();
                } else {
                    self::$isInstalled = false;
                }
            } catch (Exception $e) {
                error_log(
                    "[IO Exception :: Is Installed Check] (Line " . $e->getLine() . " | " . $e->getFile(
                    ) . ")  - " . $e->getCode() . " | " . $e->getMessage()
                );
                self::$isInstalled = false; //not installed.
            }
        }

        return self::$isInstalled;
    }

    /**
     * This function allows for a custom root install path to be specified. It is also possible to provide custom install methods.
     * @return string
     */
    public static function getInstallPath()
    {
        return isset(self::$config->app->install->path) && !empty(self::$config->app->install->path) ? self::$config->app->install->path : "install";
    }

    /**
     * Add core services to the PHP DI container so that middleware constructors can access them
     * @param $services
     * @return Container
     * @throws Exception
     */
    protected function addCoreServices($services)
    {
        $containerBuilder = new ContainerBuilder();
        $containerBuilder->useAttributes(false);
        $containerBuilder->useAutowiring(true);

        //initialise core services. We array_merge so that these can be overridden by modules if needed.
        $coreServices = [
            pathUtility::class => function () {
                return pathUtility::getInstance();
            },
            Singleton::class => function () {
                return Configuration::getInstance();
            },
            Vault::class => function () {
                return Vault::getInstance();
            },
            Mailer::class => function () {
                return new Mailer();
            },
            globalData::class => function () {
                return globalData::getInstance();
            },
            Output::class => function () {
                return Output::getInstance();
            },
        ];

        //Register services declared by modules.
        $services = array_merge($coreServices, $services);
        $containerBuilder->addDefinitions($services);

        //Build PHP DI Containers
        return $containerBuilder->build();
    }

    private function getInstaller()
    {
        return isset(self::$config->app->installer) && !empty(self::$config->app->installer) ? self::$config->app->installer : "\apexl\Io\modules\install\installModule";
    }
}
