/**
 * OpenClaw Auth Profiles Utility
 * Writes API keys to configured OpenClaw agent auth-profiles.json files
 * so the OpenClaw Gateway can load them for AI provider calls.
 *
 * All file I/O is asynchronous (fs/promises) to avoid blocking the
 * Electron main thread.  On Windows + NTFS + Defender the synchronous
 * equivalents could stall for 500 ms – 2 s+ per call, causing "Not
 * Responding" hangs.
 */
import { access, mkdir, readFile, readdir, writeFile } from 'fs/promises';
import { constants, readdirSync, readFileSync, existsSync } from 'fs';
import { dirname, join } from 'path';
import { homedir } from 'os';
import { listConfiguredAgentIds } from './agent-config';
import { getOpenClawResolvedDir } from './paths';
import {
  getProviderEnvVar,
  getProviderDefaultModel,
  getProviderConfig,
} from './provider-registry';
import {
  OPENCLAW_PROVIDER_KEY_MINIMAX,
  OPENCLAW_PROVIDER_KEY_MOONSHOT,
  OPENCLAW_PROVIDER_KEY_MOONSHOT_GLOBAL,
  isOAuthProviderType,
  isOpenClawOAuthPluginProviderKey,
} from './provider-keys';
import { normalizePiAiModelCost, type PiAiModelCostRates } from '../shared/pi-ai-model-cost';
import { withConfigLock } from './config-mutex';
import {
  OPENCLAW_API_PROTOCOLS,
  assertValidApiProtocol,
} from '../shared/providers/types';

const AUTH_STORE_VERSION = 1;
const AUTH_PROFILE_FILENAME = 'auth-profiles.json';
const LEGACY_MINIMAX_OAUTH_PLUGIN_ID = 'minimax-portal-auth';
const MERGED_MINIMAX_PLUGIN_ID = 'minimax';

interface BundledPluginManifest {
  id: string;
  enabledByDefault: boolean;
  providers: string[];
  legacyPluginIds: string[];
}

interface OAuthPluginRegistration {
  canonicalPluginId: string;
  stalePluginIds: string[];
}

interface MiniMaxPluginRegistration extends OAuthPluginRegistration {
  mergedPlugin: boolean;
}

let _bundledPluginManifestCache: BundledPluginManifest[] | null = null;
let _bundledPluginCache: {
  all: Set<string>;
  enabledByDefault: string[];
  manifestsById: Map<string, BundledPluginManifest>;
} | null = null;
let _miniMaxPluginRegistrationCache: MiniMaxPluginRegistration | null = null;

export function resetOpenClawPluginDiscoveryCaches(): void {
  _bundledPluginManifestCache = null;
  _bundledPluginCache = null;
  _miniMaxPluginRegistrationCache = null;
}

function getOpenClawExtensionsRoots(): string[] {
  const openClawDir = getOpenClawResolvedDir();
  return [
    join(openClawDir, 'dist', 'extensions'),
    join(openClawDir, 'extensions'),
  ];
}

function discoverBundledPluginManifests(): BundledPluginManifest[] {
  if (_bundledPluginManifestCache) return _bundledPluginManifestCache;

  const manifests = new Map<string, BundledPluginManifest>();

  for (const extensionsDir of getOpenClawExtensionsRoots()) {
    try {
      if (!existsSync(extensionsDir)) {
        continue;
      }

      for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
        if (!entry.isDirectory()) continue;

        const manifestPath = join(extensionsDir, entry.name, 'openclaw.plugin.json');
        if (!existsSync(manifestPath)) continue;

        try {
          const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
            id?: unknown;
            enabledByDefault?: unknown;
            providers?: unknown;
            legacyPluginIds?: unknown;
          };
          if (typeof parsed.id !== 'string' || !parsed.id.trim()) {
            continue;
          }

          const existing = manifests.get(parsed.id) ?? {
            id: parsed.id,
            enabledByDefault: false,
            providers: [],
            legacyPluginIds: [],
          };

          const providers = Array.isArray(parsed.providers)
            ? parsed.providers.filter((provider): provider is string => typeof provider === 'string' && provider.trim().length > 0)
            : [];
          const legacyPluginIds = Array.isArray(parsed.legacyPluginIds)
            ? parsed.legacyPluginIds.filter((pluginId): pluginId is string => typeof pluginId === 'string' && pluginId.trim().length > 0)
            : [];

          existing.enabledByDefault = existing.enabledByDefault || parsed.enabledByDefault === true;
          existing.providers = Array.from(new Set([...existing.providers, ...providers]));
          existing.legacyPluginIds = Array.from(new Set([...existing.legacyPluginIds, ...legacyPluginIds]));

          manifests.set(parsed.id, existing);
        } catch {
          // Malformed manifest — skip silently
        }
      }
    } catch {
      // Extension directory not found or unreadable — ignore
    }
  }

  _bundledPluginManifestCache = Array.from(manifests.values());
  return _bundledPluginManifestCache;
}

function resolveMiniMaxPluginRegistration(): MiniMaxPluginRegistration {
  if (_miniMaxPluginRegistrationCache) return _miniMaxPluginRegistrationCache;

  const manifests = discoverBundledPluginManifests();
  const mergedManifest = manifests.find((manifest) => (
    manifest.id === MERGED_MINIMAX_PLUGIN_ID
      && (
        manifest.providers.includes(OPENCLAW_PROVIDER_KEY_MINIMAX)
        || manifest.legacyPluginIds.includes(LEGACY_MINIMAX_OAUTH_PLUGIN_ID)
      )
  ));
  const legacyManifest = manifests.find((manifest) => manifest.id === LEGACY_MINIMAX_OAUTH_PLUGIN_ID);

  const canonicalPluginId = mergedManifest ? MERGED_MINIMAX_PLUGIN_ID : LEGACY_MINIMAX_OAUTH_PLUGIN_ID;
  const knownPluginIds = new Set<string>([
    LEGACY_MINIMAX_OAUTH_PLUGIN_ID,
    MERGED_MINIMAX_PLUGIN_ID,
  ]);

  for (const manifest of [mergedManifest, legacyManifest]) {
    if (!manifest) continue;
    knownPluginIds.add(manifest.id);
    for (const legacyPluginId of manifest.legacyPluginIds) {
      knownPluginIds.add(legacyPluginId);
    }
  }

  _miniMaxPluginRegistrationCache = {
    canonicalPluginId,
    stalePluginIds: Array.from(knownPluginIds).filter((pluginId) => pluginId !== canonicalPluginId),
    mergedPlugin: Boolean(mergedManifest),
  };
  return _miniMaxPluginRegistrationCache;
}

function getOAuthPluginRegistration(provider: string): OAuthPluginRegistration {
  if (provider === OPENCLAW_PROVIDER_KEY_MINIMAX) {
    return resolveMiniMaxPluginRegistration();
  }

  return {
    canonicalPluginId: `${provider}-auth`,
    stalePluginIds: [],
  };
}

function ensureOAuthPluginEnabled(config: Record<string, unknown>, provider: string): void {
  const { canonicalPluginId, stalePluginIds } = getOAuthPluginRegistration(provider);
  const plugins = isPlainRecord(config.plugins) ? config.plugins as Record<string, unknown> : {};
  const allow = Array.isArray(plugins.allow)
    ? (plugins.allow as unknown[]).filter((value): value is string => typeof value === 'string')
    : [];
  const pEntries = isPlainRecord(plugins.entries) ? plugins.entries as Record<string, Record<string, unknown>> : {};

  const nextAllow = allow.filter((pluginId) => !stalePluginIds.includes(pluginId));
  if (!nextAllow.includes(canonicalPluginId)) {
    nextAllow.push(canonicalPluginId);
  }

  for (const stalePluginId of stalePluginIds) {
    delete pEntries[stalePluginId];
  }

  pEntries[canonicalPluginId] = {
    ...(isPlainRecord(pEntries[canonicalPluginId]) ? pEntries[canonicalPluginId] : {}),
    enabled: true,
  };

  plugins.allow = nextAllow;
  plugins.entries = pEntries;
  config.plugins = plugins;
}

function removePluginRegistrations(
  config: Record<string, unknown>,
  pluginIds: string[],
): boolean {
  const uniquePluginIds = Array.from(new Set(pluginIds.filter(Boolean)));
  if (uniquePluginIds.length === 0 || !isPlainRecord(config.plugins)) {
    return false;
  }

  const plugins = config.plugins as Record<string, unknown>;
  let modified = false;

  if (Array.isArray(plugins.allow)) {
    const allow = (plugins.allow as unknown[]).filter((value): value is string => typeof value === 'string');
    const nextAllow = allow.filter((pluginId) => !uniquePluginIds.includes(pluginId));
    if (nextAllow.length !== allow.length) {
      modified = true;
      if (nextAllow.length > 0) {
        plugins.allow = nextAllow;
      } else {
        delete plugins.allow;
      }
    }
  }

  if (isPlainRecord(plugins.entries)) {
    const entries = plugins.entries as Record<string, unknown>;
    for (const pluginId of uniquePluginIds) {
      if (pluginId in entries) {
        delete entries[pluginId];
        modified = true;
      }
    }
    if (Object.keys(entries).length === 0) {
      delete plugins.entries;
    }
  }

  if (plugins.enabled === true) {
    const pluginKeysExcludingEnabled = Object.keys(plugins).filter((key) => key !== 'enabled');
    if (pluginKeysExcludingEnabled.length === 0) {
      delete plugins.enabled;
      modified = true;
    }
  }

  if (Object.keys(plugins).length === 0) {
    delete config.plugins;
    modified = true;
  }

  return modified;
}

// ── Helpers ──────────────────────────────────────────────────────

/** Non-throwing async existence check (replaces existsSync). */
async function fileExists(p: string): Promise<boolean> {
  try {
    await access(p, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

/** Ensure a directory exists (replaces mkdirSync). */
async function ensureDir(dir: string): Promise<void> {
  if (!(await fileExists(dir))) {
    await mkdir(dir, { recursive: true });
  }
}

/** Read a JSON file, returning `null` on any error. */
async function readJsonFile<T>(filePath: string): Promise<T | null> {
  try {
    if (!(await fileExists(filePath))) return null;
    const raw = await readFile(filePath, 'utf-8');
    return JSON.parse(raw) as T;
  } catch {
    return null;
  }
}

/** Write a JSON file, creating parent directories if needed. */
async function writeJsonFile(filePath: string, data: unknown): Promise<void> {
  await ensureDir(join(filePath, '..'));
  await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
}

// ── Types ────────────────────────────────────────────────────────

interface AuthProfileEntry {
  type: 'api_key';
  provider: string;
  key: string;
}

interface OAuthProfileEntry {
  type: 'oauth';
  provider: string;
  access: string;
  refresh: string;
  expires: number;
  email?: string;
  projectId?: string;
}

interface AuthProfilesStore {
  version: number;
  profiles: Record<string, AuthProfileEntry | OAuthProfileEntry>;
  order?: Record<string, string[]>;
  lastGood?: Record<string, string>;
}

function removeProfilesForProvider(store: AuthProfilesStore, provider: string): boolean {
  const removedProfileIds = new Set<string>();

  for (const [profileId, profile] of Object.entries(store.profiles)) {
    if (profile?.provider !== provider) {
      continue;
    }
    delete store.profiles[profileId];
    removedProfileIds.add(profileId);
  }

  if (removedProfileIds.size === 0) {
    return false;
  }

  if (store.order) {
    for (const [orderProvider, profileIds] of Object.entries(store.order)) {
      const nextProfileIds = profileIds.filter((profileId) => !removedProfileIds.has(profileId));
      if (nextProfileIds.length > 0) {
        store.order[orderProvider] = nextProfileIds;
      } else {
        delete store.order[orderProvider];
      }
    }
  }

  if (store.lastGood) {
    for (const [lastGoodProvider, profileId] of Object.entries(store.lastGood)) {
      if (removedProfileIds.has(profileId)) {
        delete store.lastGood[lastGoodProvider];
      }
    }
  }

  return true;
}

function removeProfileFromStore(
  store: AuthProfilesStore,
  profileId: string,
  expectedType?: AuthProfileEntry['type'] | OAuthProfileEntry['type'],
): boolean {
  const profile = store.profiles[profileId];
  let changed = false;
  const shouldCleanReferences = !profile || !expectedType || profile.type === expectedType;
  if (profile && (!expectedType || profile.type === expectedType)) {
    delete store.profiles[profileId];
    changed = true;
  }

  if (shouldCleanReferences && store.order) {
    for (const [orderProvider, profileIds] of Object.entries(store.order)) {
      const nextProfileIds = profileIds.filter((id) => id !== profileId);
      if (nextProfileIds.length !== profileIds.length) {
        changed = true;
      }
      if (nextProfileIds.length > 0) {
        store.order[orderProvider] = nextProfileIds;
      } else {
        delete store.order[orderProvider];
      }
    }
  }

  if (shouldCleanReferences && store.lastGood) {
    for (const [lastGoodProvider, lastGoodProfileId] of Object.entries(store.lastGood)) {
      if (lastGoodProfileId === profileId) {
        delete store.lastGood[lastGoodProvider];
        changed = true;
      }
    }
  }

  return changed;
}

// ── Auth Profiles I/O ────────────────────────────────────────────

function getAuthProfilesPath(agentId = 'main'): string {
  return join(homedir(), '.openclaw', 'agents', agentId, 'agent', AUTH_PROFILE_FILENAME);
}

async function readAuthProfiles(agentId = 'main'): Promise<AuthProfilesStore> {
  const filePath = getAuthProfilesPath(agentId);
  try {
    const data = await readJsonFile<AuthProfilesStore>(filePath);
    if (data?.version && data.profiles && typeof data.profiles === 'object') {
      return data;
    }
  } catch (error) {
    console.warn('Failed to read auth-profiles.json, creating fresh store:', error);
  }
  return { version: AUTH_STORE_VERSION, profiles: {} };
}

async function writeAuthProfiles(store: AuthProfilesStore, agentId = 'main'): Promise<void> {
  await writeJsonFile(getAuthProfilesPath(agentId), store);
}

function getApiKeyFromAuthProfilesStore(
  store: AuthProfilesStore,
  provider: string,
): string | null {
  const profileIds = [
    store.lastGood?.[provider],
    ...(store.order?.[provider] ?? []),
    `${provider}:default`,
  ].filter((id): id is string => Boolean(id));

  for (const profileId of profileIds) {
    const profile = store.profiles[profileId];
    if (profile?.type === 'api_key' && profile.provider === provider && profile.key) {
      return profile.key;
    }
  }

  for (const profile of Object.values(store.profiles)) {
    if (profile.type === 'api_key' && profile.provider === provider && profile.key) {
      return profile.key;
    }
  }

  return null;
}

/**
 * Read the API key OpenClaw will use for a runtime provider key.
 *
 * This intentionally reads auth-profiles.json rather than ClawX's provider
 * cache, so UI status can reflect providers imported or preserved by the
 * OpenClaw runtime across overwrite installs.
 */
export async function getProviderApiKeyFromOpenClaw(
  provider: string,
  agentId?: string,
): Promise<string | null> {
  const agentIds = agentId ? [agentId] : await discoverAgentIds();
  if (agentIds.length === 0) agentIds.push('main');

  for (const id of agentIds) {
    const store = await readAuthProfiles(id);
    const apiKey = getApiKeyFromAuthProfilesStore(store, provider);
    if (apiKey) {
      return apiKey;
    }
  }

  return null;
}

// ── Agent Discovery ──────────────────────────────────────────────

async function discoverAgentIds(): Promise<string[]> {
  const agentsDir = join(homedir(), '.openclaw', 'agents');
  try {
    if (!(await fileExists(agentsDir))) return ['main'];
    return await listConfiguredAgentIds();
  } catch {
    return ['main'];
  }
}

// ── OpenClaw Config Helpers ──────────────────────────────────────

const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
const FEISHU_PLUGIN_ID_CANDIDATES = ['openclaw-lark', 'feishu-openclaw-plugin'] as const;
const VALID_COMPACTION_MODES = new Set(['default', 'safeguard']);
const BUILTIN_CHANNEL_IDS = new Set([
  'discord',
  'telegram',
  'whatsapp',
  'slack',
  'signal',
  'imessage',
  'matrix',
  'line',
  'msteams',
  'googlechat',
  'mattermost',
  'qqbot',
]);
const OPTIONAL_PROVIDER_LIKE_BUNDLED_PLUGIN_IDS = new Set([
  'alibaba',
  'deepgram',
  'elevenlabs',
  'groq',
  'microsoft',
  'phone-control',
  'runway',
  'talk-voice',
  'voyage',
]);
const BUNDLED_ALLOWLIST_PRESERVE_IDS = new Set([
  'browser',
  'acpx',
  'memory-core',
]);
const AUTH_PROFILE_PROVIDER_KEY_MAP: Record<string, string> = {
  'openai-codex': 'openai',
  'google-gemini-cli': 'google',
};

/**
 * Reverse of AUTH_PROFILE_PROVIDER_KEY_MAP.
 * Maps a UI provider key (e.g. "openai") to all raw auth-profile provider
 * keys that normalise to it (e.g. ["openai-codex"]).
 */
const AUTH_PROFILE_PROVIDER_KEY_REVERSE_MAP: Record<string, string[]> = Object.entries(
  AUTH_PROFILE_PROVIDER_KEY_MAP,
).reduce<Record<string, string[]>>((acc, [raw, normalized]) => {
  if (!acc[normalized]) acc[normalized] = [];
  acc[normalized].push(raw);
  return acc;
}, {});

/**
 * Return all raw auth-profile `provider` values that should be treated as
 * equivalent to `provider` when cleaning up auth-profile entries.
 * Always includes the provider itself.
 */
function expandProviderKeysForDeletion(provider: string): string[] {
  return [provider, ...(AUTH_PROFILE_PROVIDER_KEY_REVERSE_MAP[provider] ?? [])];
}

function normalizePluginPathForCompare(pluginPath: string): string {
  return pluginPath.replace(/\\/g, '/').replace(/\/+$/, '');
}

function isBundledOpenClawPluginPath(pluginPath: string): boolean {
  const normalized = normalizePluginPathForCompare(pluginPath);
  const currentDistExtensions = normalizePluginPathForCompare(
    join(getOpenClawResolvedDir(), 'dist', 'extensions'),
  );
  const currentLegacyExtensions = normalizePluginPathForCompare(
    join(getOpenClawResolvedDir(), 'extensions'),
  );

  if (
    normalized === currentDistExtensions
    || normalized.startsWith(`${currentDistExtensions}/`)
    || normalized === currentLegacyExtensions
    || normalized.startsWith(`${currentLegacyExtensions}/`)
  ) {
    return true;
  }

  return /\/node_modules(?:\/\.pnpm\/[^/]+\/node_modules)?\/openclaw\/(?:dist\/)?extensions(?:\/|$)/.test(normalized);
}

/**
 * Scan OpenClaw's bundled extensions directory to find all plugins that have
 * `enabledByDefault: true` in their `openclaw.plugin.json` manifest.
 *
 * When `plugins.allow` is explicitly set (e.g. for third-party channel
 * plugins), OpenClaw blocks ALL plugins not in the allowlist — even bundled
 * ones with `enabledByDefault: true`.  This function discovers those plugins
 * so they can be preserved in the allowlist.
 *
 * Results are cached for the lifetime of the process since bundled
 * extensions don't change at runtime.
 */
function discoverBundledPlugins(): {
  all: Set<string>;
  enabledByDefault: string[];
  manifestsById: Map<string, BundledPluginManifest>;
} {
  if (_bundledPluginCache) return _bundledPluginCache;

  const all = new Set<string>();
  const enabledByDefault: string[] = [];
  const manifestsById = new Map<string, BundledPluginManifest>();

  for (const manifest of discoverBundledPluginManifests()) {
    all.add(manifest.id);
    manifestsById.set(manifest.id, manifest);
    if (manifest.enabledByDefault) {
      enabledByDefault.push(manifest.id);
    }
  }

  _bundledPluginCache = { all, enabledByDefault, manifestsById };
  return _bundledPluginCache;
}

function normalizeAuthProfileProviderKey(provider: string): string {
  return AUTH_PROFILE_PROVIDER_KEY_MAP[provider] ?? provider;
}

function addProvidersFromProfileEntries(
  profiles: Record<string, unknown> | undefined,
  target: Set<string>,
): void {
  if (!profiles || typeof profiles !== 'object') {
    return;
  }

  for (const profile of Object.values(profiles)) {
    const provider = typeof (profile as Record<string, unknown>)?.provider === 'string'
      ? ((profile as Record<string, unknown>).provider as string)
      : undefined;
    if (!provider) continue;
    target.add(normalizeAuthProfileProviderKey(provider));
  }
}

async function getProvidersFromAuthProfileStores(): Promise<Set<string>> {
  const providers = new Set<string>();
  const agentIds = await discoverAgentIds();

  for (const agentId of agentIds) {
    const store = await readAuthProfiles(agentId);
    addProvidersFromProfileEntries(store.profiles, providers);
  }

  return providers;
}

async function collectActiveProviderIdsFromConfig(config: Record<string, unknown>): Promise<Set<string>> {
  const activeProviders = new Set<string>();
  const providers = (config.models as Record<string, unknown> | undefined)?.providers;
  if (providers && typeof providers === 'object') {
    for (const key of Object.keys(providers as Record<string, unknown>)) {
      activeProviders.add(key);
    }
  }

  const plugins = (config.plugins as Record<string, unknown> | undefined)?.entries;
  if (plugins && typeof plugins === 'object') {
    for (const [pluginId, meta] of Object.entries(plugins as Record<string, unknown>)) {
      if (pluginId.endsWith('-auth') && (meta as Record<string, unknown>).enabled) {
        activeProviders.add(pluginId.replace(/-auth$/, ''));
      }
    }
  }

  const agents = config.agents as Record<string, unknown> | undefined;
  const defaults = agents?.defaults as Record<string, unknown> | undefined;
  const modelConfig = defaults?.model as Record<string, unknown> | undefined;
  const primaryModel = typeof modelConfig?.primary === 'string' ? modelConfig.primary : undefined;
  if (primaryModel?.includes('/')) {
    activeProviders.add(primaryModel.split('/')[0]);
  }

  const auth = config.auth as Record<string, unknown> | undefined;
  addProvidersFromProfileEntries(auth?.profiles as Record<string, unknown> | undefined, activeProviders);

  const authProfileProviders = await getProvidersFromAuthProfileStores();
  for (const provider of authProfileProviders) {
    activeProviders.add(provider);
  }

  for (const deprecated of DEPRECATED_PROVIDER_IDS) {
    activeProviders.delete(deprecated);
  }

  return activeProviders;
}

async function readOpenClawJson(): Promise<Record<string, unknown>> {
  return (await readJsonFile<Record<string, unknown>>(OPENCLAW_CONFIG_PATH)) ?? {};
}

async function resolveInstalledFeishuPluginId(): Promise<string | null> {
  const extensionRoot = join(homedir(), '.openclaw', 'extensions');
  for (const dirName of FEISHU_PLUGIN_ID_CANDIDATES) {
    const manifestPath = join(extensionRoot, dirName, 'openclaw.plugin.json');
    const manifest = await readJsonFile<{ id?: unknown }>(manifestPath);
    if (typeof manifest?.id === 'string' && manifest.id.trim()) {
      return manifest.id.trim();
    }
  }
  return null;
}

async function discoverInstalledExtensionPluginIds(): Promise<Set<string>> {
  const ids = new Set<string>();
  const extensionRoot = join(homedir(), '.openclaw', 'extensions');

  let entries: Awaited<ReturnType<typeof readdir>>;
  try {
    entries = await readdir(extensionRoot, { withFileTypes: true });
  } catch {
    return ids;
  }

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const manifestPath = join(extensionRoot, entry.name, 'openclaw.plugin.json');
    const manifest = await readJsonFile<{ id?: unknown }>(manifestPath);
    if (typeof manifest?.id === 'string' && manifest.id.trim()) {
      ids.add(manifest.id.trim());
    }
  }

  return ids;
}

function collectPluginLoadPathsFromConfig(plugins: unknown): string[] {
  const paths: string[] = [];
  const pushPath = (value: unknown): void => {
    if (typeof value === 'string' && value.trim()) {
      paths.push(value);
    }
  };

  if (Array.isArray(plugins)) {
    for (const value of plugins) pushPath(value);
    return paths;
  }

  if (!isPlainRecord(plugins)) {
    return paths;
  }

  const load = plugins.load;
  if (Array.isArray(load)) {
    for (const value of load) pushPath(value);
  } else if (isPlainRecord(load) && Array.isArray(load.paths)) {
    for (const value of load.paths) pushPath(value);
  }

  return paths;
}

async function readPluginManifestIdFromPath(pluginPath: string): Promise<string | null> {
  const candidates = [
    join(pluginPath, 'openclaw.plugin.json'),
    join(dirname(pluginPath), 'openclaw.plugin.json'),
  ];

  for (const manifestPath of candidates) {
    const manifest = await readJsonFile<{ id?: unknown }>(manifestPath);
    if (typeof manifest?.id === 'string' && manifest.id.trim()) {
      return manifest.id.trim();
    }
  }

  return null;
}

async function discoverLoadedPluginIdsFromConfig(config: Record<string, unknown>): Promise<Set<string>> {
  const ids = new Set<string>();
  const pluginPaths = collectPluginLoadPathsFromConfig(config.plugins);

  for (const pluginPath of pluginPaths) {
    const pluginId = await readPluginManifestIdFromPath(pluginPath);
    if (pluginId) {
      ids.add(pluginId);
    }
  }

  return ids;
}

function normalizeAgentsDefaultsCompactionMode(config: Record<string, unknown>): void {
  const agents = (config.agents && typeof config.agents === 'object'
    ? config.agents as Record<string, unknown>
    : null);
  if (!agents) return;

  const defaults = (agents.defaults && typeof agents.defaults === 'object'
    ? agents.defaults as Record<string, unknown>
    : null);
  if (!defaults) return;

  const compaction = (defaults.compaction && typeof defaults.compaction === 'object'
    ? defaults.compaction as Record<string, unknown>
    : null);
  if (!compaction) return;

  const mode = compaction.mode;
  if (typeof mode === 'string' && mode.length > 0 && !VALID_COMPACTION_MODES.has(mode)) {
    compaction.mode = 'default';
  }
}

async function writeOpenClawJson(config: Record<string, unknown>): Promise<void> {
  normalizeAgentsDefaultsCompactionMode(config);

  // Ensure SIGUSR1 graceful reload is authorized by OpenClaw config.
  const commands = (
    config.commands && typeof config.commands === 'object'
      ? { ...(config.commands as Record<string, unknown>) }
      : {}
  ) as Record<string, unknown>;
  commands.restart = true;
  config.commands = commands;

  await writeJsonFile(OPENCLAW_CONFIG_PATH, config);
}

// ── Exported Functions (all async) ───────────────────────────────

/**
 * Save an OAuth token to OpenClaw's auth-profiles.json.
 */
export async function saveOAuthTokenToOpenClaw(
  provider: string,
  token: { access: string; refresh: string; expires: number; email?: string; projectId?: string },
  agentId?: string
): Promise<void> {
  const agentIds = agentId ? [agentId] : await discoverAgentIds();
  if (agentIds.length === 0) agentIds.push('main');

  for (const id of agentIds) {
    const store = await readAuthProfiles(id);
    const profileId = `${provider}:default`;

    store.profiles[profileId] = {
      type: 'oauth',
      provider,
      access: token.access,
      refresh: token.refresh,
      expires: token.expires,
      email: token.email,
      projectId: token.projectId,
    };

    if (!store.order) store.order = {};
    if (!store.order[provider]) store.order[provider] = [];
    if (!store.order[provider].includes(profileId)) {
      store.order[provider].push(profileId);
    }

    if (!store.lastGood) store.lastGood = {};
    store.lastGood[provider] = profileId;

    await writeAuthProfiles(store, id);
  }
  console.log(`Saved OAuth token for provider "${provider}" to OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
}

/**
 * Retrieve an OAuth token from OpenClaw's auth-profiles.json.
 * Useful when the Gateway does not natively inject the Authorization header.
 * 
 * @param provider - Provider type (e.g., 'minimax-portal')
 * @param agentId - Optional single agent ID to read from, defaults to 'main'
 * @returns The OAuth token access string or null if not found
 */
export async function getOAuthTokenFromOpenClaw(
  provider: string,
  agentId = 'main'
): Promise<string | null> {
  try {
    const store = await readAuthProfiles(agentId);
    const profileId = `${provider}:default`;
    const profile = store.profiles[profileId];

    if (profile && profile.type === 'oauth' && 'access' in profile) {
      return (profile as OAuthProfileEntry).access;
    }
  } catch (err) {
    console.warn(`[getOAuthToken] Failed to read token for ${provider}:`, err);
  }
  return null;
}

/**
 * Save a provider API key to OpenClaw's auth-profiles.json
 */
export async function saveProviderKeyToOpenClaw(
  provider: string,
  apiKey: string,
  agentId?: string
): Promise<void> {
  if (isOAuthProviderType(provider) && !apiKey) {
    console.log(`Skipping auth-profiles write for OAuth provider "${provider}" (no API key provided, using OAuth)`);
    return;
  }
  const agentIds = agentId ? [agentId] : await discoverAgentIds();
  if (agentIds.length === 0) agentIds.push('main');

  for (const id of agentIds) {
    const store = await readAuthProfiles(id);
    const profileId = `${provider}:default`;

    store.profiles[profileId] = { type: 'api_key', provider, key: apiKey };

    if (!store.order) store.order = {};
    if (!store.order[provider]) store.order[provider] = [];
    if (!store.order[provider].includes(profileId)) {
      store.order[provider].push(profileId);
    }

    if (!store.lastGood) store.lastGood = {};
    store.lastGood[provider] = profileId;

    await writeAuthProfiles(store, id);
  }
  console.log(`Saved API key for provider "${provider}" to OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
}

/**
 * Remove a provider API key from OpenClaw auth-profiles.json
 */
export async function removeProviderKeyFromOpenClaw(
  provider: string,
  agentId?: string
): Promise<void> {
  const agentIds = agentId ? [agentId] : await discoverAgentIds();
  if (agentIds.length === 0) agentIds.push('main');

  for (const id of agentIds) {
    const store = await readAuthProfiles(id);
    if (removeProfileFromStore(store, `${provider}:default`, 'api_key')) {
      await writeAuthProfiles(store, id);
    }
  }
  console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
}

/**
 * Remove a provider completely from OpenClaw (delete config, disable plugins, delete keys)
 */
export async function removeProviderFromOpenClaw(provider: string): Promise<void> {
  // 1. Remove from auth-profiles.json.
  // We must also remove entries whose raw `provider` field maps to this UI
  // provider key via AUTH_PROFILE_PROVIDER_KEY_MAP (e.g. "openai-codex" → "openai").
  // If those entries survive, getProvidersFromAuthProfileStores() will re-add
  // the provider and trigger a re-seed loop in listAccounts().
  const providerKeysToRemove = expandProviderKeysForDeletion(provider);
  const agentIds = await discoverAgentIds();
  if (agentIds.length === 0) agentIds.push('main');
  for (const id of agentIds) {
    const store = await readAuthProfiles(id);
    let storeModified = false;
    for (const key of providerKeysToRemove) {
      if (removeProfilesForProvider(store, key)) {
        storeModified = true;
      }
    }
    if (storeModified) {
      await writeAuthProfiles(store, id);
    }
  }

  // 2. Remove from models.json (per-agent model registry used by pi-ai directly)
  for (const id of agentIds) {
    const modelsPath = join(homedir(), '.openclaw', 'agents', id, 'agent', 'models.json');
    try {
      if (await fileExists(modelsPath)) {
        const raw = await readFile(modelsPath, 'utf-8');
        const data = JSON.parse(raw) as Record<string, unknown>;
        const providers = data.providers as Record<string, unknown> | undefined;
        if (providers && providers[provider]) {
          delete providers[provider];
          await writeFile(modelsPath, JSON.stringify(data, null, 2), 'utf-8');
          console.log(`Removed models.json entry for provider "${provider}" (agent "${id}")`);
        }
      }
    } catch (err) {
      console.warn(`Failed to remove provider ${provider} from models.json (agent "${id}"):`, err);
    }
  }

  // 3. Remove from openclaw.json
  try {
    await withConfigLock(async () => {
      const config = await readOpenClawJson();
      let modified = false;

      // Remove plugin registrations for OAuth providers (e.g. MiniMax).
      if (isOpenClawOAuthPluginProviderKey(provider)) {
        const { canonicalPluginId, stalePluginIds } = getOAuthPluginRegistration(provider);
        if (removePluginRegistrations(config, [canonicalPluginId, ...stalePluginIds])) {
          modified = true;
          console.log(`Removed OpenClaw plugin registrations for provider "${provider}"`);
        }
      }

      // Remove from models.providers
      const models = config.models as Record<string, unknown> | undefined;
      const providers = (models?.providers ?? {}) as Record<string, unknown>;
      if (providers[provider]) {
        delete providers[provider];
        modified = true;
        console.log(`Removed OpenClaw provider config: ${provider}`);
      }

      const auth = (config.auth && typeof config.auth === 'object'
        ? config.auth as Record<string, unknown>
        : null);
      const authProfiles = (
        auth?.profiles && typeof auth.profiles === 'object'
          ? auth.profiles as Record<string, AuthProfileEntry | OAuthProfileEntry>
          : null
      );
      if (authProfiles) {
        // Also clean up raw auth-profile provider keys that map to this provider
        // (e.g. "openai-codex" is stored as-is but maps to "openai" in the UI).
        const providerKeysToClean = new Set(expandProviderKeysForDeletion(provider));
        for (const [profileId, profile] of Object.entries(authProfiles)) {
          if (!providerKeysToClean.has(profile?.provider)) {
            continue;
          }
          delete authProfiles[profileId];
          modified = true;
          console.log(`Removed OpenClaw auth profile: ${profileId}`);
        }
      }

      // Clean up agents.defaults.model references that point to the deleted provider.
      // Model refs use the format "providerType/modelId", e.g. "openai/gpt-4".
      // Leaving stale refs causes the Gateway to report "Unknown model" errors.
      const agents = config.agents as Record<string, unknown> | undefined;
      const agentDefaults = (agents?.defaults && typeof agents.defaults === 'object'
        ? agents.defaults as Record<string, unknown>
        : null);
      if (agentDefaults?.model && typeof agentDefaults.model === 'object') {
        const modelCfg = agentDefaults.model as Record<string, unknown>;
        const prefix = `${provider}/`;

        if (typeof modelCfg.primary === 'string' && modelCfg.primary.startsWith(prefix)) {
          delete modelCfg.primary;
          modified = true;
          console.log(`Removed deleted provider "${provider}" from agents.defaults.model.primary`);
        }

        if (Array.isArray(modelCfg.fallbacks)) {
          const filtered = (modelCfg.fallbacks as string[]).filter((fb) => !fb.startsWith(prefix));
          if (filtered.length !== modelCfg.fallbacks.length) {
            modelCfg.fallbacks = filtered.length > 0 ? filtered : undefined;
            modified = true;
            console.log(`Removed deleted provider "${provider}" from agents.defaults.model.fallbacks`);
          }
        }
      }

      if (modified) {
        await writeOpenClawJson(config);
      }
    });
  } catch (err) {
    console.warn(`Failed to remove provider ${provider} from openclaw.json:`, err);
  }
}

/**
 * Self-heal helper: walk `models.providers.*` in openclaw.json and remove
 * any entry whose `api` field is not in the OpenClaw allow-list.
 *
 * Used opportunistically when the user switches default provider, so that
 * a legacy invalid entry (e.g. the historical `models.providers.openrouter
 * = { api: 'openrouter', ... }` bug) cannot keep the Gateway in
 * Invalid-config -> restart-loop hell on the next reload/restart.
 *
 * Returns the list of pruned provider keys for logging.
 */
export async function pruneInvalidApiProviderEntries(): Promise<string[]> {
  const removed: string[] = [];
  await withConfigLock(async () => {
    const config = await readOpenClawJson();
    const models = (config.models || {}) as Record<string, unknown>;
    const providers = (models.providers || {}) as Record<string, unknown>;
    let modified = false;

    for (const [key, entry] of Object.entries(providers)) {
      const api = isPlainRecord(entry) ? (entry as Record<string, unknown>).api : undefined;
      if (typeof api !== 'string' || !(OPENCLAW_API_PROTOCOLS as readonly string[]).includes(api)) {
        delete providers[key];
        removed.push(key);
        modified = true;
      }
    }

    if (modified) {
      models.providers = providers;
      config.models = models;
      await writeOpenClawJson(config);
    }
  });
  return removed;
}

/**
 * Build environment variables object with all stored API keys
 * for passing to the Gateway process
 */
export function buildProviderEnvVars(providers: Array<{ type: string; apiKey: string }>): Record<string, string> {
  const env: Record<string, string> = {};
  for (const { type, apiKey } of providers) {
    const envVar = getProviderEnvVar(type);
    if (envVar && apiKey) {
      env[envVar] = apiKey;
    }
  }
  return env;
}

/**
 * Update the OpenClaw config to use the given provider and model
 * Writes to ~/.openclaw/openclaw.json
 */
export async function setOpenClawDefaultModel(
  provider: string,
  modelOverride?: string,
  fallbackModels: string[] = []
): Promise<void> {
  return withConfigLock(async () => {
    const config = await readOpenClawJson();
    ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);

    const model = normalizeModelRef(provider, modelOverride);
    if (!model) {
      console.warn(`No default model mapping for provider "${provider}"`);
      return;
    }

    const modelId = extractModelId(provider, model);
    const fallbackModelIds = extractFallbackModelIds(provider, fallbackModels);

    // Set the default model for the agents
    const agents = (config.agents || {}) as Record<string, unknown>;
    const defaults = (agents.defaults || {}) as Record<string, unknown>;
    defaults.model = {
      primary: model,
      fallbacks: fallbackModels,
    };
    agents.defaults = defaults;
    config.agents = agents;

    // Configure models.providers for providers that need explicit registration.
    const providerCfg = getProviderConfig(provider);
    if (providerCfg) {
      assertValidApiProtocol(providerCfg.api, provider);
      upsertOpenClawProviderEntry(config, provider, {
        baseUrl: providerCfg.baseUrl,
        api: providerCfg.api,
        apiKeyEnv: providerCfg.apiKeyEnv,
        headers: providerCfg.headers,
        modelIds: [modelId, ...fallbackModelIds],
        includeRegistryModels: true,
        mergeExistingModels: true,
      });
      console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`);
    } else if (provider === 'openai-codex') {
      // OAuth Codex is not in the UI registry but still needs an explicit provider
      // entry with a pinned embedded runtime (see OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME).
      upsertOpenClawProviderEntry(config, provider, {
        baseUrl: OPENAI_CODEX_OAUTH_PROVIDER_CONFIG.baseUrl,
        api: OPENAI_CODEX_OAUTH_PROVIDER_CONFIG.api,
        modelIds: [modelId, ...fallbackModelIds],
        mergeExistingModels: true,
      });
      if (isOpenClawOAuthPluginProviderKey(provider)) {
        ensureOAuthPluginEnabled(config, provider);
      }
      console.log(
        `Configured models.providers.${provider} for OAuth (api=${OPENAI_CODEX_OAUTH_PROVIDER_CONFIG.api})`,
      );
    } else {
      // Built-in provider: remove any stale models.providers entry
      const models = (config.models || {}) as Record<string, unknown>;
      const providers = (models.providers || {}) as Record<string, unknown>;
      if (providers[provider]) {
        delete providers[provider];
        console.log(`Removed stale models.providers.${provider} (built-in provider)`);
        models.providers = providers;
        config.models = models;
      }
    }

    // Ensure gateway mode is set
    const gateway = (config.gateway || {}) as Record<string, unknown>;
    if (!gateway.mode) gateway.mode = 'local';
    config.gateway = gateway;

    await writeOpenClawJson(config);
    console.log(`Set OpenClaw default model to "${model}" for provider "${provider}"`);
  });
}

interface RuntimeProviderConfigOverride {
  baseUrl?: string;
  api?: string;
  apiKeyEnv?: string;
  headers?: Record<string, string>;
  authHeader?: boolean;
}

type ProviderEntryBuildOptions = {
  baseUrl: string;
  api: string;
  apiKeyEnv?: string;
  headers?: Record<string, string>;
  authHeader?: boolean;
  modelIds?: string[];
  includeRegistryModels?: boolean;
  mergeExistingModels?: boolean;
};

function normalizeModelRef(provider: string, modelOverride?: string): string | undefined {
  const rawModel = modelOverride || getProviderDefaultModel(provider);
  if (!rawModel) return undefined;
  return rawModel.startsWith(`${provider}/`) ? rawModel : `${provider}/${rawModel}`;
}

function extractModelId(provider: string, modelRef: string): string {
  return modelRef.startsWith(`${provider}/`) ? modelRef.slice(provider.length + 1) : modelRef;
}

function extractFallbackModelIds(provider: string, fallbackModels: string[]): string[] {
  return fallbackModels
    .filter((fallback) => fallback.startsWith(`${provider}/`))
    .map((fallback) => fallback.slice(provider.length + 1));
}

function mergeProviderModels(
  ...groups: Array<Array<Record<string, unknown>>>
): Array<Record<string, unknown>> {
  const merged: Array<Record<string, unknown>> = [];
  const seen = new Set<string>();

  for (const group of groups) {
    for (const item of group) {
      const id = typeof item?.id === 'string' ? item.id : '';
      if (!id || seen.has(id)) continue;
      seen.add(id);
      merged.push(item);
    }
  }
  return merged;
}

/**
 * OpenClaw 2026.5+ requires a positive `maxTokens` on each model (and can
 * fall back to provider-level `maxTokens`) when `api` is `anthropic-messages`.
 * ClawX-written entries historically only included `{ id, name }`.
 *
 * Generic Anthropic-compatible providers should not be capped at 8k by
 * default: OpenClaw's native Anthropic transport caps default requests at 32k
 * (`min(model.maxTokens, 32000)`), while high-output providers such as MiniMax
 * M2.7 advertise a larger catalog limit.
 */
export const ANTHROPIC_MESSAGES_DEFAULT_MAX_TOKENS = 32768;
export const MINIMAX_M27_MAX_TOKENS = 131072;

function resolvePositiveMaxTokens(value: unknown): number | undefined {
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return undefined;
  }
  const floored = Math.floor(value);
  return floored > 0 ? floored : undefined;
}

function isMiniMaxM27AnthropicEntry(
  providerKey: string | undefined,
  entry: Record<string, unknown> | undefined,
  model: Record<string, unknown> | undefined,
): boolean {
  const normalizedProvider = (providerKey || '').toLowerCase();
  if (normalizedProvider === 'minimax' || normalizedProvider.startsWith('minimax-portal')) {
    return true;
  }

  const baseUrl = typeof entry?.baseUrl === 'string' ? entry.baseUrl.toLowerCase() : '';
  if (baseUrl.includes('api.minimax.io') || baseUrl.includes('api.minimaxi.com')) {
    return true;
  }

  const modelId = typeof model?.id === 'string' ? model.id.toLowerCase() : '';
  return modelId === 'minimax-m2.7' || modelId === 'minimax-m2.7-highspeed';
}

function resolveAnthropicMessagesDefaultMaxTokens(
  providerKey?: string,
  entry?: Record<string, unknown>,
  model?: Record<string, unknown>,
): number {
  if (isMiniMaxM27AnthropicEntry(providerKey, entry, model)) {
    return MINIMAX_M27_MAX_TOKENS;
  }
  return ANTHROPIC_MESSAGES_DEFAULT_MAX_TOKENS;
}

function ensureAnthropicMessagesModelEntry(
  model: Record<string, unknown>,
  providerKey?: string,
  entry?: Record<string, unknown>,
): Record<string, unknown> {
  const resolved = resolvePositiveMaxTokens(model.maxTokens);
  if (resolved !== undefined) {
    if (model.maxTokens === resolved) {
      return model;
    }
    return { ...model, maxTokens: resolved };
  }
  return { ...model, maxTokens: resolveAnthropicMessagesDefaultMaxTokens(providerKey, entry, model) };
}

function resolveAnthropicMessagesProviderDefaultMaxTokens(
  providerKey: string | undefined,
  entry: Record<string, unknown>,
): number {
  if (Array.isArray(entry.models)) {
    const modelDefaults = entry.models
      .filter(isPlainRecord)
      .map((model) => resolveAnthropicMessagesDefaultMaxTokens(providerKey, entry, model));
    if (modelDefaults.length > 0) {
      return Math.max(...modelDefaults);
    }
  }
  return resolveAnthropicMessagesDefaultMaxTokens(providerKey, entry);
}

/**
 * Ensure `models.providers.*` entries using `anthropic-messages` include the
 * token limits OpenClaw's transport layer requires. Returns whether `entry`
 * was modified.
 */
function ensureAnthropicMessagesProviderDefaults(
  entry: Record<string, unknown>,
  providerKey?: string,
): boolean {
  if (entry.api !== 'anthropic-messages') {
    return false;
  }

  let modified = false;

  if (resolvePositiveMaxTokens(entry.maxTokens) === undefined) {
    entry.maxTokens = resolveAnthropicMessagesProviderDefaultMaxTokens(providerKey, entry);
    modified = true;
  }

  if (Array.isArray(entry.models)) {
    const nextModels = (entry.models as Array<Record<string, unknown>>).map((model) => {
      if (!isPlainRecord(model)) {
        return model;
      }
      const next = ensureAnthropicMessagesModelEntry(model, providerKey, entry);
      if (next !== model) {
        modified = true;
      }
      return next;
    });
    entry.models = nextModels;
  }

  return modified;
}

function healAnthropicMessagesMaxTokensInConfig(config: Record<string, unknown>): boolean {
  const models = (config.models || {}) as Record<string, unknown>;
  const providers = (models.providers || {}) as Record<string, unknown>;
  let modified = false;

  for (const [providerKey, entry] of Object.entries(providers)) {
    if (!isPlainRecord(entry)) {
      continue;
    }
    if (ensureAnthropicMessagesProviderDefaults(entry, providerKey)) {
      providers[providerKey] = entry;
      modified = true;
      console.log(
        `[openclaw-auth] Ensured anthropic-messages maxTokens defaults for models.providers.${providerKey}`,
      );
    }
  }

  if (modified) {
    models.providers = providers;
    config.models = models;
  }

  return modified;
}

/**
 * Self-heal helper: walk `models.providers.*` and ensure every
 * `anthropic-messages` entry (and its model rows) has a positive `maxTokens`.
 */
export async function ensureAnthropicMessagesModelMaxTokens(): Promise<string[]> {
  const healed: string[] = [];
  await withConfigLock(async () => {
    const config = await readOpenClawJson();
    const models = (config.models || {}) as Record<string, unknown>;
    const providers = (models.providers || {}) as Record<string, unknown>;
    let modified = false;

    for (const [providerKey, entry] of Object.entries(providers)) {
      if (!isPlainRecord(entry)) {
        continue;
      }
      if (ensureAnthropicMessagesProviderDefaults(entry, providerKey)) {
        providers[providerKey] = entry;
        healed.push(providerKey);
        modified = true;
      }
    }

    if (modified) {
      models.providers = providers;
      config.models = models;
      await writeOpenClawJson(config);
    }
  });
  return healed;
}

/**
 * Map of OpenClaw `models.providers.*` keys that must be pinned to a specific
 * embedded agent harness so that OpenClaw's auto-routing policy does not
 * dispatch the chat to an externally-bundled harness plugin that may not be
 * installed.
 *
 * OpenClaw 2026.5+ auto-routes OpenAI providers (`openai`, `openai-codex`) to the
 * external `codex` agent harness, which expects a separate codex plugin install.
 * The bundled OpenClaw distribution ClawX ships does not register that harness,
 * so without pinning both keys chat fails with
 * `Requested agent harness "codex" is not registered.`
 */
const OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME: Record<string, string> = {
  openai: 'pi',
  'openai-codex': 'pi',
};

/** Runtime models.providers entry for OpenAI Codex OAuth accounts. */
export const OPENAI_CODEX_OAUTH_PROVIDER_CONFIG = {
  baseUrl: 'https://api.openai.com/v1',
  api: 'openai-codex-responses' as const,
};

function applyPinnedAgentRuntime(
  provider: string,
  nextProvider: Record<string, unknown>,
): void {
  const pinnedRuntimeId = OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME[provider];
  if (!pinnedRuntimeId) return;

  const existing = nextProvider.agentRuntime;
  if (isPlainRecord(existing) && typeof existing.id === 'string' && existing.id.trim()) {
    return;
  }
  nextProvider.agentRuntime = { id: pinnedRuntimeId };
}

function upsertOpenClawProviderEntry(
  config: Record<string, unknown>,
  provider: string,
  options: ProviderEntryBuildOptions,
): void {
  assertValidApiProtocol(options.api, provider);
  const models = (config.models || {}) as Record<string, unknown>;
  const providers = (models.providers || {}) as Record<string, unknown>;
  const removedLegacyMoonshot = removeLegacyMoonshotProviderEntry(provider, providers);
  const existingProvider = (
    providers[provider] && typeof providers[provider] === 'object'
      ? (providers[provider] as Record<string, unknown>)
      : {}
  );

  const existingModels = options.mergeExistingModels && Array.isArray(existingProvider.models)
    ? (existingProvider.models as Array<Record<string, unknown>>)
    : [];
  const registryModels = options.includeRegistryModels
    ? ((getProviderConfig(provider)?.models ?? []).map((m) => ({ ...m })) as Array<Record<string, unknown>>)
    : [];
  const runtimeModels = (options.modelIds ?? []).map((id) => ({ id, name: id }));
  let mergedModels = mergeProviderModels(registryModels, existingModels, runtimeModels);
  if (options.api === 'anthropic-messages') {
    mergedModels = mergedModels.map((model) => ensureAnthropicMessagesModelEntry(model, provider, existingProvider));
  }

  const nextProvider: Record<string, unknown> = {
    ...existingProvider,
    baseUrl: options.baseUrl,
    api: options.api,
    models: mergedModels,
  };
  if (options.api === 'anthropic-messages') {
    ensureAnthropicMessagesProviderDefaults(nextProvider, provider);
  }
  if (options.apiKeyEnv) nextProvider.apiKey = options.apiKeyEnv;
  if (options.headers !== undefined) {
    if (Object.keys(options.headers).length > 0) {
      nextProvider.headers = options.headers;
    } else {
      delete nextProvider.headers;
    }
  }
  if (options.authHeader !== undefined) {
    nextProvider.authHeader = options.authHeader;
  } else {
    delete nextProvider.authHeader;
  }
  applyPinnedAgentRuntime(provider, nextProvider);

  providers[provider] = nextProvider;
  models.providers = providers;
  config.models = models;

  if (removedLegacyMoonshot) {
    console.log('Removed legacy models.providers.moonshot alias entry');
  }
}

/**
 * Self-heal helper: walk `models.providers.*` in openclaw.json and, for any
 * entry whose key is in {@link OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME} but
 * lacks an `agentRuntime.id`, write the pinned runtime id in place.
 *
 * Mirrors {@link pruneInvalidApiProviderEntries} — invoked opportunistically
 * before a default-provider switch so that pre-existing on-disk entries
 * (written by earlier ClawX builds that did not pin the runtime) get
 * repaired before the next Gateway reload picks them up. Without this, users
 * who upgrade ClawX while still pointing at an OpenAI provider would keep
 * hitting `Requested agent harness "codex" is not registered.` until they
 * re-saved the provider manually.
 *
 * Returns the list of provider keys that received a runtime pin, for logging.
 */
export async function ensureOpenClawProviderAgentRuntimePins(): Promise<string[]> {
  const pinned: string[] = [];
  await withConfigLock(async () => {
    const config = await readOpenClawJson();
    const models = (config.models || {}) as Record<string, unknown>;
    const providers = (models.providers || {}) as Record<string, unknown>;
    let modified = false;

    for (const [provider, runtimeId] of Object.entries(OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME)) {
      const entry = providers[provider];
      if (!isPlainRecord(entry)) continue;
      const existing = (entry as Record<string, unknown>).agentRuntime;
      if (isPlainRecord(existing) && typeof existing.id === 'string' && existing.id.trim()) {
        continue;
      }
      (entry as Record<string, unknown>).agentRuntime = { id: runtimeId };
      providers[provider] = entry;
      pinned.push(provider);
      modified = true;
    }

    if (modified) {
      models.providers = providers;
      config.models = models;
      await writeOpenClawJson(config);
    }
  });
  return pinned;
}

function removeLegacyMoonshotProviderEntry(
  _provider: string,
  _providers: Record<string, unknown>
): boolean {
  return false;
}

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function removeLegacyMoonshotKimiSearchConfig(config: Record<string, unknown>): boolean {
  const tools = isPlainRecord(config.tools) ? config.tools : null;
  const web = tools && isPlainRecord(tools.web) ? tools.web : null;
  const search = web && isPlainRecord(web.search) ? web.search : null;
  if (!search || !('kimi' in search)) return false;

  delete search.kimi;
  if (Object.keys(search).length === 0) {
    delete web.search;
  }
  if (Object.keys(web).length === 0) {
    delete tools.web;
  }
  if (Object.keys(tools).length === 0) {
    delete config.tools;
  }
  return true;
}

function upsertMoonshotWebSearchConfig(
  config: Record<string, unknown>,
  providerKey: string,
  baseUrl: string,
  legacyKimi?: Record<string, unknown>,
): void {
  const plugins = isPlainRecord(config.plugins)
    ? config.plugins
    : (Array.isArray(config.plugins) ? { load: [...config.plugins] } : {});
  const entries = isPlainRecord(plugins.entries) ? plugins.entries : {};
  const moonshot = isPlainRecord(entries[providerKey])
    ? entries[providerKey] as Record<string, unknown>
    : {};
  const moonshotConfig = isPlainRecord(moonshot.config) ? moonshot.config as Record<string, unknown> : {};
  const currentWebSearch = isPlainRecord(moonshotConfig.webSearch)
    ? moonshotConfig.webSearch as Record<string, unknown>
    : {};

  const nextWebSearch = { ...(legacyKimi || {}), ...currentWebSearch };
  delete nextWebSearch.apiKey;
  nextWebSearch.baseUrl = baseUrl;

  moonshotConfig.webSearch = nextWebSearch;
  moonshot.config = moonshotConfig;
  entries[providerKey] = moonshot;
  plugins.entries = entries;
  config.plugins = plugins;
}

function ensureMoonshotKimiWebSearchCnBaseUrl(config: Record<string, unknown>, provider: string): void {
  if (provider === OPENCLAW_PROVIDER_KEY_MOONSHOT) {
    const tools = isPlainRecord(config.tools) ? config.tools : null;
    const web = tools && isPlainRecord(tools.web) ? tools.web : null;
    const search = web && isPlainRecord(web.search) ? web.search : null;
    const legacyKimi = search && isPlainRecord(search.kimi) ? search.kimi : undefined;

    upsertMoonshotWebSearchConfig(config, OPENCLAW_PROVIDER_KEY_MOONSHOT, 'https://api.moonshot.cn/v1', legacyKimi);
    removeLegacyMoonshotKimiSearchConfig(config);
  } else if (provider === OPENCLAW_PROVIDER_KEY_MOONSHOT_GLOBAL) {
    upsertMoonshotWebSearchConfig(config, OPENCLAW_PROVIDER_KEY_MOONSHOT_GLOBAL, 'https://api.moonshot.ai/v1');
  }
}

/**
 * Register or update a provider's configuration in openclaw.json
 * without changing the current default model.
 */
export async function syncProviderConfigToOpenClaw(
  provider: string,
  modelId: string | undefined,
  override: RuntimeProviderConfigOverride
): Promise<void> {
  return withConfigLock(async () => {
    const config = await readOpenClawJson();
    ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);

    if (override.baseUrl && override.api) {
      assertValidApiProtocol(override.api, provider);
      upsertOpenClawProviderEntry(config, provider, {
        baseUrl: override.baseUrl,
        api: override.api,
        apiKeyEnv: override.apiKeyEnv,
        headers: override.headers,
        modelIds: modelId ? [modelId] : [],
      });
    }

    // Ensure extension is enabled for oauth providers to prevent gateway wiping config
    if (isOpenClawOAuthPluginProviderKey(provider)) {
      ensureOAuthPluginEnabled(config, provider);
    }

    await writeOpenClawJson(config);
  });
}

/**
 * Update OpenClaw model + provider config using runtime config values.
 */
export async function setOpenClawDefaultModelWithOverride(
  provider: string,
  modelOverride: string | undefined,
  override: RuntimeProviderConfigOverride,
  fallbackModels: string[] = []
): Promise<void> {
  return withConfigLock(async () => {
    const config = await readOpenClawJson();
    ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);

    const model = normalizeModelRef(provider, modelOverride);
    if (!model) {
      console.warn(`No default model mapping for provider "${provider}"`);
      return;
    }

    const modelId = extractModelId(provider, model);
    const fallbackModelIds = extractFallbackModelIds(provider, fallbackModels);

    const agents = (config.agents || {}) as Record<string, unknown>;
    const defaults = (agents.defaults || {}) as Record<string, unknown>;
    defaults.model = {
      primary: model,
      fallbacks: fallbackModels,
    };
    agents.defaults = defaults;
    config.agents = agents;

    if (override.baseUrl && override.api) {
      assertValidApiProtocol(override.api, provider);
      upsertOpenClawProviderEntry(config, provider, {
        baseUrl: override.baseUrl,
        api: override.api,
        apiKeyEnv: override.apiKeyEnv,
        headers: override.headers,
        authHeader: override.authHeader,
        modelIds: [modelId, ...fallbackModelIds],
      });
    }

    const gateway = (config.gateway || {}) as Record<string, unknown>;
    if (!gateway.mode) gateway.mode = 'local';
    config.gateway = gateway;

    // Ensure the extension plugin is marked as enabled in openclaw.json
    if (isOpenClawOAuthPluginProviderKey(provider)) {
      ensureOAuthPluginEnabled(config, provider);
    }

    await writeOpenClawJson(config);
    console.log(
      `Set OpenClaw default model to "${model}" for provider "${provider}" (runtime override)`
    );
  });
}

/**
 * Get a set of all active provider IDs configured in openclaw.json.
 * Reads the file ONCE and extracts both models.providers and plugins.entries.
 */
// Provider IDs that have been deprecated and should never appear as active.
// These may still linger in openclaw.json from older versions.
const DEPRECATED_PROVIDER_IDS = new Set(['qwen-portal']);

export async function getActiveOpenClawProviders(): Promise<Set<string>> {
  const activeProviders = new Set<string>();

  try {
    const config = await readOpenClawJson();

    // 1. models.providers
    const providers = (config.models as Record<string, unknown> | undefined)?.providers;
    if (providers && typeof providers === 'object') {
      for (const key of Object.keys(providers as Record<string, unknown>)) {
        activeProviders.add(key);
      }
    }

    // 2. plugins.entries for OAuth providers
    const plugins = (config.plugins as Record<string, unknown> | undefined)?.entries;
    if (plugins && typeof plugins === 'object') {
      for (const [pluginId, meta] of Object.entries(plugins as Record<string, unknown>)) {
        if (pluginId.endsWith('-auth') && (meta as Record<string, unknown>).enabled) {
          activeProviders.add(pluginId.replace(/-auth$/, ''));
        }
      }
    }

    // 3. agents.defaults.model.primary — the default model reference encodes
    //    the provider prefix (e.g. "modelstudio/qwen3.6-plus" → "modelstudio").
    //    This covers providers that are active via OAuth or env-key but don't
    //    have an explicit models.providers entry.
    const agents = config.agents as Record<string, unknown> | undefined;
    const defaults = agents?.defaults as Record<string, unknown> | undefined;
    const modelConfig = defaults?.model as Record<string, unknown> | undefined;
    const primaryModel = typeof modelConfig?.primary === 'string' ? modelConfig.primary : undefined;
    if (primaryModel?.includes('/')) {
      activeProviders.add(primaryModel.split('/')[0]);
    }

    // 4. auth.profiles — OAuth/device-token based providers may exist only in
    //    auth-profiles without explicit models.providers entries yet.
    const auth = config.auth as Record<string, unknown> | undefined;
    addProvidersFromProfileEntries(auth?.profiles as Record<string, unknown> | undefined, activeProviders);

    const authProfileProviders = await getProvidersFromAuthProfileStores();
    for (const provider of authProfileProviders) {
      activeProviders.add(provider);
    }
  } catch (err) {
    console.warn('Failed to read openclaw.json for active providers:', err);
  }

  // Remove deprecated providers that may still linger in config/auth files.
  for (const deprecated of DEPRECATED_PROVIDER_IDS) {
    activeProviders.delete(deprecated);
  }

  return activeProviders;
}

/**
 * Read models.providers entries and agents.defaults.model from openclaw.json.
 * Used by ClawX to seed the provider store when it's empty but providers are
 * configured externally (e.g. via CLI or by editing openclaw.json directly).
 */
export async function getOpenClawProvidersConfig(): Promise<{
  providers: Record<string, Record<string, unknown>>;
  defaultModel: string | undefined;
}> {
  try {
    const config = await readOpenClawJson();

    const models = config.models as Record<string, unknown> | undefined;
    const providers =
      models?.providers && typeof models.providers === 'object'
        ? (models.providers as Record<string, Record<string, unknown>>)
        : {};

    const agents = config.agents as Record<string, unknown> | undefined;
    const defaults =
      agents?.defaults && typeof agents.defaults === 'object'
        ? (agents.defaults as Record<string, unknown>)
        : undefined;
    const modelConfig =
      defaults?.model && typeof defaults.model === 'object'
        ? (defaults.model as Record<string, unknown>)
        : undefined;
    const defaultModel =
      typeof modelConfig?.primary === 'string' ? modelConfig.primary : undefined;

    const authProviders = new Set<string>();
    const auth = config.auth as Record<string, unknown> | undefined;
    addProvidersFromProfileEntries(auth?.profiles as Record<string, unknown> | undefined, authProviders);

    const authProfileProviders = await getProvidersFromAuthProfileStores();
    for (const provider of authProfileProviders) {
      authProviders.add(provider);
    }

    for (const provider of authProviders) {
      if (!providers[provider]) {
        providers[provider] = {};
      }
    }

    return { providers, defaultModel };
  } catch {
    return { providers: {}, defaultModel: undefined };
  }
}

/**
 * Write the ClawX gateway token into ~/.openclaw/openclaw.json.
 */
export async function syncGatewayTokenToConfig(token: string): Promise<void> {
  return withConfigLock(async () => {
    const config = await readOpenClawJson();

    const gateway = (
      config.gateway && typeof config.gateway === 'object'
        ? { ...(config.gateway as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;

    const auth = (
      gateway.auth && typeof gateway.auth === 'object'
        ? { ...(gateway.auth as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;

    auth.mode = 'token';
    auth.token = token;
    gateway.auth = auth;

    // Packaged ClawX loads the renderer from file://, so the gateway must allow
    // that origin for the chat WebSocket handshake.
    const controlUi = (
      gateway.controlUi && typeof gateway.controlUi === 'object'
        ? { ...(gateway.controlUi as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;
    const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
      ? (controlUi.allowedOrigins as unknown[]).filter((value): value is string => typeof value === 'string')
      : [];
    if (!allowedOrigins.includes('file://')) {
      controlUi.allowedOrigins = [...allowedOrigins, 'file://'];
    }
    gateway.controlUi = controlUi;

    if (!gateway.mode) gateway.mode = 'local';
    config.gateway = gateway;

    await writeOpenClawJson(config);
    console.log('Synced gateway token to openclaw.json');
  });
}

/**
 * Ensure browser automation is enabled in ~/.openclaw/openclaw.json.
 */
export async function syncBrowserConfigToOpenClaw(): Promise<void> {
  return withConfigLock(async () => {
    const config = await readOpenClawJson();

    const browser = (
      config.browser && typeof config.browser === 'object'
        ? { ...(config.browser as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;

    let changed = false;

    if (browser.enabled === undefined) {
      browser.enabled = true;
      changed = true;
    }

    if (browser.defaultProfile === undefined) {
      browser.defaultProfile = 'openclaw';
      changed = true;
    }

    // Default ssrfPolicy to allow private network access for enterprise/internal use
    if (browser.ssrfPolicy == null) {
      browser.ssrfPolicy = { dangerouslyAllowPrivateNetwork: true };
      changed = true;
    } else if (
      typeof browser.ssrfPolicy === 'object' &&
      (browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork === undefined
    ) {
      (browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork = true;
      changed = true;
    }

    if (!changed) return;

    config.browser = browser;
    await writeOpenClawJson(config);
    console.log('Synced browser config to openclaw.json');
  });
}

/**
 * Ensure session idle-reset is configured in ~/.openclaw/openclaw.json.
 *
 * By default OpenClaw resets the "main" session daily at 04:00 local time,
 * which means conversations disappear after roughly one day.  ClawX sets
 * `session.idleMinutes` to 10 080 (7 days) so that conversations are
 * preserved for a week unless the user has explicitly configured their own
 * value.  When `idleMinutes` is set without `session.reset` /
 * `session.resetByType`, OpenClaw stays in idle-only mode (no daily reset).
 */
export async function syncSessionIdleMinutesToOpenClaw(): Promise<void> {
  const DEFAULT_IDLE_MINUTES = 10_080; // 7 days

  return withConfigLock(async () => {
    const config = await readOpenClawJson();

    const session = (
      config.session && typeof config.session === 'object'
        ? { ...(config.session as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;

    // Only set idleMinutes if the user has not configured it yet.
    if (session.idleMinutes !== undefined) return;

    // If the user has explicit reset / resetByType / resetByChannel config,
    // they are actively managing session lifecycle — don't interfere.
    if (session.reset !== undefined
      || session.resetByType !== undefined
      || session.resetByChannel !== undefined) return;

    session.idleMinutes = DEFAULT_IDLE_MINUTES;
    config.session = session;

    await writeOpenClawJson(config);
    console.log(`Synced session.idleMinutes=${DEFAULT_IDLE_MINUTES} (7d) to openclaw.json`);
  });
}

/**
 * Batch-apply gateway token, browser config, and session idle minutes in a
 * single config lock + read + write cycle.  Replaces three separate
 * withConfigLock calls during pre-launch sync.
 */
export async function batchSyncConfigFields(token: string): Promise<void> {
  const DEFAULT_IDLE_MINUTES = 10_080; // 7 days

  return withConfigLock(async () => {
    const config = await readOpenClawJson();
    let modified = true;

    // ── Gateway token + controlUi ──
    const gateway = (
      config.gateway && typeof config.gateway === 'object'
        ? { ...(config.gateway as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;

    const auth = (
      gateway.auth && typeof gateway.auth === 'object'
        ? { ...(gateway.auth as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;
    auth.mode = 'token';
    auth.token = token;
    gateway.auth = auth;

    const controlUi = (
      gateway.controlUi && typeof gateway.controlUi === 'object'
        ? { ...(gateway.controlUi as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;
    const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
      ? (controlUi.allowedOrigins as unknown[]).filter((v): v is string => typeof v === 'string')
      : [];
    if (!allowedOrigins.includes('file://')) {
      controlUi.allowedOrigins = [...allowedOrigins, 'file://'];
    }
    gateway.controlUi = controlUi;
    if (!gateway.mode) gateway.mode = 'local';
    config.gateway = gateway;

    // ── Browser config ──
    const browser = (
      config.browser && typeof config.browser === 'object'
        ? { ...(config.browser as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;
    if (browser.enabled === undefined) {
      browser.enabled = true;
      config.browser = browser;
      modified = true;
    }
    if (browser.defaultProfile === undefined) {
      browser.defaultProfile = 'openclaw';
      config.browser = browser;
      modified = true;
    }
    // Default ssrfPolicy to allow private network access for enterprise/internal use
    if (browser.ssrfPolicy == null) {
      browser.ssrfPolicy = { dangerouslyAllowPrivateNetwork: true };
      config.browser = browser;
      modified = true;
    } else if (
      typeof browser.ssrfPolicy === 'object' &&
      (browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork === undefined
    ) {
      (browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork = true;
      config.browser = browser;
      modified = true;
    }

    // ── Session idle minutes ──
    const session = (
      config.session && typeof config.session === 'object'
        ? { ...(config.session as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;
    const hasExplicitSessionConfig = session.idleMinutes !== undefined
      || session.reset !== undefined
      || session.resetByType !== undefined
      || session.resetByChannel !== undefined;
    if (!hasExplicitSessionConfig) {
      session.idleMinutes = DEFAULT_IDLE_MINUTES;
      config.session = session;
      modified = true;
    }

    if (modified) {
      await writeOpenClawJson(config);
      console.log('Synced gateway token, browser config, and session idle to openclaw.json');
    }
  });
}

/**
 * Update a provider entry in every discovered agent's models.json.
 */
type AgentModelProviderEntry = {
  baseUrl?: string;
  api?: string;
  models?: Array<{
    id: string;
    name: string;
    cost?: PiAiModelCostRates;
    maxTokens?: number;
    [key: string]: unknown;
  }>;
  apiKey?: string;
  /** When true, pi-ai sends Authorization: Bearer instead of x-api-key */
  authHeader?: boolean;
};

async function updateModelsJsonProviderEntriesForAgents(
  agentIds: string[],
  providerType: string,
  entry: AgentModelProviderEntry,
): Promise<void> {
  for (const agentId of agentIds) {
    const modelsPath = join(homedir(), '.openclaw', 'agents', agentId, 'agent', 'models.json');
    let data: Record<string, unknown> = {};
    try {
      data = (await readJsonFile<Record<string, unknown>>(modelsPath)) ?? {};
    } catch {
      // corrupt / missing – start with an empty object
    }

    const providers = (
      data.providers && typeof data.providers === 'object' ? data.providers : {}
    ) as Record<string, Record<string, unknown>>;

    const existing: Record<string, unknown> =
      providers[providerType] && typeof providers[providerType] === 'object'
        ? { ...providers[providerType] }
        : {};

    const existingModels = Array.isArray(existing.models)
      ? (existing.models as Array<Record<string, unknown>>)
      : [];

    const mergedModels = (entry.models ?? []).map((m) => {
      const prev = existingModels.find((e) => e.id === m.id);
      const base = prev ? { ...prev, id: m.id, name: m.name } : { ...m };
      return {
        ...base,
        cost: normalizePiAiModelCost((base as { cost?: unknown }).cost),
      };
    });

    if (entry.baseUrl !== undefined) existing.baseUrl = entry.baseUrl;
    if (entry.api !== undefined) existing.api = entry.api;
    if (mergedModels.length > 0) existing.models = mergedModels;
    if (entry.apiKey !== undefined) existing.apiKey = entry.apiKey;
    if (entry.authHeader !== undefined) existing.authHeader = entry.authHeader;
    ensureAnthropicMessagesProviderDefaults(existing, providerType);

    providers[providerType] = existing;
    data.providers = providers;

    try {
      await writeJsonFile(modelsPath, data);
      console.log(`Updated models.json for agent "${agentId}" provider "${providerType}"`);
    } catch (err) {
      console.warn(`Failed to update models.json for agent "${agentId}":`, err);
    }
  }
}

export async function updateAgentModelProvider(
  providerType: string,
  entry: AgentModelProviderEntry,
): Promise<void> {
  const agentIds = await discoverAgentIds();
  await updateModelsJsonProviderEntriesForAgents(agentIds, providerType, entry);
}

export async function updateSingleAgentModelProvider(
  agentId: string,
  providerType: string,
  entry: AgentModelProviderEntry,
): Promise<void> {
  const normalizedAgentId = agentId.trim();
  if (!normalizedAgentId) {
    throw new Error('agentId is required');
  }
  await updateModelsJsonProviderEntriesForAgents([normalizedAgentId], providerType, entry);
}

/**
 * Sanitize ~/.openclaw/openclaw.json before Gateway start.
 *
 * Removes known-invalid keys that cause OpenClaw's strict Zod validation
 * to reject the entire config on startup.  Uses a conservative **blocklist**
 * approach: only strips keys that are KNOWN to be misplaced by older
 * OpenClaw/ClawX versions or external tools.
 *
 * Why blocklist instead of allowlist?
 *   • Allowlist (e.g. `VALID_SKILLS_KEYS`) would strip any NEW valid keys
 *     added by future OpenClaw releases — a forward-compatibility hazard.
 *   • Blocklist only removes keys we positively know are wrong, so new
 *     valid keys are never touched.
 *
 * This is a fast, file-based pre-check.  For comprehensive repair of
 * unknown or future config issues, the reactive auto-repair mechanism
 * (`runOpenClawDoctorRepair`) runs `openclaw doctor --fix` as a fallback.
 */
export async function sanitizeOpenClawConfig(): Promise<void> {
  return withConfigLock(async () => {
    // Skip sanitization if the config file does not exist yet.
    // Creating a skeleton config here would overwrite any data written
    // by the Gateway on its first run.
    if (!(await fileExists(OPENCLAW_CONFIG_PATH))) {
      console.log('[sanitize] openclaw.json does not exist yet, skipping sanitization');
      return;
    }

    // Read the raw file directly instead of going through readOpenClawJson()
    // which coalesces null → {}.  We need to distinguish a genuinely empty
    // file (valid, proceed normally) from a corrupt/unreadable file (null,
    // bail out to avoid overwriting the user's data with a skeleton config).
    const rawConfig = await readJsonFile<Record<string, unknown>>(OPENCLAW_CONFIG_PATH);
    if (rawConfig === null) {
      console.log('[sanitize] openclaw.json could not be parsed, skipping sanitization to preserve data');
      return;
    }
    const config: Record<string, unknown> = rawConfig;
    let modified = false;

    // ── skills section ──────────────────────────────────────────────
    // OpenClaw's Zod schema uses .strict() on the skills object, accepting
    // only: allowBundled, load, install, limits, entries.
    // The key "enabled" belongs inside skills.entries[key].enabled, NOT at
    // the skills root level.  Older versions may have placed it there.
    const skills = config.skills;
    if (skills && typeof skills === 'object' && !Array.isArray(skills)) {
      const skillsObj = skills as Record<string, unknown>;
      // Keys that are known to be invalid at the skills root level.
      const KNOWN_INVALID_SKILLS_ROOT_KEYS = ['enabled', 'disabled'];
      for (const key of KNOWN_INVALID_SKILLS_ROOT_KEYS) {
        if (key in skillsObj) {
          console.log(`[sanitize] Removing misplaced key "skills.${key}" from openclaw.json`);
          delete skillsObj[key];
          modified = true;
        }
      }
    }

    // ── plugins section ──────────────────────────────────────────────
    // Remove absolute paths in plugins that no longer exist or are bundled (preventing hardlink validation errors)
    const plugins = config.plugins;
    if (plugins) {
      if (Array.isArray(plugins)) {
        const validPlugins: unknown[] = [];
        for (const p of plugins) {
          if (typeof p === 'string' && p.startsWith('/')) {
            if (isBundledOpenClawPluginPath(p) || !(await fileExists(p))) {
              console.log(`[sanitize] Removing stale/bundled plugin path "${p}" from openclaw.json`);
              modified = true;
            } else {
              validPlugins.push(p);
            }
          } else {
            validPlugins.push(p);
          }
        }
        if (modified) config.plugins = validPlugins;
      } else if (typeof plugins === 'object') {
        const pluginsObj = plugins as Record<string, unknown>;
        if (Array.isArray(pluginsObj.load)) {
          const validLoad: unknown[] = [];
          for (const p of pluginsObj.load) {
            if (typeof p === 'string' && p.startsWith('/')) {
              if (isBundledOpenClawPluginPath(p) || !(await fileExists(p))) {
                console.log(`[sanitize] Removing stale/bundled plugin path "${p}" from openclaw.json`);
                modified = true;
              } else {
                validLoad.push(p);
              }
            } else {
              validLoad.push(p);
            }
          }
          if (modified) pluginsObj.load = validLoad;
        } else if (pluginsObj.load && typeof pluginsObj.load === 'object' && !Array.isArray(pluginsObj.load)) {
          // Handle nested shape: plugins.load.paths (array of absolute paths)
          const loadObj = pluginsObj.load as Record<string, unknown>;
          if (Array.isArray(loadObj.paths)) {
            const validPaths: unknown[] = [];
            const countBefore = loadObj.paths.length;
            for (const p of loadObj.paths) {
              if (typeof p === 'string' && p.startsWith('/')) {
                if (isBundledOpenClawPluginPath(p) || !(await fileExists(p))) {
                  console.log(`[sanitize] Removing stale/bundled plugin path "${p}" from plugins.load.paths`);
                  modified = true;
                } else {
                  validPaths.push(p);
                }
              } else {
                validPaths.push(p);
              }
            }
            if (validPaths.length !== countBefore) {
              if (validPaths.length > 0) {
                loadObj.paths = validPaths;
              } else {
                delete loadObj.paths;
              }
              if (Object.keys(loadObj).length === 0) {
                delete pluginsObj.load;
              }
            }
          }
        }
      }
    }

    // ── commands section ───────────────────────────────────────────
    // Required for SIGUSR1 in-process reload authorization.
    const commands = (
      config.commands && typeof config.commands === 'object'
        ? { ...(config.commands as Record<string, unknown>) }
        : {}
    ) as Record<string, unknown>;
    if (commands.restart !== true) {
      commands.restart = true;
      config.commands = commands;
      modified = true;
      console.log('[sanitize] Enabling commands.restart for graceful reload support');
    }

    // ── tools.web.search.kimi ─────────────────────────────────────
    // OpenClaw moved moonshot web search config under
    // plugins.entries.moonshot.config.webSearch. Migrate the old key and strip
    // any inline apiKey so auth-profiles/env remain the single source of truth.
    const providers = ((config.models as Record<string, unknown> | undefined)?.providers as Record<string, unknown> | undefined) || {};
    if (providers[OPENCLAW_PROVIDER_KEY_MOONSHOT]) {
      const tools = isPlainRecord(config.tools) ? config.tools : null;
      const web = tools && isPlainRecord(tools.web) ? tools.web : null;
      const search = web && isPlainRecord(web.search) ? web.search : null;
      const legacyKimi = search && isPlainRecord(search.kimi) ? search.kimi : undefined;
      const hadInlineApiKey = Boolean(legacyKimi && 'apiKey' in legacyKimi);
      const hadLegacyKimi = Boolean(legacyKimi);

      if (legacyKimi) {
        upsertMoonshotWebSearchConfig(config, OPENCLAW_PROVIDER_KEY_MOONSHOT, 'https://api.moonshot.cn/v1', legacyKimi);
        removeLegacyMoonshotKimiSearchConfig(config);
        modified = true;
        console.log('[sanitize] Migrated legacy "tools.web.search.kimi" to "plugins.entries.moonshot.config.webSearch"');
      } else {
        const plugins = isPlainRecord(config.plugins) ? config.plugins : null;
        const entries = plugins && isPlainRecord(plugins.entries) ? plugins.entries : null;
        const moonshot = entries && isPlainRecord(entries[OPENCLAW_PROVIDER_KEY_MOONSHOT])
          ? entries[OPENCLAW_PROVIDER_KEY_MOONSHOT] as Record<string, unknown>
          : null;
        const moonshotConfig = moonshot && isPlainRecord(moonshot.config) ? moonshot.config as Record<string, unknown> : null;
        const webSearch = moonshotConfig && isPlainRecord(moonshotConfig.webSearch)
          ? moonshotConfig.webSearch as Record<string, unknown>
          : null;
        if (webSearch && 'apiKey' in webSearch) {
          delete webSearch.apiKey;
          moonshotConfig!.webSearch = webSearch;
          modified = true;
        }
      }
      if (hadInlineApiKey) {
        console.log('[sanitize] Removing stale key "tools.web.search.kimi.apiKey" from openclaw.json');
      } else if (hadLegacyKimi) {
        console.log('[sanitize] Removing legacy key "tools.web.search.kimi" from openclaw.json');
      }
    }

    // ── tools.profile & sessions.visibility ───────────────────────
    // OpenClaw 3.8+ requires tools.profile = 'full' and tools.sessions.visibility = 'all'
    // for ClawX to properly integrate with its updated tool system.
    const toolsConfig = (config.tools as Record<string, unknown> | undefined) || {};
    let toolsModified = false;

    if (toolsConfig.profile !== 'full') {
      toolsConfig.profile = 'full';
      toolsModified = true;
    }

    const sessions = (toolsConfig.sessions as Record<string, unknown> | undefined) || {};
    if (sessions.visibility !== 'all') {
      sessions.visibility = 'all';
      toolsConfig.sessions = sessions;
      toolsModified = true;
    }

    // ── tools.exec approvals (OpenClaw 3.28+) ──────────────────────
    // ClawX is a local desktop app where the user is the trusted operator.
    // Exec approval prompts add unnecessary friction in this context, so we
    // set security="full" (allow all commands) and ask="off" (never prompt).
    // If a user has manually configured a stricter ~/.openclaw/exec-approvals.json,
    // OpenClaw's minSecurity/maxAsk merge will still respect their intent.
    const execConfig = (toolsConfig.exec as Record<string, unknown> | undefined) || {};
    if (execConfig.security !== 'full' || execConfig.ask !== 'off') {
      execConfig.security = 'full';
      execConfig.ask = 'off';
      toolsConfig.exec = execConfig;
      toolsModified = true;
      console.log('[sanitize] Set tools.exec.security="full" and tools.exec.ask="off" to disable exec approvals for ClawX desktop');
    }

    if (toolsModified) {
      config.tools = toolsConfig;
      modified = true;
    }

    // ── plugins.entries.feishu cleanup ──────────────────────────────
    // Normalize feishu plugin ids dynamically based on installed manifest.
    // Different environments may report either "openclaw-lark" or
    // "feishu-openclaw-plugin" as the runtime plugin id.
    if (typeof plugins === 'object' && !Array.isArray(plugins)) {
      const pluginsObj = plugins as Record<string, unknown>;
      const pEntries = (
        pluginsObj.entries && typeof pluginsObj.entries === 'object' && !Array.isArray(pluginsObj.entries)
          ? pluginsObj.entries
          : {}
      ) as Record<string, Record<string, unknown>>;
      if (!pluginsObj.entries || typeof pluginsObj.entries !== 'object' || Array.isArray(pluginsObj.entries)) {
        pluginsObj.entries = pEntries;
      }

      const allowArr = Array.isArray(pluginsObj.allow) ? pluginsObj.allow as string[] : [];
      if (!Array.isArray(pluginsObj.allow)) {
        pluginsObj.allow = allowArr;
      }

      // ── MiniMax merged-plugin compatibility cleanup ─────────────
      // Newer OpenClaw releases merged the legacy minimax-portal-auth plugin
      // into the canonical "minimax" plugin. Legacy ids may still be accepted
      // in some allowlist paths, but explicit plugins.entries map keys are not
      // consistently normalized upstream, which causes "plugin not found"
      // warnings. Migrate stale ids only when a merged MiniMax plugin is
      // actually installed; otherwise preserve the old plugin for compatibility.
      const miniMaxPluginRegistration = resolveMiniMaxPluginRegistration();
      if (miniMaxPluginRegistration.mergedPlugin) {
        let miniMaxModified = false;
        for (const stalePluginId of miniMaxPluginRegistration.stalePluginIds) {
          const staleAllowIdx = allowArr.indexOf(stalePluginId);
          if (staleAllowIdx !== -1) {
            allowArr.splice(staleAllowIdx, 1);
            miniMaxModified = true;
            console.log(`[sanitize] Removed stale MiniMax plugin from plugins.allow: ${stalePluginId}`);
          }
          if (pEntries[stalePluginId]) {
            delete pEntries[stalePluginId];
            miniMaxModified = true;
            console.log(`[sanitize] Removed stale MiniMax plugin from plugins.entries: ${stalePluginId}`);
          }
        }
        if (miniMaxModified) {
          modified = true;
        }
      }

      // ── acpx legacy config/install cleanup ─────────────────────
      // Older OpenClaw releases allowed plugins.entries.acpx.config.command
      // and expectedVersion overrides. Current bundled acpx schema rejects
      // them, which causes the Gateway to fail validation before startup.
      // Strip those keys and drop stale installs metadata that still points
      // at an older bundled OpenClaw tree so the current bundled plugin can
      // be re-registered cleanly.
      const acpxEntry = isPlainRecord(pEntries.acpx) ? pEntries.acpx as Record<string, unknown> : null;
      const acpxConfig = acpxEntry && isPlainRecord(acpxEntry.config)
        ? acpxEntry.config as Record<string, unknown>
        : null;
      if (acpxConfig) {
        for (const legacyKey of ['command', 'expectedVersion'] as const) {
          if (legacyKey in acpxConfig) {
            delete acpxConfig[legacyKey];
            modified = true;
            console.log(`[sanitize] Removed legacy plugins.entries.acpx.config.${legacyKey}`);
          }
        }
      }

      const installs = isPlainRecord(pluginsObj.installs) ? pluginsObj.installs as Record<string, unknown> : null;
      const acpxInstall = installs && isPlainRecord(installs.acpx) ? installs.acpx as Record<string, unknown> : null;
      if (acpxInstall) {
        const currentBundledAcpxDir = join(getOpenClawResolvedDir(), 'dist', 'extensions', 'acpx').replace(/\\/g, '/');
        const sourcePath = typeof acpxInstall.sourcePath === 'string' ? acpxInstall.sourcePath : '';
        const installPath = typeof acpxInstall.installPath === 'string' ? acpxInstall.installPath : '';
        const normalizedSourcePath = sourcePath.replace(/\\/g, '/');
        const normalizedInstallPath = installPath.replace(/\\/g, '/');
        const pointsAtDifferentBundledTree = [normalizedSourcePath, normalizedInstallPath].some(
          (candidate) => candidate.includes('/node_modules/.pnpm/openclaw@') && candidate !== currentBundledAcpxDir,
        );
        const pointsAtMissingPath = (sourcePath && !(await fileExists(sourcePath)))
          || (installPath && !(await fileExists(installPath)));

        if (pointsAtDifferentBundledTree || pointsAtMissingPath) {
          delete installs.acpx;
          if (Object.keys(installs).length === 0) {
            delete pluginsObj.installs;
          }
          modified = true;
          console.log('[sanitize] Removed stale plugins.installs.acpx metadata');
        }
      }

      const installedFeishuId = await resolveInstalledFeishuPluginId();
      const configuredFeishuId =
        FEISHU_PLUGIN_ID_CANDIDATES.find((id) => allowArr.includes(id))
        || FEISHU_PLUGIN_ID_CANDIDATES.find((id) => Boolean(pEntries[id]));
      const canonicalFeishuId = installedFeishuId || configuredFeishuId || FEISHU_PLUGIN_ID_CANDIDATES[0];

      // Only add feishu plugin to plugins.allow and plugins.entries when the
      // feishu channel is actually configured.  If not configured, remove all
      // feishu-related entries so they don't linger in the config.
      const feishuChannelSection = (config.channels as Record<string, Record<string, unknown>> | undefined)?.feishu;
      const isFeishuConfigured = feishuChannelSection
        && typeof feishuChannelSection === 'object'
        && feishuChannelSection.enabled !== false
        && Object.keys(feishuChannelSection).length > 0;

      if (isFeishuConfigured) {
        const existingFeishuEntry =
          FEISHU_PLUGIN_ID_CANDIDATES.map((id) => pEntries[id]).find(Boolean)
          || pEntries.feishu;

        const normalizedAllow = allowArr.filter(
          (id) => id !== 'feishu' && !FEISHU_PLUGIN_ID_CANDIDATES.includes(id as typeof FEISHU_PLUGIN_ID_CANDIDATES[number]),
        );
        normalizedAllow.push(canonicalFeishuId);
        if (JSON.stringify(normalizedAllow) !== JSON.stringify(allowArr)) {
          pluginsObj.allow = normalizedAllow;
          modified = true;
          console.log(`[sanitize] Normalized plugins.allow for feishu -> ${canonicalFeishuId}`);
        }

        if (existingFeishuEntry || !pEntries[canonicalFeishuId]) {
          pEntries[canonicalFeishuId] = {
            ...(existingFeishuEntry || {}),
            ...(pEntries[canonicalFeishuId] || {}),
            enabled: true,
          };
          modified = true;
        }
        for (const id of FEISHU_PLUGIN_ID_CANDIDATES) {
          if (id !== canonicalFeishuId && pEntries[id]) {
            delete pEntries[id];
            modified = true;
          }
        }
      } else {
        // Feishu channel not configured — remove all feishu plugin entries
        const normalizedAllow = allowArr.filter(
          (id) => id !== 'feishu' && !FEISHU_PLUGIN_ID_CANDIDATES.includes(id as typeof FEISHU_PLUGIN_ID_CANDIDATES[number]),
        );
        if (normalizedAllow.length !== allowArr.length) {
          pluginsObj.allow = normalizedAllow;
          modified = true;
          console.log('[sanitize] Removed unconfigured feishu plugin from plugins.allow');
        }
        for (const id of [...FEISHU_PLUGIN_ID_CANDIDATES, 'feishu'] as const) {
          if (pEntries[id]) {
            delete pEntries[id];
            modified = true;
            console.log(`[sanitize] Removed unconfigured feishu plugin entry: ${id}`);
          }
        }
      }

      // ── wecom-openclaw-plugin → wecom migration ────────────────
      const LEGACY_WECOM_ID = 'wecom-openclaw-plugin';
      const NEW_WECOM_ID = 'wecom';
      if (Array.isArray(pluginsObj.allow)) {
        const allowArr = pluginsObj.allow as string[];
        const legacyIdx = allowArr.indexOf(LEGACY_WECOM_ID);
        if (legacyIdx !== -1) {
          if (!allowArr.includes(NEW_WECOM_ID)) {
            allowArr[legacyIdx] = NEW_WECOM_ID;
          } else {
            allowArr.splice(legacyIdx, 1);
          }
          console.log(`[sanitize] Migrated plugins.allow: ${LEGACY_WECOM_ID} → ${NEW_WECOM_ID}`);
          modified = true;
        }
      }
      if (pEntries?.[LEGACY_WECOM_ID]) {
        if (!pEntries[NEW_WECOM_ID]) {
          pEntries[NEW_WECOM_ID] = pEntries[LEGACY_WECOM_ID];
        }
        delete pEntries[LEGACY_WECOM_ID];
        console.log(`[sanitize] Migrated plugins.entries: ${LEGACY_WECOM_ID} → ${NEW_WECOM_ID}`);
        modified = true;
      }

      // ── qqbot built-in channel cleanup ──────────────────────────
      // OpenClaw 3.31 moved qqbot from a third-party plugin to a built-in
      // channel.  Clean up legacy plugin entries (both bare "qqbot" and
      // manifest-declared "openclaw-qqbot") from plugins.entries.
      // plugins.allow is left untouched — having openclaw-qqbot there is harmless.
      // The channel config under channels.qqbot is preserved and works
      // identically with the built-in channel.
      const QQBOT_PLUGIN_IDS = ['qqbot', 'openclaw-qqbot'] as const;
      for (const qqbotId of QQBOT_PLUGIN_IDS) {
        if (pEntries?.[qqbotId]) {
          delete pEntries[qqbotId];
          console.log(`[sanitize] Removed built-in channel plugin from plugins.entries: ${qqbotId}`);
          modified = true;
        }
      }

      // ── qwen-portal → modelstudio migration ────────────────────
      // OpenClaw 2026.3.28 deprecated qwen-portal OAuth (portal.qwen.ai)
      // in favor of Model Studio (DashScope API key).  Clean up legacy
      // qwen-portal-auth plugin entries and qwen-portal provider config.
      const LEGACY_QWEN_PLUGIN_ID = 'qwen-portal-auth';
      if (Array.isArray(pluginsObj.allow)) {
        const allowArr = pluginsObj.allow as string[];
        const legacyIdx = allowArr.indexOf(LEGACY_QWEN_PLUGIN_ID);
        if (legacyIdx !== -1) {
          allowArr.splice(legacyIdx, 1);
          console.log(`[sanitize] Removed deprecated plugin from plugins.allow: ${LEGACY_QWEN_PLUGIN_ID}`);
          modified = true;
        }
      }
      if (pEntries?.[LEGACY_QWEN_PLUGIN_ID]) {
        delete pEntries[LEGACY_QWEN_PLUGIN_ID];
        console.log(`[sanitize] Removed deprecated plugin from plugins.entries: ${LEGACY_QWEN_PLUGIN_ID}`);
        modified = true;
      }

      // Remove deprecated models.providers.qwen-portal
      const LEGACY_QWEN_PROVIDER = 'qwen-portal';
      if (providers[LEGACY_QWEN_PROVIDER]) {
        delete providers[LEGACY_QWEN_PROVIDER];
        console.log(`[sanitize] Removed deprecated provider: ${LEGACY_QWEN_PROVIDER}`);
        modified = true;
      }

      // Clean up qwen-portal OAuth auth profile (no longer functional)
      const authConfig = config.auth as Record<string, unknown> | undefined;
      const authProfiles = authConfig?.profiles as Record<string, unknown> | undefined;
      if (authProfiles?.[LEGACY_QWEN_PROVIDER]) {
        delete authProfiles[LEGACY_QWEN_PROVIDER];
        console.log(`[sanitize] Removed deprecated auth profile: ${LEGACY_QWEN_PROVIDER}`);
        modified = true;
      }


      // ── Remove legacy built-in 'feishu' registration ───────────────
      // ClawX bundles Feishu via the official @larksuite/openclaw-lark
      // plugin and removes the old built-in dist/extensions/feishu tree.
      // Keeping plugins.entries.feishu={enabled:false} looks harmless, but
      // OpenClaw's channel startup planner treats it as an explicit blocker
      // for the feishu channel owner and skips openclaw-lark at runtime.
      const allowArr2 = Array.isArray(pluginsObj.allow) ? pluginsObj.allow as string[] : [];
      if (isFeishuConfigured) {
        const hasCanonicalFeishu = allowArr2.includes(canonicalFeishuId) || !!pEntries[canonicalFeishuId];
        if (hasCanonicalFeishu && canonicalFeishuId !== 'feishu') {
          const bareFeishuIdx = allowArr2.indexOf('feishu');
          if (bareFeishuIdx !== -1) {
            allowArr2.splice(bareFeishuIdx, 1);
            console.log('[sanitize] Removed bare "feishu" from plugins.allow (openclaw-lark plugin is configured)');
            modified = true;
          }
          if (pEntries.feishu) {
            delete pEntries.feishu;
            console.log('[sanitize] Removed legacy plugins.entries.feishu (openclaw-lark plugin is configured)');
            modified = true;
          }
        }
      }

      // ── Reconcile built-in channels with restrictive plugin allowlists ──
      // If plugins.allow is active because an external plugin is configured,
      // configured built-in channels must also be present or they will be
      // blocked on restart. If the allowlist only contains built-ins, drop it.
      const configuredBuiltIns = new Set<string>();
      const channelsObj = config.channels as Record<string, Record<string, unknown>> | undefined;
      if (channelsObj && typeof channelsObj === 'object') {
        for (const [channelId, section] of Object.entries(channelsObj)) {
          if (!BUILTIN_CHANNEL_IDS.has(channelId)) continue;
          if (!section || section.enabled === false) continue;
          if (Object.keys(section).length > 0) {
            configuredBuiltIns.add(channelId);
          }
        }
      }

      if (pEntries.whatsapp) {
        delete pEntries.whatsapp;
        console.log('[sanitize] Removed legacy plugins.entries.whatsapp for built-in channel');
        modified = true;
      }

      // Discover all bundled extension IDs so we can clean stale bundled
      // allowlist entries from older OpenClaw versions. Re-add only the
      // ClawX-critical bundled plugins, active provider plugins, and explicitly
      // enabled bundled plugins — not every enabledByDefault provider plugin.
      const bundled = discoverBundledPlugins();
      const installedExtensionIds = await discoverInstalledExtensionPluginIds();
      const loadedPluginIds = await discoverLoadedPluginIdsFromConfig(config);
      const activeProviderIds = await collectActiveProviderIdsFromConfig(config);

      const explicitlyEnabledBundledPluginIds = Object.keys(pEntries)
        .filter((pluginId) => {
          if (!bundled.all.has(pluginId)) return false;
          const entry = isPlainRecord(pEntries[pluginId]) ? pEntries[pluginId] as Record<string, unknown> : {};
          if (entry.enabled === false) return false;
          if (pluginId === 'feishu' && (!isFeishuConfigured || canonicalFeishuId !== 'feishu')) {
            return false;
          }
          return entry.enabled === true;
        });

      const activeBundledProviderPluginIds = bundled.enabledByDefault.filter((pluginId) => {
        if (pluginId === 'feishu' && (!isFeishuConfigured || canonicalFeishuId !== 'feishu')) {
          return false;
        }
        const manifest = bundled.manifestsById.get(pluginId);
        const providerIds = manifest?.providers ?? [];
        const isProviderPlugin = providerIds.length > 0
          || OPTIONAL_PROVIDER_LIKE_BUNDLED_PLUGIN_IDS.has(pluginId);
        if (!isProviderPlugin) return false;
        return providerIds.some((providerId) => activeProviderIds.has(providerId))
          || activeProviderIds.has(pluginId);
      });

      const requiredBundledPluginIds = Array.from(new Set([
        ...BUNDLED_ALLOWLIST_PRESERVE_IDS,
        ...activeBundledProviderPluginIds,
        ...explicitlyEnabledBundledPluginIds,
      ])).filter((pluginId) => bundled.all.has(pluginId));

      const externalPluginIds: string[] = [];
      for (const pluginId of allowArr2) {
        if (BUILTIN_CHANNEL_IDS.has(pluginId) || bundled.all.has(pluginId)) continue;
        const isConfiguredExternal = Boolean(pEntries[pluginId]);
        const isInstalledExternal = installedExtensionIds.has(pluginId);
        const isLoadedExternal = loadedPluginIds.has(pluginId);
        if (!isConfiguredExternal && !isInstalledExternal && !isLoadedExternal) {
          console.log(`[sanitize] Removed missing external plugin from plugins.allow: ${pluginId}`);
          modified = true;
          continue;
        }
        externalPluginIds.push(pluginId);
      }

      const retainedBundledPluginIds = allowArr2.filter((pluginId) => requiredBundledPluginIds.includes(pluginId));
      let nextAllow = [...new Set([...externalPluginIds, ...retainedBundledPluginIds])];
      if (nextAllow.length > 0) {
        for (const channelId of configuredBuiltIns) {
          if (!nextAllow.includes(channelId)) {
            nextAllow.push(channelId);
            modified = true;
            console.log(`[sanitize] Added configured built-in channel "${channelId}" to plugins.allow`);
          }
        }
        for (const pluginId of requiredBundledPluginIds) {
          if (!nextAllow.includes(pluginId)) {
            nextAllow.push(pluginId);
            modified = true;
            console.log(`[sanitize] Preserved required bundled plugin "${pluginId}" in plugins.allow`);
          }
        }
      }

      if (JSON.stringify(nextAllow) !== JSON.stringify(allowArr2)) {
        if (nextAllow.length > 0) {
          pluginsObj.allow = nextAllow;
        } else {
          delete pluginsObj.allow;
        }
        modified = true;
      }

      if (Array.isArray(pluginsObj.allow) && pluginsObj.allow.length === 0) {
        delete pluginsObj.allow;
        modified = true;
      }
      if (pluginsObj.entries && Object.keys(pEntries).length === 0) {
        delete pluginsObj.entries;
        modified = true;
      }
      const pluginKeysExcludingEnabled = Object.keys(pluginsObj).filter((key) => key !== 'enabled');
      if (pluginsObj.enabled === true && pluginKeysExcludingEnabled.length === 0) {
        delete pluginsObj.enabled;
        modified = true;
      }
      if (Object.keys(pluginsObj).length === 0) {
        delete config.plugins;
        modified = true;
      }
    }

    // ── channels default-account migration and cleanup ─────────────
    // Most OpenClaw channel plugins/built-ins read the default account's
    // credentials from the top level of `channels.<type>`.  Mirror them
    // there so the runtime can discover them.
    //
    // Channels whose top-level schema (additionalProperties:false) does NOT
    // include `defaultAccount` but DOES include `accounts`.  Strip only
    // `defaultAccount` to allow multi-account support.
    const channelsObj = config.channels as Record<string, Record<string, unknown>> | undefined;
    const CHANNELS_OMIT_DEFAULT_ACCOUNT_KEY = new Set(['dingtalk']);

    if (channelsObj && typeof channelsObj === 'object') {
      for (const [channelType, section] of Object.entries(channelsObj)) {
        if (!section || typeof section !== 'object') continue;

        // Channels that accept accounts but not defaultAccount:
        // strip defaultAccount only.
        if (CHANNELS_OMIT_DEFAULT_ACCOUNT_KEY.has(channelType) && 'defaultAccount' in section) {
          delete section['defaultAccount'];
          modified = true;
          console.log(`[sanitize] Removed incompatible 'defaultAccount' from channels.${channelType}`);
        }

        // Mirror missing keys from default account to top level.
        const accounts = section.accounts as Record<string, Record<string, unknown>> | undefined;
        const defaultAccountId =
          typeof section.defaultAccount === 'string' && section.defaultAccount.trim()
              ? section.defaultAccount
              : 'default';
        const defaultAccountData = accounts?.[defaultAccountId] ?? accounts?.['default'];
        if (!defaultAccountData || typeof defaultAccountData !== 'object') continue;
        let mirrored = false;
        for (const [key, value] of Object.entries(defaultAccountData)) {
          if (!(key in section)) {
            section[key] = value;
            mirrored = true;
          }
        }
        if (mirrored) {
          modified = true;
          console.log(`[sanitize] Mirrored ${channelType} default account credentials to top-level channels.${channelType}`);
        }

        if (channelType === 'discord') {
          const sanitizeDiscordGuildChannelConfig = (channelConfig: unknown): boolean => {
            if (!channelConfig || typeof channelConfig !== 'object' || Array.isArray(channelConfig)) return false;
            const channelRecord = channelConfig as Record<string, unknown>;
            let channelModified = false;
            if (channelRecord.allow === false && channelRecord.enabled === undefined) {
              channelRecord.enabled = false;
              channelModified = true;
            }
            for (const key of ['allow']) {
              if (key in channelRecord) {
                delete channelRecord[key];
                channelModified = true;
              }
            }
            return channelModified;
          };
          const sanitizeDiscordGuilds = (target: Record<string, unknown>): boolean => {
            const guilds = target.guilds;
            if (!guilds || typeof guilds !== 'object' || Array.isArray(guilds)) return false;
            let guildsModified = false;
            for (const guildConfig of Object.values(guilds as Record<string, unknown>)) {
              if (!guildConfig || typeof guildConfig !== 'object' || Array.isArray(guildConfig)) continue;
              const channels = (guildConfig as Record<string, unknown>).channels;
              if (!channels || typeof channels !== 'object' || Array.isArray(channels)) continue;
              for (const channelConfig of Object.values(channels as Record<string, unknown>)) {
                guildsModified = sanitizeDiscordGuildChannelConfig(channelConfig) || guildsModified;
              }
            }
            return guildsModified;
          };

          const sanitizedTopLevel = sanitizeDiscordGuilds(section);
          const sanitizedAccounts = Object.values(accounts ?? {}).some((accountConfig) => (
            accountConfig && typeof accountConfig === 'object' && sanitizeDiscordGuilds(accountConfig)
          ));
          if (sanitizedTopLevel || sanitizedAccounts) {
            modified = true;
            console.log('[sanitize] Removed incompatible Discord channel allow flags');
          }
        }
      }
    }

    if (healAnthropicMessagesMaxTokensInConfig(config)) {
      modified = true;
    }

    if (modified) {
      await writeOpenClawJson(config);
      console.log('[sanitize] openclaw.json sanitized successfully');
    }
  });
}

export { getProviderEnvVar } from './provider-registry';
