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

// Moved mechanically from scripts/src/platform-infra.ts:454-936 for #903.

import { createHash, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, join } from "node:path";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import { resolveEgressProxySourceRef } from "../egress-proxy-sources";
import { startJob } from "../jobs";
import type { RenderedCliResult } from "../output";
import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy";
import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service";
import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library";
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";

import type { Sub2ApiAccountLocalProxyConfig, Sub2ApiConfig, Sub2ApiDefaults, Sub2ApiEgressProxyConfig, Sub2ApiHostDockerConfig, Sub2ApiMasterShadowsocksConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
import { secretRoot } from "./apply-script";
import { configPath } from "./entry";
import { normalizePublicBaseUrl, validateHostOrIp, validateHostname, validateKubernetesResourceName, validatePort, validateProxyName, validateProxyUrl } from "./manifest";

export function defaultSub2ApiTargetId(): string {
  return readSub2ApiConfig().defaults.targetId;
}

export function validateOptions(args: string[], booleanOptions: Set<string>): void {
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--target") {
      index += 1;
      continue;
    }
    if (arg.startsWith("--target=") && booleanOptions.has("--target")) continue;
    if (booleanOptions.has(arg)) continue;
    throw new Error(`unsupported option: ${arg}`);
  }
}

export function readSub2ApiConfig(): Sub2ApiConfig {
  const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown;
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${configPath} must contain a YAML object`);
  const root = parsed as Record<string, unknown>;
  const version = integerField(root, "version", "");
  if (version !== 1) throw new Error(`${configPath}.version must be 1`);
  const kind = stringField(root, "kind", "");
  if (kind !== "platform-infra-sub2api") throw new Error(`${configPath}.kind must be platform-infra-sub2api`);
  const metadata = objectField(root, "metadata", "");
  const relatedIssues = integerArrayField(metadata, "relatedIssues", "metadata");
  const defaults = parseDefaults(root);
  const image = (parsed as { image?: unknown }).image;
  if (typeof image !== "object" || image === null || Array.isArray(image)) throw new Error(`${configPath}.image must be an object`);
  const record = image as Record<string, unknown>;
  const repository = stringField(record, "repository", "image");
  const tag = stringField(record, "tag", "image");
  const pullPolicy = stringField(record, "pullPolicy", "image");
  if (pullPolicy !== "Always" && pullPolicy !== "IfNotPresent" && pullPolicy !== "Never") throw new Error(`${configPath}.image.pullPolicy must be Always, IfNotPresent, or Never`);
  if (!isImageRepository(repository)) throw new Error(`${configPath}.image.repository has an unsupported format`);
  if (!/^[A-Za-z0-9._-]+$/u.test(tag)) throw new Error(`${configPath}.image.tag has an unsupported format`);
  const dependencyImages = parseDependencyImages(root);
  const security = objectField(parsed as Record<string, unknown>, "security", "");
  const urlAllowlist = objectField(security, "urlAllowlist", "security");
  const enabled = booleanField(urlAllowlist, "enabled", "security.urlAllowlist");
  const allowInsecureHttp = booleanField(urlAllowlist, "allowInsecureHttp", "security.urlAllowlist");
  const allowPrivateHosts = booleanField(urlAllowlist, "allowPrivateHosts", "security.urlAllowlist");
  const upstreamHosts = stringArrayField(urlAllowlist, "upstreamHosts", "security.urlAllowlist");
  const targets = parseTargets(root, defaults.targetId);
  const runtime = parseRuntime(root);
  return {
    version,
    kind,
    metadata: {
      id: stringField(metadata, "id", "metadata"),
      owner: stringField(metadata, "owner", "metadata"),
      relatedIssues,
    },
    defaults,
    image: { repository, tag, pullPolicy },
    dependencyImages,
    security: {
      urlAllowlist: {
        enabled,
        allowInsecureHttp,
        allowPrivateHosts,
        upstreamHosts,
      },
    },
    targets,
    runtime,
  };
}

export function parseDefaults(root: Record<string, unknown>): Sub2ApiDefaults {
  const defaults = objectField(root, "defaults", "");
  const targetId = stringField(defaults, "targetId", "defaults");
  if (!/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error(`${configPath}.defaults.targetId must be a simple target id`);
  const rollout = objectField(defaults, "rollout", "defaults");
  const imagePrepullTimeoutSeconds = integerField(rollout, "imagePrepullTimeoutSeconds", "defaults.rollout");
  const smokeTimeoutSeconds = integerField(rollout, "smokeTimeoutSeconds", "defaults.rollout");
  const smokeMaxOutputTokens = integerField(rollout, "smokeMaxOutputTokens", "defaults.rollout");
  if (imagePrepullTimeoutSeconds < 1 || imagePrepullTimeoutSeconds > 3600) throw new Error(`${configPath}.defaults.rollout.imagePrepullTimeoutSeconds must be between 1 and 3600`);
  if (smokeTimeoutSeconds < 1 || smokeTimeoutSeconds > 600) throw new Error(`${configPath}.defaults.rollout.smokeTimeoutSeconds must be between 1 and 600`);
  if (smokeMaxOutputTokens < 1 || smokeMaxOutputTokens > 1024) throw new Error(`${configPath}.defaults.rollout.smokeMaxOutputTokens must be between 1 and 1024`);
  const cleanup = objectField(defaults, "cleanup", "defaults");
  const externalDbState = objectField(cleanup, "externalDbState", "defaults.cleanup");
  const redisPersistentState = objectField(cleanup, "redisPersistentState", "defaults.cleanup");
  const publicExposure = objectField(cleanup, "publicExposure", "defaults.cleanup");
  const egressProxy = objectField(cleanup, "egressProxy", "defaults.cleanup");
  const parsed: Sub2ApiDefaults = {
    targetId,
    rollout: {
      imagePrepullTimeoutSeconds,
      smokeTimeoutSeconds,
      smokeMaxOutputTokens,
    },
    cleanup: {
      externalDbState: {
        postgresStatefulSetName: stringField(externalDbState, "postgresStatefulSetName", "defaults.cleanup.externalDbState"),
        postgresServiceName: stringField(externalDbState, "postgresServiceName", "defaults.cleanup.externalDbState"),
        postgresPvcName: stringField(externalDbState, "postgresPvcName", "defaults.cleanup.externalDbState"),
        appDataPvcName: stringField(externalDbState, "appDataPvcName", "defaults.cleanup.externalDbState"),
      },
      redisPersistentState: {
        pvcName: stringField(redisPersistentState, "pvcName", "defaults.cleanup.redisPersistentState"),
      },
      publicExposure: {
        deploymentName: stringField(publicExposure, "deploymentName", "defaults.cleanup.publicExposure"),
        configMapName: stringField(publicExposure, "configMapName", "defaults.cleanup.publicExposure"),
        secretName: stringField(publicExposure, "secretName", "defaults.cleanup.publicExposure"),
      },
      egressProxy: {
        deploymentName: stringField(egressProxy, "deploymentName", "defaults.cleanup.egressProxy"),
        serviceName: stringField(egressProxy, "serviceName", "defaults.cleanup.egressProxy"),
        secretName: stringField(egressProxy, "secretName", "defaults.cleanup.egressProxy"),
      },
    },
  };
  const resourceNames = {
    ...parsed.cleanup.externalDbState,
    redisPersistentPvcName: parsed.cleanup.redisPersistentState.pvcName,
    publicExposureDeploymentName: parsed.cleanup.publicExposure.deploymentName,
    publicExposureConfigMapName: parsed.cleanup.publicExposure.configMapName,
    publicExposureSecretName: parsed.cleanup.publicExposure.secretName,
    egressProxyDeploymentName: parsed.cleanup.egressProxy.deploymentName,
    egressProxyServiceName: parsed.cleanup.egressProxy.serviceName,
    egressProxySecretName: parsed.cleanup.egressProxy.secretName,
  };
  for (const [key, value] of Object.entries(resourceNames)) {
    validateKubernetesResourceName(value, `defaults.cleanup.${key}`);
  }
  return parsed;
}

export function parseDependencyImages(root: Record<string, unknown>): Sub2ApiConfig["dependencyImages"] {
  const value = root.dependencyImages;
  if (value === undefined) throw new Error(`${configPath}.dependencyImages must be an object`);
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.dependencyImages must be an object`);
  const record = value as Record<string, unknown>;
  const postgres = stringField(record, "postgres", "dependencyImages");
  const redis = stringField(record, "redis", "dependencyImages");
  if (!isImageReference(postgres)) throw new Error(`${configPath}.dependencyImages.postgres has an unsupported format`);
  if (!isImageReference(redis)) throw new Error(`${configPath}.dependencyImages.redis has an unsupported format`);
  return { postgres, redis };
}

export function parseTargets(root: Record<string, unknown>, defaultTargetId: string): Sub2ApiTargetConfig[] {
  const value = root.targets;
  if (value === undefined) throw new Error(`${configPath}.targets must be an array`);
  if (!Array.isArray(value)) throw new Error(`${configPath}.targets must be an array`);
  const targets = value.map((item, index) => {
    if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error(`${configPath}.targets[${index}] must be an object`);
    const record = item as Record<string, unknown>;
    const path = `targets[${index}]`;
    const id = stringField(record, "id", path);
    const route = stringField(record, "route", path);
    const targetNamespace = stringField(record, "namespace", path);
    const runtimeMode = record.runtimeMode === undefined ? "k3s" : enumField(record, "runtimeMode", path, ["k3s", "host-docker"] as const);
    const role = stringField(record, "role", path);
    const enabled = booleanField(record, "enabled", path);
    const databaseMode = enumField(record, "databaseMode", path, ["bundled", "external-pending", "external-active"] as const);
    const redisMode = enumField(record, "redisMode", path, ["bundled-persistent", "local-ephemeral"] as const);
    const appReplicas = integerField(record, "appReplicas", path);
    const redisReplicas = integerField(record, "redisReplicas", path);
    const runtime = parseTargetRuntime(record.runtime, path);
    const resourceProtection = parseResourceProtection(record.resourceProtection, path, runtimeMode);
    const image = targetImageOverride(record, path);
    const dependencyImages = targetDependencyImageOverride(record, path);
    const hostDocker = parseHostDockerConfig(record.hostDocker, path, runtimeMode);
    const publicExposure = parsePublicExposureConfig(record.publicExposure, path);
    const egressProxy = parseEgressProxyConfig(record.egressProxy, path);
    const accountLocalProxy = parseAccountLocalProxyConfig(record.accountLocalProxy, path);
    if (!/^[A-Za-z0-9._-]+$/u.test(id)) throw new Error(`${configPath}.${path}.id must be a simple target id`);
    if (!/^[A-Za-z0-9:_./-]+$/u.test(route)) throw new Error(`${configPath}.${path}.route has an unsupported format`);
    if (!isKubernetesName(targetNamespace)) throw new Error(`${configPath}.${path}.namespace must be a Kubernetes namespace name`);
    if (appReplicas < 0) throw new Error(`${configPath}.${path}.appReplicas must be >= 0`);
    if (redisReplicas < 0) throw new Error(`${configPath}.${path}.redisReplicas must be >= 0`);
    if (runtimeMode === "host-docker" && databaseMode !== "external-active") throw new Error(`${configPath}.${path}.databaseMode must be external-active for runtimeMode=host-docker`);
    if (runtimeMode === "host-docker" && egressProxy?.enabled === true) throw new Error(`${configPath}.${path}.egressProxy must be disabled or omitted for runtimeMode=host-docker`);
    if (runtimeMode === "host-docker" && accountLocalProxy?.enabled === true) throw new Error(`${configPath}.${path}.accountLocalProxy must be disabled or omitted for runtimeMode=host-docker`);
    if (runtimeMode === "host-docker" && publicExposure?.enabled === true && publicExposure.mode !== "pk01-local") throw new Error(`${configPath}.${path}.publicExposure.mode must be pk01-local for runtimeMode=host-docker`);
    if (runtimeMode === "k3s" && publicExposure?.enabled === true && publicExposure.mode === "pk01-local") throw new Error(`${configPath}.${path}.publicExposure.mode=pk01-local is only supported for runtimeMode=host-docker`);
    return { id, route, namespace: targetNamespace, runtimeMode, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, runtime, resourceProtection, image, dependencyImages, hostDocker, publicExposure, egressProxy, accountLocalProxy };
  });
  const ids = new Set<string>();
  for (const target of targets) {
    if (ids.has(target.id)) throw new Error(`${configPath}.targets contains duplicate id ${target.id}`);
    ids.add(target.id);
  }
  if (!ids.has(defaultTargetId)) throw new Error(`${configPath}.targets must include defaults.targetId ${defaultTargetId}`);
  return targets;
}

function resourceQuantity(value: unknown, path: string, kind: "cpu" | "memory"): string {
  if (typeof value !== "string" || value.length === 0) throw new Error(`${configPath}.${path} must be a non-empty Kubernetes quantity`);
  const pattern = kind === "cpu" ? /^(?:[1-9]\d*|0)(?:\.\d+)?m?$/u : /^(?:[1-9]\d*|0)(?:\.\d+)?(?:Ki|Mi|Gi|Ti)$/u;
  if (!pattern.test(value)) throw new Error(`${configPath}.${path} has an unsupported Kubernetes ${kind} quantity`);
  return value;
}

function containerResources(value: unknown, path: string): NonNullable<Sub2ApiTargetConfig["resourceProtection"]>["app"] {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.${path} must be an object`);
  const raw = value as Record<string, unknown>;
  const requests = objectField(raw, "requests", path);
  const limits = objectField(raw, "limits", path);
  const result = {
    requests: { cpu: resourceQuantity(requests.cpu, `${path}.requests.cpu`, "cpu"), memory: resourceQuantity(requests.memory, `${path}.requests.memory`, "memory") },
    limits: { cpu: resourceQuantity(limits.cpu, `${path}.limits.cpu`, "cpu"), memory: resourceQuantity(limits.memory, `${path}.limits.memory`, "memory") },
  };
  if (resourceQuantityValue(result.requests.cpu, "cpu") > resourceQuantityValue(result.limits.cpu, "cpu")) throw new Error(`${configPath}.${path}.requests.cpu must not exceed limits.cpu`);
  if (resourceQuantityValue(result.requests.memory, "memory") > resourceQuantityValue(result.limits.memory, "memory")) throw new Error(`${configPath}.${path}.requests.memory must not exceed limits.memory`);
  return result;
}

function resourceQuantityValue(value: string, kind: "cpu" | "memory"): number {
  if (kind === "cpu") return value.endsWith("m") ? Number(value.slice(0, -1)) / 1000 : Number(value);
  const units = { Ki: 1024, Mi: 1024 ** 2, Gi: 1024 ** 3, Ti: 1024 ** 4 } as const;
  const unit = value.slice(-2) as keyof typeof units;
  return Number(value.slice(0, -2)) * units[unit];
}

function parseResourceProtection(value: unknown, path: string, runtimeMode: Sub2ApiTargetConfig["runtimeMode"]): Sub2ApiTargetConfig["resourceProtection"] {
  if (value === undefined || value === null) return null;
  if (runtimeMode !== "k3s") throw new Error(`${configPath}.${path}.resourceProtection is only supported for runtimeMode=k3s`);
  if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.resourceProtection must be an object`);
  const raw = value as Record<string, unknown>;
  const priorityClassName = stringField(raw, "priorityClassName", `${path}.resourceProtection`);
  const priorityValue = integerField(raw, "priorityValue", `${path}.resourceProtection`);
  const batchPriorityClassName = stringField(raw, "batchPriorityClassName", `${path}.resourceProtection`);
  const batchPriorityValue = integerField(raw, "batchPriorityValue", `${path}.resourceProtection`);
  if (!isKubernetesName(priorityClassName)) throw new Error(`${configPath}.${path}.resourceProtection.priorityClassName must be a Kubernetes name`);
  if (!isKubernetesName(batchPriorityClassName)) throw new Error(`${configPath}.${path}.resourceProtection.batchPriorityClassName must be a Kubernetes name`);
  if (priorityValue < 1 || priorityValue >= 1000000000) throw new Error(`${configPath}.${path}.resourceProtection.priorityValue must be between 1 and 999999999`);
  if (batchPriorityValue >= 0 || batchPriorityValue <= -1000000000) throw new Error(`${configPath}.${path}.resourceProtection.batchPriorityValue must be between -999999999 and -1`);
  return {
    priorityClassName,
    priorityValue,
    batchPriorityClassName,
    batchPriorityValue,
    app: containerResources(raw.app, `${path}.resourceProtection.app`),
    redis: containerResources(raw.redis, `${path}.resourceProtection.redis`),
  };
}

export function parseTargetRuntime(value: unknown, path: string): Sub2ApiTargetConfig["runtime"] {
  if (value === undefined || value === null) return { autoSetup: true };
  if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.runtime must be an object`);
  const record = value as Record<string, unknown>;
  return {
    autoSetup: record.autoSetup === undefined ? true : booleanField(record, "autoSetup", `${path}.runtime`),
  };
}

export function parseHostDockerConfig(value: unknown, path: string, runtimeMode: Sub2ApiTargetConfig["runtimeMode"]): Sub2ApiHostDockerConfig | null {
  if (value === undefined || value === null) {
    if (runtimeMode === "host-docker") throw new Error(`${configPath}.${path}.hostDocker is required when runtimeMode=host-docker`);
    return null;
  }
  if (runtimeMode !== "host-docker") throw new Error(`${configPath}.${path}.hostDocker is only supported when runtimeMode=host-docker`);
  if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.hostDocker must be an object`);
  const record = value as Record<string, unknown>;
  const hostDocker: Sub2ApiHostDockerConfig = {
    projectName: stringField(record, "projectName", `${path}.hostDocker`),
    workDir: stringField(record, "workDir", `${path}.hostDocker`),
    composePath: stringField(record, "composePath", `${path}.hostDocker`),
    envPath: stringField(record, "envPath", `${path}.hostDocker`),
    appDataDir: stringField(record, "appDataDir", `${path}.hostDocker`),
    appPort: integerField(record, "appPort", `${path}.hostDocker`),
    redisPort: integerField(record, "redisPort", `${path}.hostDocker`),
    databaseHost: stringField(record, "databaseHost", `${path}.hostDocker`),
    databaseSslMode: stringField(record, "databaseSslMode", `${path}.hostDocker`),
    preserveEnvKeys: record.preserveEnvKeys === undefined ? [] : stringArrayField(record, "preserveEnvKeys", `${path}.hostDocker`),
    noProxy: stringArrayField(record, "noProxy", `${path}.hostDocker`),
  };
  validateProxyName(hostDocker.projectName, `${path}.hostDocker.projectName`);
  for (const key of ["workDir", "composePath", "envPath", "appDataDir"] as const) {
    if (!hostDocker[key].startsWith("/")) throw new Error(`${configPath}.${path}.hostDocker.${key} must be absolute`);
  }
  validatePort(hostDocker.appPort, `${path}.hostDocker.appPort`);
  validatePort(hostDocker.redisPort, `${path}.hostDocker.redisPort`);
  validateHostOrIp(hostDocker.databaseHost, `${path}.hostDocker.databaseHost`);
  if (!/^[A-Za-z0-9_-]+$/u.test(hostDocker.databaseSslMode)) throw new Error(`${configPath}.${path}.hostDocker.databaseSslMode has an unsupported format`);
  for (const key of hostDocker.preserveEnvKeys) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`${configPath}.${path}.hostDocker.preserveEnvKeys contains an unsupported env key`);
  }
  return hostDocker;
}

export function targetImageOverride(record: Record<string, unknown>, path: string): Partial<Sub2ApiConfig["image"]> {
  if (record.image === undefined) return {};
  const image = objectField(record, "image", path);
  const override: Partial<Sub2ApiConfig["image"]> = {};
  if (image.repository !== undefined) {
    const repository = stringField(image, "repository", `${path}.image`);
    if (!isImageRepository(repository)) throw new Error(`${configPath}.${path}.image.repository has an unsupported format`);
    override.repository = repository;
  }
  if (image.tag !== undefined) {
    const tag = stringField(image, "tag", `${path}.image`);
    if (!/^[A-Za-z0-9._-]+$/u.test(tag)) throw new Error(`${configPath}.${path}.image.tag has an unsupported format`);
    override.tag = tag;
  }
  if (image.pullPolicy !== undefined) {
    override.pullPolicy = enumField(image, "pullPolicy", `${path}.image`, ["Always", "IfNotPresent", "Never"] as const);
  }
  return override;
}

export function targetDependencyImageOverride(record: Record<string, unknown>, path: string): Partial<Sub2ApiConfig["dependencyImages"]> {
  if (record.dependencyImages === undefined) return {};
  const dependencyImages = objectField(record, "dependencyImages", path);
  const override: Partial<Sub2ApiConfig["dependencyImages"]> = {};
  if (dependencyImages.postgres !== undefined) {
    const postgres = stringField(dependencyImages, "postgres", `${path}.dependencyImages`);
    if (!isImageReference(postgres)) throw new Error(`${configPath}.${path}.dependencyImages.postgres has an unsupported format`);
    override.postgres = postgres;
  }
  if (dependencyImages.redis !== undefined) {
    const redis = stringField(dependencyImages, "redis", `${path}.dependencyImages`);
    if (!isImageReference(redis)) throw new Error(`${configPath}.${path}.dependencyImages.redis has an unsupported format`);
    override.redis = redis;
  }
  return override;
}

export function parsePublicExposureConfig(value: unknown, path: string): Sub2ApiPublicExposureConfig | null {
  if (value === undefined || value === null) return null;
  if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.publicExposure must be an object`);
  const record = value as Record<string, unknown>;
  const enabled = booleanField(record, "enabled", `${path}.publicExposure`);
  const mode = record.mode === undefined ? "frp" : enumField(record, "mode", `${path}.publicExposure`, ["frp", "pk01-local", "node-local-https"] as const);
  const publicBaseUrl = stringField(record, "publicBaseUrl", `${path}.publicExposure`);
  const dnsRaw = objectField(record, "dns", `${path}.publicExposure`);
  const localRaw = record.local === undefined || record.local === null ? null : objectField(record, "local", `${path}.publicExposure`);
  const localHttpsRaw = record.localHttps === undefined || record.localHttps === null ? null : objectField(record, "localHttps", `${path}.publicExposure`);
  const frpcRaw = record.frpc === undefined || record.frpc === null ? null : objectField(record, "frpc", `${path}.publicExposure`);
  const pk01Raw = record.pk01 === undefined || record.pk01 === null ? null : objectField(record, "pk01", `${path}.publicExposure`);
  const hostname = stringField(dnsRaw, "hostname", `${path}.publicExposure.dns`);
  const expectedA = stringField(dnsRaw, "expectedA", `${path}.publicExposure.dns`);
  const resolvers = stringArrayField(dnsRaw, "resolvers", `${path}.publicExposure.dns`);
  if (mode === "frp" && frpcRaw === null) throw new Error(`${configPath}.${path}.publicExposure.frpc is required when mode=frp`);
  if (mode === "pk01-local" && localRaw === null) throw new Error(`${configPath}.${path}.publicExposure.local is required when mode=pk01-local`);
  if (mode === "node-local-https" && localHttpsRaw === null) throw new Error(`${configPath}.${path}.publicExposure.localHttps is required when mode=node-local-https`);
  if ((mode === "frp" || mode === "pk01-local") && pk01Raw === null) throw new Error(`${configPath}.${path}.publicExposure.pk01 is required when mode=${mode}`);
  const exposure: Sub2ApiPublicExposureConfig = {
    enabled,
    mode,
    publicBaseUrl: normalizePublicBaseUrl(publicBaseUrl, `${path}.publicExposure.publicBaseUrl`),
    dns: {
      hostname,
      expectedA,
      resolvers,
    },
    local: localRaw === null ? null : {
      upstreamHost: stringField(localRaw, "upstreamHost", `${path}.publicExposure.local`),
      upstreamPort: integerField(localRaw, "upstreamPort", `${path}.publicExposure.local`),
    },
    localHttps: localHttpsRaw === null ? null : {
      route: stringField(localHttpsRaw, "route", `${path}.publicExposure.localHttps`),
      upstreamMode: enumField(localHttpsRaw, "upstreamMode", `${path}.publicExposure.localHttps`, ["kubernetes-service-cluster-ip"] as const),
      workDir: stringField(localHttpsRaw, "workDir", `${path}.publicExposure.localHttps`),
      composePath: stringField(localHttpsRaw, "composePath", `${path}.publicExposure.localHttps`),
      projectName: stringField(localHttpsRaw, "projectName", `${path}.publicExposure.localHttps`),
      serviceName: stringField(localHttpsRaw, "serviceName", `${path}.publicExposure.localHttps`),
      containerName: stringField(localHttpsRaw, "containerName", `${path}.publicExposure.localHttps`),
      image: stringField(localHttpsRaw, "image", `${path}.publicExposure.localHttps`),
      hostBind: stringField(localHttpsRaw, "hostBind", `${path}.publicExposure.localHttps`),
      httpsPort: integerField(localHttpsRaw, "httpsPort", `${path}.publicExposure.localHttps`),
      caddyfilePath: stringField(localHttpsRaw, "caddyfilePath", `${path}.publicExposure.localHttps`),
      dataDir: stringField(localHttpsRaw, "dataDir", `${path}.publicExposure.localHttps`),
      configDir: stringField(localHttpsRaw, "configDir", `${path}.publicExposure.localHttps`),
      tlsMode: enumField(localHttpsRaw, "tlsMode", `${path}.publicExposure.localHttps`, ["internal"] as const),
      responseHeaderTimeoutSeconds: integerField(localHttpsRaw, "responseHeaderTimeoutSeconds", `${path}.publicExposure.localHttps`),
    },
    frpc: frpcRaw === null ? null : {
      deploymentName: stringField(frpcRaw, "deploymentName", `${path}.publicExposure.frpc`),
      secretName: stringField(frpcRaw, "secretName", `${path}.publicExposure.frpc`),
      secretKey: stringField(frpcRaw, "secretKey", `${path}.publicExposure.frpc`),
      image: stringField(frpcRaw, "image", `${path}.publicExposure.frpc`),
      serverAddr: stringField(frpcRaw, "serverAddr", `${path}.publicExposure.frpc`),
      serverPort: integerField(frpcRaw, "serverPort", `${path}.publicExposure.frpc`),
      proxyName: stringField(frpcRaw, "proxyName", `${path}.publicExposure.frpc`),
      remotePort: integerField(frpcRaw, "remotePort", `${path}.publicExposure.frpc`),
      localIP: stringField(frpcRaw, "localIP", `${path}.publicExposure.frpc`),
      localPort: integerField(frpcRaw, "localPort", `${path}.publicExposure.frpc`),
      tokenSourceRef: stringField(frpcRaw, "tokenSourceRef", `${path}.publicExposure.frpc`),
      tokenSourceKey: stringField(frpcRaw, "tokenSourceKey", `${path}.publicExposure.frpc`),
    },
    pk01: pk01Raw === null ? null : {
      route: stringField(pk01Raw, "route", `${path}.publicExposure.pk01`),
      caddyBinaryPath: stringField(pk01Raw, "caddyBinaryPath", `${path}.publicExposure.pk01`),
      caddyDownloadUrl: stringField(pk01Raw, "caddyDownloadUrl", `${path}.publicExposure.pk01`),
      caddyDownloadProxyUrl: pk01Raw.caddyDownloadProxyUrl === undefined ? null : stringField(pk01Raw, "caddyDownloadProxyUrl", `${path}.publicExposure.pk01`),
      caddyConfigPath: stringField(pk01Raw, "caddyConfigPath", `${path}.publicExposure.pk01`),
      caddyServiceName: stringField(pk01Raw, "caddyServiceName", `${path}.publicExposure.pk01`),
      caddyStorageDir: stringField(pk01Raw, "caddyStorageDir", `${path}.publicExposure.pk01`),
      caddyEmail: stringField(pk01Raw, "caddyEmail", `${path}.publicExposure.pk01`),
      pikanodeRoot: stringField(pk01Raw, "pikanodeRoot", `${path}.publicExposure.pk01`),
      pikanodeContainerName: stringField(pk01Raw, "pikanodeContainerName", `${path}.publicExposure.pk01`),
      pikanodeImage: stringField(pk01Raw, "pikanodeImage", `${path}.publicExposure.pk01`),
      pikanodeHttpHostPort: integerField(pk01Raw, "pikanodeHttpHostPort", `${path}.publicExposure.pk01`),
      responseHeaderTimeoutSeconds: integerField(pk01Raw, "responseHeaderTimeoutSeconds", `${path}.publicExposure.pk01`),
    },
  };
  validatePublicExposureConfig(exposure, path);
  return exposure;
}

export function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgressProxyConfig | null {
  if (value === undefined || value === null) return null;
  if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.egressProxy must be an object`);
  const record = value as Record<string, unknown>;
  const sourceConfigRef = optionalConfigStringField(record, "sourceConfigRef", `${path}.egressProxy`);
  const source = sourceConfigRef === null ? null : resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.${path}.egressProxy.sourceConfigRef`);
  const sourceType = source?.sourceType ?? enumField(record, "sourceType", `${path}.egressProxy`, ["subscription-url", "master-shadowsocks"] as const);
  assertOptionalMatch(record, "sourceType", sourceType, `${path}.egressProxy`);
  const preferredOutbound = sourceType === "subscription-url" || record.preferredOutbound !== undefined
    ? source?.preferredOutbound ?? enumField(record, "preferredOutbound", `${path}.egressProxy`, ["vless-reality", "hysteria2"] as const)
    : null;
  if (source !== null && source.preferredOutbound !== null && record.preferredOutbound !== undefined) assertOptionalMatch(record, "preferredOutbound", source.preferredOutbound, `${path}.egressProxy`);
  const masterShadowsocks = sourceType === "master-shadowsocks"
    ? source?.masterShadowsocks ?? parseMasterShadowsocksConfig(record.masterShadowsocks, `${path}.egressProxy.masterShadowsocks`)
    : null;
  const imagePullPolicy = enumField(record, "imagePullPolicy", `${path}.egressProxy`, ["Always", "IfNotPresent", "Never"] as const);
  const noProxy = stringArrayField(record, "noProxy", `${path}.egressProxy`);
  const proxy: Sub2ApiEgressProxyConfig = {
    enabled: booleanField(record, "enabled", `${path}.egressProxy`),
    deploymentName: stringField(record, "deploymentName", `${path}.egressProxy`),
    serviceName: stringField(record, "serviceName", `${path}.egressProxy`),
    secretName: stringField(record, "secretName", `${path}.egressProxy`),
    secretKey: stringField(record, "secretKey", `${path}.egressProxy`),
    image: stringField(record, "image", `${path}.egressProxy`),
    imagePullPolicy,
    listenPort: integerField(record, "listenPort", `${path}.egressProxy`),
    hostNetwork: booleanField(record, "hostNetwork", `${path}.egressProxy`),
    sourceConfigRef,
    sourceFingerprint: source?.fingerprint ?? null,
    sourceRef: source?.sourceRef ?? stringField(record, "sourceRef", `${path}.egressProxy`),
    sourceKey: source?.sourceKey ?? stringField(record, "sourceKey", `${path}.egressProxy`),
    sourceType,
    preferredOutbound,
    masterShadowsocks,
    noProxy,
    applyToSub2Api: booleanField(record, "applyToSub2Api", `${path}.egressProxy`),
    applyToSentinel: booleanField(record, "applyToSentinel", `${path}.egressProxy`),
    healthProbeUrl: source?.healthProbeUrl ?? stringField(record, "healthProbeUrl", `${path}.egressProxy`),
  };
  if (source !== null) {
    assertOptionalMatch(record, "sourceRef", source.sourceRef, `${path}.egressProxy`);
    assertOptionalMatch(record, "sourceKey", source.sourceKey, `${path}.egressProxy`);
    assertOptionalMatch(record, "healthProbeUrl", source.healthProbeUrl, `${path}.egressProxy`);
  }
  validateEgressProxyConfig(proxy, path);
  return proxy;
}

export function parseAccountLocalProxyConfig(value: unknown, path: string): Sub2ApiAccountLocalProxyConfig | null {
  if (value === undefined || value === null) return null;
  if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.accountLocalProxy must be an object`);
  const record = value as Record<string, unknown>;
  const sourceConfigRef = stringField(record, "sourceConfigRef", `${path}.accountLocalProxy`);
  const source = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.${path}.accountLocalProxy.sourceConfigRef`);
  if (source.sourceType !== "master-shadowsocks" || source.masterShadowsocks === null) {
    throw new Error(`${configPath}.${path}.accountLocalProxy.sourceConfigRef must reference a master-shadowsocks source`);
  }
  const proxy: Sub2ApiAccountLocalProxyConfig = {
    enabled: booleanField(record, "enabled", `${path}.accountLocalProxy`),
    containerName: stringField(record, "containerName", `${path}.accountLocalProxy`),
    secretName: stringField(record, "secretName", `${path}.accountLocalProxy`),
    secretKey: stringField(record, "secretKey", `${path}.accountLocalProxy`),
    image: stringField(record, "image", `${path}.accountLocalProxy`),
    imagePullPolicy: enumField(record, "imagePullPolicy", `${path}.accountLocalProxy`, ["Always", "IfNotPresent", "Never"] as const),
    listenHost: enumField(record, "listenHost", `${path}.accountLocalProxy`, ["127.0.0.1"] as const),
    listenPort: integerField(record, "listenPort", `${path}.accountLocalProxy`),
    sourceConfigRef,
    sourceFingerprint: source.fingerprint,
    sourceRef: source.sourceRef,
    sourceKey: source.sourceKey,
    sourceType: "master-shadowsocks",
    masterShadowsocks: source.masterShadowsocks,
    healthProbeUrl: source.healthProbeUrl,
  };
  validateAccountLocalProxyConfig(proxy, path);
  return proxy;
}

export function parseMasterShadowsocksConfig(value: unknown, path: string): Sub2ApiMasterShadowsocksConfig {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.${path} must be an object`);
  const record = value as Record<string, unknown>;
  return {
    serverHost: stringField(record, "serverHost", path),
    serverPort: integerField(record, "serverPort", path),
    method: stringField(record, "method", path),
  };
}

export function parseRuntime(root: Record<string, unknown>): Sub2ApiConfig["runtime"] {
  const value = root.runtime;
  if (value === undefined) throw new Error(`${configPath}.runtime must be an object`);
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.runtime must be an object`);
  const runtime = value as Record<string, unknown>;
  const database = objectField(runtime, "database", "runtime");
  const databaseMode = enumField(database, "mode", "runtime.database", ["external"] as const);
  const sourceRef = stringField(database, "sourceRef", "runtime.database");
  const sourceKeysRaw = objectField(database, "sourceKeys", "runtime.database");
  const sourceKeys = {
    user: stringField(sourceKeysRaw, "user", "runtime.database.sourceKeys"),
    password: stringField(sourceKeysRaw, "password", "runtime.database.sourceKeys"),
    dbName: stringField(sourceKeysRaw, "dbName", "runtime.database.sourceKeys"),
  };
  const secret = stringField(database, "secretName", "runtime.database");
  const passwordKey = stringField(database, "passwordKey", "runtime.database");
  const host = stringField(database, "host", "runtime.database");
  const port = integerField(database, "port", "runtime.database");
  const user = stringField(database, "user", "runtime.database");
  const dbName = stringField(database, "dbName", "runtime.database");
  const sslMode = stringField(database, "sslMode", "runtime.database");
  const pendingAllowed = booleanField(database, "pendingAllowed", "runtime.database");
  if (!isKubernetesName(secret)) throw new Error(`${configPath}.runtime.database.secretName must be a Kubernetes Secret name`);
  if (!/^[A-Za-z0-9_./-]+$/u.test(sourceRef)) throw new Error(`${configPath}.runtime.database.sourceRef has an unsupported format`);
  for (const [key, value] of Object.entries(sourceKeys)) {
    if (!/^[A-Z0-9_]+$/u.test(value)) throw new Error(`${configPath}.runtime.database.sourceKeys.${key} must be an env key`);
  }
  if (!/^[A-Z0-9_]+$/u.test(passwordKey)) throw new Error(`${configPath}.runtime.database.passwordKey must be an env key`);
  if (!/^[A-Za-z0-9._-]+$/u.test(host)) throw new Error(`${configPath}.runtime.database.host has an unsupported format`);
  if (!/^[A-Za-z0-9_][-A-Za-z0-9_]*$/u.test(user)) throw new Error(`${configPath}.runtime.database.user has an unsupported format`);
  if (!/^[A-Za-z0-9_][-A-Za-z0-9_]*$/u.test(dbName)) throw new Error(`${configPath}.runtime.database.dbName has an unsupported format`);
  if (!/^[A-Za-z0-9_-]+$/u.test(sslMode)) throw new Error(`${configPath}.runtime.database.sslMode has an unsupported format`);
  if (port < 1 || port > 65535) throw new Error(`${configPath}.runtime.database.port must be a TCP port`);
  if (databaseMode !== "external") throw new Error(`${configPath}.runtime.database.mode must be external`);
  const redis = objectField(runtime, "redis", "runtime");
  const redisServiceName = stringField(redis, "serviceName", "runtime.redis");
  const redisPersistence = booleanField(redis, "persistence", "runtime.redis");
  if (!isKubernetesName(redisServiceName)) throw new Error(`${configPath}.runtime.redis.serviceName must be a Kubernetes Service name`);
  const secretsRaw = objectField(runtime, "secrets", "runtime");
  const secretRoot = stringField(secretsRaw, "root", "runtime.secrets");
  const appSourceRef = stringField(secretsRaw, "appSourceRef", "runtime.secrets");
  if (!/^[A-Za-z0-9_./-]+$/u.test(secretRoot)) throw new Error(`${configPath}.runtime.secrets.root has an unsupported format`);
  if (!/^[A-Za-z0-9_./-]+$/u.test(appSourceRef)) throw new Error(`${configPath}.runtime.secrets.appSourceRef has an unsupported format`);
  const appData = objectField(runtime, "appData", "runtime");
  const appDataMode = enumField(appData, "mode", "runtime.appData", ["persistent-pvc", "empty-dir"] as const);
  const sentinel = objectField(runtime, "sentinel", "runtime");
  const sentinelMode = enumField(sentinel, "mode", "runtime.sentinel", ["singleton"] as const);
  const enabledOnTargets = stringArrayField(sentinel, "enabledOnTargets", "runtime.sentinel");
  if (sentinelMode !== "singleton") throw new Error(`${configPath}.runtime.sentinel.mode must be singleton`);
  return {
    database: { mode: "external", sourceRef, sourceKeys, secretName: secret, passwordKey, host, port, user, dbName, sslMode, pendingAllowed },
    secrets: { root: secretRoot, appSourceRef },
    redis: { serviceName: redisServiceName, persistence: redisPersistence },
    appData: { mode: appDataMode },
    sentinel: { mode: "singleton", enabledOnTargets },
  };
}

export function stringField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = obj[key];
  if (typeof value !== "string" || value.length === 0) throw new Error(`${fieldLabel(path, key)} must be a non-empty string`);
  return value;
}

export function objectField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
  const value = obj[key];
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${fieldLabel(path, key)} must be an object`);
  return value as Record<string, unknown>;
}

export function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
  return yamlBooleanField(obj, key, configPath, path);
}

export function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
  const value = obj[key];
  if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim().length > 0)) {
    throw new Error(`${fieldLabel(path, key)} must be an array of non-empty strings`);
  }
  return value.map((item) => item.trim());
}

function optionalConfigStringField(obj: Record<string, unknown>, key: string, path: string): string | null {
  const value = obj[key];
  if (value === undefined || value === null) return null;
  if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${fieldLabel(path, key)} must be a non-empty string when set`);
  return value.trim();
}

function assertOptionalMatch(obj: Record<string, unknown>, key: string, expected: string | null, path: string): void {
  if (obj[key] === undefined || obj[key] === null) return;
  const actual = stringField(obj, key, path);
  if (actual !== expected) throw new Error(`${fieldLabel(path, key)} must match sourceConfigRef value ${expected}`);
}

export function enumField<const T extends readonly string[]>(obj: Record<string, unknown>, key: string, path: string, values: T): T[number] {
  const value = stringField(obj, key, path);
  if (!(values as readonly string[]).includes(value)) throw new Error(`${fieldLabel(path, key)} must be one of ${values.join(", ")}`);
  return value as T[number];
}

export function integerField(obj: Record<string, unknown>, key: string, path: string): number {
  return yamlIntegerField(obj, key, configPath, path);
}

export function integerArrayField(obj: Record<string, unknown>, key: string, path: string): number[] {
  const value = obj[key];
  if (!Array.isArray(value) || !value.every((item) => typeof item === "number" && Number.isInteger(item))) {
    throw new Error(`${fieldLabel(path, key)} must be an array of integers`);
  }
  return value;
}

export function fieldLabel(path: string, key: string): string {
  return yamlFieldLabel(configPath, path, key);
}

export function isKubernetesName(value: string): boolean {
  return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value);
}

export function isImageRepository(value: string): boolean {
  return /^[A-Za-z0-9._:/-]+$/u.test(value) && !value.includes("..") && !value.includes("@") && !value.endsWith(":");
}

export function isImageReference(value: string): boolean {
  return /^[A-Za-z0-9._:/-]+$/u.test(value) && value.includes(":") && !value.includes("..");
}

export function validatePublicExposureConfig(config: Sub2ApiPublicExposureConfig, path: string): void {
  const url = new URL(config.publicBaseUrl);
  if (url.protocol !== "https:") throw new Error(`${configPath}.${path}.publicExposure.publicBaseUrl must use https://`);
  if (url.hostname !== config.dns.hostname) throw new Error(`${configPath}.${path}.publicExposure.publicBaseUrl hostname must match publicExposure.dns.hostname`);
  validateHostname(config.dns.hostname, `${path}.publicExposure.dns.hostname`);
  if (!/^[0-9.]+$/u.test(config.dns.expectedA)) throw new Error(`${configPath}.${path}.publicExposure.dns.expectedA must be an IPv4 address`);
  for (const resolver of config.dns.resolvers) {
    if (!/^[0-9.]+$/u.test(resolver)) throw new Error(`${configPath}.${path}.publicExposure.dns.resolvers entries must be IPv4 resolvers`);
  }
  if (config.mode === "frp") {
    if (config.frpc === null) throw new Error(`${configPath}.${path}.publicExposure.frpc is required when mode=frp`);
    validateKubernetesResourceName(config.frpc.deploymentName, `${path}.publicExposure.frpc.deploymentName`);
    validateKubernetesResourceName(config.frpc.secretName, `${path}.publicExposure.frpc.secretName`);
    if (config.frpc.secretKey !== "frpc.toml") throw new Error(`${configPath}.${path}.publicExposure.frpc.secretKey must be frpc.toml`);
    if (!isImageReference(config.frpc.image)) throw new Error(`${configPath}.${path}.publicExposure.frpc.image has an unsupported format`);
    validateHostOrIp(config.frpc.serverAddr, `${path}.publicExposure.frpc.serverAddr`);
    validatePort(config.frpc.serverPort, `${path}.publicExposure.frpc.serverPort`);
    validateProxyName(config.frpc.proxyName, `${path}.publicExposure.frpc.proxyName`);
    validatePort(config.frpc.remotePort, `${path}.publicExposure.frpc.remotePort`);
    validateHostOrIp(config.frpc.localIP, `${path}.publicExposure.frpc.localIP`);
    validatePort(config.frpc.localPort, `${path}.publicExposure.frpc.localPort`);
    if (!/^[A-Za-z0-9_./-]+$/u.test(config.frpc.tokenSourceRef)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceRef has an unsupported format`);
    if (!/^[A-Z0-9_]+$/u.test(config.frpc.tokenSourceKey)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceKey must be an env key`);
  }
  if (config.mode === "pk01-local") {
    if (config.local === null) throw new Error(`${configPath}.${path}.publicExposure.local is required when mode=pk01-local`);
    validateHostOrIp(config.local.upstreamHost, `${path}.publicExposure.local.upstreamHost`);
    validatePort(config.local.upstreamPort, `${path}.publicExposure.local.upstreamPort`);
  }
  if (config.mode === "node-local-https") {
    const localHttps = config.localHttps;
    if (localHttps === null) throw new Error(`${configPath}.${path}.publicExposure.localHttps is required when mode=node-local-https`);
    if (!/^[A-Za-z0-9:_./-]+$/u.test(localHttps.route)) throw new Error(`${configPath}.${path}.publicExposure.localHttps.route has an unsupported format`);
    for (const key of ["workDir", "composePath", "caddyfilePath", "dataDir", "configDir"] as const) {
      if (!localHttps[key].startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.localHttps.${key} must be absolute`);
    }
    validateProxyName(localHttps.projectName, `${path}.publicExposure.localHttps.projectName`);
    validateProxyName(localHttps.serviceName, `${path}.publicExposure.localHttps.serviceName`);
    validateProxyName(localHttps.containerName, `${path}.publicExposure.localHttps.containerName`);
    if (!isImageReference(localHttps.image)) throw new Error(`${configPath}.${path}.publicExposure.localHttps.image has an unsupported format`);
    validateHostOrIp(localHttps.hostBind, `${path}.publicExposure.localHttps.hostBind`);
    validatePort(localHttps.httpsPort, `${path}.publicExposure.localHttps.httpsPort`);
    if (localHttps.responseHeaderTimeoutSeconds < 1) throw new Error(`${configPath}.${path}.publicExposure.localHttps.responseHeaderTimeoutSeconds must be >= 1`);
  }
  if (config.pk01 === null) return;
  if (!/^[A-Za-z0-9:_./-]+$/u.test(config.pk01.route)) throw new Error(`${configPath}.${path}.publicExposure.pk01.route has an unsupported format`);
  if (!config.pk01.caddyBinaryPath.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyBinaryPath must be absolute`);
  if (!/^https:\/\//u.test(config.pk01.caddyDownloadUrl)) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyDownloadUrl must use https://`);
  if (config.pk01.caddyDownloadProxyUrl !== null) validateProxyUrl(config.pk01.caddyDownloadProxyUrl, `${path}.publicExposure.pk01.caddyDownloadProxyUrl`);
  if (!config.pk01.caddyConfigPath.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyConfigPath must be absolute`);
  validateProxyName(config.pk01.caddyServiceName, `${path}.publicExposure.pk01.caddyServiceName`);
  if (!config.pk01.caddyStorageDir.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyStorageDir must be absolute`);
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/u.test(config.pk01.caddyEmail)) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyEmail must be an email address`);
  if (!config.pk01.pikanodeRoot.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.pikanodeRoot must be absolute`);
  validateProxyName(config.pk01.pikanodeContainerName, `${path}.publicExposure.pk01.pikanodeContainerName`);
  if (!isImageRepository(config.pk01.pikanodeImage) && !isImageReference(config.pk01.pikanodeImage)) throw new Error(`${configPath}.${path}.publicExposure.pk01.pikanodeImage has an unsupported format`);
  validatePort(config.pk01.pikanodeHttpHostPort, `${path}.publicExposure.pk01.pikanodeHttpHostPort`);
  if (config.pk01.responseHeaderTimeoutSeconds < 1) throw new Error(`${configPath}.${path}.publicExposure.pk01.responseHeaderTimeoutSeconds must be >= 1`);
}

export function validateEgressProxyConfig(config: Sub2ApiEgressProxyConfig, path: string): void {
  validateKubernetesResourceName(config.deploymentName, `${path}.egressProxy.deploymentName`);
  validateKubernetesResourceName(config.serviceName, `${path}.egressProxy.serviceName`);
  validateKubernetesResourceName(config.secretName, `${path}.egressProxy.secretName`);
  if (config.secretKey !== "config.json") throw new Error(`${configPath}.${path}.egressProxy.secretKey must be config.json`);
  if (!isImageReference(config.image)) throw new Error(`${configPath}.${path}.egressProxy.image has an unsupported format`);
  validatePort(config.listenPort, `${path}.egressProxy.listenPort`);
  if (config.sourceConfigRef !== null && (!config.sourceConfigRef.startsWith("config/") || !config.sourceConfigRef.includes("#sources.") || config.sourceConfigRef.includes(".."))) {
    throw new Error(`${configPath}.${path}.egressProxy.sourceConfigRef must reference config/*.yaml#sources.<id>`);
  }
  if (!/^[A-Za-z0-9_./-]+$/u.test(config.sourceRef)) throw new Error(`${configPath}.${path}.egressProxy.sourceRef has an unsupported format`);
  if (!/^[A-Z0-9_]+$/u.test(config.sourceKey)) throw new Error(`${configPath}.${path}.egressProxy.sourceKey must be an env key`);
  if (config.sourceType === "subscription-url" && config.preferredOutbound === null) {
    throw new Error(`${configPath}.${path}.egressProxy.preferredOutbound is required for sourceType=subscription-url`);
  }
  if (config.sourceType === "master-shadowsocks") {
    if (config.masterShadowsocks === null) throw new Error(`${configPath}.${path}.egressProxy.masterShadowsocks is required for sourceType=master-shadowsocks`);
    validateHostOrIp(config.masterShadowsocks.serverHost, `${path}.egressProxy.masterShadowsocks.serverHost`);
    validatePort(config.masterShadowsocks.serverPort, `${path}.egressProxy.masterShadowsocks.serverPort`);
    if (!/^[A-Za-z0-9-]+$/u.test(config.masterShadowsocks.method)) throw new Error(`${configPath}.${path}.egressProxy.masterShadowsocks.method has an unsupported format`);
  }
  for (const entry of config.noProxy) {
    if (!/^[A-Za-z0-9.:_/*-]+$/u.test(entry)) throw new Error(`${configPath}.${path}.egressProxy.noProxy contains an unsupported entry`);
  }
  const url = new URL(config.healthProbeUrl);
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path}.egressProxy.healthProbeUrl must use http:// or https://`);
}

export function validateAccountLocalProxyConfig(config: Sub2ApiAccountLocalProxyConfig, path: string): void {
  validateKubernetesResourceName(config.containerName, `${path}.accountLocalProxy.containerName`);
  validateKubernetesResourceName(config.secretName, `${path}.accountLocalProxy.secretName`);
  if (config.secretKey !== "config.json") throw new Error(`${configPath}.${path}.accountLocalProxy.secretKey must be config.json`);
  if (!isImageReference(config.image)) throw new Error(`${configPath}.${path}.accountLocalProxy.image has an unsupported format`);
  if (config.listenHost !== "127.0.0.1") throw new Error(`${configPath}.${path}.accountLocalProxy.listenHost must be 127.0.0.1`);
  validatePort(config.listenPort, `${path}.accountLocalProxy.listenPort`);
  if (!config.sourceConfigRef.startsWith("config/") || !config.sourceConfigRef.includes("#sources.") || config.sourceConfigRef.includes("..")) {
    throw new Error(`${configPath}.${path}.accountLocalProxy.sourceConfigRef must reference config/*.yaml#sources.<id>`);
  }
  if (!/^[A-Za-z0-9_./-]+$/u.test(config.sourceRef)) throw new Error(`${configPath}.${path}.accountLocalProxy.sourceRef has an unsupported format`);
  if (!/^[A-Z0-9_]+$/u.test(config.sourceKey)) throw new Error(`${configPath}.${path}.accountLocalProxy.sourceKey must be an env key`);
  validateHostOrIp(config.masterShadowsocks.serverHost, `${path}.accountLocalProxy.masterShadowsocks.serverHost`);
  validatePort(config.masterShadowsocks.serverPort, `${path}.accountLocalProxy.masterShadowsocks.serverPort`);
  if (!/^[A-Za-z0-9-]+$/u.test(config.masterShadowsocks.method)) throw new Error(`${configPath}.${path}.accountLocalProxy.masterShadowsocks.method has an unsupported format`);
  const url = new URL(config.healthProbeUrl);
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path}.accountLocalProxy.healthProbeUrl must use http:// or https://`);
}
