"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.InstanceAiMemoryService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const di_1 = require("@n8n/di");
const instance_ai_1 = require("@n8n/instance-ai");
const db_snapshot_storage_1 = require("./storage/db-snapshot-storage");
const not_found_error_1 = require("../../errors/response-errors/not-found.error");
const message_parser_1 = require("./message-parser");
const typeorm_composite_store_1 = require("./storage/typeorm-composite-store");
let InstanceAiMemoryService = class InstanceAiMemoryService {
    constructor(logger, globalConfig, compositeStore, dbSnapshotStorage) {
        this.logger = logger;
        this.compositeStore = compositeStore;
        this.dbSnapshotStorage = dbSnapshotStorage;
        this.instanceAiConfig = globalConfig.instanceAi;
    }
    async listThreads(userId, page = 0, perPage = 100) {
        const memory = this.createMemoryInstance();
        const result = await memory.listThreads({
            filter: { resourceId: userId },
            perPage,
            page,
            orderBy: { field: 'updatedAt', direction: 'DESC' },
        });
        return {
            threads: result.threads.map((t) => this.toThreadInfo(t)),
            total: result.total,
            page: result.page,
            hasMore: result.hasMore,
        };
    }
    async ensureThread(userId, threadId) {
        const memory = this.createMemoryInstance();
        const existing = await memory.getThreadById({ threadId });
        if (existing) {
            if (existing.resourceId !== userId) {
                throw new Error(`Thread ${threadId} is not owned by user ${userId}`);
            }
            return {
                thread: this.toThreadInfo(existing),
                created: false,
            };
        }
        const now = new Date();
        const created = await memory.saveThread({
            thread: {
                id: threadId,
                resourceId: userId,
                title: '',
                createdAt: now,
                updatedAt: now,
            },
        });
        return {
            thread: this.toThreadInfo(created),
            created: true,
        };
    }
    async getThreadMessages(userId, threadId, options) {
        const memory = this.createMemoryInstance();
        let result;
        try {
            result = await memory.recall({
                threadId,
                resourceId: userId,
                perPage: options?.limit ?? 50,
                page: options?.page ?? 0,
            });
        }
        catch (error) {
            if (error instanceof Error && error.message.includes('No thread found')) {
                return { threadId, messages: [] };
            }
            throw error;
        }
        return {
            threadId,
            messages: result.messages.map((m) => ({
                id: m.id,
                role: m.role,
                content: typeof m.content === 'string' ? m.content : m.content,
                type: m.type,
                createdAt: m.createdAt.toISOString(),
            })),
        };
    }
    async getRichMessages(userId, threadId, options) {
        const memory = this.createMemoryInstance();
        let result;
        try {
            result = await memory.recall({
                threadId,
                resourceId: userId,
                perPage: options?.limit ?? 50,
                page: options?.page ?? 0,
            });
        }
        catch (error) {
            if (error instanceof Error && error.message.includes('No thread found')) {
                return { threadId, messages: [] };
            }
            throw error;
        }
        let snapshots = await this.dbSnapshotStorage.getAll(threadId).catch((error) => {
            this.logger.warn('Failed to load agent tree snapshots', {
                threadId,
                error: error instanceof Error ? error.message : String(error),
            });
            return [];
        });
        if (options?.excludeRunIds?.length) {
            const excluded = new Set(options.excludeRunIds);
            snapshots = snapshots.filter((s) => !excluded.has(s.runId));
        }
        const mastraMessages = result.messages.map((m) => ({
            id: m.id,
            role: m.role,
            content: m.content,
            type: m.type,
            createdAt: m.createdAt,
        }));
        const messages = (0, message_parser_1.parseStoredMessages)(mastraMessages, snapshots);
        return { threadId, messages };
    }
    async getLatestRunSnapshot(threadId, options) {
        return await this.dbSnapshotStorage.getLatest(threadId, options);
    }
    async validateThreadOwnership(userId, threadId) {
        return (await this.checkThreadOwnership(userId, threadId)) === 'owned';
    }
    async checkThreadOwnership(userId, threadId) {
        const memory = this.createMemoryInstance();
        const thread = await memory.getThreadById({ threadId });
        if (!thread)
            return 'not_found';
        return thread.resourceId === userId ? 'owned' : 'other_user';
    }
    async deleteThread(threadId) {
        const memory = this.createMemoryInstance();
        await memory.deleteThread(threadId);
    }
    async renameThread(threadId, title) {
        return await this.updateThread(threadId, { title });
    }
    async updateThread(threadId, updates) {
        const memory = this.createMemoryInstance();
        const updated = await (0, instance_ai_1.patchThread)(memory, {
            threadId,
            update: ({ metadata }) => {
                const patch = {
                    metadata: { ...metadata, ...updates.metadata },
                };
                if (updates.title !== undefined) {
                    patch.title = updates.title;
                    patch.metadata.titleRefined = true;
                }
                return patch;
            },
        });
        if (!updated) {
            throw new not_found_error_1.NotFoundError(`Thread ${threadId} not found`);
        }
        return this.toThreadInfo(updated);
    }
    async getThreadMetadata(userId, threadId) {
        const memory = this.createMemoryInstance();
        const thread = await memory.getThreadById({ threadId });
        if (!thread || thread.resourceId !== userId)
            return undefined;
        return thread.metadata;
    }
    async cleanupExpiredThreads(onThreadDeleted) {
        const ttlDays = this.instanceAiConfig.threadTtlDays;
        if (!ttlDays || ttlDays <= 0)
            return 0;
        const memory = this.createMemoryInstance();
        const cutoff = new Date(Date.now() - ttlDays * 24 * 60 * 60 * 1000);
        let deletedCount = 0;
        const perPage = 100;
        let hasMore = true;
        while (hasMore) {
            const result = await memory.listThreads({ perPage, page: 0 });
            let deletedInPage = 0;
            for (const thread of result.threads) {
                if (thread.updatedAt < cutoff) {
                    try {
                        await onThreadDeleted?.(thread.id);
                        await memory.deleteThread(thread.id);
                        deletedCount++;
                        deletedInPage++;
                    }
                    catch (error) {
                        this.logger.warn('Failed to delete expired thread', {
                            threadId: thread.id,
                            error: error instanceof Error ? error.message : String(error),
                        });
                    }
                }
            }
            hasMore = deletedInPage > 0 && result.hasMore;
        }
        if (deletedCount > 0) {
            this.logger.info(`Cleaned up ${deletedCount} expired conversation threads (TTL: ${ttlDays} days)`);
        }
        return deletedCount;
    }
    createMemoryInstance() {
        return (0, instance_ai_1.createMemory)({
            storage: this.compositeStore,
            embedderModel: this.instanceAiConfig.embedderModel || undefined,
            lastMessages: this.instanceAiConfig.lastMessages,
            semanticRecallTopK: this.instanceAiConfig.semanticRecallTopK,
        });
    }
    toThreadInfo(thread) {
        return {
            id: thread.id,
            title: thread.title,
            resourceId: thread.resourceId,
            createdAt: thread.createdAt.toISOString(),
            updatedAt: thread.updatedAt.toISOString(),
            metadata: thread.metadata,
        };
    }
};
exports.InstanceAiMemoryService = InstanceAiMemoryService;
exports.InstanceAiMemoryService = InstanceAiMemoryService = __decorate([
    (0, di_1.Service)(),
    __metadata("design:paramtypes", [backend_common_1.Logger,
        config_1.GlobalConfig,
        typeorm_composite_store_1.TypeORMCompositeStore,
        db_snapshot_storage_1.DbSnapshotStorage])
], InstanceAiMemoryService);
//# sourceMappingURL=instance-ai-memory.service.js.map