"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContactHandler = void 0;
const prisma_1 = require("../../../infra/database/prisma");
const logger_1 = require("../../../config/logger");
// ─── Configuração de rate-limit para busca de avatares ──────────────────────
// O WhatsApp bloqueia (ban temporário) se muitas requisições profilePictureUrl
// forem feitas em curto período. Estas constantes controlam o throttling.
/** Quantos avatares buscar em paralelo por lote */
const AVATAR_BATCH_SIZE = 5;
/** Delay entre lotes de avatares (~1 req/s por JID → abaixo do threshold do WA) */
const AVATAR_DELAY_MS = 900;
/** Máximo de avatares a buscar no sync pós-conexão (evita overload em contas grandes) */
const MAX_MISSING_AVATAR_SYNC = 80;
class ContactHandler {
    constructor(
    /** UUID da instância WhatsApp */
    instanceId, 
    /** UUID do tenant (isolamento multi-tenant) */
    tenantId, 
    /** Socket Baileys — usado para profilePictureUrl e resyncAppState */
    sock, 
    /** Socket.IO — emite eventos de avatar/reconciliação para o frontend */
    io) {
        this.instanceId = instanceId;
        this.tenantId = tenantId;
        this.sock = sock;
        this.io = io;
        /** Fila de JIDs aguardando busca de avatar */
        this.avatarQueue = [];
        /** Flag de mutex simples — evita processamento concorrente da fila */
        this.processingAvatars = false;
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  RESOLUÇÃO DE NOME
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Regra de ouro para resolução de nome de contato:
     *   name (agenda do telefone) > verifiedName (conta business) > notify (pushname)
     *
     * Retorna undefined se nenhum campo estiver disponível.
     */
    resolveName(c) {
        return c.name ?? c.verifiedName ?? c.notify ?? undefined;
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  UPSERT EM MASSA (contacts.upsert + messaging-history.set)
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Cria ou atualiza contatos em massa.
     *
     * Dispara em dois cenários:
     *   1. Evento contacts.upsert do Baileys (conexão inicial ou reconexão)
     *   2. Campo contacts[] do messaging-history.set (history sync)
     *
     * Para cada contato:
     *   - Cria ou atualiza no banco com nome resolvido
     *   - Vincula chats órfãos (Chat sem contactId) ao contato
     *   - Enfileira busca de avatar se imgUrl não disponível
     *   - Captura mapeamento LID → phone JID (campo `lid` do history sync)
     *
     * TRATAMENTO DE LID:
     *   O Baileys pode enviar contatos com c.id no formato @lid.
     *   Quando o contato do history sync inclui campo `jid` (telefone real),
     *   usamos esse JID como chave primária e salvamos o LID em lidJid.
     *   Quando não há campo `jid`, o contato é criado com o LID como JID
     *   (será corrigido quando phoneNumberShare ou senderPn revelar o telefone).
     */
    async upsert(contacts) {
        /** JIDs que precisam buscar avatar via profilePictureUrl */
        const needFetchAvatar = [];
        /** Contador de avatares que já vieram prontos do Baileys (imgUrl direto) */
        let directAvatarCount = 0;
        for (const c of contacts) {
            if (!c.id)
                continue;
            const isLid = c.id.endsWith('@lid');
            const isBroadcast = c.id.includes('@broadcast');
            if (isBroadcast)
                continue;
            // imgUrl pode vir do Baileys com valores especiais:
            //   - URL string: avatar disponível (salva direto)
            //   - 'changed': avatar foi alterado (precisa buscar novamente)
            //   - null/undefined: não disponível (precisa buscar via API)
            const imgUrl = c.imgUrl;
            // LID mapping: o history sync do Baileys traz campo `lid` nos contatos
            // que associa o LID ao contato com JID real (@s.whatsapp.net)
            // Ex: { id: "5511999999999@s.whatsapp.net", lid: "123456789@lid" }
            const lidFromContact = c.lid;
            try {
                // Para contatos @lid que possuem campo `jid` (vem do history sync),
                // usa o JID real como chave primária. Caso contrário, usa c.id como está.
                // Isso evita criar contatos duplicados (um com LID, outro com phone).
                const contactJid = isLid && c.jid ? c.jid : c.id;
                const contact = await prisma_1.prisma.contact.upsert({
                    where: {
                        tenantId_instanceId_jid: {
                            tenantId: this.tenantId,
                            instanceId: this.instanceId,
                            jid: contactJid,
                        },
                    },
                    create: {
                        tenantId: this.tenantId,
                        instanceId: this.instanceId,
                        jid: contactJid,
                        name: this.resolveName(c) ?? null,
                        verifiedName: c.verifiedName ?? null,
                        notify: c.notify ?? null,
                        phone: contactJid.endsWith('@lid') ? null : contactJid.split('@')[0],
                        ...(imgUrl && imgUrl !== 'changed' ? { avatarUrl: imgUrl } : {}),
                        // Salva mapeamento LID:
                        //   Se o contato É um LID (c.id termina em @lid): salva c.id como lidJid
                        //   Se o contato TEM campo lid (history sync): salva lid como lidJid
                        ...(isLid ? { lidJid: c.id } : lidFromContact ? { lidJid: lidFromContact } : {}),
                    },
                    update: {
                        name: this.resolveName(c) ?? null,
                        verifiedName: c.verifiedName ?? null,
                        notify: c.notify ?? null,
                        ...(imgUrl && imgUrl !== 'changed' ? { avatarUrl: imgUrl } : {}),
                        ...(isLid ? { lidJid: c.id } : lidFromContact ? { lidJid: lidFromContact } : {}),
                    },
                });
                // Garante vínculo Chat ↔ Contact para chats órfãos
                // (chats criados antes do contato existir ficam com contactId=null)
                await prisma_1.prisma.chat.updateMany({
                    where: {
                        tenantId: this.tenantId,
                        instanceId: this.instanceId,
                        jid: c.id,
                        contactId: null,
                    },
                    data: { contactId: contact.id },
                });
                // ── Lógica de avatar ──────────────────────────────────────────────
                // JIDs @lid não suportam profilePictureUrl (retorna 404 no WA)
                if (!isLid) {
                    if (imgUrl && imgUrl !== 'changed') {
                        // Avatar veio pronto do Baileys — notifica frontend imediatamente
                        directAvatarCount++;
                        this.io?.to(`tenant:${this.tenantId}`).emit('contact:avatar', {
                            instanceId: this.instanceId,
                            jid: c.id,
                            avatarUrl: imgUrl,
                        });
                    }
                    else if (imgUrl === 'changed' || imgUrl === undefined) {
                        // Precisa buscar via API — adiciona à fila com rate-limit
                        needFetchAvatar.push(c.id);
                    }
                }
            }
            catch (err) {
                logger_1.logger.error({ err, jid: c.id }, '[ContactHandler] Erro ao upsert contato');
            }
        }
        logger_1.logger.debug({
            instanceId: this.instanceId,
            direct: directAvatarCount,
            toFetch: needFetchAvatar.length,
        }, '[ContactHandler] Avatares: diretos vs a buscar via API');
        // Enfileira busca de avatares com cap de 50 por batch
        // (randomiza para priorizar contatos diferentes a cada sync)
        if (this.sock && needFetchAvatar.length > 0) {
            const toQueue = needFetchAvatar.length > 50
                ? needFetchAvatar.sort(() => Math.random() - 0.5).slice(0, 50)
                : needFetchAvatar;
            this.enqueueAvatars(toQueue);
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  UPDATE PARCIAL (contacts.update)
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Atualiza campos parciais de contatos existentes.
     *
     * Dispara no evento contacts.update do Baileys — diferente do upsert,
     * aqui só atualizamos os campos que foram explicitamente alterados.
     * Não cria contatos novos (se não existir, ignora silenciosamente).
     */
    async update(contacts) {
        for (const c of contacts) {
            if (!c.id)
                continue;
            const resolvedName = this.resolveName(c);
            // Se nenhum campo relevante mudou, pula
            if (!resolvedName && c.verifiedName === undefined && c.notify === undefined)
                continue;
            try {
                await prisma_1.prisma.contact.updateMany({
                    where: {
                        tenantId: this.tenantId,
                        instanceId: this.instanceId,
                        jid: c.id,
                    },
                    data: {
                        ...(resolvedName !== undefined && { name: resolvedName }),
                        ...(c.verifiedName !== undefined && { verifiedName: c.verifiedName }),
                        ...(c.notify !== undefined && { notify: c.notify }),
                    },
                });
            }
            catch (err) {
                logger_1.logger.error({ err, jid: c.id }, '[ContactHandler] Erro ao atualizar contato');
            }
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  AVATAR DA INSTÂNCIA (foto de perfil do número da sessão)
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Busca e salva a foto de perfil do número WhatsApp da instância.
     * Chamado pelo WhatsAppConnectionManager após connection 'open'.
     */
    async syncInstanceAvatar(instanceId) {
        if (!this.sock?.user?.id)
            return null;
        try {
            const url = await this.sock.profilePictureUrl(this.sock.user.id, 'image');
            if (url) {
                await prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { avatar: url } });
                logger_1.logger.info({ instanceId }, '[ContactHandler] Avatar da instância atualizado');
                return url;
            }
        }
        catch {
            // Avatar privado ou não disponível — normal para contas com privacidade restrita
        }
        return null;
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  RECONCILIAÇÃO DE NOMES VIA PUSHNAME
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Reconcilia nomes de contatos após sync do histórico.
     *
     * PROBLEMA: O history sync do Baileys (messaging-history.set) traz contatos
     * sem nome — apenas contas business (~22 de ~3000) vêm com verifiedName.
     * Os pushNames só aparecem nas mensagens, não nos contatos do sync.
     *
     * SOLUÇÃO: Após o sync completo (isLatest=true), busca contatos com
     * name=null E notify=null, e preenche usando o pushName da mensagem
     * recebida mais recente para cada JID.
     *
     * FILTROS: Ignora @lid, @broadcast, @newsletter, @g.us (grupos usam subject)
     *
     * Emite evento `contacts:reconciled` para o frontend re-buscar a lista de chats.
     */
    async reconcileNamesFromMessages() {
        try {
            // Busca contatos "sem nome" — candidatos à reconciliação
            const unnamed = await prisma_1.prisma.contact.findMany({
                where: {
                    tenantId: this.tenantId,
                    instanceId: this.instanceId,
                    name: null,
                    notify: null,
                    NOT: [
                        { jid: { endsWith: '@lid' } },
                        { jid: { contains: '@broadcast' } },
                        { jid: { endsWith: '@newsletter' } },
                        { jid: { endsWith: '@g.us' } },
                    ],
                },
                select: { id: true, jid: true },
            });
            if (unnamed.length === 0) {
                logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] Reconciliação: todos os contatos já possuem nome');
                return;
            }
            logger_1.logger.info({ instanceId: this.instanceId, count: unnamed.length }, '[ContactHandler] Reconciliação: buscando pushNames nas mensagens');
            let updated = 0;
            for (const contact of unnamed) {
                // Busca a mensagem recebida mais recente com pushName para este JID
                const msg = await prisma_1.prisma.message.findFirst({
                    where: {
                        instanceId: this.instanceId,
                        remoteJid: contact.jid,
                        fromMe: false,
                        pushName: { not: null },
                    },
                    orderBy: { timestamp: 'desc' },
                    select: { pushName: true },
                });
                if (msg?.pushName) {
                    await prisma_1.prisma.contact.update({
                        where: { id: contact.id },
                        data: { name: msg.pushName, notify: msg.pushName },
                    });
                    updated++;
                }
            }
            logger_1.logger.info({ instanceId: this.instanceId, total: unnamed.length, updated }, '[ContactHandler] Reconciliação de nomes concluída');
            // Notifica o frontend para atualizar a lista de chats
            if (updated > 0) {
                this.io?.to(`tenant:${this.tenantId}`).emit('contacts:reconciled', {
                    instanceId: this.instanceId,
                    count: updated,
                });
            }
        }
        catch (err) {
            logger_1.logger.error({ err }, '[ContactHandler] Erro na reconciliação de nomes');
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  FORCE CONTACT SYNC (resyncAppState)
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Força re-sync dos contatos via app-state do Baileys.
     *
     * NOTA: Este método existe mas NÃO é chamado automaticamente.
     * Em reconexões, o Baileys não re-envia contacts.upsert. Este método
     * força o resync das app-state collections que contêm contatos.
     *
     * PROBLEMA CONHECIDO: a collection 'regular_low' pode falhar com
     * "tried remove, but no previous op" — erro de estado corrompido
     * no Baileys. Por isso foi removido do trigger automático pós-conexão.
     *
     * Pode ser chamado manualmente via API se necessário.
     */
    async forceContactSync() {
        if (!this.sock)
            return;
        try {
            logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] Forçando resync de contatos via app-state');
            await this.sock.resyncAppState(['critical_unblock_low', 'regular_low', 'regular'], false);
            logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] resyncAppState concluído');
        }
        catch (err) {
            logger_1.logger.warn({ err, instanceId: this.instanceId }, '[ContactHandler] Erro no resyncAppState (não-fatal)');
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  SYNC PÓS-CONEXÃO: AVATARES AUSENTES
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Busca avatares ausentes para chats ativos após reconexão.
     *
     * Chamado pelo WhatsAppConnectionManager 5s após connection 'open'.
     * Cobre o caso em que contacts.upsert não disparou (history sync timeout)
     * ou contatos vieram sem imgUrl.
     *
     * Prioriza chats não-arquivados ordenados por lastMessageAt (mais recentes primeiro).
     * Limitado a MAX_MISSING_AVATAR_SYNC (80) para não sobrecarregar a API do WA.
     */
    async syncMissingAvatars() {
        try {
            const missing = await prisma_1.prisma.chat.findMany({
                where: {
                    tenantId: this.tenantId,
                    instanceId: this.instanceId,
                    isArchived: false,
                    NOT: [
                        { jid: { endsWith: '@lid' } },
                        { jid: { contains: '@broadcast' } },
                    ],
                    contact: { avatarUrl: null },
                },
                select: { jid: true },
                orderBy: { lastMessageAt: { sort: 'desc', nulls: 'last' } },
                take: MAX_MISSING_AVATAR_SYNC,
            });
            if (missing.length === 0)
                return;
            const jids = missing.map((c) => c.jid);
            logger_1.logger.info({ instanceId: this.instanceId, count: jids.length }, '[ContactHandler] Sync pós-conexão: buscando avatares ausentes');
            this.enqueueAvatars(jids);
        }
        catch (err) {
            logger_1.logger.error({ err }, '[ContactHandler] Erro no sync de avatares ausentes');
        }
    }
    /**
     * Re-faz profilePictureUrl para contatos cuja URL CDN do WhatsApp expira
     * em menos de 48 horas ou já expirou.
     *
     * URLs do WA têm parâmetro `oe=<hex epoch>` que indica a expiração.
     * Sem renovação, <img> quebra silenciosamente no browser após o TTL.
     */
    async syncExpiredAvatars() {
        try {
            const contacts = await prisma_1.prisma.contact.findMany({
                where: {
                    tenantId: this.tenantId,
                    instanceId: this.instanceId,
                    avatarUrl: { not: null },
                    NOT: [
                        { jid: { endsWith: '@lid' } },
                        { jid: { contains: '@broadcast' } },
                    ],
                },
                select: { jid: true, avatarUrl: true },
                orderBy: { updatedAt: 'asc' },
                take: 200,
            });
            const threshold = Math.floor(Date.now() / 1000) + 48 * 3600; // now + 48h
            const expiring = contacts
                .filter(c => {
                const m = c.avatarUrl.match(/[?&]oe=([0-9a-f]+)/i);
                if (!m)
                    return false;
                return parseInt(m[1], 16) < threshold;
            })
                .map(c => c.jid);
            if (expiring.length === 0)
                return;
            logger_1.logger.info({ instanceId: this.instanceId, count: expiring.length }, '[ContactHandler] Renovando avatares próximos de expirar');
            // Limpa as URLs expiradas antes de re-buscar, para que o browser
            // não use URLs inválidas enquanto a fila processa
            await prisma_1.prisma.contact.updateMany({
                where: {
                    tenantId: this.tenantId,
                    instanceId: this.instanceId,
                    jid: { in: expiring },
                },
                data: { avatarUrl: null },
            });
            this.enqueueAvatars(expiring.slice(0, MAX_MISSING_AVATAR_SYNC));
        }
        catch (err) {
            logger_1.logger.error({ err }, '[ContactHandler] Erro no sync de avatares expirados');
        }
    }
    // ═══════════════════════════════════════════════════════════════════════════
    //  FILA DE AVATARES COM RATE-LIMIT
    // ═══════════════════════════════════════════════════════════════════════════
    /**
     * Adiciona JIDs à fila de busca de avatares (com deduplicação).
     *
     * A fila é processada em lotes de AVATAR_BATCH_SIZE com delay de
     * AVATAR_DELAY_MS entre cada lote para respeitar o rate-limit do WhatsApp.
     *
     * Se a fila já está sendo processada, os novos JIDs são adicionados
     * e serão processados na próxima iteração do loop.
     */
    enqueueAvatars(jids) {
        const inQueue = new Set(this.avatarQueue);
        for (const jid of jids) {
            if (!inQueue.has(jid))
                this.avatarQueue.push(jid);
        }
        this.processAvatarQueue();
    }
    /**
     * Processa a fila de avatares com rate-limit.
     *
     * Usa Promise.allSettled() para que falhas individuais (foto privada,
     * JID inválido) não interrompam o lote. Cada avatar encontrado é:
     *   1. Salvo no campo avatarUrl do contato no banco
     *   2. Emitido via Socket.IO para atualizar o frontend em tempo real
     */
    async processAvatarQueue() {
        // Mutex simples: apenas uma execução por vez
        if (this.processingAvatars || this.avatarQueue.length === 0)
            return;
        this.processingAvatars = true;
        logger_1.logger.info({ count: this.avatarQueue.length, instanceId: this.instanceId }, '[ContactHandler] Iniciando sync de avatares');
        while (this.avatarQueue.length > 0 && this.sock) {
            // Pega o próximo lote (remove da fila)
            const batch = this.avatarQueue.splice(0, AVATAR_BATCH_SIZE);
            await Promise.allSettled(batch.map(async (jid) => {
                try {
                    const url = await this.sock.profilePictureUrl(jid, 'image');
                    if (url) {
                        // Persiste no banco
                        await prisma_1.prisma.contact.updateMany({
                            where: { tenantId: this.tenantId, instanceId: this.instanceId, jid },
                            data: { avatarUrl: url },
                        });
                        // Notifica o frontend para atualizar o avatar na UI
                        this.io?.to(`tenant:${this.tenantId}`).emit('contact:avatar', {
                            instanceId: this.instanceId,
                            jid,
                            avatarUrl: url,
                        });
                    }
                }
                catch {
                    // Foto privada ou JID inválido — ignora silenciosamente
                    // (não loga para evitar spam com milhares de contatos)
                }
            }));
            // Delay entre lotes para respeitar rate-limit do WhatsApp
            if (this.avatarQueue.length > 0) {
                await new Promise((r) => setTimeout(r, AVATAR_DELAY_MS * AVATAR_BATCH_SIZE));
            }
        }
        this.processingAvatars = false;
        logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] Sync de avatares concluído');
    }
}
exports.ContactHandler = ContactHandler;
//# sourceMappingURL=ContactHandler.js.map