<?php

namespace FleekDash\Core;

/**
 * Module manager: extensible plugin module registry and loader.
 *
 * Discovers and bootstraps modules by context, similar in spirit to MU-plugins
 * but scoped to the FleekDash plugin.
 */
class ModuleManager
{
    /**
     * Singleton instance
     */
    private static ?self $instance = null;

    /**
     * Registered module configurations
     */
    private array $modules = [];

    /**
     * Bootstrapped module objects (or true for file-only modules)
     */
    private array $loadedModules = [];

    /**
     * Per-request cache of module directory paths (avoids wp_options writes / deadlocks).
     */
    private ?array $runtimeModuleDirs = null;

    /**
     * Per-request cache of module.json decode results keyed by absolute config path.
     *
     * @var array<string, array>
     */
    private array $runtimeModuleConfig = [];

    /**
     * Base path to includes/Modules
     */
    private string $modulesDir;

    /**
     * Private constructor (singleton)
     */
    private function __construct()
    {
        $this->modulesDir = FLEEKDASH_PLUGIN_DIR . 'includes/Modules/';
        $this->initHooks();

        // Eager-load if we are already past plugins_loaded
        if (\did_action('plugins_loaded')) {
            $this->loadModules();
        }
    }

    /**
     * Get the singleton instance
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Register init / admin / REST hooks
     */
    private function initHooks()
    {
        \add_action('init', [$this, 'loadModules'], 5);
        \add_action('admin_init', [$this, 'loadAdminModules'], 5);
        \add_action('rest_api_init', [$this, 'loadApiModules'], 5);
    }

    /**
     * Register a module configuration
     *
     * @param string $name   Module key (directory name)
     * @param array  $config Merged with defaults (file, class, contexts, etc.)
     */
    public function registerModule($name, $config = []): void
    {
        $default_config = [
            'file' => null,
            'class' => null,
            'autoload' => true,
            'priority' => 10,
            'dependencies' => [],
            'contexts' => ['frontend', 'admin', 'api'], // Load only in these runtimes
            'enabled' => true,
            'version' => '1.0.0',
            'description' => '',
            'author' => '',
        ];

        $config = array_merge($default_config, $config);

        // Default entry file when omitted
        if (!$config['file']) {
            $config['file'] = $this->modulesDir . $name . '/' . $name . '.php';
        }

        // Default PSR-4 class name when omitted
        if (!$config['class']) {
            $config['class'] = 'FleekDash\\Modules\\' . ucfirst($name) . '\\' . ucfirst($name);
        }

        $this->modules[$name] = $config;

        // Allow third parties to adjust module config
        $this->modules[$name] = \apply_filters('fleekdash_module_config', $this->modules[$name], $name);
    }

    /**
     * Scan includes/Modules and register discovered packages
     */
    public function autoDiscoverModules(): void
    {
        if (!is_dir($this->modulesDir)) {
            return;
        }

        if ($this->runtimeModuleDirs === null) {
            $discovered = glob($this->modulesDir . '*', GLOB_ONLYDIR);
            $this->runtimeModuleDirs = is_array($discovered) ? $discovered : [];
        }

        foreach ($this->runtimeModuleDirs as $module_dir) {
            $module_name = basename($module_dir);
            $module_file = $module_dir . '/' . $module_name . '.php';
            $config_file = $module_dir . '/module.json';

            // Skip if already registered explicitly
            if (isset($this->modules[$module_name])) {
                continue;
            }

            // Optional module.json metadata
            $config = $this->loadModuleConfig($config_file);

            // Require main PHP file
            if (file_exists($module_file)) {
                $config['file'] = $module_file;
                $this->registerModule($module_name, $config);
            }
        }
    }

    /**
     * Auto-discover and load frontend-context modules
     */
    public function loadModules(): void
    {
        $this->autoDiscoverModules();
        $this->loadModulesByContext('frontend');
    }

    /**
     * Load admin-context modules
     */
    public function loadAdminModules(): void
    {
        $this->loadModulesByContext('admin');
    }

    /**
     * Load REST API context modules
     */
    public function loadApiModules(): void
    {
        $this->loadModulesByContext('api');
    }

    /**
     * Bootstrap every registered module that matches $context
     */
    private function loadModulesByContext(string $context): void
    {
        // Sort by priority (ascending)
        uasort($this->modules, function ($a, $b) {
            return $a['priority'] <=> $b['priority'];
        });

        foreach ($this->modules as $name => $config) {
            // Respect enabled flag
            if (!$config['enabled']) {
                continue;
            }

            // Match current bootstrap context
            if (!in_array($context, $config['contexts'], true)) {
                continue;
            }

            // Avoid double bootstrap
            if (isset($this->loadedModules[$name])) {
                continue;
            }

            // Bootstrap module
            $this->loadModule($name, $config);
        }
    }

    /**
     * Require file and instantiate module class when configured
     */
    public function loadModule(string $name, ?array $config = null): bool
    {
        if (!$config) {
            $config = $this->modules[$name] ?? null;
        }

        if (!$config) {
            return false;
        }

        // Satisfy declared dependencies first
        if (!$this->checkDependencies($config['dependencies'])) {
            return false;
        }

        // Include module file
        if ($config['file'] && file_exists($config['file'])) {
            require_once $config['file'];
        }

        // Instantiate module class when provided
        if ($config['class'] && class_exists($config['class'])) {
            $instance = new $config['class']();

            // Call optional init()
            if (method_exists($instance, 'init')) {
                $instance->init();
            }

            $this->loadedModules[$name] = $instance;
        } else {
            $this->loadedModules[$name] = true;
        }

        // Notify listeners after successful load
        \do_action('fleekdash_module_loaded', $name, $config);

        return true;
    }

    /**
     * Ensure dependency modules are already loaded
     */
    private function checkDependencies(array $dependencies): bool
    {
        foreach ($dependencies as $dependency) {
            if (!isset($this->loadedModules[$dependency])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Return bootstrapped instance or null
     */
    public function getModule(string $name)
    {
        return $this->loadedModules[$name] ?? null;
    }

    /**
     * Whether load_module() completed for this key
     */
    public function isModuleLoaded(string $name): bool
    {
        return isset($this->loadedModules[$name]);
    }

    /**
     * All registered module configs
     */
    public function getRegisteredModules(): array
    {
        return $this->modules;
    }

    /**
     * Map of loaded module key => instance or true
     */
    public function getLoadedModules(): array
    {
        return $this->loadedModules;
    }

    /**
     * Toggle enabled flag and persist in fleekdash_disabled_modules
     */
    public function toggleModule(string $name, bool $enabled = true): void
    {
        if (isset($this->modules[$name])) {
            $this->modules[$name]['enabled'] = $enabled;

            // Persist toggle in options table
            $disabled_modules = \get_option('fleekdash_disabled_modules', []);

            if ($enabled) {
                $disabled_modules = array_diff($disabled_modules ?? [], [$name]);
            } else {
                $disabled_modules[] = $name;
            }

            \update_option('fleekdash_disabled_modules', array_unique($disabled_modules));
        }
    }

    private function loadModuleConfig(string $configFile): array
    {
        if (array_key_exists($configFile, $this->runtimeModuleConfig)) {
            return $this->runtimeModuleConfig[$configFile];
        }

        $config = [];
        if (file_exists($configFile)) {
            $config = json_decode(file_get_contents($configFile), true) ?: [];
        }

        $this->runtimeModuleConfig[$configFile] = $config;

        return $config;
    }

}
