import { randomUUID } from "node:crypto";
import { getErrorMessage } from "../error-utils.js";
export class SubagentBackgroundJobManager {
	jobs = new Map();
	evictionTimers = new Map();
	maxRunning;
	maxTotal;
	evictionMs;
	onJobComplete;
	constructor(options = {}) {
		this.maxRunning = options.maxRunning ?? 8;
		this.maxTotal = options.maxTotal ?? 50;
		this.evictionMs = options.evictionMs ?? 10 * 60 * 1000;
		this.onJobComplete = options.onJobComplete;
	}
	register(label, runFn, dispatchContext = null) {
		const running = this.getRunningJobs();
		if (running.length >= this.maxRunning) {
			throw new Error(
				`Maximum concurrent background subagent jobs reached (${this.maxRunning}). ` +
					`Use await_subagent or cancel_subagent to free a slot.`,
			);
		}
		if (this.jobs.size >= this.maxTotal) {
			this.evictOldest();
			if (this.jobs.size >= this.maxTotal) {
				throw new Error(
					`Maximum total background subagent jobs reached (${this.maxTotal}). ` +
						`Use cancel_subagent to remove jobs.`,
				);
			}
		}
		const id = `sub_${randomUUID().slice(0, 8)}`;
		const abortController = new AbortController();
		const job = {
			id,
			status: "running",
			startTime: Date.now(),
			label,
			abortController,
			promise: undefined,
			turns: [],
			dispatchContext,
		};
		job.promise = this._attachRun(job, runFn);
		this.jobs.set(id, job);
		return id;
	}
	/** @internal Attaches a runFn to a job and wires up completion/failure. */
	_attachRun(job, runFn) {
		return runFn(job.abortController.signal)
			.then((result) => {
				job.result = result;
				job.status = result.isError ? "failed" : "completed";
				this.scheduleEviction(job.id);
				this.deliverResult(job);
			})
			.catch((err) => {
				if (job.status === "cancelled") {
					this.scheduleEviction(job.id);
					return;
				}
				job.status = "failed";
				job.errorText = getErrorMessage(err);
				this.scheduleEviction(job.id);
				this.deliverResult(job);
			});
	}
	/**
	 * Resume a completed (or failed) job with a new runFn.
	 * Used by write_subagent for multi-turn conversations.
	 * Returns "resumed" | "not_found" | "already_running" | "concurrency_limit"
	 */
	resume(id, runFn) {
		const job = this.jobs.get(id);
		if (!job) return "not_found";
		if (job.status === "running") return "already_running";
		if (this.getRunningJobs().length >= this.maxRunning)
			return "concurrency_limit";
		// Cancel pending eviction — job is active again
		const evTimer = this.evictionTimers.get(id);
		if (evTimer) {
			clearTimeout(evTimer);
			this.evictionTimers.delete(id);
		}
		// Cancel pending delivery notification (avoids stale follow-up firing mid-run)
		if (job.deliveryTimer !== undefined) {
			clearTimeout(job.deliveryTimer);
			job.deliveryTimer = undefined;
		}
		// Fresh AbortController — previous one may be spent/aborted
		job.abortController = new AbortController();
		job.status = "running";
		job.awaited = false;
		job.result = undefined;
		job.errorText = undefined;
		job.promise = this._attachRun(job, runFn);
		return "resumed";
	}
	/** Append a conversation turn (role: "agent" | "user") to the job's turn history. */
	appendTurn(id, role, content) {
		const job = this.jobs.get(id);
		if (!job) return;
		job.turns.push({
			turnIndex: job.turns.length,
			role,
			content,
			timestamp: Date.now(),
		});
	}
	/** Return all turns for a job, optionally filtered to those after `since` (exclusive). */
	getTurns(id, since = 0) {
		const job = this.jobs.get(id);
		if (!job) return null;
		return job.turns.slice(since);
	}
	cancel(id) {
		const job = this.jobs.get(id);
		if (!job) return "not_found";
		if (job.status !== "running") return "already_completed";
		job.status = "cancelled";
		job.errorText = "Cancelled by user";
		job.abortController.abort();
		this.scheduleEviction(id);
		return "cancelled";
	}
	getJob(id) {
		return this.jobs.get(id);
	}
	getRunningJobs() {
		return [...this.jobs.values()].filter((job) => job.status === "running");
	}
	getRecentJobs(limit = 10) {
		return [...this.jobs.values()]
			.sort((a, b) => b.startTime - a.startTime)
			.slice(0, limit);
	}
	suppressFollowUp(id) {
		const job = this.jobs.get(id);
		if (!job) return;
		job.awaited = true;
		if (job.deliveryTimer !== undefined) {
			clearTimeout(job.deliveryTimer);
			job.deliveryTimer = undefined;
		}
	}
	shutdown() {
		for (const timer of this.evictionTimers.values()) clearTimeout(timer);
		this.evictionTimers.clear();
		for (const job of this.jobs.values()) {
			if (job.status === "running") {
				job.status = "cancelled";
				job.abortController.abort();
			}
		}
	}
	deliverResult(job) {
		if (!this.onJobComplete) return;
		const cb = this.onJobComplete;
		job.deliveryTimer = setTimeout(() => {
			job.deliveryTimer = undefined;
			if (!job.awaited) cb(job);
		}, 0);
		if (typeof job.deliveryTimer === "object" && "unref" in job.deliveryTimer) {
			job.deliveryTimer.unref();
		}
	}
	scheduleEviction(id) {
		const existing = this.evictionTimers.get(id);
		if (existing) clearTimeout(existing);
		const timer = setTimeout(() => {
			this.evictionTimers.delete(id);
			this.jobs.delete(id);
		}, this.evictionMs);
		this.evictionTimers.set(id, timer);
	}
	evictOldest() {
		let oldest;
		for (const job of this.jobs.values()) {
			if (job.status !== "running") {
				if (!oldest || job.startTime < oldest.startTime) oldest = job;
			}
		}
		if (!oldest) return;
		const timer = this.evictionTimers.get(oldest.id);
		if (timer) clearTimeout(timer);
		this.evictionTimers.delete(oldest.id);
		this.jobs.delete(oldest.id);
	}
}
