// 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";

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

export interface AgentRunGitMirrorRepositorySpec {
  readonly key: string;
  readonly repository: string;
  readonly sourceBranch: string;
  readonly gitopsBranch?: string;
}

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";
  readonly sourceKey: string | null;
  readonly targetRef: AgentRunSecretRef;
  readonly providerCredentialProfile: string | null;
}

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 version: string;
  readonly source: {
    readonly repository: string;
    readonly branch: string;
    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 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 gitMirror: {
    readonly namespace: string;
    readonly readService: string;
    readonly readDeployment: string;
    readonly writeService: string;
    readonly writeDeployment: 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 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[];
}

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 selectors: {
    readonly matchLabels: Readonly<Record<string, string>>;
    readonly jobNamePrefixes: readonly string[];
  };
  readonly ageBasedCleanup: {
    readonly enabled: boolean;
    readonly maxAgeHours: number | null;
  };
}

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;
}

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) {
    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 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,
      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,
    },
    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,
      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 },
      repositories: spec.gitMirror.repositories.map((repo) => ({
        key: repo.key,
        repository: repo.repository,
        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.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`,
      sourceMode: secret.sourceMode,
      sourceKey: secret.sourceKey,
      targetRef: secret.targetRef,
      providerCredential: secret.providerCredentialProfile === null ? null : { profile: secret.providerCredentialProfile },
      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)}`;
}

function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig {
  const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
  const root = readYamlRecord<Record<string, unknown>>(configPath);
  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}`),
    };
  }
  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 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);
  return {
    lane,
    nodeId: node.id,
    nodeRoute: node.route,
    nodeKubeRoute: node.kubeRoute,
    version: stringField(input, "version", path),
    source: {
      repository: stringField(source, "repository", `${path}.source`),
      branch: stringField(source, "branch", `${path}.source`),
      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`),
    },
    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`),
      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`),
      repositories: arrayField(gitMirror, "repositories", `${path}.gitMirror`).map((repo, index) => parseGitMirrorRepository(repo, `${path}.gitMirror.repositories[${index}]`)),
    },
    database: parseDatabase(database, `${path}.database`),
    secrets: arrayField(input, "secrets", path).map((secret, index) => parseLaneSecret(secret, `${path}.secrets[${index}]`)),
  };
}

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);
  return {
    maxRunners: positiveIntegerField(input, "maxRunners", path),
    cleanupOrder: enumField(input, "cleanupOrder", path, ["oldest-inactive-last-active-first"]),
    activeHeartbeatMaxAgeMs: positiveIntegerField(input, "activeHeartbeatMaxAgeMs", 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,
    },
  };
}

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,
    };
  }
  return {
    enabled: true,
    serviceName: stringField(input, "serviceName", path),
    image: stringField(input, "image", path),
    storage: stringField(input, "storage", path),
    port: integerField(input, "port", 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") throw new Error(`${path}.sourceMode must be env or file`);
  const sourceKey = optionalStringField(input, "sourceKey", path) ?? null;
  if (sourceMode === "env" && sourceKey === null) throw new Error(`${path}.sourceKey is required when sourceMode is env`);
  const providerCredential = parseProviderCredentialBinding(input, `${path}.providerCredential`);
  return {
    id: stringField(input, "id", path),
    sourceRef: secretSourceRefField(input, "sourceRef", path),
    sourceMode,
    sourceKey,
    targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
    providerCredentialProfile: providerCredential,
  };
}

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 parseGitMirrorRepository(input: Record<string, unknown>, path: string): AgentRunGitMirrorRepositorySpec {
  const gitopsBranch = optionalStringField(input, "gitopsBranch", path);
  return {
    key: stringField(input, "key", path),
    repository: stringField(input, "repository", path),
    sourceBranch: stringField(input, "sourceBranch", path),
    ...(gitopsBranch === undefined ? {} : { gitopsBranch }),
  };
}

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 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`);
}
