import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { init_agent_scope, normalizeAgentId } from "../agent/agent-scope.js";
import { buildSessionKey, init_session_key } from "../routing/session-key.js";
import { bundledChannelPlugins } from "../generated/bundled-channel-plugins.js";
import { shouldSilence, stripHeartbeatToken } from "../heartbeat/tokens.js";
import { getChannelPlugin, listChannelPlugins, syncChannelPluginsFromManager } from "../channels/plugins/registry.js";
import { getCronPayloadText } from "./job-content.js";
//#region src/cron/executor.ts
init_logger();
init_agent_scope();
init_session_key();
const log = createLogger("CronExecutor");
const ERROR_BACKOFF_MS = [
	3e4,
	6e4,
	5 * 6e4,
	15 * 6e4,
	60 * 6e4
];
function errorBackoffMs(consecutiveErrors) {
	const idx = Math.min(consecutiveErrors - 1, ERROR_BACKOFF_MS.length - 1);
	return ERROR_BACKOFF_MS[Math.max(0, idx)];
}
function resolveIsolatedCronJobModel(job) {
	const fromJob = job.model?.trim();
	if (fromJob) return fromJob;
	if (job.payload?.kind === "agentTurn") {
		const m = job.payload.model?.trim();
		if (m) return m;
	}
}
var DefaultJobExecutor = class {
	history = /* @__PURE__ */ new Map();
	runningJobs = /* @__PURE__ */ new Map();
	agentService = null;
	messageBus = null;
	heartbeatService = null;
	sessionStore;
	runLogStore = null;
	getDefaultCronAgentId = null;
	setRunLogStore(store) {
		this.runLogStore = store;
	}
	setDeps(deps) {
		this.agentService = deps.agentService;
		this.messageBus = deps.messageBus;
		this.heartbeatService = deps.heartbeatService ?? null;
		if (deps.sessionStore !== void 0) this.sessionStore = deps.sessionStore;
		this.getDefaultCronAgentId = deps.getDefaultCronAgentId ?? null;
	}
	async buildCronOutboundMessage(channel, to, content) {
		if (listChannelPlugins().length === 0) syncChannelPluginsFromManager(bundledChannelPlugins);
		const plugin = getChannelPlugin(channel);
		if (plugin?.cronDelivery) {
			const { chatId, accountId, metadata } = await plugin.cronDelivery.normalizeDeliveryTarget(to, this.sessionStore);
			const meta = accountId || metadata ? {
				...metadata ?? {},
				...accountId ? { accountId } : {}
			} : void 0;
			return {
				channel,
				chat_id: chatId,
				content,
				type: "message",
				...meta && Object.keys(meta).length > 0 ? { metadata: meta } : {}
			};
		}
		return {
			channel,
			chat_id: to,
			content,
			type: "message"
		};
	}
	async execute(job, signal, deps) {
		if (deps) this.setDeps(deps);
		const executionId = crypto.randomUUID();
		const execution = {
			id: executionId,
			jobId: job.id,
			status: "running",
			startedAt: (/* @__PURE__ */ new Date()).toISOString(),
			retryCount: 0
		};
		this.addToHistory(job.id, execution);
		this.runningJobs.set(job.id, new AbortController());
		log.info({
			jobId: job.id,
			executionId,
			preview: getCronPayloadText(job).slice(0, 100)
		}, "Job executing");
		let result;
		try {
			if (signal.aborted) throw new Error("Job was cancelled before execution");
			result = await this.performJob(job, signal);
			execution.status = result.status === "ok" ? "success" : result.status === "skipped" ? "cancelled" : "failed";
			execution.endedAt = (/* @__PURE__ */ new Date()).toISOString();
			execution.duration = Date.now() - new Date(execution.startedAt).getTime();
			execution.summary = result.summary;
			execution.error = result.error;
			execution.sessionId = result.sessionId;
			execution.sessionKey = result.sessionKey;
			execution.sessionType = result.sessionType;
			execution.model = result.model;
			if (result.status === "ok") log.info({
				jobId: job.id,
				executionId,
				duration: execution.duration
			}, "Job completed");
			else if (result.status === "skipped") log.warn({
				jobId: job.id,
				executionId,
				reason: result.error
			}, "Job skipped");
			else log.error({
				jobId: job.id,
				executionId,
				error: result.error
			}, "Job failed");
			if (result.status === "ok" && this.heartbeatService) try {
				this.heartbeatService.requestNow({ reason: `cron:${job.id}` });
			} catch (e) {
				log.warn({
					jobId: job.id,
					err: e
				}, "Heartbeat wake after cron failed");
			}
		} catch (error) {
			execution.status = "failed";
			execution.endedAt = (/* @__PURE__ */ new Date()).toISOString();
			execution.duration = Date.now() - new Date(execution.startedAt).getTime();
			execution.error = error instanceof Error ? error.message : String(error);
			log.error({
				jobId: job.id,
				executionId,
				error: execution.error
			}, "Job execution error");
			result = {
				status: "error",
				error: execution.error
			};
		} finally {
			this.runningJobs.delete(job.id);
		}
		if (this.runLogStore) await this.runLogStore.appendCompleted(execution);
	}
	/**
	* Perform the actual job work - integrate with AgentService
	*/
	async performJob(job, signal) {
		const timeout = job.timeout || 6e4;
		const sessionTarget = job.sessionTarget || "main";
		if (signal.aborted) return {
			status: "skipped",
			error: "Job was aborted before execution"
		};
		if (!this.agentService || !this.messageBus) {
			log.warn({ jobId: job.id }, "No agent service configured, using basic execution");
			return this.basicExecute(job, signal, timeout);
		}
		try {
			if (sessionTarget === "main") return await this.executeMainSession(job, signal, timeout);
			else return await this.executeIsolated(job, signal, timeout);
		} catch (error) {
			return {
				status: "error",
				error: error instanceof Error ? error.message : String(error)
			};
		}
	}
	/**
	* Execute in main session - sends system event
	*/
	async executeMainSession(job, signal, timeout) {
		const text = getCronPayloadText(job);
		if (!text || !text.trim()) return {
			status: "skipped",
			error: "Main session job requires non-empty message"
		};
		let channel;
		let to;
		let actualMessage;
		if (job.delivery?.channel === "local") {
			channel = "local";
			to = "";
			actualMessage = text;
		} else if (job.delivery?.channel && job.delivery?.to) {
			channel = job.delivery.channel;
			to = job.delivery.to;
			actualMessage = text;
		} else {
			const parts = text.split(":");
			const hasAtLeastThreeParts = parts.length >= 3;
			if (listChannelPlugins().length === 0) syncChannelPluginsFromManager(bundledChannelPlugins);
			const registeredChannelIds = listChannelPlugins().map((p) => p.id);
			const firstPartIsChannel = [...new Set([
				...registeredChannelIds,
				"cli",
				"gateway",
				"local"
			])].includes(parts[0]);
			if (hasAtLeastThreeParts && firstPartIsChannel) {
				channel = parts[0];
				to = parts[1];
				actualMessage = parts.slice(2).join(":");
				log.info({
					jobId: job.id,
					channel,
					to,
					parsedFrom: "message",
					originalLength: text.length
				}, "Parsed delivery from payload text");
			} else {
				channel = "cli";
				to = "cron";
				actualMessage = text;
				log.debug({
					jobId: job.id,
					partsCount: parts.length,
					firstPart: parts[0],
					hasDelivery: !!job.delivery
				}, "Using default delivery - message format not recognized");
			}
		}
		const timeoutPromise = new Promise((_, reject) => {
			setTimeout(() => reject(/* @__PURE__ */ new Error(`Job timed out after ${timeout}ms`)), timeout);
		});
		const executePromise = (async () => {
			if (signal.aborted) throw new Error("Job was aborted");
			if (channel === "local") {
				log.info({
					jobId: job.id,
					messageLength: actualMessage.length
				}, "Cron main session: local channel — no outbound publish");
				return {
					status: "ok",
					summary: actualMessage.slice(0, 200)
				};
			}
			const outbound = await this.buildCronOutboundMessage(channel, to, actualMessage);
			await this.messageBus.publishOutbound(outbound);
			log.info({
				jobId: job.id,
				channel,
				to: outbound.chat_id,
				messageLength: actualMessage.length
			}, "Sent message to main session");
			return {
				status: "ok",
				summary: actualMessage.slice(0, 200)
			};
		})();
		return await Promise.race([executePromise, timeoutPromise]);
	}
	/**
	* Execute in isolated mode - runs agent independently
	*/
	async executeIsolated(job, signal, timeout) {
		const message = getCronPayloadText(job);
		if (!message || !message.trim()) return {
			status: "skipped",
			error: "Isolated job requires non-empty message"
		};
		const aid = job.agentId?.trim();
		const fallbackAgentId = this.getDefaultCronAgentId?.() ?? "main";
		const sessionKey = buildSessionKey({
			agentId: normalizeAgentId(aid || fallbackAgentId),
			source: "cron",
			accountId: "default",
			peerKind: "dm",
			peerId: job.id
		});
		const timeoutPromise = new Promise((_, reject) => {
			setTimeout(() => reject(/* @__PURE__ */ new Error(`Job timed out after ${timeout}ms`)), timeout);
		});
		const executePromise = (async () => {
			if (signal.aborted) throw new Error("Job was aborted");
			await this.agentService.sessionConfig.applyCronJobWorkingDirectory(sessionKey, job.workingDirectory);
			const jobModel = resolveIsolatedCronJobModel(job);
			if (jobModel) {
				if (!await this.agentService.switchModelForSession(sessionKey, jobModel)) {
					log.warn({
						jobId: job.id,
						sessionKey,
						model: jobModel
					}, "Cron job model invalid; using agent default");
					await this.agentService.resetSessionModelToAgentDefault(sessionKey);
				}
			} else await this.agentService.resetSessionModelToAgentDefault(sessionKey);
			const response = await this.agentService.turnDispatcher.processDirect(message, sessionKey);
			const model = this.agentService.getModelForSession(sessionKey);
			log.info({
				jobId: job.id,
				sessionKey,
				responseLength: response.length,
				model
			}, "Agent execution completed");
			const delivery = job.delivery;
			const outboundChannel = delivery?.channel;
			if (delivery && delivery.mode !== "none" && outboundChannel !== "local" && delivery.to) {
				if (shouldSilence(response, 300) || response.trim() === "NO_REPLY") return {
					status: "ok",
					summary: response.slice(0, 200),
					sessionId: sessionKey,
					sessionKey,
					sessionType: "cron",
					model
				};
				const { stripped } = stripHeartbeatToken(response);
				const outboundText = stripped || response.trim();
				const targetChannel = outboundChannel || "cli";
				const outbound = await this.buildCronOutboundMessage(targetChannel, delivery.to, outboundText);
				await this.messageBus.publishOutbound(outbound);
				log.info({
					jobId: job.id,
					channel: targetChannel,
					to: outbound.chat_id
				}, "Delivered agent response");
				return {
					status: "ok",
					summary: response.slice(0, 200),
					sessionId: sessionKey,
					sessionKey,
					sessionType: "cron",
					model
				};
			}
			return {
				status: "ok",
				summary: response.slice(0, 200),
				sessionId: sessionKey,
				sessionKey,
				sessionType: "cron",
				model
			};
		})();
		try {
			return await Promise.race([executePromise, timeoutPromise]);
		} finally {
			const { retireSessionMcpRuntimeForSessionKey } = await import("../agent/mcp/bundle-mcp-tools.js");
			await retireSessionMcpRuntimeForSessionKey({
				sessionKey,
				reason: "cron-isolated-end"
			}).catch(() => {});
		}
	}
	/**
	* Basic execution without agent service (fallback)
	*/
	async basicExecute(job, signal, timeout) {
		return new Promise((resolve, reject) => {
			const timeoutId = setTimeout(() => {
				reject(/* @__PURE__ */ new Error(`Job timed out after ${timeout}ms`));
			}, timeout);
			const abortHandler = () => {
				clearTimeout(timeoutId);
				reject(/* @__PURE__ */ new Error("Job was aborted"));
			};
			signal.addEventListener("abort", abortHandler);
			setTimeout(() => {
				clearTimeout(timeoutId);
				signal.removeEventListener("abort", abortHandler);
				if (signal.aborted) reject(/* @__PURE__ */ new Error("Job was aborted"));
				else resolve({
					status: "ok",
					summary: `Executed: ${getCronPayloadText(job).slice(0, 100)}`
				});
			}, 100);
		});
	}
	/**
	* Cancel a running job
	*/
	cancelJob(jobId) {
		const controller = this.runningJobs.get(jobId);
		if (controller) {
			controller.abort();
			this.runningJobs.delete(jobId);
			return true;
		}
		return false;
	}
	/**
	* Get execution history for a job
	*/
	getHistory(jobId, limit = 10) {
		return (this.history.get(jobId) || []).slice(-limit);
	}
	/**
	* Get currently running executions
	*/
	getRunningExecutions() {
		const result = [];
		for (const [jobId] of this.runningJobs) {
			const history = this.history.get(jobId);
			if (history) {
				const running = history.find((e) => e.status === "running");
				if (running) result.push(running);
			}
		}
		return result;
	}
	/**
	* Check if a job is currently running
	*/
	isRunning(jobId) {
		return this.runningJobs.has(jobId);
	}
	/**
	* Get consecutive error count for a job
	*/
	getConsecutiveErrors(jobId) {
		const history = this.history.get(jobId) || [];
		for (let i = history.length - 1; i >= 0; i--) if (history[i].status !== "running") {
			if (history[i].status === "failed" || history[i].status === "cancelled") {
				let count = 0;
				for (let j = i - 1; j >= 0; j--) if (history[j].status === "failed" || history[j].status === "cancelled") count++;
				else break;
				return count + 1;
			}
			return 0;
		}
		return 0;
	}
	/**
	* Calculate backoff delay for a job
	*/
	calculateBackoff(jobId) {
		return errorBackoffMs(this.getConsecutiveErrors(jobId));
	}
	/**
	* Clear old history entries
	*/
	cleanupHistory(maxAgeDays = 7) {
		const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
		for (const [jobId, executions] of this.history) this.history.set(jobId, executions.filter((e) => new Date(e.startedAt).getTime() > cutoff));
	}
	addToHistory(jobId, execution) {
		const existing = this.history.get(jobId) || [];
		existing.push(execution);
		if (existing.length > 100) existing.shift();
		this.history.set(jobId, existing);
	}
};
//#endregion
export { DefaultJobExecutor };

//# sourceMappingURL=executor.js.map