<?php

namespace Apexl\Io\System\Services;

use Apexl\Io\HookManager;
use Apexl\Io\Interfaces\IoModuleInterface;
use Apexl\Io\Interfaces\ProvidesHooksInterface;
use Apexl\Io\System\Collections\ModuleCollection;
use Apexl\Io\System\Exceptions\ModuleFinderException;
use Apexl\Io\System\Interfaces\ModuleFinderInterface;
use Apexl\Io\System\Interfaces\ProvidesRoutesInterface;
use DI\Container;
use DI\DependencyException;
use DI\NotFoundException;

final readonly class ModuleManager
{
    private ModuleCollection $modules;

    public function __construct(
        private Container $container,
        private HookManager $hookManager,
        private ModuleFinderInterface $moduleFinder,
        private HookKeyResolver $hookKeys,
    ) {
        $this->modules = new ModuleCollection();
    }

    /**
     * @throws DependencyException
     * @throws NotFoundException
     * @throws ModuleFinderException
     */
    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);
        });
    }

    /**
     * @throws DependencyException
     * @throws NotFoundException
     * @throws ModuleFinderException
     */
    private function instantiateInstalledModules(): void
    {
        foreach ($this->moduleFinder->installed() as $moduleClass) {
            \Apexl\Io\assertInstanceOf($moduleClass, IoModuleInterface::class);

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

    /**
     * The class-string is what gets registered, not an instance: HookManager resolves it
     * from the container when the hook actually fires. Reading the keys is therefore the
     * only thing standing between boot and never touching a hook that will not run — which
     * is why it goes through the resolver rather than constructing one to ask it.
     */
    private function registerHooks(ProvidesHooksInterface $module): void
    {
        foreach ($module->hooks() as $hookClass) {
            foreach ($this->hookKeys->keys($hookClass) as $hook) {
                $this->hookManager->registerCallback(
                    $hook,
                    $hookClass,
                );
            }
        }
    }

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

        if ($module instanceof ProvidesRoutesInterface) {
            $module->routes();
        }

        //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
     * @throws ModuleFinderException
     */
    public function isInstalled(string $moduleClass): bool
    {
        return in_array($moduleClass, $this->moduleFinder->installed());
    }
}
