"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var McpChatService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.McpChatService = void 0;
const api_prisma_1 = require("@hed-hog/api-prisma");
const common_1 = require("@nestjs/common");
const axios_1 = __importDefault(require("axios"));
const mcp_registry_service_1 = require("../mcp/mcp-registry.service");
const setting_service_1 = require("../setting/setting.service");
const MAX_OPENAI_TOOLS = 128;
const TARGET_SELECTED_TOOLS = 112;
const MAX_ITERATIONS = 20;
const FORCE_FINISH_THRESHOLD = 2;
const MIN_DOMAIN_CONFIDENCE = 2;
const AMBIGUOUS_DOMAIN_DELTA = 1;
const SYSTEM_PROMPT = 'You are a helpful assistant with access to tools from the HedHog system. ' +
    'Use the available tools to answer the user\'s questions accurately. ' +
    'Always respond in the same language the user writes in.';
const sanitize = (n) => n.replace(/\./g, '__');
const restore = (n) => n.replace(/__/g, '.');
const DOMAIN_KEYWORDS = {
    finance: [
        'finance',
        'financeiro',
        'financeira',
        'financas',
        'finanças',
        'banco',
        'bancos',
        'bank',
        'banks',
        'conta',
        'contas',
        'account',
        'accounts',
        'conta bancaria',
        'contas bancarias',
        'conta bancária',
        'contas bancárias',
        'extrato',
        'extratos',
        'statement',
        'statements',
        'pagamento',
        'pagamentos',
        'payment',
        'payments',
        'recebimento',
        'recebimentos',
        'receivable',
        'payable',
        'currency',
        'moeda',
        'transferencia',
        'transferência',
    ],
    operations: [
        'operations',
        'operacoes',
        'operações',
        'tarefa',
        'tarefas',
        'task',
        'tasks',
        'projeto',
        'projetos',
        'project',
        'projects',
        'timesheet',
        'apontamento',
        'aprovacao',
        'aprovação',
        'approval',
        'colaborador',
        'collaborator',
    ],
    lms: [
        'lms',
        'curso',
        'cursos',
        'course',
        'courses',
        'aula',
        'aulas',
        'lesson',
        'lessons',
        'treinamento',
        'training',
        'turma',
        'class',
        'aluno',
        'student',
        'prova',
        'exam',
    ],
    core: [
        'core',
        'usuario',
        'usuário',
        'usuarios',
        'usuários',
        'user',
        'users',
        'perfil',
        'profile',
        'role',
        'roles',
        'menu',
        'menus',
        'rota',
        'rotas',
        'route',
        'routes',
        'configuracao',
        'configuração',
        'setting',
        'settings',
    ],
    contact: [
        'contato',
        'contatos',
        'contact',
        'contacts',
        'pessoa',
        'pessoas',
        'person',
        'proposal',
        'proposta',
    ],
};
const ACTION_KEYWORDS = [
    'listar',
    'lista',
    'buscar',
    'procurar',
    'consultar',
    'mostrar',
    'ver',
    'criar',
    'adicionar',
    'atualizar',
    'alterar',
    'remover',
    'deletar',
    'excluir',
    'list',
    'search',
    'find',
    'show',
    'get',
    'create',
    'add',
    'update',
    'delete',
    'remove',
];
let McpChatService = McpChatService_1 = class McpChatService {
    constructor(prisma, setting, registry) {
        this.prisma = prisma;
        this.setting = setting;
        this.registry = registry;
        this.logger = new common_1.Logger(McpChatService_1.name);
    }
    async listConversations(userId) {
        return this.prisma.mcp_conversation.findMany({
            where: { user_id: userId },
            orderBy: { created_at: 'desc' },
            select: { id: true, title: true, created_at: true, updated_at: true },
        });
    }
    async createConversation(userId, dto) {
        var _a;
        return this.prisma.mcp_conversation.create({
            data: {
                user_id: userId,
                title: (_a = dto.title) !== null && _a !== void 0 ? _a : null,
            },
        });
    }
    async deleteConversations(userId, ids) {
        await this.prisma.mcp_conversation.deleteMany({
            where: { id: { in: ids }, user_id: userId },
        });
        return { count: ids.length };
    }
    async getMessages(conversationId, userId) {
        await this.assertOwnership(conversationId, userId);
        return this.prisma.mcp_conversation_message.findMany({
            where: { conversation_id: conversationId },
            orderBy: { created_at: 'asc' },
        });
    }
    async sendMessage(conversationId, userId, locale, message) {
        const cfg = await this.setting.getSettingValues(['mcp-enabled']);
        if (cfg['mcp-enabled'] === false) {
            throw new common_1.BadRequestException('MCP server is disabled.');
        }
        await this.assertOwnership(conversationId, userId);
        const { provider, apiKey } = await this.getProviderKey();
        // Save user message
        await this.prisma.mcp_conversation_message.create({
            data: {
                conversation_id: conversationId,
                role: 'user',
                content: message,
            },
        });
        // Auto-set title on first message
        const conv = await this.prisma.mcp_conversation.findUnique({
            where: { id: conversationId },
        });
        if (!conv.title) {
            await this.prisma.mcp_conversation.update({
                where: { id: conversationId },
                data: { title: message.slice(0, 60) },
            });
        }
        if (provider === 'gemini') {
            return this.runGeminiLoop(conversationId, userId, locale, message, apiKey);
        }
        return this.runOpenAiLoop(conversationId, userId, locale, message, apiKey);
    }
    // ── OpenAI loop ──────────────────────────────────────────────────────────
    async runOpenAiLoop(conversationId, userId, locale, message, apiKey) {
        var _a, _b, _c, _d, _e, _f, _g, _h;
        const history = await this.prisma.mcp_conversation_message.findMany({
            where: { conversation_id: conversationId },
            orderBy: { created_at: 'asc' },
        });
        const openAiMessages = [
            { role: 'system', content: SYSTEM_PROMPT },
            ...this.toOpenAiMessages(history),
        ];
        const toolSelection = await this.buildOpenAiTools(message, conversationId, userId, history);
        if (toolSelection.clarification) {
            return this.recordAssistantAndReturnHistory(conversationId, toolSelection.clarification);
        }
        const tools = toolSelection.tools;
        const callSignatures = new Set();
        let maxIterations = MAX_ITERATIONS;
        while (maxIterations-- > 0) {
            const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
            let response;
            try {
                response = await axios_1.default.post('https://api.openai.com/v1/chat/completions', {
                    model: 'gpt-4o-mini',
                    messages: openAiMessages,
                    tools: !forceFinish && tools.length > 0 ? tools : undefined,
                    tool_choice: !forceFinish && tools.length > 0 ? 'auto' : undefined,
                }, {
                    headers: {
                        Authorization: `Bearer ${apiKey}`,
                        'Content-Type': 'application/json',
                    },
                });
            }
            catch (err) {
                const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
                const errBody = (_c = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.error;
                if (status === 429) {
                    throw new common_1.HttpException((_d = errBody === null || errBody === void 0 ? void 0 : errBody.message) !== null && _d !== void 0 ? _d : 'OpenAI rate limit exceeded. Please try again later.', common_1.HttpStatus.TOO_MANY_REQUESTS);
                }
                if (status === 401) {
                    throw new common_1.BadRequestException('Invalid OpenAI API key.');
                }
                throw new common_1.HttpException((_e = errBody === null || errBody === void 0 ? void 0 : errBody.message) !== null && _e !== void 0 ? _e : 'OpenAI API error.', status !== null && status !== void 0 ? status : common_1.HttpStatus.BAD_GATEWAY);
            }
            const choice = (_g = (_f = response.data) === null || _f === void 0 ? void 0 : _f.choices) === null || _g === void 0 ? void 0 : _g[0];
            const assistantMsg = choice === null || choice === void 0 ? void 0 : choice.message;
            if (((_h = assistantMsg === null || assistantMsg === void 0 ? void 0 : assistantMsg.tool_calls) === null || _h === void 0 ? void 0 : _h.length) && !forceFinish) {
                openAiMessages.push({
                    role: 'assistant',
                    content: null,
                    tool_calls: assistantMsg.tool_calls,
                });
                for (const toolCall of assistantMsg.tool_calls) {
                    const originalName = restore(toolCall.function.name);
                    const argsJson = toolCall.function.arguments;
                    let args = {};
                    try {
                        args = JSON.parse(argsJson);
                    }
                    catch (_j) {
                        args = {};
                    }
                    await this.prisma.mcp_conversation_message.create({
                        data: {
                            conversation_id: conversationId,
                            role: 'tool_call',
                            content: argsJson,
                            tool_name: originalName,
                            tool_call_id: toolCall.id,
                        },
                    });
                    const sig = `${toolCall.function.name}:${argsJson}`;
                    if (callSignatures.has(sig)) {
                        this.logger.warn(`MCP chat duplicate tool call detected — aborting re-execution. ` +
                            `conversationId=${conversationId} tool=${originalName}`);
                        const dupResult = JSON.stringify({ error: 'Duplicate tool call detected, aborting loop.' });
                        await this.prisma.mcp_conversation_message.create({
                            data: {
                                conversation_id: conversationId,
                                role: 'tool_result',
                                content: dupResult,
                                tool_name: originalName,
                                tool_call_id: toolCall.id,
                            },
                        });
                        openAiMessages.push({
                            role: 'tool',
                            tool_call_id: toolCall.id,
                            content: dupResult,
                        });
                        continue;
                    }
                    callSignatures.add(sig);
                    let resultContent;
                    const entry = this.registry.get(originalName);
                    if (entry) {
                        const context = {
                            userId,
                            sessionId: '',
                            locale,
                            toolName: originalName,
                        };
                        try {
                            const result = await entry.handler(args, context);
                            resultContent = JSON.stringify(result);
                        }
                        catch (e) {
                            resultContent = JSON.stringify({ error: e.message });
                        }
                    }
                    else {
                        resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
                    }
                    await this.prisma.mcp_conversation_message.create({
                        data: {
                            conversation_id: conversationId,
                            role: 'tool_result',
                            content: resultContent,
                            tool_name: originalName,
                            tool_call_id: toolCall.id,
                        },
                    });
                    openAiMessages.push({
                        role: 'tool',
                        tool_call_id: toolCall.id,
                        content: resultContent,
                    });
                }
                continue;
            }
            const finalContent = (assistantMsg === null || assistantMsg === void 0 ? void 0 : assistantMsg.content) || '';
            await this.prisma.mcp_conversation_message.create({
                data: {
                    conversation_id: conversationId,
                    role: 'assistant',
                    content: finalContent,
                },
            });
            return this.prisma.mcp_conversation_message.findMany({
                where: { conversation_id: conversationId },
                orderBy: { created_at: 'asc' },
            });
        }
        throw new common_1.BadRequestException('Maximum tool call iterations exceeded.');
    }
    // ── Gemini loop ──────────────────────────────────────────────────────────
    async runGeminiLoop(conversationId, userId, locale, message, apiKey) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
        const history = await this.prisma.mcp_conversation_message.findMany({
            where: { conversation_id: conversationId },
            orderBy: { created_at: 'asc' },
        });
        const geminiContents = this.toGeminiContents(history);
        const toolSelection = await this.buildGeminiTools(message, conversationId, userId, history);
        if (toolSelection.clarification) {
            return this.recordAssistantAndReturnHistory(conversationId, toolSelection.clarification);
        }
        const functionDeclarations = toolSelection.tools;
        let maxIterations = MAX_ITERATIONS;
        let callIndex = 0;
        while (maxIterations-- > 0) {
            const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
            let response;
            try {
                response = await axios_1.default.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
                    systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
                    contents: geminiContents,
                    tools: !forceFinish && functionDeclarations.length > 0
                        ? [{ functionDeclarations }]
                        : undefined,
                }, { headers: { 'Content-Type': 'application/json' } });
            }
            catch (err) {
                const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
                const errData = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.data;
                if (status === 429) {
                    throw new common_1.HttpException((_d = (_c = errData === null || errData === void 0 ? void 0 : errData.error) === null || _c === void 0 ? void 0 : _c.message) !== null && _d !== void 0 ? _d : 'Gemini rate limit exceeded. Please try again later.', common_1.HttpStatus.TOO_MANY_REQUESTS);
                }
                if (status === 400) {
                    throw new common_1.BadRequestException((_f = (_e = errData === null || errData === void 0 ? void 0 : errData.error) === null || _e === void 0 ? void 0 : _e.message) !== null && _f !== void 0 ? _f : 'Invalid Gemini API request.');
                }
                if (status === 401 || status === 403) {
                    throw new common_1.BadRequestException('Invalid Gemini API key.');
                }
                throw new common_1.HttpException((_h = (_g = errData === null || errData === void 0 ? void 0 : errData.error) === null || _g === void 0 ? void 0 : _g.message) !== null && _h !== void 0 ? _h : 'Gemini API error.', status !== null && status !== void 0 ? status : common_1.HttpStatus.BAD_GATEWAY);
            }
            const candidate = (_k = (_j = response.data) === null || _j === void 0 ? void 0 : _j.candidates) === null || _k === void 0 ? void 0 : _k[0];
            const parts = (_m = (_l = candidate === null || candidate === void 0 ? void 0 : candidate.content) === null || _l === void 0 ? void 0 : _l.parts) !== null && _m !== void 0 ? _m : [];
            const functionCallParts = parts.filter((p) => p.functionCall);
            const textParts = parts.filter((p) => p.text);
            if (functionCallParts.length > 0 && !forceFinish) {
                geminiContents.push({ role: 'model', parts: functionCallParts });
                const functionResponseParts = [];
                for (const part of functionCallParts) {
                    const { name, args } = part.functionCall;
                    const originalName = restore(name);
                    const argsJson = JSON.stringify(args !== null && args !== void 0 ? args : {});
                    const toolCallId = `gemini-${callIndex++}`;
                    await this.prisma.mcp_conversation_message.create({
                        data: {
                            conversation_id: conversationId,
                            role: 'tool_call',
                            content: argsJson,
                            tool_name: originalName,
                            tool_call_id: toolCallId,
                        },
                    });
                    let resultContent;
                    const entry = this.registry.get(originalName);
                    if (entry) {
                        const context = {
                            userId,
                            sessionId: '',
                            locale,
                            toolName: originalName,
                        };
                        try {
                            const result = await entry.handler(args !== null && args !== void 0 ? args : {}, context);
                            resultContent = JSON.stringify(result);
                        }
                        catch (e) {
                            resultContent = JSON.stringify({ error: e.message });
                        }
                    }
                    else {
                        resultContent = JSON.stringify({ error: `Tool '${originalName}' not found` });
                    }
                    await this.prisma.mcp_conversation_message.create({
                        data: {
                            conversation_id: conversationId,
                            role: 'tool_result',
                            content: resultContent,
                            tool_name: originalName,
                            tool_call_id: toolCallId,
                        },
                    });
                    functionResponseParts.push({
                        functionResponse: {
                            name,
                            response: { output: resultContent },
                        },
                    });
                }
                geminiContents.push({ role: 'user', parts: functionResponseParts });
                continue;
            }
            const finalContent = textParts.map((p) => p.text).join('');
            await this.prisma.mcp_conversation_message.create({
                data: {
                    conversation_id: conversationId,
                    role: 'assistant',
                    content: finalContent,
                },
            });
            return this.prisma.mcp_conversation_message.findMany({
                where: { conversation_id: conversationId },
                orderBy: { created_at: 'asc' },
            });
        }
        throw new common_1.BadRequestException('Maximum tool call iterations exceeded.');
    }
    // ── Helpers ──────────────────────────────────────────────────────────────
    toOpenAiMessages(history) {
        const messages = [];
        const toolCallMap = new Map(); // tool_call_id → sanitized name
        for (const msg of history) {
            if (msg.role === 'user') {
                messages.push({ role: 'user', content: msg.content });
            }
            else if (msg.role === 'assistant') {
                messages.push({ role: 'assistant', content: msg.content });
            }
            else if (msg.role === 'tool_call' && msg.tool_call_id && msg.tool_name) {
                const sanitizedName = sanitize(msg.tool_name);
                toolCallMap.set(msg.tool_call_id, sanitizedName);
                messages.push({
                    role: 'assistant',
                    content: null,
                    tool_calls: [
                        {
                            id: msg.tool_call_id,
                            type: 'function',
                            function: { name: sanitizedName, arguments: msg.content },
                        },
                    ],
                });
            }
            else if (msg.role === 'tool_result' && msg.tool_call_id) {
                messages.push({
                    role: 'tool',
                    tool_call_id: msg.tool_call_id,
                    content: msg.content,
                });
            }
        }
        return messages;
    }
    toGeminiContents(history) {
        var _a, _b;
        const contents = [];
        let i = 0;
        while (i < history.length) {
            const msg = history[i];
            if (msg.role === 'user') {
                contents.push({ role: 'user', parts: [{ text: msg.content }] });
                i++;
            }
            else if (msg.role === 'assistant') {
                contents.push({ role: 'model', parts: [{ text: msg.content }] });
                i++;
            }
            else if (msg.role === 'tool_call') {
                const functionCallParts = [];
                while (i < history.length && history[i].role === 'tool_call') {
                    const m = history[i];
                    let args = {};
                    try {
                        args = JSON.parse(m.content);
                    }
                    catch (_c) {
                        args = {};
                    }
                    functionCallParts.push({
                        functionCall: { name: sanitize((_a = m.tool_name) !== null && _a !== void 0 ? _a : ''), args },
                    });
                    i++;
                }
                contents.push({ role: 'model', parts: functionCallParts });
            }
            else if (msg.role === 'tool_result') {
                const functionResponseParts = [];
                while (i < history.length && history[i].role === 'tool_result') {
                    const m = history[i];
                    functionResponseParts.push({
                        functionResponse: {
                            name: sanitize((_b = m.tool_name) !== null && _b !== void 0 ? _b : ''),
                            response: { output: m.content },
                        },
                    });
                    i++;
                }
                contents.push({ role: 'user', parts: functionResponseParts });
            }
            else {
                i++;
            }
        }
        return contents;
    }
    async assertOwnership(conversationId, userId) {
        const conv = await this.prisma.mcp_conversation.findUnique({
            where: { id: conversationId },
            select: { user_id: true },
        });
        if (!conv) {
            throw new common_1.NotFoundException(`Conversation ${conversationId} not found.`);
        }
        if (conv.user_id !== userId) {
            throw new common_1.ForbiddenException('Access denied to this conversation.');
        }
    }
    async getProviderKey() {
        const settings = await this.setting.getSettingValues([
            'mcp-ai-provider',
            'ai-openai-api-key',
            'ai-gemini-api-key',
        ]);
        const provider = (settings['mcp-ai-provider'] || 'openai');
        if (provider === 'gemini') {
            const key = settings['ai-gemini-api-key'];
            if (!key) {
                throw new common_1.BadRequestException('Gemini API key is not configured. Please set it in Settings → AI → Gemini API Key.');
            }
            return { provider: 'gemini', apiKey: key };
        }
        const key = settings['ai-openai-api-key'];
        if (!key) {
            throw new common_1.BadRequestException('OpenAI API key is not configured. Please set it in Settings → AI → OpenAI API Key.');
        }
        return { provider: 'openai', apiKey: key };
    }
    async buildOpenAiTools(message, conversationId, userId, history) {
        return this.selectOpenAiTools(message, conversationId, userId, history);
    }
    async buildGeminiTools(message, conversationId, userId, history) {
        const selection = await this.selectOpenAiTools(message, conversationId, userId, history);
        return {
            clarification: selection.clarification,
            tools: selection.tools.map((t) => t.function),
        };
    }
    async selectOpenAiTools(message, conversationId, userId, history) {
        var _a;
        const allEntries = this.registry.getAll();
        const allowedToolNames = await this.getAllowedMcpToolNames(userId);
        const allowedEntries = allEntries.filter((entry) => allowedToolNames.has(entry.definition.name));
        const dedupedBySanitized = new Map();
        const excludedDuplicates = [];
        const routerText = this.buildToolRouterText(message, history);
        const normalizedMessage = this.normalizeText(routerText);
        const messageTerms = this.getMessageKeywordSet(normalizedMessage);
        const domainScores = this.scoreToolDomains(normalizedMessage, allowedEntries);
        const candidateDomains = this.getCandidateDomains(domainScores);
        const clarification = this.getToolSelectionClarification(message, normalizedMessage, domainScores, candidateDomains, allowedEntries);
        if (clarification) {
            this.logger.log(`MCP tools need clarification: conversationId=${conversationId} userId=${userId} ` +
                `registered=${allEntries.length} authorized=${allowedEntries.length} ` +
                `domains=${this.formatDomainScores(domainScores)} selected=0`);
            return { tools: [], clarification };
        }
        const entriesForRanking = candidateDomains.length > 0
            ? allowedEntries.filter((entry) => candidateDomains.includes(this.getToolDomain(entry.definition.name)))
            : allowedEntries;
        for (const [index, entry] of entriesForRanking.entries()) {
            const originalName = entry.definition.name;
            const sanitizedName = sanitize(originalName);
            const readOnly = Boolean(entry.definition.readOnly);
            const domain = this.getToolDomain(originalName);
            const matchScore = this.scoreToolMatch(normalizedMessage, messageTerms, entry, candidateDomains);
            if (dedupedBySanitized.has(sanitizedName)) {
                excludedDuplicates.push(originalName);
                continue;
            }
            const scored = {
                name: originalName,
                domain,
                readOnly,
                priorityBucket: this.getDomainPriorityBucket(originalName, candidateDomains),
                keywordBoost: this.getKeywordBoost(normalizedMessage, originalName),
                matchScore,
                index,
                tool: {
                    type: 'function',
                    function: {
                        name: sanitizedName,
                        description: entry.definition.description,
                        parameters: (_a = entry.definition.inputSchema) !== null && _a !== void 0 ? _a : {
                            type: 'object',
                            properties: {},
                        },
                    },
                },
            };
            dedupedBySanitized.set(sanitizedName, scored);
        }
        const scoredTools = Array.from(dedupedBySanitized.values());
        scoredTools.sort((a, b) => {
            if (a.matchScore !== b.matchScore) {
                return b.matchScore - a.matchScore;
            }
            if (a.priorityBucket !== b.priorityBucket) {
                return a.priorityBucket - b.priorityBucket;
            }
            if (a.keywordBoost !== b.keywordBoost) {
                return b.keywordBoost - a.keywordBoost;
            }
            if (a.readOnly !== b.readOnly) {
                return a.readOnly ? -1 : 1;
            }
            return a.index - b.index;
        });
        const selected = scoredTools.slice(0, TARGET_SELECTED_TOOLS);
        const excludedByLimit = scoredTools
            .slice(TARGET_SELECTED_TOOLS)
            .map((tool) => tool.name);
        if (excludedDuplicates.length > 0) {
            this.logger.warn(`MCP tools duplicate sanitize names ignored: ${excludedDuplicates.length}. ` +
                `Examples: ${excludedDuplicates.slice(0, 10).join(', ')}`);
        }
        if (excludedByLimit.length > 0) {
            this.logger.warn(`MCP tools truncated for provider budget (${TARGET_SELECTED_TOOLS}/${MAX_OPENAI_TOOLS}). ` +
                `conversationId=${conversationId} userId=${userId} ` +
                `available=${scoredTools.length} selected=${selected.length} excluded=${excludedByLimit.length}. ` +
                `Top excluded: ${excludedByLimit.slice(0, 12).join(', ')}`);
        }
        this.logger.log(`MCP tools prepared: conversationId=${conversationId} userId=${userId} ` +
            `registered=${allEntries.length} authorized=${allowedEntries.length} ` +
            `domains=${this.formatDomainScores(domainScores)} ranked=${scoredTools.length} ` +
            `selected=${selected.length}`);
        return { tools: selected.map((entry) => entry.tool) };
    }
    async getAllowedMcpToolNames(userId) {
        const routes = await this.prisma.route.findMany({
            where: {
                type: 'MCP',
                tool_name: { not: null },
                role_route: {
                    some: {
                        role: {
                            role_user: {
                                some: { user_id: userId },
                            },
                        },
                    },
                },
            },
            select: { tool_name: true },
        });
        return new Set(routes
            .map((route) => route.tool_name)
            .filter((toolName) => typeof toolName === 'string'));
    }
    getDomainPriorityBucket(toolName, candidateDomains) {
        const domain = this.getToolDomain(toolName);
        const candidateIndex = candidateDomains.indexOf(domain);
        if (candidateIndex >= 0) {
            return candidateIndex;
        }
        if (toolName.startsWith('operations.'))
            return 10;
        if (toolName.startsWith('finance.'))
            return 11;
        if (toolName.startsWith('lms.'))
            return 12;
        if (toolName.startsWith('core.'))
            return 13;
        return 99;
    }
    getKeywordBoost(message, toolName) {
        const terms = this.getMessageKeywordSet(message);
        if (terms.size === 0) {
            return 0;
        }
        let boost = 0;
        if (terms.has('task') &&
            (toolName.includes('.task') || toolName.includes('.tasks.'))) {
            boost += 4;
        }
        if (terms.has('project') &&
            (toolName.includes('.project') || toolName.includes('.projects.'))) {
            boost += 3;
        }
        if (terms.has('class') &&
            (toolName.includes('.class') ||
                toolName.includes('.course') ||
                toolName.includes('.training'))) {
            boost += 3;
        }
        if (terms.has('pending') &&
            (toolName.includes('.task') ||
                toolName.includes('.approval') ||
                toolName.includes('.time-off'))) {
            boost += 2;
        }
        return boost;
    }
    buildToolRouterText(message, history) {
        const recentContext = history
            .slice(-8)
            .map((item) => [item.role === 'user' ? item.content : undefined, item.tool_name]
            .filter(Boolean)
            .join(' '))
            .join(' ');
        return `${recentContext} ${message}`;
    }
    normalizeText(value) {
        return value
            .toLowerCase()
            .normalize('NFD')
            .replace(/[\u0300-\u036f]/g, '')
            .replace(/[^a-z0-9]+/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
    }
    getToolDomain(toolName) {
        return toolName.split('.')[0] || 'unknown';
    }
    scoreToolDomains(normalizedMessage, entries) {
        var _a, _b;
        const scores = new Map();
        for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
            let score = 0;
            if (this.hasTerm(normalizedMessage, domain)) {
                score += 3;
            }
            for (const keyword of keywords) {
                const normalizedKeyword = this.normalizeText(keyword);
                if (normalizedKeyword && normalizedMessage.includes(normalizedKeyword)) {
                    score += normalizedKeyword.includes(' ') ? 3 : 2;
                }
            }
            if (score > 0 && entries.some((entry) => this.getToolDomain(entry.definition.name) === domain)) {
                scores.set(domain, score);
            }
        }
        for (const entry of entries) {
            const domain = this.getToolDomain(entry.definition.name);
            const toolText = this.normalizeText(`${entry.definition.name} ${(_a = entry.definition.description) !== null && _a !== void 0 ? _a : ''}`);
            let score = (_b = scores.get(domain)) !== null && _b !== void 0 ? _b : 0;
            for (const term of normalizedMessage.split(' ')) {
                if (term.length >= 4 && toolText.includes(term)) {
                    score += 1;
                }
            }
            if (score > 0) {
                scores.set(domain, score);
            }
        }
        return scores;
    }
    getCandidateDomains(domainScores) {
        const ranked = Array.from(domainScores.entries())
            .filter(([, score]) => score >= MIN_DOMAIN_CONFIDENCE)
            .sort((a, b) => b[1] - a[1]);
        if (ranked.length === 0) {
            return [];
        }
        const topScore = ranked[0][1];
        return ranked
            .filter(([, score]) => topScore - score <= AMBIGUOUS_DOMAIN_DELTA)
            .map(([domain]) => domain);
    }
    getToolSelectionClarification(originalMessage, normalizedMessage, domainScores, candidateDomains, entries) {
        if (!this.messageLooksLikeToolRequest(normalizedMessage)) {
            return undefined;
        }
        const rankedDomains = Array.from(domainScores.entries()).sort((a, b) => b[1] - a[1]);
        if (rankedDomains.length === 0 || rankedDomains[0][1] < MIN_DOMAIN_CONFIDENCE) {
            return this.isLikelyPortuguese(originalMessage)
                ? 'Você quer consultar qual área do sistema? Por exemplo: financeiro, operações, LMS ou configurações.'
                : 'Which system area do you want to query? For example: finance, operations, LMS, or settings.';
        }
        if (candidateDomains.length > 1) {
            const options = candidateDomains
                .slice(0, 3)
                .map((domain) => this.getDomainLabel(domain, originalMessage))
                .join(', ');
            return this.isLikelyPortuguese(originalMessage)
                ? `Você quer consultar qual área: ${options}?`
                : `Which area do you want to query: ${options}?`;
        }
        if (candidateDomains[0] === 'finance' &&
            this.isAmbiguousFinanceAccountsRequest(normalizedMessage, entries)) {
            return this.isLikelyPortuguese(originalMessage)
                ? 'Você quer consultar contas bancárias, contas a pagar ou contas a receber?'
                : 'Do you want bank accounts, accounts payable, or accounts receivable?';
        }
        return undefined;
    }
    scoreToolMatch(normalizedMessage, terms, entry, candidateDomains) {
        var _a;
        const toolName = entry.definition.name;
        const toolText = this.normalizeText(`${toolName} ${(_a = entry.definition.description) !== null && _a !== void 0 ? _a : ''} ${this.getSchemaText(entry.definition.inputSchema)}`);
        const domain = this.getToolDomain(toolName);
        let score = candidateDomains.includes(domain) ? 8 : 0;
        for (const term of normalizedMessage.split(' ')) {
            if (term.length < 3)
                continue;
            if (toolText.includes(term)) {
                score += term.length >= 5 ? 2 : 1;
            }
        }
        if (terms.has('list') && entry.definition.readOnly)
            score += 4;
        if (terms.has('write') && !entry.definition.readOnly)
            score += 4;
        score += this.getFinanceSpecificBoost(normalizedMessage, toolName);
        score += this.getKeywordBoost(normalizedMessage, toolName);
        return score;
    }
    getSchemaText(schema) {
        if (!schema || typeof schema !== 'object') {
            return '';
        }
        const properties = schema.properties;
        if (!properties || typeof properties !== 'object') {
            return '';
        }
        return Object.keys(properties).join(' ');
    }
    getFinanceSpecificBoost(message, toolName) {
        if (!toolName.startsWith('finance.')) {
            return 0;
        }
        let boost = 0;
        if (/banco|bancaria|bancarias|bank/.test(message) &&
            toolName.includes('bank-accounts')) {
            boost += 10;
        }
        if (/extrato|statement/.test(message) && toolName.includes('statements')) {
            boost += 10;
        }
        if (/pagar|payable/.test(message) && toolName.includes('accounts-payable')) {
            boost += 10;
        }
        if (/receber|receivable/.test(message) && toolName.includes('accounts-receivable')) {
            boost += 10;
        }
        if (/moeda|currency/.test(message) && toolName.includes('currencies')) {
            boost += 8;
        }
        if (/transferencia|transfer|transferencias/.test(message) && toolName.includes('transfers')) {
            boost += 8;
        }
        return boost;
    }
    messageLooksLikeToolRequest(normalizedMessage) {
        return ACTION_KEYWORDS.some((keyword) => this.hasTerm(normalizedMessage, this.normalizeText(keyword)));
    }
    isAmbiguousFinanceAccountsRequest(normalizedMessage, entries) {
        const mentionsGenericAccounts = /\b(conta|contas|account|accounts)\b/.test(normalizedMessage);
        const hasQualifier = /banco|bancaria|bancarias|bank|extrato|statement|pagar|payable|receber|receivable/.test(normalizedMessage);
        if (!mentionsGenericAccounts || hasQualifier) {
            return false;
        }
        const financeToolNames = entries
            .map((entry) => entry.definition.name)
            .filter((name) => name.startsWith('finance.'));
        return (financeToolNames.some((name) => name.includes('bank-accounts')) &&
            (financeToolNames.some((name) => name.includes('accounts-payable')) ||
                financeToolNames.some((name) => name.includes('accounts-receivable'))));
    }
    hasTerm(text, term) {
        return new RegExp(`(^|\\s)${this.escapeRegex(term)}($|\\s)`).test(text);
    }
    escapeRegex(value) {
        return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    getDomainLabel(domain, message) {
        const labels = {
            finance: { pt: 'financeiro', en: 'finance' },
            operations: { pt: 'operações', en: 'operations' },
            lms: { pt: 'LMS/cursos', en: 'LMS/courses' },
            core: { pt: 'configurações/core', en: 'settings/core' },
            contact: { pt: 'contatos', en: 'contacts' },
        };
        const label = labels[domain];
        if (!label) {
            return domain;
        }
        return this.isLikelyPortuguese(message) ? label.pt : label.en;
    }
    isLikelyPortuguese(message) {
        return /(voce|você|qual|consultar|listar|conta|contas|bancaria|bancária|extrato|tarefas|curso|cursos)/i.test(message);
    }
    formatDomainScores(domainScores) {
        const formatted = Array.from(domainScores.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, 5)
            .map(([domain, score]) => `${domain}:${score}`)
            .join(',');
        return formatted || 'none';
    }
    async recordAssistantAndReturnHistory(conversationId, content) {
        await this.prisma.mcp_conversation_message.create({
            data: {
                conversation_id: conversationId,
                role: 'assistant',
                content,
            },
        });
        return this.prisma.mcp_conversation_message.findMany({
            where: { conversation_id: conversationId },
            orderBy: { created_at: 'asc' },
        });
    }
    getMessageKeywordSet(message) {
        const keywords = new Set();
        if (/(tarefa|tarefas|task|tasks)/i.test(message)) {
            keywords.add('task');
        }
        if (/(projeto|projetos|project|projects)/i.test(message)) {
            keywords.add('project');
        }
        if (/(turma|class|curso|cursos|course|training)/i.test(message)) {
            keywords.add('class');
        }
        if (/(pendente|pendentes|pending|open|aberta|abertas)/i.test(message)) {
            keywords.add('pending');
        }
        if (/(listar|lista|buscar|procurar|consultar|mostrar|ver|list|search|find|show|get)/i.test(message)) {
            keywords.add('list');
        }
        if (/(criar|adicionar|atualizar|alterar|remover|deletar|excluir|create|add|update|delete|remove)/i.test(message)) {
            keywords.add('write');
        }
        return keywords;
    }
};
exports.McpChatService = McpChatService;
exports.McpChatService = McpChatService = McpChatService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_prisma_1.PrismaService))),
    __param(1, (0, common_1.Inject)((0, common_1.forwardRef)(() => setting_service_1.SettingService))),
    __param(2, (0, common_1.Inject)((0, common_1.forwardRef)(() => mcp_registry_service_1.McpRegistryService))),
    __metadata("design:paramtypes", [api_prisma_1.PrismaService,
        setting_service_1.SettingService,
        mcp_registry_service_1.McpRegistryService])
], McpChatService);
//# sourceMappingURL=mcp-chat.service.js.map