/**
 * ModuleLoader - Strict dependency injection and module loading
 * Enforces proper initialization order and dependency resolution
 */

import EventBus from './EventBus.js';

class ModuleLoader {
    constructor(eventBus) {
        if (!(eventBus instanceof EventBus)) {
            throw new Error('ModuleLoader requires an EventBus instance');
        }

        // Private state
        this._eventBus = eventBus;
        this._modules = new Map();
        this._loadedModules = new Map();
        this._initializationOrder = [];
        this._isInitializing = false;

        // Seal to prevent modification
        Object.seal(this);
    }

    /**
     * Register a module class for lazy loading
     * @param {string} name - Module name
     * @param {Function} moduleClass - Module constructor
     * @param {Array} dependencies - Array of dependency names
     */
    register(name, moduleClass, dependencies = []) {
        this._validateModuleName(name);
        this._validateModuleClass(moduleClass);
        this._validateDependencies(dependencies);

        if (this._modules.has(name)) {
            throw new Error(`Module ${name} is already registered`);
        }

        this._modules.set(name, {
            name,
            moduleClass,
            dependencies,
            instance: null,
            loaded: false,
            initialized: false
        });
    }

    /**
     * Load and initialize a module with its dependencies
     * @param {string} name - Module name to load
     * @param {Object} config - Configuration object for the module
     * @returns {Promise<Module>} - The loaded module instance
     */
    async load(name, config = {}) {
        this._validateModuleName(name);

        if (this._loadedModules.has(name)) {
            return this._loadedModules.get(name);
        }

        const moduleInfo = this._modules.get(name);
        if (!moduleInfo) {
            throw new Error(`Module ${name} is not registered`);
        }

        // Resolve dependencies first
        const dependencies = await this._resolveDependencies(moduleInfo.dependencies);

        // Create module instance
        const moduleInstance = new moduleInfo.moduleClass(name, dependencies, config);

        // Validate that instance extends Module base class
        if (!moduleInstance.name || typeof moduleInstance.init !== 'function') {
            throw new Error(`Module ${name} must extend Module base class`);
        }

        // Register with EventBus
        this._eventBus.registerModule(moduleInstance);

        // Store the instance
        this._loadedModules.set(name, moduleInstance);
        moduleInfo.instance = moduleInstance;
        moduleInfo.loaded = true;

        return moduleInstance;
    }

    /**
     * Initialize a loaded module
     * @param {string} name - Module name to initialize
     * @returns {Promise<Module>} - The initialized module
     */
    async initialize(name) {
        const moduleInfo = this._modules.get(name);
        if (!moduleInfo || !moduleInfo.loaded) {
            throw new Error(`Module ${name} must be loaded before initialization`);
        }

        if (moduleInfo.initialized || moduleInfo.instance.isInitialized) {
            return moduleInfo.instance;
        }

        try {
            await moduleInfo.instance.init();
            moduleInfo.initialized = true;
            this._initializationOrder.push(name);

            // Emit module ready event
            this._eventBus.emit('module:ready', { module: name }, 'ModuleLoader');

        } catch (error) {
            throw new Error(`Failed to initialize module ${name}: ${error.message}`);
        }

        return moduleInfo.instance;
    }

    /**
     * Load and initialize a module in one step
     * @param {string} name - Module name
     * @param {Object} config - Module configuration
     * @returns {Promise<Module>} - The ready module
     */
    async loadAndInitialize(name, config = {}) {
        const module = await this.load(name, config);
        return await this.initialize(name);
    }

    /**
     * Destroy a module and clean up
     * @param {string} name - Module name to destroy
     */
    async destroy(name) {
        const moduleInfo = this._modules.get(name);
        if (!moduleInfo || !moduleInfo.loaded) {
            return; // Already destroyed or never loaded
        }

        try {
            // Call module's destroy method
            await moduleInfo.instance.destroy();
            moduleInfo.instance._setDestroyed();

            // Unregister from EventBus
            this._eventBus.unregisterModule(name);

            // Clean up references
            this._loadedModules.delete(name);
            moduleInfo.instance = null;
            moduleInfo.loaded = false;
            moduleInfo.initialized = false;

            // Remove from initialization order
            const index = this._initializationOrder.indexOf(name);
            if (index > -1) {
                this._initializationOrder.splice(index, 1);
            }

            // Emit module destroyed event
            this._eventBus.emit('module:destroyed', { module: name }, 'ModuleLoader');

        } catch (error) {
            console.error(`Error destroying module ${name}:`, error);
        }
    }

    /**
     * Get a loaded module instance
     * @param {string} name - Module name
     * @returns {Module|null} - Module instance or null if not loaded
     */
    getModule(name) {
        return this._loadedModules.get(name) || null;
    }

    /**
     * Check if a module is loaded
     * @param {string} name - Module name
     * @returns {boolean}
     */
    isLoaded(name) {
        const moduleInfo = this._modules.get(name);
        return moduleInfo ? moduleInfo.loaded : false;
    }

    /**
     * Check if a module is initialized
     * @param {string} name - Module name
     * @returns {boolean}
     */
    isInitialized(name) {
        const moduleInfo = this._modules.get(name);
        return moduleInfo ? moduleInfo.initialized : false;
    }

    /**
     * Get status information for debugging
     */
    getStatus() {
        const status = {
            registered: [],
            loaded: [],
            initialized: this._initializationOrder.slice()
        };

        for (const [name, info] of this._modules) {
            status.registered.push(name);
            if (info.loaded) {
                status.loaded.push(name);
            }
        }

        return status;
    }

    // Private methods
    async _resolveDependencies(dependencies) {
        const resolved = {};

        for (const depName of dependencies) {
            if (!this._modules.has(depName)) {
                throw new Error(`Dependency ${depName} is not registered`);
            }

            // Load dependency if not already loaded
            if (!this.isLoaded(depName)) {
                await this.load(depName);
            }

            // Initialize dependency if not already initialized
            if (!this.isInitialized(depName)) {
                await this.initialize(depName);
            }

            resolved[depName] = this.getModule(depName);
        }

        return resolved;
    }

    _validateModuleName(name) {
        if (!name || typeof name !== 'string') {
            throw new Error('Module name must be a non-empty string');
        }
    }

    _validateModuleClass(moduleClass) {
        if (typeof moduleClass !== 'function') {
            throw new Error('Module class must be a constructor function');
        }
    }

    _validateDependencies(dependencies) {
        if (!Array.isArray(dependencies)) {
            throw new Error('Dependencies must be an array');
        }

        for (const dep of dependencies) {
            if (typeof dep !== 'string') {
                throw new Error('All dependencies must be strings');
            }
        }
    }
}

// Freeze to prevent modification
Object.freeze(ModuleLoader);
Object.freeze(ModuleLoader.prototype);

export default ModuleLoader;