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);
};
import { QueueDashboardSecret } from "../EnvironmentConfig";
import { Queue as BullQueue } from "bullmq";
import { ExpressAdapter } from "@bull-board/express";
import { createBullBoard } from "@bull-board/api";
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import logger from "../Utils/Logger";
import Telemetry from "../Utils/Telemetry";
import Redis from "./Redis";
export var QueueName;
(function (QueueName) {
    QueueName["Workflow"] = "Workflow";
    QueueName["Worker"] = "Worker";
    QueueName["Telemetry"] = "Telemetry";
    QueueName["Runbook"] = "Runbook";
})(QueueName || (QueueName = {}));
class Queue {
    // BullMQ rejects custom IDs containing colons, so normalize them early.
    static sanitizeJobId(jobId) {
        return jobId.replace(/:/g, "-");
    }
    static async setupReconnectListener(queue, queueName) {
        const client = await queue.client;
        client.on("ready", async () => {
            logger.debug(`Queue ${queueName} reconnected, re-adding repeatable jobs`);
            const jobs = Queue.repeatableJobs[queueName];
            if (jobs) {
                for (const jobId in jobs) {
                    const job = jobs[jobId];
                    if (job) {
                        try {
                            logger.debug(`Re-adding repeatable job ${job.jobName} to queue ${queueName}`);
                            await queue.add(job.jobName, job.data, job.options);
                        }
                        catch (err) {
                            logger.error("Error re-adding repeatable job");
                            logger.error(err);
                        }
                    }
                }
            }
        });
    }
    static getQueue(queueName) {
        // check if the queue is already created
        if (this.queueDict[queueName]) {
            return this.queueDict[queueName];
        }
        const queue = new BullQueue(queueName, {
            connection: Redis.getRedisOptions(),
            // Keep BullMQ data under control to avoid Redis bloat
            defaultJobOptions: {
                // keep only recent completed/failed jobs
                removeOnComplete: { count: 500 }, // keep last 1000 completed jobs
                removeOnFail: { count: 100 }, // keep last 500 failed jobs
            },
            /*
             * Optionally cap the event stream length (supported in BullMQ >= v5)
             * This helps prevent the :events stream from growing indefinitely
             */
            streams: {
                events: { maxLen: 1000 },
            },
        });
        // save it to the dictionary
        this.queueDict[queueName] = queue;
        // Register the observable gauge once any queue exists in this process.
        this.registerQueueSizeMetric();
        // Add event listener to re-add repeatable jobs on reconnect
        this.setupReconnectListener(queue, queueName).catch((err) => {
            logger.error("Error setting up reconnect listener for queue");
            logger.error(err);
        });
        // Fire-and-forget initial cleanup for legacy/old data if not done before
        if (!this.cleanedQueueNames.has(queueName)) {
            this.cleanedQueueNames.add(queueName);
            // Clean jobs older than 1 days to reclaim memory from historic runs
            const oneDaysMs = 1 * 24 * 60 * 60 * 1000;
            void (async () => {
                try {
                    await queue.clean(oneDaysMs, 1000, "completed");
                    await queue.clean(oneDaysMs, 1000, "failed");
                }
                catch (_a) {
                    // ignore cleanup errors to not impact normal flow
                }
            })();
        }
        return queue;
    }
    static async removeJob(queueName, jobId) {
        if (!jobId) {
            return;
        }
        const sanitizedJobId = this.sanitizeJobId(jobId.toString());
        const job = await this.getQueue(queueName).getJob(sanitizedJobId);
        if (job) {
            await job.remove();
        }
        // remove existing repeatable job
        await this.getQueue(queueName).removeRepeatableByKey(sanitizedJobId);
    }
    static getInspectorRoute() {
        return "/worker/inspect/queue/:dashboardSecret";
    }
    static getQueueInspectorRouter() {
        const serverAdapter = new ExpressAdapter();
        const queueAdapters = Object.values(QueueName).map((queueName) => {
            return new BullMQAdapter(this.getQueue(queueName));
        });
        createBullBoard({
            // Cast keeps compatibility until bull-board widens QueueJob.progress
            queues: queueAdapters,
            serverAdapter: serverAdapter,
        });
        serverAdapter.setBasePath(this.getInspectorRoute().replace("/:dashboardSecret", "/" + QueueDashboardSecret));
        return serverAdapter.getRouter();
    }
    static async addJob(queueName, jobId, jobName, data, options) {
        var _a, _b;
        const sanitizedJobId = this.sanitizeJobId(jobId.toString());
        const optionsObject = {
            jobId: sanitizedJobId,
        };
        if (options && options.delayInMs && options.delayInMs > 0) {
            optionsObject.delay = options.delayInMs;
        }
        const attempts = (_a = options === null || options === void 0 ? void 0 : options.attempts) !== null && _a !== void 0 ? _a : (queueName === QueueName.Telemetry ? 3 : 1);
        if (attempts > 1) {
            optionsObject.attempts = attempts;
            optionsObject.backoff = {
                type: "exponential",
                delay: (_b = options === null || options === void 0 ? void 0 : options.backoffDelayInMs) !== null && _b !== void 0 ? _b : 5000,
            };
        }
        const queue = this.getQueue(queueName);
        if (options && options.scheduleAt) {
            optionsObject.repeat = {
                pattern: options.scheduleAt,
                // keep repeatable job keyed by jobId so multiple workers do not register duplicates
                jobId: sanitizedJobId,
            };
            const repeatableJobs = await queue.getRepeatableJobs();
            for (const repeatableJob of repeatableJobs) {
                const isSameJob = repeatableJob.name === jobName &&
                    repeatableJob.pattern === options.scheduleAt;
                if (isSameJob) {
                    await queue.removeRepeatableByKey(repeatableJob.key);
                }
            }
        }
        if (!(options === null || options === void 0 ? void 0 : options.skipExistenceCheck)) {
            const job = await queue.getJob(sanitizedJobId);
            if (job) {
                await job.remove();
            }
        }
        if (options === null || options === void 0 ? void 0 : options.repeatableKey) {
            // remove existing repeatable job
            await queue.removeRepeatableByKey(options === null || options === void 0 ? void 0 : options.repeatableKey);
        }
        // Store repeatable jobs for re-adding on reconnect
        if (options && options.scheduleAt) {
            if (!this.repeatableJobs[queueName]) {
                this.repeatableJobs[queueName] = {};
            }
            this.repeatableJobs[queueName][sanitizedJobId] = {
                jobName,
                data,
                options: optionsObject,
            };
        }
        const jobAdded = await queue.add(jobName, data, optionsObject);
        return jobAdded;
    }
    static registerQueueSizeMetric() {
        if (this.queueSizeMetricRegistered) {
            return;
        }
        if (!Telemetry.isMetricsEnabled()) {
            return;
        }
        try {
            Telemetry.getObservableGauge({
                name: "queue.size",
                description: "Number of BullMQ jobs in each queue, partitioned by job state.",
                unit: "1",
                callback: async (result) => {
                    for (const queueName of Object.keys(this.queueDict)) {
                        try {
                            const stats = await this.getQueueStats(queueName);
                            const baseAttrs = {
                                "messaging.system": "bullmq",
                                "messaging.destination.name": queueName,
                            };
                            result.observe(stats.waiting, Object.assign(Object.assign({}, baseAttrs), { state: "waiting" }));
                            result.observe(stats.active, Object.assign(Object.assign({}, baseAttrs), { state: "active" }));
                            result.observe(stats.delayed, Object.assign(Object.assign({}, baseAttrs), { state: "delayed" }));
                            result.observe(stats.failed, Object.assign(Object.assign({}, baseAttrs), { state: "failed" }));
                        }
                        catch (err) {
                            // Don't let one queue's stat failure break others.
                            logger.debug("Failed to read queue stats");
                            logger.debug(err);
                        }
                    }
                },
            });
            this.queueSizeMetricRegistered = true;
        }
        catch (err) {
            logger.error("Failed to register queue.size metric");
            logger.error(err);
        }
    }
    static async getQueueSize(queueName) {
        const queue = this.getQueue(queueName);
        const waitingCount = await queue.getWaitingCount();
        const activeCount = await queue.getActiveCount();
        const delayedCount = await queue.getDelayedCount();
        return waitingCount + activeCount + delayedCount;
    }
    static async getQueueStats(queueName) {
        const queue = this.getQueue(queueName);
        const waitingCount = await queue.getWaitingCount();
        const activeCount = await queue.getActiveCount();
        const completedCount = await queue.getCompletedCount();
        const failedCount = await queue.getFailedCount();
        const delayedCount = await queue.getDelayedCount();
        return {
            waiting: waitingCount,
            active: activeCount,
            completed: completedCount,
            failed: failedCount,
            delayed: delayedCount,
            total: waitingCount +
                activeCount +
                completedCount +
                failedCount +
                delayedCount,
        };
    }
    static async getFailedJobs(queueName, options) {
        const queue = this.getQueue(queueName);
        const start = (options === null || options === void 0 ? void 0 : options.start) || 0;
        const end = (options === null || options === void 0 ? void 0 : options.end) || 100;
        const failed = await queue.getFailed(start, end);
        return failed.map((job) => {
            const result = {
                id: job.id || "unknown",
                name: job.name || "unknown",
                data: job.data,
                failedReason: job.failedReason || "No reason provided",
                processedOn: job.processedOn ? new Date(job.processedOn) : null,
                finishedOn: job.finishedOn ? new Date(job.finishedOn) : null,
                attemptsMade: job.attemptsMade || 0,
            };
            if (job.stacktrace && job.stacktrace.length > 0) {
                result.stackTrace = job.stacktrace.join("\n");
            }
            return result;
        });
    }
}
Queue.queueDict = {};
// track queues we have already run initial cleanup on
Queue.cleanedQueueNames = new Set();
Queue.queueSizeMetricRegistered = false;
// store repeatable jobs to re-add on reconnect
Queue.repeatableJobs = {};
export default Queue;
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", BullQueue)
], Queue, "getQueue", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", Promise)
], Queue, "removeJob", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", String)
], Queue, "getInspectorRoute", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Function)
], Queue, "getQueueInspectorRouter", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String, String, Object, Object]),
    __metadata("design:returntype", Promise)
], Queue, "addJob", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", Promise)
], Queue, "getQueueSize", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", Promise)
], Queue, "getQueueStats", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, Object]),
    __metadata("design:returntype", Promise)
], Queue, "getFailedJobs", null);
//# sourceMappingURL=Queue.js.map