<?php

declare(strict_types=1);

namespace PpPhp\Core;

readonly class Module
{
    public function __construct(
        public string $name,
        public string $directory,
        public array $manifest,
    ) {}
}

class ModuleManager
{
    /** @var array<string, Module> */
    private array $modules = [];

    /**
     * Discover all modules in the given directory.
     *
     * @param string $modulesPath Path to the modules directory.
     */
    public function discoverModules(string $modulesPath): void
    {
        $iterator = new \DirectoryIterator($modulesPath);
        foreach ($iterator as $item) {
            if ($item->isDir() && !$item->isDot()) {
                $moduleDir = $item->getPathname();
                $manifestFile = $moduleDir . '/manifest.json';

                if (file_exists($manifestFile)) {
                    $manifest = json_decode(file_get_contents($manifestFile), true);
                    if ($manifest === null) {
                        throw new \RuntimeException("Invalid manifest JSON in {$moduleDir}");
                    }
                    // Basic validation
                    if (!isset($manifest['name'], $manifest['namespace'])) {
                        throw new \RuntimeException("Manifest in {$moduleDir} must contain 'name' and 'namespace'.");
                    }
                    $module = new Module(
                        name: $manifest['name'],
                        directory: $moduleDir,
                        manifest: $manifest,
                    );
                    $this->modules[$module->name] = $module;
                }
            }
        }
    }

    /**
     * Get all discovered modules.
     *
     * @return array<string, Module>
     */
    public function getModules(): array
    {
        return $this->modules;
    }

    /**
     * Get a single module by name.
     */
    public function getModule(string $name): ?Module
    {
        return $this->modules[$name] ?? null;
    }

    /**
     * Collect all routes from all modules.
     *
     * @return array List of route definitions, each with a 'module' key.
     */
    public function collectRoutes(): array
    {
        $routes = [];
        foreach ($this->modules as $module) {
            if (isset($module->manifest['routes'])) {
                foreach ($module->manifest['routes'] as $route) {
                    $route['module'] = $module->name;
                    // Convert handler like "Controllers\DashboardController@index"
                    // into full class: namespace + handler
                    if (isset($module->manifest['namespace'])) {
                        $route['handler'] = $module->manifest['namespace'] . '\\' . $route['handler'];
                    }
                    // Defensive: skip a route whose controller class is not present.
                    // A client bundle strips vendor-only controllers but a stale
                    // manifest entry can survive (see serverOnlyRoutePrefixes); the
                    // dispatcher does `new ReflectionClass($class)`, which FATALS on a
                    // missing class. Dropping the route degrades to a clean 404
                    // instead of an uncaught 500 for any future strip gap.
                    if (is_string($route['handler'] ?? null)) {
                        $class = explode('@', $route['handler'])[0];
                        if ($class !== '' && !class_exists($class)) {
                            continue;
                        }
                    }
                    $routes[] = $route;
                }
            }
        }
        return $routes;
    }

    /**
     * Collect all event listeners from all modules.
     *
     * @return array<string, callable[]> Event name => array of callables.
     */
    public function collectEventListeners(): array
    {
        $listeners = [];
        foreach ($this->modules as $module) {
            if (isset($module->manifest['events'])) {
                foreach ($module->manifest['events'] as $event => $handlers) {
                    foreach ($handlers as $handler) {
                        // Split class@method
                        [$class, $method] = explode('@', $handler);
                        $fullClass = $module->manifest['namespace'] . '\\' . $class;
                        $listeners[$event][] = [new $fullClass(), $method];
                    }
                }
            }
        }
        return $listeners;
    }

    /**
     * Collect admin menu items from all modules.
     * This returns a structured array ready to be rendered.
     */
    public function collectMenu(): array
    {
        $groups = Security::CurrentUserGroups(); // owner resolves to the full superset
        $isAuth = Security::IsLoggedIn();

        $menu = [];
        foreach ($this->modules as $module) {
            // A module contributes its primary `menu.admin` entry, plus any number of
            // additional entries under `menu.extra` (a list) — so one module can put
            // several links in the sidebar (e.g. Virtual Databases + Data Studio).
            $items = [];
            if (($primary = $module->manifest['menu']['admin'] ?? null) !== null) { $items[] = $primary; }
            foreach ((array) ($module->manifest['menu']['extra'] ?? []) as $e) {
                if (is_array($e)) { $items[] = $e; }
            }
            foreach ($items as $item) {
                if (!is_array($item)) { continue; }
                // Guest/auth visibility (e.g. the Login item is guests-only).
                $visible = match ($item['visibility'] ?? 'always') {
                    'guest' => !$isAuth,
                    'auth'  => $isAuth,
                    default => true,
                };
                if (!$visible) {
                    continue;
                }
                // A menu item may also name the roles allowed to see it. Anything
                // without a `roles` list stays visible to everyone (previous behaviour).
                $roles = $item['roles'] ?? null;
                if (is_array($roles) && $roles !== [] && array_intersect($groups, $roles) === []) {
                    continue;
                }
                $menu[] = $item;
            }
        }
        // Sort by position if needed
        usort($menu, fn($a, $b) => ($a['position'] ?? 0) <=> ($b['position'] ?? 0));
        return $menu;
    }

    /**
     * Get all asset files from modules for publishing.
     *
     * @return array Module name => [ 'js' => [...], 'css' => [...] ]
     */
    public function collectAssets(): array
    {
        $assets = [];
        foreach ($this->modules as $name => $module) {
            if (isset($module->manifest['assets'])) {
                $assets[$name] = $module->manifest['assets'];
            }
        }
        return $assets;
    }
}