"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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebhookIntegrationService = void 0;
const api_mail_1 = require("@hed-hog/api-mail");
const api_pagination_1 = require("@hed-hog/api-pagination");
const api_prisma_1 = require("@hed-hog/api-prisma");
const axios_1 = require("@nestjs/axios");
const common_1 = require("@nestjs/common");
const events_1 = require("events");
const rxjs_1 = require("rxjs");
const integration_profile_utils_1 = require("../integration-profile/integration-profile.utils");
const mail_service_1 = require("../mail/mail.service");
const mcp_api_key_service_1 = require("../mcp/mcp-api-key.service");
const setting_service_1 = require("../setting/setting.service");
const webhook_command_registry_1 = require("./webhook-command.registry");
const webhook_utils_1 = require("./webhook-utils");
let WebhookIntegrationService = class WebhookIntegrationService {
    constructor(prisma, pagination, http, mailMainService, coreMailService, settingService, commandRegistry, mcpApiKeyService) {
        this.prisma = prisma;
        this.pagination = pagination;
        this.http = http;
        this.mailMainService = mailMainService;
        this.coreMailService = coreMailService;
        this.settingService = settingService;
        this.commandRegistry = commandRegistry;
        this.mcpApiKeyService = mcpApiKeyService;
        this.logEmitter = new events_1.EventEmitter();
        this.webhookActionSelect = {
            id: true,
            webhook_integration_id: true,
            type: true,
            name: true,
            order: true,
            status: true,
            integration_profile_id: true,
            mail_id: true,
            email_to: true,
            email_cc: true,
            email_bcc: true,
            whatsapp_target_type: true,
            whatsapp_target: true,
            whatsapp_template: true,
            whatsapp_instance: true,
            whatsapp_base_url: true,
            whatsapp_token: true,
            http_url: true,
            http_method: true,
            http_headers: true,
            http_query: true,
            http_body: true,
            http_timeout_ms: true,
            http_retry_count: true,
            internal_api_path: true,
            internal_api_method: true,
            internal_api_query: true,
            internal_api_body: true,
            internal_api_token: true,
            internal_api_user_id: true,
            app_command_slug: true,
            app_command_params: true,
            created_at: true,
            updated_at: true,
        };
    }
    get integrations() {
        return this.prisma.webhook_integration;
    }
    get actions() {
        return this.prisma.webhook_action;
    }
    get logs() {
        return this.prisma.webhook_call_log;
    }
    get actionLogs() {
        return this.prisma.webhook_action_log;
    }
    async list(paginationParams) {
        const fields = ['slug', 'name', 'description', 'public_uuid', 'status'];
        const OR = this.prisma.createInsensitiveSearch(fields, paginationParams);
        const result = await this.pagination.paginate(this.integrations, paginationParams, {
            where: OR.length > 0 ? { OR } : {},
            include: {
                webhook_action: {
                    select: this.webhookActionSelect,
                    orderBy: [{ order: 'asc' }, { id: 'asc' }],
                },
            },
            orderBy: [{ updated_at: 'desc' }, { id: 'desc' }],
        });
        return Object.assign(Object.assign({}, result), { data: await this.withPublicUrls(result.data || []) });
    }
    async get(id) {
        const record = await this.integrations.findUnique({
            where: { id },
            include: {
                webhook_action: {
                    select: this.webhookActionSelect,
                    orderBy: [{ order: 'asc' }, { id: 'asc' }],
                },
            },
        });
        if (!record) {
            throw new common_1.NotFoundException(`Webhook integration with id ${id} not found.`);
        }
        return this.withPublicUrl(record);
    }
    async create(data) {
        await this.ensureSlugAvailable(data.slug);
        let plainToken = null;
        if (data.require_token) {
            plainToken = (0, webhook_utils_1.createPlainToken)();
        }
        const record = await this.integrations.create({
            data: Object.assign(Object.assign({}, this.normalizeIntegration(data)), { public_uuid: (0, webhook_utils_1.createPublicUuid)(), token_hash: plainToken ? (0, webhook_utils_1.hashToken)(plainToken) : null }),
        });
        return Object.assign(Object.assign({}, (await this.withPublicUrl(record))), { plainToken });
    }
    async update(id, data) {
        const existing = await this.get(id);
        if (data.slug && data.slug !== existing.slug) {
            await this.ensureSlugAvailable(data.slug);
        }
        let plainToken = null;
        const updateData = this.normalizeIntegration(data, true);
        if (data.require_token === false) {
            updateData.token_hash = null;
        }
        else if (data.require_token === true && !existing.token_hash) {
            plainToken = (0, webhook_utils_1.createPlainToken)();
            updateData.token_hash = (0, webhook_utils_1.hashToken)(plainToken);
        }
        const record = await this.integrations.update({
            where: { id },
            data: updateData,
        });
        return Object.assign(Object.assign({}, (await this.withPublicUrl(record))), { plainToken });
    }
    async delete(data) {
        var _a;
        if (!((_a = data.ids) === null || _a === void 0 ? void 0 : _a.length)) {
            throw new common_1.BadRequestException('You must select at least one item to delete.');
        }
        return this.integrations.deleteMany({
            where: { id: { in: data.ids } },
        });
    }
    async regenerateUuid(id) {
        await this.get(id);
        const record = await this.integrations.update({
            where: { id },
            data: { public_uuid: (0, webhook_utils_1.createPublicUuid)() },
        });
        return this.withPublicUrl(record);
    }
    async regenerateToken(id) {
        await this.get(id);
        const plainToken = (0, webhook_utils_1.createPlainToken)();
        const record = await this.integrations.update({
            where: { id },
            data: {
                require_token: true,
                token_hash: (0, webhook_utils_1.hashToken)(plainToken),
            },
        });
        return Object.assign(Object.assign({}, (await this.withPublicUrl(record))), { plainToken });
    }
    async duplicate(id) {
        var _a;
        const original = await this.get(id);
        const newSlug = await this.generateUniqueSlug(original.slug, this.integrations);
        let plainToken = null;
        let tokenHash = null;
        if (original.require_token) {
            plainToken = (0, webhook_utils_1.createPlainToken)();
            tokenHash = (0, webhook_utils_1.hashToken)(plainToken);
        }
        const record = await this.integrations.create({
            data: {
                slug: newSlug,
                name: `${original.name} (copy)`,
                description: original.description,
                public_uuid: (0, webhook_utils_1.createPublicUuid)(),
                status: original.status,
                require_token: original.require_token,
                token_hash: tokenHash,
                allowed_ips: original.allowed_ips,
                body_schema: original.body_schema,
                webhook_action: {
                    create: ((_a = original.webhook_action) !== null && _a !== void 0 ? _a : []).map((a) => {
                        var _a;
                        return ({
                            type: a.type,
                            name: a.name,
                            order: a.order,
                            status: a.status,
                            integration_profile_id: (_a = a.integration_profile_id) !== null && _a !== void 0 ? _a : null,
                            mail_id: a.mail_id,
                            email_to: a.email_to,
                            email_cc: a.email_cc,
                            email_bcc: a.email_bcc,
                            whatsapp_target_type: a.whatsapp_target_type,
                            whatsapp_target: a.whatsapp_target,
                            whatsapp_template: a.whatsapp_template,
                            whatsapp_instance: a.whatsapp_instance,
                            whatsapp_base_url: a.whatsapp_base_url,
                            whatsapp_token: a.whatsapp_token,
                            http_url: a.http_url,
                            http_method: a.http_method,
                            http_headers: a.http_headers,
                            http_query: a.http_query,
                            http_body: a.http_body,
                            http_timeout_ms: a.http_timeout_ms,
                            http_retry_count: a.http_retry_count,
                            internal_api_path: a.internal_api_path,
                            internal_api_method: a.internal_api_method,
                            internal_api_query: a.internal_api_query,
                            internal_api_body: a.internal_api_body,
                            internal_api_token: a.internal_api_token,
                            internal_api_user_id: a.internal_api_user_id,
                            app_command_slug: a.app_command_slug,
                            app_command_params: a.app_command_params,
                        });
                    }),
                },
            },
        });
        return Object.assign(Object.assign({}, (await this.withPublicUrl(record))), { plainToken });
    }
    async generateUniqueSlug(base, table) {
        const candidate = `${base}-copy`;
        const existing = await table.findUnique({ where: { slug: candidate } });
        if (!existing)
            return candidate;
        for (let i = 2;; i++) {
            const next = `${base}-copy-${i}`;
            const taken = await table.findUnique({ where: { slug: next } });
            if (!taken)
                return next;
        }
    }
    async getApplicationBaseUrl() {
        const settings = await this.settingService.getSettingValues([
            'api-url',
            'url',
        ]);
        const value = (settings === null || settings === void 0 ? void 0 : settings['api-url']) || (settings === null || settings === void 0 ? void 0 : settings.url);
        return typeof value === 'string' ? value.replace(/\/+$/, '') : '';
    }
    async withPublicUrl(record) {
        const baseUrl = await this.getApplicationBaseUrl();
        return Object.assign(Object.assign({}, record), { public_url: baseUrl && record.public_uuid
                ? `${baseUrl}/webhook/${record.public_uuid}`
                : null });
    }
    async withPublicUrls(records) {
        const baseUrl = await this.getApplicationBaseUrl();
        return records.map((record) => (Object.assign(Object.assign({}, record), { public_url: baseUrl && record.public_uuid
                ? `${baseUrl}/webhook/${record.public_uuid}`
                : null })));
    }
    async listActions(integrationId) {
        await this.get(integrationId);
        return this.actions.findMany({
            where: { webhook_integration_id: integrationId },
            orderBy: [{ order: 'asc' }, { id: 'asc' }],
        });
    }
    async createAction(integrationId, data) {
        await this.get(integrationId);
        const normalized = this.normalizeAction(data);
        this.validateActionConfiguration(normalized);
        return this.actions.create({
            data: Object.assign(Object.assign({}, normalized), { webhook_integration_id: integrationId }),
        });
    }
    async updateAction(integrationId, actionId, data) {
        const existing = await this.ensureAction(integrationId, actionId);
        const normalized = this.normalizeAction(data, true);
        this.validateActionConfiguration(Object.assign(Object.assign({}, existing), normalized));
        return this.actions.update({
            where: { id: actionId },
            data: normalized,
        });
    }
    async deleteActions(integrationId, data) {
        var _a;
        await this.get(integrationId);
        if (!((_a = data.ids) === null || _a === void 0 ? void 0 : _a.length)) {
            throw new common_1.BadRequestException('You must select at least one item to delete.');
        }
        return this.actions.deleteMany({
            where: {
                webhook_integration_id: integrationId,
                id: { in: data.ids },
            },
        });
    }
    async listUserApiTokens(userId) {
        return this.prisma.mcp_api_key.findMany({
            where: { user_id: userId, type: 'api', revoked_at: null },
            select: {
                id: true,
                name: true,
                token_prefix: true,
                last_used_at: true,
                created_at: true,
            },
            orderBy: { created_at: 'desc' },
        });
    }
    async createUserApiToken(userId, name) {
        return this.mcpApiKeyService.create(userId, name, 'api');
    }
    async listInternalRoutes() {
        return this.prisma.route.findMany({
            select: {
                id: true,
                url: true,
                method: true,
            },
            where: {
                url: { not: null },
                method: { not: null },
            },
            orderBy: [{ url: 'asc' }, { method: 'asc' }],
        });
    }
    async listAppCommands() {
        return this.commandRegistry.list().map((cmd) => ({
            slug: cmd.slug,
            name: cmd.name,
            description: cmd.description,
            inputSchema: cmd.inputSchema,
        }));
    }
    async listLogs(integrationId, paginationParams, sort = 'desc') {
        await this.get(integrationId);
        const page = Math.max(Number(paginationParams.page || 1), 1);
        const pageSize = Math.max(Number(paginationParams.pageSize || 10), 1);
        const skip = (page - 1) * pageSize;
        const normalizedSort = sort === 'asc' ? 'asc' : 'desc';
        const where = { webhook_integration_id: integrationId };
        const [data, total] = await Promise.all([
            this.logs.findMany({
                where,
                orderBy: [{ created_at: normalizedSort }, { id: normalizedSort }],
                skip,
                take: pageSize,
            }),
            this.logs.count({ where }),
        ]);
        const lastPage = Math.max(1, Math.ceil(total / pageSize));
        const result = {
            total,
            lastPage,
            page,
            pageSize,
            prev: page > 1 ? page - 1 : null,
            next: page < lastPage ? page + 1 : null,
            data,
        };
        const logIds = (result.data || [])
            .map((item) => item.id)
            .filter((id) => typeof id === 'number');
        if (!logIds.length || !this.actionLogs) {
            return result;
        }
        const actionLogs = await this.actionLogs.findMany({
            where: {
                webhook_call_log_id: { in: logIds },
            },
            orderBy: [{ created_at: 'asc' }, { id: 'asc' }],
        });
        const actionLogsByCallId = new Map();
        for (const actionLog of actionLogs || []) {
            const list = actionLogsByCallId.get(actionLog.webhook_call_log_id) || [];
            list.push(actionLog);
            actionLogsByCallId.set(actionLog.webhook_call_log_id, list);
        }
        return Object.assign(Object.assign({}, result), { data: (result.data || []).map((item) => (Object.assign(Object.assign({}, item), { webhook_action_log: actionLogsByCallId.get(item.id) || [] }))) });
    }
    streamLogs(integrationId, res) {
        res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
        res.setHeader('Cache-Control', 'no-cache, no-transform');
        res.setHeader('Connection', 'keep-alive');
        res.setHeader('X-Accel-Buffering', 'no');
        res.flushHeaders();
        res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
        const handler = (payload) => {
            res.write(`data: ${JSON.stringify({ type: 'log', payload })}\n\n`);
        };
        const eventKey = `log:${integrationId}`;
        this.logEmitter.on(eventKey, handler);
        res.on('close', () => {
            this.logEmitter.off(eventKey, handler);
        });
    }
    async executePublic(uuid, body, headers, req) {
        const startedAt = Date.now();
        const remoteIp = this.getRemoteIp(req);
        const actionLogs = [];
        let integration = null;
        try {
            integration = await this.integrations.findUnique({
                where: { public_uuid: uuid },
                include: {
                    webhook_action: {
                        select: this.webhookActionSelect,
                        where: { status: 'active' },
                        orderBy: [{ order: 'asc' }, { id: 'asc' }],
                    },
                },
            });
            if (!integration || integration.status !== 'active') {
                throw new common_1.NotFoundException('Webhook not found.');
            }
            this.validateToken(integration, headers);
            this.validateIp(integration, remoteIp);
            this.validateBodySchema(integration, body);
            const actionResults = [];
            for (const action of integration.webhook_action || []) {
                const actionStartedAt = Date.now();
                try {
                    const result = await this.executeAction(action, body);
                    const durationMs = Date.now() - actionStartedAt;
                    actionResults.push({
                        id: action.id,
                        type: action.type,
                        success: true,
                        duration_ms: durationMs,
                        response: (0, webhook_utils_1.summarizePayload)(result.responsePayload),
                    });
                    actionLogs.push({
                        webhookActionId: action.id,
                        actionName: action.name,
                        actionType: action.type,
                        status: 'success',
                        durationMs,
                        requestPayload: result.requestPayload,
                        responsePayload: result.responsePayload,
                    });
                }
                catch (error) {
                    const durationMs = Date.now() - actionStartedAt;
                    const errorMessage = this.extractErrorMessage(error);
                    actionResults.push({
                        id: action.id,
                        type: action.type,
                        success: false,
                        duration_ms: durationMs,
                        error: errorMessage,
                        response: (0, webhook_utils_1.summarizePayload)(this.extractErrorResponse(error)),
                    });
                    actionLogs.push({
                        webhookActionId: action.id,
                        actionName: action.name,
                        actionType: action.type,
                        status: 'failed',
                        durationMs,
                        requestPayload: this.extractErrorRequestPayload(error),
                        errorMessage,
                        responsePayload: this.extractErrorResponse(error),
                    });
                    throw error;
                }
            }
            const response = {
                success: true,
                actions: actionResults,
            };
            const callLog = await this.createCallLog({
                integration,
                uuid,
                status: 'success',
                remoteIp,
                durationMs: Date.now() - startedAt,
                payload: body,
                response,
            });
            await this.createActionLogs(callLog === null || callLog === void 0 ? void 0 : callLog.id, actionLogs);
            if ((integration === null || integration === void 0 ? void 0 : integration.id) && (callLog === null || callLog === void 0 ? void 0 : callLog.id)) {
                this.logEmitter.emit(`log:${integration.id}`, { id: callLog.id });
            }
            return response;
        }
        catch (error) {
            const callLog = await this.createCallLog({
                integration,
                uuid,
                status: 'failed',
                remoteIp,
                durationMs: Date.now() - startedAt,
                payload: body,
                response: {
                    success: false,
                    actions: actionLogs.map((item) => ({
                        id: item.webhookActionId,
                        type: item.actionType,
                        success: item.status === 'success',
                        duration_ms: item.durationMs,
                        error: item.errorMessage || null,
                    })),
                },
                error,
            }).catch(() => null);
            await this.createActionLogs(callLog === null || callLog === void 0 ? void 0 : callLog.id, actionLogs).catch(() => undefined);
            if ((integration === null || integration === void 0 ? void 0 : integration.id) && (callLog === null || callLog === void 0 ? void 0 : callLog.id)) {
                this.logEmitter.emit(`log:${integration.id}`, { id: callLog.id });
            }
            if (error instanceof common_1.HttpException) {
                throw error;
            }
            return {
                success: false,
                actions: actionLogs.map((item) => ({
                    id: item.webhookActionId,
                    type: item.actionType,
                    success: item.status === 'success',
                    duration_ms: item.durationMs,
                    error: item.errorMessage || null,
                })),
            };
        }
    }
    async executeAction(action, body) {
        if (action.type === 'email') {
            return this.executeEmailAction(action, body);
        }
        if (action.type === 'whatsapp_evolution') {
            return this.executeWhatsappAction(action, body);
        }
        if (action.type === 'http_request') {
            return this.executeHttpRequestAction(action, body);
        }
        if (action.type === 'internal_api') {
            return this.executeInternalApiAction(action, body);
        }
        if (action.type === 'app_command') {
            return this.executeAppCommandAction(action, body);
        }
        throw new common_1.BadRequestException(`Unsupported action type: ${action.type}`);
    }
    async executeEmailAction(action, body) {
        var _a, _b, _c, _d;
        this.validateActionConfiguration(action);
        const profile = await this.prisma.integration_profile.findUnique({
            where: { id: Number(action.integration_profile_id) },
            include: { integration_provider: { select: { slug: true } } },
        });
        const mail = await this.prisma.mail.findUnique({
            where: { id: Number(action.mail_id) },
            include: {
                mail_locale: {
                    orderBy: [{ locale_id: 'asc' }],
                },
            },
        });
        if (!profile || !((_a = mail === null || mail === void 0 ? void 0 : mail.mail_locale) === null || _a === void 0 ? void 0 : _a.length)) {
            throw new common_1.BadRequestException('Email action references an invalid profile or template.');
        }
        const template = mail.mail_locale[0];
        const config = (0, integration_profile_utils_1.buildMailConfigFromIntegration)(profile.integration_provider.slug, profile.config);
        const sender = (0, integration_profile_utils_1.getFromAddress)(profile.config);
        const replyTo = (0, integration_profile_utils_1.getReplyToAddress)(profile.config);
        const renderedTo = String((_b = (0, webhook_utils_1.renderTemplate)(action.email_to, body)) !== null && _b !== void 0 ? _b : '').trim();
        const renderedCc = action.email_cc
            ? String((_c = (0, webhook_utils_1.renderTemplate)(action.email_cc, body)) !== null && _c !== void 0 ? _c : '').trim()
            : '';
        const renderedBcc = action.email_bcc
            ? String((_d = (0, webhook_utils_1.renderTemplate)(action.email_bcc, body)) !== null && _d !== void 0 ? _d : '').trim()
            : '';
        if (!renderedTo) {
            throw new common_1.BadRequestException('Email action rendered an empty recipient for email_to.');
        }
        const requestPayload = {
            to: renderedTo,
            cc: renderedCc || undefined,
            bcc: renderedBcc || undefined,
            from: sender,
            replyTo,
            subject: (0, webhook_utils_1.renderTemplate)(template.subject, body),
            body: (0, webhook_utils_1.renderTemplate)(template.body, body),
            mail_id: mail.id,
        };
        try {
            this.mailMainService.setConfig(config);
            const providerResponse = await this.mailMainService.send(requestPayload);
            return {
                requestPayload,
                responsePayload: {
                    provider: 'email',
                    response: providerResponse || null,
                },
            };
        }
        finally {
            await this.coreMailService.reloadConfig().catch(() => undefined);
        }
    }
    async executeWhatsappAction(action, body) {
        var _a, _b;
        const required = [
            action.whatsapp_target,
            action.whatsapp_template,
            action.whatsapp_instance,
            action.whatsapp_base_url,
            action.whatsapp_token,
        ];
        if (required.some((value) => !String(value || '').trim())) {
            throw new common_1.BadRequestException('WhatsApp action is missing required configuration.');
        }
        const baseUrl = String(action.whatsapp_base_url).replace(/\/+$/, '');
        const instance = encodeURIComponent(action.whatsapp_instance);
        const url = `${baseUrl}/message/sendText/${instance}`;
        const targetType = action.whatsapp_target_type === 'group' ? 'group' : 'phone';
        const renderedMessage = String((_a = (0, webhook_utils_1.renderTemplate)(action.whatsapp_template, body)) !== null && _a !== void 0 ? _a : '').trim();
        const renderedTarget = String((_b = (0, webhook_utils_1.renderTemplate)(action.whatsapp_target, body)) !== null && _b !== void 0 ? _b : '').trim();
        if (!renderedMessage) {
            throw new common_1.BadRequestException('WhatsApp action template rendered an empty message.');
        }
        const target = this.normalizeWhatsappTarget(renderedTarget, targetType);
        const requestPayload = {
            provider: 'evolution',
            url,
            instance: action.whatsapp_instance,
            targetType,
            number: target,
            text: renderedMessage,
        };
        let response;
        try {
            response = await (0, rxjs_1.firstValueFrom)(this.http.post(url, {
                number: target,
                text: renderedMessage,
            }, {
                headers: {
                    apikey: action.whatsapp_token,
                },
                timeout: 15000,
            }));
        }
        catch (error) {
            this.attachErrorRequestPayload(error, requestPayload);
            throw error;
        }
        return {
            requestPayload,
            responsePayload: {
                status: (response === null || response === void 0 ? void 0 : response.status) || null,
                statusText: (response === null || response === void 0 ? void 0 : response.statusText) || null,
                data: (response === null || response === void 0 ? void 0 : response.data) || null,
            },
        };
    }
    async executeHttpRequestAction(action, body) {
        var _a, _b, _c;
        this.validateActionConfiguration(action);
        const method = String(action.http_method || 'POST').toUpperCase();
        const rawUrl = String(action.http_url || '').trim();
        const timeoutMs = Number(action.http_timeout_ms || 30000);
        const maxAttempts = Math.max(1, Number(action.http_retry_count || 0) + 1);
        const renderedUrl = String((_a = (0, webhook_utils_1.renderTemplate)(rawUrl, body)) !== null && _a !== void 0 ? _a : rawUrl).trim();
        const rawHeaders = (0, webhook_utils_1.normalizeJsonObject)(action.http_headers) || {};
        const renderedHeaders = {};
        for (const [k, v] of Object.entries(rawHeaders)) {
            renderedHeaders[k] = String((_b = (0, webhook_utils_1.renderTemplate)(String(v), body)) !== null && _b !== void 0 ? _b : v);
        }
        const rawQuery = (0, webhook_utils_1.normalizeJsonObject)(action.http_query) || {};
        const renderedQuery = {};
        for (const [k, v] of Object.entries(rawQuery)) {
            renderedQuery[k] = String((_c = (0, webhook_utils_1.renderTemplate)(String(v), body)) !== null && _c !== void 0 ? _c : v);
        }
        const rawBody = String(action.http_body || '').trim();
        const renderedBody = rawBody
            ? (0, webhook_utils_1.renderTemplate)(rawBody, body)
            : undefined;
        let parsedBody = renderedBody;
        if (renderedBody) {
            try {
                parsedBody = JSON.parse(renderedBody);
            }
            catch (_d) {
                parsedBody = renderedBody;
            }
        }
        const requestPayload = {
            method,
            url: renderedUrl,
            headers: renderedHeaders,
            query: renderedQuery,
            body: parsedBody,
        };
        let lastError;
        for (let attempt = 0; attempt < maxAttempts; attempt++) {
            try {
                const response = await (0, rxjs_1.firstValueFrom)(this.http.request({
                    method,
                    url: renderedUrl,
                    headers: renderedHeaders,
                    params: Object.keys(renderedQuery).length ? renderedQuery : undefined,
                    data: parsedBody,
                    timeout: timeoutMs,
                }));
                return {
                    requestPayload,
                    responsePayload: {
                        status: (response === null || response === void 0 ? void 0 : response.status) || null,
                        statusText: (response === null || response === void 0 ? void 0 : response.statusText) || null,
                        data: (response === null || response === void 0 ? void 0 : response.data) || null,
                    },
                };
            }
            catch (error) {
                lastError = error;
                if (attempt === maxAttempts - 1) {
                    this.attachErrorRequestPayload(error, requestPayload);
                    throw error;
                }
            }
        }
        this.attachErrorRequestPayload(lastError, requestPayload);
        throw lastError;
    }
    async executeInternalApiAction(action, body) {
        var _a, _b;
        this.validateActionConfiguration(action);
        const baseUrl = await this.getApplicationBaseUrl();
        if (!baseUrl) {
            throw new common_1.BadRequestException('Internal API action requires the api-url setting to be configured.');
        }
        const method = String(action.internal_api_method || 'POST').toUpperCase();
        const rawPath = String(action.internal_api_path || '').trim();
        const token = String(action.internal_api_token || '').trim();
        const renderedPath = String((_a = (0, webhook_utils_1.renderTemplate)(rawPath, body)) !== null && _a !== void 0 ? _a : rawPath).trim();
        const rawQuery = (0, webhook_utils_1.normalizeJsonObject)(action.internal_api_query) || {};
        const renderedQuery = {};
        for (const [k, v] of Object.entries(rawQuery)) {
            renderedQuery[k] = String((_b = (0, webhook_utils_1.renderTemplate)(String(v), body)) !== null && _b !== void 0 ? _b : v);
        }
        const rawBody = (0, webhook_utils_1.normalizeJsonObject)(action.internal_api_body) || {};
        const renderedBodyObj = {};
        for (const [k, v] of Object.entries(rawBody)) {
            renderedBodyObj[k] = typeof v === 'string' ? (0, webhook_utils_1.renderTemplate)(v, body) : v;
        }
        const url = `${baseUrl}${renderedPath.startsWith('/') ? '' : '/'}${renderedPath}`;
        const requestPayload = {
            method,
            url,
            query: renderedQuery,
            body: renderedBodyObj,
        };
        let response;
        try {
            response = await (0, rxjs_1.firstValueFrom)(this.http.request({
                method,
                url,
                headers: {
                    Authorization: `Bearer ${token}`,
                    'Content-Type': 'application/json',
                },
                params: Object.keys(renderedQuery).length ? renderedQuery : undefined,
                data: Object.keys(renderedBodyObj).length ? renderedBodyObj : undefined,
                timeout: 30000,
            }));
        }
        catch (error) {
            this.attachErrorRequestPayload(error, requestPayload);
            throw error;
        }
        return {
            requestPayload,
            responsePayload: {
                status: (response === null || response === void 0 ? void 0 : response.status) || null,
                statusText: (response === null || response === void 0 ? void 0 : response.statusText) || null,
                data: (response === null || response === void 0 ? void 0 : response.data) || null,
            },
        };
    }
    async executeAppCommandAction(action, body) {
        this.validateActionConfiguration(action);
        const slug = String(action.app_command_slug || '').trim();
        const command = this.commandRegistry.find(slug);
        if (!command) {
            throw new common_1.BadRequestException(`app_command: unknown command slug "${slug}".`);
        }
        const rawParams = (0, webhook_utils_1.normalizeJsonObject)(action.app_command_params) || {};
        const renderedParams = {};
        for (const [k, v] of Object.entries(rawParams)) {
            renderedParams[k] = typeof v === 'string' ? (0, webhook_utils_1.renderTemplate)(v, body) : v;
        }
        const schemaErrors = this.validateJsonSchema(command.inputSchema, renderedParams);
        if (schemaErrors.length) {
            throw new common_1.BadRequestException({
                message: `app_command "${slug}" params do not match expected schema.`,
                errors: schemaErrors,
            });
        }
        const requestPayload = { slug, params: renderedParams };
        let result;
        try {
            result = await this.commandRegistry.dispatch(slug, renderedParams, {
                webhookBody: body,
            });
        }
        catch (error) {
            this.attachErrorRequestPayload(error, requestPayload);
            throw error;
        }
        return {
            requestPayload,
            responsePayload: result,
        };
    }
    validateToken(integration, headers) {
        var _a;
        if (!integration.require_token) {
            return;
        }
        const provided = headers['x-webhook-token'] ||
            headers['X-Webhook-Token'] ||
            ((_a = headers.authorization) === null || _a === void 0 ? void 0 : _a.replace(/^Bearer\s+/i, ''));
        if (!provided || (0, webhook_utils_1.hashToken)(String(provided)) !== integration.token_hash) {
            throw new common_1.BadRequestException('Invalid webhook token.');
        }
    }
    validateIp(integration, remoteIp) {
        const allowedIps = Array.isArray(integration.allowed_ips)
            ? integration.allowed_ips
            : [];
        if (!allowedIps.length) {
            return;
        }
        if (!remoteIp || !allowedIps.includes(remoteIp)) {
            throw new common_1.BadRequestException('Remote IP is not allowed.');
        }
    }
    validateActionConfiguration(action) {
        if ((action === null || action === void 0 ? void 0 : action.type) === 'email') {
            const missing = [];
            if (!this.nullableNumber(action.integration_profile_id)) {
                missing.push('integration_profile_id');
            }
            if (!this.nullableNumber(action.mail_id)) {
                missing.push('mail_id');
            }
            if (!this.nullableString(action.email_to, true)) {
                missing.push('email_to');
            }
            if (missing.length) {
                throw new common_1.BadRequestException(`Email action is missing required configuration: ${missing.join(', ')}.`);
            }
        }
        if ((action === null || action === void 0 ? void 0 : action.type) === 'whatsapp_evolution') {
            const missing = [
                ['whatsapp_target', action.whatsapp_target],
                ['whatsapp_template', action.whatsapp_template],
                ['whatsapp_instance', action.whatsapp_instance],
                ['whatsapp_base_url', action.whatsapp_base_url],
                ['whatsapp_token', action.whatsapp_token],
            ]
                .filter(([, value]) => !String(value || '').trim())
                .map(([key]) => key);
            if (missing.length) {
                throw new common_1.BadRequestException(`WhatsApp action is missing required configuration: ${missing.join(', ')}.`);
            }
        }
        if ((action === null || action === void 0 ? void 0 : action.type) === 'http_request') {
            if (!this.nullableString(action.http_url)) {
                throw new common_1.BadRequestException('HTTP request action is missing required configuration: http_url.');
            }
        }
        if ((action === null || action === void 0 ? void 0 : action.type) === 'internal_api') {
            const missing = [];
            if (!this.nullableString(action.internal_api_path)) {
                missing.push('internal_api_path');
            }
            if (!this.nullableNumber(action.internal_api_user_id)) {
                missing.push('internal_api_user_id');
            }
            if (!this.nullableString(action.internal_api_token)) {
                missing.push('internal_api_token');
            }
            if (missing.length) {
                throw new common_1.BadRequestException(`Internal API action is missing required configuration: ${missing.join(', ')}.`);
            }
        }
        if ((action === null || action === void 0 ? void 0 : action.type) === 'app_command') {
            if (!this.nullableString(action.app_command_slug)) {
                throw new common_1.BadRequestException('App command action is missing required configuration: app_command_slug.');
            }
        }
    }
    validateBodySchema(integration, body) {
        if (!integration.body_schema) {
            return;
        }
        const errors = this.validateJsonSchema(integration.body_schema, body);
        if (errors.length) {
            throw new common_1.BadRequestException({
                message: 'Webhook body does not match the configured schema.',
                errors,
            });
        }
    }
    validateJsonSchema(schema, value, path = '$') {
        if (!schema || typeof schema !== 'object') {
            return [];
        }
        const errors = [];
        const type = schema.type;
        if (type && !this.matchesJsonSchemaType(type, value)) {
            errors.push(`${path} must be ${type}`);
            return errors;
        }
        if (type === 'object' || schema.properties || schema.required) {
            if (!value || typeof value !== 'object' || Array.isArray(value)) {
                errors.push(`${path} must be object`);
                return errors;
            }
            for (const requiredKey of schema.required || []) {
                if (value[requiredKey] === undefined || value[requiredKey] === null) {
                    errors.push(`${path}.${requiredKey} is required`);
                }
            }
            for (const [key, childSchema] of Object.entries(schema.properties || {})) {
                if (value[key] !== undefined) {
                    errors.push(...this.validateJsonSchema(childSchema, value[key], `${path}.${key}`));
                }
            }
        }
        if (type === 'array' && schema.items && Array.isArray(value)) {
            value.forEach((item, index) => {
                errors.push(...this.validateJsonSchema(schema.items, item, `${path}[${index}]`));
            });
        }
        return errors;
    }
    matchesJsonSchemaType(type, value) {
        const types = Array.isArray(type) ? type : [type];
        return types.some((item) => {
            switch (item) {
                case 'object':
                    return (value !== null && typeof value === 'object' && !Array.isArray(value));
                case 'array':
                    return Array.isArray(value);
                case 'string':
                    return typeof value === 'string';
                case 'number':
                    return typeof value === 'number';
                case 'integer':
                    return Number.isInteger(value);
                case 'boolean':
                    return typeof value === 'boolean';
                case 'null':
                    return value === null;
                default:
                    return true;
            }
        });
    }
    async createCallLog(input) {
        var _a, _b;
        if (!this.logs) {
            return null;
        }
        return this.logs.create({
            data: {
                webhook_integration_id: (_b = (_a = input.integration) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null,
                public_uuid: input.uuid,
                status: input.status,
                remote_ip: input.remoteIp,
                duration_ms: input.durationMs,
                error_message: input.error ? this.extractErrorMessage(input.error) : null,
                payload_summary: (0, webhook_utils_1.summarizePayload)((0, webhook_utils_1.sanitizeSensitiveData)(input.payload)),
                response_summary: (0, webhook_utils_1.summarizePayload)((0, webhook_utils_1.sanitizeSensitiveData)(input.response)),
            },
            select: {
                id: true,
            },
        });
    }
    async createActionLogs(callLogId, logs) {
        if (!callLogId || !logs.length || !this.actionLogs) {
            return;
        }
        await this.actionLogs.createMany({
            data: logs.map((item) => ({
                webhook_call_log_id: callLogId,
                webhook_action_id: item.webhookActionId,
                action_name: item.actionName,
                action_type: item.actionType,
                status: item.status,
                duration_ms: item.durationMs,
                request_payload: (0, webhook_utils_1.summarizePayload)((0, webhook_utils_1.sanitizeSensitiveData)(item.requestPayload)),
                response_payload: (0, webhook_utils_1.summarizePayload)((0, webhook_utils_1.sanitizeSensitiveData)(item.responsePayload)),
                error_message: item.errorMessage || null,
            })),
        });
    }
    extractErrorMessage(error) {
        if (!error) {
            return 'Unknown error';
        }
        if (error instanceof Error && error.message) {
            return error.message;
        }
        if (typeof error === 'object' && error.message) {
            return String(error.message);
        }
        return String(error);
    }
    extractErrorResponse(error) {
        if (!error || typeof error !== 'object') {
            return null;
        }
        const maybeResponse = error.response;
        if (!maybeResponse) {
            return null;
        }
        return {
            status: maybeResponse.status || null,
            statusText: maybeResponse.statusText || null,
            data: maybeResponse.data || null,
        };
    }
    extractErrorRequestPayload(error) {
        if (!error || typeof error !== 'object') {
            return null;
        }
        const maybePayload = error.__webhookRequestPayload;
        return maybePayload || null;
    }
    attachErrorRequestPayload(error, requestPayload) {
        if (!error || typeof error !== 'object') {
            return;
        }
        error.__webhookRequestPayload = requestPayload;
    }
    normalizeWhatsappTarget(renderedTarget, targetType) {
        const rawTarget = String(renderedTarget || '').trim();
        if (!rawTarget) {
            throw new common_1.BadRequestException('WhatsApp action target template rendered an empty value.');
        }
        if (targetType === 'group') {
            return rawTarget;
        }
        const normalized = rawTarget.replace(/\D+/g, '');
        if (!normalized) {
            throw new common_1.BadRequestException('WhatsApp action target must contain at least one digit for phone targets.');
        }
        return normalized;
    }
    getRemoteIp(req) {
        const forwardedFor = req.headers['x-forwarded-for'];
        const forwardedIp = Array.isArray(forwardedFor)
            ? forwardedFor[0]
            : forwardedFor === null || forwardedFor === void 0 ? void 0 : forwardedFor.split(',')[0];
        return (forwardedIp === null || forwardedIp === void 0 ? void 0 : forwardedIp.trim()) || req.ip || req.socket.remoteAddress || null;
    }
    async ensureAction(integrationId, actionId) {
        const record = await this.actions.findFirst({
            where: {
                id: actionId,
                webhook_integration_id: integrationId,
            },
        });
        if (!record) {
            throw new common_1.NotFoundException(`Webhook action with id ${actionId} not found.`);
        }
        return record;
    }
    async ensureSlugAvailable(slug) {
        const existing = await this.integrations.findUnique({
            where: { slug: String(slug || '').trim() },
            select: { id: true },
        });
        if (existing) {
            throw new common_1.BadRequestException(`Webhook integration "${slug}" already exists.`);
        }
    }
    normalizeIntegration(data, partial = false) {
        var _a, _b;
        const normalized = {};
        const assign = (key, value) => {
            if (!partial || value !== undefined) {
                normalized[key] = value;
            }
        };
        assign('slug', String((_a = data.slug) !== null && _a !== void 0 ? _a : '').trim());
        assign('name', String((_b = data.name) !== null && _b !== void 0 ? _b : '').trim());
        assign('description', this.nullableString(data.description, partial));
        assign('status', data.status === 'inactive' ? 'inactive' : 'active');
        assign('require_token', Boolean(data.require_token));
        assign('allowed_ips', (0, webhook_utils_1.normalizeJsonObject)(data.allowed_ips));
        assign('body_schema', (0, webhook_utils_1.normalizeJsonObject)(data.body_schema));
        return normalized;
    }
    normalizeAction(data, partial = false) {
        var _a, _b;
        const normalized = {};
        const assign = (key, value) => {
            if (!partial || value !== undefined) {
                normalized[key] = value;
            }
        };
        assign('type', data.type);
        assign('name', String((_a = data.name) !== null && _a !== void 0 ? _a : '').trim());
        assign('order', Number((_b = data.order) !== null && _b !== void 0 ? _b : 0));
        assign('status', data.status === 'inactive' ? 'inactive' : 'active');
        assign('integration_profile_id', this.nullableNumber(data.integration_profile_id, partial));
        assign('mail_id', this.nullableNumber(data.mail_id, partial));
        assign('email_to', this.nullableString(data.email_to, partial));
        assign('email_cc', this.nullableString(data.email_cc, partial));
        assign('email_bcc', this.nullableString(data.email_bcc, partial));
        assign('whatsapp_target_type', data.whatsapp_target_type || null);
        assign('whatsapp_target', this.nullableString(data.whatsapp_target, partial));
        assign('whatsapp_template', this.nullableString(data.whatsapp_template, partial));
        assign('whatsapp_instance', this.nullableString(data.whatsapp_instance, partial));
        assign('whatsapp_base_url', this.nullableString(data.whatsapp_base_url, partial));
        assign('whatsapp_token', this.nullableString(data.whatsapp_token, partial));
        assign('http_url', this.nullableString(data.http_url, partial));
        assign('http_method', this.nullableString(data.http_method, partial));
        assign('http_headers', (0, webhook_utils_1.normalizeJsonObject)(data.http_headers));
        assign('http_query', (0, webhook_utils_1.normalizeJsonObject)(data.http_query));
        assign('http_body', this.nullableString(data.http_body, partial));
        assign('http_timeout_ms', this.nullableNumber(data.http_timeout_ms, partial));
        assign('http_retry_count', this.nullableNumber(data.http_retry_count, partial));
        assign('internal_api_path', this.nullableString(data.internal_api_path, partial));
        assign('internal_api_method', this.nullableString(data.internal_api_method, partial));
        assign('internal_api_query', (0, webhook_utils_1.normalizeJsonObject)(data.internal_api_query));
        assign('internal_api_body', (0, webhook_utils_1.normalizeJsonObject)(data.internal_api_body));
        assign('internal_api_token', this.nullableString(data.internal_api_token, partial));
        assign('internal_api_user_id', this.nullableNumber(data.internal_api_user_id, partial));
        assign('app_command_slug', this.nullableString(data.app_command_slug, partial));
        assign('app_command_params', (0, webhook_utils_1.normalizeJsonObject)(data.app_command_params));
        return normalized;
    }
    nullableString(value, partial = false) {
        if (value === undefined) {
            return partial ? undefined : null;
        }
        if (value === null) {
            return null;
        }
        const normalized = String(value).trim();
        return normalized || null;
    }
    nullableNumber(value, partial = false) {
        if (value === undefined) {
            return partial ? undefined : null;
        }
        if (value === null || value === '') {
            return null;
        }
        return Number(value);
    }
};
exports.WebhookIntegrationService = WebhookIntegrationService;
exports.WebhookIntegrationService = WebhookIntegrationService = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [api_prisma_1.PrismaService,
        api_pagination_1.PaginationService,
        axios_1.HttpService,
        api_mail_1.MailService,
        mail_service_1.MailService,
        setting_service_1.SettingService,
        webhook_command_registry_1.WebhookCommandRegistry,
        mcp_api_key_service_1.McpApiKeyService])
], WebhookIntegrationService);
//# sourceMappingURL=webhook-integration.service.js.map