<?php

namespace apexl\Io\modules\system\services;

use apexl\ClassEngine\EngineSingleton;
use apexl\Config\Configuration as Config;
use apexl\Io\Client;
use apexl\Io\hooks\AbstractHook;
use apexl\Io\includes\Hook;
use apexl\Io\includes\System;
use apexl\Io\interfaces\HasServicesInterface;
use apexl\Io\interfaces\IoModuleInterface;
use apexl\Io\modules\system\exceptions\HookException;
use apexl\Vault\Vault;
use app\vendor\apexl\io\src\Io\interfaces\HasHooksInterface;
use Exception;

class modules extends EngineSingleton
{
    /** @var array */
    protected array $moduleNamespaces = [];

    /** @var class-string[] */
    protected array $loadedModules = [];

    /** @var IoModuleInterface[] */
    protected array $instantiatedModules = [];

    protected array $namespaceModules = [];

    protected array $services = [];
    /** @var Vault */
    protected Vault $database;

    protected bool $loadedFromNamespace = false;
    protected Config $config;
    protected Client $client;

    protected function __construct()
    {
        parent::__construct();

        $this->client = Client::getInstance();
        $this->config = Config::getInstance();

        //register any additional module namespaces to load.
        $this->moduleNamespaces = $config->app->module->namespaces ?? [];

        // Allow override at project top-level - in module folder
        if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/../module')) {
            $this->moduleNamespaces[] = 'app\module';
        }

        //add the core namespace last.
        $this->moduleNamespaces[] = 'apexl\Io\modules';

        $this->database = Vault::getInstance();
    }

    /**
     * Function to get modules in all known namespaces.
     * @throws Exception
     * @todo add caching class to allow the application to cache into a service such as memcache or redis.
     */
    public function getAvailableModules()
    {
        $system = System::getInstance();
        //test if the modules table exists, only if we're installed. If it does, load our modules from there.
        if ($system->isInstalled()) {
            try {
                $modules = $this->getActiveModules();
                foreach ($modules as $active) {
                    $this->loadedModules[$active->name] = $active->namespace;
                }
            } catch (Exception $e) {
                error_log("[IO Exception :: Available Modules] -" . $e->getCode() . " | " . $e->getMessage());
                $this->loadedModules = $this->getModulesFromNamespace();
            }
        } else {
            $this->loadedModules = $this->getModulesFromNamespace();
        }
    }

    public function isInstalled(string $name): bool
    {
        $installed = $this->getActiveModules();
        foreach ($installed as $module) {
            if ($module->name === $name) {
                return true;
            }
        }

        return false;
    }

    public function getActiveModules(): array
    {
        return $this->database
            ->select('modules')
            ->fields()
            ->where('installed', 1)
            ->orderBy('weight')
            ->execute()
            ->fetchAll();
    }

    public function getModulesFromNamespace(): array
    {
        static $mapped = null;

        if ($mapped === null) {
            $modules = include($this->client->getConfigDir() . '/modules.php');

            $keys = array_map(function (string $moduleClass): string {
                $explodedClass = explode('\\', $moduleClass);

                return preg_replace('/Module$/', '', end($explodedClass));
            }, $modules);

            $mapped = array_combine($keys, $modules);
        }

        return $mapped;
    }

    public function addModule($namespace)
    {
        $namespace = explode('\\', (string) $namespace);
        $this->loadedModules[end($namespace)] = $namespace;
    }

    /**
     * Method to initialise available modules.
     * @return array
     * @throws HookException
     * @todo change this to only initialise installed or those that dont need install.
     */
    public function initialiseActiveModuleServices(): array
    {
        //first loop over the modules and add any services they register.
        foreach ($this->getLoadedModules() as $moduleClass) {
            //we have to double instantiate so that $app is available the second time around.
            if (class_exists($moduleClass)) {
                /** @var IoModuleInterface $module */
                $module = new $moduleClass();
                if ($module->isInstalled()) {
                    $this->initialiseModuleServices($module);
                    //allow modules to register service callbacks before we process things.
                    if ($module instanceof HasHooksInterface) {
                        foreach ($module->hooks() as $hook) {
                            if (!is_a($hook, AbstractHook::class, true)) {
                                throw new HookException(
                                    sprintf(
                                        'Hook %s must extend %s',
                                        $hook,
                                        AbstractHook::class
                                    )
                                );
                            }
                            $hook::register();
                        }
                    }
                    if (method_exists($module, 'registerCallbacks')) {
                        logger('error')->warning(
                            sprintf(
                                'Deprecation - Module \'%s\' should register hooks by implementing %s\n',
                                $module->getName(),
                                HasHooksInterface::class,
                            ),
                        );
                        $module->registerCallbacks();
                    }
                    $this->instantiatedModules[] = $module;
                }
            } else {
                logger('error')->warning(sprintf('Module %s cannot be found.', $moduleClass));
            }
        }

        return $this->services;
    }

    /**
     * Get the loaded modules.
     * @return class-string[]
     */
    public function getLoadedModules(): array
    {
        return $this->loadedModules;
    }

    protected function initialiseModuleServices(IoModuleInterface $module): void
    {
        if ($module instanceof HasServicesInterface) {
            $this->services = array_merge($this->services, $module->addServices());
        }
    }

    public function initialiseActiveModules(): void
    {
        //use the already instantiated modules and initialise them
        foreach ($this->instantiatedModules as $module) {
            //we have to double instantiate so that $app is available the second time around.
            if ($module->isInstalled()) {
                $this->initialiseModule($module);
            }
        }
    }

    /**
     * Initialise modules. Calls a few core methods and allows other modules to extend the methods used.
     */
    protected function initialiseModule(IoModuleInterface $module)
    {
        //initialise module now that app is available
        $module->initialise($this->config, $this->database);

        //Then we need to register the routes, we do this with reflection
        if (method_exists($module, 'routes')) {
            $module->routes();
        }

        //not all modules will add middleware so add a check for those that do.
        if (method_exists($module, 'addMiddleware')) {
            $module->addMiddleware();
        }

        if (method_exists($module, 'addRouteArgs')) {
            $module->addRouteArgs();
        }

        //allow other services to intercept this process to add additional functionality.
        //Allows for 'safe' required method calls to be added, for example user permissions, menu building etc
        Hook::processHook("initialiseModule", $module);
    }

    /**
     * Push the provided module to the top of the loaded list.
     * @param $module
     */
    public function setModuleFirst($module)
    {
        if (isset($this->loadedModules[$module])) {
            $path = $this->loadedModules[$module];
            unset($this->loadedModules[$module]);
            $this->loadedModules = [$module => $path] + $this->loadedModules;
        }
    }

    public function getModule($name, $namespaceOnly = false)
    {
        $loadedModules = $this->getModulesFromNamespace();
        if (isset($loadedModules[$name])) {
            $namespace = $loadedModules[$name];
            if ($namespaceOnly) {
                return $namespace;
            }

            return new $namespace();
        } else {
            throw new Exception("The requested module (" . $name . ") doesnt exist / isn't composer installed.", 400);
        }
    }
}
