<?php

namespace apexl\Io\includes;

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\pathUtility;
use apexl\Config\Singleton;
use apexl\Vault\Vault;

use DI\ContainerBuilder;
use ForceUTF8\Encoding;
use Slim\Exception\HttpNotFoundException;
use Slim\Factory\AppFactory;
use Psr\Http\Message\ResponseInterface as Response;
use Whoops\Handler\JsonResponseHandler;
use Zeuxisoo\Whoops\Slim\WhoopsMiddleware;

/**
 * @todo add in the core hook system, maybe as a loaded service?
 * @todo - auto generate CRUD routes for registered entities.
 * @todo - ^^ allow the registration of entities.
 * @todo add events handler for post request operations (Redirect on save, reload components etc)
 * Class System
 * @package apexl\Io\includes
 */
class System {
    /** @var AppFactory */
    public static $app;

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

    /** @var modules */
    public static $module;

    /** @var Singleton */
    protected static $config;

    /** @var $pathUtility */
    protected static $pathUtility;

    protected static $installing = FALSE;

    protected static $basePath;

    protected static $isInstalled = FALSE;

    /**
     * System constructor.
     * @throws \Exception
     */
    private function __construct()
    {
        self::$config = Singleton::getInstance();
        self::$pathUtility = pathUtility::getInstance();
        self::$module = modules::getInstance();
    }

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

        $this->preFlightChecks();
        $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();
    }

    /**
     * Add core services to the PHP DI container so that middleware constructors can access them
     * @param $services
     * @return \DI\Container
     * @throws \Exception
     */
    protected function addCoreServices($services){
        $containerBuilder = new ContainerBuilder();
        $containerBuilder->useAnnotations(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 Singleton::getInstance();
            },
            Vault::class => function(){
                return Vault::getInstance();
            },
            Mailer::class => function(){
                return new Mailer();
            },
            globalData::class => function(){
                return globalData::getInstance();
            }
        ];

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

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

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

    /**
     * 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) {
                throw new \Exception("This Application requires installation. Please set {app: {installRequired: FALSE}} in config, or install the application. Install Path: " . $installPath, 400);
            }

            //add the installer to the modules list.
            self::$module->setModuleFirst("install");
            self::$installing = TRUE;
        }
    }
    /**
     * 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){
                self::$isInstalled = FALSE; //not installed.
            }
        }
        return self::$isInstalled;
    }

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

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

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

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

    public static function start(){
        self::$app->add(bodyParserController::class);
        self::$app->addRoutingMiddleware();
        self::$app->add(new WhoopsMiddleware(['enable' => true], [new JsonResponseHandler()]));

        //deal with cors:
        self::$app->options('/{routes:.+}', function ($request, $response, $args) {
            return $response;
        });

        self::$app->add(function ($request, $handler) {
            //load the config and get the referer
            $config = Singleton::getInstance();
            //split out to fix reference error.
            $referer = $request->getHeader("HTTP_REFERER");
            $referer = array_shift($referer);
            $response = $handler->handle($request);

            if($config->app->environment == 'development' && is_null($referer)){
                //assume Postman for now, so allow it.
                return self::addCorsHeaders($response, "*");
            }

            //get the host and check it exists in the allowed domains.
            $parsedUrl = parse_url($referer);

            //if it does, set the headers. if not, just return response.
            if(in_array($parsedUrl['scheme'].'://'.$parsedUrl['host'], $config->app->allowedDomains)){
                $domain = $parsedUrl['scheme'].'://'.$parsedUrl['host'];
                $domain .= isset($parsedUrl['port']) ? ':'.$parsedUrl['port'] : '';
                return self::addCorsHeaders($response, $domain);
            }
            return $response;
        });
        /**
         * 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) {
            throw new HttpNotFoundException($request);
        });

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

    /**
     * Function to add CORS as needed on the fly.
     * @param $response
     * @param string $domain
     * @return mixed
     */
    public static function addCorsHeaders($response, $domain = "*"){
        return $response
            ->withHeader('Access-Control-Allow-Origin', $domain)
            ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
            ->withHeader('Access-Control-Allow-Credentials', 'true');
    }

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

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

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

    public static function asJson(Response $response, $data, $status = 200) : Response
    {
        $response->getBody()->write(json_encode($data));
        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus($status);
    }

    public static function output($messages, $route = '', $success = TRUE, $type = 'redirect'): array
    {
        switch ($type){
            case 'redirect':
                $event = 'redirectTo';
                break;
            case 'external':
                $event = 'redirectToExternal';
                break;
            case 'refresh':
                $event = 'refresh';
                break;
            default:
                $event = $type;
                break;
        }
        //build messages;
        $messagesData = [];
        if($content = $messages['messages'] ?? FALSE){
            $messagesData = $content;
        } else if($content =  $messages['content'] ?? FALSE){
            $messagesData[] = [
                'content' => $messages['content'],
                'type' => $success ? 'success' : 'error'
            ];
        } else {
            $messagesData = null;
        }
        $output['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 setBasePath($basePath){
        self::$basePath = $basePath;
    }

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

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

    protected static function testAvailableConfig(){
        $missingConfig = [];
        $requiredAppConfig = [
            "app" => [
                "environment",
                "allowedDomains"
            ]
        ];
        //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;
                    }
                }
            } else {
                if (!self::isConfigSet($key, $configItem)) {
                    $missingConfig[] = $key.': '.$configItem;
                }
            }

        }

        if(!empty($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;
    }

}