import { t as asNullableRecord } from "./record-coerce-DVQGe6I0.js";
import { a as hasLegacyAccountStreamingAliases, i as stripRetiredChannelKeys, n as normalizeChannelAccounts } from "./channel-doctor-helpers-DgwV6wzV.js";
import { t as defineKeyMoveMigration } from "./runtime-doctor-migrations-M02m0S_a.js";
import { t as defineChannelAliasMigration } from "./channel-alias-migration-2d6kaHal.js";
import "./src-DQHRnEuc.js";
/** Count alphanumeric words in a configured activation name. */
function realtimeVoiceActivationNameWordCount(value) {
	return Array.from(value.matchAll(/[a-z0-9]+/gi)).length;
}
/** Extract the supported leading activation-name prefix from a longer phrase. */
function normalizeRealtimeVoiceActivationNamePrefix(value, maxWords = 2) {
	const words = Array.from(value.matchAll(/[a-z0-9]+/gi), (match) => match[0]);
	if (words.length === 0) return;
	return words.slice(0, maxWords).join(" ");
}
/** Validate the configured activation name length bound. */
function isSupportedRealtimeVoiceActivationName(value, maxWords = 2) {
	const wordCount = realtimeVoiceActivationNameWordCount(value);
	return wordCount >= 1 && wordCount <= maxWords;
}
//#endregion
//#region extensions/discord/src/doctor-contract.ts
const LEGACY_TTS_PROVIDER_KEYS = [
	"openai",
	"elevenlabs",
	"microsoft",
	"edge"
];
const RETIRED_TUNING_KEYS = /* @__PURE__ */ new Set([
	"gatewayInfoTimeoutMs",
	"gatewayReadyTimeoutMs",
	"gatewayRuntimeReadyTimeoutMs",
	"eventQueue",
	"retry"
]);
const streamingAliasMigration = defineChannelAliasMigration({
	channelId: "discord",
	streaming: {
		defaultMode: "off",
		includePreviewChunk: true
	},
	accountStreamingReplacesRoot: true,
	dm: {
		root: true,
		accounts: true
	},
	normalizeAccountExtra: ({ account, pathPrefix, changes }) => {
		const accountVoice = asNullableRecord(account.voice);
		if (!accountVoice || !migrateLegacyTtsConfig(asNullableRecord(accountVoice.tts), `${pathPrefix}.voice.tts`, changes)) return {
			entry: account,
			changed: false
		};
		return {
			entry: {
				...account,
				voice: accountVoice
			},
			changed: true
		};
	}
});
function hasLegacyTtsProviderKeys(value) {
	const tts = asNullableRecord(value);
	if (!tts) return false;
	return LEGACY_TTS_PROVIDER_KEYS.some((key) => Object.hasOwn(tts, key));
}
const guildChannelAllowMigration = defineKeyMoveMigration({
	scope: [
		"guilds",
		"*",
		"channels",
		"*"
	],
	from: ["allow"],
	to: ["enabled"]
});
function hasLegacyDiscordGuildChannelAgentId(value) {
	const guilds = asNullableRecord(asNullableRecord(value)?.guilds);
	if (!guilds) return false;
	return Object.values(guilds).some((guildValue) => {
		const channels = asNullableRecord(asNullableRecord(guildValue)?.channels);
		if (!channels) return false;
		return Object.values(channels).some((channel) => Object.hasOwn(asNullableRecord(channel) ?? {}, "agentId"));
	});
}
function hasUnsupportedRealtimeWakeNamesInVoice(value) {
	const wakeNames = asNullableRecord(asNullableRecord(value)?.realtime)?.wakeNames;
	return Array.isArray(wakeNames) ? wakeNames.length === 0 || wakeNames.some((wakeName) => typeof wakeName === "string" && !isSupportedRealtimeVoiceActivationName(wakeName)) : false;
}
function hasUnsupportedDiscordRealtimeWakeNames(value) {
	const entry = asNullableRecord(value);
	if (!entry) return false;
	return hasUnsupportedRealtimeWakeNamesInVoice(entry.voice);
}
function mergeMissing(target, source) {
	for (const [key, value] of Object.entries(source)) {
		if (value === void 0) continue;
		const existing = target[key];
		if (existing === void 0) {
			target[key] = value;
			continue;
		}
		if (existing && typeof existing === "object" && !Array.isArray(existing) && value && typeof value === "object" && !Array.isArray(value)) mergeMissing(existing, value);
	}
}
function getOrCreateTtsProviders(tts) {
	const providers = asNullableRecord(tts.providers) ?? {};
	tts.providers = providers;
	return providers;
}
function mergeLegacyTtsProviderConfig(tts, legacyKey, providerId) {
	const legacyValue = asNullableRecord(tts[legacyKey]);
	if (!legacyValue) return false;
	const providers = getOrCreateTtsProviders(tts);
	const existing = asNullableRecord(providers[providerId]) ?? {};
	const merged = structuredClone(existing);
	mergeMissing(merged, legacyValue);
	providers[providerId] = merged;
	delete tts[legacyKey];
	return true;
}
function migrateLegacyTtsConfig(tts, pathLabel, changes) {
	if (!tts) return false;
	let changed = false;
	if (mergeLegacyTtsProviderConfig(tts, "openai", "openai")) {
		changes.push(`Moved ${pathLabel}.openai → ${pathLabel}.providers.openai.`);
		changed = true;
	}
	if (mergeLegacyTtsProviderConfig(tts, "elevenlabs", "elevenlabs")) {
		changes.push(`Moved ${pathLabel}.elevenlabs → ${pathLabel}.providers.elevenlabs.`);
		changed = true;
	}
	if (mergeLegacyTtsProviderConfig(tts, "microsoft", "microsoft")) {
		changes.push(`Moved ${pathLabel}.microsoft → ${pathLabel}.providers.microsoft.`);
		changed = true;
	}
	if (mergeLegacyTtsProviderConfig(tts, "edge", "microsoft")) {
		changes.push(`Moved ${pathLabel}.edge → ${pathLabel}.providers.microsoft.`);
		changed = true;
	}
	return changed;
}
function normalizeUnsupportedRealtimeWakeNames(entry, pathPrefix, changes) {
	const voice = asNullableRecord(entry.voice);
	const realtime = asNullableRecord(voice?.realtime);
	const wakeNames = realtime?.wakeNames;
	if (!voice || !realtime || !Array.isArray(wakeNames)) return {
		entry,
		changed: false
	};
	if (wakeNames.length === 0) {
		const nextRealtime = { ...realtime };
		delete nextRealtime.wakeNames;
		changes.push(`Removed empty ${pathPrefix}.voice.realtime.wakeNames; unset wake names use the default agent/OpenClaw fallback.`);
		return {
			entry: {
				...entry,
				voice: {
					...voice,
					realtime: nextRealtime
				}
			},
			changed: true
		};
	}
	let normalized = 0;
	let removed = 0;
	const nextWakeNames = wakeNames.flatMap((wakeName) => {
		if (typeof wakeName !== "string" || isSupportedRealtimeVoiceActivationName(wakeName)) return [wakeName];
		const nextWakeName = normalizeRealtimeVoiceActivationNamePrefix(wakeName);
		if (!nextWakeName) {
			removed += 1;
			return [];
		}
		normalized += 1;
		return [nextWakeName];
	});
	if (normalized === 0 && removed === 0) return {
		entry,
		changed: false
	};
	const dedupedWakeNames = Array.from(new Set(nextWakeNames));
	const nextRealtime = { ...realtime };
	if (dedupedWakeNames.length > 0) nextRealtime.wakeNames = dedupedWakeNames;
	else delete nextRealtime.wakeNames;
	if (normalized > 0) changes.push(`Shortened ${normalized} unsupported ${pathPrefix}.voice.realtime.wakeNames entries to one or two words.`);
	if (removed > 0) changes.push(`Removed ${removed} unsupported ${pathPrefix}.voice.realtime.wakeNames entries with no usable words.`);
	return {
		entry: {
			...entry,
			voice: {
				...voice,
				realtime: nextRealtime
			}
		},
		changed: true
	};
}
function isDiscordChannelAgentBinding(value, match) {
	const binding = asNullableRecord(value);
	const bindingMatch = asNullableRecord(binding?.match);
	const peer = asNullableRecord(bindingMatch?.peer);
	if (!binding || !bindingMatch || !peer) return false;
	return bindingMatch.channel === "discord" && bindingMatch.guildId === match.guildId && (match.accountId === void 0 || bindingMatch.accountId === match.accountId) && peer.kind === "channel" && peer.id === match.channelId;
}
function normalizeDiscordGuildChannelAgentIds(params) {
	const guilds = asNullableRecord(params.entry.guilds);
	if (!guilds) return {
		entry: params.entry,
		changed: false
	};
	const existingBindings = Array.isArray(params.cfg.bindings) ? params.cfg.bindings : [];
	let changed = false;
	const nextGuilds = { ...guilds };
	for (const [guildId, guildValue] of Object.entries(guilds)) {
		const guild = asNullableRecord(guildValue);
		const channels = asNullableRecord(guild?.channels);
		if (!guild || !channels) continue;
		let channelsChanged = false;
		const nextChannels = { ...channels };
		for (const [channelId, channelValue] of Object.entries(channels)) {
			const channel = asNullableRecord(channelValue);
			if (!channel || !Object.hasOwn(channel, "agentId")) continue;
			const nextChannel = { ...channel };
			const rawAgentId = nextChannel.agentId;
			delete nextChannel.agentId;
			nextChannels[channelId] = nextChannel;
			channelsChanged = true;
			const path = `${params.pathPrefix}.guilds.${guildId}.channels.${channelId}.agentId`;
			const agentId = typeof rawAgentId === "string" ? rawAgentId.trim() : "";
			if (!agentId) {
				params.changes.push(`Removed ${path}; configure top-level bindings[] for per-channel Discord agent routing.`);
				continue;
			}
			const match = {
				accountId: params.accountId,
				guildId,
				channelId
			};
			if (existingBindings.find((binding) => isDiscordChannelAgentBinding(binding, match))) {
				params.changes.push(`Removed ${path}; a matching top-level bindings[] route already exists for Discord channel ${channelId}.`);
				continue;
			}
			const bindingMatch = {
				channel: "discord",
				guildId,
				peer: {
					kind: "channel",
					id: channelId
				}
			};
			if (params.accountId) bindingMatch.accountId = params.accountId;
			params.bindingsToAdd.push({
				agentId,
				match: bindingMatch
			});
			params.changes.push(`Moved ${path} → top-level bindings[] route for Discord channel ${channelId}.`);
		}
		if (!channelsChanged) continue;
		nextGuilds[guildId] = {
			...guild,
			channels: nextChannels
		};
		changed = true;
	}
	return changed ? {
		entry: {
			...params.entry,
			guilds: nextGuilds
		},
		changed: true
	} : {
		entry: params.entry,
		changed: false
	};
}
const legacyConfigRules = [
	{
		path: [
			"channels",
			"discord",
			"voice",
			"tts"
		],
		message: "channels.discord.voice.tts.<provider> keys (openai/elevenlabs/microsoft/edge) are legacy; use channels.discord.voice.tts.providers.<provider>. Run \"openclaw doctor --fix\".",
		match: hasLegacyTtsProviderKeys
	},
	{
		path: [
			"channels",
			"discord",
			"accounts"
		],
		message: "channels.discord.accounts.<id>.voice.tts.<provider> keys (openai/elevenlabs/microsoft/edge) are legacy; use channels.discord.accounts.<id>.voice.tts.providers.<provider>. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyAccountStreamingAliases(value, (accountValue) => {
			return hasLegacyTtsProviderKeys(asNullableRecord(asNullableRecord(accountValue)?.voice)?.tts);
		})
	},
	{
		path: ["channels", "discord"],
		message: "channels.discord.guilds.<id>.channels.<id>.allow is legacy; use channels.discord.guilds.<id>.channels.<id>.enabled instead. Run \"openclaw doctor --fix\".",
		match: guildChannelAllowMigration.hasLegacy
	},
	{
		path: [
			"channels",
			"discord",
			"accounts"
		],
		message: "channels.discord.accounts.<id>.guilds.<id>.channels.<id>.allow is legacy; use channels.discord.accounts.<id>.guilds.<id>.channels.<id>.enabled instead. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyAccountStreamingAliases(value, guildChannelAllowMigration.hasLegacy)
	},
	{
		path: ["channels", "discord"],
		message: "channels.discord.guilds.<id>.channels.<id>.agentId is legacy; use top-level bindings[] for per-channel Discord agent routing. Run \"openclaw doctor --fix\".",
		match: hasLegacyDiscordGuildChannelAgentId
	},
	{
		path: [
			"channels",
			"discord",
			"accounts"
		],
		message: "channels.discord.accounts.<id>.guilds.<id>.channels.<id>.agentId is legacy; use top-level bindings[] with match.accountId for per-channel Discord agent routing. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyAccountStreamingAliases(value, hasLegacyDiscordGuildChannelAgentId)
	},
	{
		path: ["channels", "discord"],
		message: "channels.discord.voice.realtime.wakeNames entries longer than two words are unsupported; use one- or two-word activation names. Run \"openclaw doctor --fix\".",
		match: hasUnsupportedDiscordRealtimeWakeNames
	},
	{
		path: [
			"channels",
			"discord",
			"accounts"
		],
		message: "channels.discord.accounts.<id>.voice.realtime.wakeNames entries longer than two words are unsupported; use one- or two-word activation names. Run \"openclaw doctor --fix\".",
		match: (value) => hasLegacyAccountStreamingAliases(value, hasUnsupportedDiscordRealtimeWakeNames)
	},
	...streamingAliasMigration.legacyConfigRules
];
function normalizeCompatibilityConfig({ cfg }) {
	const changes = [];
	const bindingsToAdd = [];
	const tuningKnobs = stripRetiredChannelKeys({
		cfg: streamingAliasMigration.normalizeChannelConfig({
			cfg,
			changes
		}).config,
		channelId: "discord",
		keys: RETIRED_TUNING_KEYS,
		scope: "root-and-accounts"
	});
	const rawEntry = asNullableRecord(tuningKnobs.config.channels?.discord);
	if (!rawEntry) return {
		config: cfg,
		changes: []
	};
	let updated = rawEntry;
	let changed = tuningKnobs.config !== cfg;
	if (tuningKnobs.changed) changes.push("Removed retired Discord tuning knobs.");
	const guildAliases = guildChannelAllowMigration.normalize({
		entry: updated,
		pathPrefix: "channels.discord",
		changes
	});
	updated = guildAliases.entry;
	changed = changed || guildAliases.changed;
	const channelAgentIds = normalizeDiscordGuildChannelAgentIds({
		cfg,
		entry: updated,
		pathPrefix: "channels.discord",
		changes,
		bindingsToAdd
	});
	updated = channelAgentIds.entry;
	changed = changed || channelAgentIds.changed;
	const accounts = normalizeChannelAccounts({
		entry: updated,
		pathPrefix: "channels.discord",
		changes,
		normalizeAccount: ({ account, accountId, pathPrefix, changes: accountChanges }) => {
			const guilds = guildChannelAllowMigration.normalize({
				entry: account,
				pathPrefix,
				changes: accountChanges
			});
			const agentIds = normalizeDiscordGuildChannelAgentIds({
				cfg,
				entry: guilds.entry,
				pathPrefix,
				accountId,
				changes: accountChanges,
				bindingsToAdd
			});
			const wakeNames = normalizeUnsupportedRealtimeWakeNames(agentIds.entry, pathPrefix, accountChanges);
			return {
				entry: wakeNames.entry,
				changed: guilds.changed || agentIds.changed || wakeNames.changed
			};
		}
	});
	updated = accounts.entry;
	changed = changed || accounts.changed;
	const voice = asNullableRecord(updated.voice);
	if (voice && migrateLegacyTtsConfig(asNullableRecord(voice.tts), "channels.discord.voice.tts", changes)) {
		updated = {
			...updated,
			voice
		};
		changed = true;
	}
	const normalizedWakeNames = normalizeUnsupportedRealtimeWakeNames(updated, "channels.discord", changes);
	updated = normalizedWakeNames.entry;
	changed = changed || normalizedWakeNames.changed;
	if (!changed) return {
		config: cfg,
		changes: []
	};
	return {
		config: {
			...tuningKnobs.config,
			channels: {
				...tuningKnobs.config.channels,
				discord: updated
			},
			bindings: bindingsToAdd.length > 0 ? [...cfg.bindings ?? [], ...bindingsToAdd] : cfg.bindings
		},
		changes
	};
}
//#endregion
export { legacyConfigRules, normalizeCompatibilityConfig };
