require('dotenv').config()
const express      = require('express')
const cors         = require('cors')
const cookieParser = require('cookie-parser')
const rateLimit    = require('express-rate-limit')
const path         = require('path')
const { initialize } = require('./database/database')
const { query, queryOne, execute } = require('./database/postgres')
const PluginManager  = require('./plugins/index')
const { startWorker } = require('./workers/raffleWorker')
const { startClubWorker } = require('./workers/clubWorker')
require('./workers/whatsappWorker')

const PORT = process.env.PORT || 4001


async function startServer() {
    // Initialize database
    await initialize();

    // Start background workers
    startWorker();
    startClubWorker();

    const app = express();

    // Middleware
    const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean)
    app.use(cors({ origin: allowedOrigins.length ? allowedOrigins : false, credentials: true }));
    app.use(express.json({
      verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8') },
    }));
    app.use(express.urlencoded({ extended: true }));
    app.use(cookieParser());
    app.use(express.static(path.join(__dirname, 'public'), {
      setHeaders(res, filePath) {
        if (filePath.endsWith('.html')) {
          // Impede cache de HTML para que o browser sempre busque a versão mais recente
          res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
          res.setHeader('Pragma', 'no-cache')
          res.setHeader('Expires', '0')
        }
      },
    }));

    // Favicon handler to avoid 404
    app.get('/favicon.ico', (req, res) => res.status(204).end());

    const { verifyToken, verifyUserToken, JWT_SECRET: _JWT_SECRET } = require('./middleware/auth');
    const jwt = require('jsonwebtoken');

    // Rate limiting — público mais restrito; admins com token válido têm limite maior
    const publicLimiter = rateLimit({
        windowMs: 15 * 60 * 1000,
        max: 300,
        message: { error: 'Muitas requisições' },
        skip: (req) => {
          const token = req.cookies?.cloud_token
          if (!token) return false
          try { jwt.verify(token, _JWT_SECRET); return true } catch { return false }
        },
    });
    app.use('/api/', publicLimiter);
    const { verifyAnyToken, verifyTeamToken, signTeamToken: _signTeamToken } = require('./middleware/teamAuth');
    const { tenantMiddleware, tenantAdminMiddleware, invalidateTenantCache } = require('./middleware/tenant');
    const adminController = require('./controllers/adminController');
    const sseService = require('./services/sse');

    // Aplica detecção de tenant em todas as rotas /api/ e páginas públicas
    app.use('/api/', tenantMiddleware);
    app.use('/', tenantMiddleware);

    // API Routes
    const apiUsers = require('./routes/apiUsers');
    const apiPromotions = require('./routes/apiPromotions');
    const apiRaffle = require('./routes/apiRaffle');
    const apiAnalytics = require('./routes/apiAnalytics');
    const analyticsController = require('./controllers/analyticsController');
    const webhookController = require('./controllers/webhookController');
    const apiClientes       = require('./routes/apiClientes');
    const apiProdutos       = require('./routes/apiProdutos');
    const apiCotacoes       = require('./routes/apiCotacoes');
    const apiWhatsappTools  = require('./routes/apiWhatsappTools');
    const apiRecorrentes    = require('./routes/apiRecorrentes');

    app.use('/api', apiUsers);
    app.use('/api/promotions', apiPromotions);
    app.use('/api/raffle', apiRaffle);
    app.use('/api/analytics', apiAnalytics);
    app.post('/api/webhooks/asaas', webhookController.asaasWebhook);
    // Módulo Cotação Online
    app.use('/api/clientes',         apiClientes);
    app.use('/api/produtos',         apiProdutos);
    app.use('/api/cotacoes',         apiCotacoes);
    app.use('/api/recorrentes',      apiRecorrentes);
    // Ferramentas WhatsApp IA (WPP-05/06/07/09)
    app.use('/api/whatsapp',         apiWhatsappTools);

    // Club routes
    const clubController = require('./controllers/clubController');
    app.get('/api/clubs',                                clubController.listClubs);
    app.get('/api/clubs/card/:cpf',                     clubController.getCard);
    app.get('/api/clubs/:slug/faqs',                    clubController.listFaqsBySlug);
    app.get('/api/clubs/:slug',                         clubController.getClub);
    app.get('/api/clubs/:slug/partners',                clubController.listPartnersBySlug);
    app.get('/api/clubs/:slug/validate/:cpf',           clubController.validateMember);
    app.post('/api/clubs/:slug/subscribe',               clubController.subscribe);
    app.post('/api/clubs/member/cancel', verifyUserToken, clubController.cancelMembership);
    // Club reports
    app.get('/api/admin/club/reports', verifyToken, async (req, res) => {
      try {
        const { query: dbQuery } = require('./database/postgres')
        const tenantId = req.tenant?.id || 1

        // MRR por mês (últimos 12 meses)
        const mrr = await dbQuery(
          `SELECT TO_CHAR(DATE_TRUNC('month', cm.started_at), 'YYYY-MM') AS month,
                  bc.id AS club_id, bc.name AS club_name,
                  SUM(cp.price_monthly) AS mrr,
                  COUNT(cm.id) AS members
           FROM club_members cm
           JOIN benefit_clubs bc ON bc.id = cm.club_id
           JOIN club_plans    cp ON cp.id = cm.plan_id
           WHERE cm.tenant_id = $1
             AND cm.started_at >= NOW() - INTERVAL '12 months'
             AND cm.status NOT IN ('canceled')
           GROUP BY 1,2,3
           ORDER BY 1`,
          [tenantId]
        )

        // Novos vs cancelamentos por mês
        const growth = await dbQuery(
          `SELECT TO_CHAR(DATE_TRUNC('month', started_at), 'YYYY-MM') AS month,
                  COUNT(*) AS new_members
           FROM club_members
           WHERE tenant_id = $1 AND started_at >= NOW() - INTERVAL '12 months'
           GROUP BY 1
           ORDER BY 1`,
          [tenantId]
        )
        const churn = await dbQuery(
          `SELECT TO_CHAR(DATE_TRUNC('month', canceled_at), 'YYYY-MM') AS month,
                  COUNT(*) AS canceled
           FROM club_members
           WHERE tenant_id = $1 AND canceled_at >= NOW() - INTERVAL '12 months'
           GROUP BY 1
           ORDER BY 1`,
          [tenantId]
        )

        // Distribuição de planos
        const planDist = await dbQuery(
          `SELECT bc.name AS club_name, cp.name AS plan_name,
                  COUNT(cm.id) AS total
           FROM club_members cm
           JOIN benefit_clubs bc ON bc.id = cm.club_id
           JOIN club_plans    cp ON cp.id = cm.plan_id
           WHERE cm.tenant_id = $1 AND cm.status = 'active'
           GROUP BY 1,2`,
          [tenantId]
        )

        // Inadimplência
        const overdue = await dbQuery(
          `SELECT bc.name AS club_name,
                  COUNT(CASE WHEN cm.status = 'suspended' THEN 1 END) AS suspended,
                  COUNT(CASE WHEN cm.status = 'active'    THEN 1 END) AS active
           FROM club_members cm
           JOIN benefit_clubs bc ON bc.id = cm.club_id
           WHERE cm.tenant_id = $1
           GROUP BY 1`,
          [tenantId]
        )

        res.json({
          mrr:      mrr     ?? [],
          growth:   growth  ?? [],
          churn:    churn   ?? [],
          planDist: planDist ?? [],
          overdue:  overdue  ?? [],
        })
      } catch (err) {
        console.error('[club reports]', err.message)
        res.status(500).json({ error: 'Erro interno' })
      }
    })

    // Admin club management
    app.get('/api/admin/club/members',           verifyToken, clubController.adminListMembers);
    app.patch('/api/admin/club/members/:id',     verifyToken, clubController.adminUpdateMemberStatus);
    app.get('/api/admin/club/partners',          verifyToken, clubController.adminListPartners);
    app.post('/api/admin/club/partners',         verifyToken, clubController.adminCreatePartner);
    app.put('/api/admin/club/partners/:id',      verifyToken, clubController.adminUpdatePartner);
    app.delete('/api/admin/club/partners/:id',   verifyToken, clubController.adminDeletePartner);
    // Admin club — planos
    app.get('/api/admin/club/plans',             verifyToken, clubController.adminListPlans);
    app.post('/api/admin/club/plans',            verifyToken, clubController.adminCreatePlan);
    app.put('/api/admin/club/plans/:id',         verifyToken, clubController.adminUpdatePlan);
    app.delete('/api/admin/club/plans/:id',      verifyToken, clubController.adminDeletePlan);
    // Admin club — conteúdo (condições + regras)
    app.put('/api/admin/club/:id/content',       verifyToken, clubController.adminSaveContent);
    // Admin club — FAQs
    app.get('/api/admin/club/faqs',              verifyToken, clubController.adminListFaqs);
    app.post('/api/admin/club/faqs',             verifyToken, clubController.adminCreateFaq);
    app.put('/api/admin/club/faqs/:id',          verifyToken, clubController.adminUpdateFaq);
    app.delete('/api/admin/club/faqs/:id',       verifyToken, clubController.adminDeleteFaq);
    // Asaas club webhook
    app.post('/api/webhooks/asaas/club', clubController.asaasClubWebhook);

    // ── Banco de Talentos ─────────────────────────────────────────────────────
    const jobController = require('./controllers/jobController');
    // Público
    app.get('/api/jobs',                             jobController.listJobs);
    app.get('/api/jobs/:id',                         jobController.getJob);
    app.post('/api/jobs/:id/apply',                  jobController.applyToJob);
    // Candidato autenticado
    app.get('/api/user/resume',      verifyUserToken, jobController.getMyResume);
    app.post('/api/user/resume',     verifyUserToken, jobController.saveMyResume);
    app.put('/api/user/resume',      verifyUserToken, jobController.saveMyResume);
    app.get('/api/user/applications',verifyUserToken, jobController.getMyApplications);
    app.delete('/api/user/applications/:id', verifyUserToken, jobController.cancelMyApplication);
    // Admin
    app.get('/api/admin/jobs/kpi',           verifyToken, jobController.adminJobsKpi);
    app.get('/api/admin/jobs',               verifyToken, jobController.adminListJobs);
    app.post('/api/admin/jobs',              verifyToken, jobController.adminCreateJob);
    app.put('/api/admin/jobs/:id',           verifyToken, jobController.adminUpdateJob);
    app.patch('/api/admin/jobs/:id/toggle',  verifyToken, jobController.adminToggleJob);
    app.delete('/api/admin/jobs/:id',        verifyToken, jobController.adminDeleteJob);
    app.get('/api/admin/jobs/:id/applications', verifyToken, jobController.adminGetApplications);
    app.get('/api/admin/applications/:id',   verifyToken, jobController.adminGetApplication);
    app.patch('/api/admin/applications/:id', verifyToken, jobController.adminUpdateApplication);
    app.get('/api/admin/resumes',            verifyToken, jobController.adminSearchResumes);
    app.get('/api/admin/resumes/:cpf',       verifyToken, jobController.adminGetResume);

    // ── Fase 12 — Equipe Virtual ──────────────────────────────────────────────
    const equipeCtrl  = require('./controllers/equipeController')
    const nwTeamsCtrl = require('./controllers/nwTeamsController')

    // nw-teams: proxy de sectors/teams do WA-Inbox
    app.get('/api/admin/nw-teams', verifyAnyToken, nwTeamsCtrl.listNwTeams)
    // Perfil do colaborador logado (admin OU team) — rota exclusiva para a
    // gestão de equipe no painel. NÃO pode ser /api/admin/me: o Express usa a
    // primeira rota registrada, e /api/admin/me já é registrada mais abaixo
    // (verifyToken → dados do admin + branding do tenant). Qualquer duplicata
    // aqui interceptaria aquela rota, causando 401/404 para admins legítimos.
    app.get('/api/admin/equipe/me', verifyAnyToken, equipeCtrl.me)

    // accounts
    app.get   ('/api/admin/accounts',        verifyToken, equipeCtrl.listAccounts)
    app.get   ('/api/admin/accounts/:id',    verifyToken, equipeCtrl.getAccount)
    app.post  ('/api/admin/accounts',        verifyToken, equipeCtrl.createAccount)
    app.put   ('/api/admin/accounts/:id',    verifyToken, equipeCtrl.updateAccount)
    app.delete('/api/admin/accounts/:id',    verifyToken, equipeCtrl.deleteAccount)

    // sectors
    app.get   ('/api/admin/sectors',         verifyToken, equipeCtrl.listSectors)
    app.get   ('/api/admin/sectors/:id',     verifyToken, equipeCtrl.getSector)
    app.post  ('/api/admin/sectors',         verifyToken, equipeCtrl.createSector)
    app.put   ('/api/admin/sectors/:id',     verifyToken, equipeCtrl.updateSector)
    app.delete('/api/admin/sectors/:id',     verifyToken, equipeCtrl.deleteSector)

    // team members
    app.get   ('/api/admin/team-members',    verifyToken, equipeCtrl.listTeamMembers)
    app.post  ('/api/admin/team-members',    verifyToken, equipeCtrl.addTeamMember)
    app.put   ('/api/admin/team-members/:id',verifyToken, equipeCtrl.updateTeamMember)
    app.delete('/api/admin/team-members/:id',verifyToken, equipeCtrl.removeTeamMember)

    // sector brain nodes
    app.get   ('/api/admin/sectors/:sectorId/brain',    verifyToken, equipeCtrl.listSectorBrains)
    app.post  ('/api/admin/sectors/:sectorId/brain',    verifyToken, equipeCtrl.createSectorBrain)
    app.put   ('/api/admin/sectors/brain/:id',          verifyToken, equipeCtrl.updateSectorBrain)
    app.delete('/api/admin/sectors/brain/:id',          verifyToken, equipeCtrl.deleteSectorBrain)

    // inbox interna
    app.get ('/api/admin/inbox/threads',              verifyAnyToken, equipeCtrl.listThreads)
    app.get ('/api/admin/inbox/threads/:threadId',    verifyAnyToken, equipeCtrl.getThread)
    app.post('/api/admin/inbox/messages',             verifyAnyToken, equipeCtrl.postMessage)
    app.post('/api/admin/inbox/threads/:threadId/read', verifyAnyToken, equipeCtrl.markRead)
    app.get ('/api/admin/inbox/unread',               verifyAnyToken, equipeCtrl.unreadCount)

    // direct messages (DMs entre colaboradores) — Fase 13
    const dmCtrl = require('./controllers/directMessageController')
    app.post('/api/admin/inbox/direct-message',       verifyAnyToken, (req, res) => dmCtrl.send(req, res))
    app.get ('/api/admin/inbox/direct/:otherAccountId', verifyAnyToken, (req, res) => dmCtrl.getConversation(req, res))
    app.get ('/api/admin/inbox/direct-messages',      verifyAnyToken, (req, res) => dmCtrl.getLastMessages(req, res))
    app.get ('/api/admin/inbox/colleagues',           verifyAnyToken, (req, res) => dmCtrl.getColleagues(req, res))
    app.patch('/api/admin/inbox/direct/:messageId/read', verifyAnyToken, (req, res) => dmCtrl.markAsRead(req, res))

    // group chats por setor — Fase 14
    const gcCtrl = require('./controllers/groupChatController')
    app.get ('/api/admin/inbox/groups',                    verifyAnyToken, (req, res) => gcCtrl.list(req, res))
    app.get ('/api/admin/inbox/groups/:id/messages',       verifyAnyToken, (req, res) => gcCtrl.getMessages(req, res))
    app.post('/api/admin/inbox/groups/:id/message',        verifyAnyToken, (req, res) => gcCtrl.sendMessage(req, res))
    app.get ('/api/admin/inbox/groups/:id/members',        verifyAnyToken, (req, res) => gcCtrl.getMembers(req, res))
    app.post('/api/admin/inbox/groups/:id/read',           verifyAnyToken, (req, res) => gcCtrl.markRead(req, res))

    // broadcasts — Fase 15
    const bcCtrl = require('./controllers/broadcastController')
    app.post('/api/admin/inbox/broadcast',      verifyAnyToken, (req, res) => bcCtrl.send(req, res))
    app.get ('/api/admin/inbox/broadcasts',     verifyAnyToken, (req, res) => bcCtrl.list(req, res))
    app.post('/api/admin/inbox/broadcasts/seen', verifyAnyToken, (req, res) => bcCtrl.markSeen(req, res))

    // ── BullMQ — filas (declaradas antes das rotas que as usam) ───────────────
    const { Queue, Worker } = require('bullmq')
    const redisConn = { connection: { host: process.env.REDIS_HOST || '127.0.0.1', port: parseInt(process.env.REDIS_PORT || '6379') } }
    const sseHandoffQueue = new Queue('handoff-expire', redisConn)
    const humanApiQueue   = new Queue('human-api-timeout', redisConn)

    // ── Assumir escalação ──────────────────────────────────────────────────────
    app.post('/api/admin/escalation/assume', verifyAnyToken, async (req, res) => {
      try {
        const { chatId, protocolNumber, chatName } = req.body
        const accountId = req.teamAccount?.accountId || req.admin?.id
        const tenantId  = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id
        if (!accountId || !chatId) return res.status(400).json({ error: 'chatId e accountId obrigatórios' })

        const threadId = `esc-${chatId.replace(/[^a-z0-9]/gi, '')}-${Date.now()}`
        const content  = `Escalação assumida${chatName ? ` — cliente: ${chatName}` : ''}${protocolNumber ? ` — protocolo #${protocolNumber}` : ''}`

        const msg = await queryOne(
          `INSERT INTO internal_messages (tenant_id, thread_id, from_account, type, content, ref_id, chat_id)
           VALUES ($1,$2,$3,'escalation',$4,$5,$6) RETURNING *`,
          [tenantId, threadId, accountId, content, protocolNumber || null, chatId || null]
        )

        // Pausa IA para o chat
        const humanUntil = new Date(Date.now() + 15 * 60 * 1000)
        await execute(
          `INSERT INTO nw_handoff (chat_id, tenant_id, human_until, updated_at)
           VALUES ($1,$2,$3,NOW())
           ON CONFLICT (chat_id, tenant_id) DO UPDATE SET human_until=$3, updated_at=NOW()`,
          [chatId, tenantId, humanUntil]
        )

        // Agenda expiração do handoff via SSE
        sseHandoffQueue.add('handoff-expire', { chatId, tenantId }, { delay: 15 * 60 * 1000 })

        res.json({ threadId, msgId: msg.id, protocolNumber })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── Transferir protocolo ───────────────────────────────────────────────────
    app.post('/api/admin/protocol/transfer', verifyAnyToken, async (req, res) => {
      try {
        const { threadId, toAccountId, protocolNumber, message } = req.body
        const fromAccountId = req.teamAccount?.accountId || req.admin?.id
        const tenantId      = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id
        if (!toAccountId) return res.status(400).json({ error: 'toAccountId obrigatório' })

        const content = message?.trim() || `Protocolo${protocolNumber ? ` #${protocolNumber}` : ''} transferido para você.`
        const newThreadId = threadId || `trf-${Date.now()}-${Math.random().toString(36).slice(2,6)}`

        const msg = await queryOne(
          `INSERT INTO internal_messages (tenant_id, thread_id, from_account, to_account, type, content, ref_id)
           VALUES ($1,$2,$3,$4,'collaboration',$5,$6) RETURNING *`,
          [tenantId, newThreadId, fromAccountId, toAccountId, content, protocolNumber || null]
        )
        res.json({ threadId: newThreadId, msgId: msg.id })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── SSE — Server-Sent Events ───────────────────────────────────────────────
    const { register: sseRegister, unregister: sseUnregister, sseBroadcast, sseBroadcastTenant } = sseService

    app.get('/api/sse', verifyAnyToken, async (req, res) => {
      res.setHeader('Content-Type', 'text/event-stream')
      res.setHeader('Cache-Control', 'no-cache')
      res.setHeader('Connection', 'keep-alive')
      res.flushHeaders()

      const isTeam   = !!req.teamAccount
      const accountId = req.teamAccount?.accountId || req.admin?.id || 'admin'
      const tenantId  = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id || 1

      sseRegister(accountId, tenantId, res)

      // Colaborador conectado → marcar online e notificar colegas
      if (isTeam && req.teamAccount?.accountId) {
        try {
          await execute('UPDATE accounts SET availability=$1 WHERE id=$2', ['online', accountId])
          sseBroadcastTenant(tenantId, 'presence.change', {
            accountId, availability: 'online',
          }, accountId)
        } catch {}
      }

      // Enviar estado inicial confirmando conexão
      res.write(`event: connected\ndata: ${JSON.stringify({ accountId })}\n\n`)

      const heartbeat = setInterval(() => res.write(': ping\n\n'), 25000)

      req.on('close', async () => {
        clearInterval(heartbeat)
        sseUnregister(accountId)

        // Colaborador desconectado → marcar offline e notificar colegas
        if (isTeam && req.teamAccount?.accountId) {
          try {
            await execute('UPDATE accounts SET availability=$1 WHERE id=$2', ['offline', accountId])
            sseBroadcastTenant(tenantId, 'presence.change', {
              accountId, availability: 'offline',
            }, accountId)
          } catch {}
        }
      })
    })

    // ── Heartbeat de presença — colaborador altera seu status manualmente ──────
    app.patch('/api/team/availability', verifyTeamToken, async (req, res) => {
      try {
        const { availability } = req.body
        const accountId = req.teamAccount.accountId
        const tenantId  = req.teamAccount.tenantId

        if (!['online', 'busy', 'offline'].includes(availability)) {
          return res.status(400).json({ error: 'availability inválido: online | busy | offline' })
        }

        await execute('UPDATE accounts SET availability=$1 WHERE id=$2', [availability, accountId])
        sseBroadcastTenant(tenantId, 'presence.change', { accountId, availability }, accountId)

        res.json({ ok: true, availability })
      } catch (err) {
        console.error('[team/availability]', err)
        res.status(500).json({ error: 'Erro ao atualizar status' })
      }
    })

    app.patch('/api/team/me/duty', verifyTeamToken, async (req, res) => {
      try {
        const { on_duty } = req.body
        const accountId = req.teamAccount.accountId
        if (typeof on_duty !== 'boolean') return res.status(400).json({ error: 'on_duty deve ser boolean' })
        await execute(
          `UPDATE team_members SET on_duty=$1 WHERE account_id=$2`,
          [on_duty, accountId]
        )
        res.json({ ok: true, on_duty })
      } catch (err) {
        console.error('[team/duty]', err)
        res.status(500).json({ error: 'Erro ao atualizar plantão' })
      }
    })

    // ── BullMQ — workers ───────────────────────────────────────────────────────
    new Worker('handoff-expire', async job => {
      const { chatId, tenantId } = job.data
      await execute(
        `UPDATE nw_handoff SET human_until=NULL, updated_at=NOW()
         WHERE chat_id=$1 AND tenant_id=$2 AND human_until IS NOT NULL AND human_until <= NOW()`,
        [chatId, tenantId]
      )
      sseBroadcast('handoff.expired', { chatId })
    }, redisConn)

    new Worker('human-api-timeout', async job => {
      const { requestId } = job.data
      const row = await queryOne(
        `UPDATE human_api_requests SET status='timeout', answered_at=NOW()
         WHERE id=$1 AND status='pending'
         RETURNING id, thread_id, tenant_id, conversation_id, question`,
        [requestId]
      )
      if (row) {
        await execute(
          `INSERT INTO internal_messages (tenant_id, thread_id, type, content, ref_id)
           VALUES ($1,$2,'system','⏱ Nenhum atendente respondeu no prazo — IA retomará sem essa informação.',$3)`,
          [row.tenant_id, row.thread_id, String(requestId)]
        )
        sseBroadcast('human_api.timeout', { requestId, threadId: row.thread_id })

        // Notifica o motor para retomar a conversa sem a resposta humana
        if (row.conversation_id) {
          try {
            const cfg = {}
            const cfgRows = await query(`SELECT key, value FROM plugin_configs WHERE plugin_id='newwhats'`)
            for (const r of cfgRows) cfg[r.key] = r.value
            if (cfg.newwhats_url && cfg.integration_key) {
              await fetch(`${cfg.newwhats_url}/api/ext/v1/secretaria/ask`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'x-nw-key': cfg.integration_key },
                body: JSON.stringify({
                  conversationId: row.conversation_id,
                  message: '[SISTEMA] Nenhum especialista respondeu no prazo. Informe ao cliente de forma cordial e ofereça alternativas.',
                  systemExtra: 'O timeout da Human API ocorreu. Retome o atendimento sem a informação solicitada.',
                }),
              })
            }
          } catch (e) { console.warn('[human-api-timeout] motor notify failed:', e.message) }
        }
      }
    }, redisConn)

    const INTERNAL_SECRET = process.env.INTERNAL_WEBHOOK_SECRET

    // ── Human API — IA pede resposta de especialista humano ───────────────────
    app.post('/api/human-api/request', async (req, res) => {
      try {
        const internalKey = req.headers['x-internal-secret']
        if (!INTERNAL_SECRET || internalKey !== INTERNAL_SECRET) return res.status(403).json({ error: 'Proibido' })

        const { chatId, conversationId, instanceId, question, sectorName } = req.body
        const tenantId = 1 // single-tenant por enquanto

        // Busca setor e conta disponível via on_duty
        const sectorRow = sectorName
          ? await queryOne(`SELECT id FROM sectors WHERE name ILIKE $1 AND tenant_id=$2 LIMIT 1`, [sectorName, tenantId])
          : null

        const accountRow = sectorRow
          ? await queryOne(
              `SELECT tm.account_id FROM team_members tm
               WHERE tm.sector_id=$1 AND tm.on_duty=TRUE AND tm.notify_human_api=TRUE
               ORDER BY RANDOM() LIMIT 1`,
              [sectorRow.id]
            )
          : null

        const threadId = `hapi-${chatId.replace(/[^a-z0-9]/gi,'')}-${Date.now()}`
        const timeoutAt = new Date(Date.now() + 5 * 60 * 1000)

        // ── Enriquecer a mensagem com contexto do banco ────────────────────────
        let contexto = ''
        try {
          const { extractMentions, findCotacaoById, findClienteByPhone, normalizePhone } =
            require('./plugins/newwhats/context-builder')

          // Descobrir telefone do cliente via nw_event_logs
          const logRow = await queryOne(
            `SELECT payload->>'author' AS author, payload->>'pushName' AS push_name
             FROM nw_event_logs
             WHERE chat_id=$1 AND direction='in'
             ORDER BY created_at DESC LIMIT 1`,
            [chatId]
          )
          const phone      = normalizePhone(logRow?.author || '')
          const pushName   = logRow?.push_name || ''

          // Cliente cadastrado
          const cliente = phone ? await findClienteByPhone(phone, tenantId) : null
          if (cliente || pushName) {
            contexto += `\n👤 Cliente: ${cliente?.nome || pushName || 'Não identificado'}`
            if (cliente?.tipo)     contexto += ` (${cliente.tipo})`
            if (cliente?.cpf_cnpj) contexto += ` — ${cliente.cpf_cnpj}`
            if (phone)             contexto += `\n📱 WhatsApp: ${phone}`
          }

          // Menções a cotações/pedidos na pergunta
          const mencoes = extractMentions(question)
          for (const m of mencoes) {
            if (m.type === 'cotacao_id') {
              const cot = await findCotacaoById(m.value, tenantId)
              if (cot) {
                const jaEPedido = cot.cotacao_status === 'finalizada' && cot.pedido_id
                if (jaEPedido) {
                  // Pedido é a entidade principal — cotação virou histórico
                  const prot = cot.pedido_protocolo || `#${cot.pedido_id}`
                  contexto += `\n\n📦 PEDIDO ${prot}:`
                  contexto += `\n• Status: ${cot.pedido_status || '—'}`
                  contexto += `\n• Total: R$ ${parseFloat(cot.total || 0).toFixed(2)}`
                  contexto += `\n• Pagamento: ${cot.forma_pagto || '—'}`
                  if (cot.financeiro_status && cot.financeiro_status !== 'n/a')
                    contexto += ` (financeiro: ${cot.financeiro_status})`
                  contexto += `\n• Entrega: ${cot.tipo_entrega || '—'}`
                  if (cot.pedido_criado_em)
                    contexto += `\n• Pedido criado em: ${new Date(cot.pedido_criado_em).toLocaleDateString('pt-BR')}`
                  // Itens do pedido (mais atualizados que os da cotação)
                  const itensPed = await query(
                    `SELECT nome_produto, quantidade, subtotal, observacao FROM pedido_itens
                     WHERE pedido_id=$1 ORDER BY id LIMIT 10`,
                    [cot.pedido_id]
                  )
                  if (itensPed.length) {
                    contexto += `\n• Itens (${itensPed.length}):`
                    itensPed.forEach(i => {
                      contexto += `\n  - ${i.quantidade}x ${i.nome_produto} — R$ ${parseFloat(i.subtotal).toFixed(2)}`
                      if (i.observacao) contexto += ` [obs: ${i.observacao}]`
                    })
                  }
                } else {
                  // Cotação ainda em aberto
                  contexto += `\n\n📋 Cotação #${m.value}:`
                  contexto += `\n• Status: ${cot.cotacao_status}`
                  contexto += `\n• Total: R$ ${parseFloat(cot.total || 0).toFixed(2)}`
                  const itensCot = await query(
                    `SELECT nome_produto, quantidade, subtotal, observacao FROM cotacao_itens
                     WHERE cotacao_id=$1 ORDER BY id LIMIT 10`,
                    [m.value]
                  )
                  if (itensCot.length) {
                    contexto += `\n• Itens (${itensCot.length}):`
                    itensCot.forEach(i => {
                      contexto += `\n  - ${i.quantidade}x ${i.nome_produto} — R$ ${parseFloat(i.subtotal).toFixed(2)}`
                      if (i.observacao) contexto += ` [obs: ${i.observacao}]`
                    })
                  }
                }
              }
            }
          }
        } catch (e) { console.warn('[human-api/request] enrich failed:', e.message) }

        const mensagemOperador = `🤖 A IA precisa de ajuda:\n"${question}"${contexto}\n\n↩️ Responda com as informações para que a IA possa continuar o atendimento.`

        const reqRow = await queryOne(
          `INSERT INTO human_api_requests
             (tenant_id, conversation_id, chat_id, sector_id, account_id, question, status, delivery_mode, timeout_at)
           VALUES ($1,$2,$3,$4,$5,$6,'pending','inbox_interna',$7)
           RETURNING id`,
          [tenantId, conversationId || null, chatId || null, sectorRow?.id || null, accountRow?.account_id || null, question, timeoutAt]
        )

        await execute(
          `INSERT INTO internal_messages (tenant_id, thread_id, from_account, to_account, sector_id, type, content, ref_id)
           VALUES ($1,$2,NULL,$3,$4,'human_api',$5,$6)`,
          [tenantId, threadId, accountRow?.account_id || null, sectorRow?.id || null, mensagemOperador, String(reqRow.id)]
        )

        await humanApiQueue.add('timeout', { requestId: reqRow.id }, { delay: 5 * 60 * 1000, jobId: `hapi-${reqRow.id}` })

        sseBroadcast('human_api.new', { requestId: reqRow.id, threadId, question, sectorName })
        res.json({ requestId: reqRow.id, threadId })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    app.post('/api/human-api/answer/:id', verifyAnyToken, async (req, res) => {
      try {
        const requestId = parseInt(req.params.id)
        const { answer } = req.body
        const accountId  = req.teamAccount?.accountId || req.admin?.id
        const tenantId   = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id
        if (!answer?.trim()) return res.status(400).json({ error: 'answer obrigatório' })

        const reqRow = await queryOne(
          `UPDATE human_api_requests
           SET status='answered', answer=$1, account_id=$2, answered_at=NOW()
           WHERE id=$3 AND status='pending'
           RETURNING id, thread_id, conversation_id`,
          [answer.trim(), accountId, requestId]
        )
        if (!reqRow) return res.status(404).json({ error: 'Pedido não encontrado ou já respondido' })

        await execute(
          `INSERT INTO internal_messages (tenant_id, thread_id, from_account, type, content, ref_id)
           VALUES ($1,$2,$3,'collaboration',$4,$5)`,
          [tenantId, reqRow.thread_id, accountId, answer.trim(), String(requestId)]
        )

        // Remove job de timeout do BullMQ
        try {
          const job = await humanApiQueue.getJob(`hapi-${requestId}`)
          if (job) await job.remove()
        } catch { /* sem Redis ou job já processado */ }

        sseBroadcast('human_api.answered', { requestId, threadId: reqRow.thread_id, answer: answer.trim() })

        // Retoma a IA no motor com a resposta do especialista
        if (reqRow.conversation_id) {
          try {
            const cfgRows = await query(`SELECT key, value FROM plugin_configs WHERE plugin_id='newwhats'`)
            const cfg = {}; for (const r of cfgRows) cfg[r.key] = r.value
            if (cfg.newwhats_url && cfg.integration_key) {
              await fetch(`${cfg.newwhats_url}/api/ext/v1/secretaria/ask`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'x-nw-key': cfg.integration_key },
                body: JSON.stringify({
                  conversationId: reqRow.conversation_id,
                  message: `[SISTEMA] Especialista respondeu: ${answer.trim()}`,
                  systemExtra: 'Use esta informação para responder ao cliente de forma adequada.',
                }),
              })
            }
          } catch (e) { console.warn('[human-api/answer] motor notify failed:', e.message) }
        }

        res.json({ ok: true, requestId, threadId: reqRow.thread_id })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── Sincronizar setor do chat (motor → banco local) ────────────────────────
    app.post('/api/nw/chat-sector', async (req, res) => {
      try {
        const internalKey = req.headers['x-internal-secret']
        if (!INTERNAL_SECRET || internalKey !== INTERNAL_SECRET) return res.status(403).json({ error: 'Proibido' })
        const { chatId, sectorId, tenantId = 1 } = req.body
        if (!chatId || !sectorId) return res.status(400).json({ error: 'chatId e sectorId obrigatórios' })
        await execute(
          `INSERT INTO nw_chat_sectors (chat_id, tenant_id, sector_id, updated_at)
           VALUES ($1,$2,$3,NOW())
           ON CONFLICT (chat_id, tenant_id) DO UPDATE SET sector_id=$3, updated_at=NOW()`,
          [chatId, tenantId, sectorId]
        )
        res.json({ ok: true })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── SLA monitor — verifica escalações sem resposta (cron a cada minuto) ────
    const cron = require('node-cron')
    cron.schedule('* * * * *', async () => {
      try {
        const breaches = await query(
          `SELECT im.thread_id, im.sector_id, s.name AS sector_name, s.sla_minutes,
                  im.created_at AS escalated_at,
                  EXTRACT(EPOCH FROM (NOW() - im.created_at))/60 AS elapsed_minutes
           FROM internal_messages im
           JOIN sectors s ON s.id = im.sector_id
           WHERE im.type = 'escalation'
             AND s.sla_minutes IS NOT NULL
             AND im.created_at > NOW() - INTERVAL '24 hours'
             AND NOT EXISTS (
               SELECT 1 FROM internal_messages r
               WHERE r.thread_id = im.thread_id AND r.from_account IS NOT NULL
                 AND r.created_at > im.created_at
             )
             AND EXTRACT(EPOCH FROM (NOW() - im.created_at))/60 > s.sla_minutes`
        )
        for (const b of breaches) {
          sseBroadcast('sla.breach', {
            threadId:       b.thread_id,
            sectorId:       b.sector_id,
            sectorName:     b.sector_name,
            slaMins:        b.sla_minutes,
            elapsedMins:    Math.round(b.elapsed_minutes),
          })
        }
      } catch { /* silencioso */ }
    })

    // ── Encerrar protocolo + pesquisa de satisfação ───────────────────────────
    app.post('/api/admin/protocol/close', verifyAnyToken, async (req, res) => {
      try {
        const { threadId, chatId, protocolNumber } = req.body
        const accountId = req.teamAccount?.accountId || req.admin?.id
        const tenantId  = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id || 1
        if (!threadId || !chatId) return res.status(400).json({ error: 'threadId e chatId obrigatórios' })

        // Cria registro de avaliação pendente
        const ratingRow = await queryOne(
          `INSERT INTO protocol_ratings (tenant_id, thread_id, chat_id, account_id)
           VALUES ($1,$2,$3,$4) RETURNING id`,
          [tenantId, threadId, chatId, accountId || null]
        )

        // Insere mensagem de sistema na thread
        await execute(
          `INSERT INTO internal_messages (tenant_id, thread_id, type, content, ref_id)
           VALUES ($1,$2,'system','✅ Protocolo encerrado. Pesquisa de satisfação enviada ao cliente.',$3)`,
          [tenantId, threadId, protocolNumber || null]
        )

        // Envia mensagem WhatsApp ao cliente via motor
        const cfgRows = await query(`SELECT key, value FROM plugin_configs WHERE plugin_id='newwhats'`)
        const cfg = {}; for (const r of cfgRows) cfg[r.key] = r.value
        if (cfg.newwhats_url && cfg.integration_key) {
          const surveyText = `Seu atendimento foi encerrado${protocolNumber ? ` (protocolo #${protocolNumber})` : ''}.\n\nComo você avalia o atendimento que recebeu?\n\nResponda com um número de *1 a 5*:\n1️⃣ Muito ruim\n2️⃣ Ruim\n3️⃣ Regular\n4️⃣ Bom\n5️⃣ Excelente`
          await fetch(`${cfg.newwhats_url}/api/ext/v1/inbox/${encodeURIComponent(chatId)}/send`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'x-nw-key': cfg.integration_key },
            body: JSON.stringify({ text: surveyText }),
          }).catch(e => console.warn('[protocol/close] send survey failed:', e.message))
        }

        // Retoma IA (zera handoff)
        await execute(
          `UPDATE nw_handoff SET human_until=NULL, updated_at=NOW()
           WHERE chat_id=$1 AND tenant_id=$2`,
          [chatId, tenantId]
        )
        sseBroadcast('handoff.expired', { chatId })
        res.json({ ok: true, ratingId: ratingRow.id })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── Flags de protocolo (reclamação / sugestão / elogio) ───────────────────
    app.post('/api/admin/protocol/flag', verifyAnyToken, async (req, res) => {
      try {
        const { threadId, type, note } = req.body
        const accountId = req.teamAccount?.accountId || req.admin?.id
        const tenantId  = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id || 1
        if (!threadId || !['complaint','suggestion','compliment'].includes(type))
          return res.status(400).json({ error: 'threadId e type obrigatórios' })

        const row = await queryOne(
          `INSERT INTO protocol_flags (tenant_id, thread_id, account_id, type, note)
           VALUES ($1,$2,$3,$4,$5) RETURNING id`,
          [tenantId, threadId, accountId || null, type, note?.trim() || null]
        )
        const typeLabel = { complaint: '🔴 Reclamação', suggestion: '💡 Sugestão', compliment: '⭐ Elogio' }[type]
        await execute(
          `INSERT INTO internal_messages (tenant_id, thread_id, from_account, type, content)
           VALUES ($1,$2,$3,'system',$4)`,
          [tenantId, threadId, accountId || null, `${typeLabel} registrado${note ? `: ${note.trim()}` : ''}`]
        )
        res.json({ ok: true, flagId: row.id })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    app.get('/api/admin/protocol/flags', verifyAnyToken, async (req, res) => {
      try {
        const tenantId = req.tenantId || req.admin?.tenant_id || req.teamAccount?.tenantId || 1
        const { period = '7d' } = req.query
        const intervalMap = { today: '1 day', '7d': '7 days', '30d': '30 days' }
        const interval = intervalMap[period] || '7 days'
        const rows = await query(
          `SELECT pf.id, pf.type, pf.note, pf.thread_id, pf.created_at, a.name AS account_name
           FROM protocol_flags pf
           LEFT JOIN accounts a ON a.id = pf.account_id
           WHERE pf.tenant_id=$1 AND pf.created_at > NOW()-INTERVAL '${interval}'
           ORDER BY pf.created_at DESC LIMIT 200`,
          [tenantId]
        )
        const summary = await queryOne(
          `SELECT
             COUNT(*) FILTER (WHERE type='complaint')  AS complaints,
             COUNT(*) FILTER (WHERE type='suggestion') AS suggestions,
             COUNT(*) FILTER (WHERE type='compliment') AS compliments
           FROM protocol_flags WHERE tenant_id=$1 AND created_at > NOW()-INTERVAL '${interval}'`,
          [tenantId]
        )
        res.json({ rows, summary })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── Knowledge Learning — salvar e promover respostas ao brain ─────────────
    app.post('/api/admin/knowledge/save', verifyAnyToken, async (req, res) => {
      try {
        const { question, answer, sectorId } = req.body
        const accountId = req.teamAccount?.accountId || req.admin?.id
        const tenantId  = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id || 1
        if (!question?.trim() || !answer?.trim()) return res.status(400).json({ error: 'question e answer obrigatórios' })
        const row = await queryOne(
          `INSERT INTO knowledge_learning (tenant_id, sector_id, account_id, question, answer)
           VALUES ($1,$2,$3,$4,$5) RETURNING id`,
          [tenantId, sectorId || null, accountId || null, question.trim(), answer.trim()]
        )
        res.json({ ok: true, id: row.id })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    app.post('/api/admin/knowledge/promote/:id', verifyAnyToken, async (req, res) => {
      try {
        const tenantId = req.tenantId || req.admin?.tenant_id || req.teamAccount?.tenantId || 1
        const kl = await queryOne(
          `UPDATE knowledge_learning SET promoted=TRUE, promoted_at=NOW()
           WHERE id=$1 AND tenant_id=$2 AND promoted=FALSE
           RETURNING *`,
          [req.params.id, tenantId]
        )
        if (!kl) return res.status(404).json({ error: 'Registro não encontrado ou já promovido' })
        if (!kl.sector_id) return res.status(400).json({ error: 'Conhecimento sem setor associado não pode ser promovido' })
        const brain = await queryOne(
          `INSERT INTO sector_brain_nodes (sector_id, type, content)
           VALUES ($1,'knowledge',$2) RETURNING id`,
          [kl.sector_id, `P: ${kl.question}\nR: ${kl.answer}`]
        )
        res.json({ ok: true, brainNodeId: brain.id })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    app.get('/api/admin/knowledge', verifyAnyToken, async (req, res) => {
      try {
        const tenantId = req.tenantId || req.admin?.tenant_id || req.teamAccount?.tenantId || 1
        const { promoted } = req.query
        const promotedFilter = promoted === 'false' || promoted === 'true'
        const rows = await query(
          `SELECT kl.*, a.name AS account_name, s.name AS sector_name
           FROM knowledge_learning kl
           LEFT JOIN accounts a ON a.id = kl.account_id
           LEFT JOIN sectors  s ON s.id = kl.sector_id
           WHERE kl.tenant_id=$1
             AND ($2::boolean IS NULL OR kl.promoted = $2)
           ORDER BY kl.created_at DESC LIMIT 100`,
          [tenantId, promotedFilter ? promoted === 'true' : null]
        )
        res.json(rows)
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── Métricas de atendimento ────────────────────────────────────────────────
    app.get('/api/admin/metrics/attendance', verifyAnyToken, async (req, res) => {
      try {
        const tenantId = req.tenantId || req.admin?.tenant_id || req.teamAccount?.tenantId || 1
        const rawPeriod = req.query.period || '7d'
        const intervalMap = { today: '1 day', '7d': '7 days', '30d': '30 days' }
        const interval = intervalMap[rawPeriod] || '7 days'

        const [byAccount, bySector, humanApiStats, autoReplyStats, dailyEscalations, flagsStats, ratingsStats, resolutionStats] = await Promise.all([
          // Por colaborador: escalações assumidas, human_api respondidas, transferências, tempo médio 1ª resposta
          query(`
            SELECT
              a.id AS account_id,
              a.name,
              a.email,
              COUNT(DISTINCT CASE WHEN im.type='escalation' THEN im.thread_id END)     AS escalations_assumed,
              COUNT(DISTINCT CASE WHEN im.type='collaboration' THEN im.id END)         AS messages_sent,
              COUNT(DISTINCT har.id)                                                   AS human_api_answered,
              ROUND(AVG(
                CASE WHEN im.type='escalation' THEN
                  EXTRACT(EPOCH FROM (
                    (SELECT MIN(r.created_at) FROM internal_messages r
                     WHERE r.thread_id=im.thread_id AND r.from_account IS NOT NULL AND r.created_at > im.created_at)
                    - im.created_at
                  ))/60
                END
              )::numeric, 1)                                                           AS avg_first_response_min
            FROM accounts a
            LEFT JOIN internal_messages im ON im.from_account=a.id
              AND im.tenant_id=$1 AND im.created_at > NOW()-INTERVAL '${interval}'
            LEFT JOIN human_api_requests har ON har.account_id=a.id AND har.status='answered'
              AND har.answered_at > NOW()-INTERVAL '${interval}'
            WHERE a.tenant_id=$1
            GROUP BY a.id, a.name, a.email
            ORDER BY escalations_assumed DESC, messages_sent DESC
          `, [tenantId]),

          // Por setor: total de escalações, breaches de SLA, tempo médio de resposta
          query(`
            SELECT
              s.id AS sector_id, s.name, s.color, s.sla_minutes,
              COUNT(DISTINCT im.thread_id)                             AS escalations_total,
              COUNT(DISTINCT CASE
                WHEN EXTRACT(EPOCH FROM (
                  (SELECT MIN(r.created_at) FROM internal_messages r
                   WHERE r.thread_id=im.thread_id AND r.from_account IS NOT NULL AND r.created_at > im.created_at)
                  - im.created_at
                ))/60 > s.sla_minutes THEN im.thread_id END)          AS sla_breaches,
              ROUND(AVG(
                EXTRACT(EPOCH FROM (
                  (SELECT MIN(r.created_at) FROM internal_messages r
                   WHERE r.thread_id=im.thread_id AND r.from_account IS NOT NULL AND r.created_at > im.created_at)
                  - im.created_at
                ))/60
              )::numeric, 1)                                           AS avg_response_min
            FROM sectors s
            LEFT JOIN internal_messages im ON im.sector_id=s.id AND im.type='escalation'
              AND im.tenant_id=$1 AND im.created_at > NOW()-INTERVAL '${interval}'
            WHERE s.tenant_id=$1 AND s.active=TRUE
            GROUP BY s.id, s.name, s.color, s.sla_minutes
            ORDER BY escalations_total DESC
          `, [tenantId]),

          // Human API: total, respondidas, timeout
          queryOne(`
            SELECT
              COUNT(*)                                              AS total,
              COUNT(*) FILTER (WHERE status='answered')            AS answered,
              COUNT(*) FILTER (WHERE status='timeout')            AS timeout,
              COUNT(*) FILTER (WHERE status='ignored')            AS ignored,
              ROUND(
                100.0 * COUNT(*) FILTER (WHERE status='answered')
                / NULLIF(COUNT(*),0), 1
              )                                                    AS rate_pct
            FROM human_api_requests
            WHERE tenant_id=$1 AND asked_at > NOW()-INTERVAL '${interval}'
          `, [tenantId]),

          // Auto-reply IA: mensagens recebidas vs respondidas (com isolamento por tenant)
          queryOne(`
            SELECT
              (SELECT COUNT(*) FROM nw_event_logs
               WHERE event='message.new' AND direction='in' AND tenant_id=$1
                 AND created_at > NOW()-INTERVAL '${interval}')  AS total_in,
              (SELECT COUNT(*) FROM nw_auto_replies
               WHERE status='sent' AND tenant_id=$1
                 AND created_at > NOW()-INTERVAL '${interval}')  AS total_replied
          `, [tenantId]),

          // Escalações por dia (gráfico temporal)
          query(`
            SELECT TO_CHAR(DATE_TRUNC('day', created_at), 'DD/MM') AS day,
                   COUNT(DISTINCT thread_id) AS count
            FROM internal_messages
            WHERE tenant_id=$1 AND type='escalation'
              AND created_at > NOW()-INTERVAL '${interval}'
            GROUP BY DATE_TRUNC('day', created_at)
            ORDER BY DATE_TRUNC('day', created_at)
          `, [tenantId]),

          // Flags: reclamações, sugestões, elogios
          queryOne(`
            SELECT
              COUNT(*) FILTER (WHERE type='complaint')  AS complaints,
              COUNT(*) FILTER (WHERE type='suggestion') AS suggestions,
              COUNT(*) FILTER (WHERE type='compliment') AS compliments
            FROM protocol_flags
            WHERE tenant_id=$1 AND created_at > NOW()-INTERVAL '${interval}'
          `, [tenantId]),

          // Avaliações de satisfação
          queryOne(`
            SELECT
              COUNT(*)                                         AS total_sent,
              COUNT(*) FILTER (WHERE status='rated')          AS total_rated,
              ROUND(AVG(rating) FILTER (WHERE rating IS NOT NULL), 1) AS avg_rating
            FROM protocol_ratings
            WHERE tenant_id=$1 AND sent_at > NOW()-INTERVAL '${interval}'
          `, [tenantId]),

          // Tempo médio de resolução (escalação → encerramento do protocolo)
          queryOne(`
            SELECT ROUND(
              AVG(
                EXTRACT(EPOCH FROM (pr.sent_at - esc.first_escalation)) / 60
              )::numeric, 1
            ) AS avg_resolution_min,
            COUNT(pr.id) AS total_resolved
            FROM protocol_ratings pr
            JOIN (
              SELECT thread_id, MIN(created_at) AS first_escalation
              FROM internal_messages
              WHERE tenant_id=$1 AND type='escalation'
              GROUP BY thread_id
            ) esc ON esc.thread_id = pr.thread_id
            WHERE pr.tenant_id=$1
              AND pr.sent_at > NOW()-INTERVAL '${interval}'
          `, [tenantId])
        ])

        const tin  = parseInt(autoReplyStats?.total_in  ?? 0)
        const trep = parseInt(autoReplyStats?.total_replied ?? 0)

        res.json({
          period: rawPeriod,
          by_account: byAccount,
          by_sector:  bySector,
          human_api: {
            total:    parseInt(humanApiStats?.total   ?? 0),
            answered: parseInt(humanApiStats?.answered ?? 0),
            timeout:  parseInt(humanApiStats?.timeout  ?? 0),
            ignored:  parseInt(humanApiStats?.ignored  ?? 0),
            rate_pct: parseFloat(humanApiStats?.rate_pct ?? 0),
          },
          auto_replies: {
            total_in:      tin,
            total_replied: trep,
            rate_pct: tin > 0 ? Math.round((trep / tin) * 100) : 0,
          },
          daily_escalations: dailyEscalations,
          flags: {
            complaints:  parseInt(flagsStats?.complaints  ?? 0),
            suggestions: parseInt(flagsStats?.suggestions ?? 0),
            compliments: parseInt(flagsStats?.compliments ?? 0),
          },
          ratings: {
            total_sent:  parseInt(ratingsStats?.total_sent  ?? 0),
            total_rated: parseInt(ratingsStats?.total_rated ?? 0),
            avg_rating:  ratingsStats?.avg_rating ? parseFloat(ratingsStats.avg_rating) : null,
          },
          resolution: {
            avg_minutes:    resolutionStats?.avg_resolution_min ? parseFloat(resolutionStats.avg_resolution_min) : null,
            total_resolved: parseInt(resolutionStats?.total_resolved ?? 0),
          },
        })
      } catch (err) {
        console.error('[metrics/attendance]', err.message)
        res.status(500).json({ error: err.message })
      }
    })

    // ── Histórico de avaliações de satisfação (paginado) ──────────────────────
    app.get('/api/admin/metrics/ratings-history', verifyAnyToken, async (req, res) => {
      try {
        const tenantId = req.tenantId || req.admin?.tenant_id || req.teamAccount?.tenantId || 1
        const limit    = Math.min(parseInt(req.query.limit  || '20', 10), 100)
        const offset   = Math.max(parseInt(req.query.offset || '0',  10), 0)
        const rating   = req.query.rating ? parseInt(req.query.rating, 10) : null   // filtro por nota (1–5)
        const rows = await query(`
          SELECT
            pr.id, pr.thread_id, pr.chat_id, pr.rating, pr.status,
            pr.sent_at, pr.rated_at,
            a.name   AS account_name,
            a.email  AS account_email
          FROM protocol_ratings pr
          LEFT JOIN accounts a ON a.id = pr.account_id
          WHERE pr.tenant_id = $1
            AND ($2::smallint IS NULL OR pr.rating = $2)
          ORDER BY pr.sent_at DESC
          LIMIT $3 OFFSET $4
        `, [tenantId, rating, limit, offset])
        const { count } = await queryOne(`
          SELECT COUNT(*) AS count FROM protocol_ratings
          WHERE tenant_id=$1 AND ($2::smallint IS NULL OR rating=$2)
        `, [tenantId, rating])
        res.json({ rows, total: parseInt(count), limit, offset })
      } catch (err) {
        console.error('[ratings-history]', err.message)
        res.status(500).json({ error: err.message })
      }
    })

    // ── Falhas definitivas de envio WhatsApp ───────────────────────────────────
    app.get('/api/admin/notification-errors', verifyToken, async (req, res) => {
      try {
        const { phone } = req.query
        const limit  = Math.min(parseInt(req.query.limit  || '100', 10), 1000)
        const offset = Math.max(parseInt(req.query.offset || '0',   10), 0)
        const rows = await query(
          `SELECT id, job_id, log_id, phone, type, error_message, payload, tenant_id, created_at
           FROM notification_errors
           WHERE ($1::text IS NULL OR phone ILIKE '%' || $1 || '%')
           ORDER BY created_at DESC
           LIMIT $2 OFFSET $3`,
          [phone || null, limit, offset]
        )
        const total = await queryOne(
          `SELECT COUNT(*) AS cnt FROM notification_errors
           WHERE ($1::text IS NULL OR phone ILIKE '%' || $1 || '%')`,
          [phone || null]
        )
        res.json({ rows, total: parseInt(total?.cnt ?? 0) })
      } catch (err) {
        console.error('[notification-errors]', err.message)
        res.status(500).json({ error: 'Erro interno' })
      }
    })

    // ── Sync Knowledge — sincroniza base de conhecimento com o motor ──────────
    app.post('/api/admin/nw/sync-knowledge', verifyToken, async (req, res) => {
      try {
        const { syncKnowledge } = require('./plugins/newwhats/sync-knowledge')
        const result = await syncKnowledge()
        res.json(result)
      } catch (err) {
        console.error('[sync-knowledge]', err.message)
        res.status(500).json({ error: err.message })
      }
    })

    // ── Preview knowledge — retorna o texto sem sincronizar ───────────────────
    app.get('/api/admin/nw/sync-knowledge/preview', verifyToken, async (req, res) => {
      try {
        const { buildKnowledgeText } = require('./plugins/newwhats/sync-knowledge')
        const preview = await buildKnowledgeText()
        res.json({ preview, chars: preview.length })
      } catch (err) {
        console.error('[sync-knowledge/preview]', err.message)
        res.status(500).json({ error: err.message })
      }
    })

    // ── Métricas de auto-resposta IA (Frente 5 v2.2) ──────────────────────────
    app.get('/api/admin/nw/metrics', verifyToken, async (req, res) => {
      try {
        const period = req.query.period === '30d' ? 30 : req.query.period === '1d' ? 1 : 7
        const [total_in, total_replied, by_instance, recent_replies] = await Promise.all([
          queryOne(
            `SELECT COUNT(*) AS cnt FROM nw_event_logs
             WHERE event = 'message.new' AND direction = 'in'
               AND created_at > NOW() - ($1::int * INTERVAL '1 day')`,
            [period]
          ),
          queryOne(
            `SELECT COUNT(*) AS cnt FROM nw_auto_replies
             WHERE status = 'sent' AND created_at > NOW() - ($1::int * INTERVAL '1 day')`,
            [period]
          ),
          query(
            `SELECT instance_id, COUNT(*) AS total
             FROM nw_auto_replies
             WHERE created_at > NOW() - ($1::int * INTERVAL '1 day')
             GROUP BY instance_id ORDER BY total DESC LIMIT 5`,
            [period]
          ),
          query(
            `SELECT chat_id, reply, created_at FROM nw_auto_replies
             WHERE status = 'sent' ORDER BY created_at DESC LIMIT 10`
          ),
        ])
        const tin  = parseInt(total_in?.cnt  ?? 0, 10)
        const trep = parseInt(total_replied?.cnt ?? 0, 10)
        res.json({
          period_days:   period,
          total_in:      tin,
          total_replied: trep,
          auto_rate:     tin > 0 ? Math.round((trep / tin) * 100) : 0,
          by_instance,
          recent_replies,
        })
      } catch (err) {
        console.error('[nw/metrics]', err.message)
        res.status(500).json({ error: 'Erro interno' })
      }
    })

    // ── Handoff local por chat (pausa IA 15 min) ──────────────────────────────
    app.get('/api/nw/handoff/:chatId', verifyAnyToken, async (req, res) => {
      try {
        const tenantId = req.tenantId || req.admin?.tenant_id || req.teamAccount?.tenantId
        const row = await queryOne(
          `SELECT human_until FROM nw_handoff WHERE chat_id=$1 AND tenant_id=$2`,
          [req.params.chatId, tenantId]
        )
        const humanUntil = row?.human_until ? new Date(row.human_until) : null
        const isHuman = humanUntil && humanUntil > new Date()
        res.json({ mode: isHuman ? 'humano' : 'ia', handoffHumanAt: humanUntil?.toISOString() ?? null })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    app.patch('/api/nw/handoff/:chatId', verifyAnyToken, async (req, res) => {
      try {
        const tenantId = req.tenantId || req.admin?.tenant_id || req.teamAccount?.tenantId
        const { mode } = req.body
        if (!['ia', 'humano'].includes(mode)) return res.status(400).json({ error: 'mode inválido' })
        const humanUntil = mode === 'humano'
          ? new Date(Date.now() + 15 * 60 * 1000)
          : null
        await execute(
          `INSERT INTO nw_handoff (chat_id, tenant_id, human_until, updated_at)
           VALUES ($1,$2,$3,NOW())
           ON CONFLICT (chat_id, tenant_id) DO UPDATE SET human_until=$3, updated_at=NOW()`,
          [req.params.chatId, tenantId, humanUntil]
        )
        if (mode === 'humano') {
          sseHandoffQueue.add('handoff-expire', { chatId: req.params.chatId, tenantId }, { delay: 15 * 60 * 1000 })
        }
        res.json({ mode, handoffHumanAt: humanUntil?.toISOString() ?? null })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── Modo auto-resposta (lê e salva) ────────────────────────────────────────
    app.get('/api/admin/nw/auto-reply-mode', verifyAnyToken, async (req, res) => {
      try {
        const row = await queryOne(
          `SELECT value FROM plugin_configs WHERE plugin_id = 'newwhats' AND key = 'auto_reply_mode'`
        )
        res.json({ mode: row?.value ?? 'off' })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    app.put('/api/admin/nw/auto-reply-mode', verifyToken, async (req, res) => {
      try {
        const { mode } = req.body
        if (!['off', 'automatico'].includes(mode))
          return res.status(400).json({ error: 'mode deve ser off ou automatico' })
        await execute(
          `INSERT INTO plugin_configs (plugin_id, key, value)
           VALUES ('newwhats', 'auto_reply_mode', $1)
           ON CONFLICT (plugin_id, key) DO UPDATE SET value = $1`,
          [mode]
        )
        res.json({ ok: true, mode })
      } catch (err) { res.status(500).json({ error: err.message }) }
    })

    // ── Eventos recebidos via webhook NewWhats (nw_event_logs) ─────────────────
    app.get('/api/admin/nw-events', verifyToken, async (req, res) => {
      try {
        const { event, instance_id, direction, limit = 100, offset = 0 } = req.query
        const rows = await query(
          `SELECT id, instance_id, event, direction, chat_id, from_me, msg_type, payload, created_at
           FROM nw_event_logs
           WHERE ($1::text IS NULL OR event = $1)
             AND ($2::text IS NULL OR instance_id = $2)
             AND ($3::text IS NULL OR direction = $3)
           ORDER BY created_at DESC
           LIMIT $4 OFFSET $5`,
          [event || null, instance_id || null, direction || null, +limit, +offset]
        )
        const total = await queryOne(
          `SELECT COUNT(*) AS cnt FROM nw_event_logs
           WHERE ($1::text IS NULL OR event = $1)
             AND ($2::text IS NULL OR instance_id = $2)
             AND ($3::text IS NULL OR direction = $3)`,
          [event || null, instance_id || null, direction || null]
        )
        res.json({ rows, total: parseInt(total?.cnt ?? 0) })
      } catch (err) {
        console.error('[nw-events]', err.message)
        res.status(500).json({ error: 'Erro interno' })
      }
    })

    // ── Super Admin API ────────────────────────────────────────────────────────
    const { verifySuperToken } = require('./middleware/superAuth')
    const superAdminCtrl       = require('./controllers/superAdminController')

    app.post('/superadmin/api/login',    superAdminCtrl.login)
    app.post('/superadmin/api/logout',   superAdminCtrl.logout)
    app.get( '/superadmin/api/me',       verifySuperToken, superAdminCtrl.me)

    app.get( '/superadmin/api/tenants',          verifySuperToken, superAdminCtrl.listTenants)
    app.post('/superadmin/api/tenants',          verifySuperToken, superAdminCtrl.createTenant)
    app.put( '/superadmin/api/tenants/:id',      verifySuperToken, superAdminCtrl.updateTenant)
    app.delete('/superadmin/api/tenants/:id',    verifySuperToken, superAdminCtrl.deleteTenant)
    app.get( '/superadmin/api/tenants/:id/stats',verifySuperToken, superAdminCtrl.getTenantStats)
    app.post('/superadmin/api/tenants/:id/admins',verifySuperToken, superAdminCtrl.createTenantAdmin)

    // Serve superadmin SPA
    app.get('/superadmin*', (req, res) => {
      res.sendFile(path.join(__dirname, 'public', 'superadmin', 'index.html'))
    })

    // Status endpoint
    app.get('/api/status', (req, res) => {
        res.json({ status: 'online', system: 'nuvem', timestamp: new Date().toISOString() });
    });

    // ── Tenant config pública ─────────────────────────────────────────────────
    app.get('/api/tenant/config', (req, res) => {
        const t = req.tenant
        if (!t) return res.status(404).json({ error: 'Tenant não encontrado' })
        res.json({
            id:              t.id,
            slug:            t.slug,
            name:            t.name,
            primary_color:   t.primary_color   || '#f59e0b',
            secondary_color: t.secondary_color || '#10b981',
            logo_url:        t.logo_url        || null,
            favicon_url:     t.favicon_url     || null,
            contact_email:   t.contact_email   || null,
            contact_phone:   t.contact_phone   || null,
        })
    })

    // ── Tenant config admin (atualizar branding) ──────────────────────────────
    app.put('/api/tenant/config', verifyToken, async (req, res) => {
        try {
            const { primary_color, secondary_color, logo_url, favicon_url,
                    contact_email, contact_phone, app_url, asaas_key } = req.body
            const tenantId = req.tenant?.id
            if (!tenantId) return res.status(400).json({ error: 'Tenant não identificado' })

            await execute(
                `UPDATE tenant_config
                 SET primary_color=$1, secondary_color=$2, logo_url=$3, favicon_url=$4,
                     contact_email=$5, contact_phone=$6, app_url=$7, asaas_key=$8
                 WHERE tenant_id=$9`,
                [primary_color, secondary_color, logo_url || null, favicon_url || null,
                 contact_email || null, contact_phone || null, app_url || null,
                 asaas_key || null, tenantId]
            )
            invalidateTenantCache(req.tenant.slug)
            invalidateTenantCache(req.tenant.domain)
            res.json({ ok: true })
        } catch (err) {
            console.error('[tenant config update]', err.message)
            res.status(500).json({ error: 'Erro ao atualizar configuração' })
        }
    })

    // Customer App (React SPA) — serve at /app/*
    app.get('/app', (req, res) => res.redirect('/app/'))
    app.get('/app/*', (req, res) => {
      res.sendFile(path.join(__dirname, 'public', 'app', 'index.html'))
    })

    // React Admin (V2) SPA Route
    app.get('/admin-v2*', (req, res) => {
        res.sendFile(path.join(__dirname, 'public', 'admin-v2', 'index.html'));
    });

    // Admin Auth routes — legacy HTML desabilitado, redireciona para o React app
    app.get('/admin', (req, res) => res.redirect('/admin-v2/'));
    app.post('/admin/login', adminController.login);
    app.get('/admin/dashboard', (req, res) => res.redirect('/admin-v2/'));
    app.get('/admin/logout', adminController.logout);

    // ── Equipe: login e perfil de funcionários ────────────────────────────────
    const bcryptTeam = require('bcryptjs')
    const signTeamToken = _signTeamToken

    app.post('/api/team/login', async (req, res) => {
      try {
        const { email, password } = req.body
        if (!email || !password) return res.status(400).json({ error: 'Email e senha obrigatórios' })

        const account = await queryOne(
          `SELECT id, tenant_id, name, email, password_hash, role, availability, active
           FROM accounts WHERE email = $1`,
          [email.trim().toLowerCase()]
        )

        if (!account) return res.status(401).json({ error: 'Credenciais inválidas' })
        if (!account.active) return res.status(403).json({ error: 'Conta suspensa' })
        if (!account.password_hash) return res.status(401).json({ error: 'Senha não configurada. Peça ao administrador para definir sua senha.' })

        const valid = await bcryptTeam.compare(password, account.password_hash)
        if (!valid) return res.status(401).json({ error: 'Credenciais inválidas' })

        const token = signTeamToken(account)
        // Remove sessão de admin antes de criar sessão de equipe
        res.clearCookie('cloud_token')
        res.cookie('team_token', token, {
          httpOnly: true,
          maxAge: 7 * 24 * 60 * 60 * 1000,
          sameSite: 'lax',
        })

        // atualiza availability para online
        await execute('UPDATE accounts SET availability=$1 WHERE id=$2', ['online', account.id])

        res.json({
          account: { id: account.id, name: account.name, email: account.email, role: account.role },
          tenantId: account.tenant_id,
        })
      } catch (e) {
        console.error('[team/login]', e)
        res.status(500).json({ error: 'Erro interno' })
      }
    })

    // Aceita cloud_token (admin) OU team_token (colaborador).
    // Admin pode ter um account vinculado (campo account_id em admins) que o
    // identifica na Inbox Interna — nesse caso retorna o perfil desse account.
    // Se admin não tiver account vinculado, retorna 404 (sem acesso à inbox).
    // NOTA: account_id NÃO está no payload JWT do admin (evita token inchado),
    // por isso é lido do banco quando necessário.
    app.get('/api/team/me', verifyAnyToken, async (req, res) => {
      try {
        // Resolve o ID do account:
        //   colaborador → req.teamAccount.accountId (do JWT team_token)
        //   admin       → account_id vinculado na tabela admins (não está no JWT)
        let accountId = req.teamAccount?.accountId ?? null
        if (!accountId && req.admin?.id) {
          const adminRow = await queryOne('SELECT account_id FROM admins WHERE id=$1', [req.admin.id])
          accountId = adminRow?.account_id ?? null
        }
        if (!accountId) return res.status(404).json({ error: 'Sem conta de colaborador vinculada' })

        const account = await queryOne(
          `SELECT a.id, a.tenant_id, a.name, a.email, a.role, a.availability, a.phone, a.phone_connected, a.avatar_url,
                  COALESCE(
                    json_agg(json_build_object(
                      'sector_id', tm.sector_id,
                      'sector_name', s.name,
                      'sector_color', s.color,
                      'function', tm.function,
                      'is_primary', tm.is_primary,
                      'on_duty', tm.on_duty,
                      'notify_escalation', tm.notify_escalation,
                      'notify_human_api', tm.notify_human_api
                    )) FILTER (WHERE tm.id IS NOT NULL), '[]'
                  ) AS sectors
           FROM accounts a
           LEFT JOIN team_members tm ON tm.account_id = a.id
           LEFT JOIN sectors s ON s.id = tm.sector_id
           WHERE a.id = $1
           GROUP BY a.id`,
          [accountId]
        )
        if (!account) return res.status(404).json({ error: 'Conta não encontrada' })
        res.json({ account })
      } catch (e) {
        console.error('[team/me]', e)
        res.status(500).json({ error: 'Erro interno' })
      }
    })

    app.post('/api/team/logout', (req, res) => {
      res.clearCookie('team_token')
      res.clearCookie('cloud_token') // garante que nenhuma sessão cruzada persiste
      res.json({ ok: true })
    })

    // Upload de imagens de produtos
    const multer = require('multer');
    const uploadStorage = multer.diskStorage({
        destination: path.join(__dirname, 'public', 'images', 'products'),
        filename: (req, file, cb) => {
            const ext = path.extname(file.originalname).toLowerCase();
            const name = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, '-').toLowerCase();
            cb(null, `${name}-${Date.now()}${ext}`);
        },
    });
    const upload = multer({
        storage: uploadStorage,
        limits: { fileSize: 5 * 1024 * 1024 },
        fileFilter: (req, file, cb) => {
            cb(null, /^image\/(jpeg|png|webp|gif)$/.test(file.mimetype));
        },
    });
    app.post('/api/promotions/upload', verifyToken, upload.single('image'), (req, res) => {
        if (!req.file) return res.status(400).json({ error: 'Ficheiro inválido' });
        res.json({ path: `/images/products/${req.file.filename}` });
    });

    // Admin "me" — retorna info do admin logado + branding do seu tenant.
    // ATENÇÃO: esta DEVE ser a única rota GET /api/admin/me. Uma segunda
    // definição acima (Fase 12) foi movida para /api/admin/equipe/me porque
    // o Express executa apenas a primeira rota que bate — duplicatas silenciosas
    // são a causa dos erros 401 no frontend mesmo com admin autenticado.
    app.get('/api/admin/me', verifyToken, async (req, res) => {
      try {
        const tenantId = req.admin?.tenant_id || 1
        const [admin, tenantRow] = await Promise.all([
          // Incluímos account_id no SELECT: quando preenchido, o frontend usa
          // esse valor como identidade na Inbox Interna sem exigir login duplo.
          queryOne('SELECT id, username, nome, email, tenant_id, account_id FROM admins WHERE id=$1', [req.admin.id]),
          queryOne(`SELECT t.id, t.slug, t.name, tc.primary_color, tc.secondary_color, tc.logo_url
                    FROM tenants t
                    LEFT JOIN tenant_config tc ON tc.tenant_id = t.id
                    WHERE t.id=$1`, [tenantId]),
        ])

        // Se o admin tem um account vinculado, carrega os dados completos do
        // colaborador (nome, avatar, role) para exibir na Inbox Interna.
        let linkedAccount = null
        if (admin?.account_id) {
          linkedAccount = await queryOne(
            'SELECT id, name, email, role, avatar_url FROM accounts WHERE id=$1 AND tenant_id=$2',
            [admin.account_id, tenantId]
          )
        }

        res.json({ admin, tenant: tenantRow, linkedAccount })
      } catch (err) {
        res.status(500).json({ error: 'Erro interno' })
      }
    })

    // ── Vincular admin a um account de colaborador ────────────────────────────
    // Permite que o admin indique qual account da equipe é a sua identidade.
    // Chamado uma única vez pelo painel (ex: Settings ou EquipeTab).
    // Depois disso, /api/admin/me devolve linkedAccount e a Inbox Interna
    // funciona normalmente sem segundo login.
    app.patch('/api/admin/me/account', verifyToken, async (req, res) => {
      try {
        const { account_id } = req.body
        const tenantId = req.admin?.tenant_id || 1

        // Valida que o account pertence ao mesmo tenant (segurança multi-tenant)
        if (account_id !== null) {
          const acc = await queryOne(
            'SELECT id FROM accounts WHERE id=$1 AND tenant_id=$2',
            [account_id, tenantId]
          )
          if (!acc) return res.status(404).json({ error: 'Account não encontrado neste tenant' })
        }

        await execute(
          'UPDATE admins SET account_id=$1 WHERE id=$2',
          [account_id || null, req.admin.id]
        )

        const updated = await queryOne(
          'SELECT id, username, nome, email, tenant_id, account_id FROM admins WHERE id=$1',
          [req.admin.id]
        )
        res.json({ ok: true, admin: updated })
      } catch (err) {
        res.status(500).json({ error: err.message })
      }
    })

    // Admin User Management API
    app.get('/api/admins', verifyToken, adminController.listAdmins);
    app.post('/api/admins', verifyToken, adminController.createAdmin);
    app.put('/api/admins/:id', verifyToken, adminController.updateAdmin);
    app.delete('/api/admins/:id', verifyToken, adminController.deleteAdmin);

    // Admin Order Management API
    app.get('/api/orders', verifyToken, adminController.listOrders);

    // Admin Claim Management API
    app.get('/api/claims', verifyToken, adminController.listClaims);
    app.patch('/api/claims/:id', verifyToken, adminController.updateClaimStatus);

    // Settings Asaas — lê/salva por tenant em tenant_config
    app.get('/api/settings/asaas', verifyToken, async (req, res) => {
        try {
            const tenantId = req.admin?.tenant_id || 1
            const row = await queryOne('SELECT asaas_key FROM tenant_config WHERE tenant_id=$1', [tenantId])
            // Fallback para variável de ambiente para compatibilidade
            res.json({ key: row?.asaas_key || process.env.ASAAS_API_KEY || '' })
        } catch {
            res.json({ key: process.env.ASAAS_API_KEY || '' })
        }
    });

    app.post('/api/settings/asaas', verifyToken, async (req, res) => {
        const { key } = req.body;
        if (!key) return res.status(400).json({ error: 'Key is required' });
        try {
            const tenantId = req.admin?.tenant_id || 1
            await execute(
                `UPDATE tenant_config SET asaas_key=$1 WHERE tenant_id=$2`,
                [key, tenantId]
            )
            res.json({ success: true });
        } catch (err) {
            res.status(500).json({ error: 'Erro ao salvar chave' })
        }
    });

    // ── Rotas públicas �� customer-app React ──────────────────────────────────
    // Legado: redirecionar URLs antigas para o novo app React em /app/
    app.get('/login',          (req, res) => res.redirect('/app/login'))
    app.get('/area-do-usuario',(req, res) => res.redirect('/app/minha-area'))
    app.get('/sorteio',        (req, res) => res.redirect('/app/raffle'))
    app.get('/raffle',         (req, res) => res.redirect('/app/raffle'))
    app.get('/meus-premios',   (req, res) => res.redirect('/app/minha-area'))
    app.get('/clube',          (req, res) => res.redirect('/app/'))
    app.get('/clube/:slug',    (req, res) => res.redirect(`/app/clube/${req.params.slug}`))
    app.get('/carteirinha/:cpf',(req, res) => res.redirect(`/app/carteirinha/${req.params.cpf}`))

    app.get('/user/logout', (req, res) => {
        res.clearCookie('user_token');
        res.clearCookie('_usr');
        res.redirect('/app/login');
    });

    // Landing page → customer-app React
    app.get('/', (req, res) => {
        // Preserva os query params (session, gateway) ao redirecionar para o app
        res.redirect(302, '/app/' + (req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''));
    });

    // Blocked Warning Page (Squidguard Redirect) — mantém HTML estático
    app.get('/bloqueado', (req, res) => {
        res.sendFile(path.join(__dirname, 'views', 'landing', 'bloqueado.html'));
    });

    // ── Plugin System ─────────────────────────────────────────────────────────
    // httpServer criado antes dos plugins para que possam registrar handlers de upgrade (WS proxy)
    const http       = require('http')
    const httpServer = http.createServer(app)
    const pluginManager = new PluginManager(app, { query, queryOne, execute }, httpServer)
    await pluginManager.loadAll()

    // Admin API — Plugins (protegido por verifyToken)
    app.get('/api/plugins', verifyToken, async (req, res) => {
        res.json(await pluginManager.list())
    })

    app.post('/api/plugins/:id/activate', verifyToken, async (req, res) => {
        try {
            await pluginManager.activate(req.params.id)
            res.json({ ok: true })
        } catch (err) {
            res.status(400).json({ error: err.message })
        }
    })

    app.post('/api/plugins/:id/deactivate', verifyToken, async (req, res) => {
        try {
            await pluginManager.deactivate(req.params.id)
            res.json({ ok: true })
        } catch (err) {
            res.status(400).json({ error: err.message })
        }
    })

    app.get('/api/plugins/:id/config', verifyToken, async (req, res) => {
        const config = await pluginManager.getConfig(req.params.id)
        res.json(config)
    })

    app.put('/api/plugins/:id/config', verifyToken, async (req, res) => {
        try {
            await pluginManager.setConfig(req.params.id, req.body)
            res.json({ ok: true })
        } catch (err) {
            res.status(400).json({ error: err.message })
        }
    })

    // ── Proxy de pareamento NewWhats (evita CORS no browser) ─────────────────
    app.post('/api/pair/proxy/request', verifyToken, async (req, res) => {
        const { email, nwUrl } = req.body
        if (!email || !nwUrl) return res.status(400).json({ error: 'email e nwUrl obrigatórios' })
        try {
            const r = await fetch(`${nwUrl}/api/pair/request`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    email,
                    clientSystem: 'alemao-conveniencias',
                    clientName:   'Alemão Conveniências',
                    clientUrl:    `http://localhost:${PORT}`,
                }),
            })
            const data = await r.json()
            res.status(r.status).json(data)
        } catch (err) {
            res.status(502).json({ error: `Não foi possível conectar ao NewWhats: ${err.message}` })
        }
    })

    app.post('/api/pair/proxy/confirm', verifyToken, async (req, res) => {
        const { email, code, nwUrl } = req.body
        if (!email || !code || !nwUrl) return res.status(400).json({ error: 'email, code e nwUrl obrigatórios' })
        try {
            const r = await fetch(`${nwUrl}/api/pair/confirm`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    email,
                    code,
                    clientSystem: 'alemao-conveniencias',
                    clientName:   'Alemão Conveniências',
                    clientUrl:    `http://localhost:${PORT}`,
                }),
            })
            const data = await r.json()
            if (!r.ok) return res.status(r.status).json(data)
            res.json({ integration_key: data.integration_key, account: data.account })
        } catch (err) {
            res.status(502).json({ error: `Não foi possível conectar ao NewWhats: ${err.message}` })
        }
    })

    // WPP-09 — Job de reativação de clientes inativos
    try {
      const { startReativacaoJob } = require('./jobs/reativacaoClientes')
      startReativacaoJob()
    } catch (e) { console.warn('[reativacao] Job não iniciado:', e.message) }

    // FUT-02 — Job de pedidos recorrentes PJ
    try {
      const { startRecorrentesJob } = require('./jobs/pedidosRecorrentes')
      startRecorrentesJob()
    } catch (e) { console.warn('[recorrentes] Job não iniciado:', e.message) }

    // MON-01 — Monitor de mensagens WhatsApp sem resposta
    try {
      const { startMonitorJob } = require('./jobs/monitorMsgsSemResposta')
      startMonitorJob()
    } catch (e) { console.warn('[monitor-wpp] Job não iniciado:', e.message) }

    // Start server
    httpServer.listen(PORT, '0.0.0.0', () => {
        console.log('');
        console.log('╔══════════════════════════════════════════════╗');
        console.log('║   ☁️  SISTEMA NUVEM - PROMOÇÕES + SORTEIO     ║');
        console.log('║   🌐 http://localhost:' + PORT + '                  ║');
        console.log('║   🔒 Admin: http://localhost:' + PORT + '/admin     ║');
        console.log('╚══════════════════════════════════════════════╝');
        console.log('');
    });
}

startServer().catch(err => {
    console.error('❌ Erro ao iniciar servidor nuvem:', err);
    process.exit(1);
});
