import { createLogger } from "../../utils/logger/index.js";
import { init_logger } from "../../utils/logger.js";
import { renderWorkflowText } from "./snapshot.js";
//#region src/agent/workflow/progress-broker.ts
init_logger();
const log = createLogger("workflow-progress-broker");
const WORKFLOW_TOOL_NAME = "workflow";
const RENDER_MAX_AGENTS_PER_PHASE = 4;
const RENDER_MAX_LOGS = 2;
var WorkflowProgressBroker = class {
	subscribers = [];
	states = /* @__PURE__ */ new Map();
	/** Now() factory — overridable in tests for deterministic time. */
	now;
	/** Cached resolved settings per (channelId), invalidated on registration. */
	resolved = /* @__PURE__ */ new Map();
	constructor(opts = {}) {
		this.opts = opts;
		this.now = opts.now ?? (() => Date.now());
	}
	registerChannel(cap) {
		if (this.subscribers.some((s) => s.channelId === cap.channelId)) {
			log.warn({ channelId: cap.channelId }, "channel capability already registered; replacing");
			this.subscribers = this.subscribers.filter((s) => s.channelId !== cap.channelId);
		}
		this.subscribers.push(cap);
		this.resolved.delete(cap.channelId);
		return () => {
			this.subscribers = this.subscribers.filter((s) => s !== cap);
			this.resolved.delete(cap.channelId);
		};
	}
	attachTo(bus) {
		const offUpdate = bus.registerListener("tool_execution_update", (event, ctx) => {
			const e = event;
			if (e.toolName !== WORKFLOW_TOOL_NAME) return;
			const snap = extractWorkflowSnapshot(e.partialResult);
			if (!snap) return;
			this.onUpdate(ctx.sessionKey, e.toolCallId, snap);
		});
		const offEnd = bus.registerListener("tool_execution_end", (event, ctx) => {
			const e = event;
			if (e.toolName !== WORKFLOW_TOOL_NAME) return;
			const snap = extractWorkflowSnapshot(e.result, { fromResultEnvelope: true });
			this.onEnd(ctx.sessionKey, e.toolCallId, snap);
		});
		return { dispose: () => {
			offUpdate();
			offEnd();
			this.disposeAllPending();
		} };
	}
	/** Visible for tests — direct entry path bypassing the SessionBus glue. */
	onUpdate(sessionKey, toolCallId, snapshot) {
		const key = stateKey(sessionKey, toolCallId);
		const state = this.getOrCreateState(key, snapshot);
		state.prevSnapshot = state.snapshot;
		state.snapshot = snapshot;
		const isKey = isKeyEvent(state.prevSnapshot, snapshot);
		for (const cap of this.subscribers) this.dispatchToChannel(state, sessionKey, cap, {
			isFinal: false,
			isKey
		});
	}
	/** Visible for tests — direct entry path bypassing the SessionBus glue. */
	onEnd(sessionKey, toolCallId, snapshot) {
		const key = stateKey(sessionKey, toolCallId);
		const state = this.states.get(key);
		if (!state) return;
		if (snapshot) state.snapshot = snapshot;
		for (const cap of this.subscribers) {
			this.cancelPending(state, cap.channelId);
			this.dispatchToChannel(state, sessionKey, cap, {
				isFinal: true,
				isKey: true
			});
		}
		setTimeout(() => this.states.delete(key), 2e3);
	}
	dispatchToChannel(state, sessionKey, cap, flags) {
		const cfg = this.resolveChannelSettings(cap);
		if (!cfg.enabled) return;
		if (cfg.mode === "final-only" && !flags.isFinal) return;
		const chState = this.getOrCreateChannelState(state, cap.channelId);
		if (flags.isFinal || flags.isKey) {
			this.cancelPending(state, cap.channelId);
			this.sendNow(state, sessionKey, cap, cfg, flags.isFinal);
			return;
		}
		const elapsed = this.now() - chState.lastSentAt;
		const wait = Math.max(0, cfg.throttleMs - elapsed);
		if (wait === 0) {
			this.sendNow(state, sessionKey, cap, cfg, false);
			return;
		}
		if (chState.pendingTimer) return;
		chState.pendingTimer = setTimeout(() => {
			chState.pendingTimer = void 0;
			this.sendNow(state, sessionKey, cap, cfg, false);
		}, wait);
	}
	async sendNow(state, sessionKey, cap, cfg, isFinal) {
		const chState = this.getOrCreateChannelState(state, cap.channelId);
		if (chState.inflight) await chState.inflight.catch(() => void 0);
		const text = renderWorkflowText(state.snapshot, isFinal, {
			maxAgentsPerPhase: RENDER_MAX_AGENTS_PER_PHASE,
			maxLogs: RENDER_MAX_LOGS,
			showResultPreviews: isFinal
		});
		const previousMessageId = cfg.mode === "edit" ? chState.lastMessageId : void 0;
		const task = cap.postProgress({
			sessionKey,
			text,
			previousMessageId,
			isFinal,
			mode: cfg.mode
		}).then((r) => {
			chState.lastMessageId = r.messageId;
			chState.lastSentAt = this.now();
		}).catch((err) => {
			const msg = err instanceof Error ? err.message : String(err);
			log.warn({
				err,
				errorMessage: msg,
				channelId: cap.channelId,
				sessionKey
			}, `workflow progress postProgress failed: ${msg}`);
		}).finally(() => {
			chState.inflight = void 0;
		});
		chState.inflight = task;
		await task;
	}
	cancelPending(state, channelId) {
		const chState = state.perChannel.get(channelId);
		if (chState?.pendingTimer) {
			clearTimeout(chState.pendingTimer);
			chState.pendingTimer = void 0;
		}
	}
	disposeAllPending() {
		for (const state of this.states.values()) for (const ch of state.perChannel.values()) if (ch.pendingTimer) clearTimeout(ch.pendingTimer);
		this.states.clear();
	}
	getOrCreateState(key, snapshot) {
		let state = this.states.get(key);
		if (!state) {
			state = {
				snapshot,
				perChannel: /* @__PURE__ */ new Map()
			};
			this.states.set(key, state);
		}
		return state;
	}
	getOrCreateChannelState(state, channelId) {
		let ch = state.perChannel.get(channelId);
		if (!ch) {
			ch = { lastSentAt: 0 };
			state.perChannel.set(channelId, ch);
		}
		return ch;
	}
	/** Resolved (enabled / throttleMs / mode) for a channel, with config overrides. */
	resolveChannelSettings(cap) {
		const cached = this.resolved.get(cap.channelId);
		if (cached) return cached;
		const override = readChannelConfig(this.opts.getConfig?.(), cap.channelId);
		const resolved = {
			enabled: override?.enabled ?? true,
			throttleMs: override?.throttleMs ?? cap.defaultThrottleMs,
			mode: override?.mode ?? cap.defaultMode
		};
		this.resolved.set(cap.channelId, resolved);
		return resolved;
	}
	/** Drop any cached config so the next dispatch re-reads. Call after config reload. */
	invalidateConfigCache() {
		this.resolved.clear();
	}
	/** @internal — for tests only. */
	_stateCount() {
		return this.states.size;
	}
};
let singleton = null;
/**
* Process-wide broker singleton. Channels register against this one; the
* service wires it to the session bus during startup.
*/
function getWorkflowProgressBroker() {
	if (!singleton) singleton = new WorkflowProgressBroker();
	return singleton;
}
/** Test-only — reset the singleton between cases. */
function _resetWorkflowProgressBrokerForTests() {
	singleton = null;
}
function stateKey(sessionKey, toolCallId) {
	return `${sessionKey}${toolCallId ?? ""}`;
}
/**
* Compare two snapshots and decide whether the new one is "key" — i.e. worth
* bypassing the per-channel throttle. Anything visible to the user as a
* progress milestone qualifies; counts ticking by alone do not.
*/
function isKeyEvent(prev, next) {
	if (!prev) return true;
	if (prev.currentPhase !== next.currentPhase) return true;
	if (next.errorCount > prev.errorCount) return true;
	if (next.skippedCount > prev.skippedCount) return true;
	if (prev.phases.length !== next.phases.length) return true;
	if (hasNewFailedAgent(prev.agents, next.agents)) return true;
	return false;
}
function hasNewFailedAgent(prev, next) {
	const prevBad = new Set(prev.filter((a) => a.status === "error" || a.status === "skipped").map((a) => a.id));
	for (const a of next) if ((a.status === "error" || a.status === "skipped") && !prevBad.has(a.id)) return true;
	return false;
}
/**
* Pull a {@link WorkflowSnapshot} out of an AgentToolResult-shaped value.
* Returns null when the payload is not snapshot-shaped (text-only updates,
* non-workflow tools, etc.).
*
* `fromResultEnvelope = true` (used for `tool_end.result`) tolerates the
* `{ content, details }` wrapper.
*/
function extractWorkflowSnapshot(payload, opts = {}) {
	if (!payload || typeof payload !== "object") return null;
	const rec = payload;
	if (opts.fromResultEnvelope) {
		const details = rec.details;
		if (details && typeof details === "object") return coerce(details);
		return null;
	}
	if ("details" in rec) {
		const details = rec.details;
		if (details && typeof details === "object") return coerce(details);
		return null;
	}
	return coerce(rec);
}
function coerce(value) {
	if (!value || typeof value !== "object") return null;
	const rec = value;
	if (typeof rec.name !== "string") return null;
	if (!Array.isArray(rec.agents)) return null;
	return value;
}
function readChannelConfig(config, channelId) {
	const channels = config?.channels;
	if (!channels) return void 0;
	const cfg = channels[channelId];
	if (!cfg || typeof cfg !== "object") return void 0;
	const wf = cfg.workflowProgress;
	if (!wf || typeof wf !== "object") return void 0;
	return wf;
}
//#endregion
export { WorkflowProgressBroker, _resetWorkflowProgressBrokerForTests, getWorkflowProgressBroker };

//# sourceMappingURL=progress-broker.js.map