"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.ChatService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const crypto_1 = require("crypto");
const decorators_1 = require("@n8n/decorators");
const di_1 = require("@n8n/di");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const ws_1 = require("ws");
const zod_1 = require("zod");
const chat_execution_manager_1 = require("./chat-execution-manager");
const chat_service_types_1 = require("./chat-service.types");
const utils_1 = require("./utils");
const CHECK_FOR_RESPONSE_INTERVAL = 3000;
const DRAIN_TIMEOUT = 50;
const HEARTBEAT_INTERVAL = 30 * 1000;
const HEARTBEAT_TIMEOUT = 60 * 1000;
const N8N_CONTINUE = 'n8n|continue';
const N8N_HEARTBEAT = 'n8n|heartbeat';
const N8N_HEARTBEAT_ACK = 'n8n|heartbeat-ack';
function closeConnection(ws) {
    if (ws.readyState !== ws_1.WebSocket.OPEN)
        return;
    ws.once('drain', () => {
        ws.close();
    });
    setTimeout(() => {
        if (ws.readyState === ws_1.WebSocket.OPEN) {
            ws.close();
        }
    }, DRAIN_TIMEOUT);
}
let ChatService = class ChatService {
    constructor(executionManager, logger, errorReporter) {
        this.executionManager = executionManager;
        this.logger = logger;
        this.errorReporter = errorReporter;
        this.sessions = new Map();
        this.heartbeatIntervalId = setInterval(async () => await this.checkHeartbeats(), HEARTBEAT_INTERVAL);
    }
    async startSession(req) {
        const { ws, query: { sessionId, executionId, isPublic, token }, } = req;
        if (!ws) {
            throw new n8n_workflow_1.UnexpectedError('WebSocket connection is missing');
        }
        if (!sessionId || !executionId) {
            ws.close(1008);
            return;
        }
        const execution = await this.executionManager.findExecution(executionId);
        if (!execution) {
            ws.send('Connection rejected');
            ws.close(1008);
            return;
        }
        if (execution.data?.resumeToken) {
            const tokenBuf = Buffer.from(token ?? '');
            const storedBuf = Buffer.from(execution.data.resumeToken);
            if (!token || tokenBuf.length !== storedBuf.length || !(0, crypto_1.timingSafeEqual)(tokenBuf, storedBuf)) {
                ws.send('Connection rejected');
                ws.close(1008);
                return;
            }
        }
        ws.isAlive = true;
        const key = `${sessionId}|${executionId}|${isPublic ? 'public' : 'integrated'}`;
        const existingSession = this.sessions.get(key);
        if (existingSession) {
            this.cleanupSession(existingSession, key);
        }
        const onMessage = this.incomingMessageHandler(key);
        const respondToChat = this.pollAndProcessChatResponses(key);
        const intervalId = setInterval(async () => await respondToChat(), CHECK_FOR_RESPONSE_INTERVAL);
        ws.once('close', () => {
            ws.off('message', onMessage);
            clearInterval(intervalId);
            this.sessions.delete(key);
        });
        ws.on('message', onMessage);
        const session = {
            connection: ws,
            executionId,
            sessionId,
            intervalId,
            isPublic: isPublic ?? false,
            isProcessing: false,
            lastHeartbeat: Date.now(),
        };
        this.sessions.set(key, session);
        ws.send(N8N_HEARTBEAT);
    }
    async processWaitingExecution(execution, session, sessionKey) {
        let message = (0, utils_1.getMessage)(execution);
        if (typeof message === 'object') {
            message = JSON.stringify(message);
        }
        if (message === undefined)
            return;
        session.connection.send(message);
        const lastNode = (0, utils_1.getLastNodeExecuted)(execution);
        if (lastNode && (0, utils_1.shouldResumeImmediately)(lastNode)) {
            session.connection.send(N8N_CONTINUE);
            const data = {
                action: 'sendMessage',
                chatInput: (0, utils_1.getLastNodeMessage)(execution, lastNode),
                sessionId: session.sessionId,
            };
            await this.resumeExecution(session.executionId, data, sessionKey);
            session.nodeWaitingForChatResponse = undefined;
        }
        else {
            session.nodeWaitingForChatResponse = lastNode?.name;
        }
    }
    processSuccessExecution(session) {
        closeConnection(session.connection);
        return;
    }
    waitForChatResponseOrContinue(execution, session) {
        const lastNode = (0, utils_1.getLastNodeExecuted)(execution);
        if (execution.status === 'waiting' && lastNode?.name !== session.nodeWaitingForChatResponse) {
            session.connection.send(N8N_CONTINUE);
            session.nodeWaitingForChatResponse = undefined;
        }
    }
    pollAndProcessChatResponses(sessionKey) {
        return async () => {
            const session = this.sessions.get(sessionKey);
            if (!session)
                return;
            if (session.isProcessing)
                return;
            try {
                session.isProcessing = true;
                if (!session.executionId || !session.connection)
                    return;
                const execution = await this.getExecutionOrCleanupSession(session.executionId, sessionKey);
                if (!execution)
                    return;
                if (execution.status === 'success') {
                    this.processSuccessExecution(session);
                    return;
                }
                if (session.nodeWaitingForChatResponse) {
                    this.waitForChatResponseOrContinue(execution, session);
                    return;
                }
                if (execution.status === 'waiting') {
                    await this.processWaitingExecution(execution, session, sessionKey);
                    return;
                }
            }
            catch (e) {
                const error = (0, n8n_workflow_1.ensureError)(e);
                this.errorReporter.error(error);
                this.logger.error(`Error sending message to chat in session ${sessionKey}: ${error.message}`);
            }
            finally {
                const activeSession = this.sessions.get(sessionKey);
                if (activeSession) {
                    activeSession.isProcessing = false;
                }
            }
        };
    }
    incomingMessageHandler(sessionKey) {
        return async (data) => {
            try {
                const session = this.sessions.get(sessionKey);
                if (!session)
                    return;
                const message = this.stringifyRawData(data);
                if (message === N8N_HEARTBEAT_ACK) {
                    session.lastHeartbeat = Date.now();
                    return;
                }
                const executionId = session.executionId;
                if (await this.shouldResumeOnMessage(executionId)) {
                    await this.resumeExecution(executionId, this.parseChatMessage(message), sessionKey);
                    session.nodeWaitingForChatResponse = undefined;
                }
            }
            catch (e) {
                const error = (0, n8n_workflow_1.ensureError)(e);
                this.errorReporter.error(error);
                this.logger.error(`Error processing message from chat in session ${sessionKey}: ${error.message}`);
            }
        };
    }
    async resumeExecution(executionId, message, sessionKey) {
        const execution = await this.getExecutionOrCleanupSession(executionId, sessionKey);
        if (!execution || execution.status !== 'waiting')
            return;
        await this.executionManager.runWorkflow(execution, message);
    }
    async getExecutionOrCleanupSession(executionId, sessionKey) {
        const execution = await this.executionManager.findExecution(executionId);
        const session = this.sessions.get(sessionKey);
        if (!execution || ['error', 'canceled', 'crashed'].includes(execution.status)) {
            if (!session)
                return null;
            this.cleanupSession(session, sessionKey);
            return null;
        }
        if (execution.status === 'running') {
            if (session?.nodeWaitingForChatResponse) {
                session.connection.send(N8N_CONTINUE);
                session.nodeWaitingForChatResponse = undefined;
            }
            return null;
        }
        return execution;
    }
    stringifyRawData(data) {
        const buffer = Array.isArray(data)
            ? Buffer.concat(data.map((chunk) => Buffer.from(chunk)))
            : data instanceof ArrayBuffer
                ? Buffer.from(data)
                : data;
        return buffer.toString('utf8');
    }
    cleanupSession(session, sessionKey) {
        session.connection.terminate();
        clearInterval(session.intervalId);
        if (sessionKey)
            this.sessions.delete(sessionKey);
    }
    parseChatMessage(message) {
        try {
            const parsedMessage = chat_service_types_1.chatMessageSchema.parse((0, n8n_workflow_1.jsonParse)(message));
            if (parsedMessage.files) {
                parsedMessage.files = parsedMessage.files.map((file) => ({
                    ...file,
                    data: file.data.includes('base64,') ? file.data.split('base64,')[1] : file.data,
                }));
            }
            return parsedMessage;
        }
        catch (error) {
            if (error instanceof zod_1.z.ZodError) {
                throw new n8n_workflow_1.UnexpectedError(`Chat message validation error: ${error.errors.map((error) => error.message).join(', ')}`);
            }
            throw error;
        }
    }
    async checkHeartbeats() {
        try {
            const now = Date.now();
            for (const [key, session] of this.sessions.entries()) {
                const timeSinceLastHeartbeat = now - (session.lastHeartbeat ?? 0);
                if (timeSinceLastHeartbeat > HEARTBEAT_TIMEOUT) {
                    await this.executionManager.cancelExecution(session.executionId);
                    this.cleanupSession(session, key);
                }
                else {
                    try {
                        session.connection.send(N8N_HEARTBEAT);
                    }
                    catch (e) {
                        this.cleanupSession(session, key);
                        const error = (0, n8n_workflow_1.ensureError)(e);
                        this.errorReporter.error(error);
                        this.logger.error(`Error sending heartbeat to session ${key}: ${error.message}`);
                    }
                }
            }
        }
        catch (e) {
            const error = (0, n8n_workflow_1.ensureError)(e);
            this.errorReporter.error(error);
            this.logger.error(`Error checking heartbeats: ${error.message}`);
        }
    }
    async shouldResumeOnMessage(executionId) {
        const execution = await this.executionManager.findExecution(executionId);
        if (!execution) {
            return true;
        }
        const lastNode = (0, utils_1.getLastNodeExecuted)(execution);
        const isChatNode = lastNode?.type === n8n_workflow_1.CHAT_NODE_TYPE || lastNode?.type === n8n_workflow_1.CHAT_TOOL_NODE_TYPE;
        if (isChatNode && lastNode?.parameters?.blockUserInput === true) {
            return false;
        }
        return true;
    }
    shutdown() {
        for (const [key, session] of this.sessions.entries()) {
            this.cleanupSession(session, key);
        }
        this.sessions.clear();
        clearInterval(this.heartbeatIntervalId);
    }
};
exports.ChatService = ChatService;
__decorate([
    (0, decorators_1.OnShutdown)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], ChatService.prototype, "shutdown", null);
exports.ChatService = ChatService = __decorate([
    (0, di_1.Service)(),
    __metadata("design:paramtypes", [chat_execution_manager_1.ChatExecutionManager,
        backend_common_1.Logger,
        n8n_core_1.ErrorReporter])
], ChatService);
//# sourceMappingURL=chat-service.js.map