'use strict'

/**
 * Receptor de Webhooks — motor NewWhats → satélite Alemão
 *
 * Rota:   POST /api/webhook/newwhats
 * Origem: motor (newwhats.local:8008) via HTTP direto
 *
 * Validação:
 *   Header x-nw-signature: sha256=<hmac>
 *   HMAC-SHA256(webhook_secret, rawBody)
 *
 * Eventos tratados:
 *   message.new    — nova mensagem recebida/enviada
 *   session.status — instância conectou / desconectou
 *
 * Auto-resposta (Frente 2 v2.2):
 *   Modo controlado por plugin_config auto_reply_mode:
 *     off        → só loga (comportamento anterior)
 *     automatico → chama brain do motor e envia reply
 *
 *   Regras de segurança:
 *     - fromMe=true  → nunca responde
 *     - role=notificacoes → nunca responde
 *     - cooldown 15s por chat_id → evita flood (era 45s, reduzido pois bloqueava 2ª msg em conversa ativa)
 *     - max 8 replies/conversa em 1h → anti-loop
 *     - só responde msg tipo TEXT
 */

const { createHmac, timingSafeEqual } = require('crypto')
const { Router }                       = require('express')
const { queryOne, query, execute }     = require('../../database/postgres')

const SECRET_KEY       = 'webhook_secret'
const MOTOR_URL_KEY    = 'newwhats_url'
const INTEG_KEY_KEY    = 'integration_key'
const AUTO_MODE_KEY    = 'auto_reply_mode'   // off | automatico
const COOLDOWN_MS      = 15_000              // 15s entre replies no mesmo chat
const MAX_REPLIES_1H   = 8                   // máx respostas automáticas por chat por hora

// ── Config helpers ────────────────────────────────────────────────────────────

// Cache em memória: evita query ao banco em cada mensagem recebida.
// Config muda raramente (admin altera via painel) — TTL de 60s é seguro.
let _cfgCache = null
let _cfgExpiry = 0

async function getPluginConfig() {
  if (_cfgCache && Date.now() < _cfgExpiry) return _cfgCache
  const rows = await query(`SELECT key, value FROM plugin_configs WHERE plugin_id = 'newwhats'`)
  _cfgCache = {}
  for (const r of rows) _cfgCache[r.key] = r.value
  _cfgExpiry = Date.now() + 60_000
  return _cfgCache
}

// Cache de role por instance_id — HTTP ao motor em cada msg custa 200-500ms.
// TTL de 5min: role muda raramente (admin altera via painel da secretária).
const _numberRoleCache = new Map()
const NUMBER_ROLE_TTL = 5 * 60_000

async function getNumberRole(motorUrl, integKey, instanceId) {
  const cached = _numberRoleCache.get(instanceId)
  if (cached && Date.now() - cached.ts < NUMBER_ROLE_TTL) return cached
  try {
    const res = await fetch(`${motorUrl}/api/ext/v1/secretaria/numbers`, {
      headers: { 'x-nw-key': integKey ?? '' },
    })
    if (!res.ok) return null
    const nums = await res.json()
    const info = Array.isArray(nums) ? nums.find(n => n.instance_id === instanceId) : null
    const entry = { role: info?.role ?? null, active: info?.active ?? true, ts: Date.now() }
    _numberRoleCache.set(instanceId, entry)
    return entry
  } catch { return null }
}

// ── Assinatura HMAC ───────────────────────────────────────────────────────────

async function getSecret() {
  const row = await queryOne(
    "SELECT value FROM plugin_configs WHERE plugin_id = 'newwhats' AND key = $1",
    [SECRET_KEY],
  )
  return row?.value ?? null
}

function verifySignature(secret, rawBody, sigHeader) {
  if (!sigHeader?.startsWith('sha256=')) return false
  const expected = Buffer.from(
    'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'),
  )
  const received = Buffer.from(sigHeader)
  if (expected.length !== received.length) return false
  return timingSafeEqual(expected, received)
}

// ── Roteador principal ────────────────────────────────────────────────────────

function createWebhookReceiver() {
  const router = Router()

  router.post('/api/webhook/newwhats', async (req, res) => {
    try {
      const sig    = req.headers['x-nw-signature']
      const secret = await getSecret()

      if (!secret) {
        console.warn('[nw-webhook] webhook_secret não configurado — assinatura ignorada')
      } else {
        const rawBody = req.rawBody ?? Buffer.from(JSON.stringify(req.body))
        if (!verifySignature(secret, rawBody, sig)) {
          console.warn('[nw-webhook] Assinatura inválida — rejeitado')
          return res.status(401).json({ error: 'Assinatura inválida' })
        }
      }

      const { event, data } = req.body ?? {}
      if (!event || !data) return res.status(400).json({ error: 'Payload inválido' })

      if (event === 'message.new') {
        await handleMessageNew(data)
      } else if (event === 'session.status') {
        await handleSessionStatus(data)
      }

      res.json({ ok: true })
    } catch (err) {
      console.error('[nw-webhook] Erro interno:', err.message)
      res.status(500).json({ error: 'Erro interno' })
    }
  })

  return router
}

// ── Handler: message.new ──────────────────────────────────────────────────────

async function handleMessageNew(data) {
  const msg = data?.message
  if (!msg) return

  // 1. Loga sempre — captura id para poder atualizar payload após extração de mídia
  const logRow = await queryOne(
    `INSERT INTO nw_event_logs
       (instance_id, event, direction, chat_id, from_me, msg_type, payload)
     VALUES ($1, $2, $3, $4, $5, $6, $7)
     RETURNING id`,
    [
      data.instanceId ?? '',
      'message.new',
      msg.fromMe ? 'out' : 'in',
      data.chatId    ?? null,
      msg.fromMe     ?? false,
      msg.type       ?? null,
      JSON.stringify({ body: msg.body, hasMedia: msg.hasMedia, author: msg.author, pushName: msg.pushName }),
    ],
  )
  const logId = logRow?.id ?? null

  // 2. Nunca responder mensagem enviada pelo próprio número.
  //    Se havia handoff ativo, o operador respondeu — libera a IA imediatamente.
  if (msg.fromMe) {
    try {
      const { sseBroadcast } = require('../../services/sse')
      const chatId = data.chatId ?? ''
      const updated = await execute(
        `UPDATE nw_handoff SET human_until=NULL, updated_at=NOW()
         WHERE chat_id=$1 AND human_until > NOW()`,
        [chatId]
      )
      if (updated?.rowCount > 0) {
        console.log(`[nw-webhook] handoff limpo após resposta do operador — ${chatId}`)
        sseBroadcast('handoff.expired', { chatId })
      }
    } catch { /* silencioso */ }
    return
  }

  // 2b. Captura avaliação de satisfação (1–5) se houver pesquisa pendente
  const chatId = data.chatId ?? ''
  const body   = (msg.body ?? '').trim()
  if (/^[1-5]$/.test(body)) {
    try {
      const { queryOne: qOne, execute: dbExec } = require('../../database/postgres')
      const pending = await qOne(
        `SELECT id FROM protocol_ratings
         WHERE chat_id=$1 AND status='pending'
         ORDER BY sent_at DESC LIMIT 1`,
        [chatId]
      )
      if (pending) {
        await dbExec(
          `UPDATE protocol_ratings SET rating=$1, status='rated', rated_at=NOW() WHERE id=$2`,
          [parseInt(body), pending.id]
        )
        console.log(`[nw-webhook] ⭐ Avaliação ${body}/5 registrada para ${chatId}`)
        return // não processa como auto-reply
      }
    } catch { /* silencioso */ }
  }

  // 3. Lê config do plugin
  const cfg = await getPluginConfig()
  const mode = cfg[AUTO_MODE_KEY] ?? 'off'
  if (mode === 'off') return

  // 4. Chats de grupos ignorados (@g.us)
  if (chatId.endsWith('@g.us')) return

  // 5. Tipo de mensagem: TEXT, AUDIO (PTT), IMAGE, DOCUMENT (PDF), VIDEO e
  //    CONTACT/VCARD são processados. Áudio é transcrito por Whisper/Gemini;
  //    imagem é descrita/classificada; PDF tem texto extraído; vídeo é
  //    descrito (cena+áudio) por Gemini nativo; vCard é parseado localmente.
  //    LOCATION (GPS): TODO — resolver coordenadas para endereço via Maps API.
  //    STICKER e outros: ignorados.
  const msgType = (msg.type ?? '').toUpperCase()
  let effectiveBody = body

  if (msgType === 'AUDIO' || msgType === 'PTT') {
    if (!msg.id) {
      console.warn('[nw-auto] áudio sem msg.id, ignorando')
      return
    }
    try {
      const { transcribeAudio } = require('./media-transcribe')
      const transcript = await transcribeAudio({
        motorUrl: cfg[MOTOR_URL_KEY],
        integKey: cfg[INTEG_KEY_KEY],
        messageId: msg.id,
      })
      if (!transcript) {
        console.log(`[nw-auto] áudio inaudível ou falha de transcrição em ${chatId}`)
        return
      }
      console.log(`[nw-auto] 🎙️ transcrição (${transcript.length} chars): "${transcript.slice(0, 80)}…"`)
      effectiveBody = transcript
    } catch (e) {
      console.warn('[nw-auto] transcribeAudio erro:', e.message)
      return
    }
  } else if (msgType === 'IMAGE') {
    if (!msg.id) {
      console.warn('[nw-auto] imagem sem msg.id, ignorando')
      return
    }
    try {
      const { describeImage } = require('./media-transcribe')
      const description = await describeImage({
        motorUrl: cfg[MOTOR_URL_KEY],
        integKey: cfg[INTEG_KEY_KEY],
        messageId: msg.id,
        captionFromUser: body || null,
      })
      if (!description) {
        console.log(`[nw-auto] imagem sem descrição obtida em ${chatId}`)
        return
      }
      console.log(`[nw-auto] 🖼️ ${description.slice(0, 100)}`)
      effectiveBody = body ? `${description}\nLegenda do cliente: ${body}` : description
    } catch (e) {
      console.warn('[nw-auto] describeImage erro:', e.message)
      return
    }
  } else if (msgType === 'DOCUMENT') {
    if (!msg.id) {
      console.warn('[nw-auto] documento sem msg.id, ignorando')
      return
    }
    try {
      const { extractPdfText } = require('./media-transcribe')
      const pdfText = await extractPdfText({
        motorUrl: cfg[MOTOR_URL_KEY],
        integKey: cfg[INTEG_KEY_KEY],
        messageId: msg.id,
        captionFromUser: body || null,
      })
      if (!pdfText) {
        console.log(`[nw-auto] documento não processável (tipo não suportado ou muito grande) em ${chatId}`)
        return
      }
      console.log(`[nw-auto] 📄 PDF extraído (${pdfText.length} chars) em ${chatId}`)
      effectiveBody = body ? `${pdfText}\nMensagem do cliente: ${body}` : pdfText

      // Persiste texto extraído no log para que pings de follow-up ("???")
      // possam recuperar o conteúdo do PDF como "pergunta pendente".
      if (logId) {
        execute(
          `UPDATE nw_event_logs SET payload = $1 WHERE id = $2`,
          [JSON.stringify({ body: pdfText, hasMedia: true, originalCaption: body || null, author: msg.author, pushName: msg.pushName }), logId],
        ).catch(e => console.warn('[nw-auto] falha ao persistir texto PDF no log:', e.message))
      }
    } catch (e) {
      console.warn('[nw-auto] extractPdfText erro:', e.message)
      return
    }
  } else if (msgType === 'VIDEO') {
    if (!msg.id) {
      console.warn('[nw-auto] vídeo sem msg.id, ignorando')
      return
    }
    try {
      const { processVideo } = require('./media-transcribe')
      const description = await processVideo({
        motorUrl: cfg[MOTOR_URL_KEY],
        integKey: cfg[INTEG_KEY_KEY],
        messageId: msg.id,
        captionFromUser: body || null,
      })
      if (!description) {
        console.log(`[nw-auto] vídeo não processável (muito grande ou erro) em ${chatId}`)
        return
      }
      console.log(`[nw-auto] 🎬 ${description.slice(0, 100)}`)
      effectiveBody = body ? `${description}\nLegenda do cliente: ${body}` : description
    } catch (e) {
      console.warn('[nw-auto] processVideo erro:', e.message)
      return
    }
  } else if (msgType === 'CONTACT' || msgType === 'VCARD' || msgType === 'CONTACTS_ARRAY') {
    try {
      const { extractVCard } = require('./media-transcribe')
      const card = extractVCard({ rawVcard: msg.vcard, msgBody: msg.body })
      if (!card) {
        console.log(`[nw-auto] vCard sem dados úteis em ${chatId}`)
        return
      }
      console.log(`[nw-auto] 👤 ${card.slice(0, 120)}`)
      effectiveBody = card
    } catch (e) {
      console.warn('[nw-auto] extractVCard erro:', e.message)
      return
    }
  } else if (msgType === 'LOCATION') {
    // TODO: implementar reverse geocoding (Maps API) para resolver
    // msg.location.latitude/longitude → endereço legível e injetar em effectiveBody.
    // Útil para confirmar endereço de entrega quando cliente compartilha localização.
    console.log(`[nw-auto] LOCATION recebida em ${chatId} — handler ainda não implementado`)
    return
  } else if (msgType !== 'TEXT' && msgType !== '') {
    return
  }
  if (!effectiveBody) return

  // 5c. Detecção de follow-up/ping ("?????", "verificou??", "alô?", "tá aí?")
  //     Quando o cliente está cobrando resposta:
  //       - Bypassa cooldown (a inação é o problema dele, não excesso de respostas)
  //       - Recupera a última pergunta sem resposta no chat
  //       - Enriquece o body com contexto antes de mandar ao LLM
  let isFollowUp = false
  let pendingQuestion = null
  try {
    const { isFollowUpPing, findPendingQuestion, buildEnrichedBody } = require('./follow-up-detector')
    if (isFollowUpPing(effectiveBody)) {
      isFollowUp = true
      pendingQuestion = await findPendingQuestion(chatId, 1440)
      const enriched = buildEnrichedBody(effectiveBody, pendingQuestion)
      console.log(`[nw-auto] 🔔 ping detectado em ${chatId} — pergunta pendente: ${pendingQuestion ? `"${pendingQuestion.body.slice(0,60)}…" (${pendingQuestion.ageMinutes}min)` : 'nenhuma'}`)
      effectiveBody = enriched

      // Se handoff humano está ativo, alertar o painel admin via SSE — operador
      // precisa ver que o cliente está cobrando. IA segue pausada (não responde).
      try {
        const handoffRow = await queryOne(
          `SELECT human_until FROM nw_handoff WHERE chat_id=$1 ORDER BY updated_at DESC LIMIT 1`,
          [chatId]
        )
        if (handoffRow?.human_until && new Date(handoffRow.human_until) > new Date()) {
          const { sseBroadcast } = require('../../services/sse')
          sseBroadcast('handoff.followup_ping', {
            chatId,
            instanceId: data.instanceId ?? '',
            pingBody: body,
            pendingQuestion: pendingQuestion?.body ?? null,
            ageMinutes: pendingQuestion?.ageMinutes ?? null,
            at: new Date().toISOString(),
          })
          console.log(`[nw-auto] 📣 handoff ativo + ping → SSE handoff.followup_ping emitido`)
        }
      } catch (e) { console.warn('[nw-auto] handoff/SSE check falhou:', e.message) }
    }
  } catch (e) {
    console.warn('[nw-auto] follow-up-detector erro:', e.message)
  }

  // 6. Verifica role do número — notificacoes nunca auto-responde (com cache 5min)
  try {
    const numInfo = await getNumberRole(cfg[MOTOR_URL_KEY], cfg[INTEG_KEY_KEY], data.instanceId ?? '')
    if (numInfo?.role === 'notificacoes') return
    if (numInfo && numInfo.active === false) return
  } catch { /* se falhar a leitura, deixa passar — melhor responder do que parar */ }

  // 7. Cooldown: evita responder o mesmo chat muito rápido
  //    EXCEÇÕES:
  //      - Pings de follow-up bypassam — cliente cobrando precisa de feedback.
  //      - Última resposta foi do smart-router (status='sent_local') — sem custo
  //        de LLM, então não há motivo de cooldown. Cliente fazendo follow-up
  //        imediato a uma resposta determinística não pode ser bloqueado.
  //    Limite anti-loop (step 8) ainda se aplica em todos os casos.
  const lastReply = await queryOne(
    `SELECT created_at, status FROM nw_auto_replies
     WHERE chat_id = $1
     ORDER BY created_at DESC LIMIT 1`,
    [chatId],
  )
  if (lastReply && !isFollowUp && lastReply.status === 'sent') {
    const elapsedMs = Date.now() - new Date(lastReply.created_at).getTime()
    if (elapsedMs < COOLDOWN_MS) {
      console.log(`[nw-auto] cooldown ativo para ${chatId} (${Math.round(elapsedMs/1000)}s < ${COOLDOWN_MS/1000}s)`)
      return
    }
  }

  // 8. Anti-loop: máx MAX_REPLIES_1H por chat por hora
  const countRow = await queryOne(
    `SELECT COUNT(*) AS cnt FROM nw_auto_replies
     WHERE chat_id = $1 AND created_at > NOW() - INTERVAL '1 hour'`,
    [chatId],
  )
  if (parseInt(countRow?.cnt ?? 0, 10) >= MAX_REPLIES_1H) {
    console.log(`[nw-auto] limite de ${MAX_REPLIES_1H} respostas/hora atingido para ${chatId}`)
    return
  }

  // 9a. Smart Router — tenta responder localmente sem LLM (cotação, FAQ, horário etc)
  //     Usa effectiveBody (texto original ou transcrição/descrição de mídia)
  //     Em caso de follow-up/ping, pula direto ao LLM — o body foi enriquecido com
  //     contexto da pergunta original e o smart-router não tem como atender.
  setImmediate(async () => {
    try {
      if (isFollowUp) {
        autoReply(cfg, data.instanceId, chatId, effectiveBody, msg.pushName, data.from)
        return
      }
      const { tryLocalReply, sendLocalReply } = require('./smart-router')
      const local = await tryLocalReply(effectiveBody, data.from, 1)
      if (local) {
        const ok = await sendLocalReply(
          cfg[MOTOR_URL_KEY], cfg[INTEG_KEY_KEY],
          data.instanceId, chatId, local.reply, local.intent, effectiveBody
        )
        if (ok) return  // Respondido localmente, não chama LLM
      }
      // Se smart-router não acertou ou envio falhou, segue ao LLM
      autoReply(cfg, data.instanceId, chatId, effectiveBody, msg.pushName, data.from)
    } catch (e) {
      console.warn('[smart-router] erro, caindo no LLM:', e.message)
      autoReply(cfg, data.instanceId, chatId, effectiveBody, msg.pushName, data.from)
    }
  })
}

// ── Auto-reply: chama brain e envia resposta ──────────────────────────────────

async function autoReply(cfg, instanceId, chatId, userMsg, contactName, fromPhone) {
  const motorUrl  = cfg[MOTOR_URL_KEY]
  const integKey  = cfg[INTEG_KEY_KEY]

  if (!motorUrl || !integKey) {
    console.warn('[nw-auto] motor URL ou integration_key não configurados')
    return
  }

  try {
    // 1. Verifica handoff local — se humano está respondendo, não chamar IA
    try {
      const { queryOne: qOne } = require('../../database/postgres')
      const hRow = await qOne(
        `SELECT human_until FROM nw_handoff WHERE chat_id=$1 ORDER BY updated_at DESC LIMIT 1`,
        [chatId]
      )
      if (hRow?.human_until && new Date(hRow.human_until) > new Date()) {
        console.log(`[nw-auto] handoff=humano (local) para ${chatId} — IA pausada`)
        return
      }
    } catch { /* falha silenciosa */ }

    // 2. Monta contexto local do projeto
    let context = undefined
    let systemExtra = undefined
    let intentTags = []
    try {
      const { buildContext } = require('./context-builder')
      const phone = (fromPhone ?? '').replace(/\D/g, '').replace(/^55/, '')
      const ctx = await buildContext(phone, 1, userMsg, chatId)
      if (ctx) {
        context = ctx
        systemExtra = ctx._systemExtra
        intentTags = ctx.intencoes_detectadas ?? []
        delete context._systemExtra
      }
    } catch (e) { console.warn('[nw-auto] context-builder falhou:', e.message) }

    // 2a-bis. Intent automático por tipo de mídia — adiciona tags ao brain
    //         (mais barato e preciso do que esperar o LLM detectar sozinho).
    try {
      const { detectMediaIntent } = require('./media-transcribe')
      const mediaTags = detectMediaIntent(userMsg)
      if (mediaTags.length > 0) {
        intentTags = [...new Set([...intentTags, ...mediaTags])]
        console.log(`[nw-auto] 🏷️  intent por mídia: [${mediaTags.join(',')}]`)
      }
    } catch { /* silencioso */ }

    // 2a. Persona — define tom de voz/escopo de atendimento por tenant.
    //     Vai ANTES da BASE DE CONHECIMENTO para o LLM ler primeiro o "como falar".
    try {
      const { resolvePersona } = require('./personas')
      const persona = await resolvePersona(1)
      if (persona) {
        const personaBlock = `=== PERSONA DE ATENDIMENTO (${persona.label}) ===\n${persona.prompt}`
        systemExtra = [personaBlock, systemExtra].filter(Boolean).join('\n\n')
      }
    } catch (e) { console.warn('[nw-auto] persona injection falhou:', e.message) }

    // 2b. Injeta brain — filtrado por intent (core sempre + tags relevantes)
    try {
      const { query: dbq, queryOne: qOne } = require('../../database/postgres')
      const chatSector = await qOne(
        `SELECT sector_id FROM nw_chat_sectors WHERE chat_id=$1 LIMIT 1`, [chatId]
      )
      const sectorClause = chatSector?.sector_id
        ? `AND sbn.sector_id = ${Number(chatSector.sector_id)}`
        : ''
      const tagsToLoad = ['core', ...intentTags]
      const brainRows = await dbq(
        `SELECT sbn.type, sbn.content, s.name AS sector_name, sbn.sort_order
         FROM sector_brain_nodes sbn
         JOIN sectors s ON s.id = sbn.sector_id
         WHERE sbn.active = TRUE ${sectorClause}
           AND sbn.tags && $1::text[]
         ORDER BY s.sort_order, sbn.sort_order LIMIT 20`,
        [tagsToLoad]
      )
      if (brainRows.length > 0) {
        const brainText = brainRows.map(r =>
          `[${r.type.toUpperCase()}${chatSector?.sector_id ? '' : ` — ${r.sector_name}`}]\n${r.content}`
        ).join('\n\n')
        const brainBlock = `=== BASE DE CONHECIMENTO ===\n${brainText}`
        systemExtra = [systemExtra, brainBlock].filter(Boolean).join('\n\n')
        console.log(`[nw-auto] brain: ${brainRows.length} nós (~${brainText.length} chars) tags=[${tagsToLoad.join(',')}]`)
      }
    } catch (e) { console.warn('[nw-auto] brain injection falhou:', e.message) }

    // 3. Chama /api/ext/v1/secretaria/ask (cria/retoma conversa + brain.chat())
    const askRes = await fetch(`${motorUrl}/api/ext/v1/secretaria/ask`, {
      method:  'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-nw-key':     integKey,
      },
      body: JSON.stringify({
        chatId,
        message:     userMsg,
        contactName: contactName ?? chatId,
        context,
        systemExtra,
        tools: ['listar_horarios', 'agendar_horario', 'escalar_humano', 'encerrar_protocolo', 'human_api'],
      }),
    })

    if (!askRes.ok) {
      const txt = await askRes.text()
      console.error(`[nw-auto] brain ask failed ${askRes.status}:`, txt)
      await logAutoReply(instanceId, chatId, null, userMsg, null, 'failed')
      return
    }

    const askJson = await askRes.json()
    const { reply, conversationId, humanApiRequest, sectorId: detectedSectorId } = askJson

    // 3a. Sincroniza setor identificado pela IA → banco local
    if (detectedSectorId) {
      try {
        const { execute: dbExec } = require('../../database/postgres')
        await dbExec(
          `INSERT INTO nw_chat_sectors (chat_id, tenant_id, sector_id, updated_at)
           VALUES ($1,1,$2,NOW())
           ON CONFLICT (chat_id, tenant_id) DO UPDATE SET sector_id=$2, updated_at=NOW()`,
          [chatId, detectedSectorId]
        )
      } catch { /* silencioso */ }
    }

    // 3b. Human API — IA precisa de resposta de especialista humano
    if (humanApiRequest) {
      try {
        const localUrl = `http://127.0.0.1:${process.env.PORT || 4001}`
        await fetch(`${localUrl}/api/human-api/request`, {
          method:  'POST',
          headers: { 'Content-Type': 'application/json', 'x-internal-secret': process.env.INTERNAL_WEBHOOK_SECRET || '' },
          body: JSON.stringify({
            chatId, conversationId, instanceId,
            question:   humanApiRequest.question || humanApiRequest,
            sectorName: humanApiRequest.sectorName || null,
          }),
        })
      } catch (e) { console.warn('[nw-auto] human_api forward falhou:', e.message) }
    }

    if (!reply?.trim()) return

    // 2. Envia resposta via motor
    const sendRes = await fetch(`${motorUrl}/api/ext/v1/inbox/${encodeURIComponent(chatId)}/send`, {
      method:  'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-nw-key':     integKey,
      },
      body: JSON.stringify({ text: reply }),
    })

    if (!sendRes.ok) {
      const txt = await sendRes.text()
      console.error(`[nw-auto] send failed ${sendRes.status}:`, txt)
      await logAutoReply(instanceId, chatId, conversationId, userMsg, reply, 'failed')
      return
    }

    // 3. Loga sucesso + telemetria
    await logAutoReply(instanceId, chatId, conversationId, userMsg, reply, 'sent')
    const ctxChars = (systemExtra?.length || 0) + (context ? JSON.stringify(context).length : 0)
    const saved = Math.max(0, 5000 - ctxChars)  // baseline de 5000 chars
    await execute(
      `INSERT INTO nw_router_metrics (chat_id, handler, intent, user_msg, reply_chars, ctx_chars, saved_chars)
       VALUES ($1, 'llm', $2, $3, $4, $5, $6)`,
      [chatId, intentTags.join(',') || null, userMsg.slice(0, 200), reply.length, ctxChars, saved]
    ).catch(() => {})
    console.log(`[nw-auto] ✅ ${chatId} → "${reply.slice(0, 60)}…" (ctx=${ctxChars}c, saved=${saved}c)`)

  } catch (err) {
    console.error('[nw-auto] erro inesperado:', err.message)
    await logAutoReply(instanceId, chatId, null, userMsg, null, 'failed').catch(() => {})
  }
}

async function logAutoReply(instanceId, chatId, convId, userMsg, reply, status) {
  await execute(
    `INSERT INTO nw_auto_replies
       (instance_id, chat_id, conv_id, user_msg, reply, status)
     VALUES ($1, $2, $3, $4, $5, $6)`,
    [instanceId, chatId, convId ?? null, userMsg, reply ?? null, status],
  )
}

// ── Handler: session.status ───────────────────────────────────────────────────

async function handleSessionStatus(data) {
  await execute(
    `INSERT INTO nw_event_logs
       (instance_id, event, direction, payload)
     VALUES ($1, $2, 'in', $3)`,
    [
      data.instanceId ?? '',
      'session.status',
      JSON.stringify({ status: data.status }),
    ],
  )
  console.log(`[nw-webhook] session.status → ${data.instanceId}: ${data.status}`)
}

module.exports = { createWebhookReceiver }
