"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModuleLoader = void 0;
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const lexer_1 = require("../lexer");
const parser_1 = require("../parser");
const logger_1 = require("./logger");
class ModuleLoader {
    constructor(resolver, options = {}, metrics, circuitBreakers) {
        this.moduleCache = new Map();
        this.loadingStack = new Set();
        this.externalSpecifiers = new Set();
        this.currentMemoryUsage = 0;
        this.warnings = [];
        this.resolver = resolver;
        this.options = {
            encoding: options.encoding || 'utf-8',
            cache: options.cache ?? true,
            circularDependencyStrategy: options.circularDependencyStrategy || 'warn',
            externals: options.externals ?? [],
            maxCacheSize: options.maxCacheSize ?? 1000, // Default 1000 modules
            maxCacheMemory: options.maxCacheMemory ?? 512 * 1024 * 1024, // Default 512MB
        };
        // Initialize production systems only when explicitly provided
        this.metrics = metrics;
        this.circuitBreakers = circuitBreakers;
        this.setExternals(options.externals);
    }
    /**
     * Load a module and all its dependencies
     */
    async load(specifier, fromFile) {
        const executeLoad = async () => {
            this.metrics?.requestCount.increment();
            logger_1.moduleLoaderLogger.debug('Loading module', { specifier, fromFile });
            const externalMatch = this.matchExternal(specifier);
            if (externalMatch) {
                return this.loadExternalModuleWithCircuitBreaker(specifier, externalMatch);
            }
            const resolved = this.resolver.resolve(specifier, fromFile);
            const moduleId = this.getModuleId(resolved.resolvedPath);
            // Atomic cache check - single operation to avoid race conditions
            const cached = this.options.cache ? this.moduleCache.get(moduleId) : undefined;
            if (cached?.isLoaded) {
                cached.lastAccessed = Date.now();
                logger_1.moduleLoaderLogger.debug('Module loaded from cache', { moduleId });
                return cached;
            }
            if (cached?.isLoading) {
                logger_1.moduleLoaderLogger.warn('Circular dependency detected during load', { moduleId });
                return this.handleCircularDependency(moduleId, cached);
            }
            // Check for circular dependency in loading stack
            if (this.loadingStack.has(moduleId)) {
                logger_1.moduleLoaderLogger.warn('Circular dependency in loading stack', { moduleId });
                return this.handleCircularDependency(moduleId);
            }
            try {
                const result = await this.loadModule(resolved, moduleId);
                logger_1.moduleLoaderLogger.info('Module loaded successfully', {
                    moduleId,
                    dependencies: result.dependencies.length,
                    sourceSize: result.source.length,
                });
                return result;
            }
            catch (error) {
                this.metrics?.loadErrors.increment();
                logger_1.moduleLoaderLogger.error('Module load failed', error, { moduleId, specifier });
                throw error;
            }
        };
        if (this.metrics) {
            return this.metrics.recordAsync(this.metrics.loadLatency, executeLoad);
        }
        return executeLoad();
    }
    /**
     * Load module synchronously
     */
    loadSync(specifier, fromFile) {
        const externalMatch = this.matchExternal(specifier);
        if (externalMatch) {
            return this.getOrCreateExternalModule(specifier, externalMatch);
        }
        const resolved = this.resolver.resolve(specifier, fromFile);
        const moduleId = this.getModuleId(resolved.resolvedPath);
        // Atomic cache check - single operation to avoid race conditions
        const cached = this.options.cache ? this.moduleCache.get(moduleId) : undefined;
        if (cached?.isLoaded) {
            cached.lastAccessed = Date.now();
            return cached;
        }
        if (cached?.isLoading) {
            return this.handleCircularDependency(moduleId, cached);
        }
        // Check for circular dependency in loading stack
        if (this.loadingStack.has(moduleId)) {
            return this.handleCircularDependency(moduleId);
        }
        return this.loadModuleSync(resolved, moduleId);
    }
    async loadExternalModuleWithCircuitBreaker(specifier, externalMatch) {
        if (!this.circuitBreakers) {
            return this.getOrCreateExternalModule(specifier, externalMatch);
        }
        return this.circuitBreakers.executeWithRetry(`external:${externalMatch}`, async () => {
            return this.getOrCreateExternalModule(specifier, externalMatch);
        }, { maxRetries: 3, initialDelay: 1000 }, async () => {
            // Fallback: create a stub external module
            logger_1.moduleLoaderLogger.warn('Using fallback for external module', { specifier, externalMatch });
            return this.createStubExternalModule(specifier, externalMatch);
        });
    }
    createStubExternalModule(specifier, canonical) {
        const moduleId = this.getExternalModuleId(canonical);
        return {
            id: moduleId,
            resolvedPath: canonical,
            source: '// External module stub',
            ast: { type: 'Program', body: [], line: 1, column: 1 },
            dependencies: [],
            exports: { named: {} },
            isLoaded: true,
            isLoading: false,
            lastAccessed: Date.now(),
            error: new Error(`External module ${specifier} failed to load - using stub`),
        };
    }
    async loadModule(resolved, moduleId) {
        const isExternal = resolved.resolvedPath.startsWith('http') || resolved.resolvedPath.includes('node_modules');
        if (isExternal && this.circuitBreakers) {
            return this.loadExternalModuleWithCircuitBreaker(moduleId, resolved.resolvedPath);
        }
        const startTime = process.hrtime.bigint();
        try {
            const result = this.loadModuleSync(resolved, moduleId);
            if (this.metrics) {
                const endTime = process.hrtime.bigint();
                const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds
                this.metrics.loadLatency.record(duration);
            }
            return result;
        }
        catch (error) {
            if (this.metrics) {
                this.metrics.loadErrors.increment();
            }
            // Ensure we're throwing an Error object for Promise rejection
            if (error instanceof Error) {
                throw error;
            }
            else {
                throw new Error(String(error));
            }
        }
    }
    loadModuleSync(resolved, moduleId) {
        // Synchronous version of loadModule for backward compatibility
        const module = {
            id: moduleId,
            resolvedPath: resolved.resolvedPath,
            source: '',
            ast: {
                type: 'Program',
                body: [],
                line: 1,
                column: 1,
            },
            dependencies: [],
            exports: { named: {} },
            isLoaded: false,
            isLoading: true,
            lastAccessed: Date.now(),
        };
        // Add to loading stack first to detect circular dependencies early
        this.loadingStack.add(moduleId);
        let moduleAddedToCache = false;
        try {
            // Add to cache only after loading stack is set
            if (this.options.cache) {
                this.moduleCache.set(moduleId, module);
                moduleAddedToCache = true;
            }
            // Read source file
            module.source = fs.readFileSync(resolved.resolvedPath, {
                encoding: this.options.encoding,
            });
            // Parse only if it's a SomonScript file
            if (resolved.extension === '.som') {
                const lexer = new lexer_1.Lexer(module.source);
                const tokens = lexer.tokenize();
                const parser = new parser_1.Parser(tokens);
                module.ast = parser.parse();
                // If parsing produced errors, surface them as a load error
                const parseErrors = parser.getErrors();
                if (parseErrors.length > 0) {
                    throw new Error(`Parse error(s) in ${resolved.resolvedPath}: ${parseErrors[0]}`);
                }
                // Extract dependency specifiers (raw, e.g. "./utils")
                module.dependencies = this.extractDependencies(module.ast);
                // Load dependencies recursively (keep original specifiers in metadata)
                for (const dep of module.dependencies) {
                    this.loadSync(dep, resolved.resolvedPath);
                }
            }
            // Mark as loaded successfully
            module.isLoaded = true;
            module.isLoading = false;
            // Update memory usage tracking and enforce cache limits
            if (this.options.cache && moduleAddedToCache) {
                this.currentMemoryUsage += this.estimateModuleSize(module);
                this.enforceCacheLimits();
            }
            return module;
        }
        catch (error) {
            // Handle loading/parsing errors with proper cleanup
            module.error = error;
            module.isLoading = false;
            // Keep module in cache with error state for validation purposes
            // Don't delete from cache - let the registry validate broken dependencies
            throw error;
        }
        finally {
            // Always clean up loading stack
            this.loadingStack.delete(moduleId);
        }
    }
    handleCircularDependency(moduleId, module) {
        const message = `Circular dependency detected: ${moduleId}`;
        const chain = Array.from(this.loadingStack);
        switch (this.options.circularDependencyStrategy) {
            case 'error':
                throw new Error(message);
            case 'warn': {
                const warningMessage = `${message} (chain: ${chain.join(' -> ')} -> ${moduleId})`;
                this.warnings.push(warningMessage);
                logger_1.moduleLoaderLogger.warn('Circular dependency detected', {
                    moduleId,
                    chain,
                    message,
                });
                break;
            }
            case 'ignore':
                break;
        }
        // Return partial module for circular dependencies
        return (module || {
            id: moduleId,
            resolvedPath: moduleId,
            source: '',
            ast: { type: 'Program', body: [], line: 1, column: 1 },
            dependencies: [],
            exports: { named: {} },
            isLoaded: false,
            isLoading: true,
            lastAccessed: Date.now(),
        });
    }
    /**
     * Extract raw dependency specifiers from AST (e.g., "./utils", "../math")
     * These need to be resolved to absolute paths before being stored in the dependency graph
     */
    extractDependencies(ast) {
        const rawSpecifiers = [];
        // Validate AST input
        if (!ast?.body || !Array.isArray(ast.body)) {
            return rawSpecifiers;
        }
        for (const statement of ast.body) {
            if (statement?.type === 'ImportDeclaration') {
                const importDecl = statement;
                // Validate import declaration structure
                if (!importDecl.source?.value) {
                    continue;
                }
                const specifier = importDecl.source.value;
                // Validate specifier is string and reasonable length
                if (typeof specifier !== 'string' || specifier.length === 0 || specifier.length > 500) {
                    continue;
                }
                // Basic security validation - prevent obvious path traversal attacks
                const normalizedSpec = specifier.trim();
                if (normalizedSpec.includes('\\') || normalizedSpec.split('..').length > 5) {
                    logger_1.moduleLoaderLogger.warn('Suspicious import specifier detected and skipped', {
                        specifier,
                        normalizedSpec,
                        hasBackslash: normalizedSpec.includes('\\'),
                        dotDotCount: normalizedSpec.split('..').length - 1,
                    });
                    continue;
                }
                rawSpecifiers.push(normalizedSpec);
            }
        }
        return rawSpecifiers;
    }
    getModuleId(resolvedPath) {
        return path.resolve(resolvedPath);
    }
    /**
     * Get a loaded module from cache
     */
    getModule(moduleId) {
        return this.moduleCache.get(moduleId);
    }
    /**
     * Check if a module is loaded
     */
    isLoaded(moduleId) {
        const module = this.moduleCache.get(moduleId);
        return module?.isLoaded ?? false;
    }
    /**
     * Clear module cache
     */
    clearCache() {
        this.moduleCache.clear();
        this.loadingStack.clear();
        this.currentMemoryUsage = 0;
    }
    /**
     * Get cache statistics
     */
    getCacheStats() {
        return {
            size: this.moduleCache.size,
            memoryUsage: this.currentMemoryUsage,
            maxSize: this.options.maxCacheSize,
            maxMemory: this.options.maxCacheMemory,
        };
    }
    /**
     * Enforce cache limits by evicting least recently used modules
     */
    enforceCacheLimits() {
        if (!this.options.cache)
            return;
        // Check size limit
        if (this.moduleCache.size > this.options.maxCacheSize) {
            const excess = this.moduleCache.size - this.options.maxCacheSize;
            const keysToEvict = Array.from(this.moduleCache.keys()).slice(0, excess);
            for (const key of keysToEvict) {
                this.evictModule(key);
            }
        }
        // Check memory limit
        if (this.currentMemoryUsage > this.options.maxCacheMemory) {
            // Evict modules until under memory limit (simple LRU approximation)
            const sortedEntries = Array.from(this.moduleCache.entries()).sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
            for (const [key] of sortedEntries) {
                this.evictModule(key);
                if (this.currentMemoryUsage <= this.options.maxCacheMemory * 0.8) {
                    break; // Leave some headroom
                }
            }
        }
    }
    /**
     * Evict a specific module from cache
     */
    evictModule(moduleId) {
        const module = this.moduleCache.get(moduleId);
        if (module) {
            this.currentMemoryUsage -= this.estimateModuleSize(module);
            this.moduleCache.delete(moduleId);
        }
    }
    /**
     * Estimate memory usage of a module
     */
    estimateModuleSize(module) {
        return (module.source.length * 2 + // UTF-16 characters
            JSON.stringify(module.ast).length * 2 +
            module.dependencies.length * 50 + // Estimated string overhead
            200 // Object overhead
        );
    }
    /**
     * Get all loaded modules
     */
    getAllModules() {
        return Array.from(this.moduleCache.values());
    }
    /**
     * Get module dependency graph
     */
    getDependencyGraph() {
        const graph = new Map();
        for (const [moduleId, module] of this.moduleCache) {
            graph.set(moduleId, module.dependencies);
        }
        return graph;
    }
    /**
     * Note: topological ordering is provided by ModuleRegistry.
     */
    setExternals(externals) {
        this.externalSpecifiers = new Set((externals ?? []).map(ext => ext.trim()).filter(ext => ext.length > 0));
    }
    getExternals() {
        return Array.from(this.externalSpecifiers);
    }
    matchExternal(specifier) {
        if (this.externalSpecifiers.size === 0)
            return null;
        const trimmed = specifier.trim();
        const candidates = this.buildExternalCandidates(trimmed);
        for (const candidate of candidates) {
            if (this.externalSpecifiers.has(candidate)) {
                return candidate;
            }
        }
        return null;
    }
    buildExternalCandidates(specifier) {
        const variants = new Set();
        variants.add(specifier);
        if (/\.js$/i.test(specifier)) {
            const withoutExt = specifier.replace(/\.js$/i, '');
            variants.add(withoutExt);
            variants.add(`${withoutExt}.som`);
        }
        else {
            variants.add(`${specifier}.js`);
        }
        if (/\.som$/i.test(specifier)) {
            const withoutExt = specifier.replace(/\.som$/i, '');
            variants.add(withoutExt);
            variants.add(`${withoutExt}.js`);
        }
        else {
            variants.add(`${specifier}.som`);
        }
        return Array.from(variants.values());
    }
    getOrCreateExternalModule(specifier, canonical) {
        const moduleId = this.getExternalModuleId(canonical);
        if (this.options.cache && this.moduleCache.has(moduleId)) {
            const cached = this.moduleCache.get(moduleId);
            cached.lastAccessed = Date.now();
            return cached;
        }
        const module = {
            id: moduleId,
            resolvedPath: canonical,
            source: '',
            ast: { type: 'Program', body: [], line: 1, column: 1 },
            dependencies: [],
            exports: { named: {} },
            isLoaded: true,
            isLoading: false,
            lastAccessed: Date.now(),
        };
        if (this.options.cache) {
            this.moduleCache.set(moduleId, module);
            this.currentMemoryUsage += this.estimateModuleSize(module);
            this.enforceCacheLimits();
        }
        return module;
    }
    getExternalModuleId(specifier) {
        return `external:${specifier}`;
    }
    /**
     * Get collected warnings
     */
    getWarnings() {
        return [...this.warnings];
    }
    /**
     * Clear collected warnings
     */
    clearWarnings() {
        this.warnings = [];
    }
}
exports.ModuleLoader = ModuleLoader;
//# sourceMappingURL=module-loader.js.map