"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WhatsAppConnectionManager = void 0;
/**
 * WhatsAppConnectionManager — Orquestra conexões Baileys para todas as instâncias.
 *
 * Esta é a classe central do módulo WhatsApp. Ela gerencia:
 *   1. Ciclo de vida das conexões (connect, disconnect, reconnect)
 *   2. Registro de TODOS os event listeners do Baileys
 *   3. Reconexão automática com backoff exponencial
 *   4. Watchdog para detectar conexões mortas silenciosamente
 *   5. Sincronização de chats, contatos e histórico
 *   6. Mapeamento LID → phone JID (via chats.phoneNumberShare)
 *   7. Emissão de eventos Socket.IO para o frontend
 *
 * ARQUITETURA:
 *   - Uma instância de WhatsAppConnectionManager por servidor (singleton prático)
 *   - Cada instância WhatsApp (número conectado) tem seu próprio WASocket
 *   - Os Maps internos indexam por instanceId para acesso O(1)
 *   - Multi-tenant: tenantByInstance mapeia instância → tenant para isolamento
 *
 * EVENTOS BAILEYS REGISTRADOS:
 *   - creds.update → salva credenciais (auth state)
 *   - connection.update → gerencia QR, conexão, desconexão, reconexão
 *   - messages.upsert → delega para MessageHandler (tempo real + histórico)
 *   - messages.update → atualiza status (sent→delivered→read)
 *   - contacts.upsert/update → delega para ContactHandler
 *   - chats.upsert/update → sincroniza lista de chats no banco
 *   - chats.phoneNumberShare → captura mapeamento LID → phone JID
 *   - groups.upsert/update → sincroniza grupos (subject, metadata)
 *   - messaging-history.set → history sync (contatos, chats, mensagens em lote)
 *
 * @see MessageHandler — processa mensagens individuais
 * @see ContactHandler — gerencia contatos e avatares
 */
const path_1 = __importDefault(require("path"));
const promises_1 = __importDefault(require("fs/promises"));
const promises_2 = __importDefault(require("dns/promises"));
const engine_1 = require("../engine");
const qrcode_1 = __importDefault(require("qrcode"));
const env_1 = require("../../../config/env");
const logger_1 = require("../../../config/logger");
const dragonfly_1 = require("../../../infra/cache/dragonfly");
const prisma_1 = require("../../../infra/database/prisma");
const client_1 = require("@prisma/client");
const MessageHandler_1 = require("../handlers/MessageHandler");
const ContactHandler_1 = require("../handlers/ContactHandler");
const chatbot_service_1 = require("../../chatbot/chatbot.service");
const hook_bus_1 = require("../../../core/hook-bus");
const chat_cache_1 = require("../../chats/chat.cache");
// ─── Chaves de cache no DragonflyDB (Redis-compatible) ──────────────────────
const CACHE_KEY_STATUS = (instanceId) => `instance:${instanceId}:status`;
const CACHE_KEY_QR = (instanceId) => `instance:${instanceId}:qr`;
// ─── Configuração de reconexão automática ───────────────────────────────────
// Backoff exponencial: 3s → 6s → 12s → 24s → 48s → ... → max 5min
// Nunca desiste, exceto loggedOut (usuário deslogou) ou forbidden (ban)
const RECONNECT_BASE_DELAY_MS = 3000; // delay inicial
const RECONNECT_MAX_DELAY_MS = 300000; // teto geral: 5 minutos
// Teto MENOR para erros transientes de rede (DNS, ECONN, ETIMEDOUT).
// Esses erros costumam resolver em segundos; um teto de 5min deixa a
// sessão "dormindo" num setTimeout longo enquanto a rede já voltou.
const RECONNECT_NETWORK_MAX_MS = 60000; // teto rede: 60s
const RECONNECT_MAX_ATTEMPTS = Infinity;
// ─── Network probe — acorda reconexões pendentes quando a rede volta ────────
// Enquanto uma instância está aguardando backoff, um DNS lookup leve é
// disparado em intervalo fixo. Se o lookup funciona E existe um retry
// agendado para daqui a mais de PROBE_ADVANCE_THRESHOLD_MS, cancela o timer
// e dispara retry imediato. Zero tráfego para o WhatsApp — só DNS.
const NETWORK_PROBE_INTERVAL_MS = 30000; // a cada 30s
const PROBE_ADVANCE_THRESHOLD_MS = 15000; // só adianta se faltar > 15s
const PROBE_DNS_HOST = 'web.whatsapp.com';
// ─── Watchdog: detecta conexão morta silenciosamente ────────────────────────
// Conservador por design: reconexões frequentes sinalizam "cliente instável"
// para o WhatsApp e aumentam risco de banimento do número. Por isso:
//   - Timeout alto (10 min): conversas calmas são normais, não disparam
//   - Intervalo baixo de verificação (3 min): menos overhead
//   - Ping leve antes de matar: tenta reviver sem derrubar o socket
//   - Cool-down entre kills: evita loop de reconexão se a rede oscila
const WATCHDOG_INTERVAL_MS = 180000; // verifica a cada 3 min
const WATCHDOG_TIMEOUT_MS = 600000; // 10 min sem atividade antes de agir
const WATCHDOG_PING_GRACE_MS = 30000; // grace padrão (official): 30s
const WATCHDOG_PING_GRACE_INFIN_MS = 60000; // grace InfiniteAPI: 60s (ACK mais lento)
const WATCHDOG_COOLDOWN_MS = 900000; // 15 min entre force-kills consecutivos
class WhatsAppConnectionManager {
    constructor(io) {
        // ─── Maps indexados por instanceId (UUID) ──────────────────────────────
        /** Socket Baileys ativo por instância */
        this.sockets = new Map();
        /** Handler de contatos por instância */
        this.contactHandlers = new Map();
        /** Handler de mensagens por instância */
        this.msgHandlers = new Map();
        /** Contador de tentativas de reconexão (reseta ao conectar com sucesso) */
        this.reconnectAttempts = new Map();
        /** Timer de reconexão pendente (clearTimeout para cancelar) */
        this.reconnectTimers = new Map();
        /**
         * Timestamp absoluto (ms) em que o retry pendente vai disparar.
         * Usado pelo network probe para decidir se vale adiantar um retry
         * que ainda está dormindo num backoff longo.
         */
        this.reconnectScheduledAt = new Map();
        /** Timer global do network probe (singleton) */
        this.networkProbeTimer = null;
        /** Timer do watchdog (clearInterval para parar) */
        this.watchdogTimers = new Map();
        /** Timestamp da última atividade (qualquer evento Baileys) */
        this.lastActivity = new Map();
        /** Timestamp do último force-kill do watchdog (cool-down anti-loop) */
        this.lastWatchdogKill = new Map();
        /** Timestamp do último ping do watchdog (aguarda resposta antes de matar) */
        this.lastWatchdogPing = new Map();
        /** Mapeamento instanceId → tenantId (necessário para reconexões) */
        this.tenantByInstance = new Map();
        /** Engine ativa por instância (usada pelo watchdog para ajustar tolerâncias) */
        this.engineByInstance = new Map();
        this.io = io;
        this.chatbotService = new chatbot_service_1.ChatbotService(io);
        this.startNetworkProbe();
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  CONEXÃO PRINCIPAL
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Conecta (ou reconecta) uma instância WhatsApp.
     *
     * Fluxo:
     *   1. Carrega auth state do disco (useMultiFileAuthState)
     *   2. Cria socket Baileys com configuração padrão
     *   3. Registra TODOS os event listeners
     *   4. Baileys inicia handshake com WhatsApp Web
     *   5. Se precisar de QR: emite evento 'qr' via Socket.IO
     *   6. Se já autenticado: connection 'open' dispara sync
     *
     * IMPORTANTE: Este método é idempotente — se a instância já está
     * conectada, retorna sem fazer nada (evita sockets duplicados).
     */
    async connect(instanceId, tenantId) {
        if (this.sockets.has(instanceId)) {
            // Socket já existe — re-emite o status atual para sincronizar o frontend
            // (acontece quando o backend reconectou em background antes do frontend abrir)
            const cachedStatus = await dragonfly_1.dragonfly.get(CACHE_KEY_STATUS(instanceId));
            const currentStatus = cachedStatus ?? client_1.InstanceStatus.CONNECTED;
            logger_1.logger.info({ instanceId, currentStatus }, 'Instância já conectada — re-emitindo status para o frontend');
            this.io.to(`tenant:${tenantId}`).emit('instance:status', {
                instanceId,
                status: currentStatus,
            });
            return;
        }
        // Limpa timers pendentes de reconexão anterior
        this.clearReconnectTimer(instanceId);
        this.tenantByInstance.set(instanceId, tenantId);
        // ── Auth state: credenciais persistidas em disco ──────────────────────
        // Cada instância tem seu diretório em BAILEYS_SESSIONS_PATH/{instanceId}/
        // Contém: creds.json, app-state-sync-key-*, pre-key-*, etc.
        const sessionDir = path_1.default.resolve(env_1.env.BAILEYS_SESSIONS_PATH, instanceId);
        try {
            await promises_1.default.mkdir(sessionDir, { recursive: true });
        }
        catch (err) {
            logger_1.logger.error({ err, instanceId, sessionDir }, 'Falha ao criar diretório de sessão');
            await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED);
            this.io.to(`tenant:${tenantId}`).emit('instance:status', { instanceId, status: client_1.InstanceStatus.DISCONNECTED });
            return;
        }
        // ── Seleciona engine por instância ───────────────────────────────────
        // Lê o campo engine da instância no banco (padrão: "infinite").
        // Permite que cada instância use uma engine diferente sem reiniciar o container.
        const instanceRecord = await prisma_1.prisma.instance.findFirst({
            where: { id: instanceId },
            select: { engine: true },
        });
        const engineType = instanceRecord?.engine ?? 'infinite';
        const { makeWASocket, useMultiFileAuthState, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, } = (0, engine_1.getEngine)(engineType);
        this.engineByInstance.set(instanceId, engineType);
        logger_1.logger.info({ instanceId, engine: engineType }, 'Engine selecionada para instância');
        const { state, saveCreds } = await useMultiFileAuthState(sessionDir);
        // fetchLatestBaileysVersion faz requisição HTTP ao WhatsApp — pode falhar
        // se não houver internet. Usa versão embutida como fallback seguro.
        let version = [2, 3000, 1023504162];
        try {
            const fetched = await fetchLatestBaileysVersion();
            version = fetched.version;
        }
        catch (err) {
            logger_1.logger.warn({ instanceId }, 'fetchLatestBaileysVersion falhou — usando versão embutida como fallback');
        }
        logger_1.logger.info({ instanceId, version }, 'Iniciando socket Baileys');
        // ── Cria socket Baileys ──────────────────────────────────────────────
        // DEBUG: logger trace só para capturar XML de mensagens enviadas
        const debugLogger = {
            level: 'trace',
            child: () => debugLogger,
            trace: (obj) => {
                if (obj?.xml && typeof obj.xml === 'string' && obj.xml.startsWith('<message')) {
                    console.log('[NEWWHATS/STANZA-XML]\n' + obj.xml);
                }
            },
            debug: () => { }, info: () => { }, warn: () => { }, error: () => { }, fatal: () => { },
        };
        const sock = makeWASocket({
            version,
            auth: {
                creds: state.creds,
                // Cache de chaves Signal para performance (evita leituras de disco)
                keys: makeCacheableSignalKeyStore(state.keys, logger_1.logger),
            },
            printQRInTerminal: false,
            qrTimeout: 60000,
            logger: debugLogger,
            browser: ['NewWhats', 'Chrome', '124.0.0'],
            syncFullHistory: true, // Solicita histórico completo ao telefone
            markOnlineOnConnect: true, // Marca como "online" ao conectar
            // getMessage: callback para reenvio de mensagens (retry)
            // O Baileys chama quando precisa reenviar uma mensagem que falhou
            getMessage: async (key) => {
                if (!key.id)
                    return undefined;
                const msg = await prisma_1.prisma.message.findUnique({
                    where: { instanceId_messageId: { instanceId, messageId: key.id } },
                });
                if (!msg?.body)
                    return undefined;
                return { conversation: msg.body };
            },
        });
        this.sockets.set(instanceId, sock);
        this.touchActivity(instanceId);
        sock.ev.on('creds.update', saveCreds);
        sock.ev.on('connection.update', (update) => {
            this.touchActivity(instanceId);
            this.baileysLog(tenantId, instanceId, 'connection.update', {
                connection: update.connection,
                qr: update.qr ? '(qr gerado)' : undefined,
                lastDisconnect: update.lastDisconnect
                    ? { statusCode: update.lastDisconnect.error?.output?.statusCode }
                    : undefined,
            });
            this.handleConnectionUpdate(instanceId, tenantId, update, sock);
        });
        // ─── Mensagens recebidas (tempo real + histórico) ────────────────────
        // type 'notify': mensagem em tempo real → processMessage completo
        // type 'append': histórico sync → handleHistory (bulk, sem socket)
        const msgHandler = new MessageHandler_1.MessageHandler(sock, instanceId, tenantId, this.io, this.chatbotService);
        this.msgHandlers.set(instanceId, msgHandler);
        sock.ws.on('CB:message', (node) => {
            const msgId = node?.attrs?.id;
            const peerRecipientPn = node?.attrs?.peer_recipient_pn;
            if (msgId && peerRecipientPn) {
                msgHandler.setPeerRecipientHint(msgId, peerRecipientPn);
            }
        });
        sock.ev.on('messages.upsert', (m) => {
            this.touchActivity(instanceId);
            // Log de debug para monitoramento via página /baileys
            for (const msg of m.messages ?? []) {
                const text = msg.message?.conversation ??
                    msg.message?.extendedTextMessage?.text ??
                    msg.message?.imageMessage?.caption ??
                    msg.message?.videoMessage?.caption ??
                    null;
                this.baileysLog(tenantId, instanceId, 'message', {
                    fromMe: msg.key?.fromMe,
                    remoteJid: msg.key?.remoteJid,
                    body: text,
                    type: Object.keys(msg.message ?? {}).find(k => k !== 'messageContextInfo') ?? 'unknown',
                    status: msg.status,
                });
                // Log de monitoramento para mensagens LID
                // Ajuda a verificar se key.senderPn está presente para resolução
                if (msg.key?.remoteJid?.endsWith('@lid')) {
                    logger_1.logger.debug({
                        instanceId,
                        remoteJid: msg.key.remoteJid,
                        senderPn: msg.key.senderPn,
                        pushName: msg.pushName,
                    }, '[LID] Mensagem @lid recebida — será resolvida pelo MessageHandler');
                }
            }
            // A persistência do mapeamento LID→phone vive dentro do
            // MessageHandler.processMessage (no upsert do Contact), porque
            // updateMany aqui afetava 0 rows em contatos inexistentes — bug
            // silencioso que descartava o mapeamento na primeira mensagem LID.
            // Delega processamento completo para o MessageHandler
            msgHandler.handle(m);
        });
        sock.ev.on('messages.update', (updates) => {
            this.touchActivity(instanceId);
            for (const { key, update } of updates) {
                if (!key.id || !update.status)
                    continue;
                const statusMap = {
                    0: 'PENDING', 1: 'PENDING', 2: 'SENT', 3: 'DELIVERED', 4: 'READ', 5: 'READ',
                };
                const status = statusMap[update.status];
                if (!status)
                    continue;
                // Atualiza no banco e emite para o frontend (ticks azuis/cinza)
                prisma_1.prisma.message.updateMany({
                    where: { instanceId, messageId: key.id },
                    data: { status: status },
                }).then((r) => {
                    if (r.count === 0)
                        return;
                    this.io.to(`tenant:${tenantId}`).emit('message:status', {
                        messageId: key.id,
                        status,
                    });
                    hook_bus_1.hookBus.emit('ext:message.update', {
                        instanceId,
                        tenantId,
                        messageId: key.id,
                        status,
                        timestamp: Date.now(),
                    }).catch(() => { });
                }).catch(() => { });
            }
        });
        // ─── Contatos ────────────────────────────────────────────────────────
        // contacts.upsert: lista completa de contatos (conexão inicial/sync)
        // contacts.update: atualizações parciais (nome alterado, etc)
        const contactHandler = new ContactHandler_1.ContactHandler(instanceId, tenantId, sock, this.io);
        this.contactHandlers.set(instanceId, contactHandler);
        sock.ev.on('contacts.upsert', (contacts) => {
            this.touchActivity(instanceId);
            this.baileysLog(tenantId, instanceId, 'contacts.upsert', { count: contacts.length });
            logger_1.logger.info({ instanceId, count: contacts.length }, '[contacts.upsert] Recebido');
            contactHandler.upsert(contacts);
        });
        sock.ev.on('contacts.update', (contacts) => {
            this.touchActivity(instanceId);
            this.baileysLog(tenantId, instanceId, 'contacts.update', { count: contacts.length });
            contactHandler.update(contacts);
        });
        sock.ev.on('chats.phoneNumberShare', ({ lid, jid: phoneJid }) => {
            this.touchActivity(instanceId);
            logger_1.logger.info({ instanceId, lid, phoneJid }, '[LID] phoneNumberShare recebido');
            if (lid && phoneJid) {
                // Salva o mapeamento LID no contato que já existe com o phone JID
                prisma_1.prisma.contact.updateMany({
                    where: { tenantId, instanceId, jid: phoneJid },
                    data: { lidJid: lid },
                }).catch(() => { });
                // Tenta corrigir contatos que foram criados com JID = LID
                // (pode acontecer se o contato chegou antes do phoneNumberShare)
                prisma_1.prisma.contact.updateMany({
                    where: { tenantId, instanceId, jid: lid },
                    data: { jid: phoneJid, phone: phoneJid.split('@')[0], lidJid: lid },
                }).catch(() => { });
            }
        });
        sock.ev.on('chats.upsert', (chats) => {
            this.touchActivity(instanceId);
            this.baileysLog(tenantId, instanceId, 'chats.upsert', { count: chats.length });
            this.syncChats(instanceId, tenantId, chats).catch(() => { });
        });
        sock.ev.on('chats.update', (chats) => {
            this.touchActivity(instanceId);
            this.baileysLog(tenantId, instanceId, 'chats.update', { count: chats.length });
            this.handleChatsUpdate(instanceId, tenantId, chats).catch(() => { });
        });
        sock.ev.on('groups.upsert', (groups) => {
            this.touchActivity(instanceId);
            this.baileysLog(tenantId, instanceId, 'groups.upsert', { count: groups.length });
            const chatsFromGroups = groups.map((g) => ({
                id: g.id,
                name: g.subject,
                conversationTimestamp: g.creation,
                unreadCount: 0,
                pinned: false,
                archived: false,
            }));
            this.syncChats(instanceId, tenantId, chatsFromGroups).catch(() => { });
        });
        sock.ev.on('groups.update', (updates) => {
            this.touchActivity(instanceId);
            this.baileysLog(tenantId, instanceId, 'groups.update', { count: updates.length });
            const chatsFromUpdates = updates
                .filter((g) => g.id && g.subject)
                .map((g) => ({ id: g.id, name: g.subject }));
            this.syncChats(instanceId, tenantId, chatsFromUpdates).catch(() => { });
        });
        sock.ev.on('messaging-history.set', async ({ chats: histChats, contacts: histContacts, messages: histMessages, isLatest }) => {
            this.touchActivity(instanceId);
            logger_1.logger.info({
                instanceId,
                chats: histChats.length,
                contacts: histContacts.length,
                messages: histMessages.length,
                isLatest,
            }, '[History] messaging-history.set recebido');
            this.baileysLog(tenantId, instanceId, 'messaging-history.set', {
                chats: histChats.length,
                contacts: histContacts.length,
                messages: histMessages.length,
                isLatest,
            });
            // Processa na ordem: contatos → chats → mensagens
            // (contatos primeiro para que o vínculo Chat→Contact funcione)
            if (histContacts.length > 0) {
                await contactHandler.upsert(histContacts).catch(() => { });
            }
            if (histChats.length > 0) {
                await this.syncChats(instanceId, tenantId, histChats).catch(() => { });
            }
            if (histMessages.length > 0) {
                await msgHandler.handleHistory(histMessages).catch(() => { });
            }
            // Quando isLatest=true, o sync do histórico terminou completamente.
            // Inicia reconciliação: busca contatos sem nome e preenche via pushName
            // das mensagens recebidas (o history sync não traz pushName nos contatos).
            if (isLatest) {
                logger_1.logger.info({ instanceId }, '[History] Sync completo (isLatest=true) — iniciando reconciliação de nomes');
                contactHandler.reconcileNamesFromMessages().catch(() => { });
            }
        });
        // ── Guard: emite DISCONNECTED se o Baileys lançar erro síncrono ─────────
        // makeWASocket pode lançar em casos raros (OpenSSL, auth corrompida, etc.)
        // Não há como capturar erros async do Baileys aqui além dos event listeners,
        // mas garantimos que o socket foi registrado antes de prosseguir.
        if (!this.sockets.has(instanceId)) {
            logger_1.logger.error({ instanceId }, 'Socket Baileys não foi registrado após inicialização — marcando como DISCONNECTED');
            await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED);
            this.io.to(`tenant:${tenantId}`).emit('instance:status', { instanceId, status: client_1.InstanceStatus.DISCONNECTED });
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  HANDLER DE ATUALIZAÇÃO DE CONEXÃO
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Processa eventos de mudança de estado da conexão Baileys.
     *
     * Estados possíveis:
     *   - qr: QR Code gerado → emite para o frontend escanear
     *   - open: conectado com sucesso → inicia sync, watchdog, etc.
     *   - close: desconectado → decide se reconecta ou para
     *   - connecting: tentando conectar → atualiza status
     *
     * DECISÃO DE RECONEXÃO:
     *   - loggedOut (401) → sessão expirada, limpa arquivos, exige novo QR
     *   - forbidden (403) → conta banida, para completamente
     *   - Qualquer outro código → reconecta com backoff exponencial
     */
    async handleConnectionUpdate(instanceId, tenantId, update, sock) {
        const { connection, lastDisconnect, qr } = update;
        // ── QR Code emitido ───────────────────────────────────────────────────
        if (qr) {
            logger_1.logger.info({ instanceId }, 'QR Code gerado — aguardando escaneamento');
            const qrBase64 = await qrcode_1.default.toDataURL(qr);
            // Salva no cache com TTL de 60s (QR expira rapidamente)
            await dragonfly_1.dragonfly.set(CACHE_KEY_QR(instanceId), qrBase64, 60);
            await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.QR_PENDING);
            // Emite para a sala da instância (página de conexão) E para o tenant (dashboard)
            this.io.to(`instance:${instanceId}`).emit('qr', { instanceId, qrBase64 });
            this.io.to(`tenant:${tenantId}`).emit('instance:status', {
                instanceId,
                status: client_1.InstanceStatus.QR_PENDING,
                qrBase64,
            });
            // ext: bridge — satélites externos recebem via WS /api/ext/v1/stream
            hook_bus_1.hookBus.emit('ext:session.qr', { instanceId, tenantId, qrBase64, timestamp: Date.now() }).catch(() => { });
        }
        // ── Conectado com sucesso ─────────────────────────────────────────────
        if (connection === 'open') {
            logger_1.logger.info({ instanceId }, 'WhatsApp conectado com sucesso');
            // Reseta contadores de reconexão
            this.reconnectAttempts.set(instanceId, 0);
            this.clearReconnectTimer(instanceId);
            // Inicia watchdog para detectar conexão morta
            this.startWatchdog(instanceId);
            await dragonfly_1.dragonfly.del(CACHE_KEY_QR(instanceId));
            await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTED);
            // Salva o número de telefone da sessão no banco
            // (sock.user.id vem no formato "5511999999999:1@s.whatsapp.net")
            const phoneJid = sock.user?.id;
            if (phoneJid) {
                const phone = phoneJid.split(':')[0].split('@')[0];
                await prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { phone } })
                    .catch(() => { });
            }
            // Pós-conexão: sincroniza avatar da instância e busca avatares ausentes
            const handler = this.contactHandlers.get(instanceId);
            if (handler) {
                // Avatar da própria instância (foto de perfil do número)
                handler.syncInstanceAvatar(instanceId)
                    .then((avatar) => {
                    if (avatar) {
                        this.io.to(`tenant:${tenantId}`).emit('instance:avatar', { instanceId, avatar });
                    }
                })
                    .catch(() => { });
                // Busca avatares ausentes após 5s (dá tempo para o sync inicial)
                setTimeout(() => handler.syncMissingAvatars().catch(() => { }), 5000);
                // Renova avatares próximos de expirar após 30s (não compete com syncMissing)
                setTimeout(() => handler.syncExpiredAvatars().catch(() => { }), 30000);
            }
            // Sync incremental: verifica gap de mensagens durante downtime
            this.syncMissedMessages(instanceId, tenantId).catch(() => { });
            this.io.to(`tenant:${tenantId}`).emit('instance:status', {
                instanceId,
                status: client_1.InstanceStatus.CONNECTED,
            });
            hook_bus_1.hookBus.emit('ext:session.status', { instanceId, tenantId, status: 'connected', timestamp: Date.now() }).catch(() => { });
        }
        // ── Desconectado ──────────────────────────────────────────────────────
        if (connection === 'close') {
            const rawErr = lastDisconnect?.error;
            const error = rawErr?.output;
            const statusCode = error?.statusCode;
            const isNetworkErr = this.isTransientNetworkError(rawErr);
            logger_1.logger.warn({ instanceId, statusCode, isNetworkErr }, 'Conexao encerrada');
            // Limpa referências da instância desconectada
            this.sockets.delete(instanceId);
            this.contactHandlers.delete(instanceId);
            this.msgHandlers.delete(instanceId);
            this.engineByInstance.delete(instanceId);
            this.stopWatchdog(instanceId);
            // Decide se deve reconectar baseado no código de erro
            const shouldReconnect = statusCode !== engine_1.DisconnectReason.loggedOut &&
                statusCode !== engine_1.DisconnectReason.forbidden;
            if (shouldReconnect) {
                const attempts = (this.reconnectAttempts.get(instanceId) ?? 0) + 1;
                this.reconnectAttempts.set(instanceId, attempts);
                // Backoff exponencial: 3s, 6s, 12s, 24s, 48s…
                // Teto menor (60s) para erros de rede transientes: DNS/ECONN se
                // resolvem em segundos, não faz sentido dormir 5min. Teto geral
                // (5min) fica para erros de servidor/protocolo do WhatsApp, onde
                // paciência é melhor do que pressa.
                const maxDelay = isNetworkErr ? RECONNECT_NETWORK_MAX_MS : RECONNECT_MAX_DELAY_MS;
                const delay = Math.min(RECONNECT_BASE_DELAY_MS * Math.pow(2, attempts - 1), maxDelay);
                const scheduledAt = Date.now() + delay;
                const nextAt = new Date(scheduledAt).toISOString();
                logger_1.logger.info({ instanceId, attempts, delayMs: delay, nextAt, isNetworkErr }, '[Reconexao] Agendando reconexao com backoff exponencial');
                await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTING);
                this.io.to(`tenant:${tenantId}`).emit('instance:status', {
                    instanceId,
                    status: client_1.InstanceStatus.CONNECTING,
                    reconnectIn: delay,
                    attempt: attempts,
                });
                this.scheduleReconnect(instanceId, tenantId, delay);
            }
            else {
                // loggedOut → limpa sessão e exige novo QR
                if (statusCode === engine_1.DisconnectReason.loggedOut) {
                    logger_1.logger.warn({ instanceId }, 'Sessao desconectada pelo usuario — limpando sessao');
                    await this.deleteSessionFiles(instanceId);
                }
                // forbidden → conta banida, apenas atualiza status
                await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED);
                this.io.to(`tenant:${tenantId}`).emit('instance:status', {
                    instanceId,
                    status: client_1.InstanceStatus.DISCONNECTED,
                    reason: engine_1.DisconnectReason[statusCode] ?? statusCode,
                });
                hook_bus_1.hookBus.emit('ext:session.status', { instanceId, tenantId, status: 'disconnected', timestamp: Date.now() }).catch(() => { });
            }
        }
        // ── Conectando ────────────────────────────────────────────────────────
        if (connection === 'connecting') {
            await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTING);
            this.io.to(`tenant:${tenantId}`).emit('instance:status', {
                instanceId,
                status: client_1.InstanceStatus.CONNECTING,
            });
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  SYNC INCREMENTAL — Mensagens perdidas durante downtime
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Verifica e notifica sobre gap de mensagens durante downtime.
     *
     * Consulta a última mensagem no banco e calcula quanto tempo o servidor
     * ficou offline. O Baileys com syncFullHistory=true já solicita as
     * mensagens pendentes automaticamente — este método apenas:
     *   1. Loga o gap para visibilidade (monitoramento)
     *   2. Emite 'instance:syncing' para o frontend mostrar indicador de sync
     */
    async syncMissedMessages(instanceId, tenantId) {
        try {
            const lastMsg = await prisma_1.prisma.message.findFirst({
                where: { instanceId },
                orderBy: { timestamp: 'desc' },
                select: { timestamp: true, messageId: true },
            });
            if (!lastMsg) {
                logger_1.logger.info({ instanceId }, '[Sync] Nenhuma mensagem no DB — history sync completo via Baileys');
                return;
            }
            const gap = Date.now() - lastMsg.timestamp.getTime();
            const gapMinutes = Math.round(gap / 60000);
            logger_1.logger.info({ instanceId, lastMsgAt: lastMsg.timestamp, gapMinutes }, '[Sync] Reconectado — gap de mensagens detectado');
            this.baileysLog(tenantId, instanceId, 'sync:gap', {
                lastMsgAt: lastMsg.timestamp.toISOString(),
                gapMinutes,
            });
            // Notifica o frontend que está sincronizando
            this.io.to(`tenant:${tenantId}`).emit('instance:syncing', {
                instanceId,
                gapMinutes,
                since: lastMsg.timestamp.toISOString(),
            });
        }
        catch (err) {
            logger_1.logger.error({ err, instanceId }, '[Sync] Erro ao verificar gap de mensagens');
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  WATCHDOG — Detecta conexão morta silenciosamente
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Registra timestamp da última atividade para o watchdog.
     * Chamado por TODOS os event listeners do Baileys.
     */
    touchActivity(instanceId) {
        this.lastActivity.set(instanceId, Date.now());
    }
    /**
     * Inicia o watchdog para uma instância.
     *
     * Fluxo conservador em 3 etapas — escolhido pra minimizar reconexões:
     *   1. Se inativo > TIMEOUT (10 min): envia um presence update como
     *      ping leve e marca timestamp. Se o socket estiver vivo, Baileys
     *      responde e touchActivity é chamado no próximo evento.
     *   2. Se continua inativo GRACE (+30s) após o ping: confirma que a
     *      conexão está de fato morta. Procede pro kill.
     *   3. Antes de matar: verifica cool-down (15 min desde último kill)
     *      pra não entrar em loop quando a rede está oscilando. Se estiver
     *      no cool-down, só loga e espera o próximo ciclo.
     *
     * Esta sequência dá ~10-11 min de folga antes do primeiro kill, e no
     * mínimo 15 min entre kills consecutivos — 7-8× mais conservador que
     * a versão anterior (2 min sem verificação de estado).
     */
    startWatchdog(instanceId) {
        this.stopWatchdog(instanceId);
        const timer = setInterval(async () => {
            const now = Date.now();
            const lastTs = this.lastActivity.get(instanceId) ?? 0;
            const elapsed = now - lastTs;
            const tenantId = this.tenantByInstance.get(instanceId);
            if (elapsed <= WATCHDOG_TIMEOUT_MS) {
                // Conexão com atividade recente — reseta estado de ping/cool-down
                this.lastWatchdogPing.delete(instanceId);
                return;
            }
            const sock = this.sockets.get(instanceId);
            // Caso 1: socket sumiu sem connection.close — reconecta imediatamente
            if (!sock) {
                if (tenantId) {
                    logger_1.logger.warn({ instanceId }, '[Watchdog] Socket ausente — reagendando conexao');
                    this.stopWatchdog(instanceId);
                    this.connect(instanceId, tenantId).catch(() => { });
                }
                return;
            }
            const pingedAt = this.lastWatchdogPing.get(instanceId) ?? 0;
            const sincePing = now - pingedAt;
            // Etapa 1: envia ping leve (presence update) se ainda não pingou
            // neste ciclo de inatividade. Não conta como atividade — só estímulo.
            if (pingedAt === 0 || sincePing > WATCHDOG_TIMEOUT_MS) {
                try {
                    await sock.sendPresenceUpdate('available');
                    this.lastWatchdogPing.set(instanceId, now);
                    logger_1.logger.debug({ instanceId, elapsedMs: elapsed }, '[Watchdog] Ping leve enviado — aguardando resposta');
                }
                catch (err) {
                    // Ping falhou → socket já está morto, pula pra etapa de kill
                    logger_1.logger.warn({ instanceId, err }, '[Watchdog] Ping falhou — socket morto');
                    this.forceKillSocket(instanceId, sock, tenantId, elapsed);
                }
                return;
            }
            // Etapa 2: ping já foi enviado, mas ainda sem atividade após GRACE.
            // InfiniteAPI pode ter ACK mais lento — usa tolerância maior.
            const engine = this.engineByInstance.get(instanceId) ?? 'infinite';
            const graceMs = engine === 'infinite' ? WATCHDOG_PING_GRACE_INFIN_MS : WATCHDOG_PING_GRACE_MS;
            if (sincePing > graceMs) {
                this.forceKillSocket(instanceId, sock, tenantId, elapsed);
            }
        }, WATCHDOG_INTERVAL_MS);
        this.watchdogTimers.set(instanceId, timer);
    }
    /**
     * Força o fechamento do socket, respeitando cool-down para evitar loop.
     * Só mata se não houve outro kill nos últimos WATCHDOG_COOLDOWN_MS.
     */
    forceKillSocket(instanceId, sock, tenantId, elapsedMs) {
        const now = Date.now();
        const lastKill = this.lastWatchdogKill.get(instanceId) ?? 0;
        const sinceKill = now - lastKill;
        if (sinceKill < WATCHDOG_COOLDOWN_MS) {
            logger_1.logger.warn({ instanceId, elapsedMs, sinceLastKillMs: sinceKill }, '[Watchdog] Em cool-down — adiando kill para proteger numero de ban');
            return;
        }
        logger_1.logger.warn({ instanceId, elapsedMs }, '[Watchdog] Conexao morta confirmada — forcando reconexao');
        this.lastWatchdogKill.set(instanceId, now);
        this.lastWatchdogPing.delete(instanceId);
        try {
            sock.end(new Error('Watchdog timeout'));
        }
        catch { /* ignore */ }
        // Se o sock.end não disparar connection.close (raro), reconecta manualmente
        if (!this.sockets.has(instanceId) && tenantId) {
            this.stopWatchdog(instanceId);
            this.connect(instanceId, tenantId).catch(() => { });
        }
    }
    /** Para o watchdog de uma instância */
    stopWatchdog(instanceId) {
        const timer = this.watchdogTimers.get(instanceId);
        if (timer) {
            clearInterval(timer);
            this.watchdogTimers.delete(instanceId);
        }
        // Limpa estado de ping pendente — novo ciclo começa limpo.
        // NÃO limpa lastWatchdogKill aqui: o cool-down deve sobreviver a
        // stop/start para bloquear reconexões em cascata.
        this.lastWatchdogPing.delete(instanceId);
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  TIMER DE RECONEXÃO
    // ═══════════════════════════════════════════════════════════════════════════
    /** Cancela timer de reconexão pendente (evita reconexão duplicada) */
    clearReconnectTimer(instanceId) {
        const timer = this.reconnectTimers.get(instanceId);
        if (timer) {
            clearTimeout(timer);
            this.reconnectTimers.delete(instanceId);
        }
        this.reconnectScheduledAt.delete(instanceId);
    }
    /**
     * Agenda uma reconexão após `delay` ms.
     *
     * Centraliza o setTimeout + tracking do scheduledAt (usado pelo network
     * probe pra saber se vale adiantar). Cancela qualquer timer anterior.
     */
    scheduleReconnect(instanceId, tenantId, delay) {
        this.clearReconnectTimer(instanceId);
        const scheduledAt = Date.now() + delay;
        this.reconnectScheduledAt.set(instanceId, scheduledAt);
        const timer = setTimeout(() => {
            this.reconnectScheduledAt.delete(instanceId);
            this.connect(instanceId, tenantId);
        }, delay);
        this.reconnectTimers.set(instanceId, timer);
    }
    /**
     * Classifica um erro de conexão como "rede transiente".
     *
     * Erros de DNS/TCP costumam resolver em segundos. Erros de status do
     * WhatsApp (loggedOut, conflict, restartRequired) são categoria diferente
     * e devem usar o backoff longo tradicional.
     */
    isTransientNetworkError(err) {
        if (!err)
            return false;
        const code = err.code || err.cause?.code || err.errno;
        if (code && typeof code === 'string') {
            const transient = [
                'EAI_AGAIN', // DNS retry
                'ENOTFOUND', // DNS não resolveu
                'ECONNRESET',
                'ECONNREFUSED',
                'ETIMEDOUT',
                'EHOSTUNREACH',
                'ENETUNREACH',
                'ENETDOWN',
                'EPIPE',
            ];
            if (transient.includes(code))
                return true;
        }
        // Fallback: mensagem contém nome do host — é quase certo que é DNS/WS
        const msg = (err.message || '').toString();
        if (/getaddrinfo|web\.whatsapp\.com|WebSocket Error/i.test(msg))
            return true;
        return false;
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  NETWORK PROBE — Acorda reconexões quando a rede volta
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Inicia o network probe global.
     *
     * A cada NETWORK_PROBE_INTERVAL_MS, testa se o DNS resolve
     * `web.whatsapp.com`. Se resolver E houver alguma instância com retry
     * agendado para daqui a mais de PROBE_ADVANCE_THRESHOLD_MS, cancela o
     * timer dessa instância e dispara o retry imediatamente.
     *
     * Por quê DNS em vez de HTTP? Zero bytes para o WhatsApp. DNS falha com
     * EAI_AGAIN quando a rede está caída, e resolve rápido quando volta —
     * é um sinal confiável de disponibilidade de rede sem tocar no WA.
     */
    startNetworkProbe() {
        if (this.networkProbeTimer)
            return;
        this.networkProbeTimer = setInterval(async () => {
            // Curto-circuito: se não há retries pendentes, nem faz o DNS.
            if (this.reconnectScheduledAt.size === 0)
                return;
            try {
                await promises_2.default.lookup(PROBE_DNS_HOST);
            }
            catch {
                return; // Rede ainda caída — não adianta nada, espera o tick seguinte.
            }
            // DNS OK → adianta qualquer retry que ainda falte > threshold
            const now = Date.now();
            for (const [instanceId, scheduledAt] of this.reconnectScheduledAt) {
                const remaining = scheduledAt - now;
                if (remaining <= PROBE_ADVANCE_THRESHOLD_MS)
                    continue;
                const tenantId = this.tenantByInstance.get(instanceId);
                if (!tenantId)
                    continue;
                logger_1.logger.info({ instanceId, remainingMs: remaining }, '[NetworkProbe] Rede detectada — adiantando retry pendente');
                this.clearReconnectTimer(instanceId);
                // Dispara sem await — probe roda em interval e não deve bloquear
                this.connect(instanceId, tenantId).catch((err) => logger_1.logger.error({ err, instanceId }, '[NetworkProbe] Falha ao adiantar retry'));
            }
        }, NETWORK_PROBE_INTERVAL_MS);
        // Não segura o processo vivo só por causa do probe
        this.networkProbeTimer.unref?.();
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  DESCONEXÃO MANUAL
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Desconecta uma instância manualmente (logout).
     *
     * Diferente de uma desconexão por erro — aqui o usuário está
     * intencionalmente desconectando. Limpa todos os timers e
     * faz logout no WhatsApp (invalidando a sessão).
     */
    async disconnect(instanceId) {
        const sock = this.sockets.get(instanceId);
        if (!sock)
            return;
        this.clearReconnectTimer(instanceId);
        this.stopWatchdog(instanceId);
        this.reconnectAttempts.delete(instanceId);
        // Logout manual zera o estado de cool-down: reconexão futura é um
        // novo ciclo e não deve herdar o bloqueio do watchdog.
        this.lastWatchdogKill.delete(instanceId);
        this.lastActivity.delete(instanceId);
        await sock.logout();
        this.sockets.delete(instanceId);
        this.contactHandlers.delete(instanceId);
        this.msgHandlers.delete(instanceId);
        this.engineByInstance.delete(instanceId);
        logger_1.logger.info({ instanceId }, 'Instancia desconectada manualmente');
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  ACESSORES PÚBLICOS
    // ═══════════════════════════════════════════════════════════════════════════
    /** Retorna o socket Baileys ativo para uma instância (usado pelas rotas de envio) */
    getSocket(instanceId) {
        return this.sockets.get(instanceId);
    }
    /** Retorna o Socket.IO server (usado pelas rotas para emitir eventos) */
    getIO() {
        return this.io;
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  HELPERS INTERNOS
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Atualiza o status da instância no banco E no cache.
     * Dupla escrita para que o frontend possa consultar status tanto
     * via API (banco) quanto via polling rápido (cache DragonflyDB).
     */
    async updateInstanceStatus(instanceId, status) {
        await Promise.all([
            prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { status } })
                .catch((err) => { if (err?.code !== 'P2025')
                throw err; }),
            dragonfly_1.dragonfly.set(CACHE_KEY_STATUS(instanceId), status),
        ]);
    }
    /** Remove arquivos de sessão do disco (após loggedOut) */
    async deleteSessionFiles(instanceId) {
        const sessionDir = path_1.default.resolve(env_1.env.BAILEYS_SESSIONS_PATH, instanceId);
        await promises_1.default.rm(sessionDir, { recursive: true, force: true });
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  SINCRONIZAÇÃO DE CHATS
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Sincroniza lista de chats no banco.
     *
     * Chamado por: chats.upsert, chats.update, groups.upsert, groups.update,
     * messaging-history.set
     *
     * Para cada chat:
     *   - Cria ou atualiza no banco com JID, nome (grupo), timestamps
     *   - Vincula ao Contact existente (se houver)
     *   - Sincroniza estado de pin/archive/unread do WhatsApp
     *
     * Otimização: busca todos os contatos existentes em uma única query
     * e usa Map para lookup O(1) por JID.
     */
    async syncChats(instanceId, tenantId, chats) {
        if (chats.length === 0)
            return;
        // Busca todos os contatos existentes para os JIDs (1 query)
        const jids = chats.map((c) => c.id).filter(Boolean);
        const existingContacts = await prisma_1.prisma.contact.findMany({
            where: { tenantId, instanceId, jid: { in: jids } },
            select: { id: true, jid: true },
        });
        const contactIdByJid = new Map(existingContacts.map((c) => [c.jid, c.id]));
        // Pré-carrega mapeamento LID → phone JID para chats @lid.
        // ContactHandler.upsert já rodou antes (messaging-history.set processa
        // contacts → chats → messages nessa ordem), então lidJid já está populado.
        const lidJids = jids.filter((j) => j.endsWith('@lid'));
        const lidToPhoneJid = new Map(); // @lid → @s.whatsapp.net
        const lidToContactId = new Map(); // @lid → contactId
        if (lidJids.length > 0) {
            const resolvedByLid = await prisma_1.prisma.contact.findMany({
                where: {
                    tenantId,
                    instanceId,
                    lidJid: { in: lidJids },
                    NOT: { jid: { endsWith: '@lid' } }, // só aceita se resolveu para phone JID
                },
                select: { id: true, jid: true, lidJid: true },
            });
            for (const rc of resolvedByLid) {
                if (rc.lidJid) {
                    lidToPhoneJid.set(rc.lidJid, rc.jid);
                    lidToContactId.set(rc.lidJid, rc.id);
                }
            }
        }
        for (const chat of chats) {
            const rawJid = chat.id;
            if (!rawJid)
                continue;
            // Descarta JIDs de sistema: broadcasts, newsletters e conta PSA (0@s.whatsapp.net)
            if (rawJid.includes('@broadcast') || rawJid.endsWith('@newsletter') || rawJid.startsWith('0@'))
                continue;
            // ── Resolução LID → phone JID ─────────────────────────────────────
            // Chats @lid são artefatos do protocolo WhatsApp multi-device:
            // o JID real do contato é @s.whatsapp.net.
            // Fontes de resolução (ordem de prioridade):
            //   1. Mapeamento no banco (ContactHandler.upsert populou via lidJid)
            //   2. Campo pnJid do objeto de chat — enviado pelo InfiniteAPI v7+ no
            //      messaging-history.set direto do proto.IConversation.pnJid
            let jid = rawJid;
            if (rawJid.endsWith('@lid')) {
                const phoneJid = lidToPhoneJid.get(rawJid)
                    ?? chat.pnJid;
                if (phoneJid) {
                    jid = phoneJid;
                    // Garante que o contactId mapeado seja usado abaixo
                    const resolvedContactId = lidToContactId.get(rawJid) ?? contactIdByJid.get(jid);
                    if (resolvedContactId)
                        contactIdByJid.set(jid, resolvedContactId);
                    // Persiste o mapeamento no banco caso tenha vindo só do pnJid (fallback)
                    if (!lidToPhoneJid.has(rawJid)) {
                        prisma_1.prisma.contact.updateMany({
                            where: { tenantId, instanceId, jid: phoneJid },
                            data: { lidJid: rawJid },
                        }).catch(() => { });
                    }
                }
                else {
                    // Sem resolução disponível: chat será criado via handleHistory quando
                    // a mensagem chegar com senderPn resolvível.
                    logger_1.logger.debug({ rawJid, instanceId }, '[syncChats] @lid sem resolução — descartando');
                    continue;
                }
            }
            try {
                // Converte timestamp Unix (segundos) para Date.
                // Fallback: lastMessageRecvTimestamp (InfiniteAPI define a partir das mensagens do sync)
                // quando conversationTimestamp é 0 ou null (ex: chats antigos fora da janela de sync).
                const rawTs = Number(chat.conversationTimestamp || 0)
                    || Number(chat.lastMessageRecvTimestamp || 0);
                const lastAt = rawTs > 0 ? new Date(rawTs * 1000) : undefined;
                const isGroup = jid.endsWith('@g.us');
                // Nome do chat: apenas grupos têm nome (subject) — individuais usam Contact.name
                const groupName = isGroup ? (chat.name ?? null) : null;
                const contactId = contactIdByJid.get(jid) ?? null;
                await prisma_1.prisma.chat.upsert({
                    where: { tenantId_instanceId_jid: { tenantId, instanceId, jid } },
                    create: {
                        tenantId,
                        instanceId,
                        jid,
                        name: groupName,
                        unreadCount: chat.unreadCount ?? 0,
                        lastMessageAt: lastAt ?? null,
                        isPinned: !!chat.pinned,
                        isArchived: chat.archived ?? false,
                        ...(contactId && { contactId }),
                    },
                    update: {
                        ...(groupName && { name: groupName }),
                        ...(contactId && { contactId }),
                        // Só aplica campos que vieram explicitamente no evento.
                        // chats.upsert pode ser parcial (nova mensagem, metadata) — se
                        // pinned/archived/unreadCount estiverem ausentes, não sobrescreve
                        // o valor salvo no banco.
                        ...(chat.unreadCount !== undefined && { unreadCount: chat.unreadCount }),
                        ...(lastAt && { lastMessageAt: lastAt }),
                        ...(chat.pinned !== undefined && { isPinned: !!chat.pinned }),
                        ...(chat.archived !== undefined && { isArchived: !!chat.archived }),
                    },
                });
            }
            catch (err) {
                logger_1.logger.error({ err, jid }, '[WhatsApp] Erro ao sincronizar chat');
            }
        }
        // Invalida cache da listagem de chats — refletir alterações imediatamente na inbox
        (0, chat_cache_1.invalidateChatListCache)(tenantId, instanceId).catch(() => { });
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  HANDLER DE ATUALIZAÇÕES PARCIAIS DE CHAT (chats.update)
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Processa atualizações parciais de chats (pin, archive, unread).
     *
     * Diferente de syncChats (usado para bulk/history), este método:
     *   1. Só atualiza campos presentes na atualização (sem sobrescrever com defaults)
     *   2. Emite chat:upsert via Socket.IO para atualizar o frontend em tempo real
     *
     * O evento chats.update do Baileys traz objetos parciais — apenas os campos
     * que mudaram. Por isso não podemos usar ?? 0 para unreadCount, pois isso
     * sobrescreveria o valor correto com zero.
     */
    async handleChatsUpdate(instanceId, tenantId, updates) {
        for (const update of updates) {
            const jid = update.id;
            if (!jid || jid.endsWith('@lid'))
                continue;
            try {
                // Monta patch com apenas os campos presentes na atualização
                const patch = {};
                if (update.pinned !== undefined)
                    patch.isPinned = !!update.pinned;
                if (update.archived !== undefined)
                    patch.isArchived = !!update.archived;
                if (update.unreadCount !== undefined)
                    patch.unreadCount = update.unreadCount;
                if (Object.keys(patch).length === 0)
                    continue;
                const chat = await prisma_1.prisma.chat.update({
                    where: { tenantId_instanceId_jid: { tenantId, instanceId, jid } },
                    data: patch,
                    include: {
                        contact: {
                            select: {
                                name: true, verifiedName: true, notify: true,
                                phone: true, avatarUrl: true,
                                scoreReputacao: true, flagRestricao: true,
                            },
                        },
                    },
                }).catch(() => null);
                if (!chat)
                    continue;
                // Emite para o frontend atualizar em tempo real
                const c = chat.contact;
                const isGroup = jid.endsWith('@g.us');
                const displayName = isGroup
                    ? (chat.name ?? null)
                    : (c?.name ?? c?.verifiedName ?? c?.notify ?? null);
                this.io.to(`tenant:${tenantId}`).emit('chat:upsert', {
                    id: chat.id,
                    tenantId: chat.tenantId,
                    instanceId: chat.instanceId,
                    jid: chat.jid,
                    name: chat.name ?? null,
                    unreadCount: chat.unreadCount,
                    lastMessageAt: chat.lastMessageAt,
                    isPinned: chat.isPinned,
                    isArchived: chat.isArchived,
                    botPaused: chat.botPaused,
                    contact: c
                        ? {
                            name: displayName,
                            phone: c.phone,
                            avatarUrl: c.avatarUrl,
                            scoreReputacao: c.scoreReputacao,
                            flagRestricao: c.flagRestricao,
                        }
                        : isGroup
                            ? { name: displayName, phone: null, avatarUrl: null, scoreReputacao: 0, flagRestricao: false }
                            : null,
                    lastMessage: null,
                });
            }
            catch (err) {
                logger_1.logger.error({ err, jid }, '[WhatsApp] Erro ao aplicar chats.update');
            }
        }
        (0, chat_cache_1.invalidateChatListCache)(tenantId, instanceId).catch(() => { });
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  DEBUG LOG (página /baileys no frontend)
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Emite evento de debug para a página /baileys do frontend.
     *
     * Permite monitorar em tempo real todos os eventos Baileys
     * sem precisar acessar os logs do servidor.
     */
    baileysLog(tenantId, instanceId, event, data) {
        this.io.to(`tenant:${tenantId}`).emit('baileys:log', {
            timestamp: new Date().toISOString(),
            sessionId: instanceId,
            event,
            data,
        });
    }
}
exports.WhatsAppConnectionManager = WhatsAppConnectionManager;
//# sourceMappingURL=WhatsAppConnectionManager.js.map