/**
 * Rotas de envio e operações WhatsApp.
 *
 * Endpoints:
 *   POST /api/instances/:instanceId/send              — Envia mensagem de texto (com @mentions e reply)
 *   GET  /api/instances/:instanceId/groups/:jid/participants — Lista participantes de grupo
 *   POST /api/instances/:instanceId/reconcile-names   — Reconcilia nomes via pushName (bulk SQL)
 *   POST /api/instances/:instanceId/broadcast         — Disparo em massa (async, até 500 números)
 *   GET  /api/instances/:instanceId/broadcast         — Lista broadcasts da instância
 *   GET  /api/instances/:instanceId/broadcast/:id     — Detalhe de um broadcast
 *   GET  /api/inbox/avatar/:jid                       — Proxy de foto de perfil (evita CORS)
 *
 * Todas as rotas exigem autenticação (tenantId no req).
 * O WhatsAppConnectionManager é injetado via buildSendRoutes() para
 * acessar os sockets Baileys ativos.
 *
 * @see WhatsAppConnectionManager — provê getSocket() e getIO()
 * @see ContactHandler — reconciliação de nomes (mesma lógica, mas via endpoint HTTP)
 */
import { Router, type Request, type Response } from 'express'
import { createHash } from 'crypto'
import { z } from 'zod'
import { prisma } from '../../infra/database/prisma'
import { dragonfly } from '../../infra/cache/dragonfly'
import { logger } from '../../config/logger'
import type { WhatsAppConnectionManager } from './connection/WhatsAppConnectionManager'
import { sendRichMessage } from './rich-message'

/**
 * Constrói as rotas de envio e operações WhatsApp.
 *
 * Montadas em: /api/instances/:instanceId/
 * Usa mergeParams para herdar :instanceId do router pai.
 */
export function buildSendRoutes(manager: WhatsAppConnectionManager): Router {
  const router = Router({ mergeParams: true })

  // ═══════════════════════════════════════════════════════════════════════════
  //  POST /api/instances/:instanceId/send — Envio de mensagem de texto
  // ═══════════════════════════════════════════════════════════════════════════

  /**
   * Envia uma mensagem de texto para um JID (contato ou grupo).
   *
   * Body:
   *   - jid: string (obrigatório) — JID destino (ex: "5511999999999@s.whatsapp.net")
   *   - text: string (obrigatório) — Texto da mensagem (até 4096 chars)
   *   - replyToMessageId?: string — ID da mensagem sendo respondida (quote/reply)
   *   - mentions?: string[] — Lista de JIDs mencionados com @ (para grupos)
   *
   * Fluxo:
   *   1. Valida instância conectada e socket disponível
   *   2. Monta contexto de reply (se houver replyToMessageId)
   *   3. Envia via Baileys sock.sendMessage()
   *   4. Cria/atualiza Chat no banco
   *   5. Persiste a mensagem enviada
   *   6. Retorna 201 com o registro da mensagem
   */
  router.post('/send', async (req: Request, res: Response) => {
    const tenantId = req.tenantId!
    const instanceId = req.params['instanceId'] as string
    const {
      jid,
      text,
      footer,
      replyToMessageId,
      mentions,
      // ── Tipos ricos (InfiniteAPI/Baileys) ────────────────────────────────
      nativeButtons,
      nativeList,
      nativeCarousel,
      poll,
    } = req.body as {
      jid: string
      text?: string
      footer?: string
      replyToMessageId?: string
      mentions?: string[]
      nativeButtons?: Array<{ type: string; id?: string; text: string; url?: string; copyText?: string; phoneNumber?: string }>
      nativeList?: { buttonText: string; sections: Array<{ title: string; rows: Array<{ id: string; title: string; description?: string }> }> }
      nativeCarousel?: { cards: Array<{ title?: string; body?: string; footer?: string; image?: { url: string }; buttons: Array<{ type?: string; id?: string; text: string }> }> }
      poll?: { name: string; values: string[]; selectableCount?: number }
    }

    const hasRichContent = !!(nativeButtons || nativeList || nativeCarousel || poll)

    if (!jid || (!text && !hasRichContent)) {
      res.status(400).json({ error: 'jid e text (ou tipo rico) são obrigatórios' })
      return
    }

    // Valida que a instância existe e está conectada
    const instance = await prisma.instance.findFirst({
      where: { id: instanceId, tenantId },
    })
    if (!instance || instance.status !== 'CONNECTED') {
      res.status(400).json({ error: 'Instância não conectada' })
      return
    }

    // Busca o socket Baileys ativo
    const sock = manager.getSocket(instanceId)
    if (!sock) {
      res.status(503).json({ error: 'Socket não disponível' })
      return
    }

    // ── Resolve o JID real via onWhatsApp (corrige o dígito 9 brasileiro) ────
    // Números brasileiros migraram de 8 para 9 dígitos. O usuário pode digitar
    // com ou sem o 9 — o WhatsApp sabe o JID correto. Grupos (@g.us) são
    // excluídos pois já têm JID definitivo.
    let resolvedJid = jid
    if (!jid.endsWith('@g.us')) {
      try {
        const waResults = await sock.onWhatsApp(jid)
        const waResult = waResults?.[0]
        if (!waResult?.exists) {
          res.status(422).json({ error: 'Número não encontrado no WhatsApp' })
          return
        }
        // Se onWhatsApp retornar @lid (WhatsApp moderno), ignoramos — o Baileys
        // resolve @lid internamente ao enviar. Armazenar @lid no banco criaria
        // um chat duplicado conflitando com o @s.whatsapp.net do MessageHandler.
        if (!waResult.jid.endsWith('@lid')) {
          resolvedJid = waResult.jid // JID correto com ou sem o 9
        }
      } catch (err) {
        logger.warn({ err, jid }, 'onWhatsApp falhou — usando JID original')
        // Continua com o JID original se a consulta falhar
      }
    }

    // Monta contexto de reply (quoted message) se necessário
    // O Baileys precisa da mensagem original para exibir o quote no WhatsApp
    let quotedMsg: any
    if (replyToMessageId) {
      const reply = await prisma.message.findUnique({
        where: { instanceId_messageId: { instanceId, messageId: replyToMessageId } },
      })
      if (reply?.body) {
        quotedMsg = {
          key: { remoteJid: resolvedJid, fromMe: reply.fromMe, id: reply.messageId },
          message: { conversation: reply.body },
        }
      }
    }

    // ── Mensagens ricas: delega ao rich-message builder ──────────────────────
    // O @whiskeysockets/baileys padrão não suporta nativeButtons/nativeList/
    // nativeCarousel. O builder converte para os protos corretos e usa
    // relayMessage() para enviar sem passar pelo generateWAMessageContent.
    const hasRich = !!(nativeButtons || nativeList || nativeCarousel || poll)

    let sentMessageId: string
    if (hasRich) {
      sentMessageId = await sendRichMessage(
        sock,
        resolvedJid,
        { text, footer, nativeButtons, nativeList, nativeCarousel, poll } as any,
        quotedMsg,
        instance.engine,
      )
    } else {
      // Texto simples (com suporte a @mentions)
      const msgContent: any = { text: text ?? '' }
      if (mentions?.length) msgContent.mentions = mentions
      const sent = await sock.sendMessage(resolvedJid, msgContent, quotedMsg ? { quoted: quotedMsg } : undefined)
      sentMessageId = sent?.key?.id ?? `text-${Date.now()}`
    }

    const sent = { key: { id: sentMessageId } }

    // ── Persiste chat e mensagem imediatamente ────────────────────────────────
    // O Baileys dispara messages.upsert com type='append' para mensagens enviadas
    // por nós, o que não emite eventos Socket.IO. Por isso fazemos a criação aqui
    // e emitimos os eventos manualmente para o frontend atualizar em tempo real.

    const isGroup = resolvedJid.endsWith('@g.us')

    // Garantir que o Contato existe no banco
    const contact = await prisma.contact.upsert({
      where: { tenantId_instanceId_jid: { tenantId, instanceId, jid: resolvedJid } },
      create: {
        tenantId,
        instanceId,
        jid: resolvedJid,
        phone: isGroup ? null : resolvedJid.split('@')[0],
      },
      update: {},
    })

    const chat = await prisma.chat.upsert({
      where: { tenantId_instanceId_jid: { tenantId, instanceId, jid: resolvedJid } },
      create: { tenantId, instanceId, jid: resolvedJid, contactId: contact.id, lastMessageAt: new Date() },
      update: { contactId: contact.id, lastMessageAt: new Date() },
    })

    const messageId = sent?.key.id ?? `local-${Date.now()}`

    // Determina tipo, body e richPayload para persistência
    let msgType: 'TEXT' | 'POLL' | 'BUTTONS' | 'LIST' | 'CAROUSEL' | 'INTERACTIVE' = 'TEXT'
    let msgBody = text ?? ''
    let richPayloadData: Record<string, unknown> | undefined

    if (poll) {
      msgType = 'POLL'
      msgBody = poll.name
      richPayloadData = { poll }
    } else if (nativeButtons) {
      // Detecta se há botões CTA (url/copy/call) → INTERACTIVE, senão → BUTTONS
      const hasCta = nativeButtons.some((b: any) => ['url','copy','call'].includes(b.type))
      msgType = hasCta ? 'INTERACTIVE' : 'BUTTONS'
      msgBody = text ?? ''
      richPayloadData = { text, footer, nativeButtons }
    } else if (nativeList) {
      msgType = 'LIST'
      msgBody = text ?? nativeList.buttonText
      richPayloadData = { text, footer, nativeList }
    } else if (nativeCarousel) {
      msgType = 'CAROUSEL'
      msgBody = text ?? nativeCarousel.cards[0]?.title ?? '[Carrossel]'
      richPayloadData = { text, footer, nativeCarousel }
    }

    const msg = await prisma.message.upsert({
      where: { instanceId_messageId: { instanceId, messageId } },
      create: {
        tenantId,
        instanceId,
        chatId: chat.id,
        remoteJid: resolvedJid,
        messageId,
        fromMe: true,
        type: msgType,
        body: msgBody,
        richPayload: richPayloadData ? JSON.parse(JSON.stringify(richPayloadData)) : undefined,
        status: 'SENT',
        timestamp: new Date(),
      },
      update: {}, // já existe (eco do Baileys chegou primeiro) — não sobrescreve
    })

    // ── Emite eventos Socket.IO para o frontend atualizar em tempo real ───────
    const io = manager.getIO()

    // Inclui replyTo quando é uma resposta — necessário para exibir a caixa
    // de citação em sessões adicionais do mesmo tenant que tenham o chat aberto.
    const msgToEmit = replyToMessageId
      ? await prisma.message.findUnique({
          where: { id: msg.id },
          include: {
            replyTo: {
              select: { id: true, messageId: true, body: true, fromMe: true, type: true, mediaUrl: true },
            },
          },
        }) ?? msg
      : msg

    // Atualiza o chat aberto (se o usuário já estiver nele)
    io.to(`chat:${chat.id}`).emit('message:new', { ...msgToEmit, pushName: null, senderJid: null })

    const displayName = isGroup
      ? (chat.name ?? null)
      : (contact.name ?? contact.verifiedName ?? contact.notify ?? null)

    // Atualiza a lista de chats do tenant (novo chat ou reordenar pelo lastMessageAt)
    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: {
        name:          displayName,
        phone:         contact.phone,
        avatarUrl:     contact.avatarUrl,
        scoreReputacao: contact.scoreReputacao,
        flagRestricao:  contact.flagRestricao,
      },
      lastMessage: {
        body:      msgBody,
        fromMe:    true,
        status:    'SENT',
        type:      msgType,
        timestamp: msg.timestamp,
      },
    })

    res.status(201).json({ id: msg.id, messageId: msg.messageId })
  })

  // ═══════════════════════════════════════════════════════════════════════════
  //  GET /api/instances/:instanceId/groups/:groupJid/participants
  //  Lista participantes de um grupo (para autocomplete de @mentions)
  // ═══════════════════════════════════════════════════════════════════════════

  /**
   * Retorna a lista de participantes de um grupo WhatsApp.
   *
   * Usado pelo InputBar do frontend para autocomplete de @mentions.
   * Busca metadata via Baileys groupMetadata() e enriquece com dados do banco:
   *   - Nome do contato (se existir no banco)
   *   - Para JIDs @lid sem nome: busca pushName da última mensagem no grupo
   *   - Telefone formatado
   *
   * Response: { groupJid, subject, participants: [{ jid, admin, name, phone }] }
   */
  router.get('/groups/:groupJid/participants', async (req: Request, res: Response) => {
    const tenantId = req.tenantId!
    const instanceId = req.params['instanceId'] as string
    const groupJid = req.params['groupJid'] as string

    if (!groupJid?.endsWith('@g.us')) {
      res.status(400).json({ error: 'JID de grupo inválido' })
      return
    }

    const instance = await prisma.instance.findFirst({
      where: { id: instanceId, tenantId },
    })
    if (!instance || instance.status !== 'CONNECTED') {
      res.status(400).json({ error: 'Instância não conectada' })
      return
    }

    const sock = manager.getSocket(instanceId)
    if (!sock) {
      res.status(503).json({ error: 'Socket não disponível' })
      return
    }

    try {
      // Busca metadata do grupo via Baileys (inclui lista de participantes)
      const metadata = await sock.groupMetadata(groupJid)

      const participants = await Promise.all(
        metadata.participants.map(async (p) => {
          // Enriquece com dados do banco (nome, telefone)
          const contact = await prisma.contact.findFirst({
            where: { tenantId, instanceId, jid: p.id },
            select: { name: true, notify: true, phone: true },
          })

          let name = contact?.name || contact?.notify || null
          let phone = contact?.phone || null

          // Para @lid JIDs sem nome: tenta buscar pushName da última mensagem no grupo
          // (LID não tem telefone no JID, então o nome só pode vir de mensagens)
          if (!name && p.id.endsWith('@lid')) {
            const lastMsg = await prisma.message.findFirst({
              where: {
                instanceId,
                remoteJid: groupJid,
                senderJid: p.id,
                fromMe: false,
                pushName: { not: null },
              },
              orderBy: { timestamp: 'desc' },
              select: { pushName: true },
            })
            if (lastMsg?.pushName) name = lastMsg.pushName
          }

          // Extrai telefone do JID (apenas para @s.whatsapp.net, não @lid)
          if (!phone) {
            phone = p.id.endsWith('@lid') ? null : p.id.split('@')[0].split(':')[0]
          }

          return {
            jid: p.id,
            admin: p.admin ?? null,  // 'admin' | 'superadmin' | null
            name,
            phone,
          }
        })
      )
      res.json({ groupJid, subject: metadata.subject, participants })
    } catch (err) {
      logger.error({ err, groupJid }, '[Groups] Erro ao buscar participantes')
      res.status(500).json({ error: 'Erro ao buscar participantes do grupo' })
    }
  })

  // ═══════════════════════════════════════════════════════════════════════════
  //  POST /api/instances/:instanceId/reconcile-names
  //  Reconciliação bulk de nomes via pushName (endpoint HTTP)
  // ═══════════════════════════════════════════════════════════════════════════

  /**
   * Reconcilia nomes de contatos usando pushName das mensagens recebidas.
   *
   * Diferente de ContactHandler.reconcileNamesFromMessages() que roda no sync,
   * este endpoint pode ser chamado pelo frontend a qualquer momento (ex: page reload).
   *
   * Implementação:
   *   - Usa raw SQL com DISTINCT ON para buscar o pushName mais recente por JID
   *   - Uma única query UPDATE...FROM (subquery) atualiza todos os contatos sem nome
   *   - Cache DragonflyDB de 30min para evitar re-execução em loop
   *   - Idempotente: se já executou nos últimos 30min, retorna 204
   *
   * NOTA sobre tabelas: Prisma usa nomes como "Contact" e "Message" nos models,
   * mas as tabelas PostgreSQL reais são "contacts" e "messages" (via @@map).
   * No $executeRaw, SEMPRE usar os nomes reais das tabelas.
   */
  // Fallback in-memory para o TTL de reconciliação quando Redis está fora
  // Map<cacheKey, timestampMs do último reconcile>
  const reconcileMemCache = new Map<string, number>()
  const RECONCILE_TTL_MS = 30 * 60 * 1000 // 30 min

  router.post('/reconcile-names', async (req: Request, res: Response) => {
    const tenantId = req.tenantId!
    const instanceId = req.params['instanceId'] as string
    const cacheKey = `reconcile:${tenantId}:${instanceId}`

    // Camada 1: Redis (30min TTL)
    const cached = await dragonfly.get(cacheKey)
    if (cached) { res.status(204).end(); return }

    // Camada 2: in-memory (protege quando Redis está fora)
    const memTs = reconcileMemCache.get(cacheKey)
    if (memTs && Date.now() - memTs < RECONCILE_TTL_MS) {
      res.status(204).end()
      return
    }

    try {
      // Query bulk: encontra contatos sem nome que têm msgs recebidas com pushName.
      // Usa DISTINCT ON (PostgreSQL) para pegar o pushName mais recente por JID.
      // Uma única query UPDATE...FROM atualiza todos — O(1) round-trips ao banco.
      const updated = await prisma.$executeRaw`
        UPDATE contacts c
        SET
          "name"      = sub."pushName",
          "notify"    = COALESCE(c."notify", sub."pushName"),
          "updatedAt" = NOW()
        FROM (
          SELECT DISTINCT ON (m."remoteJid")
            m."remoteJid",
            m."pushName"
          FROM messages m
          WHERE m."instanceId" = ${instanceId}
            AND m."fromMe" = false
            AND m."pushName" IS NOT NULL
            AND m."pushName" != ''
          ORDER BY m."remoteJid", m."timestamp" DESC
        ) sub
        WHERE c."instanceId" = ${instanceId}
          AND c."tenantId"   = ${tenantId}
          AND c."jid"        = sub."remoteJid"
          AND c."name"       IS NULL
          AND c."notify"     IS NULL
      `

      // Seta flag no Redis (30min) + in-memory — independente do resultado
      await dragonfly.set(cacheKey, String(updated), 1800)
      reconcileMemCache.set(cacheKey, Date.now())

      if (updated > 0) {
        logger.info({ instanceId, updated }, '[Reconcile] Nomes atualizados via pushName')
        // Notifica frontend para re-buscar a lista de chats
        const io = manager.getIO()
        if (io) {
          io.to(`tenant:${tenantId}`).emit('chats:refresh', { instanceId, reason: 'reconcile', updated })
        }
      }

      res.json({ updated })
    } catch (err) {
      logger.error({ err, instanceId }, '[Reconcile] Erro na reconciliação bulk')
      // Mesmo com erro, seta cache curto (60s) para não ficar retentando em loop
      await dragonfly.set(cacheKey, '0', 60).catch(() => {})
      res.status(500).json({ error: 'Erro na reconciliação' })
    }
  })

  // ═══════════════════════════════════════════════════════════════════════════
  //  POST /api/instances/:instanceId/broadcast — Disparo em massa
  // ═══════════════════════════════════════════════════════════════════════════

  /**
   * Inicia um disparo em massa (broadcast) para até 500 números.
   *
   * Validação:
   *   - numbers: array de strings (mínimo 5 chars cada, 1-500 itens)
   *   - message: texto (1-4096 chars)
   *   - delayMs: delay entre envios (500-60000ms, default 2000ms)
   *
   * Fluxo:
   *   1. Valida payload via Zod
   *   2. Cria registro Broadcast com status 'RUNNING'
   *   3. Retorna 202 com jobId imediatamente
   *   4. Processa envios em background (async IIFE)
   *   5. Atualiza sentCount/failedCount a cada envio
   *   6. Marca como 'DONE' ao finalizar
   *
   * IMPORTANTE: O delay entre envios é obrigatório para evitar ban do WhatsApp.
   * O default de 2s é conservador — ajuste conforme volume e tolerância ao risco.
   */
  router.post('/broadcast', async (req: Request, res: Response) => {
    const schema = z.object({
      numbers:  z.array(z.string().min(5)).min(1).max(500),
      message:  z.string().min(1).max(4096),
      delayMs:  z.number().int().min(500).max(60000).default(2000),
    })

    let body: z.infer<typeof schema>
    try {
      body = schema.parse(req.body)
    } catch (err: any) {
      res.status(400).json({ error: err.errors ?? err.message }); return
    }

    const tenantId   = req.tenantId!
    const instanceId = req.params['instanceId'] as string

    const instance = await prisma.instance.findFirst({ where: { id: instanceId, tenantId } })
    if (!instance || instance.status !== 'CONNECTED') {
      res.status(400).json({ error: 'Instância não conectada' }); return
    }

    const sock = manager.getSocket(instanceId)
    if (!sock) {
      res.status(503).json({ error: 'Socket não disponível' }); return
    }

    // Cria registro do broadcast no banco
    const broadcast = await prisma.broadcast.create({
      data: {
        tenantId,
        instanceId,
        message: body.message,
        totalCount: body.numbers.length,
        sentCount: 0,
        failedCount: 0,
        status: 'RUNNING',
      },
    })

    // Retorna imediatamente (processamento assíncrono)
    res.status(202).json({ jobId: broadcast.id, total: body.numbers.length })

    // ── Processamento em background ─────────────────────────────────────
    ;(async () => {
      let sent = 0, failed = 0

      for (let i = 0; i < body.numbers.length; i++) {
        const rawNumber = body.numbers[i]!
        // Normaliza número: remove caracteres não-numéricos
        const number = rawNumber.replace(/\D/g, '')
        // Se já é um JID completo (contém @), usa como está; senão, adiciona sufixo
        const jid    = number.includes('@') ? number : `${number}@s.whatsapp.net`

        try {
          await sock.sendMessage(jid, { text: body.message })

          await prisma.broadcastRecipient.create({
            data: { broadcastId: broadcast.id, jid, status: 'SENT' },
          })
          sent++
        } catch (err) {
          logger.warn({ jid, err }, '[Broadcast] Falha ao enviar')
          await prisma.broadcastRecipient.create({
            data: { broadcastId: broadcast.id, jid, status: 'FAILED' },
          }).catch(() => {})
          failed++
        }

        // Atualiza contadores em tempo real (permite consulta de progresso)
        await prisma.broadcast.update({
          where: { id: broadcast.id },
          data: { sentCount: sent, failedCount: failed },
        })

        // Delay entre envios para evitar ban do WhatsApp (pula no último item)
        if (i < body.numbers.length - 1) {
          await new Promise(r => setTimeout(r, body.delayMs))
        }
      }

      // Marca como concluído
      await prisma.broadcast.update({
        where: { id: broadcast.id },
        data: { status: 'DONE', finishedAt: new Date() },
      })
    })().catch(err => {
      logger.error({ err }, '[Broadcast] Erro no job')
      prisma.broadcast.update({ where: { id: broadcast.id }, data: { status: 'FAILED' } }).catch(() => {})
    })
  })

  // ═══════════════════════════════════════════════════════════════════════════
  //  GET /api/instances/:instanceId/broadcast — Lista broadcasts
  // ═══════════════════════════════════════════════════════════════════════════

  /** Lista os últimos broadcasts da instância (paginado, max 50) */
  router.get('/broadcast', async (req: Request, res: Response) => {
    const tenantId   = req.tenantId!
    const instanceId = req.params['instanceId'] as string
    const limit = Math.min(Number(req.query['limit'] ?? 20), 50)

    const rows = await prisma.broadcast.findMany({
      where:   { tenantId, instanceId },
      orderBy: { createdAt: 'desc' },
      take:    limit,
      include: { _count: { select: { recipients: true } } },
    })
    res.json(rows)
  })

  // ═══════════════════════════════════════════════════════════════════════════
  //  GET /api/instances/:instanceId/broadcast/:id — Detalhe de broadcast
  // ═══════════════════════════════════════════════════════════════════════════

  /** Retorna detalhe de um broadcast específico, incluindo lista de destinatários */
  router.get('/broadcast/:broadcastId', async (req: Request, res: Response) => {
    const tenantId    = req.tenantId!
    const broadcastId = req.params['broadcastId'] as string

    const row = await prisma.broadcast.findFirst({
      where:   { id: broadcastId, tenantId },
      include: { recipients: { orderBy: { createdAt: 'asc' } } },
    })
    if (!row) { res.status(404).json({ error: 'Não encontrado' }); return }
    res.json(row)
  })

  return router
}

// ═══════════════════════════════════════════════════════════════════════════════
//  ROTA DE AVATAR — Proxy de foto de perfil (evita CORS)
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Constrói a rota de proxy de avatar.
 *
 * Montada em: /api/inbox/avatar/:jid
 *
 * O frontend não pode buscar avatares diretamente do CDN do WhatsApp (CORS).
 * Esta rota age como proxy:
 *   1. Busca avatarUrl salvo no banco (cache DB)
 *   2. Se URL da DB expirou, busca via Baileys profilePictureUrl
 *   3. Faz proxy do buffer (download → response) para evitar redirect/CORS
 *   4. Salva URL nova no banco para próximas requisições
 *
 * Retorna 204 (sem conteúdo) quando avatar não disponível:
 *   - Foto de perfil privada
 *   - JID @lid (não suporta profilePictureUrl)
 *   - Socket não disponível
 *   O frontend trata 204 mostrando iniciais em vez da foto.
 *
 * Cache HTTP: max-age=86400 (24h) + stale-while-revalidate=604800 (7d) para
 * avatares encontrados; 300s para 204. Valida revalidação via ETag.
 *
 * Negative cache: quando decide 204, grava `avatar:none:*` no Dragonfly
 * com TTL de 1h — próximos hits respondem sem tocar no Prisma nem no Baileys.
 */
export function buildAvatarRoute(manager: WhatsAppConnectionManager): Router {
  const router = Router()

  // Cabeçalho longo com stale-while-revalidate: o browser serve do cache
  // local por 24h e, entre 24h e 7d, usa o stale enquanto revalida em
  // background. Corta requisições repetidas na navegação normal.
  const HIT_CACHE_CONTROL = 'public, max-age=86400, stale-while-revalidate=604800'
  const MISS_CACHE_CONTROL = 'public, max-age=300'
  const NO_AVATAR_TTL_SECONDS = 3600 // 1h

  const noAvatarKey = (tenantId: string, instanceId: string, jid: string) =>
    `avatar:none:${tenantId}:${instanceId}:${jid}`

  // ETag baseado no avatarUrl conhecido. É determinístico (hash da string),
  // logo quando a URL do banco muda — sinal de que a foto mudou — o ETag
  // também muda e força o browser a buscar bytes novos.
  const etagFor = (urlOrMarker: string) =>
    `"${createHash('md5').update(urlOrMarker).digest('hex')}"`

  router.get('/avatar/:jid', async (req: Request, res: Response) => {
    const jid = req.params['jid'] as string
    const instanceId = req.query['instance'] as string | undefined
    const t0 = Date.now()
    const tenantId = req.tenantId!

    // Helper: retorna 204 No Content (não 404) para que o browser não logue
    // erro no console. O <img onError> do frontend detecta a falta de imagem
    // e mostra iniciais. Também grava no negative cache para curto-circuitar
    // as próximas chamadas ao mesmo JID.
    const noAvatar = (persist = true) => {
      if (persist && instanceId) {
        dragonfly
          .set(noAvatarKey(tenantId, instanceId, jid), '1', NO_AVATAR_TTL_SECONDS)
          .catch(() => {})
      }
      res.set('Cache-Control', MISS_CACHE_CONTROL).status(204).end()
    }

    if (!instanceId) {
      res.set('Cache-Control', MISS_CACHE_CONTROL).status(204).end()
      return
    }

    // 0. Negative cache: se sabemos que não tem avatar há menos de 1h, corta
    // antes de tocar Prisma/Baileys (o backend está sob pressão de memória).
    try {
      if (await dragonfly.exists(noAvatarKey(tenantId, instanceId, jid))) {
        res.set('Cache-Control', MISS_CACHE_CONTROL).status(204).end()
        logger.debug({ jid, ms: Date.now() - t0, source: 'neg_cache' }, '[Avatar] 204 cached')
        return
      }
    } catch {
      // Dragonfly indisponível — segue o fluxo normal sem quebrar
    }

    // 1. Tenta avatarUrl já salvo no banco (funciona para todos os tipos de JID)
    const contact = await prisma.contact.findFirst({
      where: { tenantId, instanceId, jid },
      select: { avatarUrl: true },
    })

    if (contact?.avatarUrl) {
      // Revalidação condicional: se o browser mandou If-None-Match e bate,
      // responde 304 sem baixar do CDN. Reduz o custo em rehit a ~zero.
      const etag = etagFor(contact.avatarUrl)
      if (req.headers['if-none-match'] === etag) {
        res.set('ETag', etag)
        res.set('Cache-Control', HIT_CACHE_CONTROL)
        res.status(304).end()
        logger.debug({ jid, ms: Date.now() - t0, source: 'etag_304' }, '[Avatar] 304')
        return
      }

      try {
        // Busca imagem do CDN do WhatsApp (URL temporária, expira em ~24h)
        const upstream = await fetch(contact.avatarUrl)
        if (upstream.ok) {
          const buffer = Buffer.from(await upstream.arrayBuffer())
          res.set('Content-Type', upstream.headers.get('content-type') || 'image/jpeg')
          res.set('Cache-Control', HIT_CACHE_CONTROL)
          res.set('ETag', etag)
          res.set('Content-Length', String(buffer.length))
          res.send(buffer)
          logger.debug({ jid, ms: Date.now() - t0, source: 'db_cache' }, '[Avatar] served')
          return
        }
        // URL da DB expirou — tenta via Baileys abaixo
        logger.debug({ jid, ms: Date.now() - t0 }, '[Avatar] DB URL expirada, fallback Baileys')
      } catch {
        // URL da DB expirou ou erro de rede — tenta via Baileys abaixo
      }
    }

    // 2. JIDs @lid não suportam profilePictureUrl no Baileys (retorna 404 no WA)
    if (jid.endsWith('@lid')) { noAvatar(); return }

    // 3. Busca URL fresca via Baileys profilePictureUrl
    const sock = manager.getSocket(instanceId)
    if (!sock) { noAvatar(); return }

    try {
      const url = await sock.profilePictureUrl(jid, 'image')
      if (!url) { noAvatar(); return }

      // Faz proxy do buffer — evita CORS ao redirecionar para o CDN do WhatsApp
      const upstream = await fetch(url)
      if (!upstream.ok) { noAvatar(); return }

      const buffer = Buffer.from(await upstream.arrayBuffer())
      res.set('Content-Type', upstream.headers.get('content-type') || 'image/jpeg')
      res.set('Cache-Control', HIT_CACHE_CONTROL)
      res.set('ETag', etagFor(url))
      res.set('Content-Length', String(buffer.length))
      res.send(buffer)
      logger.debug({ jid, ms: Date.now() - t0, source: 'baileys' }, '[Avatar] served')

      // Salva URL nova no banco para próximas requisições (fire-and-forget).
      // Também invalida o negative cache, caso exista: agora temos foto.
      await prisma.contact.updateMany({
        where: { tenantId, instanceId, jid },
        data: { avatarUrl: url },
      }).catch(() => {})
      dragonfly.del(noAvatarKey(tenantId, instanceId, jid)).catch(() => {})
    } catch {
      noAvatar()
    }
  })

  return router
}
