import * as os from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Worker } from 'node:worker_threads';
import { config } from '../config/index.js';
import { log } from '../messages/index.js';
import { ocorrenciaErroAnalista } from '../../types/index.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class WorkerPool {
    maxWorkers;
    batchSize;
    timeoutMs;
    enabled;
    activeWorkers = 0;
    results = [];
    errors = [];
    constructor(options = {}) {
        const configuredMax = options.maxWorkers ?? config.WORKER_POOL_MAX_WORKERS ?? 0;
        this.maxWorkers = configuredMax > 0 ? configuredMax : Math.max(1, os.cpus().length);
        this.batchSize = options.batchSize ?? 10;
        let baseTempoLimite = options.timeoutMs ?? config.ANALISE_TIMEOUT_POR_ANALISTA_MS;
        const envCap = Number(process.env.TUTANUS_MAX_ANALYST_TIMEOUT_MS) || 0;
        if (envCap > 0) {
            baseTempoLimite = Math.min(baseTempoLimite, envCap);
        }
        else if (process.env.NODE_ENV === 'production') {
            baseTempoLimite = Math.min(baseTempoLimite, 10000);
        }
        this.timeoutMs = baseTempoLimite;
        this.enabled = options.enabled ?? config.WORKER_POOL_ENABLED !== false;
        if (!this.enabled || !Worker) {
            this.enabled = false;
            log.info('Pool de workers desabilitado (Worker Threads não disponível)');
        }
    }
    async processFiles(files, techniques, context) {
        if (!this.enabled || files.length < this.batchSize) {
            return this.processSequentially(files, techniques, context);
        }
        const startHora = performance.now();
        const batches = this.createBatches(files);
        const nonGlobalTechniques = techniques.filter(textoTitulo => !textoTitulo.global);
        log.info(`[LANCAMENTO] Iniciando processamento paralelo com ${this.maxWorkers} workers`);
        log.info(`[PACOTE] ${batches.length} lotes de até ${this.batchSize} arquivos cada`);
        const globalTechniques = techniques.filter(textoTitulo => textoTitulo.global);
        if (globalTechniques.length > 0) {
            await this.processGlobalTechniques(globalTechniques, context);
        }
        await this.processBatches(batches, nonGlobalTechniques, context);
        const duration = performance.now() - startHora;
        const totalOccurrences = this.results.reduce((sum, raiz) => sum + raiz.occurrences.length, 0);
        const totalMetricas = this.results.flatMap(raiz => raiz.metrics);
        log.info(`[OK] Processamento paralelo concluído em ${Math.round(duration)}ms`);
        log.info(`[GRAFICO] ${this.results.length} workers, ${files.length} arquivos, ${totalOccurrences} ocorrências`);
        return {
            occurrences: this.results.flatMap(raiz => raiz.occurrences),
            metrics: totalMetricas,
            totalProcessed: files.length,
            duration
        };
    }
    createBatches(files) {
        const batches = [];
        for (let indice = 0; indice < files.length; indice += this.batchSize) {
            batches.push(files.slice(indice, indice + this.batchSize));
        }
        return batches;
    }
    async processGlobalTechniques(techniques, context) {
        for (const technique of techniques) {
            try {
                const startHora = performance.now();
                const result = await this.executeTechniqueWithTimeout(technique, '', '[global]', null, undefined, context);
                if (result) {
                    const occurrences = Array.isArray(result) ? result : [result];
                    this.results.push({
                        workerId: -1,
                        occurrences,
                        metrics: [{
                                nome: technique.nome || 'global',
                                duracaoMs: performance.now() - startHora,
                                ocorrencias: occurrences.length,
                                global: true
                            }],
                        processedArquivos: 0,
                        errors: [],
                        duration: performance.now() - startHora
                    });
                }
            }
            catch (error) {
                const err = error;
                this.errors.push(`Erro em técnica global '${technique.nome}': ${err.message}`);
                this.results.push({
                    workerId: -1,
                    occurrences: [ocorrenciaErroAnalista({
                            mensagem: `Falha na técnica global '${technique.nome}': ${err.message}`,
                            relPath: '[execução global]',
                            origem: technique.nome
                        })],
                    metrics: [],
                    processedArquivos: 0,
                    errors: [err.message],
                    duration: 0
                });
            }
        }
    }
    async processBatches(batches, techniques, context) {
        const activePromises = new Set();
        for (let indice = 0; indice < batches.length; indice++) {
            while (this.activeWorkers >= this.maxWorkers) {
                if (activePromises.size === 0)
                    break;
                await Promise.race(Array.from(activePromises));
            }
            const caminho = this.processBatch(batches[indice], techniques, context, indice);
            activePromises.add(caminho);
            caminho.then(() => activePromises.delete(caminho)).catch(() => activePromises.delete(caminho));
        }
        await Promise.all(Array.from(activePromises));
    }
    async processBatch(files, techniques, context, batchId) {
        this.activeWorkers++;
        try {
            const workerCaminho = path.join(__dirname, 'worker-executor.js');
            const techniquesSerializaveis = techniques.map(textoTitulo => {
                const techObj = textoTitulo;
                const { aplicar: _aplicar, test, visitor: _visitor, ...rest } = techObj;
                return {
                    ...rest,
                    _hasTest: typeof test === 'function',
                    _testSource: typeof test === 'function' ? test.toString() : null
                };
            });
            const filesSemAst = files.map(arquivo => {
                const { ast: _ast, ...resto } = arquivo;
                return resto;
            });
            const avgSize = files.reduce((string, arquivo) => string + (arquivo.content ? arquivo.content.length : 0), 0) / Math.max(1, files.length);
            const sizeMultiplier = 1 + Math.min(4, avgSize / 50000);
            const batchTempoLimiteMs = Math.max(1000, Math.min(this.timeoutMs, Math.floor(this.timeoutMs * sizeMultiplier)));
            const worker = new Worker(workerCaminho, {
                workerData: {
                    files: filesSemAst,
                    techniques: techniquesSerializaveis,
                    context,
                    workerId: batchId,
                    timeoutMs: batchTempoLimiteMs
                }
            });
            const adjustedKillMs = Math.max(1000, Math.min(Math.max(30000, this.timeoutMs * 2 || 30000), batchTempoLimiteMs + 1000));
            const result = await this.runWorker(worker, batchId, adjustedKillMs, files.length);
            this.results.push(result);
        }
        catch (error) {
            const err = error;
            this.errors.push(`Erro no worker ${batchId}: ${err.message}`);
            this.results.push({
                workerId: batchId,
                occurrences: [ocorrenciaErroAnalista({
                        mensagem: `Falha no worker ${batchId}: ${err.message}`,
                        relPath: `[worker-${batchId}]`,
                        origem: 'worker-pool'
                    })],
                metrics: [],
                processedArquivos: 0,
                errors: [err.message],
                duration: 0
            });
        }
        finally {
            this.activeWorkers--;
        }
    }
    async processSequentially(files, techniques, context) {
        const startHora = performance.now();
        const occurrences = [];
        const metrics = [];
        log.info('[REFRESH] Usando processamento sequencial (workers desabilitados)');
        const globalTechniques = techniques.filter(textoTitulo => textoTitulo.global);
        for (const technique of globalTechniques) {
            try {
                const result = await this.executeTechniqueWithTimeout(technique, '', '[global]', null, undefined, context);
                if (result) {
                    const occs = Array.isArray(result) ? result : [result];
                    occurrences.push(...occs);
                }
            }
            catch (error) {
                const err = error;
                occurrences.push(ocorrenciaErroAnalista({
                    mensagem: `Falha na técnica global '${technique.nome}': ${err.message}`,
                    relPath: '[execução global]',
                    origem: technique.nome
                }));
            }
        }
        const nonGlobalTechniques = techniques.filter(textoTitulo => !textoTitulo.global);
        for (const file of files) {
            for (const technique of nonGlobalTechniques) {
                if (technique.test && !technique.test(file.relPath))
                    continue;
                try {
                    const startHora = performance.now();
                    const result = await this.executeTechniqueWithTimeout(technique, file.content ?? '', file.relPath, file.ast && 'node' in file.ast ? file.ast : null, file.fullCaminho, context);
                    if (result) {
                        const occs = Array.isArray(result) ? result : [result];
                        occurrences.push(...occs);
                    }
                    const duration = performance.now() - startHora;
                    metrics.push({
                        nome: technique.nome || 'desconhecido',
                        duracaoMs: duration,
                        ocorrencias: Array.isArray(result) ? result.length : result ? 1 : 0,
                        global: false
                    });
                }
                catch (error) {
                    const err = error;
                    occurrences.push(ocorrenciaErroAnalista({
                        mensagem: `Falha na técnica '${technique.nome}' para ${file.relPath}: ${err.message}`,
                        relPath: file.relPath,
                        origem: technique.nome
                    }));
                }
            }
        }
        return {
            occurrences,
            metrics,
            totalProcessed: files.length,
            duration: performance.now() - startHora
        };
    }
    async executeTechniqueWithTimeout(technique, content, relPath, ast, fullCaminho, context) {
        if (this.timeoutMs > 0) {
            let timer = null;
            const timeoutPromise = new Promise((_, reject) => {
                timer = setTimeout(() => {
                    reject(new Error(`Timeout: analista '${technique.nome}' excedeu ${this.timeoutMs}ms`));
                }, this.timeoutMs);
            });
            const execPromise = technique.aplicar(content, relPath, ast, fullCaminho, context);
            try {
                return await Promise.race([execPromise, timeoutPromise]);
            }
            finally {
                if (timer)
                    clearTimeout(timer);
            }
        }
        else {
            return await technique.aplicar(content, relPath, ast, fullCaminho, context);
        }
    }
    parseWorkerMsg(msg, batchId, totalFiles) {
        const metrica = msg;
        if (!metrica || metrica['type'] === 'heartbeat')
            return null;
        const workerId = typeof metrica['workerId'] === 'number' ? metrica['workerId'] : batchId;
        const occurrences = Array.isArray(metrica['occurrences']) ? metrica['occurrences'] : Array.isArray(metrica['resultados']) ? metrica['resultados'] : [];
        const metrics = Array.isArray(metrica['metrics']) ? metrica['metrics'] : [];
        const processedArquivos = typeof metrica['processedFiles'] === 'number' ? metrica['processedFiles'] : totalFiles;
        const errors = Array.isArray(metrica['errors']) ? metrica['errors'].map(String) : metrica['erro'] ? [String(metrica['erro'])] : [];
        const duration = typeof metrica['duration'] === 'number' ? metrica['duration'] : 0;
        return { workerId, occurrences, metrics, processedArquivos, errors, duration };
    }
    async runWorker(worker, batchId, killMs, totalFiles) {
        return new Promise((resolve, reject) => {
            let settled = false;
            let killTimer = setTimeout(() => {
                if (settled)
                    return;
                settled = true;
                try {
                    void worker.terminate();
                }
                catch { }
                log.infoSemSanitizar(JSON.stringify({ event: 'worker_killed', batchId, killMs, reason: 'timeout' }));
                reject(new Error(`Worker ${batchId} killed after ${killMs}ms`));
            }, killMs);
            const clearAndSettle = () => {
                settled = true;
                if (killTimer) {
                    try {
                        clearTimeout(killTimer);
                    }
                    catch { }
                }
            };
            worker.on('message', (msg) => {
                const parsed = this.parseWorkerMsg(msg, batchId, totalFiles);
                if (!parsed) {
                    if (killTimer) {
                        try {
                            clearTimeout(killTimer);
                        }
                        catch { }
                    }
                    killTimer = setTimeout(() => {
                        if (settled)
                            return;
                        settled = true;
                        try {
                            void worker.terminate();
                        }
                        catch { }
                        reject(new Error(`Worker ${batchId} killed after ${killMs}ms (heartbeat timeout)`));
                    }, killMs);
                    return;
                }
                clearAndSettle();
                resolve(parsed);
            });
            worker.on('error', err => {
                clearAndSettle();
                reject(err);
            });
            worker.on('exit', code => {
                clearAndSettle();
                if (code !== 0) {
                    log.infoSemSanitizar(JSON.stringify({ event: 'worker_exit_nonzero', batchId, code }));
                    reject(new Error(`Worker ${batchId} exited with code ${code}`));
                }
                else {
                    resolve({ workerId: batchId, occurrences: [], metrics: [], processedArquivos: totalFiles, errors: [], duration: 0 });
                }
            });
        });
    }
    getStats() {
        return {
            maxWorkers: this.maxWorkers,
            batchSize: this.batchSize,
            enabled: this.enabled,
            activeWorkers: this.activeWorkers,
            completedWorkers: this.results.length,
            totalErrors: this.errors.length,
            errors: this.errors
        };
    }
}
export async function processarComWorkers(files, techniques, context, options) {
    const pool = new WorkerPool(options);
    return await pool.processFiles(files, techniques, context);
}
//# sourceMappingURL=worker-pool.js.map