'use strict'

/**
 * media-transcribe.js — transcreve áudio (PTT) e descreve imagens recebidas
 *                       via WhatsApp usando Gemini 1.5 Flash (multimodal nativo).
 *
 * Fluxo:
 *   1. webhook-receiver detecta msg.type === 'AUDIO' ou 'IMAGE'
 *   2. Baixa o blob via /api/ext/v1/media/:messageId no motor (com retry,
 *      pois o motor baixa a mídia de forma assíncrona após emitir o webhook).
 *   3. Envia base64 + mime para Gemini Flash com prompt curto.
 *   4. Retorna texto pronto para entrar no fluxo de auto-reply normal.
 *
 * Custo: Gemini 1.5 Flash é ~10× mais barato que GPT-4 e processa áudio/imagem
 * nativamente sem pipeline de Whisper + OCR. Fallback para OpenAI Whisper só
 * se a chamada Gemini falhar (rede, quota, etc).
 */

const GEMINI_MODEL  = 'gemini-2.0-flash'
const GEMINI_API    = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`

const OPENAI_WHISPER_MODEL = 'whisper-1'
const OPENAI_API_BASE = 'https://api.openai.com/v1'

const DOWNLOAD_RETRIES = 4         // motor baixa mídia async — espera até ~6s
const DOWNLOAD_BACKOFF = [500, 1000, 2000, 3000]

const MAX_AUDIO_SECONDS = 300      // 5min — áudios maiores rejeita
const MAX_IMAGE_BYTES   = 8 * 1024 * 1024  // 8MB
const MAX_PDF_BYTES     = 20 * 1024 * 1024 // 20MB — limite inline do Gemini
const MAX_VIDEO_BYTES   = 18 * 1024 * 1024 // 18MB — Gemini inline limit prático

// ── Cache Redis para extrações ────────────────────────────────────────────────
// Evita reprocessar a mesma mídia (msg.id) se motor re-entregar o webhook.
// Chave: nw:media:<msg.id>; TTL: 24h. Falha silenciosa se Redis indisponível.

const CACHE_TTL_SECONDS = 86_400

let _redisClient = null
function getRedis() {
  if (_redisClient) return _redisClient
  try {
    const Redis = require('ioredis')
    _redisClient = new Redis({
      host: process.env.REDIS_HOST || '127.0.0.1',
      port: parseInt(process.env.REDIS_PORT || '6379', 10),
      maxRetriesPerRequest: 1,
      enableOfflineQueue: false,
      lazyConnect: false,
    })
    _redisClient.on('error', () => { /* silencia spam — operação cai sem cache */ })
  } catch { _redisClient = null }
  return _redisClient
}

async function cacheGet(key) {
  try {
    const r = getRedis()
    if (!r) return null
    const v = await r.get(`nw:media:${key}`)
    return v || null
  } catch { return null }
}

async function cacheSet(key, value) {
  try {
    const r = getRedis()
    if (!r || !value) return
    await r.set(`nw:media:${key}`, value, 'EX', CACHE_TTL_SECONDS)
  } catch { /* silencioso */ }
}

// ── Download da mídia do motor ────────────────────────────────────────────────

async function fetchMediaBuffer(motorUrl, integKey, messageId) {
  for (let i = 0; i < DOWNLOAD_RETRIES; i++) {
    try {
      const res = await fetch(`${motorUrl}/api/ext/v1/media/${encodeURIComponent(messageId)}`, {
        headers: { 'x-nw-key': integKey },
        redirect: 'follow',
      })
      if (res.status === 404 && i < DOWNLOAD_RETRIES - 1) {
        // motor ainda não baixou — backoff e tenta de novo
        await new Promise(r => setTimeout(r, DOWNLOAD_BACKOFF[i]))
        continue
      }
      if (!res.ok) throw new Error(`motor /media respondeu ${res.status}`)
      const mime = res.headers.get('content-type') || 'application/octet-stream'
      const buf  = Buffer.from(await res.arrayBuffer())
      return { buffer: buf, mime }
    } catch (err) {
      if (i === DOWNLOAD_RETRIES - 1) throw err
      await new Promise(r => setTimeout(r, DOWNLOAD_BACKOFF[i]))
    }
  }
  throw new Error('Mídia indisponível após retries')
}

// ── Chamada Gemini ────────────────────────────────────────────────────────────

async function callGemini(prompt, inlineData) {
  const apiKey = process.env.GEMINI_API_KEY
  if (!apiKey) throw new Error('GEMINI_API_KEY não configurado')

  const body = {
    contents: [{
      role: 'user',
      parts: [
        { text: prompt },
        { inline_data: { mime_type: inlineData.mime, data: inlineData.base64 } },
      ],
    }],
    generationConfig: { temperature: 0.2, maxOutputTokens: 512 },
  }

  const res = await fetch(`${GEMINI_API}?key=${apiKey}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })

  if (!res.ok) {
    const txt = await res.text().catch(() => '')
    throw new Error(`Gemini ${res.status}: ${txt.slice(0, 200)}`)
  }
  const data = await res.json()
  const text = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim()
  if (!text) throw new Error('Gemini retornou resposta vazia')
  return text
}

// ── Transcrição de áudio ──────────────────────────────────────────────────────

// ── Transcrição via OpenAI Whisper (purpose-built, $0.006/min) ────────────────

async function transcribeWithWhisper(buffer, mime) {
  const apiKey = process.env.OPENAI_API_KEY
  if (!apiKey) throw new Error('OPENAI_API_KEY não configurado')

  // FormData multipart (multipart/form-data com Buffer Blob)
  const ext  = mime?.includes('mp3') ? 'mp3' : mime?.includes('wav') ? 'wav' : 'ogg'
  const blob = new Blob([buffer], { type: mime || 'audio/ogg' })
  const fd   = new FormData()
  fd.append('file', blob, `audio.${ext}`)
  fd.append('model', OPENAI_WHISPER_MODEL)
  fd.append('language', 'pt')
  fd.append('response_format', 'text')

  const res = await fetch(`${OPENAI_API_BASE}/audio/transcriptions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${apiKey}` },
    body: fd,
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => '')
    throw new Error(`Whisper ${res.status}: ${txt.slice(0, 200)}`)
  }
  const text = (await res.text()).trim()
  if (!text) throw new Error('Whisper devolveu vazio')
  return text
}

/**
 * Transcreve um áudio (PTT) recebido via WhatsApp.
 * Tenta Whisper (mais confiável e barato para áudio) e cai para Gemini se falhar.
 * @returns {Promise<string|null>} texto transcrito ou null em caso de falha
 */
async function transcribeAudio({ motorUrl, integKey, messageId }) {
  try {
    const cached = await cacheGet(`audio:${messageId}`)
    if (cached) {
      console.log(`[transcribe] ⚡ cache hit (${cached.length} chars)`)
      return cached
    }
    const { buffer, mime } = await fetchMediaBuffer(motorUrl, integKey, messageId)

    if (buffer.length > 16 * 1024 * 1024) {
      console.warn('[transcribe] áudio muito grande, ignorando:', buffer.length)
      return null
    }

    // 1ª tentativa: Whisper
    try {
      const text = await transcribeWithWhisper(buffer, mime)
      console.log(`[transcribe] ✅ Whisper transcribed ${buffer.length}B → ${text.length} chars`)
      cacheSet(`audio:${messageId}`, text)
      return text
    } catch (e) {
      console.warn('[transcribe] Whisper falhou, tentando Gemini:', e.message)
    }

    // 2ª tentativa: Gemini Flash (multimodal)
    const prompt =
      'Transcreva fielmente o áudio em português brasileiro. ' +
      'Devolva APENAS o texto transcrito, sem comentários, sem timestamps e sem prefixo. ' +
      'Se o áudio estiver vazio, com muito ruído ou inaudível, devolva exatamente: [inaudível]'

    const text = await callGemini(prompt, {
      mime: mime?.startsWith('audio/') ? mime : 'audio/ogg',
      base64: buffer.toString('base64'),
    })

    if (/^\[inaudível\]/i.test(text)) return null
    console.log(`[transcribe] ✅ Gemini transcribed → ${text.length} chars`)
    cacheSet(`audio:${messageId}`, text)
    return text
  } catch (err) {
    console.warn('[transcribe-audio]', err.message)
    return null
  }
}

// ── Descrição de imagem ───────────────────────────────────────────────────────

// ── Vision via OpenAI gpt-4o-mini ($0.15/1M input tokens, suporta imagem) ────

async function describeWithOpenAI(buffer, mime, captionFromUser) {
  const apiKey = process.env.OPENAI_API_KEY
  if (!apiKey) throw new Error('OPENAI_API_KEY não configurado')

  const dataUrl = `data:${mime || 'image/jpeg'};base64,${buffer.toString('base64')}`
  const userText =
    'Analise a imagem enviada por um cliente de uma loja de conveniência via WhatsApp. ' +
    'Classifique em UMA destas categorias e descreva em uma frase curta:\n' +
    '- COMPROVANTE_PIX: comprovantes de pagamento PIX/boleto/transferência. Extraia valor e destinatário se visíveis.\n' +
    '- RECEITA_MEDICA: receitas médicas/odontológicas. Extraia medicamento, médico/CRM se legíveis.\n' +
    '- PRODUTO: foto de produto (alimento, bebida, item de mercado). Identifique marca/produto.\n' +
    '- DOCUMENTO: RG, CPF, CNH, conta de luz/água, contrato.\n' +
    '- OUTRO: qualquer outra coisa.\n\n' +
    (captionFromUser ? `Legenda do cliente: "${captionFromUser}"\n\n` : '') +
    'Devolva no máximo UMA linha no formato exato: TIPO: descrição. Sem markdown, sem comentários.'

  const body = {
    model: 'gpt-4o-mini',
    messages: [{
      role: 'user',
      content: [
        { type: 'text', text: userText },
        { type: 'image_url', image_url: { url: dataUrl } },
      ],
    }],
    max_tokens: 200,
    temperature: 0.2,
  }

  const res = await fetch(`${OPENAI_API_BASE}/chat/completions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => '')
    throw new Error(`OpenAI vision ${res.status}: ${txt.slice(0, 200)}`)
  }
  const data = await res.json()
  const text = data?.choices?.[0]?.message?.content?.trim()
  if (!text) throw new Error('OpenAI vision devolveu vazio')
  return text
}

/**
 * Analisa uma imagem recebida e retorna uma descrição curta + classificação
 * útil para o LLM entender a intenção (comprovante, receita, foto de produto…).
 *
 * Tenta Gemini Flash (multimodal nativo) primeiro; se falhar (quota/rede),
 * cai para OpenAI gpt-4o-mini vision.
 *
 * @returns {Promise<string|null>} texto pronto para entrar no fluxo
 *   formato: "[imagem] TIPO: descrição curta"
 */
async function describeImage({ motorUrl, integKey, messageId, captionFromUser }) {
  try {
    const cached = await cacheGet(`image:${messageId}`)
    if (cached) {
      console.log(`[describe-image] ⚡ cache hit (${cached.length} chars)`)
      return cached
    }
    const { buffer, mime } = await fetchMediaBuffer(motorUrl, integKey, messageId)
    if (buffer.length > MAX_IMAGE_BYTES) {
      console.warn('[describe-image] imagem muito grande:', buffer.length)
      return null
    }

    const prompt =
      'Analise a imagem enviada por um cliente de uma loja de conveniência via WhatsApp. ' +
      'Classifique em UMA destas categorias e descreva em uma frase curta:\n' +
      '- COMPROVANTE_PIX: prints/fotos de comprovante de pagamento PIX/boleto/transferência. Extraia valor e nome do destinatário se visível.\n' +
      '- RECEITA_MEDICA: receitas médicas/odontológicas. Extraia nome do medicamento, médico/CRM se legível.\n' +
      '- PRODUTO: foto de um produto (alimento, bebida, item de mercado). Identifique marca/produto.\n' +
      '- DOCUMENTO: RG, CPF, CNH, conta de luz/água, contrato.\n' +
      '- OUTRO: qualquer outra coisa.\n\n' +
      (captionFromUser ? `O cliente escreveu junto com a imagem: "${captionFromUser}"\n\n` : '') +
      'Devolva NO MÁXIMO uma linha no formato exato: TIPO: descrição curta. ' +
      'Não use markdown. Não comente.'

    // 1ª tentativa: Gemini (mais barato em alta escala se quota disponível)
    try {
      const text = await callGemini(prompt, {
        mime: mime?.startsWith('image/') ? mime : 'image/jpeg',
        base64: buffer.toString('base64'),
      })
      console.log(`[describe-image] ✅ Gemini → ${text.length} chars`)
      const result = `[imagem] ${text}`
      cacheSet(`image:${messageId}`, result)
      return result
    } catch (e) {
      console.warn('[describe-image] Gemini falhou, caindo para OpenAI:', e.message)
    }

    // 2ª tentativa: OpenAI Vision
    const text = await describeWithOpenAI(buffer, mime, captionFromUser)
    console.log(`[describe-image] ✅ OpenAI → ${text.length} chars`)
    const result = `[imagem] ${text}`
    cacheSet(`image:${messageId}`, result)
    return result
  } catch (err) {
    console.warn('[describe-image]', err.message)
    return null
  }
}

// ── Extração de texto de PDF ──────────────────────────────────────────────────

const PDF_PROMPT = (captionFromUser) =>
  'Extraia o conteúdo deste documento PDF enviado por um cliente via WhatsApp. ' +
  'Identifique o tipo do documento (cupom fiscal, nota fiscal, comprovante de pagamento, ' +
  'contrato, boleto, pedido, etc.) e extraia as informações mais relevantes como: ' +
  'número do pedido/protocolo, valores, datas, produtos listados, nome do cliente/empresa, ' +
  'status do pedido, CNPJ/CPF e qualquer instrução ao cliente. ' +
  (captionFromUser ? `O cliente escreveu junto com o arquivo: "${captionFromUser}"\n\n` : '') +
  'Devolva em texto corrido objetivo, máximo 3 parágrafos. Sem markdown, sem comentários.'

async function extractPdfWithOpenAI(buffer, captionFromUser) {
  const apiKey = process.env.OPENAI_API_KEY
  if (!apiKey) throw new Error('OPENAI_API_KEY não configurado')

  const body = {
    model: 'gpt-4o-mini',
    messages: [{
      role: 'user',
      content: [
        {
          type: 'file',
          file: {
            filename: 'document.pdf',
            file_data: `data:application/pdf;base64,${buffer.toString('base64')}`,
          },
        },
        { type: 'text', text: PDF_PROMPT(captionFromUser) },
      ],
    }],
    max_tokens: 1024,
    temperature: 0.2,
  }

  const res = await fetch(`${OPENAI_API_BASE}/chat/completions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => '')
    throw new Error(`OpenAI PDF ${res.status}: ${txt.slice(0, 200)}`)
  }
  const data = await res.json()
  const text = data?.choices?.[0]?.message?.content?.trim()
  if (!text) throw new Error('OpenAI devolveu vazio')
  return text
}

async function extractPdfWithClaude(buffer, captionFromUser) {
  const apiKey = process.env.ANTHROPIC_API_KEY
  if (!apiKey) throw new Error('ANTHROPIC_API_KEY não configurado')

  const body = {
    model: 'claude-haiku-4-5-20251001',
    max_tokens: 1024,
    messages: [{
      role: 'user',
      content: [
        {
          type: 'document',
          source: {
            type: 'base64',
            media_type: 'application/pdf',
            data: buffer.toString('base64'),
          },
        },
        { type: 'text', text: PDF_PROMPT(captionFromUser) },
      ],
    }],
  }

  const res = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => '')
    throw new Error(`Anthropic PDF ${res.status}: ${txt.slice(0, 200)}`)
  }
  const data = await res.json()
  const text = data?.content?.[0]?.text?.trim()
  if (!text) throw new Error('Anthropic devolveu vazio')
  return text
}

/**
 * Extrai e resume o conteúdo de um PDF recebido via WhatsApp.
 * Cadeia de fallback: Gemini 2.0 Flash → OpenAI gpt-4o-mini → Claude Haiku.
 *
 * @returns {Promise<string|null>} texto extraído prefixado com "[documento PDF]" ou null
 */
async function extractPdfText({ motorUrl, integKey, messageId, captionFromUser }) {
  try {
    const cached = await cacheGet(`pdf:${messageId}`)
    if (cached) {
      console.log(`[extract-pdf] ⚡ cache hit (${cached.length} chars)`)
      return cached
    }
    const { buffer, mime } = await fetchMediaBuffer(motorUrl, integKey, messageId)

    // Aceita por mime OU por magic bytes (%PDF) — motor às vezes serve
    // documentos como application/octet-stream sem distinguir o tipo real.
    const isPdfMime  = mime?.toLowerCase().includes('pdf')
    const isPdfMagic = buffer.length >= 4 && buffer.slice(0, 4).toString('ascii') === '%PDF'
    if (!isPdfMime && !isPdfMagic) {
      console.log('[extract-pdf] não é PDF — mime:', mime, 'magic:', buffer.slice(0, 4).toString('hex'))
      return null
    }
    if (buffer.length > MAX_PDF_BYTES) {
      console.warn('[extract-pdf] PDF muito grande para inline:', buffer.length)
      return null
    }

    const base64 = buffer.toString('base64')

    // 1ª tentativa: Gemini Flash (mais barato)
    try {
      const text = await callGemini(PDF_PROMPT(captionFromUser), {
        mime: 'application/pdf',
        base64,
      })
      console.log(`[extract-pdf] ✅ Gemini extraiu ${text.length} chars (${buffer.length}B)`)
      const result = `[documento PDF] ${text}`
      cacheSet(`pdf:${messageId}`, result)
      return result
    } catch (e) {
      console.warn('[extract-pdf] Gemini falhou, tentando OpenAI:', e.message)
    }

    // 2ª tentativa: OpenAI gpt-4o-mini
    try {
      const text = await extractPdfWithOpenAI(buffer, captionFromUser)
      console.log(`[extract-pdf] ✅ OpenAI extraiu ${text.length} chars`)
      const result = `[documento PDF] ${text}`
      cacheSet(`pdf:${messageId}`, result)
      return result
    } catch (e) {
      console.warn('[extract-pdf] OpenAI falhou, tentando Anthropic:', e.message)
    }

    // 3ª tentativa: Claude Haiku
    const text = await extractPdfWithClaude(buffer, captionFromUser)
    console.log(`[extract-pdf] ✅ Anthropic extraiu ${text.length} chars`)
    const result = `[documento PDF] ${text}`
    cacheSet(`pdf:${messageId}`, result)
    return result

  } catch (err) {
    console.warn('[extract-pdf]', err.message)
    return null
  }
}

// ── Vídeo (Gemini nativo, sem ffmpeg) ────────────────────────────────────────

/**
 * Processa um vídeo enviado via WhatsApp. Gemini 2.0 Flash aceita video/mp4
 * (e variantes) como inline_data nativamente, descrevendo cena + áudio juntos.
 * Para vídeos > 18MB retorna null (motor poderia subir via File API; fora do escopo).
 *
 * @returns {Promise<string|null>} texto prefixado com "[vídeo]" ou null
 */
async function processVideo({ motorUrl, integKey, messageId, captionFromUser }) {
  try {
    const cached = await cacheGet(`video:${messageId}`)
    if (cached) {
      console.log(`[process-video] ⚡ cache hit (${cached.length} chars)`)
      return cached
    }

    const { buffer, mime } = await fetchMediaBuffer(motorUrl, integKey, messageId)
    if (buffer.length > MAX_VIDEO_BYTES) {
      console.warn('[process-video] vídeo muito grande para inline:', buffer.length)
      return null
    }

    const prompt =
      'Analise este vídeo enviado por um cliente de loja de conveniência via WhatsApp. ' +
      'Descreva em UMA frase curta o que está acontecendo na cena (foco em produto, problema, ' +
      'comprovante ou ambiente). Se houver fala/áudio relevante, transcreva o conteúdo principal. ' +
      'Classifique em UMA categoria: COMPROVANTE, PRODUTO_DEFEITO, PRODUTO_DUVIDA, RECLAMACAO, OUTRO. ' +
      (captionFromUser ? `Legenda do cliente: "${captionFromUser}"\n\n` : '') +
      'Devolva no formato exato: TIPO: descrição (+ fala transcrita se houver). Sem markdown.'

    const text = await callGemini(prompt, {
      mime: mime?.startsWith('video/') ? mime : 'video/mp4',
      base64: buffer.toString('base64'),
    })

    console.log(`[process-video] ✅ Gemini → ${text.length} chars (${buffer.length}B)`)
    const result = `[vídeo] ${text}`
    cacheSet(`video:${messageId}`, result)
    return result
  } catch (err) {
    console.warn('[process-video]', err.message)
    return null
  }
}

// ── vCard / Contato (parsing local, sem LLM) ─────────────────────────────────

/**
 * Faz parsing de um vCard (cartão de contato) compartilhado via WhatsApp.
 * O motor pode entregar o vCard cru no msg.body OU em msg.vcard. Aceita ambos.
 *
 * @param {{ rawVcard?: string, msgBody?: string }} input
 * @returns {string|null} texto formatado prefixado com "[contato]" ou null
 */
function extractVCard({ rawVcard, msgBody } = {}) {
  const text = (rawVcard || msgBody || '').trim()
  if (!text || !/BEGIN:VCARD/i.test(text)) return null

  const pick = (re) => {
    const m = text.match(re)
    return m ? m[1].trim() : null
  }

  const fn   = pick(/^FN[^:]*:(.+)$/im)
  const n    = pick(/^N[^:]*:(.+)$/im)
  const tels = [...text.matchAll(/^TEL[^:]*:(\+?[\d\s().-]+)$/gim)]
                  .map(m => m[1].replace(/\D/g, '')).filter(Boolean)
  const emails = [...text.matchAll(/^EMAIL[^:]*:(\S+@\S+)$/gim)].map(m => m[1])
  const org  = pick(/^ORG[^:]*:(.+)$/im)

  const nome = fn || (n ? n.split(';').filter(Boolean).reverse().join(' ').trim() : null)
  if (!nome && tels.length === 0) return null

  const partes = []
  if (nome) partes.push(`nome: ${nome}`)
  if (org)  partes.push(`empresa: ${org}`)
  if (tels.length > 0)   partes.push(`tel: ${tels.join(', ')}`)
  if (emails.length > 0) partes.push(`email: ${emails.join(', ')}`)

  return `[contato] cliente compartilhou contato — ${partes.join(' | ')}`
}

// ── Detecção automática de intent a partir do conteúdo de mídia ──────────────

/**
 * Mapeia o `effectiveBody` (com prefixos [imagem]/[vídeo]/[documento PDF]/[contato])
 * para tags de intent compreendidas pelo brain (sector_brain_nodes.tags).
 * Reduz dependência do LLM detectar intenção sozinho — economiza tokens e
 * melhora roteamento de setor.
 *
 * @param {string} effectiveBody
 * @returns {string[]} array de tags (vazio se nenhuma reconhecida)
 */
function detectMediaIntent(effectiveBody) {
  if (!effectiveBody) return []
  const tags = new Set()
  const t = effectiveBody.toLowerCase()

  // Imagem
  if (/\[imagem\]\s*comprovante_pix/i.test(effectiveBody)) tags.add('pagamento')
  if (/\[imagem\]\s*receita_medica/i.test(effectiveBody))  tags.add('saude')
  if (/\[imagem\]\s*produto\b/i.test(effectiveBody))       tags.add('produto')
  if (/\[imagem\]\s*documento\b/i.test(effectiveBody))     tags.add('documento')

  // Vídeo
  if (/\[v[íi]deo\]\s*comprovante\b/i.test(effectiveBody))      tags.add('pagamento')
  if (/\[v[íi]deo\]\s*produto_defeito/i.test(effectiveBody))    { tags.add('reclamacao'); tags.add('produto') }
  if (/\[v[íi]deo\]\s*produto_duvida/i.test(effectiveBody))     tags.add('produto')
  if (/\[v[íi]deo\]\s*reclamacao/i.test(effectiveBody))         tags.add('reclamacao')

  // Contato
  if (/^\[contato\]/.test(effectiveBody)) tags.add('contato_compartilhado')

  // PDF — heurística por palavras-chave no texto extraído
  if (/^\[documento pdf\]/i.test(effectiveBody)) {
    if (/\b(boleto|2[º°]\s*via|c[óo]digo\s+de\s+barras|linha\s+digit[áa]vel)\b/.test(t)) tags.add('pagamento')
    if (/\b(comprovante|recibo|pix\s+enviado|transfer[êe]ncia)\b/.test(t))               tags.add('pagamento')
    if (/\b(cupom\s+fiscal|nota\s+fiscal|nfc-?e|nf-?e|sat\b)\b/.test(t))                 tags.add('nota_fiscal')
    if (/\b(receita\s+m[ée]dica|prescri[çc][ãa]o|crm\b)\b/.test(t))                      tags.add('saude')
    if (/\b(contrato|termo\s+de|ades[ãa]o)\b/.test(t))                                   tags.add('contrato')
    if (/\b(pedido|protocolo|ped-\d{8})\b/.test(t))                                      tags.add('pedido')
  }

  return [...tags]
}

module.exports = {
  transcribeAudio,
  describeImage,
  extractPdfText,
  processVideo,
  extractVCard,
  detectMediaIntent,
  fetchMediaBuffer,
}
