<?php

namespace apexl\Io\modules\system\services;

use apexl\Io\includes\HookManager;
use apexl\Io\includes\RouteManager;
use apexl\Io\interfaces\AbstractHook;
use apexl\Io\interfaces\IoModuleInterface;
use apexl\Io\modules\system\collections\ModuleCollection;
use apexl\Io\modules\system\interfaces\ProvidesMiddlewareInterface;
use apexl\Io\modules\system\interfaces\ProvidesRoutesInterface;
use app\vendor\apexl\io\src\Io\interfaces\ProvidesHooksInterface;
use DI\Container;
use Slim\App;
use function apexl\Io\assertInstanceOf;

final readonly class ModuleManager
{
    private ModuleCollection $modules;

    public function __construct(
        private Container $container,
        private App $application,
        private HookManager $hookManager,
        private ModuleFinder $moduleFinder,
        private RouteManager $routeManager,
    ) {
        $this->modules = new ModuleCollection();
    }

    public function init(): void
    {
        $this->instantiateInstalledModules();

        $this->modules->each(function (IoModuleInterface $module): void {
            if ($module instanceof ProvidesHooksInterface) {
                $this->registerHooks($module);
            }
        });

        $this->modules->each(function (IoModuleInterface $module): void {
            $this->initialiseModule($module);
        });
    }

    private function instantiateInstalledModules(): void
    {
        //first loop over the modules and add any services they register.
        foreach ($this->moduleFinder->installed() as $moduleClass) {
            //we have to double instantiate so that $app is available the second time around.
            assertInstanceOf($moduleClass, IoModuleInterface::class);

            /** @var IoModuleInterface $module */
            $module = $this->container->get($moduleClass);
            $this->modules->add($module);
        }
    }

    private function registerHooks(ProvidesHooksInterface $module): void
    {
        foreach ($module->hooks() as $hookClass) {
            assertInstanceOf($hookClass, AbstractHook::class);

            $instance = $this->container->get($hookClass);

            foreach ($instance->hooks() as $hook) {
                $this->hookManager->registerCallback(
                    $hook,
                    function (...$args) use ($instance) {
                        return $this->container->call($instance, $args);
                    }
                );
            }
        }
    }

    /**
     * Initialise modules. Calls a few core methods and allows other modules to extend the methods used.
     */
    private function initialiseModule(IoModuleInterface $module): void
    {
        $module->initialise();

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

        if ($module instanceof ProvidesMiddlewareInterface) {
            $module->middleware($this->application);
        }

        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
        $this->hookManager->processHook("initialiseModule", $module);
    }

    /** @param class-string<IoModuleInterface> $moduleClass */
    public function isInstalled(string $moduleClass): bool
    {
        return in_array($moduleClass, $this->moduleFinder->installed());
    }
}
