const require_chunk = require("./chunk-Ble4zEEl.js");
const require_manifest_python_deps = require("./manifest-python-deps-DgWowaW0.js");
const require_worker_device_restore = require("./worker-device-restore-B_Di-zuW.js");
let _camstack_types = require("@camstack/types");
let node_fs = require("node:fs");
node_fs = require_chunk.__toESM(node_fs);
let node_path = require("node:path");
node_path = require_chunk.__toESM(node_path);
//#region src/moleculer/group-runner.ts
/**
* Group Runner — entry point for spawned subprocesses that host MULTIPLE
* addons sharing one Moleculer broker.
*
* The companion of `process-runner.ts` (single addon). When the new
* `AddonExecution.group` schema lands, the kernel groups co-located
* addons by their `group` value and spawns ONE subprocess per group via
* this runner. Inside the spawned process:
*
*   - Single Moleculer ServiceBroker, hierarchical nodeID
*     `${parentNodeId}/${groupId}`.
*   - Single shared `CapabilityRegistry` (per-broker, keyed by
*     `broker.nodeID` in `addon-context-factory.perBrokerRegistries`).
*   - One Moleculer service per addon (same naming as
*     `addon-service-factory.createAddonService`).
*   - Cross-addon `ctx.api.<cap>.<method>(...)` calls between siblings
*     of the same group resolve via `localProviderLink` (zero IPC) —
*     they share the same registry. Cross-group / cross-node calls
*     keep going through `brokerTransportLink` (Moleculer TCP).
*
* Environment variables:
* - CAMSTACK_GROUP_ID         — group label (becomes the nodeID suffix)
* - CAMSTACK_GROUP_ADDONS     — JSON array of `{ addonId, addonDir }`
* - CAMSTACK_PARENT_NODE_ID   — hub or agent nodeID
* - CAMSTACK_PARENT_HOST/PORT — TCP coordinates for the parent broker
* - CAMSTACK_DATA_DIR         — addon data root (default ./camstack-data)
* - CAMSTACK_CLUSTER_SECRET   — Moleculer namespace
* - CAMSTACK_LOG_LEVEL        — broker log level (default debug)
*
* Exits non-zero on fatal init failures so the parent process supervisor
* can respawn. Graceful SIGTERM/SIGINT triggers `addon.shutdown()` for
* every loaded addon before stopping the broker.
*/
function isRecord(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Resolve the addon class from a freshly imported module. Mirrors the
* heuristic in `addon-loader.resolveAddonClass` (keep these three
* copies — addon-loader, process-runner, group-runner — in sync).
*
* Contract: the addon class extends `BaseAddon`, whose prototype carries
* an `initialize` method. Device classes (`BaseDevice` subclasses) don't.
* Probing for that method disambiguates when a CJS bundle re-exports
* both shapes and the operator-friendly "first function" heuristic
* would otherwise pick the wrong one (regression seen with
* `@camstack/addon-provider-reolink` after a tsup rebuild reshuffled
* the named-export order so `ReolinkCamera` came before
* `ReolinkProviderAddon`).
*/
function isAddonConstructor(fn) {
	if (typeof fn !== "function") return false;
	const proto = fn.prototype;
	if (!isRecord(proto)) return false;
	return typeof proto["initialize"] === "function";
}
function resolveAddonClass(mod) {
	let candidate = mod["default"];
	if (isRecord(candidate) && "default" in candidate) candidate = candidate["default"];
	if (isAddonConstructor(candidate)) return candidate;
	if (isRecord(candidate)) {
		const fn = Object.values(candidate).find(isAddonConstructor);
		if (fn) return fn;
	}
	return Object.values(mod).find(isAddonConstructor) ?? null;
}
function parseAddonsSpec(raw) {
	if (!raw) return [];
	let parsed;
	try {
		parsed = JSON.parse(raw);
	} catch (err) {
		throw new Error(`[group-runner] CAMSTACK_GROUP_ADDONS is not valid JSON: ${(0, _camstack_types.errMsg)(err)}`, { cause: err });
	}
	if (!Array.isArray(parsed)) throw new Error(`[group-runner] CAMSTACK_GROUP_ADDONS must be a JSON array`);
	const specs = [];
	for (const entry of parsed) {
		if (!isRecord(entry)) throw new Error(`[group-runner] addon spec is not an object: ${JSON.stringify(entry)}`);
		const addonId = entry["addonId"];
		const addonDir = entry["addonDir"];
		if (typeof addonId !== "string" || typeof addonDir !== "string") throw new Error(`[group-runner] addon spec missing addonId/addonDir: ${JSON.stringify(entry)}`);
		specs.push({
			addonId,
			addonDir
		});
	}
	return specs;
}
async function loadAddonModule(addonId, addonDir) {
	const pkgJsonPath = node_path.join(addonDir, "package.json");
	if (!node_fs.existsSync(pkgJsonPath)) throw new Error(`No package.json at ${addonDir}`);
	const declaration = JSON.parse(node_fs.readFileSync(pkgJsonPath, "utf-8"))["camstack"]?.addons?.find((a) => a.id === addonId);
	if (!declaration?.entry) throw new Error(`Addon "${addonId}" not found in ${pkgJsonPath} manifest`);
	const entryPath = node_path.resolve(addonDir, declaration.entry);
	if (!node_fs.existsSync(entryPath)) throw new Error(`Entry point not found: ${entryPath}`);
	const modUnknown = await import(entryPath);
	const mod = isRecord(modUnknown) ? modUnknown : {};
	const AddonClass = resolveAddonClass(mod);
	if (!AddonClass) throw new Error(`No addon class exported from ${entryPath}`);
	return {
		declaration,
		mod,
		AddonClass
	};
}
async function main() {
	const groupId = process.env["CAMSTACK_GROUP_ID"];
	const addonsJson = process.env["CAMSTACK_GROUP_ADDONS"];
	const parentNodeId = process.env["CAMSTACK_PARENT_NODE_ID"];
	const dataDir = process.env["CAMSTACK_DATA_DIR"] ?? "./camstack-data";
	if (!groupId || !addonsJson || !parentNodeId) {
		console.error("[group-runner] Missing required env vars: CAMSTACK_GROUP_ID, CAMSTACK_GROUP_ADDONS, CAMSTACK_PARENT_NODE_ID");
		process.exit(1);
	}
	let specs;
	try {
		specs = parseAddonsSpec(addonsJson);
	} catch (err) {
		console.error((0, _camstack_types.errMsg)(err));
		process.exit(1);
	}
	if (specs.length === 0) {
		console.error("[group-runner] CAMSTACK_GROUP_ADDONS resolved to 0 addons — refusing to start");
		process.exit(1);
	}
	const nodeId = `${parentNodeId}/${groupId}`;
	console.log(`[group-runner] Starting "${nodeId}" with ${specs.length} addon(s): ${specs.map((s) => s.addonId).join(", ")}`);
	const childLogLevel = process.env["CAMSTACK_LOG_LEVEL"] ?? "debug";
	const parentPort = Number(process.env["CAMSTACK_PARENT_TCP_PORT"] ?? "6000");
	const parentUrl = `${process.env["CAMSTACK_PARENT_HOST"] ?? "127.0.0.1"}:${parentPort}/${parentNodeId}`;
	const broker = new require_manifest_python_deps.ServiceBroker({
		nodeID: nodeId,
		namespace: process.env["CAMSTACK_CLUSTER_SECRET"] || void 0,
		serializer: "MsgPack",
		transporter: {
			type: "TCP",
			options: {
				udpDiscovery: false,
				urls: parentUrl,
				port: 0,
				maxPacketSize: 10 * 1024 * 1024
			}
		},
		metrics: {
			enabled: true,
			reporter: [{
				type: "Event",
				options: {
					eventName: "$metrics.snapshot",
					interval: 5
				}
			}]
		},
		logger: {
			type: "Console",
			options: { level: {
				BROKER: "error",
				TRANSPORTER: "error",
				REGISTRY: "error",
				DISCOVERER: "error",
				"**": childLogLevel
			} }
		}
	});
	const loadedModules = [];
	for (const spec of specs) try {
		const { declaration, AddonClass } = await loadAddonModule(spec.addonId, spec.addonDir);
		loadedModules.push({
			spec,
			declaration,
			AddonClass
		});
	} catch (err) {
		console.error(`[group-runner] Failed to load "${spec.addonId}": ${(0, _camstack_types.errMsg)(err)}`);
		process.exit(1);
	}
	require_manifest_python_deps.registerClusterEventsService(broker);
	broker.createService({
		name: "$node-mgmt",
		actions: { setLogLevel: { handler(ctx) {
			const level = ctx.params.level;
			const factory = broker.logger;
			const appenders = factory?.appenders;
			if (appenders) for (const appender of appenders) appender.opts.level = level;
			const cache = factory?.cache;
			if (cache) cache.clear();
			console.log(`[group-runner] Log level changed to "${level}"`);
			return true;
		} } }
	});
	for (const { spec } of loadedModules) require_manifest_python_deps.mountNativeCapService(broker, spec.addonId, _camstack_types.deviceOpsCapability);
	await broker.start();
	await broker.waitForServices(["$hub"]);
	require_manifest_python_deps.setHubConnected(broker);
	let storageProvider;
	try {
		const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("./node-9OubKnvt.js")).then((n) => n.node_exports);
		storageProvider = new FilesystemStorageProvider(dataDir);
		console.log(`[group-runner] "${nodeId}" storage provider rooted at ${dataDir}`);
	} catch (err) {
		console.warn(`[group-runner] "${nodeId}" failed to load FilesystemStorageProvider: ${(0, _camstack_types.errMsg)(err)}`);
	}
	const INFRA_CAP_NAMES = new Set([
		"settings-store",
		"storage",
		"log-destination",
		"metrics-provider",
		"platform-probe"
	]);
	const infraPriority = (decl) => {
		const caps = decl.capabilities ?? [];
		for (const cap of caps) {
			const name = typeof cap === "string" ? cap : cap.name;
			if (INFRA_CAP_NAMES.has(name)) return 0;
		}
		return 1;
	};
	loadedModules.sort((a, b) => infraPriority(a.declaration) - infraPriority(b.declaration));
	const loaded = [];
	const customActionRegistry = new require_manifest_python_deps.CustomActionRegistry();
	for (const { spec, declaration, AddonClass } of loadedModules) try {
		const addon = new AddonClass();
		const context = require_manifest_python_deps.createAddonContext(broker, declaration, dataDir, storageProvider ? { storageProvider } : void 0);
		await require_manifest_python_deps.installManifestPythonDeps(declaration, spec.addonDir, context.deps, context.logger);
		const registeredProviders = /* @__PURE__ */ new Map();
		const initResult = (0, _camstack_types.normalizeAddonInitResult)(await addon.initialize(context));
		for (const reg of initResult?.providers ?? []) {
			const capName = reg.capability.name;
			registeredProviders.set(capName, reg.provider);
			context.registerProvider(capName, reg.provider);
		}
		if (initResult?.customActions && initResult.actionHandlers) {
			const handlers = initResult.actionHandlers;
			customActionRegistry.registerAddon(spec.addonId, initResult.customActions, (action, input) => {
				const fn = handlers[action];
				if (!fn) throw new Error(`addon '${spec.addonId}' has no handler for custom action '${action}'`);
				return fn(input);
			});
		}
		require_manifest_python_deps.validateProviderRegistrations(spec.addonId, declaration, registeredProviders, console);
		const methodResolver = (capName) => {
			const reg = initResult?.providers?.find((r) => r.capability.name === capName);
			if (!reg) return [];
			return Object.keys((0, _camstack_types.expandCapMethods)(reg.capability));
		};
		const timeoutResolver = (capName, methodName) => {
			const reg = initResult?.providers?.find((r) => r.capability.name === capName);
			if (!reg) return void 0;
			return (0, _camstack_types.expandCapMethods)(reg.capability)[methodName]?.timeoutMs;
		};
		const schema = require_manifest_python_deps.createAddonService(addon, declaration, {
			providers: registeredProviders,
			methodResolver,
			timeoutResolver,
			customActions: initResult?.customActions,
			actionHandlers: initResult?.actionHandlers
		});
		const actionNames = Object.keys(schema.actions ?? {});
		console.log(`[group-runner] "${spec.addonId}" actions: ${actionNames.length > 0 ? actionNames.join(", ") : "(none)"}`);
		broker.createService(schema);
		if (typeof addon.restoreDevices === "function") require_worker_device_restore.runWorkerDeviceRestoreWithRetry(addon, context, spec.addonId, broker.nodeID);
		addon.postBrokerStart();
		loaded.push({
			addonId: spec.addonId,
			declaration,
			addon,
			initResult: initResult ?? null
		});
	} catch (err) {
		console.error(`[group-runner] Init failed for "${spec.addonId}": ${(0, _camstack_types.errMsg)(err)}`);
		process.exit(1);
	}
	const parentBase = nodeId.split("/")[0] ?? nodeId;
	let hubReachableFired = false;
	broker.localBus.on("$node.connected", ({ node }) => {
		if (node.id !== parentBase && node.id !== "hub") return;
		require_manifest_python_deps.setHubConnected(broker);
		if (hubReachableFired) return;
		hubReachableFired = true;
		for (const entry of loaded) if (typeof entry.addon.onHubReachable === "function") Promise.resolve(entry.addon.onHubReachable()).catch((err) => {
			console.error(`[group-runner] "${entry.addonId}" onHubReachable() threw: ${(0, _camstack_types.errMsg)(err)}`);
		});
	});
	console.log(`[group-runner] "${nodeId}" started — ${loaded.length} addon(s) ready, awaiting parent discovery`);
	process.stdout.write(`READY:${nodeId}\n`);
	const shutdown = async () => {
		console.log(`[group-runner] "${nodeId}" shutting down ${loaded.length} addon(s)...`);
		for (const entry of loaded) try {
			await entry.addon.shutdown();
		} catch (err) {
			console.error(`[group-runner] "${entry.addonId}" shutdown threw: ${(0, _camstack_types.errMsg)(err)}`);
		}
		await broker.stop();
		process.exit(0);
	};
	process.on("SIGTERM", () => {
		shutdown();
	});
	process.on("SIGINT", () => {
		shutdown();
	});
	const initialPpid = process.ppid;
	setInterval(() => {
		if (process.ppid === 1 && initialPpid !== 1) {
			console.error(`[group-runner] "${nodeId}" parent died (reparented to init) — exiting`);
			shutdown();
		}
	}, 5e3);
}
main().catch((err) => {
	console.error(`[group-runner] Fatal error: ${(0, _camstack_types.errMsg)(err)}`);
	process.exit(1);
});
//#endregion

//# sourceMappingURL=group-runner.js.map