/**
 * Performance Optimizations for Code Generation
 * Implements caching, lazy-loading, and parallel generation
 */
import { Logger } from '../config/logger.js';
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import { fs } from '../node-singletons/index.js';
import * as path from '../node-singletons/path.js';
const GENERATION_CACHE_STATE_SYMBOL = Symbol.for('zintrust:GenerationCacheState');
function isUnrefableTimer(value) {
    if (typeof value !== 'object' || value === null)
        return false;
    return 'unref' in value && typeof value.unref === 'function';
}
function deleteFileNonBlocking(filePath) {
    try {
        const anyFs = fs;
        if (typeof anyFs.promises?.unlink === 'function') {
            void anyFs.promises.unlink(filePath).catch((err) => {
                const maybeErr = err;
                if (maybeErr.code === 'ENOENT')
                    return;
                Logger.error(`Failed to delete cache file: ${filePath} (${err instanceof Error ? err.message : String(err)})`);
            });
            return;
        }
        if (typeof anyFs.unlink === 'function') {
            anyFs.unlink(filePath, (err) => {
                if (err && err.code !== 'ENOENT') {
                    Logger.error(`Failed to delete cache file: ${filePath} (${err.message})`);
                }
            });
        }
    }
    catch (err) {
        Logger.error(`Failed to schedule cache file deletion: ${filePath} (${err instanceof Error ? err.message : String(err)})`);
    }
}
/**
 * Generation Cache - Cache generated code to avoid re-generating identical code
 * Sealed namespace for immutability
 */
export const GenerationCache = Object.freeze({
    /**
     * Create a new generation cache instance
     */
    create(cacheDir = path.join(process.cwd(), '.gen-cache'), ttlMs = 3600000, maxEntries = 1000) {
        const state = createCacheState(cacheDir, ttlMs, maxEntries);
        initializeCacheState(state);
        startCacheCleanup(state);
        const instance = createCacheInstance(state);
        attachCacheStateForTests(instance, state);
        return instance;
    },
});
function createCacheState(cacheDir, ttlMs, maxEntries) {
    return {
        cache: new Map(),
        cacheDir,
        ttlMs,
        maxEntries,
        pendingWrites: new Map(),
    };
}
async function ensureCacheDir(cacheDir) {
    try {
        await fs.fsPromises.access(cacheDir);
    }
    catch {
        await fs.fsPromises.mkdir(cacheDir, { recursive: true });
    }
}
async function flushPendingWrites(state) {
    if (state.pendingWrites.size === 0) {
        state.flushTimer = undefined;
        return false;
    }
    const pending = state.pendingWrites;
    state.pendingWrites = new Map();
    state.flushTimer = undefined;
    try {
        await ensureCacheDir(state.cacheDir);
    }
    catch (error) {
        Logger.error('Failed to ensure cache directory before flush', error);
        return false;
    }
    const writes = Array.from(pending.entries()).map(async ([key, entry]) => {
        const file = path.join(state.cacheDir, `${key}.json`);
        await fs.fsPromises.writeFile(file, entry.payload);
    });
    await Promise.all(writes);
    return true;
}
function scheduleCacheWrite(state, key, payload) {
    state.pendingWrites.set(key, { payload });
    if (state.flushTimer !== undefined)
        return;
    state.flushTimer = setTimeout(() => {
        void flushPendingWrites(state).catch((error) => {
            Logger.error('Failed to flush generation cache writes', error);
        });
    }, 50);
    if (isUnrefableTimer(state.flushTimer)) {
        state.flushTimer.unref();
    }
}
function initializeCacheState(state) {
    loadFromDisk(state);
}
function startCacheCleanup(state) {
    // Active cleanup every 10 minutes
    state.cleanupInterval = setInterval(() => {
        const now = Date.now();
        for (const [key, entry] of state.cache.entries()) {
            if (now - entry.timestamp > state.ttlMs) {
                state.cache.delete(key);
                const file = path.join(state.cacheDir, `${key}.json`);
                deleteFileNonBlocking(file);
            }
        }
        // Enforce maxEntries by evicting oldest keys
        if (state.maxEntries !== undefined) {
            while (state.cache.size > state.maxEntries) {
                const oldestKey = state.cache.keys().next().value;
                if (oldestKey === undefined)
                    break;
                state.cache.delete(oldestKey);
                const file = path.join(state.cacheDir, `${oldestKey}.json`);
                deleteFileNonBlocking(file);
            }
        }
    }, 600000);
    // Node: allow process to exit; other runtimes may not support unref()
    if (isUnrefableTimer(state.cleanupInterval)) {
        state.cleanupInterval.unref();
    }
}
function createCacheInstance(state) {
    return {
        /**
         * Get from cache (async)
         */
        async get(type, params) {
            const key = getCacheKey(type, params);
            const entry = state.cache.get(key);
            if (entry === undefined)
                return Promise.resolve(null); //NoSONAR
            // Check TTL
            if (Date.now() - entry.timestamp > state.ttlMs) {
                state.cache.delete(key);
                const file = path.join(state.cacheDir, `${key}.json`);
                deleteFileNonBlocking(file);
                return Promise.resolve(null); //NoSONAR
            }
            return Promise.resolve(entry.code); //NoSONAR
        },
        /**
         * Set in cache (async)
         */
        async set(type, params, code) {
            const key = getCacheKey(type, params);
            // If key already exists, delete first so insertion order updates for LRU
            if (state.cache.has(key))
                state.cache.delete(key);
            state.cache.set(key, {
                code,
                timestamp: Date.now(),
            });
            // Enforce maxEntries immediately
            if (state.maxEntries !== undefined) {
                while (state.cache.size > state.maxEntries) {
                    const oldest = state.cache.keys().next().value;
                    if (oldest === undefined)
                        break;
                    state.cache.delete(oldest);
                    const file = path.join(state.cacheDir, `${oldest}.json`);
                    deleteFileNonBlocking(file);
                }
            }
            const payload = JSON.stringify({ code, timestamp: Date.now() }, null, 2);
            scheduleCacheWrite(state, key, payload);
            return Promise.resolve(); // NOSONAR
        },
        /**
         * Save cache to disk (async)
         */
        async save() {
            await saveCacheToDisk(state);
        },
        /**
         * Clear cache (async)
         */
        async clear() {
            stopCacheTimers(state);
            state.pendingWrites.clear();
            await clearCache(state);
        },
        // eslint-disable-next-line @typescript-eslint/require-await
        async dispose() {
            stopCacheTimers(state);
            state.pendingWrites.clear();
            state.cache.clear();
        },
        /**
         * Get cache statistics (async)
         */
        async getStats() {
            return getCacheStats(state);
        },
    };
}
function stopCacheTimers(state) {
    if (state.cleanupInterval) {
        clearInterval(state.cleanupInterval);
        state.cleanupInterval = undefined;
    }
    if (state.flushTimer) {
        clearTimeout(state.flushTimer);
        state.flushTimer = undefined;
    }
}
function attachCacheStateForTests(instance, state) {
    Object.defineProperty(instance, GENERATION_CACHE_STATE_SYMBOL, {
        value: state,
        enumerable: false,
    });
}
/**
 * Save cache to disk (async)
 */
async function saveCacheToDisk(state) {
    try {
        if (state.flushTimer !== undefined) {
            clearTimeout(state.flushTimer);
            state.flushTimer = undefined;
        }
        const flushedEnsured = await flushPendingWrites(state);
        if (!flushedEnsured) {
            await ensureCacheDir(state.cacheDir);
        }
        const writes = Array.from(state.cache.entries()).map(async ([key, entry]) => {
            const file = path.join(state.cacheDir, `${key}.json`);
            await fs.fsPromises.writeFile(file, JSON.stringify(entry, null, 2));
        });
        await Promise.all(writes);
    }
    catch (error) {
        Logger.error('Failed to save cache to disk', error);
    }
}
/**
 * Clear cache (async)
 */
async function clearCache(state) {
    state.cache.clear();
    state.pendingWrites.clear();
    try {
        try {
            await fs.fsPromises.access(state.cacheDir);
        }
        catch {
            return; // Dir doesn't exist
        }
        await fs.fsPromises.rm(state.cacheDir, { recursive: true });
    }
    catch (error) {
        Logger.error('Failed to clear cache', error);
    }
}
/**
 * Get cache statistics (async)
 */
async function getCacheStats(state) {
    let diskUsage = 0;
    try {
        try {
            await fs.fsPromises.access(state.cacheDir);
            const files = await fs.fsPromises.readdir(state.cacheDir);
            const sizes = await Promise.all(files.map(async (file) => {
                const stats = await fs.fsPromises.stat(path.join(state.cacheDir, file));
                return stats.size;
            }));
            diskUsage = sizes.reduce((sum, size) => sum + size, 0);
        }
        catch {
            // ignore
        }
    }
    catch {
        // ignore
    }
    return {
        size: diskUsage,
        entries: state.cache.size,
        diskUsage: formatBytes(diskUsage),
        keys: Array.from(state.cache.keys()),
    };
}
function formatBytes(bytes) {
    const units = ['B', 'KB', 'MB'];
    let size = bytes;
    let i = 0;
    while (size > 1024 && i < units.length - 1) {
        size /= 1024;
        i++;
    }
    return `${size.toFixed(2)} ${units[i]}`;
}
/**
 * Get cache key from params
 */
function getCacheKey(type, params) {
    // Use a more efficient key generation
    // Simple hash-like string for the key
    const paramStr = JSON.stringify(params, Object.keys(params).sort((a, b) => a.localeCompare(b)));
    let hash = 0;
    for (let i = 0; i < paramStr.length; i++) {
        const char = paramStr.codePointAt(i);
        hash = (hash << 5) - hash + (char ?? 0);
        hash = toInt32(hash); // Convert to 32bit integer
    }
    return `${type}:${hash.toString(36)}:${Buffer.from(paramStr.slice(0, 32)).toString('base64')}`;
}
/**
 * Convert a number to a signed 32-bit integer (equivalent to JS ToInt32)
 */
function toInt32(value) {
    const truncated = Math.trunc(value);
    const uint32 = ((truncated % 4294967296) + 4294967296) % 4294967296;
    return uint32 > 2147483647 ? uint32 - 4294967296 : uint32;
}
/**
 * Load cache from disk (async)
 */
async function loadFromDisk(state) {
    try {
        try {
            await fs.fsPromises.access(state.cacheDir);
        }
        catch {
            return;
        }
        const files = await fs.fsPromises.readdir(state.cacheDir);
        const jsonFiles = files.filter((file) => file.endsWith('.json') === true);
        const parsedEntries = await Promise.all(jsonFiles.map(async (file) => {
            const filePath = path.join(state.cacheDir, file);
            const content = await fs.fsPromises.readFile(filePath, 'utf-8');
            try {
                const data = JSON.parse(content);
                return { key: file.replace('.json', ''), data };
            }
            catch {
                // ignore corrupted files
                return null;
            }
        }));
        for (const entry of parsedEntries) {
            if (entry !== null) {
                state.cache.set(entry.key, entry.data);
            }
        }
    }
    catch (err) {
        Logger.error(`Failed to load cache from disk: ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Lazy Module Loader - Load dependencies only when needed
 * Sealed namespace for immutability
 */
export const LazyLoader = Object.freeze({
    /**
     * Create a new lazy loader instance
     */
    create() {
        const modules = new Map();
        return {
            /**
             * Lazy load a module
             */
            async load(modulePath) {
                const cached = modules.get(modulePath);
                if (cached !== undefined) {
                    return cached;
                }
                try {
                    const module = await import(modulePath);
                    modules.set(modulePath, module);
                    return module;
                }
                catch (err) {
                    throw ErrorFactory.createTryCatchError(`Failed to load module: ${modulePath}`, err);
                }
            },
            /**
             * Preload modules
             */
            async preload(modulePaths) {
                await Promise.all(modulePaths.map(async (path) => this.load(path)));
            },
            /**
             * Clear loaded modules
             */
            clear() {
                modules.clear();
            },
        };
    },
});
/**
 * Parallel Generator - Run multiple generators in parallel
 */
/**
 * Run generators in parallel batches
 */
export async function runBatch(generators, batchSize = 3) {
    const batches = [];
    for (let i = 0; i < generators.length; i += batchSize) {
        batches.push(generators.slice(i, i + batchSize));
    }
    return batches.reduce(async (accPromise, batch) => {
        const acc = await accPromise;
        const batchResults = await Promise.all(batch.map(async (gen) => gen()));
        return acc.concat(batchResults);
    }, Promise.resolve([]));
}
/**
 * Run all generators in parallel
 */
export async function runAll(generators) {
    return Promise.all(generators.map(async (gen) => gen()));
}
export const ParallelGenerator = Object.freeze({
    runBatch,
    runAll,
});
/**
 * Memoize - Cache function results based on arguments
 */
/**
 * Create a memoized version of a function
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createMemoized(fn, options = {}) {
    const cache = new Map();
    const maxSize = options.maxSize ?? 1000; // Default max size: 1000 entries
    const evictLRU = () => {
        if (cache.size < maxSize)
            return;
        // Find oldest entry (LRU)
        let oldestKey = null;
        let oldestTime = Infinity;
        for (const [key, entry] of cache.entries()) {
            if (entry.lastAccessAt < oldestTime) {
                oldestTime = entry.lastAccessAt;
                oldestKey = key;
            }
        }
        if (oldestKey !== null) {
            cache.delete(oldestKey);
        }
    };
    return ((...args) => {
        let key;
        if (options.keyGenerator === undefined) {
            // Optimization: Avoid JSON.stringify for simple primitive arguments
            const arePrimitives = args.every((a) => a === null ||
                typeof a === 'string' ||
                typeof a === 'number' ||
                typeof a === 'boolean' ||
                a === undefined);
            key = arePrimitives ? args.join('|') : JSON.stringify(args);
        }
        else {
            key = options.keyGenerator(args);
        }
        const entry = cache.get(key);
        if (entry !== undefined) {
            if (options.ttl === undefined || Date.now() - entry.createdAt < options.ttl) {
                // Update access time for LRU; TTL remains based on creation time
                entry.lastAccessAt = Date.now();
                return entry.result;
            }
            cache.delete(key);
        }
        const result = fn(...args);
        // Evict LRU before adding new entry
        evictLRU();
        const now = Date.now();
        cache.set(key, { result, createdAt: now, lastAccessAt: now });
        return result;
    });
}
export const Memoize = Object.freeze({
    create: createMemoized,
});
/**
 * Performance Optimizer - Wrapper for optimizations
 * Sealed namespace for immutability
 */
export const PerformanceOptimizer = Object.freeze({
    /**
     * Create a new performance optimizer instance
     */
    create() {
        const cache = GenerationCache.create();
        const lazyLoader = LazyLoader.create();
        const stats = {
            cacheHits: 0,
            cacheMisses: 0,
            parallelRuns: 0,
            savedTime: 0,
        };
        return {
            /**
             * Generate with caching
             */
            async generateWithCache(type, params, generatorFn) {
                return generateWithCache(cache, stats, type, params, generatorFn);
            },
            /**
             * Generate multiple in parallel
             */
            async generateInParallel(generators, batchSize) {
                return generateInParallel(stats, generators, batchSize);
            },
            /**
             * Preload modules
             */
            async preloadModules(paths) {
                await lazyLoader.preload(paths);
            },
            /**
             * Get optimization statistics
             */
            getStats() {
                return getOptimizerStats(cache, stats);
            },
            /**
             * Save cache to disk
             */
            saveCache() {
                cache.save();
            },
            /**
             * Clear everything
             */
            clear() {
                resetOptimizer(cache, lazyLoader, stats);
            },
        };
    },
});
/**
 * Reset optimizer state
 */
function resetOptimizer(cache, lazyLoader, stats) {
    cache.clear();
    lazyLoader.clear();
    stats.cacheHits = 0;
    stats.cacheMisses = 0;
    stats.parallelRuns = 0;
    stats.savedTime = 0;
}
/**
 * Generate multiple in parallel
 */
async function generateInParallel(stats, generators, batchSize) {
    stats.parallelRuns++;
    if (batchSize !== undefined) {
        return ParallelGenerator.runBatch(generators, batchSize);
    }
    return ParallelGenerator.runAll(generators);
}
/**
 * Generate with caching
 */
async function generateWithCache(cache, stats, type, params, generatorFn) {
    // Try cache
    let cached = null;
    try {
        cached = await cache.get(type, params);
    }
    catch (err) {
        Logger.warn('GenerationCache.get failed; treating as cache miss', {
            type,
            error: err instanceof Error ? err.message : String(err),
        });
    }
    if (cached !== null) {
        try {
            stats.cacheHits++;
            return JSON.parse(cached);
        }
        catch (err) {
            Logger.warn('Failed to parse cached generation result; treating as cache miss', {
                type,
                error: err instanceof Error ? err.message : String(err),
            });
        }
    }
    // Generate
    const startTime = performance.now();
    const result = await generatorFn();
    const duration = performance.now() - startTime;
    // Cache result (fire-and-forget)
    void cache
        .set(type, params, JSON.stringify(result))
        .catch((err) => Logger.error('GenerationCache.set failed', err));
    stats.cacheMisses++;
    stats.savedTime += duration;
    return result;
}
/**
 * Get optimization statistics
 */
function getCacheStatusSync(cache) {
    const state = cache[GENERATION_CACHE_STATE_SYMBOL];
    const map = state?.cache;
    if (!(map instanceof Map))
        return { size: 0, keys: [] };
    return {
        size: map.size,
        keys: Array.from(map.keys()),
    };
}
function getOptimizerStats(cache, stats) {
    const total = stats.cacheHits + stats.cacheMisses;
    const hitRate = total > 0 ? (stats.cacheHits / total) * 100 : 0;
    return {
        cacheHits: stats.cacheHits,
        cacheMisses: stats.cacheMisses,
        hitRate: `${hitRate.toFixed(1)}%`,
        parallelRuns: stats.parallelRuns,
        estimatedSavedTime: `${stats.savedTime.toFixed(2)}ms`,
        cacheStatus: getCacheStatusSync(cache),
    };
}
export default PerformanceOptimizer;
