// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config module for scripts/src/platform-infra-sub2api-codex.ts.

// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:1317-2034 for #903.

import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import type { RenderedCliResult } from "../output";
import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service";
import { shortSha256Fingerprint } from "../platform-infra-ops-library";
import {
  codexPoolSentinelSummary,
  codexPoolSentinelRuntimeImage,
  readCodexPoolSentinelConfig,
  renderCodexPoolSentinelManifest,
  type CodexPoolSentinelConfig,
  type CodexPoolSentinelProfileSecret,
} from "../platform-infra-sub2api-codex-sentinel";
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";

import type { CodexPoolCaddyEdgeRetryConfig, CodexPoolConfig, CodexPoolLocalCodexConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualAccountsConfig, CodexPoolManualBindingKind, CodexPoolManualBindingProvider, CodexPoolManualBindingSource, CodexPoolManualBindingSourcesConfig, CodexPoolProfileConfig, CodexPoolPublicExposureConfig, CodexProfile, CodexSentinelProtectPolicy, CodexTempUnschedulablePolicy, CodexTempUnschedulableRule, OpenAIResponsesWebSocketsV2Mode } from "./types";
import { booleanValue, integerArrayConfigField, integerConfigField, isRecord, normalizeBaseUrl, numberValue, readRequiredBaseUrl, readRequiredDescription, readRequiredEmail, readRequiredPort, readRequiredPositiveNumber, requiredRecordConfigField, requiredStringConfigField, stringValue, validateKubernetesName } from "./config-utils";
import { readAuthAPIKey, uniqueAccountName } from "./redaction";
import { codexPoolConfigPath } from "./types";

export function collectCodexProfiles(): CodexProfile[] {
  const codexDir = join(homedir(), ".codex");
  const pool = readCodexPoolConfig();
  if (!existsSync(codexDir)) return [];
  const seenAccountNames = new Set<string>();
  return pool.profiles.map((entry) => {
    const resolved = resolveProfileFiles(codexDir, entry);
    const profile = entry.profile;
    const accountName = entry.accountName ?? uniqueAccountName(profile, seenAccountNames);
    seenAccountNames.add(accountName);
    const configFile = resolved.configFile;
    const authFile = resolved.authFile;
    const configPath = join(codexDir, configFile);
    const authPath = join(codexDir, authFile);
    const base: CodexProfile = {
      profile,
      accountName,
      configFile,
      authFile,
      provider: "",
      baseUrl: "",
      wireApi: null,
      model: null,
      envKey: null,
      apiKey: null,
      apiKeySource: null,
      openaiResponsesWebSocketsV2Mode: entry.openaiResponsesWebSocketsV2Mode,
      upstreamUserAgent: entry.upstreamUserAgent,
      trustUpstream: entry.trustUpstream,
      sentinelProtect: entry.sentinelProtect,
      priority: entry.priority,
      capacity: entry.capacity ?? pool.defaultAccountCapacity,
      loadFactor: entry.loadFactor ?? pool.defaultAccountLoadFactor,
      tempUnschedulable: entry.tempUnschedulable,
      authOpenAIKeyShape: existsSync(authPath) ? "unknown" : "missing",
      ok: false,
      error: null,
    };

    try {
      if (!existsSync(configPath)) throw new Error(`config file ${configFile} is missing`);
      const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as unknown;
      if (!isRecord(parsed)) throw new Error("config is not a TOML object");
      const providers = parsed.model_providers;
      if (!isRecord(providers)) throw new Error("model_providers is missing");
      const provider = stringValue(parsed.model_provider) ?? Object.keys(providers)[0] ?? "";
      if (provider === "") throw new Error("model_provider is missing");
      const providerConfig = providers[provider];
      if (!isRecord(providerConfig)) throw new Error(`model provider ${provider} is missing`);
      const baseUrl = normalizeBaseUrl(stringValue(providerConfig.base_url));
      if (baseUrl === null) throw new Error(`model provider ${provider} base_url is missing or invalid`);
      base.provider = provider;
      base.baseUrl = baseUrl;
      base.envKey = stringValue(providerConfig.env_key);
      base.wireApi = stringValue(providerConfig.wire_api);
      base.model = stringValue(parsed.model);

      const auth = readAuthAPIKey(authPath);
      base.authOpenAIKeyShape = auth.shape;
      if (auth.apiKey !== null) {
        base.apiKey = auth.apiKey;
        base.apiKeySource = "auth-json";
      } else if (base.envKey !== null && typeof process.env[base.envKey] === "string" && process.env[base.envKey]!.length > 0) {
        base.apiKey = process.env[base.envKey]!;
        base.apiKeySource = "env";
      }
      if (base.apiKey === null || base.apiKey.length === 0) {
        throw new Error(base.envKey === null ? "auth OPENAI_API_KEY is missing or empty" : `auth OPENAI_API_KEY is missing and env ${base.envKey} is not present`);
      }
      base.ok = true;
      return base;
    } catch (error) {
      base.error = error instanceof Error ? error.message : String(error);
      return base;
    }
  });
}

export function resolveProfileFiles(codexDir: string, profile: CodexPoolProfileConfig): { configFile: string; authFile: string } {
  const configFile = existsSync(join(codexDir, profile.configFile)) || profile.fallbackConfigFile === null
    ? profile.configFile
    : profile.fallbackConfigFile;
  const authFile = existsSync(join(codexDir, profile.authFile)) || profile.fallbackAuthFile === null
    ? profile.authFile
    : profile.fallbackAuthFile;
  return { configFile, authFile };
}

export function readCodexPoolConfig(): CodexPoolConfig {
  if (!existsSync(codexPoolConfigPath)) throw new Error(`${codexPoolConfigPath} is required`);
  const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown;
  if (!isRecord(parsed)) throw new Error(`${codexPoolConfigPath} must contain a YAML object`);
  const version = integerConfigField(parsed, "version", "");
  if (version !== 1) throw new Error(`${codexPoolConfigPath}.version must be 1`);
  const kind = requiredStringConfigField(parsed, "kind", "");
  if (kind !== "platform-infra-sub2api-codex-pool") throw new Error(`${codexPoolConfigPath}.kind must be platform-infra-sub2api-codex-pool`);
  const metadata = requiredRecordConfigField(parsed, "metadata", "");
  const pool = parsed.pool;
  if (!isRecord(pool)) throw new Error(`${codexPoolConfigPath}.pool must be a YAML object`);
  if (!isRecord(parsed.profiles)) throw new Error(`${codexPoolConfigPath}.profiles must be a YAML object`);
  rejectSchedulableYamlField(pool, "pool");
  const defaultTempUnschedulable = readTempUnschedulablePolicy(pool.defaultTempUnschedulable, "pool.defaultTempUnschedulable");
  const defaultAccountPriorityValue = readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority");
  const defaultAccountCapacityValue = readAccountCapacity(pool.defaultAccountCapacity, "pool.defaultAccountCapacity");
  const defaultAccountLoadFactorValue = readAccountLoadFactor(pool.defaultAccountLoadFactor, "pool.defaultAccountLoadFactor");
  const defaultSentinelProtect = readSentinelProtectPolicy(pool.defaultSentinelProtect, "pool.defaultSentinelProtect");
  const profiles = readProfileConfig(
    parsed.profiles,
    defaultTempUnschedulable,
    defaultSentinelProtect,
    defaultAccountPriorityValue,
  );
  const manualAccounts = readManualAccountsConfig(parsed.manualAccounts);
  assertProtectedManualAccountsNotManaged(profiles, manualAccounts);
  const declaredAccountCapacity = desiredProfileCapacityTotal(profiles, defaultAccountCapacityValue);
  const minOwnerConcurrencySource = pool.minOwnerConcurrency === undefined || pool.minOwnerConcurrency === null ? "auto" : "yaml";
  const minOwnerConcurrency = minOwnerConcurrencySource === "auto"
    ? Math.max(1, declaredAccountCapacity)
    : readOwnerConcurrency(pool.minOwnerConcurrency, "pool.minOwnerConcurrency");
  const config: CodexPoolConfig = {
    version,
    kind,
    metadata: {
      id: requiredStringConfigField(metadata, "id", "metadata"),
      owner: requiredStringConfigField(metadata, "owner", "metadata"),
      relatedIssues: integerArrayConfigField(metadata, "relatedIssues", "metadata"),
    },
    groupName: requiredStringConfigField(pool, "groupName", "pool"),
    groupDescription: readRequiredDescription(pool.groupDescription, "pool.groupDescription", 300),
    apiKeyName: requiredStringConfigField(pool, "apiKeyName", "pool"),
    apiKeySecretName: requiredStringConfigField(pool, "apiKeySecretName", "pool"),
    apiKeySecretKey: requiredStringConfigField(pool, "apiKeySecretKey", "pool"),
    adminEmailDefault: readRequiredEmail(pool.adminEmailDefault, "pool.adminEmailDefault"),
    minOwnerBalanceUsd: readRequiredPositiveNumber(pool.minOwnerBalanceUsd, "pool.minOwnerBalanceUsd"),
    minOwnerConcurrency,
    minOwnerConcurrencySource,
    defaultAccountPriority: defaultAccountPriorityValue,
    defaultAccountCapacity: defaultAccountCapacityValue,
    defaultAccountLoadFactor: defaultAccountLoadFactorValue,
    defaultTempUnschedulable,
    defaultSentinelProtect,
    profiles,
    manualAccounts,
    publicExposure: readPublicExposureConfig(parsed.publicExposure),
    localCodex: readLocalCodexConfig(parsed.localCodex),
    sentinel: {
      ...readCodexPoolSentinelConfig(parsed.sentinel, codexPoolConfigPath),
      protectedManualAccounts: manualAccounts.protected.map((account) => account.accountName),
    },
  };
  validateKubernetesName(config.groupName, "pool.groupName", false);
  validateKubernetesName(config.apiKeySecretName, "pool.apiKeySecretName", true);
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(config.apiKeySecretKey)) {
    throw new Error(`${codexPoolConfigPath}.pool.apiKeySecretKey must be a valid secret key name`);
  }
  if (config.minOwnerBalanceUsd <= 0) throw new Error(`${codexPoolConfigPath}.pool.minOwnerBalanceUsd must be > 0`);
  if (declaredAccountCapacity > 0 && config.minOwnerConcurrency < declaredAccountCapacity) {
    throw new Error(`${codexPoolConfigPath}.pool.minOwnerConcurrency must be >= the sum of declared account capacities (${declaredAccountCapacity})`);
  }
  return config;
}

export function readProfileConfig(
  value: unknown,
  defaultTempUnschedulable: CodexTempUnschedulablePolicy,
  defaultSentinelProtect: CodexSentinelProtectPolicy,
  defaultPriority: number,
): CodexPoolProfileConfig[] {
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.profiles must be a YAML object`);
  const entries = value.entries;
  if (!Array.isArray(entries)) throw new Error(`${codexPoolConfigPath}.profiles.entries must be a YAML array`);
  return entries.map((entry, index) => {
    if (!isRecord(entry)) throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}] must be an object`);
    rejectSchedulableYamlField(entry, `profiles.entries[${index}]`);
    const profile = stringValue(entry.profile);
    const configFile = stringValue(entry.configFile);
    const authFile = stringValue(entry.authFile);
    if (profile === null || configFile === null || authFile === null) {
      throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}] requires profile, configFile, and authFile`);
    }
    const accountName = stringValue(entry.accountName);
    if (accountName !== null) validateKubernetesName(accountName, `profiles.entries[${index}].accountName`, false);
    validateCodexFileName(configFile, `profiles.entries[${index}].configFile`);
    validateCodexFileName(authFile, `profiles.entries[${index}].authFile`);
    const fallbackConfigFile = stringValue(entry.fallbackConfigFile);
    const fallbackAuthFile = stringValue(entry.fallbackAuthFile);
    if (fallbackConfigFile !== null) validateCodexFileName(fallbackConfigFile, `profiles.entries[${index}].fallbackConfigFile`);
    if (fallbackAuthFile !== null) validateCodexFileName(fallbackAuthFile, `profiles.entries[${index}].fallbackAuthFile`);
    const openaiResponsesWebSocketsV2Mode = readOpenAIResponsesWebSocketsV2Mode(entry.openaiResponsesWebSocketsV2Mode, `profiles.entries[${index}].openaiResponsesWebSocketsV2Mode`);
    const upstreamUserAgent = readUpstreamUserAgent(entry.upstreamUserAgent, `profiles.entries[${index}].upstreamUserAgent`);
    const trustUpstream = readTrustUpstream(entry.trustUpstream, `profiles.entries[${index}].trustUpstream`);
    const sentinelProtect = readSentinelProtectPolicy(entry.sentinelProtect, `profiles.entries[${index}].sentinelProtect`, defaultSentinelProtect);
    const priority = readAccountPriority(entry.priority, `profiles.entries[${index}].priority`, defaultPriority);
    const capacity = entry.capacity === undefined || entry.capacity === null ? null : readAccountCapacity(entry.capacity, `profiles.entries[${index}].capacity`);
    const loadFactor = entry.loadFactor === undefined || entry.loadFactor === null ? null : readAccountLoadFactor(entry.loadFactor, `profiles.entries[${index}].loadFactor`);
    const tempUnschedulable = readTempUnschedulablePolicy(entry.tempUnschedulable, `profiles.entries[${index}].tempUnschedulable`, defaultTempUnschedulable);
    return {
      profile,
      accountName,
      configFile,
      authFile,
      fallbackConfigFile,
      fallbackAuthFile,
      openaiResponsesWebSocketsV2Mode,
      upstreamUserAgent,
      trustUpstream,
      sentinelProtect,
      priority,
      capacity,
      loadFactor,
      tempUnschedulable,
    };
  });
}

export function desiredProfileCapacityTotal(profiles: CodexPoolProfileConfig[], defaultCapacity: number): number {
  return profiles.reduce((total, profile) => total + (profile.capacity ?? defaultCapacity), 0);
}

export function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig {
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.manualAccounts must be a YAML object`);
  const bindingSources = readManualBindingSources(value.bindingSources, "manualAccounts.bindingSources");
  const protectedRaw = value.protected;
  if (!Array.isArray(protectedRaw)) throw new Error(`${codexPoolConfigPath}.manualAccounts.protected must be a YAML array`);
  const seen = new Set<string>();
  const protectedAccounts = protectedRaw.map((entry, index): CodexPoolManualAccountProtection => {
    const key = `manualAccounts.protected[${index}]`;
    const accountName = typeof entry === "string"
      ? readManualAccountName(entry, key)
      : isRecord(entry)
        ? readManualAccountName(entry.accountName, `${key}.accountName`)
        : null;
    if (accountName === null) throw new Error(`${codexPoolConfigPath}.${key} must be an account name string or object with accountName`);
    const normalized = accountName.toLowerCase();
    if (seen.has(normalized)) throw new Error(`${codexPoolConfigPath}.${key}.accountName is duplicated in manualAccounts.protected`);
    seen.add(normalized);
    const reason = isRecord(entry) ? readManualAccountReason(entry.reason, `${key}.reason`) : null;
    const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`, bindingSources) : null;
    const groupBinding = isRecord(entry) ? readManualAccountGroupBinding(entry.groupBinding, `${key}.groupBinding`, bindingSources) : null;
    return { accountName, reason, proxyBinding, groupBinding };
  });
  return { bindingSources, protected: protectedAccounts };
}

export function readManualBindingSources(value: unknown, key: string): CodexPoolManualBindingSourcesConfig {
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
  const items: CodexPoolManualBindingSource[] = [];
  const byId: Record<string, CodexPoolManualBindingSource> = {};
  for (const [id, rawSource] of Object.entries(value)) {
    const path = `${key}.${id}`;
    validateManualBindingSourceId(id, path);
    if (!isRecord(rawSource)) throw new Error(`${codexPoolConfigPath}.${path} must be a YAML object`);
    const source: CodexPoolManualBindingSource = {
      id,
      enabled: readBooleanConfig(rawSource.enabled, `${path}.enabled`),
      kind: readManualBindingKind(rawSource.kind, `${path}.kind`),
      provider: readManualBindingProvider(rawSource.provider, `${path}.provider`),
      description: readManualAccountReason(rawSource.description, `${path}.description`),
    };
    if (source.kind === "proxy" && source.provider !== "target-egress-proxy") {
      throw new Error(`${codexPoolConfigPath}.${path}.provider must be target-egress-proxy for kind=proxy`);
    }
    if (source.kind === "group" && source.provider !== "pool-group") {
      throw new Error(`${codexPoolConfigPath}.${path}.provider must be pool-group for kind=group`);
    }
    byId[id] = source;
    items.push(source);
  }
  if (items.length === 0) throw new Error(`${codexPoolConfigPath}.${key} must declare at least one source`);
  return { items, byId };
}

export function readManualAccountProxyBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): CodexPoolManualAccountProxyBinding | null {
  if (value === undefined || value === null) return null;
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
  const enabled = readBooleanConfig(value.enabled, `${key}.enabled`);
  const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "proxy", enabled);
  const proxyName = stringValue(value.proxyName);
  if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`);
  validateProxyName(proxyName, `${key}.proxyName`);
  return {
    enabled,
    source,
    proxyName,
  };
}

export function readManualAccountGroupBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): CodexPoolManualAccountGroupBinding | null {
  if (value === undefined || value === null) return null;
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
  const enabled = readBooleanConfig(value.enabled, `${key}.enabled`);
  const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "group", enabled);
  return {
    enabled,
    source,
  };
}

export function readManualBindingSourceRef(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig, expectedKind: CodexPoolManualBindingKind, bindingEnabled: boolean): string {
  const sourceId = stringValue(value);
  if (sourceId === null) throw new Error(`${codexPoolConfigPath}.${key} is required`);
  validateManualBindingSourceId(sourceId, key);
  const source = bindingSources.byId[sourceId];
  if (source === undefined) throw new Error(`${codexPoolConfigPath}.${key} references unknown manualAccounts.bindingSources id ${sourceId}`);
  if (source.kind !== expectedKind) throw new Error(`${codexPoolConfigPath}.${key} references ${sourceId} with kind=${source.kind}; expected ${expectedKind}`);
  if (bindingEnabled && !source.enabled) throw new Error(`${codexPoolConfigPath}.${key} references disabled manualAccounts.bindingSources.${sourceId}`);
  return sourceId;
}

export function readManualBindingKind(value: unknown, key: string): CodexPoolManualBindingKind {
  const text = stringValue(value);
  if (text !== "proxy" && text !== "group") throw new Error(`${codexPoolConfigPath}.${key} must be proxy or group`);
  return text;
}

export function readManualBindingProvider(value: unknown, key: string): CodexPoolManualBindingProvider {
  const text = stringValue(value);
  if (text !== "target-egress-proxy" && text !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key} must be target-egress-proxy or pool-group`);
  return text;
}

export function validateManualBindingSourceId(value: string, key: string): void {
  if (!/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported source id format`);
}

export function readManualAccountName(value: unknown, key: string): string | null {
  const text = stringValue(value)?.trim() ?? null;
  if (text === null || text.length === 0) return null;
  if (text.length > 256) throw new Error(`${codexPoolConfigPath}.${key} must be at most 256 characters`);
  if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`);
  if (!/^[^<>"'`\\]+$/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} contains unsupported characters`);
  return text;
}

export function readManualAccountReason(value: unknown, key: string): string | null {
  if (value === undefined || value === null) return null;
  const text = stringValue(value)?.trim() ?? null;
  if (text === null || text.length === 0) return null;
  if (text.length > 240) throw new Error(`${codexPoolConfigPath}.${key} must be at most 240 characters`);
  if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`);
  return text;
}

export function assertProtectedManualAccountsNotManaged(profiles: CodexPoolProfileConfig[], manualAccounts: CodexPoolManualAccountsConfig): void {
  const protectedNames = new Set(manualAccounts.protected.map((account) => account.accountName.toLowerCase()));
  for (const [index, profile] of profiles.entries()) {
    const accountName = profile.accountName;
    if (accountName !== null && protectedNames.has(accountName.toLowerCase())) {
      throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}].accountName is listed in manualAccounts.protected; protected manual accounts must not be YAML-managed`);
    }
  }
}

export function rejectSchedulableYamlField(value: Record<string, unknown>, key: string): void {
  if (Object.prototype.hasOwnProperty.call(value, "schedulable")) {
    throw new Error(`${codexPoolConfigPath}.${key}.schedulable is process control, not durable YAML config; use codex-pool sync actions instead`);
  }
}

export function readOpenAIResponsesWebSocketsV2Mode(value: unknown, key: string): OpenAIResponsesWebSocketsV2Mode | null {
  if (value === undefined || value === null) return null;
  const text = stringValue(value);
  if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a string`);
  if (text === "off" || text === "ctx_pool" || text === "passthrough") return text;
  throw new Error(`${codexPoolConfigPath}.${key} must be one of off, ctx_pool, passthrough`);
}

export function readUpstreamUserAgent(value: unknown, key: string): string | null {
  if (value === undefined || value === null) return null;
  const text = stringValue(value);
  if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a string`);
  if (text.length > 512) throw new Error(`${codexPoolConfigPath}.${key} must be at most 512 characters`);
  if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`);
  return text;
}

export function readTrustUpstream(value: unknown, key: string): boolean {
  if (value === undefined || value === null) return false;
  const parsed = booleanValue(value);
  if (parsed === null) throw new Error(`${codexPoolConfigPath}.${key} must be a boolean`);
  return parsed;
}

export function readSentinelProtectPolicy(value: unknown, key: string, fallback?: CodexSentinelProtectPolicy): CodexSentinelProtectPolicy {
  if (value === undefined || value === null) {
    if (fallback !== undefined) return fallback;
    throw new Error(`${codexPoolConfigPath}.${key} is required`);
  }
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
  const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback?.enabled);
  const required = ["consecutiveFailures", "initialRetryDelaySeconds", "maxRetryDelaySeconds", "backoffMultiplier"];
  for (const field of required) {
    if (fallback === undefined && (value[field] === undefined || value[field] === null)) {
      throw new Error(`${codexPoolConfigPath}.${key}.${field} is required`);
    }
    if (enabled && value[field] === undefined && fallback === undefined) {
      throw new Error(`${codexPoolConfigPath}.${key}.${field} is required when enabled=true`);
    }
  }
  const consecutiveFailures = value.consecutiveFailures === undefined || value.consecutiveFailures === null
    ? fallback?.consecutiveFailures
    : readBoundedInteger(value.consecutiveFailures, `${key}.consecutiveFailures`, 1, 20);
  const initialRetryDelaySeconds = value.initialRetryDelaySeconds === undefined || value.initialRetryDelaySeconds === null
    ? fallback?.initialRetryDelaySeconds
    : readBoundedInteger(value.initialRetryDelaySeconds, `${key}.initialRetryDelaySeconds`, 1, 3600);
  const maxRetryDelaySeconds = value.maxRetryDelaySeconds === undefined || value.maxRetryDelaySeconds === null
    ? fallback?.maxRetryDelaySeconds
    : readBoundedInteger(value.maxRetryDelaySeconds, `${key}.maxRetryDelaySeconds`, 1, 3600);
  const backoffMultiplier = value.backoffMultiplier === undefined || value.backoffMultiplier === null
    ? fallback?.backoffMultiplier
    : readBoundedInteger(value.backoffMultiplier, `${key}.backoffMultiplier`, 1, 10);
  if (consecutiveFailures === undefined || initialRetryDelaySeconds === undefined || maxRetryDelaySeconds === undefined || backoffMultiplier === undefined) {
    throw new Error(`${codexPoolConfigPath}.${key} is missing required sentinel protection fields`);
  }
  if (maxRetryDelaySeconds < initialRetryDelaySeconds) {
    throw new Error(`${codexPoolConfigPath}.${key}.maxRetryDelaySeconds must be >= initialRetryDelaySeconds`);
  }
  return {
    enabled,
    consecutiveFailures,
    initialRetryDelaySeconds,
    maxRetryDelaySeconds,
    backoffMultiplier,
  };
}

export function readBoundedInteger(value: unknown, key: string, min: number, max: number): number {
  const parsed = numberValue(value);
  if (parsed === null || !Number.isInteger(parsed) || parsed < min || parsed > max) {
    throw new Error(`${codexPoolConfigPath}.${key} must be an integer from ${min} to ${max}`);
  }
  return parsed;
}

export function readAccountPriority(value: unknown, key: string, fallback?: number): number {
  if (value === undefined || value === null) {
    if (fallback !== undefined) return fallback;
    throw new Error(`${codexPoolConfigPath}.${key} is required`);
  }
  const priority = numberValue(value);
  if (priority === null || !Number.isInteger(priority) || priority < 0 || priority > 1000) {
    throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 0 to 1000`);
  }
  return priority;
}

export function readAccountCapacity(value: unknown, key: string): number {
  const capacity = numberValue(value);
  if (capacity === null || !Number.isInteger(capacity) || capacity < 1 || capacity > 1000) {
    throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 1 to 1000`);
  }
  return capacity;
}

export function readOwnerConcurrency(value: unknown, key: string): number {
  const concurrency = numberValue(value);
  if (concurrency === null || !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100000) {
    throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 1 to 100000`);
  }
  return concurrency;
}

export function readAccountLoadFactor(value: unknown, key: string): number {
  const loadFactor = numberValue(value);
  if (loadFactor === null || !Number.isInteger(loadFactor) || loadFactor < 1 || loadFactor > 1000) {
    throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 1 to 1000`);
  }
  return loadFactor;
}

export function readCaddyTimeoutSeconds(value: unknown, key: string): number {
  if (value === undefined || value === null) throw new Error(`${codexPoolConfigPath}.${key} is required`);
  const seconds = numberValue(value);
  if (seconds === null || !Number.isInteger(seconds) || seconds < 30 || seconds > 900) {
    throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 30 to 900`);
  }
  return seconds;
}

export function readCaddyEdgeRetryConfig(value: unknown, key: string): CodexPoolCaddyEdgeRetryConfig {
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
  const enabled = readBooleanConfig(value.enabled, `${key}.enabled`);

  const tryDurationSeconds = readPositiveInteger(value.tryDurationSeconds, `${key}.tryDurationSeconds`);
  const tryIntervalMilliseconds = readPositiveInteger(value.tryIntervalMilliseconds, `${key}.tryIntervalMilliseconds`);
  const retryMatchValue = isRecord(value.retryMatch) ? value.retryMatch : null;
  if (retryMatchValue === null) throw new Error(`${codexPoolConfigPath}.${key}.retryMatch must be a YAML object`);
  const retryMatch = {
    methods: readCaddyRetryMethods(retryMatchValue.methods, `${key}.retryMatch.methods`),
    paths: readCaddyRetryPaths(retryMatchValue.paths, `${key}.retryMatch.paths`),
  };
  if (retryMatch.methods.length === 0 && retryMatch.paths.length === 0) {
    throw new Error(`${codexPoolConfigPath}.${key}.retryMatch must include at least one method or path matcher when enabled=true`);
  }
  return { enabled, tryDurationSeconds, tryIntervalMilliseconds, retryMatch };
}

export function readPositiveInteger(value: unknown, key: string): number {
  const parsed = numberValue(value);
  if (parsed === null || !Number.isInteger(parsed) || parsed < 1) {
    throw new Error(`${codexPoolConfigPath}.${key} must be a positive integer`);
  }
  return parsed;
}

export function readPositiveIntegerConfig(value: unknown, key: string): number {
  if (value === undefined || value === null) throw new Error(`${codexPoolConfigPath}.${key} is required`);
  return readPositiveInteger(value, key);
}

export function readCaddyRetryMethods(value: unknown, key: string): string[] {
  if (value === undefined || value === null) return [];
  if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`);
  const seen = new Set<string>();
  const methods: string[] = [];
  for (const item of value) {
    const method = stringValue(item)?.toUpperCase() ?? null;
    if (method === null || !/^[A-Z][A-Z0-9_-]*$/u.test(method)) throw new Error(`${codexPoolConfigPath}.${key} entries must be HTTP method tokens`);
    if (seen.has(method)) continue;
    seen.add(method);
    methods.push(method);
  }
  return methods;
}

export function readCaddyRetryPaths(value: unknown, key: string): string[] {
  if (value === undefined || value === null) return [];
  if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`);
  const seen = new Set<string>();
  const paths: string[] = [];
  for (const item of value) {
    const path = stringValue(item);
    if (path === null || !path.startsWith("/") || /[\r\n]/u.test(path)) {
      throw new Error(`${codexPoolConfigPath}.${key} entries must be Caddy path matchers starting with / and without newlines`);
    }
    if (seen.has(path)) continue;
    seen.add(path);
    paths.push(path);
  }
  return paths;
}

export function readTempUnschedulablePolicy(value: unknown, key: string, fallback?: CodexTempUnschedulablePolicy): CodexTempUnschedulablePolicy {
  if (value === undefined || value === null) {
    if (fallback !== undefined) return cloneTempUnschedulablePolicy(fallback);
    throw new Error(`${codexPoolConfigPath}.${key} is required`);
  }
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
  const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback?.enabled);
  let rules: CodexTempUnschedulableRule[];
  if (value.rules === undefined || value.rules === null) {
    if (fallback === undefined) throw new Error(`${codexPoolConfigPath}.${key}.rules is required`);
    rules = cloneTempUnschedulablePolicy(fallback).rules;
  } else {
    rules = readTempUnschedulableRules(value.rules, `${key}.rules`);
  }
  if (enabled && rules.length === 0) throw new Error(`${codexPoolConfigPath}.${key}.rules must not be empty when enabled=true`);
  return { enabled, rules };
}

export function readTempUnschedulableRules(value: unknown, key: string): CodexTempUnschedulableRule[] {
  if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`);
  return value.map((entry, index) => {
    if (!isRecord(entry)) throw new Error(`${codexPoolConfigPath}.${key}[${index}] must be an object`);
    const statusCode = numberValue(entry.statusCode ?? entry.errorCode);
    if (statusCode === null || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) {
      throw new Error(`${codexPoolConfigPath}.${key}[${index}].statusCode must be an HTTP status code from 100 to 599`);
    }
    const durationMinutes = numberValue(entry.durationMinutes);
    if (durationMinutes === null || !Number.isInteger(durationMinutes) || durationMinutes < 1 || durationMinutes > 1440) {
      throw new Error(`${codexPoolConfigPath}.${key}[${index}].durationMinutes must be an integer from 1 to 1440`);
    }
    const keywords = readTempUnschedulableKeywords(entry.keywords, `${key}[${index}].keywords`);
    const description = stringValue(entry.description);
    if (description !== null && description.length > 240) throw new Error(`${codexPoolConfigPath}.${key}[${index}].description must be at most 240 characters`);
    return { statusCode, keywords, durationMinutes, description };
  });
}

export function readTempUnschedulableKeywords(value: unknown, key: string): string[] {
  if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`);
  const seen = new Set<string>();
  const keywords: string[] = [];
  for (const item of value) {
    const keyword = stringValue(item);
    if (keyword === null) throw new Error(`${codexPoolConfigPath}.${key} entries must be non-empty strings`);
    if (keyword.length > 120) throw new Error(`${codexPoolConfigPath}.${key} entries must be at most 120 characters`);
    if (/[\r\n]/u.test(keyword)) throw new Error(`${codexPoolConfigPath}.${key} entries must not contain newlines`);
    const normalized = keyword.toLowerCase();
    if (seen.has(normalized)) continue;
    seen.add(normalized);
    keywords.push(keyword);
  }
  if (keywords.length === 0) throw new Error(`${codexPoolConfigPath}.${key} must not be empty`);
  return keywords;
}

export function cloneTempUnschedulablePolicy(policy: CodexTempUnschedulablePolicy): CodexTempUnschedulablePolicy {
  return {
    enabled: policy.enabled,
    rules: policy.rules.map((rule) => ({
      statusCode: rule.statusCode,
      keywords: [...rule.keywords],
      durationMinutes: rule.durationMinutes,
      description: rule.description,
    })),
  };
}

export function readBooleanConfig(value: unknown, key: string, fallback?: boolean): boolean {
  if (value === undefined || value === null) {
    if (fallback !== undefined) return fallback;
    throw new Error(`${codexPoolConfigPath}.${key} is required`);
  }
  const parsed = booleanValue(value);
  if (parsed === null) throw new Error(`${codexPoolConfigPath}.${key} must be a boolean`);
  return parsed;
}

export function readPublicExposureConfig(value: unknown): CodexPoolPublicExposureConfig {
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.publicExposure must be a YAML object`);
  const masterFrpsValue = requiredRecordConfigField(value, "masterFrps", "publicExposure");
  const masterCaddyValue = requiredRecordConfigField(value, "masterCaddy", "publicExposure");
  const config: CodexPoolPublicExposureConfig = {
    enabled: readBooleanConfig(value.enabled, "publicExposure.enabled"),
    proxyName: requiredStringConfigField(value, "proxyName", "publicExposure"),
    configMapName: requiredStringConfigField(value, "configMapName", "publicExposure"),
    deploymentName: requiredStringConfigField(value, "deploymentName", "publicExposure"),
    frpcImage: requiredStringConfigField(value, "frpcImage", "publicExposure"),
    serverAddr: requiredStringConfigField(value, "serverAddr", "publicExposure"),
    serverPort: readRequiredPort(value.serverPort, "publicExposure.serverPort"),
    remotePort: readRequiredPort(value.remotePort, "publicExposure.remotePort"),
    localIP: requiredStringConfigField(value, "localIP", "publicExposure"),
    localPort: readRequiredPort(value.localPort, "publicExposure.localPort"),
    publicBaseUrl: readRequiredBaseUrl(value.publicBaseUrl, "publicExposure.publicBaseUrl"),
    masterBaseUrl: readRequiredBaseUrl(value.masterBaseUrl, "publicExposure.masterBaseUrl"),
    masterFrps: {
      configPath: requiredStringConfigField(masterFrpsValue, "configPath", "publicExposure.masterFrps"),
      containerName: requiredStringConfigField(masterFrpsValue, "containerName", "publicExposure.masterFrps"),
    },
    masterCaddy: {
      enabled: readBooleanConfig(masterCaddyValue.enabled, "publicExposure.masterCaddy.enabled"),
      domain: requiredStringConfigField(masterCaddyValue, "domain", "publicExposure.masterCaddy"),
      configPath: requiredStringConfigField(masterCaddyValue, "configPath", "publicExposure.masterCaddy"),
      serviceName: requiredStringConfigField(masterCaddyValue, "serviceName", "publicExposure.masterCaddy"),
      upstreamBaseUrl: readRequiredBaseUrl(masterCaddyValue.upstreamBaseUrl, "publicExposure.masterCaddy.upstreamBaseUrl"),
      responseHeaderTimeoutSeconds: readCaddyTimeoutSeconds(
        masterCaddyValue.responseHeaderTimeoutSeconds,
        "publicExposure.masterCaddy.responseHeaderTimeoutSeconds",
      ),
      edgeRetry: readCaddyEdgeRetryConfig(
        masterCaddyValue.edgeRetry,
        "publicExposure.masterCaddy.edgeRetry",
      ),
    },
  };
  validateKubernetesName(config.configMapName, "publicExposure.configMapName", true);
  validateKubernetesName(config.deploymentName, "publicExposure.deploymentName", true);
  validateProxyName(config.proxyName, "publicExposure.proxyName");
  validatePort(config.serverPort, "publicExposure.serverPort");
  validatePort(config.remotePort, "publicExposure.remotePort");
  validatePort(config.localPort, "publicExposure.localPort");
  if (!/^[A-Za-z0-9._:/-]+$/u.test(config.frpcImage)) throw new Error(`${codexPoolConfigPath}.publicExposure.frpcImage has an unsupported format`);
  if (!/^[A-Za-z0-9._:-]+$/u.test(config.serverAddr)) throw new Error(`${codexPoolConfigPath}.publicExposure.serverAddr has an unsupported format`);
  if (!/^[A-Za-z0-9._:-]+$/u.test(config.localIP)) throw new Error(`${codexPoolConfigPath}.publicExposure.localIP has an unsupported format`);
  if (!config.masterFrps.configPath.startsWith("/")) throw new Error(`${codexPoolConfigPath}.publicExposure.masterFrps.configPath must be absolute`);
  validateProxyName(config.masterFrps.containerName, "publicExposure.masterFrps.containerName");
  validatePublicHostname(config.masterCaddy.domain, "publicExposure.masterCaddy.domain");
  if (!config.masterCaddy.configPath.startsWith("/")) throw new Error(`${codexPoolConfigPath}.publicExposure.masterCaddy.configPath must be absolute`);
  validateProxyName(config.masterCaddy.serviceName, "publicExposure.masterCaddy.serviceName");
  const upstream = new URL(config.masterCaddy.upstreamBaseUrl);
  if (upstream.protocol !== "http:") throw new Error(`${codexPoolConfigPath}.publicExposure.masterCaddy.upstreamBaseUrl must use http:// for the local upstream`);
  return config;
}

export function readLocalCodexConfig(value: unknown): CodexPoolLocalCodexConfig {
  if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.localCodex must be a YAML object`);
  const config = {
    backupSuffix: requiredStringConfigField(value, "backupSuffix", "localCodex"),
    providerName: requiredStringConfigField(value, "providerName", "localCodex"),
    wireApi: requiredStringConfigField(value, "wireApi", "localCodex"),
    modelContextWindow: readPositiveIntegerConfig(value.modelContextWindow, "localCodex.modelContextWindow"),
    modelAutoCompactTokenLimit: readPositiveIntegerConfig(value.modelAutoCompactTokenLimit, "localCodex.modelAutoCompactTokenLimit"),
    supportsWebSockets: readBooleanConfig(value.supportsWebSockets, "localCodex.supportsWebSockets"),
    responsesWebSocketsV2: readBooleanConfig(value.responsesWebSocketsV2, "localCodex.responsesWebSocketsV2"),
    responsesSmokeModel: requiredStringConfigField(value, "responsesSmokeModel", "localCodex"),
  };
  if (!/^[A-Za-z0-9._-]+$/u.test(config.backupSuffix)) throw new Error(`${codexPoolConfigPath}.localCodex.backupSuffix has an unsupported format`);
  validateProxyName(config.providerName, "localCodex.providerName");
  validateProxyName(config.wireApi, "localCodex.wireApi");
  validateModelName(config.responsesSmokeModel, "localCodex.responsesSmokeModel");
  return config;
}

export function validateCodexFileName(value: string, key: string): void {
  if (!/^(config\.toml|config\.toml\.[A-Za-z0-9._-]+|auth\.json|auth\.json\.[A-Za-z0-9._-]+)$/u.test(value)) {
    throw new Error(`${codexPoolConfigPath}.${key} has an unsupported file name`);
  }
}

export function validateProxyName(value: string, key: string): void {
  if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`);
}

export function validateModelName(value: string, key: string): void {
  if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported model name`);
}

export function validatePublicHostname(value: string, key: string): void {
  if (value.length > 253 || !/^[A-Za-z0-9.-]+$/u.test(value) || value.startsWith(".") || value.endsWith(".")) {
    throw new Error(`${codexPoolConfigPath}.${key} has an unsupported hostname format`);
  }
}

export function validatePort(value: number, key: string): void {
  if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error(`${codexPoolConfigPath}.${key} must be an integer port`);
}
