import { QuotaExceededError } from "./quota.js";
export class StudioJobQueue {
    runner;
    options;
    events;
    jobs = new Map();
    bookConfigs = new Map();
    queue = [];
    controllers = new Map();
    constructor(runner, options, events = {}) {
        this.runner = runner;
        this.options = options;
        this.events = events;
    }
    enqueue(input) {
        this.assertUserLimits(input.userId);
        const now = new Date().toISOString();
        const job = {
            id: `job_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`,
            userId: input.userId,
            projectRoot: input.projectRoot,
            type: "full_book_generation",
            status: "queued",
            bookId: input.bookId,
            title: input.title,
            targetChapters: input.targetChapters,
            chapterWordCount: input.chapterWordCount,
            chaptersWritten: 0,
            createdAt: now,
            updatedAt: now,
        };
        this.jobs.set(job.id, job);
        if (input.bookConfig)
            this.bookConfigs.set(job.id, input.bookConfig);
        this.queue.push(job.id);
        this.emit("job:queued", this.withQueuePosition(job));
        this.pump();
        return this.withQueuePosition(job);
    }
    list(userId) {
        return [...this.jobs.values()]
            .filter((job) => !userId || job.userId === userId)
            .map((job) => this.withQueuePosition(job))
            .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    }
    get(id) {
        const job = this.jobs.get(id);
        return job ? this.withQueuePosition(job) : undefined;
    }
    getBookConfig(id) {
        return this.bookConfigs.get(id);
    }
    cancel(id) {
        const job = this.requireJob(id);
        if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
            throw new Error("Cannot cancel a finished job.");
        }
        const now = new Date().toISOString();
        const updated = this.update(job.id, {
            status: "cancelled",
            cancelledAt: now,
            completedAt: now,
        });
        const queuedIndex = this.queue.indexOf(id);
        if (queuedIndex >= 0)
            this.queue.splice(queuedIndex, 1);
        this.controllers.get(id)?.abort();
        this.emit("job:cancelled", updated);
        this.pump();
        return updated;
    }
    resume(id) {
        const job = this.requireJob(id);
        if (job.status !== "paused_quota" && job.status !== "failed") {
            throw new Error("Only paused or failed jobs can be resumed.");
        }
        const updated = this.update(id, { status: "queued", error: undefined });
        this.queue.push(id);
        this.emit("job:queued", this.withQueuePosition(updated));
        this.pump();
        return this.withQueuePosition(updated);
    }
    pump() {
        const active = this.activeJobs().length;
        const slots = Math.max(0, this.options.concurrency - active);
        for (let i = 0; i < slots; i += 1) {
            const nextId = this.dequeueStartableJob();
            if (!nextId)
                return;
            void this.start(nextId);
        }
    }
    resumePausedQuotaJobs() {
        for (const job of this.jobs.values()) {
            if (job.status !== "paused_quota")
                continue;
            const updated = this.update(job.id, { status: "queued", error: undefined });
            if (!this.queue.includes(job.id))
                this.queue.push(job.id);
            this.emit("job:queued", updated);
        }
        this.pump();
    }
    async start(id) {
        const controller = new AbortController();
        this.controllers.set(id, controller);
        const started = this.update(id, {
            status: "running",
            startedAt: new Date().toISOString(),
        });
        this.emit("job:started", started);
        try {
            await this.runner(started, controller.signal, (chaptersWritten) => {
                const progress = this.update(id, { chaptersWritten });
                this.emit("job:progress", progress);
            });
            const current = this.requireJob(id);
            if (current.status === "cancelled")
                return;
            const completed = this.update(id, {
                status: "completed",
                completedAt: new Date().toISOString(),
            });
            this.emit("job:completed", completed);
        }
        catch (error) {
            const current = this.requireJob(id);
            if (current.status === "cancelled" || controller.signal.aborted) {
                const cancelled = this.update(id, {
                    status: "cancelled",
                    cancelledAt: new Date().toISOString(),
                });
                this.emit("job:cancelled", cancelled);
            }
            else if (error instanceof QuotaExceededError) {
                const paused = this.update(id, {
                    status: "paused_quota",
                    error: error.message,
                });
                this.emit("job:paused", paused);
            }
            else {
                const failed = this.update(id, {
                    status: "failed",
                    error: error instanceof Error ? error.message : String(error),
                    completedAt: new Date().toISOString(),
                });
                this.emit("job:failed", failed);
            }
        }
        finally {
            this.controllers.delete(id);
            this.pump();
        }
    }
    dequeueStartableJob() {
        for (let index = 0; index < this.queue.length; index += 1) {
            const id = this.queue[index];
            const job = this.jobs.get(id);
            if (!job || job.status !== "queued") {
                this.queue.splice(index, 1);
                index -= 1;
                continue;
            }
            if (this.activeJobs(job.userId).length >= this.options.perUserActiveLimit)
                continue;
            this.queue.splice(index, 1);
            return id;
        }
        return undefined;
    }
    activeJobs(userId) {
        return [...this.jobs.values()].filter((job) => job.status === "running" && (!userId || job.userId === userId));
    }
    assertUserLimits(userId) {
        const active = this.activeJobs(userId).length;
        const queued = [...this.jobs.values()].filter((job) => job.userId === userId && job.status === "queued").length;
        if (active >= this.options.perUserActiveLimit && queued >= this.options.perUserQueuedLimit) {
            throw new Error("User job limit exceeded.");
        }
        if (queued >= this.options.perUserQueuedLimit) {
            throw new Error("User queued job limit exceeded.");
        }
    }
    requireJob(id) {
        const job = this.jobs.get(id);
        if (!job)
            throw new Error("Job not found.");
        return job;
    }
    update(id, patch) {
        const current = this.requireJob(id);
        const updated = {
            ...current,
            ...patch,
            updatedAt: new Date().toISOString(),
        };
        this.jobs.set(id, updated);
        return this.withQueuePosition(updated);
    }
    withQueuePosition(job) {
        if (job.status !== "queued") {
            const { queuePosition: _queuePosition, ...rest } = job;
            return rest;
        }
        const position = this.queue.indexOf(job.id);
        return {
            ...job,
            queuePosition: position >= 0 ? position + 1 : undefined,
        };
    }
    emit(event, job) {
        this.events.onEvent?.(event, this.withQueuePosition(job));
    }
}
//# sourceMappingURL=job-queue.js.map