import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { CHANNEL_RESTART_POLICY, computeBackoff } from "./restart-policy.js";
//#region src/channels/lifecycle-supervisor.ts
init_logger();
const log = createLogger("ChannelLifecycleSupervisor");
function asChannelConfig(raw) {
	if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
}
var ChannelLifecycleSupervisor = class {
	opts;
	/** Plugin ids whose `init()` completed (used by start/stop loops). */
	initializedPluginIds = /* @__PURE__ */ new Set();
	restartAttempts = /* @__PURE__ */ new Map();
	/** When set, failed-start auto-restart is suppressed for that channel id. */
	manuallyStopped = /* @__PURE__ */ new Set();
	/** Plugins that skipped `start()` until `startDeferredConnects()`. */
	deferredConnectPending = /* @__PURE__ */ new Set();
	initialized = false;
	running = false;
	constructor(opts) {
		this.opts = opts;
	}
	isInitialized(pluginId) {
		return this.initializedPluginIds.has(pluginId);
	}
	snapshot() {
		return {
			initialized: this.initialized,
			running: this.running,
			initializedPluginIds: [...this.initializedPluginIds],
			manuallyStopped: [...this.manuallyStopped],
			restartAttempts: Object.fromEntries(this.restartAttempts)
		};
	}
	/** Channel ids that would run and declare `meta.deferConnectUntilAfterListen` (for logging / metrics). */
	listDeferConnectChannelIds(cfg) {
		const out = [];
		for (const [id, plugin] of this.opts.registry.entries()) {
			const channelConfig = asChannelConfig(cfg.channels?.[id]);
			if (!this.shouldRunChannelPlugin(plugin, channelConfig)) continue;
			if (plugin.meta.deferConnectUntilAfterListen === true) out.push(id);
		}
		return out;
	}
	async initialize() {
		if (this.initialized) {
			log.warn({ pluginCount: this.opts.registry.ids().length }, "initialize() called again; channels already initialized — skipping");
			return;
		}
		const cfg = this.opts.getConfig();
		const initPromises = [];
		for (const [id, plugin] of this.opts.registry.entries()) {
			const channelConfig = asChannelConfig(cfg.channels?.[id]);
			initPromises.push(this.initializePlugin(plugin, channelConfig ?? {}));
		}
		await Promise.allSettled(initPromises);
		this.initialized = true;
		log.info("All channel plugins initialized");
	}
	/** Phase 1: start every enabled channel except those in `deferConnectPluginIds`. */
	async start(options) {
		if (!this.initialized) throw new Error("Channels not initialized");
		if (this.running) {
			log.warn({ pluginCount: this.opts.registry.ids().length }, "start() called while channels already running — skipping");
			return;
		}
		const deferIds = options?.deferConnectPluginIds ?? /* @__PURE__ */ new Set();
		this.deferredConnectPending.clear();
		const cfg = this.opts.getConfig();
		const startPromises = [];
		for (const [id, plugin] of this.opts.registry.entries()) {
			const channelConfig = asChannelConfig(cfg.channels?.[id]);
			if (!this.shouldRunChannelPlugin(plugin, channelConfig)) continue;
			if (deferIds.has(id)) {
				this.deferredConnectPending.add(id);
				log.info({ channel: id }, "Channel connect deferred until HTTP listen");
				continue;
			}
			startPromises.push(this.startPlugin(plugin, {}).catch((err) => {
				log.error({
					channel: id,
					err
				}, "Failed to start channel plugin");
			}));
		}
		await Promise.allSettled(startPromises);
		this.running = true;
		log.info({ deferred: [...this.deferredConnectPending] }, this.deferredConnectPending.size > 0 ? "Channel plugins started (deferred connects pending)" : "All channel plugins started");
	}
	/** Phase 2: `start()` for channels deferred at Phase 1. No-op if none pending. */
	async startDeferredConnects() {
		if (this.deferredConnectPending.size === 0) return;
		if (!this.running) {
			log.warn("startDeferredConnects called before channel phase-1 start; skipping");
			return;
		}
		const ids = [...this.deferredConnectPending];
		this.deferredConnectPending.clear();
		const cfg = this.opts.getConfig();
		const startPromises = [];
		for (const id of ids) {
			const plugin = this.opts.registry.get(id);
			if (!plugin) continue;
			const channelConfig = asChannelConfig(cfg.channels?.[id]);
			if (!this.shouldRunChannelPlugin(plugin, channelConfig)) continue;
			startPromises.push(this.startPlugin(plugin, {}).catch((err) => {
				log.error({
					channel: id,
					err
				}, "Failed to start deferred channel plugin");
			}));
		}
		await Promise.allSettled(startPromises);
		log.info({ channels: ids }, "Deferred channel connects completed");
	}
	async stop() {
		if (!this.running) return;
		this.deferredConnectPending.clear();
		const stopPromises = [];
		for (const id of this.initializedPluginIds) {
			const plugin = this.opts.registry.get(id);
			if (!plugin) continue;
			stopPromises.push(this.stopPlugin(plugin).catch((err) => {
				log.error({
					channel: id,
					err
				}, "Failed to stop channel plugin");
			}));
		}
		await Promise.allSettled(stopPromises);
		this.running = false;
		log.info("All channel plugins stopped");
	}
	/** Stop one channel and suppress automatic restart until `startChannel` is called. */
	async stopChannel(channelId) {
		this.manuallyStopped.add(channelId);
		const plugin = this.opts.registry.get(channelId);
		if (!plugin || !this.initializedPluginIds.has(channelId)) return;
		await this.stopPlugin(plugin).catch((err) => {
			log.error({
				channel: channelId,
				err
			}, "Failed to stop channel plugin");
		});
	}
	/** Clear manual-stop and start one channel (requires prior `initialize()`). */
	async startChannel(channelId) {
		this.manuallyStopped.delete(channelId);
		const plugin = this.opts.registry.get(channelId);
		if (!plugin) {
			log.warn({ channel: channelId }, "Unknown channel");
			return;
		}
		if (!this.initialized) {
			log.warn({ channel: channelId }, "Channels not initialized");
			return;
		}
		const channelConfig = asChannelConfig(this.opts.getConfig().channels?.[channelId]);
		if (!this.shouldRunChannelPlugin(plugin, channelConfig)) {
			log.debug({ channel: channelId }, "Channel disabled in config, skipping start");
			return;
		}
		if (!this.initializedPluginIds.has(channelId)) {
			log.warn({ channel: channelId }, "Channel was never initialized; call initialize() first");
			return;
		}
		await this.startPlugin(plugin, {});
	}
	/** Called by heartbeat: stop + start without clearing manualStop. No-op if user stopped it. */
	async softRestart(channelId) {
		if (this.manuallyStopped.has(channelId)) return;
		const plugin = this.opts.registry.get(channelId);
		if (!plugin || !this.initializedPluginIds.has(channelId)) return;
		try {
			await this.stopPlugin(plugin);
			await this.startPlugin(plugin, {});
		} catch (err) {
			log.error({
				channel: channelId,
				err
			}, "Channel soft restart after heartbeat failed");
		}
	}
	/** Forward a config snapshot to every initialised plugin's `onConfigUpdated` hook. */
	async forwardConfigUpdate(cfg) {
		for (const id of this.initializedPluginIds) {
			const plugin = this.opts.registry.get(id);
			if (plugin?.onConfigUpdated) await Promise.resolve(plugin.onConfigUpdated(cfg));
		}
	}
	/**
	* Builtin channels require `channels.<id>.enabled`. Extension-managed channels
	* run unless explicitly disabled.
	*/
	shouldRunChannelPlugin(plugin, channelConfig) {
		if (plugin.extensionManagedConfig) return channelConfig?.enabled !== false;
		return !!channelConfig?.enabled;
	}
	async initializePlugin(plugin, channelConfig) {
		try {
			const options = {
				bus: this.opts.bus,
				config: this.opts.getConfig(),
				channelConfig,
				sessionModel: this.opts.getSessionModelHooks?.()
			};
			await plugin.init(options);
			this.initializedPluginIds.add(plugin.id);
			log.debug({ channel: plugin.id }, "Channel plugin initialized");
		} catch (err) {
			log.error({
				channel: plugin.id,
				err
			}, "Failed to initialize channel plugin");
		}
	}
	async startPlugin(plugin, opts) {
		const options = {};
		try {
			await plugin.start(options);
			if (!opts.preserveRestartAttempts) this.restartAttempts.delete(plugin.id);
			this.opts.onPluginStarted?.(plugin);
			log.debug({ channel: plugin.id }, "Channel plugin started");
		} catch (err) {
			const attempt = (this.restartAttempts.get(plugin.id) ?? 0) + 1;
			this.restartAttempts.set(plugin.id, attempt);
			if (attempt <= CHANNEL_RESTART_POLICY.maxAttempts) {
				const delayMs = computeBackoff(CHANNEL_RESTART_POLICY, attempt);
				log.warn({
					channel: plugin.id,
					attempt,
					delayMs,
					err
				}, "Channel failed to start, scheduling restart");
				setTimeout(() => {
					if (this.manuallyStopped.has(plugin.id)) {
						log.debug({ channel: plugin.id }, "Skipping scheduled restart (manual stop)");
						return;
					}
					this.startPlugin(plugin, { preserveRestartAttempts: true }).catch((e) => {
						log.error({
							channel: plugin.id,
							err: e
						}, "Channel restart attempt failed");
					});
				}, delayMs);
			} else log.error({
				channel: plugin.id,
				err
			}, "Channel exceeded max restart attempts");
		}
	}
	async stopPlugin(plugin) {
		this.opts.onPluginStopped?.(plugin.id);
		await plugin.stop();
		log.info({ channel: plugin.id }, "Channel plugin stopped");
	}
};
//#endregion
export { ChannelLifecycleSupervisor };

//# sourceMappingURL=lifecycle-supervisor.js.map