"use strict";
/**
 * Memory management and cleanup utilities for DataPilot
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.globalCleanupHandler = exports.ProcessCleanupHandler = exports.globalResourceManager = exports.ResourceManager = exports.globalMemoryManager = exports.MemoryManager = void 0;
const events_1 = require("events");
const types_1 = require("../core/types");
const logger_1 = require("./logger");
class MemoryManager extends events_1.EventEmitter {
    config;
    monitoringTimer;
    cleanupCallbacks = [];
    isMonitoring = false;
    lastMemoryStats;
    memoryHistory = [];
    maxHistorySize = 100;
    constructor(config = {}) {
        super();
        this.config = {
            thresholds: {
                warningMB: 256,
                criticalMB: 512,
                maxMB: 1024,
            },
            monitoringInterval: 5000, // 5 seconds
            enableAutomaticCleanup: true,
            enableGarbageCollection: true,
            logMemoryUsage: false,
            ...config,
        };
        // Set up event listeners
        this.on('warning', this.handleMemoryWarning.bind(this));
        this.on('critical', this.handleMemoryCritical.bind(this));
        this.on('max', this.handleMemoryMax.bind(this));
    }
    /**
     * Start memory monitoring
     */
    startMonitoring(context) {
        if (this.isMonitoring) {
            return;
        }
        this.isMonitoring = true;
        logger_1.logger.debug('Starting memory monitoring', context);
        this.monitoringTimer = setInterval(() => {
            try {
                this.checkMemoryUsage(context);
            }
            catch (error) {
                logger_1.logger.error('Error during memory monitoring', context, error);
            }
        }, this.config.monitoringInterval);
        // Initial check
        this.checkMemoryUsage(context);
    }
    /**
     * Stop memory monitoring
     */
    stopMonitoring(context) {
        if (!this.isMonitoring) {
            return;
        }
        this.isMonitoring = false;
        logger_1.logger.debug('Stopping memory monitoring', context);
        if (this.monitoringTimer) {
            clearInterval(this.monitoringTimer);
            this.monitoringTimer = undefined;
        }
    }
    /**
     * Check current memory usage and emit events if thresholds are exceeded
     */
    checkMemoryUsage(context) {
        const memoryUsage = process.memoryUsage();
        const stats = {
            heapUsed: memoryUsage.heapUsed,
            heapTotal: memoryUsage.heapTotal,
            external: memoryUsage.external,
            rss: memoryUsage.rss,
            arrayBuffers: memoryUsage.arrayBuffers || 0,
        };
        // Add to history
        this.memoryHistory.push(stats);
        if (this.memoryHistory.length > this.maxHistorySize) {
            this.memoryHistory = this.memoryHistory.slice(-this.maxHistorySize);
        }
        const heapMB = stats.heapUsed / (1024 * 1024);
        const rssMB = stats.rss / (1024 * 1024);
        const totalMB = Math.max(heapMB, rssMB);
        // Log memory usage if enabled
        if (this.config.logMemoryUsage) {
            logger_1.logger.trace(`Memory usage: ${heapMB.toFixed(1)}MB heap, ${rssMB.toFixed(1)}MB RSS`, {
                ...context,
                memoryUsage: stats.heapUsed,
            });
        }
        // Check thresholds and emit events
        if (totalMB >= this.config.thresholds.maxMB) {
            this.emit('max', stats, context);
        }
        else if (totalMB >= this.config.thresholds.criticalMB) {
            this.emit('critical', stats, context);
        }
        else if (totalMB >= this.config.thresholds.warningMB) {
            this.emit('warning', stats, context);
        }
        this.lastMemoryStats = stats;
        return stats;
    }
    /**
     * Register cleanup callback
     */
    registerCleanupCallback(callback) {
        this.cleanupCallbacks.push(callback);
    }
    /**
     * Force garbage collection if available
     */
    forceGarbageCollection(context) {
        if (!this.config.enableGarbageCollection) {
            return;
        }
        if (global.gc) {
            logger_1.logger.debug('Forcing garbage collection', context);
            const before = process.memoryUsage().heapUsed;
            global.gc();
            const after = process.memoryUsage().heapUsed;
            const freedMB = (before - after) / (1024 * 1024);
            if (freedMB > 1) {
                logger_1.logger.debug(`Garbage collection freed ${freedMB.toFixed(1)}MB`, context);
            }
        }
        else {
            logger_1.logger.debug('Garbage collection not available (run with --expose-gc)', context);
        }
    }
    /**
     * Run cleanup callbacks
     */
    runCleanup(context) {
        if (!this.config.enableAutomaticCleanup) {
            return;
        }
        logger_1.logger.debug(`Running ${this.cleanupCallbacks.length} cleanup callbacks`, context);
        for (const callback of this.cleanupCallbacks) {
            try {
                callback();
            }
            catch (error) {
                logger_1.logger.error('Error in cleanup callback', context, error);
            }
        }
    }
    /**
     * Get memory statistics
     */
    getMemoryStats() {
        if (this.memoryHistory.length === 0) {
            return {
                current: this.lastMemoryStats,
                peak: undefined,
                average: undefined,
                history: [],
            };
        }
        const peak = this.memoryHistory.reduce((max, stats) => stats.heapUsed > max.heapUsed ? stats : max);
        const average = {
            heapUsed: this.memoryHistory.reduce((sum, stats) => sum + stats.heapUsed, 0) /
                this.memoryHistory.length,
            heapTotal: this.memoryHistory.reduce((sum, stats) => sum + stats.heapTotal, 0) /
                this.memoryHistory.length,
            external: this.memoryHistory.reduce((sum, stats) => sum + stats.external, 0) /
                this.memoryHistory.length,
            rss: this.memoryHistory.reduce((sum, stats) => sum + stats.rss, 0) / this.memoryHistory.length,
            arrayBuffers: this.memoryHistory.reduce((sum, stats) => sum + stats.arrayBuffers, 0) /
                this.memoryHistory.length,
        };
        return {
            current: this.lastMemoryStats,
            peak,
            average,
            history: [...this.memoryHistory],
        };
    }
    /**
     * Get memory growth rate
     */
    getMemoryGrowthRate() {
        if (this.memoryHistory.length < 2) {
            return undefined;
        }
        const recent = this.memoryHistory.slice(-10); // Last 10 measurements
        if (recent.length < 2) {
            return undefined;
        }
        const first = recent[0];
        const last = recent[recent.length - 1];
        const timeDiff = recent.length * this.config.monitoringInterval; // Approximate time diff
        const memoryDiff = last.heapUsed - first.heapUsed;
        return memoryDiff / timeDiff; // Bytes per millisecond
    }
    /**
     * Predict memory exhaustion time
     */
    predictMemoryExhaustion() {
        const growthRate = this.getMemoryGrowthRate();
        if (!growthRate || growthRate <= 0 || !this.lastMemoryStats) {
            return undefined;
        }
        const currentMB = this.lastMemoryStats.heapUsed / (1024 * 1024);
        const remainingMB = this.config.thresholds.maxMB - currentMB;
        const remainingBytes = remainingMB * 1024 * 1024;
        const timeToExhaustionMs = remainingBytes / growthRate;
        if (timeToExhaustionMs > 0 && timeToExhaustionMs < 300000) {
            // Within 5 minutes
            return new Date(Date.now() + timeToExhaustionMs);
        }
        return undefined;
    }
    /**
     * Create memory error based on current usage
     */
    createMemoryError(context) {
        const stats = this.checkMemoryUsage(context);
        const heapMB = stats.heapUsed / (1024 * 1024);
        return types_1.DataPilotError.memory(`Memory limit exceeded: ${heapMB.toFixed(1)}MB used (limit: ${this.config.thresholds.maxMB}MB)`, 'MEMORY_LIMIT_EXCEEDED', {
            ...context,
            memoryUsage: stats.heapUsed,
        }, [
            {
                action: 'Reduce data scope',
                description: 'Process data in smaller chunks or reduce maxRows',
                severity: types_1.ErrorSeverity.HIGH,
                command: '--maxRows 10000 or --chunkSize 1000',
            },
            {
                action: 'Increase memory limit',
                description: 'Increase available memory for the process',
                severity: types_1.ErrorSeverity.MEDIUM,
                command: '--max-old-space-size=2048',
            },
            {
                action: 'Use streaming mode',
                description: 'Enable streaming analysis for large datasets',
                severity: types_1.ErrorSeverity.HIGH,
                automated: true,
            },
        ]);
    }
    /**
     * Cleanup and dispose
     */
    dispose() {
        this.stopMonitoring();
        this.removeAllListeners();
        this.cleanupCallbacks = [];
        this.memoryHistory = [];
    }
    handleMemoryWarning(stats, context) {
        const heapMB = stats.heapUsed / (1024 * 1024);
        logger_1.logger.warn(`Memory usage warning: ${heapMB.toFixed(1)}MB`, {
            ...context,
            memoryUsage: stats.heapUsed,
        });
        if (this.config.enableAutomaticCleanup) {
            this.forceGarbageCollection(context);
        }
    }
    handleMemoryCritical(stats, context) {
        const heapMB = stats.heapUsed / (1024 * 1024);
        logger_1.logger.error(`Critical memory usage: ${heapMB.toFixed(1)}MB`, {
            ...context,
            memoryUsage: stats.heapUsed,
        });
        if (this.config.enableAutomaticCleanup) {
            this.runCleanup(context);
            this.forceGarbageCollection(context);
        }
        // Predict exhaustion
        const exhaustionTime = this.predictMemoryExhaustion();
        if (exhaustionTime) {
            const timeLeft = exhaustionTime.getTime() - Date.now();
            logger_1.logger.error(`Memory exhaustion predicted in ${(timeLeft / 1000).toFixed(1)} seconds`, context);
        }
    }
    handleMemoryMax(stats, context) {
        const heapMB = stats.heapUsed / (1024 * 1024);
        logger_1.logger.error(`Maximum memory limit reached: ${heapMB.toFixed(1)}MB`, {
            ...context,
            memoryUsage: stats.heapUsed,
        });
        if (this.config.enableAutomaticCleanup) {
            this.runCleanup(context);
            this.forceGarbageCollection(context);
        }
        // Throw error to stop processing
        throw this.createMemoryError(context);
    }
}
exports.MemoryManager = MemoryManager;
// Global memory manager instance
exports.globalMemoryManager = new MemoryManager();
/**
 * Resource cleanup utilities
 */
class ResourceManager {
    resources = new Map();
    disposed = false;
    /**
     * Register a resource for cleanup
     */
    register(id, cleanup, type = 'generic') {
        if (this.disposed) {
            logger_1.logger.warn(`Cannot register resource ${id}: ResourceManager is disposed`);
            return;
        }
        this.resources.set(id, { cleanup, type });
    }
    /**
     * Unregister a resource
     */
    unregister(id) {
        return this.resources.delete(id);
    }
    /**
     * Clean up a specific resource
     */
    cleanup(id, context) {
        const resource = this.resources.get(id);
        if (!resource) {
            return false;
        }
        try {
            resource.cleanup();
            this.resources.delete(id);
            logger_1.logger.debug(`Cleaned up resource: ${id} (${resource.type})`, context);
            return true;
        }
        catch (error) {
            logger_1.logger.error(`Failed to clean up resource ${id}`, context, error);
            return false;
        }
    }
    /**
     * Clean up all resources of a specific type
     */
    cleanupByType(type, context) {
        let cleaned = 0;
        for (const [id, resource] of this.resources.entries()) {
            if (resource.type === type) {
                if (this.cleanup(id, context)) {
                    cleaned++;
                }
            }
        }
        return cleaned;
    }
    /**
     * Clean up all resources
     */
    cleanupAll(context) {
        let cleaned = 0;
        for (const id of this.resources.keys()) {
            if (this.cleanup(id, context)) {
                cleaned++;
            }
        }
        return cleaned;
    }
    /**
     * Get resource count by type
     */
    getResourceCount(type) {
        if (!type) {
            return this.resources.size;
        }
        return Array.from(this.resources.values()).filter((r) => r.type === type).length;
    }
    /**
     * List all resources
     */
    listResources() {
        return Array.from(this.resources.entries()).map(([id, resource]) => ({
            id,
            type: resource.type,
        }));
    }
    /**
     * Dispose of the resource manager
     */
    dispose(context) {
        if (this.disposed) {
            return;
        }
        logger_1.logger.debug(`Disposing ResourceManager with ${this.resources.size} resources`, context);
        this.cleanupAll(context);
        this.disposed = true;
    }
    /**
     * Check if disposed
     */
    isDisposed() {
        return this.disposed;
    }
}
exports.ResourceManager = ResourceManager;
// Global resource manager instance
exports.globalResourceManager = new ResourceManager();
/**
 * Process cleanup handler
 */
class ProcessCleanupHandler {
    static instance;
    handlers = [];
    isShuttingDown = false;
    constructor() {
        // Register process exit handlers
        process.on('exit', this.handleExit.bind(this));
        process.on('SIGINT', (signal) => {
            void this.handleSignal(signal);
        });
        process.on('SIGTERM', (signal) => {
            void this.handleSignal(signal);
        });
        process.on('uncaughtException', (error) => {
            void this.handleUncaughtException(error);
        });
        process.on('unhandledRejection', (reason) => {
            void this.handleUnhandledRejection(reason);
        });
    }
    static getInstance() {
        if (!ProcessCleanupHandler.instance) {
            ProcessCleanupHandler.instance = new ProcessCleanupHandler();
        }
        return ProcessCleanupHandler.instance;
    }
    /**
     * Register cleanup handler
     */
    register(handler) {
        this.handlers.push(handler);
    }
    /**
     * Run all cleanup handlers
     */
    async runCleanup() {
        if (this.isShuttingDown) {
            return;
        }
        this.isShuttingDown = true;
        logger_1.logger.info('Running process cleanup handlers');
        for (const handler of this.handlers) {
            try {
                await handler();
            }
            catch (error) {
                logger_1.logger.error('Error in cleanup handler', undefined, error);
            }
        }
        // Clean up global managers
        try {
            exports.globalMemoryManager.dispose();
            exports.globalResourceManager.dispose();
        }
        catch (error) {
            logger_1.logger.error('Error disposing global managers', undefined, error);
        }
    }
    handleExit() {
        if (!this.isShuttingDown) {
            // Synchronous cleanup only for exit event
            logger_1.logger.info('Process exiting, running synchronous cleanup');
            exports.globalMemoryManager.dispose();
            exports.globalResourceManager.dispose();
        }
    }
    async handleSignal(signal) {
        logger_1.logger.info(`Received ${signal}, initiating graceful shutdown`);
        await this.runCleanup();
        process.exit(0);
    }
    async handleUncaughtException(error) {
        logger_1.logger.error('Uncaught exception, running cleanup before exit', undefined, error);
        await this.runCleanup();
        process.exit(1);
    }
    async handleUnhandledRejection(reason) {
        logger_1.logger.error('Unhandled promise rejection, running cleanup before exit', undefined, reason);
        await this.runCleanup();
        process.exit(1);
    }
}
exports.ProcessCleanupHandler = ProcessCleanupHandler;
// Initialize global cleanup handler
exports.globalCleanupHandler = ProcessCleanupHandler.getInstance();
//# sourceMappingURL=memory-manager.js.map