import { PACKAGE_VERSION, init_package_version } from "../package-version.js";
import { createLogger, createServiceLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { init_agent_scope, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agent/agent-scope.js";
import { init_paths, resolveBundledExtensionsDir, resolveExtensionSdkPath, resolveExtensionsDir, resolveWorkspaceExtensionsDir } from "../config/paths.js";
import { init_loader, loadConfig } from "../config/loader.js";
import { getProviderRegistry, init_plugin_registry } from "../providers/plugin-registry.js";
import { ExtensionRegistryImpl } from "./extension-registry-impl.js";
import { ExtensionApiImpl, createExtensionLogger, createPathResolver } from "./api.js";
import { ActivationPlanner } from "./activation-planner.js";
import { mergeActivationContext } from "./activation-context.js";
import { normalizeExtensionManifest } from "./normalize-manifest.js";
import { areExtensionsGloballyDisabled, discoverExtensionsFromDisk } from "./discover-extensions.js";
import { ManifestRegistry } from "./manifest-registry.js";
import { checkEngineCompatibility } from "./engine-check.js";
import { bundledChannelPlugins } from "../generated/bundled-channel-plugins.js";
import "../channels/plugins/bundled.js";
import { validateSpeechProviderContracts } from "./speech-provider-contracts.js";
import { validateMediaUnderstandingProviderContracts } from "./media-provider-contracts.js";
import { DEFAULT_SECURITY_CONFIG, checkExtensionPathSafety, isExtensionAllowed, logSecurityIssue, provenanceTracker } from "./security.js";
import { getSlotRegistry } from "./slots.js";
import { getExtensionCache, getExtensionDiagnostics } from "./diagnostics.js";
import { createRequire } from "node:module";
import { dirname, isAbsolute, join } from "path";
import { existsSync, readFileSync } from "fs";
import { fileURLToPath } from "node:url";
import { createJiti } from "jiti";
//#region src/extensions/loader.ts
/**
* Extension Loader and Registry
* 
* Supports three-tier extension storage:
* 1. Workspace level (workspace/.extensions/) - highest priority
* 2. Global level (~/.xopc/extensions/) - shared across workspaces
* 3. Bundled level (xopc/extensions/) - shipped with xopc
*/
init_agent_scope();
init_loader();
init_paths();
init_package_version();
init_logger();
init_plugin_registry();
const EXTENSION_MANIFEST_FILE = "xopc.extension.json";
const log = createLogger("ExtensionLoader");
const DEFAULT_EXTENSION_LOAD_CONCURRENCY = 4;
async function mapWithConcurrency(items, concurrency, fn) {
	if (items.length === 0) return;
	const queue = [...items];
	const workerCount = Math.min(Math.max(1, concurrency), queue.length);
	await Promise.all(Array.from({ length: workerCount }, async () => {
		while (queue.length > 0) {
			const item = queue.shift();
			if (item === void 0) break;
			await fn(item);
		}
	}));
}
var ExtensionLoader = class {
	registry;
	options;
	extensionInstances = /* @__PURE__ */ new Map();
	jiti;
	_appConfig;
	_runtimeContext;
	securityConfig;
	providerRegistry;
	slotRegistry;
	slotsConfig = {};
	cache;
	diagnostics;
	/** Manifest-only registry cache (no runtime module load). */
	manifestRegistry = null;
	manifestSnapshot = null;
	constructor(options) {
		this.registry = new ExtensionRegistryImpl();
		for (const p of bundledChannelPlugins) this.registry.addChannelPlugin(p);
		this.options = options || (() => {
			const c = loadConfig();
			const aid = resolveDefaultAgentId(c);
			return {
				workspaceDir: resolveAgentWorkspaceDir(c, aid),
				extensionsDir: resolveWorkspaceExtensionsDir(c, aid)
			};
		})();
		this.securityConfig = DEFAULT_SECURITY_CONFIG;
		this.providerRegistry = getProviderRegistry();
		this.slotRegistry = getSlotRegistry();
		this.cache = getExtensionCache();
		this.diagnostics = getExtensionDiagnostics();
		const alias = {};
		const sdkPath = resolveExtensionSdkPath();
		if (sdkPath) {
			alias["xopc/extension-sdk"] = sdkPath;
			const sdkDir = dirname(sdkPath);
			alias["xopc/extension-sdk/core"] = join(sdkDir, "core.ts");
			alias["xopc/extension-sdk/lazy"] = join(sdkDir, "lazy.ts");
			alias["xopc/extension-sdk/provider"] = join(sdkDir, "provider.ts");
			alias["xopc/extension-sdk/channel"] = join(sdkDir, "channel.ts");
			alias["xopc/extension-sdk/hooks"] = join(sdkDir, "hooks.ts");
			alias["xopc/extension-sdk/tools"] = join(sdkDir, "tools.ts");
			alias["xopc/extension-sdk/testing"] = join(sdkDir, "testing.ts");
			alias["xopc/extension-sdk/speech"] = join(sdkDir, "speech.ts");
			alias["xopc/extension-sdk/media"] = join(sdkDir, "media.ts");
		}
		this.jiti = createJiti(fileURLToPath(import.meta.url), {
			interopDefault: true,
			extensions: [
				".ts",
				".tsx",
				".mts",
				".cts",
				".js",
				".mjs",
				".cjs",
				".json"
			],
			alias
		});
	}
	/**
	* Reuse a pre-built metadata snapshot (avoids duplicate filesystem discovery).
	*/
	setManifestSnapshot(snapshot) {
		this.manifestSnapshot = snapshot;
		this.manifestRegistry = snapshot?.manifestRegistry ?? null;
	}
	getManifestSnapshot() {
		return this.manifestSnapshot;
	}
	/**
	* Set security configuration 
	*/
	setSecurityConfig(config) {
		this.securityConfig = {
			...this.securityConfig,
			...config
		};
	}
	/**
	* Get security configuration 
	*/
	getSecurityConfig() {
		return this.securityConfig;
	}
	/**
	* Get provider registry 
	*/
	getProviderRegistry() {
		return this.providerRegistry;
	}
	/**
	* Get slot registry 
	*/
	getSlotRegistry() {
		return this.slotRegistry;
	}
	/**
	* Get diagnostics 
	*/
	getDiagnostics() {
		return this.diagnostics;
	}
	/**
	* Set configuration from main Config object 
	*/
	setConfig(config) {
		this._appConfig = config;
		const slots = config.extensions?.slots || {};
		this.slotsConfig = {
			memory: slots.memory,
			tts: slots.tts,
			imageGeneration: slots.imageGeneration,
			webSearch: slots.webSearch
		};
		const security = config.extensions?.security;
		if (security) this.securityConfig = {
			checkPermissions: security.checkPermissions ?? true,
			allowUntrusted: security.allowUntrusted ?? false,
			allow: security.allow ?? [],
			trackProvenance: security.trackProvenance ?? true,
			allowPromptInjection: security.allowPromptInjection ?? false
		};
	}
	/**
	* Inject MessageBus and optional SessionManager for ExtensionApi.runtime (Gateway).
	*/
	setRuntimeContext(ctx) {
		this._runtimeContext = ctx;
		this.cache.invalidate();
	}
	getRegistry() {
		return this.registry;
	}
	/**
	* Phase 1: build manifest registry (filesystem manifests only, no extension code load).
	*/
	buildManifestRegistry() {
		if (this.manifestRegistry) return this.manifestRegistry;
		const discovered = this.discoverExtensions();
		this.manifestRegistry = ManifestRegistry.fromDiscovered(discovered);
		return this.manifestRegistry;
	}
	/**
	* Phase 2: activation planner over the manifest registry (pure logic).
	*/
	planActivation(_context) {
		return new ActivationPlanner(this.buildManifestRegistry());
	}
	/**
	* Phase 3: load only extensions selected by the activation plan.
	*/
	async loadByActivationPlan(context) {
		if (areExtensionsGloballyDisabled(this._appConfig)) {
			log.debug("Extension loading skipped (extensions globally disabled)");
			return;
		}
		const registry = this.buildManifestRegistry();
		const planner = new ActivationPlanner(registry);
		const fullContext = mergeActivationContext(this._appConfig, context);
		let activatedIds = planner.getActivatedIds(fullContext).sort((a, b) => a.localeCompare(b));
		const phase = context?.phase ?? "all";
		if (phase === "startup" || phase === "deferred") activatedIds = planner.filterActivatedIdsByLoadPhase(activatedIds, phase);
		const concurrency = Number.parseInt(process.env.XOPC_EXTENSION_LOAD_CONCURRENCY ?? "", 10);
		await mapWithConcurrency(activatedIds, Number.isFinite(concurrency) && concurrency > 0 ? concurrency : DEFAULT_EXTENSION_LOAD_CONCURRENCY, async (extensionId) => {
			if (this.extensionInstances.has(extensionId)) return;
			const entry = registry.getEntry(extensionId);
			if (!entry) {
				log.debug({ extensionId }, "Activation plan references extension id that was not discovered on disk");
				return;
			}
			const config = {
				id: extensionId,
				name: entry.manifest.name || extensionId,
				source: entry.source,
				path: entry.path,
				enabled: true,
				config: this.resolveExtensionConfig(extensionId)
			};
			await this.loadExtension(config);
		});
	}
	getManifestRegistry() {
		return this.buildManifestRegistry();
	}
	invalidateManifestCache() {
		this.manifestRegistry = this.manifestSnapshot?.manifestRegistry ?? null;
	}
	resolveExtensionConfig(extensionId) {
		if (!this._appConfig) return {};
		const extensionsConfig = this._appConfig.extensions;
		if (!extensionsConfig || typeof extensionsConfig !== "object") return {};
		const raw = extensionsConfig[extensionId];
		if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
		return {};
	}
	/**
	* Discover extensions from all three tiers:
	* 1. Workspace (.extensions/) - highest priority
	* 2. Global (~/.xopc/extensions/) - shared
	* 3. Bundled (xopc/extensions/) - lowest priority
	*/
	discoverExtensions() {
		if (this.manifestSnapshot) return this.manifestSnapshot.discovered;
		return discoverExtensionsFromDisk(this.options, this._appConfig);
	}
	/**
	* Load all discovered extensions
	*/
	async loadAllExtensions(enabledIds) {
		const extensions = this.discoverExtensions();
		for (const extension of extensions) {
			if (enabledIds && !enabledIds.includes(extension.id)) continue;
			const config = {
				id: extension.id,
				name: extension.manifest.name || extension.id,
				source: extension.source,
				path: extension.path,
				enabled: true,
				config: {}
			};
			await this.loadExtension(config);
		}
	}
	async loadExtensions(configs) {
		for (const extensionConfig of configs) if (extensionConfig.enabled) await this.loadExtension(extensionConfig);
	}
	async loadExtension(config) {
		try {
			const cacheKey = this.cache.buildKey(this.options, [config.id]);
			const cached = this.cache.get(cacheKey);
			if (cached) {
				log.debug({ extensionId: config.id }, "Extension loaded from cache");
				return cached;
			}
			if (this.extensionInstances.has(config.id)) return this.extensionInstances.get(config.id);
			let extensionPath;
			if (isAbsolute(config.path)) extensionPath = config.path;
			else extensionPath = resolveExtensionPath(config.path, this.options) || resolveExtensionPath(config.id, this.options);
			if (!extensionPath) {
				log.error({
					extensionId: config.id,
					path: config.path
				}, `Could not resolve extension path`);
				this.diagnostics.error(config.id, `Could not resolve extension path: ${config.path}`);
				return null;
			}
			log.debug({
				extensionId: config.id,
				extensionPath
			}, "Resolved extension path");
			const source = config.source || "bundled";
			const safetyResult = checkExtensionPathSafety(extensionPath, extensionPath, source);
			if (!safetyResult.safe) {
				logSecurityIssue(config.id, safetyResult);
				if (this.securityConfig.checkPermissions && source !== "bundled") {
					if (!isExtensionAllowed(config.id, this.securityConfig)) {
						this.diagnostics.error(config.id, `Extension not allowed: ${safetyResult.detail}`);
						log.error({
							extensionId: config.id,
							reason: safetyResult.reason
						}, "Extension blocked by security policy");
						return null;
					}
				}
			}
			provenanceTracker.track(config.id, source);
			const manifest = this.loadManifest(extensionPath);
			if (!manifest) {
				log.error({
					extensionId: config.id,
					extensionPath
				}, `Failed to load manifest for extension`);
				this.diagnostics.error(config.id, `Failed to load manifest`);
				return null;
			}
			if (manifest.engines?.xopc) {
				const range = manifest.engines.xopc;
				const engineResult = checkEngineCompatibility(PACKAGE_VERSION, range);
				if (engineResult.parseWarning) log.warn({
					extensionId: config.id,
					range,
					reason: engineResult.reason
				}, "Engine range parse warning — loading anyway");
				else if (!engineResult.compatible) {
					log.warn({
						extensionId: config.id,
						range,
						currentVersion: PACKAGE_VERSION
					}, "Extension engine requirement not met (engines.xopc) — skipping load");
					this.diagnostics.error(config.id, engineResult.reason ?? "Incompatible xopc version (engines.xopc)");
					return null;
				}
			}
			if (manifest.configSchema) try {
				const schema = manifest.configSchema;
				const extensionConfig = config.config;
				if (schema.type === "object" && schema.properties) {
					const props = schema.properties;
					const required = schema.required || [];
					for (const field of required) if (extensionConfig[field] === void 0) {
						log.error({
							extensionId: config.id,
							field
						}, "Extension config validation failed: missing required field");
						return null;
					}
					for (const [key, value] of Object.entries(extensionConfig)) {
						const propSchema = props[key];
						if (propSchema) {
							if (propSchema.type && !this.validateType(value, propSchema.type)) {
								log.error({
									extensionId: config.id,
									field: key,
									expected: propSchema.type,
									actual: typeof value
								}, "Extension config validation failed: type mismatch");
								return null;
							}
						}
					}
				}
				log.debug({ extensionId: config.id }, "Extension config validated");
			} catch (err) {
				log.warn({
					err,
					extensionId: config.id
				}, "Config schema validation skipped");
			}
			const extensionDir = dirname(extensionPath);
			const api = this.createExtensionApi(manifest, config, extensionDir);
			const module = await this.loadModule(extensionPath, manifest);
			if (!module) {
				log.error({ extensionId: config.id }, `Failed to load module for extension`);
				this.diagnostics.error(config.id, `Failed to load module`);
				return null;
			}
			if (!this.claimExtensionSlots(config.id, manifest)) log.warn({ extensionId: config.id }, "Failed to claim required slots");
			await this.initializeExtension(module, api, manifest);
			validateSpeechProviderContracts({
				extensionId: config.id,
				manifest,
				registeredProviderIds: api.getRegisteredSpeechProviderIds(),
				logger: api.logger
			});
			validateMediaUnderstandingProviderContracts({
				extensionId: config.id,
				manifest,
				registeredProviderIds: api.getRegisteredMediaUnderstandingProviderIds(),
				logger: api.logger
			});
			this.registry.addExtension({
				id: config.id,
				name: manifest.name,
				version: manifest.version,
				path: extensionPath,
				module,
				config: config.config,
				enabled: true,
				source: config.source
			});
			const extensionTools = api._getTools();
			for (const tool of extensionTools.values()) this.registry.addTool(tool);
			this.extensionInstances.set(config.id, api);
			this.cache.set(cacheKey, api);
			this.diagnostics.info(config.id, `Loaded extension: ${manifest.name}`);
			log.info({
				name: manifest.name,
				id: manifest.id,
				source: config.source
			}, `Loaded extension`);
			return api;
		} catch (error) {
			log.error({
				err: error,
				extensionId: config.id
			}, `Error loading extension`);
			this.diagnostics.error(config.id, `Error loading extension: ${error}`);
			return null;
		}
	}
	/**
	*  Claim extension slots based on manifest kind
	* Respects configured preferred plugin from config.extensions.slots
	*/
	claimExtensionSlots(extensionId, manifest) {
		const slotKey = {
			"memory": "memory",
			"tts": "tts",
			"speech-provider": "tts",
			"image-generation": "imageGeneration",
			"imageGeneration": "imageGeneration",
			"web-search": "webSearch",
			"webSearch": "webSearch"
		}[manifest.kind];
		if (!slotKey) return true;
		const preferredPlugin = this.slotsConfig[slotKey];
		if (preferredPlugin && preferredPlugin !== extensionId) {
			log.info({
				extensionId,
				slotKey,
				preferredPlugin
			}, `Skipping slot claim: slot "${slotKey}" is reserved for "${preferredPlugin}"`);
			this.diagnostics.info(extensionId, `Slot "${slotKey}" is reserved for "${preferredPlugin}", skipping claim`);
			return false;
		}
		const claimed = this.slotRegistry.claim(slotKey, extensionId, null);
		if (!claimed) this.diagnostics.warn(extensionId, `Slot "${slotKey}" already claimed by another extension`);
		return claimed;
	}
	loadManifest(extensionPath) {
		const manifestPath = join(extensionPath, EXTENSION_MANIFEST_FILE);
		if (existsSync(manifestPath)) try {
			const raw = JSON.parse(readFileSync(manifestPath, "utf-8"));
			if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
				log.error({ manifestPath }, "Manifest root must be a JSON object");
				return null;
			}
			return normalizeExtensionManifest(raw);
		} catch (error) {
			log.error({
				err: error,
				manifestPath
			}, `Failed to parse manifest`);
			return null;
		}
		const packagePath = join(extensionPath, "package.json");
		if (existsSync(packagePath)) try {
			const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
			if (packageJson.xopc?.extension) {
				const xopcConfig = packageJson.xopc;
				return normalizeExtensionManifest({
					id: xopcConfig.id || packageJson.name,
					name: xopcConfig.name || packageJson.name,
					description: xopcConfig.description || packageJson.description,
					version: xopcConfig.version || packageJson.version || "1.0.0",
					kind: xopcConfig.kind || "utility",
					main: xopcConfig.main || packageJson.main || "index.js",
					configSchema: xopcConfig.configSchema
				});
			}
			if (packageJson.name?.startsWith("xopc-extension-")) return normalizeExtensionManifest({
				id: packageJson.name.replace("xopc-extension-", ""),
				name: packageJson.name,
				description: packageJson.description,
				version: packageJson.version || "1.0.0",
				kind: "utility",
				main: packageJson.main || "index.js"
			});
		} catch (error) {
			log.error({
				err: error,
				packagePath
			}, `Failed to parse package.json`);
		}
		return null;
	}
	/**
	* Build candidate extension entry paths. After `tsdown`, manifests may still
	* point at `*.ts` while the shipped tree only contains the emitted `*.js`.
	*/
	moduleEntryCandidates(manifest) {
		const raw = [
			manifest.main,
			"index.ts",
			"index.js",
			"extension.ts",
			"extension.js"
		].filter(Boolean);
		const out = [];
		for (const entry of raw) {
			out.push(entry);
			if (entry.endsWith(".tsx")) out.push(`${entry.slice(0, -4)}.js`);
			else if (entry.endsWith(".ts")) out.push(`${entry.slice(0, -3)}.js`);
			else if (entry.endsWith(".mts")) out.push(`${entry.slice(0, -4)}.mjs`);
			else if (entry.endsWith(".cts")) out.push(`${entry.slice(0, -4)}.cjs`);
		}
		return [...new Set(out)];
	}
	async loadModule(extensionPath, manifest) {
		const entryPoints = this.moduleEntryCandidates(manifest);
		for (const entry of entryPoints) {
			const fullPath = isAbsolute(entry) ? entry : join(extensionPath, entry);
			if (existsSync(fullPath)) try {
				if (/\.(js|mjs|cjs)$/.test(fullPath)) {
					const mod = createRequire(import.meta.url)(fullPath);
					return mod.default || mod;
				}
				const mod = this.jiti(fullPath);
				return mod.default || mod;
			} catch (error) {
				log.warn({
					err: error,
					path: fullPath
				}, `Failed to load module`);
			}
		}
		return null;
	}
	createExtensionApi(manifest, resolved, extensionDir) {
		const logger = createExtensionLogger(`[${manifest.id}]`);
		const resolvePath = createPathResolver(extensionDir, this.options.workspaceDir || "");
		const appConfig = this._appConfig ?? {};
		const extensionOnly = resolved.config ?? {};
		const runtime = this._appConfig && this._runtimeContext ? {
			config: this._appConfig,
			bus: this._runtimeContext.bus,
			log: logger,
			sessionManager: this._runtimeContext.sessionManager,
			scheduleWebchatContinuation: this._runtimeContext.scheduleWebchatContinuation
		} : this._appConfig ? {
			config: this._appConfig,
			log: logger
		} : void 0;
		const api = new ExtensionApiImpl(manifest.id, manifest.name, manifest.version, extensionDir, appConfig, extensionOnly, logger, resolvePath, this.registry, runtime);
		if (manifest.reload?.configPrefixes?.length) api._setReloadConfigPrefixes(manifest.reload.configPrefixes);
		const manifestCommands = manifest.ui?.contributions?.commands;
		if (manifestCommands?.length) api._setManifestCommands(manifestCommands);
		return api;
	}
	async initializeExtension(module, api, _manifest) {
		if (typeof module === "function") await module(api);
		else if (typeof module === "object" && module.register) await module.register(api);
	}
	async startServices() {
		const services = Array.from(this.registry.services.values());
		for (const service of services) {
			const serviceLog = createServiceLogger(service.id);
			try {
				await service.start?.();
				serviceLog.info(`Started service`);
			} catch (error) {
				serviceLog.error({ err: error }, `Failed to start service`);
			}
		}
	}
	async stopServices() {
		const services = Array.from(this.registry.services.values()).reverse();
		for (const service of services) if (service.stop) {
			const serviceLog = createServiceLogger(service.id);
			try {
				await service.stop();
				serviceLog.info(`Stopped service`);
			} catch (error) {
				serviceLog.error({ err: error }, `Failed to stop service`);
			}
		}
	}
	/**
	* Basic type validation for config values
	*/
	validateType(value, expectedType) {
		if (expectedType === "string") return typeof value === "string";
		if (expectedType === "number" || expectedType === "integer") return typeof value === "number";
		if (expectedType === "boolean") return typeof value === "boolean";
		if (expectedType === "array") return Array.isArray(value);
		if (expectedType === "object") return typeof value === "object" && value !== null && !Array.isArray(value);
		return true;
	}
};
function resolveExtensionPath(id, _options) {
	const c = loadConfig();
	const workspacePath = join(resolveWorkspaceExtensionsDir(c, resolveDefaultAgentId(c)), id);
	if (existsSync(workspacePath)) return workspacePath;
	const globalPath = join(resolveExtensionsDir(), id);
	if (existsSync(globalPath)) return globalPath;
	const bundledDir = resolveBundledExtensionsDir();
	if (bundledDir) {
		const bundledPath = join(bundledDir, id);
		if (existsSync(bundledPath)) return bundledPath;
	}
	try {
		return dirname(createRequire(import.meta.url).resolve(id));
	} catch {
		return null;
	}
}
function normalizeExtensionConfig(rawConfig) {
	const extensions = [];
	const enabled = rawConfig.enabled || [];
	const disabled = rawConfig.disabled || [];
	for (const id of enabled) {
		const config = rawConfig[id] || {};
		extensions.push({
			id,
			name: id,
			source: "config",
			path: id,
			enabled: true,
			config
		});
	}
	for (const id of disabled) if (!extensions.find((p) => p.id === id)) {
		const config = rawConfig[id] || {};
		extensions.push({
			id,
			name: id,
			source: "config",
			path: id,
			enabled: false,
			config
		});
	}
	return extensions;
}
//#endregion
export { ExtensionLoader, ExtensionRegistryImpl, areExtensionsGloballyDisabled, normalizeExtensionConfig, resolveExtensionPath };

//# sourceMappingURL=loader.js.map