import crypto from 'node:crypto';
import path from 'node:path';
import { config, formatMs, traverse } from '../config/index.js';
import { messages } from '../messages/index.js';
import { createDefaultReporter } from '../reporting/index.js';
import { WorkerPool } from '../workers/index.js';
import { calcularHashEstrutural } from '../../shared/helpers/index.js';
import { lerEstado, salvarEstado } from '../../shared/persistence/index.js';
import XXH from 'xxhashjs';
import { ocorrenciaErroAnalista } from '../../types/index.js';
import { analysisCache } from './analysis-cache.js';
import { ScopeAstCache } from './ast-cache.js';
async function withTimeout(value, timeoutMs, errorMessage) {
    let timer = null;
    try {
        return await Promise.race([
            Promise.resolve(value),
            new Promise((_, reject) => {
                timer = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
            })
        ]);
    }
    finally {
        if (timer)
            clearTimeout(timer);
    }
}
function getGlobalExec() {
    return globalThis;
}
async function persistMetricHistory(metricas) {
    const historicoCaminho = config.ANALISE_METRICAS_HISTORICO_PATH;
    if (!historicoCaminho)
        return;
    const anterior = await lerEstado(historicoCaminho).catch(() => []);
    const lista = Array.isArray(anterior) ? anterior : [];
    lista.push({ ...metricas, timestamp: Date.now() });
    const max = config.ANALISE_METRICAS_HISTORICO_MAX || 200;
    await salvarEstado(historicoCaminho, lista.slice(-max));
}
function infoDestaque(msg) {
    const logCasted = messages.log;
    if (typeof logCasted.infoDestaque === 'function') {
        logCasted.infoDestaque(msg);
    }
    else {
        logCasted.info(msg);
    }
}
function combinarVisitors(tecnicas) {
    const visitantes = tecnicas.filter((t) => 'visitor' in t);
    const visitorCombinado = {};
    if (visitantes.length === 0)
        return visitorCombinado;
    for (const item of visitantes) {
        if (!item.visitor)
            continue;
        for (const [key, val] of Object.entries(item.visitor)) {
            if (!visitorCombinado[key]) {
                visitorCombinado[key] = val;
            }
            else {
                const prev = visitorCombinado[key];
                visitorCombinado[key] = (path, state) => {
                    if (typeof prev === 'function') {
                        prev(path, state);
                    }
                    else if (prev && typeof prev === 'object' && 'enter' in prev && typeof prev.enter === 'function') {
                        prev.enter(path, state);
                    }
                    if (typeof val === 'function') {
                        val(path, state);
                    }
                    else if (val && typeof val === 'object' && 'enter' in val && typeof val.enter === 'function') {
                        val.enter(path, state);
                    }
                };
            }
        }
    }
    return visitorCombinado;
}
export async function executarInquisicao(fileEntriesComAst, tecnicas, baseDir, guardianResultado, opts) {
    const ocorrencias = [];
    const metricasAnalistas = [];
    const arquivosValidosSet = new Set(fileEntriesComAst.map(arquivo => arquivo.relPath));
    const emitter = opts?.events;
    const onOcorrencia = opts?.onOcorrencia;
    const pushOcorrencia = (...items) => {
        ocorrencias.push(...items);
        if (onOcorrencia) {
            for (const item of items) {
                onOcorrencia(item);
            }
        }
    };
    const contextoGlobalBase = {
        baseDir,
        arquivos: fileEntriesComAst,
        ambiente: {
            arquivosValidosSet,
            guardian: guardianResultado
        }
    };
    const reporter = createDefaultReporter();
    const inicioExecucao = performance.now();
    const scopeAstCache = new ScopeAstCache();
    function hasNodeType(x) {
        if (typeof x !== 'object' || x === null)
            return false;
        const maybe = x;
        if (!maybe.node || typeof maybe.node !== 'object')
            return false;
        return 'type' in maybe.node;
    }
    async function processGlobalTechniques(tecnicas, ctx, pushFn, metricaList, opts) {
        const timeoutMs = config.ANALISE_TIMEOUT_POR_ANALISTA_MS;
        for (const tecnica of tecnicas) {
            if (!tecnica.global)
                continue;
            try {
                const inicioAnalista = performance.now();
                const result = timeoutMs > 0
                    ? await withTimeout(tecnica.aplicar('', '', null, undefined, ctx), timeoutMs, messages.ExecutorExtraMensagens.timeoutGlobal.replace('{nome}', tecnica.nome || 'desconhecido').replace('{timeout}', String(timeoutMs)))
                    : await tecnica.aplicar('', '', null, undefined, ctx);
                if (Array.isArray(result))
                    pushFn(...result);
                const duracaoMs = performance.now() - inicioAnalista;
                if (config.ANALISE_METRICAS_ENABLED) {
                    metricaList.push({
                        nome: tecnica.nome || 'desconhecido',
                        duracaoMs,
                        ocorrencias: Array.isArray(result) ? result.length : result ? 1 : 0,
                        global: true
                    });
                }
                if (opts?.verbose)
                    messages.log.sucesso(messages.ExecutorExtraMensagens.tecnicaGlobal.replace('{nome}', tecnica.nome || 'desconhecido'));
                if (config.LOG_ESTRUTURADO) {
                    messages.log.info(JSON.stringify({ tipo: 'analista', escopo: 'global', nome: tecnica.nome, duracaoMs, ocorrencias: metricaList.at(-1)?.ocorrencias }));
                }
            }
            catch (error) {
                const err = error instanceof Error ? error : new Error(String(error));
                const isTempoLimite = err.message.includes('Timeout: analista global');
                const nivelLog = isTempoLimite ? 'aviso' : 'erro';
                const prefixo = isTempoLimite ? '[CLOCK]' : '[ERRO]';
                if (nivelLog === 'aviso') {
                    messages.log.aviso(`${prefixo} ${err.message}`);
                }
                else {
                    messages.log.erro(`${prefixo} ${messages.ExecutorExtraMensagens.erroTecnicaGlobal.replace('{prefixo}', prefixo).replace('{nome}', tecnica.nome || 'desconhecido').replace('{erro}', err.message)}`);
                    if (err.stack && (opts?.verbose || config.DEV_MODE)) {
                        messages.log.info(messages.ExecutorExtraMensagens.stackTrace);
                        messages.log.info(err.stack);
                    }
                }
                pushFn(ocorrenciaErroAnalista({
                    mensagem: isTempoLimite ? `Timeout na técnica global '${tecnica.nome}': ${timeoutMs}ms excedido` : `Falha na técnica global '${tecnica.nome}': ${err.message}`,
                    relPath: '[execução global]',
                    origem: tecnica.nome,
                    stack: !isTempoLimite && err.stack ? err.stack : undefined
                }));
            }
        }
    }
    let estadoIncremental = null;
    if (config.ANALISE_INCREMENTAL_ENABLED) {
        const lido = await lerEstado(config.ANALISE_INCREMENTAL_STATE_PATH).catch(() => null);
        if (lido && lido.versao === config.ANALISE_INCREMENTAL_VERSION)
            estadoIncremental = lido;
    }
    const novoEstado = {
        versao: config.ANALISE_INCREMENTAL_VERSION,
        arquivos: {},
        estatisticas: {
            totalReaproveitamentos: 0,
            totalArquivosProcessados: 0,
            ultimaDuracaoMs: 0
        }
    };
    function hashConteudo(elem) {
        try {
            return XXH.h64(elem, 0xabcd).toString(16);
        }
        catch {
            return crypto.createHash('sha1').update(elem).digest('hex');
        }
    }
    await processGlobalTechniques(tecnicas, contextoGlobalBase, pushOcorrencia, metricasAnalistas, opts);
    async function processarWorkerPool() {
        messages.log.info(messages.ExecutorExtraMensagens.modoRapido);
        const wp = new WorkerPool({
            enabled: true,
            maxWorkers: config.WORKER_POOL_MAX_WORKERS || undefined,
            batchSize: 10,
            timeoutMs: config.ANALISE_TIMEOUT_POR_ANALISTA_MS
        });
        const resultado = await wp.processFiles(fileEntriesComAst, tecnicas.filter(t => !t.global), contextoGlobalBase);
        pushOcorrencia(...resultado.occurrences);
        metricasAnalistas.push(...resultado.metrics);
        const duracao = performance.now() - inicioExecucao;
        messages.log.sucesso(messages.ExecutorExtraMensagens.analiseRapidaConcluida.replace('{arquivos}', String(fileEntriesComAst.length)).replace('{duracao}', formatMs(duracao)));
        if (config.ANALISE_INCREMENTAL_ENABLED) {
            try {
                for (const entry of fileEntriesComAst) {
                    const hash = hashConteudo(entry.content ?? '');
                    novoEstado.arquivos[entry.relPath] = { hash, ocorrencias: [], reaproveitadoCount: 0 };
                }
                if (novoEstado.estatisticas) {
                    novoEstado.estatisticas.totalArquivosProcessados = fileEntriesComAst.length;
                    novoEstado.estatisticas.ultimaDuracaoMs = duracao;
                }
                await salvarEstado(config.ANALISE_INCREMENTAL_STATE_PATH, novoEstado);
            }
            catch { }
        }
        const metricasExecucao = {
            totalArquivos: fileEntriesComAst.length,
            tempoTotal: duracao,
            ocorrenciasEncontradas: ocorrencias.length,
            analistas: metricasAnalistas,
            workerPool: { workersAtivos: resultado.totalProcessed, duracaoTotalMs: resultado.duration }
        };
        const resultadoFast = {
            totalArquivos: fileEntriesComAst.length,
            arquivosAnalisados: fileEntriesComAst.map(e => e.relPath),
            ocorrencias,
            timestamp: Date.now(),
            duracaoMs: duracao,
            metricas: metricasExecucao
        };
        if (emitter)
            emitter.emit('analysis:complete', resultadoFast);
        return resultadoFast;
    }
    const usarWorkerPool = (() => {
        if (opts?.sequential)
            return false;
        if (opts?.fast !== undefined)
            return opts.fast;
        return config.WORKER_POOL_ENABLED && fileEntriesComAst.length > config.WORKER_POOL_AUTO_THRESHOLD;
    })();
    if (usarWorkerPool && fileEntriesComAst.length > 0) {
        return processarWorkerPool();
    }
    const contextoGlobal = {
        ...contextoGlobalBase,
        report: event => {
            try {
                pushOcorrencia(reporter(event));
            }
            catch (erro) {
                pushOcorrencia(ocorrenciaErroAnalista({
                    mensagem: messages.ExecutorExtraMensagens.falhaReporter(erro instanceof Error ? erro.message : String(erro)),
                    relPath: event?.relPath || '[report]',
                    origem: 'reporter'
                }));
            }
        }
    };
    const visitorCombinado = combinarVisitors(tecnicas);
    let arquivoAtual = 0;
    const totalArquivos = fileEntriesComAst.length;
    const LIMIAR_DETALHE_TOTAL = 100;
    const LIMIAR_DETALHE_LIMITADO_MAX = 250;
    function passoDeLog(total) {
        if (total <= 100)
            return 1;
        if (total <= 250)
            return 10;
        if (total <= 500)
            return 25;
        return 100;
    }
    const frames = ['->', '=>', '>>', '=>'];
    const stepVerbose = passoDeLog(totalArquivos);
    const detalharPorArquivo = (opts?.verbose ?? false) && totalArquivos <= LIMIAR_DETALHE_TOTAL;
    const permitirArquivoXY = (opts?.verbose ?? false) && totalArquivos <= LIMIAR_DETALHE_LIMITADO_MAX;
    messages.logAnalistas.iniciarBatch(totalArquivos);
    const resultadosOtimizados = new Map();
    for (const entry of fileEntriesComAst) {
        arquivoAtual++;
        resultadosOtimizados.clear();
        if (opts?.compact) {
            if (arquivoAtual === totalArquivos) {
                messages.log.info(messages.ExecutorExtraMensagens.arquivosAnalisados.replace('{total}', String(totalArquivos)));
            }
        }
        else if (opts?.verbose) {
            if (permitirArquivoXY) {
                if (arquivoAtual === 1 || arquivoAtual % stepVerbose === 0 || arquivoAtual === totalArquivos) {
                    const seta = frames[arquivoAtual % frames.length];
                    messages.log.info(messages.ExecutorExtraMensagens.arquivoAtual.replace('{seta}', seta).replace('{atual}', String(arquivoAtual)).replace('{total}', String(totalArquivos)).replace('{arquivo}', entry.relPath));
                }
            }
            else {
                if (arquivoAtual === 1 || arquivoAtual % stepVerbose === 0 || arquivoAtual === totalArquivos) {
                    messages.log.info(messages.ExecutorExtraMensagens.progresso.replace('{atual}', String(arquivoAtual)).replace('{total}', String(totalArquivos)));
                }
            }
        }
        else if (arquivoAtual % 50 === 0 || arquivoAtual === totalArquivos) {
            if (arquivoAtual === totalArquivos) {
                infoDestaque(messages.ExecutorExtraMensagens.progresso.replace('{atual}', String(arquivoAtual)).replace('{total}', String(totalArquivos)));
            }
        }
        const ext = path.extname(entry.relPath);
        const astDoCache = await scopeAstCache.getOrParse(entry, ext);
        const astParaUso = astDoCache ?? entry.ast;
        const conteudo = entry.content ?? '';
        const hash = hashConteudo(conteudo);
        const cacheAnterior = estadoIncremental?.arquivos[entry.relPath];
        let reaproveitou = false;
        if (config.ANALISE_INCREMENTAL_ENABLED && cacheAnterior) {
            if (cacheAnterior.hash === hash) {
                reaproveitou = true;
            }
            else if (cacheAnterior.hashEstrutural) {
                const hashEstrutural = calcularHashEstrutural(astParaUso, conteudo);
                if (hashEstrutural && hashEstrutural === cacheAnterior.hashEstrutural) {
                    reaproveitou = true;
                    if (config.LOG_ESTRUTURADO) {
                        messages.log.info(JSON.stringify({ tipo: 'structural-reuse', arquivo: entry.relPath }));
                    }
                }
            }
            if (reaproveitou) {
                pushOcorrencia(...cacheAnterior.ocorrencias);
                novoEstado.arquivos[entry.relPath] = { ...cacheAnterior, hash, hashEstrutural: cacheAnterior.hashEstrutural };
                novoEstado.arquivos[entry.relPath].reaproveitadoCount = (cacheAnterior.reaproveitadoCount || 0) + 1;
                if (novoEstado.estatisticas) {
                    novoEstado.estatisticas.totalReaproveitamentos = (novoEstado.estatisticas.totalReaproveitamentos || 0) + 1;
                }
                if (detalharPorArquivo)
                    messages.logCore.reaproveitadoIncremental(entry.relPath);
                if (config.LOG_ESTRUTURADO) {
                    messages.log.info(JSON.stringify({
                        tipo: 'incremental-reuse',
                        arquivo: entry.relPath,
                        ocorrencias: cacheAnterior.ocorrencias.length
                    }));
                }
            }
        }
        if (reaproveitou)
            continue;
        if (config.ANALISE_CACHE_ENABLED) {
            const cached = await analysisCache.get(entry.relPath, entry.content ?? '');
            if (cached !== null) {
                pushOcorrencia(...cached);
                novoEstado.arquivos[entry.relPath] = {
                    hash,
                    ocorrencias: cached,
                    reaproveitadoCount: 1
                };
                if (detalharPorArquivo)
                    messages.logCore.reaproveitadoIncremental(`${entry.relPath} (analysis cache)`);
                if (config.LOG_ESTRUTURADO) {
                    messages.log.info(JSON.stringify({ tipo: 'cache-reuse', arquivo: entry.relPath, ocorrencias: cached.length }));
                }
                continue;
            }
        }
        if (Object.keys(visitorCombinado).length > 0 && astParaUso) {
            try {
                const astParam = hasNodeType(astParaUso) ? astParaUso.node : astParaUso;
                traverse(astParam, visitorCombinado, undefined, {
                    relPath: entry.relPath,
                    contexto: contextoGlobal,
                    resultados: resultadosOtimizados
                });
            }
            catch (err) {
                messages.log.aviso(`[CLOCK] Falha na travessia otimizada para ${entry.relPath}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        for (const tecnica of tecnicas) {
            if (tecnica.global)
                continue;
            if (tecnica.test && !tecnica.test(entry.relPath))
                continue;
            const timeoutMs = config.ANALISE_TIMEOUT_POR_ANALISTA_MS;
            try {
                const inicioAnalista = performance.now();
                const tamanhoArquivo = entry.content ? Math.round(entry.content.length / 1024) : 0;
                messages.logAnalistas.iniciandoAnalista(tecnica.nome || 'analista-desconhecido', entry.relPath, tamanhoArquivo);
                let resultado;
                const astNode = hasNodeType(astParaUso) ? astParaUso.node : null;
                const temVisitor = 'visitor' in tecnica;
                const astParam = temVisitor ? {
                    node: astNode,
                    preColetado: resultadosOtimizados.get(tecnica.nome || '')
                } : null;
                const execPromise = tecnica.aplicar(entry.content ?? '', entry.relPath, astParam, entry.fullCaminho, contextoGlobal);
                if (timeoutMs > 0) {
                    resultado = await withTimeout(execPromise, timeoutMs, messages.ExecutorExtraMensagens.timeoutAnalista.replace('{nome}', tecnica.nome || 'desconhecido').replace('{timeout}', String(timeoutMs)).replace('{arquivo}', entry.relPath));
                }
                else {
                    resultado = await execPromise;
                }
                if (Array.isArray(resultado)) {
                    pushOcorrencia(...resultado);
                }
                const duracaoMs = performance.now() - inicioAnalista;
                if (config.ANALISE_METRICAS_ENABLED) {
                    metricasAnalistas.push({
                        nome: tecnica.nome || 'desconhecido',
                        duracaoMs,
                        ocorrencias: Array.isArray(resultado) ? resultado.length : resultado ? 1 : 0,
                        global: false
                    });
                }
                const ocorrenciasContagem = Array.isArray(resultado) ? resultado.length : resultado ? 1 : 0;
                messages.logAnalistas.concluido(tecnica.nome || 'analista-desconhecido', entry.relPath, ocorrenciasContagem, duracaoMs);
                if (detalharPorArquivo) {
                    messages.log.info(`[FILE] '${tecnica.nome}' analisou ${entry.relPath} em ${formatMs(duracaoMs)}`);
                }
                if (config.LOG_ESTRUTURADO) {
                    messages.log.info(JSON.stringify({
                        tipo: 'analista',
                        arquivo: entry.relPath,
                        nome: tecnica.nome,
                        duracaoMs,
                        ocorrencias: metricasAnalistas.at(-1)?.ocorrencias
                    }));
                }
            }
            catch (error) {
                const err = error instanceof Error ? error : new Error(String(error));
                const isTempoLimite = err.message.includes('Timeout: analista');
                const nivelLog = isTempoLimite ? 'aviso' : 'erro';
                const prefixo = isTempoLimite ? '[CLOCK]' : '[ERRO]';
                if (isTempoLimite) {
                    const duracaoEstimada = config.ANALISE_TIMEOUT_POR_ANALISTA_MS || 30000;
                    messages.logAnalistas.timeout(tecnica.nome || 'analista-desconhecido', duracaoEstimada);
                }
                else {
                    messages.logAnalistas.erro(tecnica.nome || 'analista-desconhecido', err.message);
                }
                if (nivelLog === 'aviso') {
                    messages.log.aviso(`${prefixo} ${err.message}`);
                }
                else {
                    messages.log.erro(`${prefixo} Erro em '${tecnica.nome}' para ${entry.relPath}: ${err.message}`);
                    if (err.stack) {
                        if (opts?.verbose || config.DEV_MODE) {
                            messages.log.info(messages.ExecutorExtraMensagens.stackTrace);
                            messages.log.info(err.stack);
                        }
                    }
                }
                pushOcorrencia(ocorrenciaErroAnalista({
                    mensagem: isTempoLimite ? `Timeout na técnica '${tecnica.nome}' para ${entry.relPath}: ${timeoutMs}ms excedido` : `Falha na técnica '${tecnica.nome}' para ${entry.relPath}: ${err.message}`,
                    relPath: entry.relPath,
                    origem: tecnica.nome,
                    stack: !isTempoLimite && err.stack ? err.stack : undefined
                }));
            }
        }
        if (config.ANALISE_CACHE_ENABLED) {
            const ocorrArquivoCache = ocorrencias.filter(o => o.relPath === entry.relPath);
            await analysisCache.set(entry.relPath, entry.content ?? '', ocorrArquivoCache);
        }
        if (config.ANALISE_INCREMENTAL_ENABLED) {
            const ocorrArq = ocorrencias.filter(ocorrencia => ocorrencia.relPath === entry.relPath);
            const analistasArquivo = {};
            for (const metrica of metricasAnalistas.filter(metrica => !metrica.global)) {
                analistasArquivo[metrica.nome] = {
                    ocorrencias: metrica.ocorrencias,
                    duracaoMs: metrica.duracaoMs
                };
            }
            const hashEstrutural = calcularHashEstrutural(astParaUso, conteudo);
            novoEstado.arquivos[entry.relPath] = {
                hash,
                hashEstrutural: hashEstrutural || undefined,
                ocorrencias: ocorrArq,
                analistas: analistasArquivo,
                ultimaExecucaoMs: Date.now(),
                reaproveitadoCount: 0
            };
            if (novoEstado.estatisticas) {
                novoEstado.estatisticas.totalArquivosProcessados = (novoEstado.estatisticas.totalArquivosProcessados || 0) + 1;
            }
        }
        messages.logAnalistas.arquivoProcessado();
        if (emitter) {
            const ocorrArquivo = ocorrencias.filter(ocorrencia => ocorrencia.relPath === entry.relPath);
            emitter.emit('file:processed', {
                relPath: entry.relPath,
                ocorrencias: ocorrArquivo.length
            });
        }
    }
    const fimExecucao = performance.now();
    const duracaoMs = Math.round(fimExecucao - inicioExecucao);
    messages.logAnalistas.finalizarBatch(ocorrencias.length, duracaoMs);
    let metricasExecucao = null;
    if (config.ANALISE_METRICAS_ENABLED) {
        const metricasGlobais = getGlobalExec().__TUTANUS_METRICAS__ || { parsingTimeMs: 0, cacheHits: 0, cacheMiss: 0 };
        const astCacheMetrics = scopeAstCache.getMetrics();
        metricasExecucao = {
            totalArquivos: fileEntriesComAst.length,
            tempoParsingMs: Math.round(metricasGlobais.parsingTimeMs),
            tempoAnaliseMs: duracaoMs,
            cacheAstHits: metricasGlobais.cacheHits,
            cacheAstMiss: metricasGlobais.cacheMiss,
            analistas: metricasAnalistas,
            astCache: {
                hits: astCacheMetrics.hits,
                misses: astCacheMetrics.misses,
                parseTimeMs: Math.round(astCacheMetrics.parseTimeMs),
                cachedFiles: astCacheMetrics.cachedFiles,
            },
        };
        if (config.LOG_ESTRUTURADO) {
            messages.log.info(JSON.stringify({ tipo: 'metricas', ...metricasExecucao }));
        }
        try {
            await persistMetricHistory(metricasExecucao);
        }
        catch (erro) {
            messages.log.erro(`Falha ao persistir histórico de métricas: ${erro instanceof Error ? erro.message : String(erro)}`);
        }
    }
    if (config.ANALISE_CACHE_ENABLED) {
        await analysisCache.persistir().catch(() => { });
    }
    if (config.ANALISE_INCREMENTAL_ENABLED) {
        if (novoEstado.estatisticas) {
            novoEstado.estatisticas.ultimaDuracaoMs = duracaoMs;
        }
        await salvarEstado(config.ANALISE_INCREMENTAL_STATE_PATH, novoEstado);
        if (config.LOG_ESTRUTURADO) {
            messages.log.info(JSON.stringify({
                tipo: 'incremental-salvo',
                arquivos: Object.keys(novoEstado.arquivos).length,
                totalReaproveitamentos: novoEstado.estatisticas?.totalReaproveitamentos,
                processados: novoEstado.estatisticas?.totalArquivosProcessados
            }));
        }
    }
    const resultado = {
        totalArquivos: fileEntriesComAst.length,
        arquivosAnalisados: fileEntriesComAst.map(e => e.relPath),
        ocorrencias,
        timestamp: Date.now(),
        duracaoMs,
        metricas: metricasExecucao || undefined
    };
    if (emitter) {
        emitter.emit('analysis:complete', resultado);
    }
    return resultado;
}
export function registrarUltimasMetricas(metricas) {
    try {
        getGlobalExec().__ULTIMAS_METRICAS_TUTANUS__ = metricas || null;
    }
    catch {
    }
}
//# sourceMappingURL=executor.js.map