// SPEC: PJ2026-01060305 AgentRun execution policy + PJ2026-01020108 cancel lifecycle draft-2026-06-25-p0.
// Parses AgentRun YAML lane policy, including cancel lifecycle values owned by config/agentrun.yaml.
import { rootPath } from "./config";
import {
  asRecord,
  arrayField,
  booleanField,
  integerField,
  optionalIntegerField,
  optionalStringField,
  readYamlRecord,
  recordField,
  stringArrayField,
  stringField,
} from "./platform-infra-ops-library";
import { materializeYamlComposition } from "./yaml-composition";
import { resolveConfigRef } from "./ops/config-refs";

export const AGENTRUN_CONFIG_PATH = "config/agentrun.yaml";

export interface AgentRunGitMirrorRepositorySpec {
  readonly key: string;
  readonly repository: string;
  readonly remote: string;
  readonly sourceBranch: string;
  readonly additionalSourceBranches: readonly string[];
  readonly gitopsBranch?: string;
}

export interface AgentRunManagedRepositoryReconcilerSpec {
  readonly enabled: boolean;
  readonly deploymentName: string;
  readonly configMapName: string;
  readonly serviceAccountName: string;
  readonly remoteAuth: {
    readonly configRef: string;
    readonly capabilitySha256: string;
    readonly apiVersion: "unidesk.ai/v1";
    readonly kind: "GitFetchCredential";
    readonly authMode: "github-https-token";
    readonly host: "github.com";
    readonly secretRef: {
      readonly namespace: string;
      readonly name: string;
      readonly key: string;
    };
  };
  readonly hostNetwork: boolean;
  readonly dnsPolicy: "ClusterFirst" | "ClusterFirstWithHostNet" | "Default";
  readonly imagePullPolicy: "Always" | "IfNotPresent" | "Never";
  readonly cacheMountPath: string;
  readonly stateDirectory: string;
  readonly reconcileIntervalMs: number;
  readonly fetchTimeoutMs: number;
  readonly shutdownGraceMs: number;
  readonly maxConcurrentRepositories: number;
  readonly retry: {
    readonly maxAttempts: number;
    readonly initialDelayMs: number;
    readonly maxDelayMs: number;
  };
  readonly freshness: {
    readonly maxAgeMs: number;
  };
  readonly readiness: {
    readonly initialDelaySeconds: number;
    readonly periodSeconds: number;
    readonly timeoutSeconds: number;
    readonly failureThreshold: number;
  };
  readonly status: {
    readonly defaultRepositoryLimit: number;
  };
  readonly lifecycle: {
    readonly undeclaredRepositoryPolicy: "retain";
    readonly managedRefs: "source-branch-only";
  };
  readonly resources: AgentRunContainerResources;
}

export interface AgentRunSourceAuthoritySpec {
  readonly mode: "gitMirrorSnapshot";
  readonly resolver: "k8s-git-mirror";
  readonly allowHostGit: false;
  readonly allowHostWorkspace: false;
  readonly allowGithubDirectInPipeline: false;
}

export interface AgentRunSourceSnapshotSpec {
  readonly stageRefPrefix: string;
  readonly missingObjectPolicy: "fail-fast";
  readonly refreshPolicy: "sync-before-snapshot";
}

export interface AgentRunSecretRef {
  readonly namespace: string;
  readonly name: string;
  readonly key: string;
}

export interface AgentRunLaneSecretSpec {
  readonly id: string;
  readonly sourceRef: string;
  readonly sourceMode: "env" | "file" | "codex-config" | "json";
  readonly sourceKey: string | null;
  readonly sourceFormat: "env" | "raw-token" | null;
  readonly transform: "codex-auth-json-openai-api-key" | null;
  readonly codexConfig: AgentRunCodexConfigSpec | null;
  readonly jsonValue: unknown;
  readonly targetRef: AgentRunSecretRef;
  readonly providerCredentialProfile: string | null;
}

export interface AgentRunToolCredentialSpec {
  readonly id: string;
  readonly tool: string;
  readonly purpose: string;
  readonly secretRef: {
    readonly namespace: string;
    readonly name: string;
    readonly keys: readonly string[];
  };
  readonly projection:
    | { readonly kind: "env"; readonly envName: string; readonly secretKey: string }
    | { readonly kind: "volume"; readonly mountPath: string };
}

export interface AgentRunCodexConfigSpec {
  readonly modelProvider: string;
  readonly model: string;
  readonly reviewModel: string | null;
  readonly modelReasoningEffort: string | null;
  readonly disableResponseStorage: boolean | null;
  readonly networkAccess: string | null;
  readonly windowsWslSetupAcknowledged: boolean | null;
  readonly serviceTier: string | null;
  readonly modelContextWindow: number | null;
  readonly modelAutoCompactTokenLimit: number | null;
  readonly modelCatalogJson: string | null;
  readonly approvalsReviewer: string | null;
  readonly noticeHideFullAccessWarning: boolean | null;
  readonly modelProviders: readonly AgentRunCodexModelProviderSpec[];
  readonly features: Readonly<Record<string, boolean>>;
  readonly projects: readonly AgentRunCodexProjectSpec[];
  readonly tuiModelAvailabilityNux: Readonly<Record<string, number>>;
}

export interface AgentRunCodexModelProviderSpec {
  readonly id: string;
  readonly name: string;
  readonly baseUrl: string;
  readonly wireApi: string;
  readonly requiresOpenaiAuth: boolean | null;
}

export interface AgentRunCodexProjectSpec {
  readonly path: string;
  readonly trustLevel: string;
}

export interface AgentRunProviderCredentialRef {
  readonly profile: string;
  readonly secretRef: {
    readonly namespace: string;
    readonly name: string;
    readonly keys: readonly string[];
  };
}

export interface AgentRunLaneSpec {
  readonly lane: string;
  readonly nodeId: string;
  readonly nodeRoute: string;
  readonly nodeKubeRoute: string;
  readonly nodeUnideskWorkspace: string;
  readonly version: string;
  readonly status: {
    readonly readOnlyCapture: {
      readonly timeoutMs: number;
      readonly configPath: string;
    };
  };
  readonly source: {
    readonly statusMode: "host-worktree" | "k3s-git-mirror";
    readonly repository: string;
    readonly worktreeRemote: string;
    readonly branch: string;
    readonly sourceAuthority: AgentRunSourceAuthoritySpec | null;
    readonly sourceSnapshot: AgentRunSourceSnapshotSpec | null;
    readonly bootstrapFromBranch: string | null;
    readonly bootstrapTimeoutSeconds: number;
    readonly bootstrapPollSeconds: number;
    readonly remote: string;
    readonly workspace: string;
  };
  readonly runtime: {
    readonly namespace: string;
    readonly managerDeployment: string;
    readonly managerService: string;
    readonly managerPort: number;
    readonly internalBaseUrl: string;
  };
  readonly ci: {
    readonly namespace: string;
    readonly pipeline: string;
    readonly pipelineRunPrefix: string;
    readonly serviceAccountName: string;
    readonly registryPrefix: string;
    readonly toolsImage: string;
    readonly buildkitImage: string | null;
  };
  readonly gitops: {
    readonly branch: string;
    readonly path: string;
    readonly argoNamespace: string;
    readonly argoApplication: string;
    readonly repoURL: string;
  };
  readonly deployment: {
    readonly format: "unidesk-yaml-only";
    readonly gitopsRoot: string;
    readonly runtimeRenderDir: string;
    readonly artifactCatalogPath: string;
    readonly argocd: {
      readonly project: string;
      readonly applicationFile: string;
    };
    readonly manager: {
      readonly serviceAccount: string;
      readonly apiKeySecretRef: { readonly name: string; readonly key: string };
      readonly env: Readonly<Record<string, string>>;
      readonly unideskSshEndpointEnv: { readonly name: string; readonly value: string } | null;
      readonly bootRepoUrl: string;
      readonly imageBuild: AgentRunImageBuildSpec;
      readonly resources: AgentRunContainerResources;
    };
    readonly runner: {
      readonly serviceAccount: string;
      readonly jobNamePrefix: string;
      readonly idleTimeoutMs: number;
      readonly backendRetry: {
        readonly maxAttempts: number;
        readonly initialBackoffMs: number;
        readonly maxBackoffMs: number;
      };
      readonly apiKeySecretRef: { readonly name: string; readonly key: string };
      readonly egressProxyUrl: string | null;
      readonly noProxyExtra: readonly string[];
      readonly retention: AgentRunRunnerRetentionSpec;
      readonly cancelLifecycle: AgentRunCancelLifecycleSpec;
    };
    readonly localPostgres: {
      readonly enabled: boolean;
      readonly serviceName: string | null;
      readonly image: string | null;
      readonly storage: string | null;
      readonly port: number | null;
      readonly database: string | null;
      readonly user: string | null;
      readonly passwordSourceRef: string | null;
      readonly passwordSourceKey: string | null;
    };
  };
  readonly gitMirror: {
    readonly namespace: string;
    readonly readService: string;
    readonly readDeployment: string;
    readonly writeService: string;
    readonly writeDeployment: string;
    readonly resourceBundleBaseUrl: string;
    readonly readUrl: string;
    readonly writeUrl: string;
    readonly cachePvc: string;
    readonly cacheHostPath: string | null;
    readonly sshSecretName: string;
    readonly githubProxy: {
      readonly host: string;
      readonly port: number;
    };
    readonly toolsImage: string;
    readonly syncJobPrefix: string;
    readonly flushJobPrefix: string;
    readonly repositoryReconciler: AgentRunManagedRepositoryReconcilerSpec;
    readonly repositories: readonly AgentRunGitMirrorRepositorySpec[];
  };
  readonly database: {
    readonly mode: "local-postgres" | "external-postgres";
    readonly provider: string | null;
    readonly configRef: string | null;
    readonly database: string | null;
    readonly user: string | null;
    readonly sslmode: "require" | null;
    readonly secretSourceRef: string | null;
    readonly secretRef: { readonly name: string; readonly key: string };
    readonly localPostgresExpectedAbsent: boolean;
  };
  readonly secrets: readonly AgentRunLaneSecretSpec[];
  readonly toolCredentials: readonly AgentRunToolCredentialSpec[];
}

export interface AgentRunContainerResources {
  readonly requests: {
    readonly cpu: string;
    readonly memory: string;
  };
  readonly limits: {
    readonly cpu: string;
    readonly memory: string;
  };
}

export interface AgentRunImageBuildSpec {
  readonly context: string;
  readonly containerfile: string;
  readonly repository: string;
  readonly network: string;
  readonly buildArgs: Readonly<Record<string, string>>;
  readonly httpProxy: string | null;
  readonly httpsProxy: string | null;
  readonly noProxy: readonly string[];
  readonly buildContainerProxy: {
    readonly httpProxy: string | null;
    readonly httpsProxy: string | null;
    readonly noProxy: readonly string[];
  };
  readonly envIdentityFiles: readonly string[];
  readonly timeoutSeconds: number;
  readonly pollSeconds: number;
}

export interface AgentRunRunnerRetentionSpec {
  readonly maxRunners: number;
  readonly cleanupOrder: "oldest-inactive-last-active-first";
  readonly activeHeartbeatMaxAgeMs: number;
  readonly stalePendingMaxAgeMs: number;
  readonly selectors: {
    readonly matchLabels: Readonly<Record<string, string>>;
    readonly jobNamePrefixes: readonly string[];
  };
  readonly ageBasedCleanup: {
    readonly enabled: boolean;
    readonly maxAgeHours: number | null;
  };
  readonly sessionPvcRetention: {
    readonly enabled: boolean;
    readonly prefixes: readonly string[];
    readonly maxDeletePerRun: number;
  };
}

export type AgentRunCancelLifecycleStage = "accepted" | "persisted" | "delivered" | "aborting" | "terminalized" | "fenced" | "late-write-rejected";

export interface AgentRunCancelLifecycleSpec {
  readonly deliveryMode: "manager-epoch";
  readonly gracefulAbortMs: number;
  readonly killEscalationMs: number;
  readonly staleHeartbeatFencingMs: number;
  readonly lateWriteFencing: {
    readonly enabled: boolean;
  };
  readonly eventStages: readonly AgentRunCancelLifecycleStage[];
}

export interface AgentRunLaneTarget {
  readonly configPath: string;
  readonly spec: AgentRunLaneSpec;
}

interface AgentRunNodeSpec {
  readonly id: string;
  readonly route: string;
  readonly kubeRoute: string;
  readonly unideskWorkspace: string;
}

interface AgentRunControlPlaneConfig {
  readonly sourcePath: string;
  readonly defaultTarget: {
    readonly node: string;
    readonly lane: string;
  };
  readonly nodes: Record<string, AgentRunNodeSpec>;
  readonly lanes: Record<string, AgentRunLaneSpec>;
}

export function resolveAgentRunLaneTarget(options: { node?: string | null; lane?: string | null }, env: NodeJS.ProcessEnv = process.env): AgentRunLaneTarget {
  const config = readAgentRunControlPlaneConfig(env);
  const requestedNode = options.node ?? null;
  const requestedLane = options.lane ?? null;
  if (requestedLane !== null) {
    const spec = config.lanes[requestedLane];
    if (spec === undefined) throw new Error(`${config.sourcePath}: controlPlane.lanes.${requestedLane} is not declared`);
    if (requestedNode !== null && requestedNode !== spec.nodeId) throw new Error(`--node ${requestedNode} does not match controlPlane.lanes.${requestedLane}.node=${spec.nodeId}`);
    return { configPath: config.sourcePath, spec };
  }
  if (requestedNode !== null) {
    if (requestedNode === config.defaultTarget.node) {
      const spec = config.lanes[config.defaultTarget.lane];
      if (spec === undefined) throw new Error(`${config.sourcePath}: default controlPlane lane ${config.defaultTarget.lane} is not declared`);
      if (spec.nodeId !== requestedNode) throw new Error(`${config.sourcePath}: controlPlane.default.node does not match default lane node`);
      return { configPath: config.sourcePath, spec };
    }
    const candidates = Object.values(config.lanes).filter((lane) => lane.nodeId === requestedNode);
    if (candidates.length === 0) throw new Error(`${config.sourcePath}: no AgentRun lane is declared for node ${requestedNode}`);
    if (candidates.length > 1) throw new Error(`--lane is required because node ${requestedNode} has multiple AgentRun lanes: ${candidates.map((lane) => lane.lane).join(", ")}`);
    return { configPath: config.sourcePath, spec: candidates[0] };
  }
  const spec = config.lanes[config.defaultTarget.lane];
  if (spec === undefined) throw new Error(`${config.sourcePath}: default controlPlane lane ${config.defaultTarget.lane} is not declared`);
  if (spec.nodeId !== config.defaultTarget.node) throw new Error(`${config.sourcePath}: controlPlane.default.node does not match default lane node`);
  return { configPath: config.sourcePath, spec };
}

export function resolveAgentRunLaneTargetsForNode(nodeId: string, env: NodeJS.ProcessEnv = process.env): readonly AgentRunLaneTarget[] {
  const targets = listAgentRunLaneTargetsForNode(nodeId, env);
  if (targets.length === 0) {
    const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
    throw new Error(`${configPath}: no AgentRun lane is declared for node ${nodeId}`);
  }
  return targets;
}

export function listAgentRunLaneTargetsForNode(nodeId: string, env: NodeJS.ProcessEnv = process.env): readonly AgentRunLaneTarget[] {
  const config = readAgentRunControlPlaneConfig(env);
  return Object.values(config.lanes)
    .filter((lane) => lane.nodeId === nodeId)
    .sort((left, right) => left.lane.localeCompare(right.lane))
    .map((spec) => ({ configPath: config.sourcePath, spec }));
}

export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unknown> {
  return {
    node: {
      id: spec.nodeId,
      route: spec.nodeRoute,
      kubeRoute: spec.nodeKubeRoute,
    },
    lane: spec.lane,
    version: spec.version,
    source: {
      repository: spec.source.repository,
      branch: spec.source.branch,
      statusMode: spec.source.statusMode,
      sourceAuthority: spec.source.sourceAuthority,
      sourceSnapshot: spec.source.sourceSnapshot,
      bootstrapFromBranch: spec.source.bootstrapFromBranch,
      bootstrapTimeoutSeconds: spec.source.bootstrapTimeoutSeconds,
      bootstrapPollSeconds: spec.source.bootstrapPollSeconds,
      workspace: spec.source.workspace,
    },
    runtime: {
      namespace: spec.runtime.namespace,
      managerDeployment: spec.runtime.managerDeployment,
      managerService: spec.runtime.managerService,
      managerPort: spec.runtime.managerPort,
      internalBaseUrl: spec.runtime.internalBaseUrl,
    },
    ci: {
      namespace: spec.ci.namespace,
      pipeline: spec.ci.pipeline,
      pipelineRunPrefix: spec.ci.pipelineRunPrefix,
      serviceAccountName: spec.ci.serviceAccountName,
      registryPrefix: spec.ci.registryPrefix,
      buildkitImage: spec.ci.buildkitImage,
    },
    gitops: {
      branch: spec.gitops.branch,
      path: spec.gitops.path,
      argoNamespace: spec.gitops.argoNamespace,
      argoApplication: spec.gitops.argoApplication,
      repoURL: spec.gitops.repoURL,
    },
    deployment: {
      format: spec.deployment.format,
      gitopsRoot: spec.deployment.gitopsRoot,
      runtimeRenderDir: spec.deployment.runtimeRenderDir,
      artifactCatalogPath: spec.deployment.artifactCatalogPath,
      argocd: spec.deployment.argocd,
      manager: {
        serviceAccount: spec.deployment.manager.serviceAccount,
        apiKeySecretRef: spec.deployment.manager.apiKeySecretRef,
        envNames: Object.keys(spec.deployment.manager.env).sort(),
        unideskSshEndpointEnv: spec.deployment.manager.unideskSshEndpointEnv === null
          ? null
          : { name: spec.deployment.manager.unideskSshEndpointEnv.name, valuesPrinted: false },
        bootRepoUrl: spec.deployment.manager.bootRepoUrl,
        imageBuild: {
          context: spec.deployment.manager.imageBuild.context,
          containerfile: spec.deployment.manager.imageBuild.containerfile,
          repository: spec.deployment.manager.imageBuild.repository,
          network: spec.deployment.manager.imageBuild.network,
          buildArgNames: Object.keys(spec.deployment.manager.imageBuild.buildArgs).sort(),
          proxyConfigured: spec.deployment.manager.imageBuild.httpProxy !== null || spec.deployment.manager.imageBuild.httpsProxy !== null,
          noProxyCount: spec.deployment.manager.imageBuild.noProxy.length,
          buildContainerProxyConfigured:
            spec.deployment.manager.imageBuild.buildContainerProxy.httpProxy !== null || spec.deployment.manager.imageBuild.buildContainerProxy.httpsProxy !== null,
          buildContainerNoProxyCount: spec.deployment.manager.imageBuild.buildContainerProxy.noProxy.length,
          envIdentityFileCount: spec.deployment.manager.imageBuild.envIdentityFiles.length,
          timeoutSeconds: spec.deployment.manager.imageBuild.timeoutSeconds,
          pollSeconds: spec.deployment.manager.imageBuild.pollSeconds,
        },
        resources: spec.deployment.manager.resources,
      },
      runner: {
        serviceAccount: spec.deployment.runner.serviceAccount,
        jobNamePrefix: spec.deployment.runner.jobNamePrefix,
        idleTimeoutMs: spec.deployment.runner.idleTimeoutMs,
        apiKeySecretRef: spec.deployment.runner.apiKeySecretRef,
        egressProxyUrl: spec.deployment.runner.egressProxyUrl,
        noProxyExtra: spec.deployment.runner.noProxyExtra,
        retention: spec.deployment.runner.retention,
        cancelLifecycle: spec.deployment.runner.cancelLifecycle,
      },
      localPostgres: spec.deployment.localPostgres,
    },
    gitMirror: {
      namespace: spec.gitMirror.namespace,
      readService: spec.gitMirror.readService,
      readDeployment: spec.gitMirror.readDeployment,
      writeService: spec.gitMirror.writeService,
      writeDeployment: spec.gitMirror.writeDeployment,
      resourceBundleBaseUrl: spec.gitMirror.resourceBundleBaseUrl,
      readUrl: spec.gitMirror.readUrl,
      writeUrl: spec.gitMirror.writeUrl,
      cachePvc: spec.gitMirror.cachePvc,
      cacheHostPath: spec.gitMirror.cacheHostPath,
      sshSecretName: spec.gitMirror.sshSecretName,
      githubProxy: { host: spec.gitMirror.githubProxy.host, port: spec.gitMirror.githubProxy.port },
      repositoryReconciler: spec.gitMirror.repositoryReconciler,
      repositories: spec.gitMirror.repositories.map((repo) => ({
        key: repo.key,
        repository: repo.repository,
        remoteConfigured: repo.remote.length > 0,
        sourceBranch: repo.sourceBranch,
        gitopsBranch: repo.gitopsBranch ?? null,
      })),
    },
    database: {
      mode: spec.database.mode,
      provider: spec.database.provider,
      configRef: spec.database.configRef,
      database: spec.database.database,
      user: spec.database.user,
      sslmode: spec.database.sslmode,
      secretSourceRef: spec.database.secretSourceRef,
      secretRef: spec.database.secretRef,
      localPostgresExpectedAbsent: spec.database.localPostgresExpectedAbsent,
      valuesPrinted: false,
    },
    secrets: spec.secrets.map((secret) => ({
      id: secret.id,
      sourceRef: secret.sourceMode === "codex-config" || secret.sourceMode === "json" ? secret.sourceRef : secret.sourceRef.startsWith("/") ? secret.sourceRef : rootPath(".state", "secrets", secret.sourceRef),
      sourceMode: secret.sourceMode,
      sourceKey: secret.sourceKey,
      sourceFormat: secret.sourceFormat,
      transform: secret.transform,
      targetRef: secret.targetRef,
      providerCredential: secret.providerCredentialProfile === null ? null : { profile: secret.providerCredentialProfile },
      codexConfig: secret.codexConfig === null ? null : {
        modelProvider: secret.codexConfig.modelProvider,
        model: secret.codexConfig.model,
        reviewModel: secret.codexConfig.reviewModel,
        modelReasoningEffort: secret.codexConfig.modelReasoningEffort,
        networkAccess: secret.codexConfig.networkAccess,
        serviceTier: secret.codexConfig.serviceTier,
        modelContextWindow: secret.codexConfig.modelContextWindow,
        modelAutoCompactTokenLimit: secret.codexConfig.modelAutoCompactTokenLimit,
        modelCatalogJson: secret.codexConfig.modelCatalogJson,
        approvalsReviewer: secret.codexConfig.approvalsReviewer,
        noticeHideFullAccessWarning: secret.codexConfig.noticeHideFullAccessWarning,
        modelProviderCount: secret.codexConfig.modelProviders.length,
        projectCount: secret.codexConfig.projects.length,
      },
      valuesPrinted: false,
    })),
    toolCredentials: spec.toolCredentials.map((credential) => ({
      id: credential.id,
      tool: credential.tool,
      purpose: credential.purpose,
      secretRef: credential.secretRef,
      projection: credential.projection,
      valuesPrinted: false,
    })),
    providerCredentials: agentRunProviderCredentialRefs(spec).map((credential) => ({
      profile: credential.profile,
      secretRef: credential.secretRef,
      valuesPrinted: false,
    })),
  };
}

export function agentRunProviderCredentialRefs(spec: AgentRunLaneSpec, profile?: string | null): readonly AgentRunProviderCredentialRef[] {
  const groups = new Map<string, { profile: string; namespace: string; name: string; keys: string[] }>();
  for (const secret of spec.secrets) {
    const credentialProfile = secret.providerCredentialProfile;
    if (credentialProfile === null) continue;
    if (profile !== undefined && profile !== null && credentialProfile !== profile) continue;
    const groupKey = `${credentialProfile}\0${secret.targetRef.namespace}\0${secret.targetRef.name}`;
    const group = groups.get(groupKey) ?? {
      profile: credentialProfile,
      namespace: secret.targetRef.namespace,
      name: secret.targetRef.name,
      keys: [],
    };
    if (!group.keys.includes(secret.targetRef.key)) group.keys.push(secret.targetRef.key);
    groups.set(groupKey, group);
  }
  return Array.from(groups.values()).map((group) => ({
    profile: group.profile,
    secretRef: {
      namespace: group.namespace,
      name: group.name,
      keys: group.keys,
    },
  }));
}

export function agentRunPipelineRunName(spec: AgentRunLaneSpec, sourceCommit: string): string {
  return `${spec.ci.pipelineRunPrefix}-${sourceCommit.slice(0, 12)}`;
}

export function agentRunSourceSnapshotStageRefPrefix(spec: AgentRunLaneSpec): string {
  const snapshot = spec.source.sourceSnapshot;
  if (snapshot === null) throw new Error(`config/agentrun.yaml controlPlane.lanes.${spec.lane}.source.sourceSnapshot is required for k8s git-mirror snapshot source authority`);
  return snapshot.stageRefPrefix.replaceAll("{branch}", spec.source.branch);
}

export function agentRunSourceSnapshotRef(spec: AgentRunLaneSpec, sourceCommit: string): string {
  if (!/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error(`sourceCommit must be a 40-hex git SHA for source snapshot ref: ${sourceCommit}`);
  return `${agentRunSourceSnapshotStageRefPrefix(spec).replace(/\/+$/u, "")}/${sourceCommit.toLowerCase()}`;
}

function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig {
  const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
  const root = materializeYamlComposition(readYamlRecord<Record<string, unknown>>(configPath), { label: configPath }).value;
  validateConfigEnvelope(root, configPath);
  const controlPlane = recordField(root, "controlPlane", configPath);
  const defaultTarget = recordField(controlPlane, "default", `${configPath}.controlPlane`);
  const nodes = parseNodes(recordField(controlPlane, "nodes", `${configPath}.controlPlane`), configPath);
  const lanes = parseLanes(recordField(controlPlane, "lanes", `${configPath}.controlPlane`), nodes, configPath);
  return {
    sourcePath: configPath,
    defaultTarget: {
      node: stringField(defaultTarget, "node", `${configPath}.controlPlane.default`),
      lane: stringField(defaultTarget, "lane", `${configPath}.controlPlane.default`),
    },
    nodes,
    lanes,
  };
}

function validateConfigEnvelope(root: Record<string, unknown>, configPath: string): void {
  if (root.version !== 1) throw new Error(`${configPath}.version must be 1`);
  if (root.kind !== "AgentRunConfig") throw new Error(`${configPath}.kind must be AgentRunConfig`);
  const metadata = recordField(root, "metadata", configPath);
  if (stringField(metadata, "name", `${configPath}.metadata`) !== "agentrun") {
    throw new Error(`${configPath}.metadata.name must be agentrun`);
  }
}

function parseNodes(input: Record<string, unknown>, configPath: string): Record<string, AgentRunNodeSpec> {
  const result: Record<string, AgentRunNodeSpec> = {};
  for (const [id, raw] of Object.entries(input)) {
    validateSimpleId(id, `${configPath}.controlPlane.nodes`);
    const node = asRecord(raw, `${configPath}.controlPlane.nodes.${id}`);
    result[id] = {
      id,
      route: stringField(node, "route", `${configPath}.controlPlane.nodes.${id}`),
      kubeRoute: stringField(node, "kubeRoute", `${configPath}.controlPlane.nodes.${id}`),
      unideskWorkspace: absolutePathField(node, "unideskWorkspace", `${configPath}.controlPlane.nodes.${id}`),
    };
  }
  if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.nodes must declare at least one node`);
  return result;
}

function parseLanes(input: Record<string, unknown>, nodes: Record<string, AgentRunNodeSpec>, configPath: string): Record<string, AgentRunLaneSpec> {
  const result: Record<string, AgentRunLaneSpec> = {};
  for (const [lane, raw] of Object.entries(input)) {
    validateSimpleId(lane, `${configPath}.controlPlane.lanes`);
    const item = asRecord(raw, `${configPath}.controlPlane.lanes.${lane}`);
    const nodeId = stringField(item, "node", `${configPath}.controlPlane.lanes.${lane}`);
    const node = nodes[nodeId];
    if (node === undefined) throw new Error(`${configPath}.controlPlane.lanes.${lane}.node references undeclared node ${nodeId}`);
    result[lane] = parseLane(lane, node, item, configPath);
  }
  if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.lanes must declare at least one lane`);
  return result;
}

function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, unknown>, configPath: string): AgentRunLaneSpec {
  const path = `${configPath}.controlPlane.lanes.${lane}`;
  const status = recordField(input, "status", path);
  const readOnlyCapture = recordField(status, "readOnlyCapture", `${path}.status`);
  const source = recordField(input, "source", path);
  const runtime = recordField(input, "runtime", path);
  const ci = recordField(input, "ci", path);
  const gitops = recordField(input, "gitops", path);
  const deployment = recordField(input, "deployment", path);
  const gitMirror = recordField(input, "gitMirror", path);
  const database = recordField(input, "database", path);
  const version = stringField(input, "version", path);
  const statusMode = sourceStatusModeField(optionalStringField(source, "statusMode", `${path}.source`) ?? (version === "v0.2" ? "k3s-git-mirror" : "host-worktree"), `${path}.source.statusMode`);
  const secrets = parseLaneSecrets(input, path);
  const spec: AgentRunLaneSpec = {
    lane,
    nodeId: node.id,
    nodeRoute: node.route,
    nodeKubeRoute: node.kubeRoute,
    nodeUnideskWorkspace: node.unideskWorkspace,
    version,
    status: {
      readOnlyCapture: {
        timeoutMs: integerField(readOnlyCapture, "timeoutMs", `${path}.status.readOnlyCapture`),
        configPath: `${path}.status.readOnlyCapture.timeoutMs`,
      },
    },
    source: {
      statusMode,
      repository: stringField(source, "repository", `${path}.source`),
      worktreeRemote: stringField(source, "worktreeRemote", `${path}.source`),
      branch: stringField(source, "branch", `${path}.source`),
      sourceAuthority: sourceAuthorityConfig(source.sourceAuthority, `${path}.source.sourceAuthority`),
      sourceSnapshot: sourceSnapshotConfig(source.sourceSnapshot, `${path}.source.sourceSnapshot`),
      bootstrapFromBranch: optionalStringField(source, "bootstrapFromBranch", `${path}.source`) ?? null,
      bootstrapTimeoutSeconds: integerField(source, "bootstrapTimeoutSeconds", `${path}.source`),
      bootstrapPollSeconds: integerField(source, "bootstrapPollSeconds", `${path}.source`),
      remote: stringField(source, "remote", `${path}.source`),
      workspace: absolutePathField(source, "workspace", `${path}.source`),
    },
    runtime: {
      namespace: stringField(runtime, "namespace", `${path}.runtime`),
      managerDeployment: stringField(runtime, "managerDeployment", `${path}.runtime`),
      managerService: stringField(runtime, "managerService", `${path}.runtime`),
      managerPort: integerField(runtime, "managerPort", `${path}.runtime`),
      internalBaseUrl: urlField(runtime, "internalBaseUrl", `${path}.runtime`),
    },
    ci: {
      namespace: stringField(ci, "namespace", `${path}.ci`),
      pipeline: stringField(ci, "pipeline", `${path}.ci`),
      pipelineRunPrefix: stringField(ci, "pipelineRunPrefix", `${path}.ci`),
      serviceAccountName: stringField(ci, "serviceAccountName", `${path}.ci`),
      registryPrefix: stringField(ci, "registryPrefix", `${path}.ci`),
      toolsImage: stringField(ci, "toolsImage", `${path}.ci`),
      buildkitImage: optionalStringField(ci, "buildkitImage", `${path}.ci`) ?? null,
    },
    gitops: {
      branch: stringField(gitops, "branch", `${path}.gitops`),
      path: relativePathField(gitops, "path", `${path}.gitops`),
      argoNamespace: stringField(gitops, "argoNamespace", `${path}.gitops`),
      argoApplication: stringField(gitops, "argoApplication", `${path}.gitops`),
      repoURL: urlField(gitops, "repoURL", `${path}.gitops`),
    },
    deployment: parseDeployment(deployment, `${path}.deployment`),
    gitMirror: {
      namespace: stringField(gitMirror, "namespace", `${path}.gitMirror`),
      readService: stringField(gitMirror, "readService", `${path}.gitMirror`),
      readDeployment: stringField(gitMirror, "readDeployment", `${path}.gitMirror`),
      writeService: stringField(gitMirror, "writeService", `${path}.gitMirror`),
      writeDeployment: stringField(gitMirror, "writeDeployment", `${path}.gitMirror`),
      resourceBundleBaseUrl: urlField(gitMirror, "resourceBundleBaseUrl", `${path}.gitMirror`),
      readUrl: urlField(gitMirror, "readUrl", `${path}.gitMirror`),
      writeUrl: urlField(gitMirror, "writeUrl", `${path}.gitMirror`),
      cachePvc: stringField(gitMirror, "cachePvc", `${path}.gitMirror`),
      cacheHostPath: optionalAbsolutePathField(gitMirror, "cacheHostPath", `${path}.gitMirror`) ?? null,
      sshSecretName: stringField(gitMirror, "sshSecretName", `${path}.gitMirror`),
      githubProxy: parseGithubProxy(recordField(gitMirror, "githubProxy", `${path}.gitMirror`), `${path}.gitMirror.githubProxy`),
      toolsImage: stringField(gitMirror, "toolsImage", `${path}.gitMirror`),
      syncJobPrefix: stringField(gitMirror, "syncJobPrefix", `${path}.gitMirror`),
      flushJobPrefix: stringField(gitMirror, "flushJobPrefix", `${path}.gitMirror`),
      repositoryReconciler: parseManagedRepositoryReconciler(recordField(gitMirror, "repositoryReconciler", `${path}.gitMirror`), `${path}.gitMirror.repositoryReconciler`),
      repositories: parseGitMirrorRepositories(arrayField(gitMirror, "repositories", `${path}.gitMirror`), `${path}.gitMirror.repositories`),
    },
    database: parseDatabase(database, `${path}.database`),
    secrets,
    toolCredentials: parseToolCredentials(input, path),
  };
  validateAgentRunLaneSourceAuthority(spec, path);
  validateToolCredentialSources(spec, path);
  return spec;
}

function sourceStatusModeField(value: string, path: string): "host-worktree" | "k3s-git-mirror" {
  if (value !== "host-worktree" && value !== "k3s-git-mirror") throw new Error(`${path} must be host-worktree or k3s-git-mirror`);
  return value;
}

function sourceAuthorityConfig(value: unknown, path: string): AgentRunSourceAuthoritySpec | null {
  if (value === undefined) return null;
  const raw = asRecord(value, path);
  return {
    mode: enumField(raw, "mode", path, ["gitMirrorSnapshot"]),
    resolver: enumField(raw, "resolver", path, ["k8s-git-mirror"]),
    allowHostGit: falseBooleanField(raw, "allowHostGit", path),
    allowHostWorkspace: falseBooleanField(raw, "allowHostWorkspace", path),
    allowGithubDirectInPipeline: falseBooleanField(raw, "allowGithubDirectInPipeline", path),
  };
}

function sourceSnapshotConfig(value: unknown, path: string): AgentRunSourceSnapshotSpec | null {
  if (value === undefined) return null;
  const raw = asRecord(value, path);
  const stageRefPrefix = stringField(raw, "stageRefPrefix", path);
  if (!stageRefPrefix.startsWith("refs/")) throw new Error(`${path}.stageRefPrefix must start with refs/`);
  if (stageRefPrefix.includes("..") || /\s/u.test(stageRefPrefix)) throw new Error(`${path}.stageRefPrefix must not contain whitespace or ..`);
  if (!stageRefPrefix.includes("{branch}")) throw new Error(`${path}.stageRefPrefix must include {branch}`);
  return {
    stageRefPrefix,
    missingObjectPolicy: enumField(raw, "missingObjectPolicy", path, ["fail-fast"]),
    refreshPolicy: enumField(raw, "refreshPolicy", path, ["sync-before-snapshot"]),
  };
}

function falseBooleanField(obj: Record<string, unknown>, key: string, path: string): false {
  const value = booleanField(obj, key, path);
  if (value !== false) throw new Error(`${path}.${key} must be false`);
  return false;
}

function validateAgentRunLaneSourceAuthority(spec: AgentRunLaneSpec, path: string): void {
  if (spec.version !== "v0.2") return;
  if (spec.source.statusMode !== "k3s-git-mirror") throw new Error(`${path}.source.statusMode must be k3s-git-mirror for AgentRun v0.2`);
  if (spec.source.sourceAuthority === null) throw new Error(`${path}.source.sourceAuthority is required for AgentRun v0.2`);
  if (spec.source.sourceSnapshot === null) throw new Error(`${path}.source.sourceSnapshot is required for AgentRun v0.2`);
  const remoteAuth = spec.gitMirror.repositoryReconciler.remoteAuth;
  if (remoteAuth.secretRef.namespace !== spec.gitMirror.namespace) {
    throw new Error(`${path}.gitMirror.repositoryReconciler.remoteAuth credential namespace must match gitMirror.namespace`);
  }
  for (const [index, repository] of spec.gitMirror.repositories.entries()) {
    const expected = `https://${remoteAuth.host}/${repository.repository}.git`;
    if (repository.remote !== expected) {
      throw new Error(`${path}.gitMirror.repositories[${index}].remote must exactly match the declared remoteAuth host and repository path`);
    }
  }
}

function parseDeployment(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"] {
  const argocd = recordField(input, "argocd", path);
  const manager = recordField(input, "manager", path);
  const runner = recordField(input, "runner", path);
  const localPostgres = recordField(input, "localPostgres", path);
  return {
    format: enumField(input, "format", path, ["unidesk-yaml-only"]),
    gitopsRoot: relativePathField(input, "gitopsRoot", path),
    runtimeRenderDir: relativePathField(input, "runtimeRenderDir", path),
    artifactCatalogPath: relativePathField(input, "artifactCatalogPath", path),
    argocd: {
      project: stringField(argocd, "project", `${path}.argocd`),
      applicationFile: relativePathField(argocd, "applicationFile", `${path}.argocd`),
    },
    manager: {
      serviceAccount: stringField(manager, "serviceAccount", `${path}.manager`),
      apiKeySecretRef: parseSecretRef(recordField(manager, "apiKeySecretRef", `${path}.manager`), `${path}.manager.apiKeySecretRef`),
      env: optionalEnvRecord(manager, "env", `${path}.manager`),
      unideskSshEndpointEnv: optionalEnvPair(manager, "unideskSshEndpointEnv", `${path}.manager`),
      bootRepoUrl: urlField(manager, "bootRepoUrl", `${path}.manager`),
      imageBuild: parseImageBuild(recordField(manager, "imageBuild", `${path}.manager`), `${path}.manager.imageBuild`),
      resources: parseContainerResources(recordField(manager, "resources", `${path}.manager`), `${path}.manager.resources`),
    },
    runner: {
      serviceAccount: stringField(runner, "serviceAccount", `${path}.runner`),
      jobNamePrefix: stringField(runner, "jobNamePrefix", `${path}.runner`),
      idleTimeoutMs: positiveIntegerField(runner, "idleTimeoutMs", `${path}.runner`),
      backendRetry: parseRunnerBackendRetry(recordField(runner, "backendRetry", `${path}.runner`), `${path}.runner.backendRetry`),
      apiKeySecretRef: parseSecretRef(recordField(runner, "apiKeySecretRef", `${path}.runner`), `${path}.runner.apiKeySecretRef`),
      egressProxyUrl: optionalStringField(runner, "egressProxyUrl", `${path}.runner`) ?? null,
      noProxyExtra: optionalStringArrayField(runner, "noProxyExtra", `${path}.runner`),
      retention: parseRunnerRetention(recordField(runner, "retention", `${path}.runner`), `${path}.runner.retention`),
      cancelLifecycle: parseCancelLifecycle(recordField(runner, "cancelLifecycle", `${path}.runner`), `${path}.runner.cancelLifecycle`),
    },
    localPostgres: parseLocalPostgres(localPostgres, `${path}.localPostgres`),
  };
}

function parseCancelLifecycle(input: Record<string, unknown>, path: string): AgentRunCancelLifecycleSpec {
  const lateWriteFencing = recordField(input, "lateWriteFencing", path);
  return {
    deliveryMode: enumField(input, "deliveryMode", path, ["manager-epoch"]),
    gracefulAbortMs: positiveIntegerField(input, "gracefulAbortMs", path),
    killEscalationMs: positiveIntegerField(input, "killEscalationMs", path),
    staleHeartbeatFencingMs: positiveIntegerField(input, "staleHeartbeatFencingMs", path),
    lateWriteFencing: {
      enabled: booleanField(lateWriteFencing, "enabled", `${path}.lateWriteFencing`),
    },
    eventStages: parseCancelLifecycleStages(input.eventStages, `${path}.eventStages`),
  };
}

function parseCancelLifecycleStages(input: unknown, path: string): readonly AgentRunCancelLifecycleStage[] {
  const values: readonly AgentRunCancelLifecycleStage[] = ["accepted", "persisted", "delivered", "aborting", "terminalized", "fenced", "late-write-rejected"];
  if (!Array.isArray(input)) throw new Error(`${path} must be an array`);
  if (input.length === 0) throw new Error(`${path} must declare at least one stage`);
  const result = input.map((value, index) => {
    if (typeof value !== "string" || !values.includes(value as AgentRunCancelLifecycleStage)) throw new Error(`${path}[${index}] must be one of ${values.join(", ")}`);
    return value as AgentRunCancelLifecycleStage;
  });
  const duplicates = result.filter((value, index) => result.indexOf(value) !== index);
  if (duplicates.length > 0) throw new Error(`${path} must not contain duplicate stages: ${[...new Set(duplicates)].join(", ")}`);
  return result;
}

function parseRunnerRetention(input: Record<string, unknown>, path: string): AgentRunRunnerRetentionSpec {
  const selectors = recordField(input, "selectors", path);
  const ageBasedCleanup = recordField(input, "ageBasedCleanup", path);
  const sessionPvcRetentionRaw = input.sessionPvcRetention;
  const sessionPvcRetention = typeof sessionPvcRetentionRaw === "object" && sessionPvcRetentionRaw !== null && !Array.isArray(sessionPvcRetentionRaw)
    ? sessionPvcRetentionRaw as Record<string, unknown>
    : {};
  const sessionPvcPrefixes = sessionPvcRetention.prefixes === undefined ? [] : stringArrayField(sessionPvcRetention, "prefixes", `${path}.sessionPvcRetention`);
  for (const [index, prefix] of sessionPvcPrefixes.entries()) {
    if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9-])?$/u.test(prefix)) throw new Error(`${path}.sessionPvcRetention.prefixes[${index}] must be a lowercase Kubernetes PVC name prefix`);
  }
  return {
    maxRunners: positiveIntegerField(input, "maxRunners", path),
    cleanupOrder: enumField(input, "cleanupOrder", path, ["oldest-inactive-last-active-first"]),
    activeHeartbeatMaxAgeMs: positiveIntegerField(input, "activeHeartbeatMaxAgeMs", path),
    stalePendingMaxAgeMs: positiveIntegerField(input, "stalePendingMaxAgeMs", path),
    selectors: {
      matchLabels: kubernetesLabelRecordField(recordField(selectors, "matchLabels", `${path}.selectors`), `${path}.selectors.matchLabels`),
      jobNamePrefixes: stringArrayField(selectors, "jobNamePrefixes", `${path}.selectors`).map((prefix, index) => {
        validateKubernetesNamePrefix(prefix, `${path}.selectors.jobNamePrefixes[${index}]`);
        return prefix;
      }),
    },
    ageBasedCleanup: {
      enabled: booleanField(ageBasedCleanup, "enabled", `${path}.ageBasedCleanup`),
      maxAgeHours: optionalPositiveIntegerField(ageBasedCleanup, "maxAgeHours", `${path}.ageBasedCleanup`) ?? null,
    },
    sessionPvcRetention: {
      enabled: sessionPvcRetention.enabled === undefined ? false : booleanField(sessionPvcRetention, "enabled", `${path}.sessionPvcRetention`),
      prefixes: sessionPvcPrefixes,
      maxDeletePerRun: optionalPositiveIntegerField(sessionPvcRetention, "maxDeletePerRun", `${path}.sessionPvcRetention`) ?? 100,
    },
  };
}

function parseRunnerBackendRetry(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"]["runner"]["backendRetry"] {
  return {
    maxAttempts: positiveIntegerField(input, "maxAttempts", path),
    initialBackoffMs: positiveIntegerField(input, "initialBackoffMs", path),
    maxBackoffMs: positiveIntegerField(input, "maxBackoffMs", path),
  };
}

function positiveIntegerField(input: Record<string, unknown>, key: string, path: string): number {
  const value = integerField(input, key, path);
  if (value <= 0) throw new Error(`${path}.${key} must be a positive integer`);
  return value;
}

function optionalPositiveIntegerField(input: Record<string, unknown>, key: string, path: string): number | undefined {
  const value = optionalIntegerField(input, key, path);
  if (value === undefined) return undefined;
  if (value <= 0) throw new Error(`${path}.${key} must be a positive integer when set`);
  return value;
}

function parseLocalPostgres(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"]["localPostgres"] {
  const enabled = booleanField(input, "enabled", path);
  if (!enabled) {
    return {
      enabled: false,
      serviceName: optionalStringField(input, "serviceName", path) ?? null,
      image: optionalStringField(input, "image", path) ?? null,
      storage: optionalStringField(input, "storage", path) ?? null,
      port: optionalIntegerField(input, "port", path) ?? null,
      database: optionalStringField(input, "database", path) ?? null,
      user: optionalStringField(input, "user", path) ?? null,
      passwordSourceRef: optionalStringField(input, "passwordSourceRef", path) ?? null,
      passwordSourceKey: optionalStringField(input, "passwordSourceKey", path) ?? null,
    };
  }
  return {
    enabled: true,
    serviceName: stringField(input, "serviceName", path),
    image: stringField(input, "image", path),
    storage: stringField(input, "storage", path),
    port: integerField(input, "port", path),
    database: stringField(input, "database", path),
    user: stringField(input, "user", path),
    passwordSourceRef: secretSourceRefField(input, "passwordSourceRef", path),
    passwordSourceKey: stringField(input, "passwordSourceKey", path),
  };
}

function parseContainerResources(input: Record<string, unknown>, path: string): AgentRunContainerResources {
  const requests = recordField(input, "requests", path);
  const limits = recordField(input, "limits", path);
  return {
    requests: {
      cpu: stringField(requests, "cpu", `${path}.requests`),
      memory: stringField(requests, "memory", `${path}.requests`),
    },
    limits: {
      cpu: stringField(limits, "cpu", `${path}.limits`),
      memory: stringField(limits, "memory", `${path}.limits`),
    },
  };
}

function parseImageBuild(input: Record<string, unknown>, path: string): AgentRunImageBuildSpec {
  const buildContainerProxy = recordField(input, "buildContainerProxy", path);
  return {
    context: relativePathField(input, "context", path),
    containerfile: relativePathField(input, "containerfile", path),
    repository: stringField(input, "repository", path),
    network: stringField(input, "network", path),
    buildArgs: stringRecordField(recordField(input, "buildArgs", path), `${path}.buildArgs`),
    httpProxy: optionalStringField(input, "httpProxy", path) ?? null,
    httpsProxy: optionalStringField(input, "httpsProxy", path) ?? null,
    noProxy: stringArrayField(input, "noProxy", path),
    buildContainerProxy: {
      httpProxy: optionalStringField(buildContainerProxy, "httpProxy", `${path}.buildContainerProxy`) ?? null,
      httpsProxy: optionalStringField(buildContainerProxy, "httpsProxy", `${path}.buildContainerProxy`) ?? null,
      noProxy: stringArrayField(buildContainerProxy, "noProxy", `${path}.buildContainerProxy`),
    },
    envIdentityFiles: stringArrayField(input, "envIdentityFiles", path).map((item, index) => {
      if (item.startsWith("/") || item.includes("..")) throw new Error(`${path}.envIdentityFiles[${index}] must be a relative path without ..`);
      return item;
    }),
    timeoutSeconds: integerField(input, "timeoutSeconds", path),
    pollSeconds: integerField(input, "pollSeconds", path),
  };
}

function parseLaneSecret(input: Record<string, unknown>, path: string): AgentRunLaneSecretSpec {
  const sourceMode = optionalStringField(input, "sourceMode", path) ?? "env";
  if (sourceMode !== "env" && sourceMode !== "file" && sourceMode !== "codex-config" && sourceMode !== "json") throw new Error(`${path}.sourceMode must be env, file, codex-config, or json`);
  const sourceKey = optionalStringField(input, "sourceKey", path) ?? null;
  if (sourceMode === "env" && sourceKey === null) throw new Error(`${path}.sourceKey is required when sourceMode is env`);
  if (sourceMode !== "env" && sourceKey !== null) throw new Error(`${path}.sourceKey is only supported when sourceMode is env`);
  const sourceFormat = optionalStringField(input, "sourceFormat", path) ?? null;
  if (sourceFormat !== null && sourceFormat !== "env" && sourceFormat !== "raw-token") throw new Error(`${path}.sourceFormat must be env or raw-token`);
  if (sourceFormat === "raw-token" && sourceMode !== "env") throw new Error(`${path}.sourceFormat raw-token requires sourceMode env`);
  if (sourceMode !== "env" && sourceFormat !== null) throw new Error(`${path}.sourceFormat is only supported when sourceMode is env`);
  const transform = optionalStringField(input, "transform", path) ?? null;
  if (transform !== null && transform !== "codex-auth-json-openai-api-key") throw new Error(`${path}.transform must be codex-auth-json-openai-api-key when set`);
  if (transform !== null && sourceMode !== "env") throw new Error(`${path}.transform ${transform} requires sourceMode env`);
  const jsonValue = sourceMode === "json" ? jsonValueField(input, "jsonValue", path) : null;
  const providerCredential = parseProviderCredentialBinding(input, `${path}.providerCredential`);
  return {
    id: stringField(input, "id", path),
    sourceRef: secretSourceRefField(input, "sourceRef", path),
    sourceMode,
    sourceKey,
    sourceFormat,
    transform,
    codexConfig: sourceMode === "codex-config" ? parseCodexConfig(recordField(input, "codexConfig", path), `${path}.codexConfig`) : null,
    jsonValue,
    targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
    providerCredentialProfile: providerCredential,
  };
}

function parseLaneSecrets(input: Record<string, unknown>, path: string): AgentRunLaneSecretSpec[] {
  const secrets = arrayField(input, "secrets", path).map((secret, index) => parseLaneSecret(secret, `${path}.secrets[${index}]`));
  const idPaths = new Map<string, string>();
  const targetRefPaths = new Map<string, string>();
  for (const [index, secret] of secrets.entries()) {
    const secretPath = `${path}.secrets[${index}]`;
    const previousIdPath = idPaths.get(secret.id);
    if (previousIdPath !== undefined) {
      throw new Error(`${secretPath}.id duplicates ${previousIdPath}.id (${secret.id})`);
    }
    idPaths.set(secret.id, secretPath);
    const targetRef = `${secret.targetRef.namespace}/${secret.targetRef.name}/${secret.targetRef.key}`;
    const previousTargetRefPath = targetRefPaths.get(targetRef);
    if (previousTargetRefPath !== undefined) {
      throw new Error(`${secretPath}.targetRef duplicates ${previousTargetRefPath}.targetRef (${targetRef})`);
    }
    targetRefPaths.set(targetRef, secretPath);
  }
  return secrets;
}

function parseToolCredentials(input: Record<string, unknown>, path: string): AgentRunToolCredentialSpec[] {
  const credentials = arrayField(input, "toolCredentials", path).map((value, index) => {
    const credentialPath = `${path}.toolCredentials[${index}]`;
    const secretRef = recordField(value, "secretRef", credentialPath);
    const projection = recordField(value, "projection", credentialPath);
    const kind = stringField(projection, "kind", `${credentialPath}.projection`);
    const keys = stringArrayField(secretRef, "keys", `${credentialPath}.secretRef`);
    if (keys.length === 0) throw new Error(`${credentialPath}.secretRef.keys must not be empty`);
    if (new Set(keys).size !== keys.length) throw new Error(`${credentialPath}.secretRef.keys must not contain duplicates`);
    const normalizedProjection = kind === "env"
      ? parseToolCredentialEnvProjection(projection, keys, `${credentialPath}.projection`)
      : kind === "volume"
        ? parseToolCredentialVolumeProjection(projection, `${credentialPath}.projection`)
        : null;
    if (normalizedProjection === null) throw new Error(`${credentialPath}.projection.kind must be env or volume`);
    return {
      id: stringField(value, "id", credentialPath),
      tool: credentialSlugField(value, "tool", credentialPath),
      purpose: credentialSlugField(value, "purpose", credentialPath),
      secretRef: {
        namespace: stringField(secretRef, "namespace", `${credentialPath}.secretRef`),
        name: stringField(secretRef, "name", `${credentialPath}.secretRef`),
        keys,
      },
      projection: normalizedProjection,
    } satisfies AgentRunToolCredentialSpec;
  });
  const ids = new Map<string, string>();
  const identities = new Map<string, string>();
  for (const [index, credential] of credentials.entries()) {
    const credentialPath = `${path}.toolCredentials[${index}]`;
    const priorId = ids.get(credential.id);
    if (priorId !== undefined) throw new Error(`${credentialPath}.id duplicates ${priorId}.id (${credential.id})`);
    ids.set(credential.id, credentialPath);
    const identity = `${credential.tool}/${credential.purpose}`;
    const priorIdentity = identities.get(identity);
    if (priorIdentity !== undefined) throw new Error(`${credentialPath} duplicates ${priorIdentity} tool/purpose (${identity})`);
    identities.set(identity, credentialPath);
  }
  return credentials;
}

function parseToolCredentialEnvProjection(input: Record<string, unknown>, keys: readonly string[], path: string): AgentRunToolCredentialSpec["projection"] {
  const envName = stringField(input, "envName", path);
  if (!/^[A-Z_][A-Z0-9_]{0,63}$/u.test(envName)) throw new Error(`${path}.envName must be an uppercase environment variable name`);
  const secretKey = stringField(input, "secretKey", path);
  if (!keys.includes(secretKey)) throw new Error(`${path}.secretKey must be present in secretRef.keys`);
  return { kind: "env", envName, secretKey };
}

function parseToolCredentialVolumeProjection(input: Record<string, unknown>, path: string): AgentRunToolCredentialSpec["projection"] {
  const mountPath = absolutePathField(input, "mountPath", path);
  if (!mountPath.startsWith("/home/agentrun/") || mountPath.includes("..")) throw new Error(`${path}.mountPath must stay under /home/agentrun`);
  return { kind: "volume", mountPath };
}

function credentialSlugField(input: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(input, key, path);
  if (!/^[a-z0-9][a-z0-9._-]{0,63}$/u.test(value)) throw new Error(`${path}.${key} must be a lowercase credential slug`);
  return value;
}

function validateToolCredentialSources(spec: AgentRunLaneSpec, path: string): void {
  const declaredTargets = new Set(spec.secrets.map((secret) => `${secret.targetRef.namespace}\0${secret.targetRef.name}\0${secret.targetRef.key}`));
  for (const [index, credential] of spec.toolCredentials.entries()) {
    const credentialPath = `${path}.toolCredentials[${index}]`;
    if (credential.secretRef.namespace !== spec.runtime.namespace) {
      throw new Error(`${credentialPath}.secretRef.namespace must match ${path}.runtime.namespace (${spec.runtime.namespace})`);
    }
    const missingKeys = credential.secretRef.keys.filter((key) => !declaredTargets.has(`${credential.secretRef.namespace}\0${credential.secretRef.name}\0${key}`));
    if (missingKeys.length > 0) {
      throw new Error(`${credentialPath}.secretRef.keys have no matching YAML Secret source: ${missingKeys.join(", ")}`);
    }
  }
}

function parseCodexConfig(input: Record<string, unknown>, path: string): AgentRunCodexConfigSpec {
  const providersRaw = recordField(input, "modelProviders", path);
  const modelProviders = Object.entries(providersRaw).map(([id, raw]) => {
    validateSimpleId(id, `${path}.modelProviders`);
    const provider = asRecord(raw, `${path}.modelProviders.${id}`);
    return {
      id,
      name: stringField(provider, "name", `${path}.modelProviders.${id}`),
      baseUrl: stringField(provider, "baseUrl", `${path}.modelProviders.${id}`),
      wireApi: stringField(provider, "wireApi", `${path}.modelProviders.${id}`),
      requiresOpenaiAuth: optionalBooleanField(provider, "requiresOpenaiAuth", `${path}.modelProviders.${id}`),
    };
  });
  if (modelProviders.length === 0) throw new Error(`${path}.modelProviders must not be empty`);
  const features = booleanRecord(input.features, `${path}.features`);
  const projectsRaw = input.projects === undefined || input.projects === null ? {} : asRecord(input.projects, `${path}.projects`);
  const projects = Object.entries(projectsRaw).map(([projectPath, raw]) => {
    const project = asRecord(raw, `${path}.projects.${projectPath}`);
    return { path: projectPath, trustLevel: stringField(project, "trustLevel", `${path}.projects.${projectPath}`) };
  });
  const tuiModelAvailabilityNux = integerRecord(input.tuiModelAvailabilityNux, `${path}.tuiModelAvailabilityNux`);
  return {
    modelProvider: stringField(input, "modelProvider", path),
    model: stringField(input, "model", path),
    reviewModel: optionalStringField(input, "reviewModel", path) ?? null,
    modelReasoningEffort: optionalStringField(input, "modelReasoningEffort", path) ?? null,
    disableResponseStorage: optionalBooleanField(input, "disableResponseStorage", path),
    networkAccess: optionalStringField(input, "networkAccess", path) ?? null,
    windowsWslSetupAcknowledged: optionalBooleanField(input, "windowsWslSetupAcknowledged", path),
    serviceTier: optionalStringField(input, "serviceTier", path) ?? null,
    modelContextWindow: optionalIntegerField(input, "modelContextWindow", path) ?? null,
    modelAutoCompactTokenLimit: optionalIntegerField(input, "modelAutoCompactTokenLimit", path) ?? null,
    modelCatalogJson: optionalStringField(input, "modelCatalogJson", path) ?? null,
    approvalsReviewer: optionalStringField(input, "approvalsReviewer", path) ?? null,
    noticeHideFullAccessWarning: optionalBooleanField(input, "noticeHideFullAccessWarning", path),
    modelProviders,
    features,
    projects,
    tuiModelAvailabilityNux,
  };
}

function jsonValueField(obj: Record<string, unknown>, key: string, path: string): unknown {
  if (!(key in obj)) throw new Error(`${path}.${key} is required when sourceMode is json`);
  const value = obj[key];
  try {
    if (JSON.stringify(value) === undefined) throw new Error("not-json");
  } catch {
    throw new Error(`${path}.${key} must be JSON-serializable`);
  }
  return value;
}

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

function booleanRecord(value: unknown, path: string): Readonly<Record<string, boolean>> {
  if (value === undefined || value === null) return {};
  const raw = asRecord(value, path);
  const result: Record<string, boolean> = {};
  for (const [key, item] of Object.entries(raw)) {
    validateSimpleId(key, path);
    if (typeof item !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
    result[key] = item;
  }
  return result;
}

function integerRecord(value: unknown, path: string): Readonly<Record<string, number>> {
  if (value === undefined || value === null) return {};
  const raw = asRecord(value, path);
  const result: Record<string, number> = {};
  for (const [key, item] of Object.entries(raw)) {
    if (!Number.isInteger(item)) throw new Error(`${path}.${key} must be an integer`);
    result[key] = item;
  }
  return result;
}

function parseProviderCredentialBinding(input: Record<string, unknown>, path: string): string | null {
  const value = input.providerCredential;
  if (value === undefined || value === null) return null;
  const record = asRecord(value, path);
  const profile = stringField(record, "profile", path);
  validateSimpleId(profile, path);
  return profile;
}

function parseGitMirrorRepositories(input: readonly Record<string, unknown>[], path: string): readonly AgentRunGitMirrorRepositorySpec[] {
  if (input.length === 0) throw new Error(`${path} must declare at least one repository`);
  const repositories = input.map((item, index) => parseGitMirrorRepository(item, `${path}[${index}]`));
  const keys = new Set<string>();
  const names = new Set<string>();
  const remotes = new Set<string>();
  for (const item of repositories) {
    if (keys.has(item.key)) throw new Error(`${path} contains duplicate key ${item.key}`);
    if (names.has(item.repository)) throw new Error(`${path} contains duplicate repository ${item.repository}`);
    if (remotes.has(item.remote)) throw new Error(`${path} contains duplicate remote ownership for ${item.remote}`);
    keys.add(item.key);
    names.add(item.repository);
    remotes.add(item.remote);
  }
  return repositories;
}

function parseGitMirrorRepository(input: Record<string, unknown>, path: string): AgentRunGitMirrorRepositorySpec {
  const key = stringField(input, "key", path);
  const repository = stringField(input, "repository", path);
  const remote = stringField(input, "remote", path);
  const sourceBranch = stringField(input, "sourceBranch", path);
  const additionalSourceBranches = input.additionalSourceBranches === undefined
    ? []
    : stringArrayField(input, "additionalSourceBranches", path).map((value, index) => {
      validateGitRefName(value, `${path}.additionalSourceBranches[${index}]`);
      return value;
    });
  const gitopsBranch = optionalStringField(input, "gitopsBranch", path);
  validateSimpleId(key, `${path}.key`);
  validateRepositoryPath(repository, `${path}.repository`);
  validateRepositoryRemote(remote, repository, `${path}.remote`);
  validateGitRefName(sourceBranch, `${path}.sourceBranch`);
  if (new Set([sourceBranch, ...additionalSourceBranches]).size !== additionalSourceBranches.length + 1) {
    throw new Error(`${path} contains duplicate source branches`);
  }
  if (gitopsBranch !== undefined) validateGitRefName(gitopsBranch, `${path}.gitopsBranch`);
  return {
    key,
    repository,
    remote,
    sourceBranch,
    additionalSourceBranches,
    ...(gitopsBranch === undefined ? {} : { gitopsBranch }),
  };
}

function parseManagedRepositoryReconciler(input: Record<string, unknown>, path: string): AgentRunManagedRepositoryReconcilerSpec {
  const retry = recordField(input, "retry", path);
  const freshness = recordField(input, "freshness", path);
  const readiness = recordField(input, "readiness", path);
  const status = recordField(input, "status", path);
  const lifecycle = recordField(input, "lifecycle", path);
  const stateDirectory = relativePathField(input, "stateDirectory", path);
  const hostNetwork = booleanField(input, "hostNetwork", path);
  const dnsPolicy = enumField(input, "dnsPolicy", path, ["ClusterFirst", "ClusterFirstWithHostNet", "Default"]);
  if (stateDirectory === "." || stateDirectory.length === 0) throw new Error(`${path}.stateDirectory must name a dedicated relative directory`);
  if (hostNetwork && dnsPolicy !== "ClusterFirstWithHostNet") {
    throw new Error(`${path}.dnsPolicy must be ClusterFirstWithHostNet when ${path}.hostNetwork is true`);
  }
  if (!hostNetwork && dnsPolicy === "ClusterFirstWithHostNet") {
    throw new Error(`${path}.dnsPolicy must not be ClusterFirstWithHostNet when ${path}.hostNetwork is false`);
  }
  return {
    enabled: booleanField(input, "enabled", path),
    deploymentName: kubernetesNameField(input, "deploymentName", path),
    configMapName: kubernetesNameField(input, "configMapName", path),
    serviceAccountName: kubernetesNameField(input, "serviceAccountName", path),
    remoteAuth: parseManagedRepositoryRemoteAuth(recordField(input, "remoteAuth", path), `${path}.remoteAuth`),
    hostNetwork,
    dnsPolicy,
    imagePullPolicy: enumField(input, "imagePullPolicy", path, ["Always", "IfNotPresent", "Never"]),
    cacheMountPath: absolutePathField(input, "cacheMountPath", path),
    stateDirectory,
    reconcileIntervalMs: positiveIntegerField(input, "reconcileIntervalMs", path),
    fetchTimeoutMs: positiveIntegerField(input, "fetchTimeoutMs", path),
    shutdownGraceMs: positiveIntegerField(input, "shutdownGraceMs", path),
    maxConcurrentRepositories: positiveIntegerField(input, "maxConcurrentRepositories", path),
    retry: {
      maxAttempts: positiveIntegerField(retry, "maxAttempts", `${path}.retry`),
      initialDelayMs: positiveIntegerField(retry, "initialDelayMs", `${path}.retry`),
      maxDelayMs: positiveIntegerField(retry, "maxDelayMs", `${path}.retry`),
    },
    freshness: {
      maxAgeMs: positiveIntegerField(freshness, "maxAgeMs", `${path}.freshness`),
    },
    readiness: {
      initialDelaySeconds: positiveIntegerField(readiness, "initialDelaySeconds", `${path}.readiness`),
      periodSeconds: positiveIntegerField(readiness, "periodSeconds", `${path}.readiness`),
      timeoutSeconds: positiveIntegerField(readiness, "timeoutSeconds", `${path}.readiness`),
      failureThreshold: positiveIntegerField(readiness, "failureThreshold", `${path}.readiness`),
    },
    status: {
      defaultRepositoryLimit: positiveIntegerField(status, "defaultRepositoryLimit", `${path}.status`),
    },
    lifecycle: {
      undeclaredRepositoryPolicy: enumField(lifecycle, "undeclaredRepositoryPolicy", `${path}.lifecycle`, ["retain"]),
      managedRefs: enumField(lifecycle, "managedRefs", `${path}.lifecycle`, ["source-branch-only"]),
    },
    resources: parseContainerResources(recordField(input, "resources", path), `${path}.resources`),
  };
}

function parseManagedRepositoryRemoteAuth(input: Record<string, unknown>, path: string): AgentRunManagedRepositoryReconcilerSpec["remoteAuth"] {
  const configRef = stringField(input, "configRef", path);
  const resolved = resolveConfigRef(configRef, `${path}.configRef`);
  const capability = parseAgentRunGitFetchCredentialCapability(resolved.value, configRef);
  return {
    configRef,
    capabilitySha256: resolved.sha256,
    ...capability,
  };
}

export function parseAgentRunGitFetchCredentialCapability(value: unknown, path = "gitFetchCredential"): Omit<AgentRunManagedRepositoryReconcilerSpec["remoteAuth"], "configRef" | "capabilitySha256"> {
  const capability = asRecord(value, path);
  const secretRef = asRecord(capability.secretRef, `${path}.secretRef`);
  const apiVersion = enumField(capability, "apiVersion", path, ["unidesk.ai/v1"] as const);
  const kind = enumField(capability, "kind", path, ["GitFetchCredential"] as const);
  const authMode = enumField(capability, "authMode", path, ["github-https-token"] as const);
  const host = stringField(capability, "host", path);
  if (host !== "github.com") throw new Error(`${path}.host must be github.com for github-https-token`);
  const key = stringField(secretRef, "key", `${path}.secretRef`);
  if (!/^[A-Za-z0-9._-]+$/u.test(key)) throw new Error(`${path}.secretRef.key must be a Kubernetes Secret data key`);
  return {
    apiVersion,
    kind,
    authMode,
    host,
    secretRef: {
      namespace: kubernetesNameField(secretRef, "namespace", `${path}.secretRef`),
      name: kubernetesNameField(secretRef, "name", `${path}.secretRef`),
      key,
    },
  };
}

function validateRepositoryPath(value: string, path: string): void {
  if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u.test(value) || value.includes("..")) {
    throw new Error(`${path} must be an owner/name repository path without ..`);
  }
}

function validateRepositoryRemote(value: string, repository: string, path: string): void {
  try {
    const remote = new URL(value);
    const expectedPath = `/${repository}.git`;
    if (
      remote.protocol !== "https:"
      || remote.port.length > 0
      || remote.username.length > 0
      || remote.password.length > 0
      || remote.search.length > 0
      || remote.hash.length > 0
      || remote.pathname !== expectedPath
      || value !== `https://${remote.hostname}${expectedPath}`
    ) {
      throw new Error("unsupported remote");
    }
  } catch {
    throw new Error(`${path} must be a canonical HTTPS URL owned by the same repository entry (${repository}) without userinfo, port, query, or fragment`);
  }
}

function validateGitRefName(value: string, path: string): void {
  if (
    value.length === 0
    || value.startsWith("-")
    || value.startsWith("/")
    || value.endsWith("/")
    || value.endsWith(".")
    || value.includes("..")
    || value.includes("@{")
    || /[\u0000-\u0020~^:?*[\\]/u.test(value)
  ) {
    throw new Error(`${path} must be a safe Git branch name`);
  }
}

function parseDatabase(input: Record<string, unknown>, path: string): AgentRunLaneSpec["database"] {
  const mode = enumField(input, "mode", path, ["local-postgres", "external-postgres"]);
  const sslmode = optionalStringField(input, "sslmode", path);
  if (sslmode !== undefined && sslmode !== "require") throw new Error(`${path}.sslmode must be require when set`);
  return {
    mode,
    provider: optionalStringField(input, "provider", path) ?? null,
    configRef: optionalStringField(input, "configRef", path) ?? null,
    database: optionalStringField(input, "database", path) ?? null,
    user: optionalStringField(input, "user", path) ?? null,
    sslmode: sslmode === undefined ? null : "require",
    secretSourceRef: optionalStringField(input, "secretSourceRef", path) ?? null,
    secretRef: parseSecretRef(recordField(input, "secretRef", path), `${path}.secretRef`),
    localPostgresExpectedAbsent: booleanField(input, "localPostgresExpectedAbsent", path),
  };
}

function parseSecretRef(input: Record<string, unknown>, path: string): { name: string; key: string } {
  return {
    name: stringField(input, "name", path),
    key: stringField(input, "key", path),
  };
}

function parseNamespacedSecretRef(input: Record<string, unknown>, path: string): AgentRunSecretRef {
  return {
    namespace: stringField(input, "namespace", path),
    name: stringField(input, "name", path),
    key: stringField(input, "key", path),
  };
}

function parseGithubProxy(input: Record<string, unknown>, path: string): { host: string; port: number } {
  return {
    host: stringField(input, "host", path),
    port: integerField(input, "port", path),
  };
}

function optionalEnvPair(obj: Record<string, unknown>, key: string, path: string): { name: string; value: string } | null {
  const value = obj[key];
  if (value === undefined || value === null) return null;
  const record = asRecord(value, `${path}.${key}`);
  return {
    name: stringField(record, "name", `${path}.${key}`),
    value: stringField(record, "value", `${path}.${key}`),
  };
}

function optionalEnvRecord(obj: Record<string, unknown>, key: string, path: string): Readonly<Record<string, string>> {
  const value = obj[key];
  if (value === undefined || value === null) return {};
  return stringRecordField(asRecord(value, `${path}.${key}`), `${path}.${key}`);
}

function optionalStringArrayField(obj: Record<string, unknown>, key: string, path: string): readonly string[] {
  const value = obj[key];
  if (value === undefined || value === null) return [];
  return stringArrayField(obj, key, path);
}

function stringRecordField(obj: Record<string, unknown>, path: string): Readonly<Record<string, string>> {
  const result: Record<string, string> = {};
  for (const [key, value] of Object.entries(obj)) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`${path}.${key} must be a valid build arg name`);
    if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
    result[key] = value.trim();
  }
  return result;
}

function kubernetesLabelRecordField(obj: Record<string, unknown>, path: string): Readonly<Record<string, string>> {
  const result: Record<string, string> = {};
  for (const [key, value] of Object.entries(obj)) {
    validateKubernetesLabelKey(key, `${path}.${key}`);
    if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
    const trimmed = value.trim();
    validateKubernetesLabelValue(trimmed, `${path}.${key}`);
    result[key] = trimmed;
  }
  if (Object.keys(result).length === 0) throw new Error(`${path} must declare at least one label`);
  return result;
}

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

function absolutePathField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (!value.startsWith("/") || value.includes("..")) throw new Error(`${path}.${key} must be an absolute path without ..`);
  return value;
}

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

function relativePathField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (value.startsWith("/") || value.includes("..")) throw new Error(`${path}.${key} must be a relative path without ..`);
  return value;
}

function secretSourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (value.includes("..")) throw new Error(`${path}.${key} must not contain ..`);
  if (!value.startsWith("/") && value.startsWith(".")) throw new Error(`${path}.${key} must be absolute or relative without a leading dot`);
  return value;
}

function urlField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  try {
    const parsed = new URL(value);
    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
  } catch {
    throw new Error(`${path}.${key} must be an http(s) URL`);
  }
  return value;
}

function validateSimpleId(value: string, path: string): void {
  if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path}.${value} must use a simple id`);
}

function validateKubernetesNamePrefix(value: string, path: string): void {
  if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${path} must be a Kubernetes name prefix`);
}

function kubernetesNameField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  validateKubernetesNamePrefix(value, `${path}.${key}`);
  if (value.length > 63) throw new Error(`${path}.${key} must be at most 63 characters`);
  return value;
}

function validateKubernetesLabelKey(value: string, path: string): void {
  const parts = value.split("/");
  const name = parts.length === 2 ? parts[1] : parts[0];
  if (parts.length > 2 || !name || !/^[A-Za-z0-9]([A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u.test(name)) throw new Error(`${path} must be a Kubernetes label key`);
}

function validateKubernetesLabelValue(value: string, path: string): void {
  if (!/^[A-Za-z0-9]([A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u.test(value)) throw new Error(`${path} must be a Kubernetes label value`);
}
