"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildExtRoutes = buildExtRoutes;
/**
 * External REST API — /api/ext/v1/
 *
 * Endpoints implementados nesta fase:
 *
 *   GET    /api/ext/v1/sessions                     — lista instâncias do tenant
 *   POST   /api/ext/v1/sessions                     — cria nova instância
 *   DELETE /api/ext/v1/sessions/:id                 — remove instância
 *   GET    /api/ext/v1/sessions/:id/qr              — QR base64 da instância
 *   GET    /api/ext/v1/inbox?session=&search=&limit= — lista chats
 *   GET    /api/ext/v1/inbox/:chatId/messages        — mensagens paginadas
 *   POST   /api/ext/v1/inbox/:chatId/send            — envia mensagem de texto
 *
 * Envelope de resposta de erro: { error: string }
 * Envelope de resposta de sucesso: dados diretos (sem wrapper extra)
 */
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const express_1 = require("express");
const dragonfly_1 = require("../../../backend/src/infra/cache/dragonfly");
const brain_1 = require("../../secretaria/brain");
const rich_message_1 = require("../../../backend/dist/modules/whatsapp/rich-message");
function uuid() {
    try {
        return crypto.randomUUID();
    }
    catch {
        return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
    }
}
// ── Handoff timers (in-memory, por convId) ────────────────────────────────────
const HANDOFF_TIMEOUT_MS = 15 * 60 * 1000;
const handoffTimers = new Map();
function scheduleHandoffTimeout(convId, extChatId, tenantId, db, hooks) {
    const existing = handoffTimers.get(convId);
    if (existing)
        clearTimeout(existing);
    const timer = setTimeout(async () => {
        handoffTimers.delete(convId);
        try {
            await db('sec_conversations').where({ id: convId })
                .update({ handoff_mode: 'ia', handoff_human_at: null });
            // chatId = parte após o primeiro ":" no extChatId ("tenantId:chatId")
            const chatId = extChatId.includes(':') ? extChatId.split(':').slice(1).join(':') : extChatId;
            hooks.emit('ext:handoff', { tenantId, conversationId: convId, chatId, mode: 'ia', reason: 'timeout' });
        }
        catch { }
    }, HANDOFF_TIMEOUT_MS);
    handoffTimers.set(convId, timer);
}
function cancelHandoffTimeout(convId) {
    const existing = handoffTimers.get(convId);
    if (existing) {
        clearTimeout(existing);
        handoffTimers.delete(convId);
    }
}
function buildExtRoutes(prisma, manager, db, config, hooks) {
    const router = (0, express_1.Router)();
    // ── Escalation notification listener ──────────────────────────────────────
    // When escalar_humano tool fires, send a WA message to the configured admin phone.
    hooks.register('ext:escalated', async (data) => {
        try {
            const rawPhone = await config.get('admin_notify_phone');
            const adminPhone = typeof rawPhone === 'string' ? rawPhone.trim() : '';
            if (!adminPhone)
                return;
            // Normalise to JID
            const digits = adminPhone.replace(/\D/g, '');
            const normalized = digits.startsWith('55') ? digits : '55' + digits;
            const jid = `${normalized}@s.whatsapp.net`;
            // Find first connected instance for this tenant
            const instance = await prisma.instance.findFirst({
                where: { tenantId: data.tenantId },
                select: { id: true },
            });
            if (!instance)
                return;
            const sock = manager.getSocket(instance.id);
            if (!sock)
                return;
            const proto = data.protocolNumber ? `#${data.protocolNumber}` : '';
            const motivo = data.motivo ? ` — ${data.motivo}` : '';
            const msg = `⚠️ *Escalação* ${proto}${motivo}\nUm cliente precisa de atendimento humano. Abra o painel para assumir.`;
            await sock.sendMessage(jid, { text: msg });
        }
        catch { /* notificação é best-effort */ }
    });
    // ── GET /sessions ──────────────────────────────────────────────────────────
    // Lista todas as instâncias do tenant com status e telefone.
    router.get('/sessions', async (req, res) => {
        const tenantId = req.extTenantId;
        try {
            const instances = await prisma.instance.findMany({
                where: { tenantId },
                orderBy: { createdAt: 'asc' },
                select: {
                    id: true,
                    name: true,
                    phone: true,
                    status: true,
                    avatar: true,
                    createdAt: true,
                },
            });
            res.json(instances);
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── POST /sessions ────────────────────────────────────────────────────────
    // Cria uma nova instância e dispara a conexão (QR chega via WS).
    router.post('/sessions', async (req, res) => {
        const tenantId = req.extTenantId;
        const { name } = req.body;
        if (!name?.trim()) {
            res.status(400).json({ error: 'name é obrigatório' });
            return;
        }
        try {
            const instance = await prisma.instance.create({
                data: { tenantId, name: name.trim() },
                select: { id: true, name: true, phone: true, status: true, avatar: true, createdAt: true },
            });
            manager.connect(instance.id, tenantId).catch((err) => {
                console.error('[ext-api] Falha ao conectar instância após criação:', err);
            });
            res.status(201).json(instance);
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── DELETE /sessions/:id ──────────────────────────────────────────────────
    // Desconecta e remove a instância do tenant.
    router.delete('/sessions/:id', async (req, res) => {
        const tenantId = req.extTenantId;
        const instanceId = req.params['id'];
        try {
            const instance = await prisma.instance.findFirst({ where: { id: instanceId, tenantId } });
            if (!instance) {
                res.status(404).json({ error: 'Instância não encontrada' });
                return;
            }
            await manager.disconnect(instanceId).catch(() => { });
            await prisma.instance.delete({ where: { id: instanceId } });
            res.json({ message: 'Instância removida' });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── GET /sessions/:id/qr ──────────────────────────────────────────────────
    // Retorna o QR code atual em base64 (poll antes de receber via WS).
    router.get('/sessions/:id/qr', async (req, res) => {
        const tenantId = req.extTenantId;
        const instanceId = req.params['id'];
        try {
            const instance = await prisma.instance.findFirst({
                where: { id: instanceId, tenantId },
                select: { id: true },
            });
            if (!instance) {
                res.status(404).json({ error: 'Instância não encontrada' });
                return;
            }
            const qrBase64 = await dragonfly_1.dragonfly.get(`instance:${instanceId}:qr`);
            if (!qrBase64) {
                res.status(404).json({ error: 'QR não disponível — inicie a conexão primeiro' });
                return;
            }
            res.json({ instanceId, qrBase64 });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── GET /media/:messageId ─────────────────────────────────────────────────
    // Serve o arquivo de mídia de uma mensagem.
    // Se mediaUrl for URL externa (Wasabi etc.) redireciona 302.
    // Se for arquivo local, faz stream com suporte a Range (áudio/vídeo).
    router.get('/media/:messageId', async (req, res) => {
        const tenantId = req.extTenantId;
        const messageId = req.params['messageId'];
        try {
            const msg = await prisma.message.findFirst({
                where: { id: messageId, tenantId },
                select: { mediaUrl: true, mediaPath: true, mimeType: true, fileName: true },
            });
            if (!msg || (!msg.mediaPath && !msg.mediaUrl)) {
                res.status(404).json({ error: 'Mídia não encontrada' });
                return;
            }
            // URL externa (Wasabi / CDN) → redirect
            if (msg.mediaUrl && msg.mediaUrl.startsWith('http')) {
                res.redirect(302, msg.mediaUrl);
                return;
            }
            const filePath = msg.mediaPath;
            if (!filePath) {
                res.status(404).json({ error: 'Arquivo não disponível' });
                return;
            }
            const info = await (0, promises_1.stat)(filePath).catch(() => null);
            if (!info) {
                res.status(404).json({ error: 'Arquivo não encontrado no disco' });
                return;
            }
            const mime = msg.mimeType || 'application/octet-stream';
            const size = info.size;
            // Content-Disposition: inline para imagens/áudio/vídeo, attachment para docs
            const isInline = mime.startsWith('image/') || mime.startsWith('audio/') || mime.startsWith('video/');
            const disposition = isInline
                ? 'inline'
                : `attachment; filename="${msg.fileName ?? 'arquivo'}"`;
            res.setHeader('Content-Disposition', disposition);
            res.setHeader('Content-Type', mime);
            res.setHeader('Accept-Ranges', 'bytes');
            res.setHeader('Cache-Control', 'private, max-age=3600');
            // Suporte a Range (essencial para <audio> e <video> no browser)
            const rangeHeader = req.headers['range'];
            if (rangeHeader) {
                const [startStr, endStr] = rangeHeader.replace(/bytes=/, '').split('-');
                const start = parseInt(startStr, 10);
                const end = endStr ? parseInt(endStr, 10) : size - 1;
                const chunkSize = end - start + 1;
                res.status(206);
                res.setHeader('Content-Range', `bytes ${start}-${end}/${size}`);
                res.setHeader('Content-Length', chunkSize);
                (0, fs_1.createReadStream)(filePath, { start, end }).pipe(res);
            }
            else {
                res.setHeader('Content-Length', size);
                (0, fs_1.createReadStream)(filePath).pipe(res);
            }
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── GET /inbox ────────────────────────────────────────────────────────────
    // Lista chats da instância com snapshot da última mensagem.
    router.get('/inbox', async (req, res) => {
        const tenantId = req.extTenantId;
        const sessionId = req.query['session'];
        const search = req.query['search'];
        const limit = Math.min(parseInt(String(req.query['limit'] || '50')), 200);
        if (!sessionId) {
            res.status(400).json({ error: 'Query param "session" (instanceId) obrigatório' });
            return;
        }
        try {
            // Valida que a instância pertence ao tenant
            const instance = await prisma.instance.findFirst({
                where: { id: sessionId, tenantId },
                select: { id: true },
            });
            if (!instance) {
                res.status(404).json({ error: 'Instância não encontrada' });
                return;
            }
            const chats = await prisma.chat.findMany({
                where: {
                    tenantId,
                    instanceId: sessionId,
                    isArchived: false,
                    NOT: [
                        { jid: { endsWith: '@lid' } },
                        { jid: { contains: '@broadcast' } },
                        { jid: { endsWith: '@newsletter' } },
                        { jid: { startsWith: '0@' } },
                    ],
                    ...(search
                        ? { OR: [
                                { name: { contains: search, mode: 'insensitive' } },
                                { jid: { contains: search } },
                            ] }
                        : {}),
                },
                orderBy: { lastMessageAt: 'desc' },
                take: limit,
                select: {
                    id: true,
                    jid: true,
                    name: true,
                    unreadCount: true,
                    lastMessageAt: true,
                    isPinned: true,
                    isArchived: true,
                    contact: {
                        select: {
                            name: true,
                            verifiedName: true,
                            notify: true,
                            avatarUrl: true,
                            phone: true,
                        },
                    },
                    messages: {
                        orderBy: { timestamp: 'desc' },
                        take: 1,
                        select: { body: true, type: true, fromMe: true, timestamp: true, status: true },
                    },
                },
            });
            // Resolve nome de exibição: Contact.name > Contact.verifiedName > Contact.notify > Chat.name
            const result = chats.map((c) => {
                const ct = c.contact;
                const displayName = ct?.name ?? ct?.verifiedName ?? ct?.notify ?? c.name ?? null;
                const lastMsg = c.messages[0] ?? null;
                return {
                    id: c.id,
                    jid: c.jid,
                    displayName,
                    avatar: ct?.avatarUrl ?? null,
                    phone: ct?.phone ?? c.jid.split('@')[0],
                    unreadCount: c.unreadCount,
                    lastMessageAt: c.lastMessageAt,
                    isPinned: c.isPinned,
                    isArchived: c.isArchived,
                    lastMessage: lastMsg
                        ? { body: lastMsg.body, type: lastMsg.type, fromMe: lastMsg.fromMe, status: lastMsg.status, timestamp: lastMsg.timestamp }
                        : null,
                };
            });
            res.json(result);
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── GET /inbox/:chatId/messages ───────────────────────────────────────────
    // Histórico paginado (cursor: before=<timestamp ISO>).
    router.get('/inbox/:chatId/messages', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = req.params['chatId'];
        const limit = Math.min(parseInt(String(req.query['limit'] || '50')), 200);
        const before = req.query['before'];
        try {
            const chat = await prisma.chat.findFirst({
                where: { id: chatId, tenantId },
                select: { id: true },
            });
            if (!chat) {
                res.status(404).json({ error: 'Chat não encontrado' });
                return;
            }
            const messages = await prisma.message.findMany({
                where: {
                    chatId,
                    tenantId,
                    ...(before ? { timestamp: { lt: new Date(before) } } : {}),
                },
                orderBy: { timestamp: 'desc' },
                take: limit,
                select: {
                    id: true,
                    messageId: true,
                    fromMe: true,
                    type: true,
                    body: true,
                    caption: true,
                    mediaUrl: true,
                    mimeType: true,
                    fileName: true,
                    status: true,
                    timestamp: true,
                    pushName: true,
                    senderJid: true,
                },
            });
            res.json(messages.reverse());
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── POST /inbox/:chatId/read ──────────────────────────────────────────────
    // Zera o contador de mensagens não lidas do chat.
    router.post('/inbox/:chatId/read', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = req.params['chatId'];
        try {
            const chat = await prisma.chat.findFirst({ where: { id: chatId, tenantId }, select: { id: true } });
            if (!chat) {
                res.status(404).json({ error: 'Chat não encontrado' });
                return;
            }
            await prisma.chat.update({ where: { id: chatId }, data: { unreadCount: 0 } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── PATCH /inbox/:chatId/archive ──────────────────────────────────────────
    // Arquiva ou desarquiva um chat.
    // Body: { archived: boolean }
    router.patch('/inbox/:chatId/archive', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = req.params['chatId'];
        const { archived } = req.body;
        if (typeof archived !== 'boolean') {
            res.status(400).json({ error: 'Campo "archived" (boolean) obrigatório' });
            return;
        }
        try {
            const chat = await prisma.chat.findFirst({ where: { id: chatId, tenantId }, select: { id: true } });
            if (!chat) {
                res.status(404).json({ error: 'Chat não encontrado' });
                return;
            }
            await prisma.chat.update({ where: { id: chatId }, data: { isArchived: archived } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── PATCH /inbox/:chatId/pin ──────────────────────────────────────────────
    // Fixa ou desfixa um chat.
    // Body: { pinned: boolean }
    router.patch('/inbox/:chatId/pin', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = req.params['chatId'];
        const { pinned } = req.body;
        if (typeof pinned !== 'boolean') {
            res.status(400).json({ error: 'Campo "pinned" (boolean) obrigatório' });
            return;
        }
        try {
            const chat = await prisma.chat.findFirst({ where: { id: chatId, tenantId }, select: { id: true } });
            if (!chat) {
                res.status(404).json({ error: 'Chat não encontrado' });
                return;
            }
            await prisma.chat.update({ where: { id: chatId }, data: { isPinned: pinned } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── DELETE /inbox/:chatId ─────────────────────────────────────────────────
    // Remove o chat e todas as mensagens associadas.
    router.delete('/inbox/:chatId', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = req.params['chatId'];
        try {
            const chat = await prisma.chat.findFirst({ where: { id: chatId, tenantId }, select: { id: true } });
            if (!chat) {
                res.status(404).json({ error: 'Chat não encontrado' });
                return;
            }
            await prisma.chat.delete({ where: { id: chatId } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── POST /inbox/:chatId/send ──────────────────────────────────────────────
    // Envia mensagem de texto ou rica para o chat.
    // Body: { text?, footer?, nativeButtons?, nativeList?, nativeCarousel?, poll? }
    router.post('/inbox/:chatId/send', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = req.params['chatId'];
        const { text, footer, nativeButtons, nativeList, nativeCarousel, poll } = req.body;
        const hasRich = !!(nativeButtons || nativeList || nativeCarousel || poll);
        if (!text && !hasRich) {
            res.status(400).json({ error: 'Campo "text" ou conteúdo rico obrigatório' });
            return;
        }
        try {
            const chat = await prisma.chat.findFirst({
                where: { id: chatId, tenantId },
                select: { id: true, jid: true, instanceId: true },
            });
            if (!chat) {
                res.status(404).json({ error: 'Chat não encontrado' });
                return;
            }
            const sock = manager.getSocket(chat.instanceId);
            if (!sock) {
                res.status(400).json({ error: 'Instância não conectada' });
                return;
            }
            // Usa o rich-message builder para converter nativeButtons/nativeList/
            // nativeCarousel nos protos corretos do @whiskeysockets/baileys.
            // O footer legado (aninhado em nativeList) é extraído para o top-level.
            const resolvedFooter = footer || nativeList?.footer || undefined;
            const cleanList = nativeList
                ? { buttonText: nativeList.buttonText, sections: nativeList.sections }
                : undefined;
            await (0, rich_message_1.sendRichMessage)(sock, chat.jid, {
                text: text?.trim() || undefined,
                footer: resolvedFooter,
                nativeButtons: nativeButtons,
                nativeList: cleanList,
                nativeCarousel: nativeCarousel,
                poll: poll,
            });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro ao enviar' });
        }
    });
    // ── POST /conversations ───────────────────────────────────────────────────
    // Inicia uma nova conversa enviando a primeira mensagem para um número ainda
    // sem chat existente. Body: { sessionId, phone, text }
    // O phone deve estar no formato brasileiro: DDD+número (10-11 dígitos) ou
    // já com código do país (55 + DDD + número = 12-13 dígitos).
    router.post('/conversations', async (req, res) => {
        const tenantId = req.extTenantId;
        const { sessionId, phone, text } = req.body;
        if (!sessionId || !phone || !text?.trim()) {
            res.status(400).json({ error: 'sessionId, phone e text são obrigatórios' });
            return;
        }
        // Normaliza para JID: remove não-dígitos e garante código do Brasil
        const digits = phone.replace(/\D/g, '');
        const normalized = (digits.startsWith('55') && (digits.length === 12 || digits.length === 13))
            ? digits
            : (digits.length === 10 || digits.length === 11) ? '55' + digits : digits;
        if (normalized.length < 10) {
            res.status(400).json({ error: 'Número inválido — informe DDD + número (ex: 67999138794)' });
            return;
        }
        const jid = `${normalized}@s.whatsapp.net`;
        try {
            const instance = await prisma.instance.findFirst({
                where: { id: sessionId, tenantId },
                select: { id: true },
            });
            if (!instance) {
                res.status(404).json({ error: 'Sessão não encontrada' });
                return;
            }
            const sock = manager.getSocket(sessionId);
            if (!sock) {
                res.status(400).json({ error: 'Sessão não conectada' });
                return;
            }
            await sock.sendMessage(jid, { text: text.trim() });
            res.status(201).json({ ok: true, jid });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro ao enviar' });
        }
    });
    // ── GET /webhooks ─────────────────────────────────────────────────────────
    // Lista os webhooks registrados pelo tenant.
    router.get('/webhooks', async (req, res) => {
        const tenantId = req.extTenantId;
        try {
            const hooks = await prisma.extWebhook.findMany({
                where: { tenantId },
                orderBy: { createdAt: 'desc' },
                select: { id: true, url: true, events: true, active: true, createdAt: true },
            });
            res.json(hooks);
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── POST /webhooks ────────────────────────────────────────────────────────
    // Registra um novo webhook.
    // Body: { url: string, events: string[], secret: string }
    router.post('/webhooks', async (req, res) => {
        const tenantId = req.extTenantId;
        const { url, events, secret } = req.body;
        if (!url || typeof url !== 'string' || !url.startsWith('http')) {
            res.status(400).json({ error: 'Campo "url" obrigatório e deve ser HTTP/HTTPS' });
            return;
        }
        if (!Array.isArray(events) || events.length === 0) {
            res.status(400).json({ error: 'Campo "events" obrigatório (array não vazio)' });
            return;
        }
        if (!secret || typeof secret !== 'string' || secret.length < 16) {
            res.status(400).json({ error: 'Campo "secret" obrigatório (mínimo 16 caracteres)' });
            return;
        }
        const VALID_EVENTS = ['message.new', 'session.status'];
        const invalid = events.filter(e => !VALID_EVENTS.includes(e));
        if (invalid.length > 0) {
            res.status(400).json({ error: `Eventos inválidos: ${invalid.join(', ')}. Válidos: ${VALID_EVENTS.join(', ')}` });
            return;
        }
        try {
            const hook = await prisma.extWebhook.create({
                data: { tenantId, url, events, secret, active: true },
                select: { id: true, url: true, events: true, active: true, createdAt: true },
            });
            res.status(201).json(hook);
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── DELETE /webhooks/:id ──────────────────────────────────────────────────
    // Remove um webhook do tenant.
    router.delete('/webhooks/:id', async (req, res) => {
        const tenantId = req.extTenantId;
        const id = req.params['id'];
        try {
            const deleted = await prisma.extWebhook.deleteMany({ where: { id, tenantId } });
            if (deleted.count === 0) {
                res.status(404).json({ error: 'Webhook não encontrado' });
                return;
            }
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── PATCH /webhooks/:id ───────────────────────────────────────────────────
    // Ativa / desativa um webhook sem removê-lo.
    // Body: { active: boolean }
    router.patch('/webhooks/:id', async (req, res) => {
        const tenantId = req.extTenantId;
        const id = req.params['id'];
        const { active } = req.body;
        if (typeof active !== 'boolean') {
            res.status(400).json({ error: 'Campo "active" (boolean) obrigatório' });
            return;
        }
        try {
            const hook = await prisma.extWebhook.findFirst({ where: { id, tenantId }, select: { id: true } });
            if (!hook) {
                res.status(404).json({ error: 'Webhook não encontrado' });
                return;
            }
            const updated = await prisma.extWebhook.update({
                where: { id },
                data: { active },
                select: { id: true, url: true, events: true, active: true, createdAt: true },
            });
            res.json(updated);
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── POST /secretaria/ask ──────────────────────────────────────────────────
    // Envia uma mensagem ao cérebro da Secretária IA em nome de um contato WA.
    // O contexto é persistido por (tenantId, chatId) — cada conversa do WhatsApp
    // tem seu próprio estado no ProtocolEngine.
    // Body: { chatId, message, contactName?, agentId?, context?, systemExtra?, tools? }
    router.post('/secretaria/ask', async (req, res) => {
        const tenantId = req.extTenantId;
        const { chatId, message, contactName, agentId, context: contextData, systemExtra, tools } = req.body;
        if (!chatId || !message?.trim()) {
            res.status(400).json({ error: 'chatId e message são obrigatórios' });
            return;
        }
        try {
            const extKey = `${tenantId}:${chatId}`;
            let conv = await db('sec_conversations')
                .where({ ext_chat_id: extKey, status: 'active' })
                .first();
            if (!conv) {
                let resolvedAgentId = agentId;
                if (!resolvedAgentId) {
                    const agent = await db('sec_agents').where({ active: true }).orderBy('created_at').first();
                    if (!agent) {
                        res.status(503).json({ error: 'Nenhum agente ativo na Secretária IA. Configure em Admin → Secretária.' });
                        return;
                    }
                    resolvedAgentId = agent.id;
                }
                const [newConv] = await db('sec_conversations').insert({
                    id: uuid(),
                    agent_id: resolvedAgentId,
                    contact_name: contactName ?? chatId,
                    protocol_number: brain_1.ProtocolEngine.generateProtocolNumber(),
                    status: 'active',
                    ext_chat_id: extKey,
                    handoff_mode: 'ia',
                }).returning('*');
                conv = newConv;
            }
            else if (contactName && conv.contact_name !== contactName) {
                await db('sec_conversations').where({ id: conv.id }).update({ contact_name: contactName });
                conv.contact_name = contactName;
            }
            // Handoff: se humano está respondendo, não chama IA
            if (conv.handoff_mode === 'humano') {
                res.json({ skipped: true, reason: 'humano', conversationId: conv.id, protocolNumber: conv.protocol_number });
                return;
            }
            const brain = new brain_1.ProtocolEngine(db, config);
            const reply = await brain.chat(conv.id, message.trim(), {
                contextData,
                systemExtra,
                tools: Array.isArray(tools) && tools.length ? tools : undefined,
                hooks,
                tenantId,
            });
            res.json({
                reply,
                conversationId: conv.id,
                protocolNumber: conv.protocol_number,
            });
        }
        catch (err) {
            res.status(500).json({ error: err.message ?? 'Erro interno' });
        }
    });
    // ── GET /sec/handoff/:chatId ──────────────────────────────────────────────
    // Retorna o modo de handoff atual para um chat específico.
    router.get('/sec/handoff/:chatId', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = decodeURIComponent(req.params['chatId']);
        try {
            const extKey = `${tenantId}:${chatId}`;
            const conv = await db('sec_conversations')
                .where({ ext_chat_id: extKey, status: 'active' })
                .select('id', 'handoff_mode', 'handoff_human_at')
                .first();
            res.json({ mode: conv?.handoff_mode ?? 'ia', conversationId: conv?.id ?? null, handoffHumanAt: conv?.handoff_human_at ?? null });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── PATCH /sec/handoff/:chatId ────────────────────────────────────────────
    // Alterna entre modo 'ia' e 'humano' para um chat específico.
    // Body: { mode: 'ia' | 'humano' }
    router.patch('/sec/handoff/:chatId', async (req, res) => {
        const tenantId = req.extTenantId;
        const chatId = decodeURIComponent(req.params['chatId']);
        const { mode } = req.body;
        if (mode !== 'ia' && mode !== 'humano') {
            res.status(400).json({ error: 'mode deve ser "ia" ou "humano"' });
            return;
        }
        try {
            const extKey = `${tenantId}:${chatId}`;
            const conv = await db('sec_conversations')
                .where({ ext_chat_id: extKey, status: 'active' })
                .first();
            if (!conv) {
                // Sem conversa ativa — retorna o modo padrão sem erro
                res.json({ mode: 'ia', conversationId: null });
                return;
            }
            const updateData = { handoff_mode: mode };
            if (mode === 'humano') {
                updateData.handoff_human_at = new Date();
                scheduleHandoffTimeout(conv.id, extKey, tenantId, db, hooks);
            }
            else {
                updateData.handoff_human_at = null;
                cancelHandoffTimeout(conv.id);
            }
            await db('sec_conversations').where({ id: conv.id }).update(updateData);
            res.json({ mode, conversationId: conv.id });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /sec/agent ────────────────────────────────────────────────────────
    // Retorna o agente primário (primeiro criado / ativo).
    router.get('/sec/agent', async (_req, res) => {
        try {
            const agent = await db('sec_agents').where({ active: true }).orderBy('created_at').first();
            res.json(agent ?? null);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /sec/agent ───────────────────────────────────────────────────────
    router.post('/sec/agent', async (req, res) => {
        try {
            const { name, model, provider, temperature, context_window } = req.body;
            const [agent] = await db('sec_agents').insert({
                id: uuid(), name, model: model ?? 'gpt-4o-mini',
                provider: provider ?? 'openai', temperature: temperature ?? 0.7,
                context_window: context_window ?? 10, active: true,
            }).returning('*');
            res.status(201).json(agent);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── PUT /sec/agent/:id ────────────────────────────────────────────────────
    router.put('/sec/agent/:id', async (req, res) => {
        try {
            const { name, model, provider, temperature, context_window } = req.body;
            const [agent] = await db('sec_agents').where({ id: req.params['id'] })
                .update({ name, model, provider, temperature, context_window, updated_at: new Date() })
                .returning('*');
            res.json(agent);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /sec/nodes ────────────────────────────────────────────────────────
    // Retorna todos os brain nodes do agente primário.
    router.get('/sec/nodes', async (_req, res) => {
        try {
            const agent = await db('sec_agents').where({ active: true }).orderBy('created_at').first();
            if (!agent) {
                res.json([]);
                return;
            }
            const nodes = await db('sec_brain_nodes').where({ agent_id: agent.id }).orderBy('sort_order');
            res.json(nodes);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /sec/nodes ───────────────────────────────────────────────────────
    router.post('/sec/nodes', async (req, res) => {
        try {
            const { type, title, content } = req.body;
            const agent = await db('sec_agents').where({ active: true }).orderBy('created_at').first();
            if (!agent) {
                res.status(503).json({ error: 'Nenhum agente ativo' });
                return;
            }
            const [node] = await db('sec_brain_nodes').insert({
                id: uuid(), agent_id: agent.id, type, title: title ?? '', content: content ?? '', active: true, sort_order: 99,
            }).returning('*');
            res.status(201).json(node);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── PUT /sec/nodes/:id ────────────────────────────────────────────────────
    router.put('/sec/nodes/:id', async (req, res) => {
        try {
            const { type, title, content } = req.body;
            const [node] = await db('sec_brain_nodes').where({ id: req.params['id'] })
                .update({ type, title: title ?? '', content, updated_at: new Date() }).returning('*');
            res.json(node);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── DELETE /sec/nodes/:id ─────────────────────────────────────────────────
    router.delete('/sec/nodes/:id', async (req, res) => {
        try {
            await db('sec_brain_nodes').where({ id: req.params['id'] }).delete();
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /sec/slots ────────────────────────────────────────────────────────
    router.get('/sec/slots', async (_req, res) => {
        try {
            const slots = await db('sec_calendar').orderBy('date').orderBy('time_start');
            res.json(slots);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /sec/slots ───────────────────────────────────────────────────────
    router.post('/sec/slots', async (req, res) => {
        try {
            const { date, time_start, time_end, attendee, notes } = req.body;
            const [slot] = await db('sec_calendar').insert({
                id: uuid(), date, time_start, time_end,
                attendee_name: attendee ?? null, notes: notes ?? null, status: 'available',
            }).returning('*');
            res.status(201).json(slot);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── DELETE /sec/slots/:id ─────────────────────────────────────────────────
    router.delete('/sec/slots/:id', async (req, res) => {
        try {
            await db('sec_calendar').where({ id: req.params['id'] }).delete();
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /sec/conversations ────────────────────────────────────────────────
    router.get('/sec/conversations', async (_req, res) => {
        try {
            const convs = await db('sec_conversations')
                .select('id', 'protocol_number', 'contact_name', 'status', 'handoff_mode', 'summary', 'created_at', 'updated_at')
                .orderBy('updated_at', 'desc').limit(100);
            res.json(convs);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /sec/conversations/:id/messages ───────────────────────────────────
    router.get('/sec/conversations/:id/messages', async (req, res) => {
        try {
            const msgs = await db('sec_messages')
                .where({ conversation_id: req.params['id'] })
                .orderBy('created_at');
            res.json(msgs);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /sec/conversations/:id/finalize ──────────────────────────────────
    router.post('/sec/conversations/:id/finalize', async (req, res) => {
        try {
            const brain = new brain_1.ProtocolEngine(db, config);
            const result = await brain.finalizeProtocol(req.params['id']);
            res.json(result);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /sec/keys ─────────────────────────────────────────────────────────
    // Retorna as API keys — valores reais mascarados (primeiros chars + ****)
    // admin_notify_phone é retornado sem máscara (não é segredo)
    router.get('/sec/keys', async (_req, res) => {
        try {
            const secretKeys = ['openai_key', 'gemini_key', 'anthropic_key', 'ollama_url'];
            const result = {};
            for (const k of secretKeys) {
                const v = await config.get(k);
                const raw = typeof v === 'string' ? v : '';
                result[k] = raw.length > 0
                    ? (raw.length > 8 ? raw.slice(0, 8) + '****' : '****')
                    : '';
            }
            // Número de notificação: retorna em claro
            const notifyRaw = await config.get('admin_notify_phone');
            result['admin_notify_phone'] = typeof notifyRaw === 'string' ? notifyRaw : '';
            res.json(result);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── PUT /sec/keys ─────────────────────────────────────────────────────────
    // Só persiste keys que não são máscara (não terminam em ****)
    router.put('/sec/keys', async (req, res) => {
        try {
            const secretKeys = ['openai_key', 'gemini_key', 'anthropic_key', 'ollama_url'];
            for (const k of secretKeys) {
                const v = req.body[k];
                if (typeof v === 'string' && v.length > 0 && !v.endsWith('****')) {
                    await config.set(k, v);
                }
            }
            // admin_notify_phone: sempre persiste (pode ser string vazia para limpar)
            if (typeof req.body['admin_notify_phone'] === 'string') {
                await config.set('admin_notify_phone', req.body['admin_notify_phone'].trim());
            }
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /templates ────────────────────────────────────────────────────────
    router.get('/templates', async (req, res) => {
        const tenantId = req.extTenantId;
        const { type, search } = req.query;
        try {
            const templates = await prisma.messageTemplate.findMany({
                where: {
                    tenantId,
                    ...(type && type !== 'ALL' ? { type: type } : {}),
                    ...(search ? { name: { contains: search, mode: 'insensitive' } } : {}),
                },
                orderBy: { createdAt: 'desc' },
            });
            res.json(templates);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /templates ───────────────────────────────────────────────────────
    router.post('/templates', async (req, res) => {
        const tenantId = req.extTenantId;
        const { name, type, payload, tags, description } = req.body;
        if (!name?.trim() || !type || !payload) {
            res.status(400).json({ error: 'name, type e payload são obrigatórios' });
            return;
        }
        try {
            const tmpl = await prisma.messageTemplate.create({
                data: { tenantId, name: name.trim(), type, payload: payload, tags: tags ?? [], description: description ?? null },
            });
            res.status(201).json(tmpl);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── DELETE /templates/:id ─────────────────────────────────────────────────
    router.delete('/templates/:id', async (req, res) => {
        const tenantId = req.extTenantId;
        const id = req.params['id'];
        try {
            const existing = await prisma.messageTemplate.findFirst({ where: { id, tenantId } });
            if (!existing) {
                res.status(404).json({ error: 'Template não encontrado' });
                return;
            }
            await prisma.messageTemplate.delete({ where: { id } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /templates/:id/use ───────────────────────────────────────────────
    router.post('/templates/:id/use', async (req, res) => {
        const tenantId = req.extTenantId;
        const id = req.params['id'];
        try {
            const existing = await prisma.messageTemplate.findFirst({ where: { id, tenantId } });
            if (!existing) {
                res.status(404).json({ error: 'Template não encontrado' });
                return;
            }
            await prisma.messageTemplate.update({ where: { id }, data: { usageCount: { increment: 1 } } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── GET /scheduled ────────────────────────────────────────────────────────
    router.get('/scheduled', async (req, res) => {
        const tenantId = req.extTenantId;
        try {
            const items = await prisma.scheduledMessage.findMany({
                where: { tenantId },
                orderBy: { createdAt: 'desc' },
            });
            res.json(items);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /scheduled ───────────────────────────────────────────────────────
    router.post('/scheduled', async (req, res) => {
        const tenantId = req.extTenantId;
        const { instanceId, name, payload, recipients, scheduleType, cronExpr, scheduledAt, eventType, timezone } = req.body;
        if (!instanceId || !name?.trim() || !payload || !Array.isArray(recipients) || !scheduleType) {
            res.status(400).json({ error: 'instanceId, name, payload, recipients e scheduleType são obrigatórios' });
            return;
        }
        try {
            const nextRunAt = scheduleType === 'ONCE' && scheduledAt ? new Date(scheduledAt) : null;
            const item = await prisma.scheduledMessage.create({
                data: {
                    tenantId, instanceId, name: name.trim(),
                    payload: payload, recipients: recipients,
                    scheduleType, cronExpr: cronExpr ?? null,
                    scheduledAt: scheduledAt ? new Date(scheduledAt) : null,
                    eventType: eventType ?? null,
                    timezone: timezone ?? 'America/Sao_Paulo',
                    nextRunAt,
                    status: 'ACTIVE',
                },
            });
            res.status(201).json(item);
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── DELETE /scheduled/:id ─────────────────────────────────────────────────
    router.delete('/scheduled/:id', async (req, res) => {
        const tenantId = req.extTenantId;
        const id = req.params['id'];
        try {
            const existing = await prisma.scheduledMessage.findFirst({ where: { id, tenantId } });
            if (!existing) {
                res.status(404).json({ error: 'Agendamento não encontrado' });
                return;
            }
            await prisma.scheduledMessage.delete({ where: { id } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /scheduled/:id/pause ─────────────────────────────────────────────
    router.post('/scheduled/:id/pause', async (req, res) => {
        const tenantId = req.extTenantId;
        const id = req.params['id'];
        try {
            const existing = await prisma.scheduledMessage.findFirst({ where: { id, tenantId } });
            if (!existing) {
                res.status(404).json({ error: 'Agendamento não encontrado' });
                return;
            }
            await prisma.scheduledMessage.update({ where: { id }, data: { status: 'PAUSED' } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    // ── POST /scheduled/:id/resume ────────────────────────────────────────────
    router.post('/scheduled/:id/resume', async (req, res) => {
        const tenantId = req.extTenantId;
        const id = req.params['id'];
        try {
            const existing = await prisma.scheduledMessage.findFirst({ where: { id, tenantId } });
            if (!existing) {
                res.status(404).json({ error: 'Agendamento não encontrado' });
                return;
            }
            await prisma.scheduledMessage.update({ where: { id }, data: { status: 'ACTIVE' } });
            res.json({ ok: true });
        }
        catch (err) {
            res.status(500).json({ error: err.message });
        }
    });
    return router;
}
