/**
 * RedisTaskStore — Redis-backed persistence for TaskManager.
 * Used automatically when backend is "bullmq".
 *
 * Key patterns:
 *   neurolink:tasks              (Hash)  — all task definitions
 *   neurolink:task:{id}:runs     (List)  — run log entries (newest first)
 *   neurolink:task:{id}:history  (List)  — continuation mode conversation history
 */
import { createClient } from "redis";
import { logger } from "../../utils/logger.js";
import { TaskError } from "../errors.js";
import { TASK_DEFAULTS, } from "../../types/taskTypes.js";
const KEY_PREFIX = "neurolink:";
const TASKS_HASH = `${KEY_PREFIX}tasks`;
function taskRunsKey(taskId) {
    return `${KEY_PREFIX}task:${taskId}:runs`;
}
function taskHistoryKey(taskId) {
    return `${KEY_PREFIX}task:${taskId}:history`;
}
export class RedisTaskStore {
    config;
    type = "redis";
    client = null;
    maxRunLogs;
    maxHistoryEntries;
    retentionConfig;
    constructor(config) {
        this.config = config;
        this.maxRunLogs = config.maxRunLogs ?? TASK_DEFAULTS.maxRunLogs;
        this.maxHistoryEntries =
            config.maxHistoryEntries ?? TASK_DEFAULTS.maxHistoryEntries;
        this.retentionConfig = {
            ...TASK_DEFAULTS.retention,
            ...config.taskRetention,
        };
    }
    async initialize() {
        const redis = this.config.redis ?? {};
        const url = redis.url ??
            `redis://${redis.host ?? TASK_DEFAULTS.redis.host}:${redis.port ?? TASK_DEFAULTS.redis.port}/${redis.db ?? 0}`;
        this.client = createClient({
            url,
            ...(redis.password ? { password: redis.password } : {}),
        });
        this.client.on("error", (err) => {
            logger.error("[TaskStore:Redis] Connection error", {
                error: String(err),
            });
        });
        await this.client.connect();
        logger.info("[TaskStore:Redis] Connected");
    }
    async shutdown() {
        if (this.client?.isOpen) {
            await this.client.quit();
            logger.info("[TaskStore:Redis] Disconnected");
        }
        this.client = null;
    }
    // ── Task CRUD ───────────────────────────────────────────
    async save(task) {
        const client = this.getClient();
        await client.hSet(TASKS_HASH, task.id, JSON.stringify(task));
        this.applyRetentionTTL(task, client);
    }
    async get(taskId) {
        const client = this.getClient();
        const data = await client.hGet(TASKS_HASH, taskId);
        if (!data) {
            return null;
        }
        return JSON.parse(String(data));
    }
    async list(filter) {
        const client = this.getClient();
        const all = await client.hGetAll(TASKS_HASH);
        let tasks = Object.values(all).map((v) => JSON.parse(String(v)));
        if (filter?.status) {
            tasks = tasks.filter((t) => t.status === filter.status);
        }
        return tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
    }
    async update(taskId, updates) {
        const client = this.getClient();
        const existing = await this.get(taskId);
        if (!existing) {
            throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
        }
        const updated = {
            ...existing,
            ...updates,
            id: existing.id, // ID is immutable
            updatedAt: new Date().toISOString(),
        };
        await client.hSet(TASKS_HASH, taskId, JSON.stringify(updated));
        this.applyRetentionTTL(updated, client);
        return updated;
    }
    async delete(taskId) {
        const client = this.getClient();
        await Promise.all([
            client.hDel(TASKS_HASH, taskId),
            client.del(taskRunsKey(taskId)),
            client.del(taskHistoryKey(taskId)),
        ]);
    }
    // ── Run Logs ──────────────────────────────────────────
    async appendRun(taskId, run) {
        const client = this.getClient();
        const key = taskRunsKey(taskId);
        await client.lPush(key, JSON.stringify(run));
        // Trim to keep only the latest maxRunLogs entries
        await client.lTrim(key, 0, this.maxRunLogs - 1);
    }
    async getRuns(taskId, options) {
        const client = this.getClient();
        const limit = options?.limit ?? 20;
        const key = taskRunsKey(taskId);
        // When a status filter is applied, we need to fetch more items than `limit`
        // because post-filter may discard many entries. Fetch all (-1) when filtering,
        // otherwise fetch exactly `limit` items.
        const fetchEnd = options?.status ? -1 : limit - 1;
        const items = await client.lRange(key, 0, fetchEnd);
        let runs = items.map((v) => JSON.parse(String(v)));
        if (options?.status) {
            runs = runs.filter((r) => r.status === options.status);
        }
        return runs.slice(0, limit);
    }
    // ── Continuation History ──────────────────────────────
    async appendHistory(taskId, messages) {
        const client = this.getClient();
        const key = taskHistoryKey(taskId);
        const serialized = messages.map((m) => JSON.stringify(m));
        if (serialized.length > 0) {
            await client.rPush(key, serialized);
            // Trim to keep only the most recent entries, preventing unbounded growth
            await client.lTrim(key, -this.maxHistoryEntries, -1);
        }
    }
    async getHistory(taskId) {
        const client = this.getClient();
        const key = taskHistoryKey(taskId);
        const items = await client.lRange(key, 0, -1);
        return items.map((v) => JSON.parse(String(v)));
    }
    async clearHistory(taskId) {
        const client = this.getClient();
        await client.del(taskHistoryKey(taskId));
    }
    // ── Internal ──────────────────────────────────────────
    ensureConnected() {
        if (!this.client?.isOpen) {
            throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskStore:Redis] Not connected. Call initialize() first.");
        }
    }
    getClient() {
        this.ensureConnected();
        if (!this.client) {
            throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskStore:Redis] Client is unavailable after initialization.");
        }
        return this.client;
    }
    /**
     * Set Redis TTL on terminal-state tasks so they auto-expire.
     * Active and paused tasks never expire.
     */
    applyRetentionTTL(task, client) {
        // We don't set EXPIRE on the hash field directly (Redis doesn't support per-field TTL).
        // Instead, run logs and history keys get TTL. The task hash field itself must be
        // cleaned up via manual deletion or BullMQ's built-in job cleanup.
        const ttlMap = {
            completed: this.retentionConfig.completedTTL,
            failed: this.retentionConfig.failedTTL,
            cancelled: this.retentionConfig.cancelledTTL,
        };
        const ttlMs = ttlMap[task.status];
        if (!ttlMs || !client.isOpen) {
            return;
        }
        const ttlSeconds = Math.ceil(ttlMs / 1000);
        // Set TTL on associated keys best-effort. A successful task write should not
        // be surfaced as a failure just because the retention metadata could not be updated.
        void (async () => {
            const runsKey = taskRunsKey(task.id);
            for (let attempt = 1; attempt <= 3; attempt++) {
                try {
                    await client.expire(runsKey, ttlSeconds);
                    break;
                }
                catch (err) {
                    if (attempt === 3) {
                        logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task runs key — task data may outlive retention window", { taskId: task.id, key: runsKey, ttlSeconds, err: String(err) });
                    }
                    else {
                        await new Promise((r) => setTimeout(r, 100 * attempt));
                    }
                }
            }
        })();
        void (async () => {
            const histKey = taskHistoryKey(task.id);
            for (let attempt = 1; attempt <= 3; attempt++) {
                try {
                    await client.expire(histKey, ttlSeconds);
                    break;
                }
                catch (err) {
                    if (attempt === 3) {
                        logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task history key — task data may outlive retention window", { taskId: task.id, key: histKey, ttlSeconds, err: String(err) });
                    }
                    else {
                        await new Promise((r) => setTimeout(r, 100 * attempt));
                    }
                }
            }
        })();
    }
}
//# sourceMappingURL=redisTaskStore.js.map