import { encodeQueueDescriptor, decodeQueueDescriptor, } from "../shared/queue-descriptor";
import * as uuid from "uuid";
import { cron, embedTimezone, parseTimezonedCron } from "../../shared/repeat";
import { QueueRepo } from "./queue-repo";
import { fastifyDecoratorPlugin } from "./helper/fastify-decorator-plugin";
import * as config from "../../client/config";
function withoutWrappingSlashes(url) {
    return withoutLeadingSlash(withoutTrailingSlash(url));
}
function withoutTrailingSlash(url) {
    if (url.endsWith("/")) {
        return url.slice(0, url.length - 1);
    }
    return url;
}
function withoutLeadingSlash(url) {
    if (url.startsWith("/")) {
        return url.slice(1);
    }
    return url;
}
export class JobsRepo {
    constructor(owl, redis) {
        this.owl = owl;
        this.producer = this.owl.createProducer();
        this.queueRepo = new QueueRepo(redis, this);
    }
    static toJobDTO(job) {
        var _a, _b, _c;
        const { endpoint } = decodeQueueDescriptor(job.queue);
        let cron = {};
        if (((_a = job.schedule) === null || _a === void 0 ? void 0 : _a.type) === "cron") {
            const [cronExpression, cronTimezone] = parseTimezonedCron(job.schedule.meta);
            cron = { cron: cronExpression, cronTimezone };
        }
        return {
            id: job.id,
            endpoint,
            body: job.payload,
            runAt: job.runAt.toISOString(),
            exclusive: job.exclusive,
            retry: job.retry,
            count: job.count,
            repeat: job.schedule
                ? {
                    ...cron,
                    count: job.count,
                    every: ((_b = job.schedule) === null || _b === void 0 ? void 0 : _b.type) === "every" ? +job.schedule.meta : undefined,
                    times: (_c = job.schedule) === null || _c === void 0 ? void 0 : _c.times,
                }
                : undefined,
        };
    }
    async close() {
        await this.producer.close();
    }
    async find(byTokenId, endpoint, { count, cursor }) {
        const { newCursor, jobs } = await this.producer.scanQueue(encodeQueueDescriptor(byTokenId, endpoint), cursor, count);
        return {
            cursor: newCursor,
            jobs: jobs.map(JobsRepo.toJobDTO),
        };
    }
    async findAll({ count, cursor }) {
        const { newCursor, jobs } = await this.producer.scanQueuePattern(encodeQueueDescriptor("*", "*"), cursor, count);
        return {
            cursor: newCursor,
            jobs: jobs.map((job) => {
                const { tokenId } = decodeQueueDescriptor(job.queue);
                return {
                    ...JobsRepo.toJobDTO(job),
                    tokenId,
                };
            }),
        };
    }
    async findByTokenId(byTokenId, { count, cursor }) {
        const { newCursor, jobs } = await this.producer.scanQueuePattern(encodeQueueDescriptor(byTokenId, "*"), cursor, count);
        return {
            cursor: newCursor,
            jobs: jobs.map(JobsRepo.toJobDTO),
        };
    }
    async queueStatsByTokenId(byTokenId) {
        let cursor = 0;
        const counts = {};
        do {
            const { newCursor, jobs } = await this.producer.scanQueuePattern(encodeQueueDescriptor(byTokenId, "*"), cursor, 1000);
            cursor = newCursor;
            for (const job of jobs) {
                const { endpoint } = decodeQueueDescriptor(job.queue);
                counts[endpoint] = (counts[endpoint] || 0) + 1;
            }
        } while (cursor !== 0);
        return Object.fromEntries(Object.entries(counts).map(([endpoint, count]) => [endpoint, { count }]));
    }
    async findById(tokenId, endpoint, id) {
        const job = await this.producer.findById(encodeQueueDescriptor(tokenId, endpoint), id);
        return job ? JobsRepo.toJobDTO(job) : undefined;
    }
    async invoke(tokenId, endpoint, id) {
        return await this.producer.invoke(encodeQueueDescriptor(tokenId, endpoint), id);
    }
    async emptyByGetter(tokenId, getter) {
        let cursor = 0;
        const allPromises = [];
        do {
            const { cursor: newCursor, jobs } = await getter(cursor);
            cursor = newCursor;
            for (const job of jobs) {
                allPromises.push(this.delete(tokenId, job.endpoint, job.id));
            }
        } while (cursor !== 0);
        await Promise.all(allPromises);
    }
    async emptyQueue(tokenId, endpoint) {
        await this.emptyByGetter(tokenId, (cursor) => this.find(tokenId, endpoint, { cursor }));
    }
    async emptyToken(tokenId) {
        await this.emptyByGetter(tokenId, (cursor) => this.findByTokenId(tokenId, { cursor }));
    }
    async delete(tokenId, endpoint, id) {
        return await this.producer.delete(encodeQueueDescriptor(tokenId, endpoint), id);
    }
    async enqueue(tokenId, endpoint, { body, runAt: runAtOption, id, delay, repeat, override, exclusive, retry, }) {
        if (typeof id === "undefined") {
            id = uuid.v4();
        }
        let runAt = undefined;
        if (runAtOption) {
            runAt = new Date(runAtOption);
        }
        else if (delay) {
            runAt = new Date(Date.now() + delay);
        }
        if (typeof (repeat === null || repeat === void 0 ? void 0 : repeat.times) === "number" && repeat.times < 1) {
            throw new Error("repeat.times must be positive");
        }
        let schedule_type = undefined;
        let schedule_meta = undefined;
        if (repeat === null || repeat === void 0 ? void 0 : repeat.cron) {
            schedule_type = "cron";
            if (repeat === null || repeat === void 0 ? void 0 : repeat.cronTimezone) {
                schedule_meta = embedTimezone(repeat.cron, repeat.cronTimezone);
            }
            else {
                schedule_meta = repeat.cron;
            }
            runAt = cron(runAt !== null && runAt !== void 0 ? runAt : new Date(), schedule_meta);
        }
        if (repeat === null || repeat === void 0 ? void 0 : repeat.every) {
            schedule_type = "every";
            schedule_meta = "" + repeat.every;
        }
        const createdJob = await this.producer.enqueue({
            queue: encodeQueueDescriptor(tokenId, endpoint),
            id,
            payload: body !== null && body !== void 0 ? body : "",
            runAt,
            exclusive,
            schedule: schedule_type
                ? {
                    type: schedule_type,
                    meta: schedule_meta,
                    times: repeat === null || repeat === void 0 ? void 0 : repeat.times,
                }
                : undefined,
            override,
            retry,
        });
        await this.queueRepo.add(endpoint, tokenId);
        return JobsRepo.toJobDTO(createdJob);
    }
    async updateCron(tokenId, { baseUrl, crons, dryRun }) {
        const deleted = [];
        const queues = await this.queueRepo.get(tokenId);
        const queuesOnSameDeployment = queues.filter((q) => q.startsWith(baseUrl));
        if (!dryRun) {
            await Promise.all(crons.map(async ({ route, schedule, timezone }) => {
                await this.enqueue(tokenId, `${config.withoutTrailingSlash(baseUrl)}/${config.withoutLeadingSlash(route)}`, {
                    id: "@cron",
                    body: "null",
                    override: true,
                    repeat: { cron: schedule, cronTimezone: timezone },
                });
            }));
        }
        const routesThatShouldPersist = crons
            .map((c) => c.route)
            .map(withoutWrappingSlashes);
        await Promise.all(queuesOnSameDeployment.map(async (queue) => {
            const route = withoutWrappingSlashes(queue.slice(baseUrl.length));
            const shouldPersist = routesThatShouldPersist.includes(route);
            if (shouldPersist) {
                return;
            }
            if (dryRun) {
                const exists = await this.findById(tokenId, queue, "@cron");
                if (exists) {
                    deleted.push(queue);
                }
            }
            else {
                const result = await this.delete(tokenId, queue, "@cron");
                if (result === "deleted") {
                    deleted.push(queue);
                }
            }
        }));
        return { deleted };
    }
    onEvent(requesterTokenId, cb) {
        const activity = this.owl.createActivity(async (event) => {
            if (event.type === "scheduled") {
                cb("scheduled", JobsRepo.toJobDTO(event.job));
                return;
            }
            const { endpoint } = decodeQueueDescriptor(event.queue);
            switch (event.type) {
                case "acknowledged":
                    cb("completed", { endpoint, id: event.id });
                    break;
                case "requested":
                    cb("started", { endpoint, id: event.id });
                    break;
                case "invoked":
                    cb("invoked", { endpoint, id: event.id });
                    break;
                case "rescheduled":
                    cb("rescheduled", {
                        endpoint,
                        id: event.id,
                        runAt: event.runAt.toISOString(),
                    });
                    break;
                case "deleted":
                    cb("deleted", { endpoint, id: event.id });
                    break;
            }
        }, {
            queue: encodeQueueDescriptor(requesterTokenId, "*"),
        });
        return () => activity.close();
    }
}
export const jobsRepoPlugin = fastifyDecoratorPlugin("jobs", (fastify) => new JobsRepo(fastify.owl, fastify.redis));
//# sourceMappingURL=jobs-repo.js.map