import { t as asNullableRecord } from "./record-coerce-DVQGe6I0.js";
import "./boolean-DX6XJaw5.js";
import "node:fs/promises";
//#region src/plugin-sdk/runtime-doctor-migrations.ts
function readKeyMovePath(entry, path, own = true) {
	let current = entry;
	for (const segment of path.slice(0, -1)) {
		const next = asNullableRecord(current[segment]);
		if (!next) return null;
		current = next;
	}
	const key = path.at(-1);
	return key && (own ? Object.hasOwn(current, key) : key in current) ? { value: current[key] } : null;
}
function setKeyMovePath(entry, path, value) {
	const [key, ...rest] = path;
	if (!key) return entry;
	if (rest.length === 0) return {
		...entry,
		[key]: value
	};
	return {
		...entry,
		[key]: setKeyMovePath(asNullableRecord(entry[key]) ?? {}, rest, value)
	};
}
function deleteKeyMovePath(entry, path, pruneEmpty) {
	const [key, ...rest] = path;
	if (!key) return entry;
	const next = { ...entry };
	if (rest.length === 0) {
		delete next[key];
		return next;
	}
	const child = asNullableRecord(entry[key]);
	if (!child) return entry;
	const updatedChild = deleteKeyMovePath(child, rest, pruneEmpty);
	if (pruneEmpty && Object.keys(updatedChild).length === 0) delete next[key];
	else next[key] = updatedChild;
	return next;
}
/** Defines an immutable legacy-key move across fixed or `*`-mapped object paths. */
function defineKeyMoveMigration(params) {
	const visitScopes = (entry, scope, visit, scopePath = []) => {
		const [segment, ...rest] = scope;
		if (!segment) return visit(entry, scopePath);
		if (segment === "*") return Object.entries(entry).some(([key, value]) => {
			const child = asNullableRecord(value);
			return child ? visitScopes(child, rest, visit, [...scopePath, key]) : false;
		});
		const child = asNullableRecord(entry[segment]);
		return child ? visitScopes(child, rest, visit, [...scopePath, segment]) : false;
	};
	const hasLegacy = (value) => {
		const entry = asNullableRecord(value);
		return entry ? visitScopes(entry, params.scope ?? [], (scopeEntry) => {
			const source = readKeyMovePath(scopeEntry, params.from, params.sourceOwn);
			return Boolean(source && (params.match?.(source.value) ?? true));
		}) : false;
	};
	const normalizeScope = (scopeEntry, scopePath, pathPrefix, changes) => {
		const source = readKeyMovePath(scopeEntry, params.from, params.sourceOwn);
		if (!source || !(params.match?.(source.value) ?? true)) return {
			entry: scopeEntry,
			changed: false
		};
		const target = readKeyMovePath(scopeEntry, params.to);
		const mapped = params.map ? params.map(source.value) : { value: source.value };
		const context = {
			sourcePath: [
				pathPrefix,
				...scopePath,
				...params.from
			].join("."),
			targetPath: [
				pathPrefix,
				...scopePath,
				...params.to
			].join("."),
			sourceValue: source.value,
			targetValue: target?.value,
			mappedValue: mapped?.value
		};
		const targetSet = params.targetIsSet?.(target?.value) ?? target?.value !== void 0;
		let updated = scopeEntry;
		if (targetSet) changes.push(params.existingMessage?.(context) ?? `Removed ${context.sourcePath} (${context.targetPath} already set).`);
		else if (mapped) {
			updated = setKeyMovePath(updated, params.to, mapped.value);
			changes.push(params.movedMessage?.(context) ?? `Moved ${context.sourcePath} → ${context.targetPath}.`);
		} else changes.push(params.invalidMessage?.(context) ?? `Removed invalid ${context.sourcePath} value.`);
		return {
			entry: deleteKeyMovePath(updated, params.from, params.pruneEmptySource ?? false),
			changed: true
		};
	};
	const normalizeScopes = (entry, scope, pathPrefix, changes, scopePath = []) => {
		const [segment, ...rest] = scope;
		if (!segment) return normalizeScope(entry, scopePath, pathPrefix, changes);
		let changed = false;
		const updated = { ...entry };
		const keys = segment === "*" ? Object.keys(entry) : [segment];
		for (const key of keys) {
			const child = asNullableRecord(entry[key]);
			if (!child) continue;
			const normalized = normalizeScopes(child, rest, pathPrefix, changes, [...scopePath, key]);
			if (normalized.changed) {
				updated[key] = normalized.entry;
				changed = true;
			}
		}
		return changed ? {
			entry: updated,
			changed: true
		} : {
			entry,
			changed: false
		};
	};
	return {
		hasLegacy,
		normalize: ({ entry, pathPrefix, changes }) => normalizeScopes(entry, params.scope ?? [], pathPrefix, changes)
	};
}
/**
* Defines the repair for channel config parked under `plugins.entries.<id>.config`.
* Channel plugins read `channels.<channelId>` only, but retired rich plugin-entry
* schemas let config UIs park values in the unread plugin-entry location.
*/
function defineStrayPluginEntryConfigMigration(params) {
	const { pluginId, channelId } = params;
	const entryConfigPath = `plugins.entries.${pluginId}.config`;
	const readStrayEntryConfig = (cfg) => {
		const config = asNullableRecord(asNullableRecord(asNullableRecord(asNullableRecord(cfg.plugins)?.entries)?.[pluginId])?.config);
		return config && Object.keys(config).length > 0 ? config : null;
	};
	return {
		legacyConfigRule: {
			path: [
				"plugins",
				"entries",
				pluginId,
				"config"
			],
			message: `${entryConfigPath} is not read by the ${channelId} channel; run "openclaw doctor --fix" to move its keys to channels.${channelId}.`,
			match: (value) => {
				const record = asNullableRecord(value);
				return Boolean(record && Object.keys(record).length > 0);
			}
		},
		normalizeConfig: ({ cfg }) => {
			const stray = readStrayEntryConfig(cfg);
			if (!stray) return {
				config: cfg,
				changes: []
			};
			const next = structuredClone(cfg);
			const currentChannel = asNullableRecord(asNullableRecord(next.channels)?.[channelId]) ?? {};
			const staged = [];
			const dropped = [];
			const merged = { ...currentChannel };
			for (const [key, value] of Object.entries(stray)) if (Object.hasOwn(currentChannel, key)) dropped.push(key);
			else {
				merged[key] = value;
				staged.push(key);
			}
			if (!params.validateMergedChannelConfig(merged)) return {
				config: cfg,
				changes: []
			};
			const channels = asNullableRecord(next.channels) ?? {};
			channels[channelId] = merged;
			next.channels = channels;
			const entry = asNullableRecord(asNullableRecord(asNullableRecord(next.plugins)?.entries)?.[pluginId]);
			if (entry) delete entry.config;
			return {
				config: next,
				changes: [...staged.map((key) => `Moved ${entryConfigPath}.${key} to channels.${channelId}.${key}.`), ...dropped.map((key) => `Removed ${entryConfigPath}.${key}; channels.${channelId}.${key} is authoritative.`)]
			};
		}
	};
}
//#endregion
export { defineStrayPluginEntryConfigMigration as n, defineKeyMoveMigration as t };
