import { createHash, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, isAbsolute, join } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import { startJob } from "./jobs";
import type { SshCaptureResult } from "./ssh";
import { capture, fingerprintValues, parseJsonOutput } from "./platform-infra-public-service";
import {
  yamlBooleanField,
  yamlIntegerArrayField,
  yamlIntegerField,
  yamlKubernetesNameField,
  yamlObjectField,
  parseEnvFile,
  yamlRecord,
  yamlStringArrayField,
  yamlStringField,
} from "./platform-infra-ops-library";

export { parseEnvFile } from "./platform-infra-ops-library";

const defaultConfigPath = "config/secrets-distribution.yaml";
const fieldManager = "unidesk-secret-distribution";

interface SecretsOptions {
  configPath: string;
  scope: string | null;
  targetId: string | null;
  confirm: boolean;
  dryRun: boolean;
  wait: boolean;
  full: boolean;
  raw: boolean;
}

interface SecretDistributionConfig {
  configPath: string;
  version: number;
  kind: "unidesk-secret-distribution";
  metadata: { id: string; owner: string; relatedIssues: number[] };
  sources: { root: string; files: EnvSourceFileConfig[]; externalFiles: SourceFileConfig[] };
  targets: DistributionTarget[];
  kubernetesSecrets: KubernetesSecretConfig[];
}

interface SourceCreateIfMissingConfig {
  enabled: boolean;
  values: Record<string, string>;
  randomHex: Record<string, number>;
  randomBase64Url: Record<string, { bytes: number; prefix: string }>;
}

interface EnvSourceFileConfig {
  sourceRef: string;
  type: "env";
  requiredKeys: string[];
  required: true;
  createIfMissing: SourceCreateIfMissingConfig;
}

interface RawSourceFileConfig {
  sourceRef: string;
  type: "raw-file";
  requiredKeys: ["contents"];
  required: boolean;
  createIfMissing: SourceCreateIfMissingConfig;
}

type SourceFileConfig = EnvSourceFileConfig | RawSourceFileConfig;

interface DistributionTarget {
  id: string;
  route: string;
  namespace: string;
  scope: string;
  enabled: boolean;
  consumerRollout: {
    deployments: string[];
    timeoutSeconds: number;
    pollIntervalSeconds: number;
  } | null;
}

interface KubernetesSecretConfig {
  name: string;
  targetId: string;
  secretName: string;
  type: "Opaque";
  data: SecretDataMapping[];
}

interface SecretDataMapping {
  sourceRef: string;
  sourceKey: string;
  targetKey: string;
}

interface SourceMaterial {
  sourceRef: string;
  type: SourceFileConfig["type"];
  sourcePath: string;
  exists: boolean;
  required: boolean;
  requiredKeys: string[];
  presentKeys: string[];
  missingKeys: string[];
  action: "none" | "create" | "update" | "blocked";
  generatedKeys: string[];
  unmaterializedGeneratedKeys: string[];
  values: Record<string, string>;
  fingerprint: string | null;
}

interface SourceInspection {
  ok: boolean;
  root: string;
  entries: Array<Record<string, unknown>>;
  materials: Map<string, SourceMaterial>;
}

interface DesiredSecret {
  name: string;
  target: DistributionTarget;
  secretName: string;
  type: "Opaque";
  data: Record<string, string>;
  keySources: Array<{ sourceRef: string; sourceKey: string; targetKey: string }>;
  missingKeys: Array<{ sourceRef: string; sourceKey: string; targetKey: string }>;
  pendingGeneratedKeys: string[];
  fingerprint: string | null;
}

export interface EnvSourceFileMaterial {
  sourceRef: string;
  sourcePath: string;
  sourcePathRedacted: string;
  values: Record<string, string>;
  valuesPrinted: false;
}

export interface DeclaredSecretValuesMaterial {
  readonly values: Readonly<Record<string, string>>;
  readonly summary: {
    readonly configRef: string;
    readonly targetId: string;
    readonly secretName: string;
    readonly keys: readonly {
      readonly sourceRef: string;
      readonly sourceKey: string;
      readonly targetKey: string;
      readonly present: boolean;
    }[];
    readonly fingerprint: string;
    readonly valuesPrinted: false;
  };
}

export function secretsHelp(): Record<string, unknown> {
  return {
    command: "secrets plan|sync|status",
    output: "json",
    usage: [
      "bun scripts/cli.ts secrets plan --config config/secrets-distribution.yaml --scope platform-infra",
      "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm",
      "bun scripts/cli.ts secrets status --config config/secrets-distribution.yaml --scope platform-infra",
    ],
    configTruth: defaultConfigPath,
    secretPolicy: "Secret values are never printed or reverse-engineered from runtime. YAML sourceRef files are the authority; sync only pushes declared keys to declared Kubernetes Secret keys.",
  };
}

export async function runSecretsCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
  const [action = "plan"] = args;
  if (action === "help" || action === "--help" || action === "-h") return secretsHelp();
  if (action === "plan") return plan(parseOptions(args.slice(1)));
  if (action === "sync") return await sync(config, parseOptions(args.slice(1)));
  if (action === "status") return await status(config, parseOptions(args.slice(1)));
  return { ok: false, error: "unsupported-secrets-command", args, help: secretsHelp() };
}

function parseOptions(args: string[]): SecretsOptions {
  let configPath = defaultConfigPath;
  let scope: string | null = null;
  let targetId: string | null = null;
  let confirm = false;
  let dryRun = false;
  let wait = false;
  let full = false;
  let raw = false;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--config") {
      configPath = readOptionValue(args, index, "--config");
      index += 1;
    } else if (arg.startsWith("--config=")) {
      configPath = arg.slice("--config=".length);
    } else if (arg === "--scope") {
      scope = simpleId(readOptionValue(args, index, "--scope"), "--scope");
      index += 1;
    } else if (arg.startsWith("--scope=")) {
      scope = simpleId(arg.slice("--scope=".length), "--scope");
    } else if (arg === "--target") {
      targetId = simpleId(readOptionValue(args, index, "--target"), "--target");
      index += 1;
    } else if (arg.startsWith("--target=")) {
      targetId = simpleId(arg.slice("--target=".length), "--target");
    } else if (arg === "--confirm") {
      confirm = true;
    } else if (arg === "--dry-run") {
      dryRun = true;
    } else if (arg === "--wait") {
      wait = true;
    } else if (arg === "--full") {
      full = true;
    } else if (arg === "--raw") {
      raw = true;
      full = true;
    } else {
      throw new Error(`unsupported secrets option: ${arg}`);
    }
  }
  if (confirm && dryRun) throw new Error("secrets sync accepts only one of --confirm or --dry-run");
  return { configPath, scope, targetId, confirm, dryRun, wait, full, raw };
}

function plan(options: SecretsOptions): Record<string, unknown> {
  const distribution = readSecretDistributionConfig(options.configPath);
  assertSelectedTargets(distribution, options);
  const sources = inspectSources(distribution, false, selectedSourceRefs(distribution, options));
  const desired = desiredSecrets(distribution, options, sources);
  return {
    ok: sources.ok && desired.every((secret) => secret.missingKeys.length === 0),
    action: "secrets-plan",
    mutation: false,
    config: configSummary(distribution, options),
    localSources: sourceSummary(sources),
    desiredSecrets: desired.map(desiredSecretSummary),
    policy: {
      sourceAuthority: "local YAML-declared managed and external sourceRef files",
      runtimeReverseEngineering: false,
      valuesPrinted: false,
    },
    next: {
      sync: `bun scripts/cli.ts secrets sync --config ${distribution.configPath}${options.scope === null ? "" : ` --scope ${options.scope}`}${options.targetId === null ? "" : ` --target ${options.targetId}`} --confirm`,
      status: `bun scripts/cli.ts secrets status --config ${distribution.configPath}${options.scope === null ? "" : ` --scope ${options.scope}`}${options.targetId === null ? "" : ` --target ${options.targetId}`}`,
    },
  };
}

async function sync(config: UniDeskConfig, options: SecretsOptions): Promise<Record<string, unknown>> {
  const distribution = readSecretDistributionConfig(options.configPath);
  assertSelectedTargets(distribution, options);
  if (!options.confirm || options.dryRun) {
    const planned = plan(options);
    return { ...planned, action: "secrets-sync", mode: "dry-run", mutation: false };
  }
  if (!options.wait) {
    const jobArgs = ["bun", "scripts/cli.ts", "secrets", "sync", "--config", distribution.configPath, "--confirm", "--wait"];
    if (options.scope !== null) jobArgs.push("--scope", options.scope);
    if (options.targetId !== null) jobArgs.push("--target", options.targetId);
    const job = startJob("secrets_sync", jobArgs, "Sync YAML-declared local secret source keys into declared Kubernetes Secrets without printing values");
    return {
      ok: true,
      action: "secrets-sync",
      mode: "async-job",
      mutation: true,
      config: configSummary(distribution, options),
      job,
      statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
    };
  }
  const sources = inspectSources(distribution, true, selectedSourceRefs(distribution, options));
  if (!sources.ok) return { ok: false, action: "secrets-sync", mode: "blocked-local-sources", mutation: true, config: configSummary(distribution, options), localSources: sourceSummary(sources) };
  const desired = desiredSecrets(distribution, options, sources);
  const missing = desired.flatMap((secret) => secret.missingKeys);
  if (missing.length > 0) {
    return { ok: false, action: "secrets-sync", mode: "blocked-missing-secret-data", mutation: true, config: configSummary(distribution, options), localSources: sourceSummary(sources), missing, valuesPrinted: false };
  }
  const perTarget = await Promise.all(groupDesiredSecretsByTarget(desired).map(async (group) => await applyTargetSecrets(config, group.target, group.secrets, options)));
  return {
    ok: perTarget.every((item) => item.ok === true),
    action: "secrets-sync",
    mode: "confirmed",
    mutation: true,
    config: configSummary(distribution, options),
    ...(options.full
      ? { localSources: sourceSummary(sources), desiredSecrets: desired.map(desiredSecretSummary) }
      : { localSources: compactSourceSummary(sources), desiredSecrets: desired.map(compactDesiredSecretSummary) }),
    targets: perTarget,
    valuesPrinted: false,
  };
}

async function status(config: UniDeskConfig, options: SecretsOptions): Promise<Record<string, unknown>> {
  const distribution = readSecretDistributionConfig(options.configPath);
  assertSelectedTargets(distribution, options);
  const sources = inspectSources(distribution, false, selectedSourceRefs(distribution, options));
  const desired = desiredSecrets(distribution, options, sources);
  const perTarget = await Promise.all(groupDesiredSecretsByTarget(desired).map(async (group) => await statusTargetSecrets(config, group.target, group.secrets, options)));
  return {
    ok: perTarget.every((item) => item.ok === true),
    action: "secrets-status",
    mutation: false,
    config: configSummary(distribution, options),
    localSources: sourceSummary(sources),
    desiredSecrets: desired.map(desiredSecretSummary),
    targets: perTarget,
    valuesPrinted: false,
  };
}

function readSecretDistributionConfig(pathArg: string): SecretDistributionConfig {
  const configPath = resolveConfigPath(pathArg);
  const label = displayConfigPath(pathArg);
  const root = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, label);
  const version = integerField(root, "version", label);
  const kind = stringField(root, "kind", label);
  if (kind !== "unidesk-secret-distribution") throw new Error(`${label}.kind must be unidesk-secret-distribution`);
  const metadata = objectField(root, "metadata", label);
  const sources = objectField(root, "sources", label);
  const config: SecretDistributionConfig = {
    configPath: label,
    version,
    kind,
    metadata: {
      id: stringField(metadata, "id", `${label}.metadata`),
      owner: stringField(metadata, "owner", `${label}.metadata`),
      relatedIssues: numberArrayField(metadata, "relatedIssues", `${label}.metadata`),
    },
    sources: {
      root: stringField(sources, "root", `${label}.sources`),
      files: arrayOfRecords(sources.files, `${label}.sources.files`).map((item, index) => parseSourceFile(item, `${label}.sources.files[${index}]`)),
      externalFiles: sources.externalFiles === undefined
        ? []
        : arrayOfRecords(sources.externalFiles, `${label}.sources.externalFiles`).map((item, index) => parseExternalSourceFile(item, `${label}.sources.externalFiles[${index}]`)),
    },
    targets: arrayOfRecords(root.targets, `${label}.targets`).map((item, index) => parseTarget(item, `${label}.targets[${index}]`)),
    kubernetesSecrets: arrayOfRecords(root.kubernetesSecrets, `${label}.kubernetesSecrets`).map((item, index) => parseKubernetesSecret(item, `${label}.kubernetesSecrets[${index}]`)),
  };
  validateDistributionConfig(config);
  return config;
}

function parseSourceFile(record: Record<string, unknown>, path: string, external = false): EnvSourceFileConfig {
  const type = stringField(record, "type", path);
  if (type !== "env") throw new Error(`${path}.type must be env`);
  const createRaw = record.createIfMissing === undefined ? {} : objectField(record, "createIfMissing", path);
  const randomBase64UrlRaw = createRaw.randomBase64Url === undefined ? {} : objectField(createRaw, "randomBase64Url", `${path}.createIfMissing`);
  return {
    sourceRef: external ? externalSourceRefField(record, "sourceRef", path) : sourceRefField(record, "sourceRef", path),
    type,
    requiredKeys: stringArrayField(record, "requiredKeys", path).map((key, index) => envKeyValue(key, `${path}.requiredKeys[${index}]`)),
    required: true,
    createIfMissing: {
      enabled: createRaw.enabled === undefined ? false : booleanField(createRaw, "enabled", `${path}.createIfMissing`),
      values: createRaw.values === undefined ? {} : stringMapField(createRaw, "values", `${path}.createIfMissing`),
      randomHex: createRaw.randomHex === undefined ? {} : numberMapField(createRaw, "randomHex", `${path}.createIfMissing`),
      randomBase64Url: Object.fromEntries(Object.entries(randomBase64UrlRaw).map(([key, value]) => [envKeyValue(key, `${path}.createIfMissing.randomBase64Url`), randomBase64UrlSpec(value, `${path}.createIfMissing.randomBase64Url.${key}`)])),
    },
  };
}

function parseExternalSourceFile(record: Record<string, unknown>, path: string): SourceFileConfig {
  const type = stringField(record, "type", path);
  if (type === "env") return parseSourceFile(record, path, true);
  if (type !== "raw-file") throw new Error(`${path}.type must be raw-file`);
  const createRaw = objectField(record, "createIfMissing", path);
  const enabled = booleanField(createRaw, "enabled", `${path}.createIfMissing`);
  const randomHex: Record<string, number> = createRaw.randomHex === undefined
    ? {}
    : { contents: randomByteCount(createRaw.randomHex, `${path}.createIfMissing.randomHex`) };
  const randomBase64Url: Record<string, { bytes: number; prefix: string }> = createRaw.randomBase64Url === undefined
    ? {}
    : { contents: randomBase64UrlSpec(createRaw.randomBase64Url, `${path}.createIfMissing.randomBase64Url`) };
  if (createRaw.values !== undefined) throw new Error(`${path}.createIfMissing.values is not allowed for raw-file sources`);
  const generatorCount = Object.keys(randomHex).length + Object.keys(randomBase64Url).length;
  if (enabled && generatorCount !== 1) throw new Error(`${path}.createIfMissing must declare exactly one of randomHex or randomBase64Url when enabled`);
  if (!enabled && generatorCount !== 0) throw new Error(`${path}.createIfMissing generators require enabled: true`);
  return {
    sourceRef: externalSourceRefField(record, "sourceRef", path),
    type,
    requiredKeys: ["contents"],
    required: booleanField(record, "required", path),
    createIfMissing: {
      enabled,
      values: {},
      randomHex,
      randomBase64Url,
    },
  };
}

function parseTarget(record: Record<string, unknown>, path: string): DistributionTarget {
  const rollout = record.consumerRollout === undefined ? null : objectField(record, "consumerRollout", path);
  const deployments = rollout === null
    ? []
    : stringArrayField(rollout, "deployments", `${path}.consumerRollout`).map((value, index) => kubernetesNameValue(value, `${path}.consumerRollout.deployments[${index}]`));
  const timeoutSeconds = rollout === null ? 0 : integerField(rollout, "timeoutSeconds", `${path}.consumerRollout`);
  const pollIntervalSeconds = rollout === null ? 0 : integerField(rollout, "pollIntervalSeconds", `${path}.consumerRollout`);
  if (rollout !== null && (deployments.length === 0 || timeoutSeconds <= 0 || pollIntervalSeconds <= 0)) {
    throw new Error(`${path}.consumerRollout requires deployments, a positive timeoutSeconds, and a positive pollIntervalSeconds`);
  }
  return {
    id: simpleId(stringField(record, "id", path), `${path}.id`),
    route: stringField(record, "route", path),
    namespace: kubernetesNameField(record, "namespace", path),
    scope: simpleId(stringField(record, "scope", path), `${path}.scope`),
    enabled: booleanField(record, "enabled", path),
    consumerRollout: rollout === null ? null : { deployments, timeoutSeconds, pollIntervalSeconds },
  };
}

function parseKubernetesSecret(record: Record<string, unknown>, path: string): KubernetesSecretConfig {
  const type = stringField(record, "type", path);
  if (type !== "Opaque") throw new Error(`${path}.type must be Opaque`);
  return {
    name: simpleId(stringField(record, "name", path), `${path}.name`),
    targetId: simpleId(stringField(record, "targetId", path), `${path}.targetId`),
    secretName: kubernetesNameField(record, "secretName", path),
    type,
    data: arrayOfRecords(record.data, `${path}.data`).map((item, index) => ({
      sourceRef: declaredSourceRefField(item, "sourceRef", `${path}.data[${index}]`),
      sourceKey: sourceDataKeyField(item, "sourceKey", `${path}.data[${index}]`),
      targetKey: kubernetesSecretKeyField(item, "targetKey", `${path}.data[${index}]`),
    })),
  };
}

function validateDistributionConfig(config: SecretDistributionConfig): void {
  const sourceFiles = allSourceFiles(config);
  if (sourceFiles.length === 0) throw new Error(`${config.configPath}.sources must declare files or externalFiles`);
  if (config.targets.length === 0) throw new Error(`${config.configPath}.targets must not be empty`);
  const sources = new Map<string, SourceFileConfig>();
  for (const source of sourceFiles) {
    if (sources.has(source.sourceRef)) throw new Error(`${config.configPath}.sources declares duplicate sourceRef ${source.sourceRef}`);
    sources.set(source.sourceRef, source);
  }
  const targets = new Set(config.targets.map((item) => item.id));
  const targetSecrets = new Set<string>();
  for (const secret of config.kubernetesSecrets) {
    if (!targets.has(secret.targetId)) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name}.targetId is not declared in targets`);
    const secretIdentity = `${secret.targetId}/${secret.secretName}`;
    if (targetSecrets.has(secretIdentity)) throw new Error(`${config.configPath} declares duplicate target Secret ${secretIdentity}`);
    targetSecrets.add(secretIdentity);
    const targetKeys = new Set<string>();
    for (const item of secret.data) {
      const source = sources.get(item.sourceRef);
      if (source === undefined) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name} references undeclared sourceRef ${item.sourceRef}`);
      const requiredKeys: readonly string[] = source.requiredKeys;
      if (!requiredKeys.includes(item.sourceKey)) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name} maps undeclared source key ${item.sourceRef}.${item.sourceKey}`);
      if (targetKeys.has(item.targetKey)) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name} maps duplicate target key ${item.targetKey}`);
      targetKeys.add(item.targetKey);
    }
  }
}

function allSourceFiles(config: SecretDistributionConfig): SourceFileConfig[] {
  return [...config.sources.files, ...config.sources.externalFiles];
}

function inspectSources(config: SecretDistributionConfig, materialize: boolean, selectedRefs: Set<string>): SourceInspection {
  const root = secretRoot(config);
  const materials = new Map<string, SourceMaterial>();
  const entries = allSourceFiles(config).filter((source) => selectedRefs.has(source.sourceRef)).map((source) => {
    const sourcePath = sourcePathFor(root, source);
    const exists = existsSync(sourcePath);
    const existingText = exists ? readFileSync(sourcePath, "utf8") : "";
    const existing = exists ? source.type === "env" ? parseEnvFile(existingText) : { contents: existingText } : {};
    const next = { ...existing };
    const generatedKeys: string[] = [];
    const unmaterializedGeneratedKeys: string[] = [];
    if (source.createIfMissing.enabled) {
      for (const [key, value] of Object.entries(source.createIfMissing.values)) {
        if (next[key] === undefined || next[key].length === 0) {
          next[key] = value;
          generatedKeys.push(key);
        }
      }
      for (const [key, bytes] of Object.entries(source.createIfMissing.randomHex)) {
        if (next[key] === undefined || next[key].length === 0) {
          next[key] = materialize ? randomBytes(bytes).toString("hex") : `<generated:${key}>`;
          generatedKeys.push(key);
          if (!materialize) unmaterializedGeneratedKeys.push(key);
        }
      }
      for (const [key, spec] of Object.entries(source.createIfMissing.randomBase64Url)) {
        if (next[key] === undefined || next[key].length === 0) {
          next[key] = materialize ? `${spec.prefix}${randomBytes(spec.bytes).toString("base64url")}` : `<generated:${key}>`;
          generatedKeys.push(key);
          if (!materialize) unmaterializedGeneratedKeys.push(key);
        }
      }
    }
    const missingBefore = source.requiredKeys.filter((key) => existing[key] === undefined || existing[key].length === 0);
    const missingKeys = source.requiredKeys.filter((key) => next[key] === undefined || next[key].length === 0);
    const action = missingKeys.length > 0 ? "blocked" : !exists ? "create" : missingBefore.length > 0 ? "update" : "none";
    if (materialize && action !== "blocked" && (action === "create" || action === "update")) {
      if (source.type === "env") writeEnvFile(sourcePath, next);
      else writeRawFile(sourcePath, next.contents);
    }
    const materialized = materialize && action !== "blocked" && (action === "create" || action === "update");
    const presence = materialized || (exists && existingText.length > 0);
    const bytes = materialized
      ? source.type === "env"
        ? Buffer.byteLength(readFileSync(sourcePath, "utf8"), "utf8")
        : Buffer.byteLength(next.contents, "utf8")
      : exists
        ? Buffer.byteLength(existingText, "utf8")
        : null;
    const fingerprint = missingKeys.length === 0 && unmaterializedGeneratedKeys.length === 0
      ? source.type === "raw-file"
        ? fingerprintRawFileContent(next.contents)
        : fingerprintValues(next, source.requiredKeys)
      : null;
    const material: SourceMaterial = {
      sourceRef: source.sourceRef,
      type: source.type,
      sourcePath,
      exists,
      required: source.required,
      requiredKeys: source.requiredKeys,
      presentKeys: source.requiredKeys.filter((key) => next[key] !== undefined && next[key].length > 0),
      missingKeys,
      action,
      generatedKeys,
      unmaterializedGeneratedKeys,
      values: next,
      fingerprint,
    };
    materials.set(source.sourceRef, material);
    if (source.type === "raw-file") {
      return {
        sourceRef: source.sourceRef,
        type: source.type,
        presence,
        bytes,
        fingerprint,
        ownerOnly: true,
        fileMode: "0600",
        valuesPrinted: false,
      };
    }
    return {
      sourceRef: source.sourceRef,
      type: source.type,
      sourcePath: redactRepoPath(sourcePath),
      exists,
      requiredKeys: source.requiredKeys,
      presentKeys: material.presentKeys,
      missingKeys,
      action,
      generatedKeys,
      unmaterializedGeneratedKeys,
      fingerprint: material.fingerprint,
      ownerOnly: true,
      fileMode: "0600",
      valuesPrinted: false,
    };
  });
  return {
    ok: Array.from(materials.values()).every((material) => !material.required || material.missingKeys.length === 0),
    root: redactRepoPath(root),
    entries,
    materials,
  };
}

function desiredSecrets(config: SecretDistributionConfig, options: SecretsOptions, sources: SourceInspection): DesiredSecret[] {
  const selectedTargetIds = new Set(selectedTargets(config, options).map((target) => target.id));
  const targets = new Map(config.targets.map((target) => [target.id, target]));
  return config.kubernetesSecrets
    .map((secret) => ({ secret, target: targets.get(secret.targetId) }))
    .filter((item): item is { secret: KubernetesSecretConfig; target: DistributionTarget } => item.target !== undefined && item.target.enabled)
    .filter(({ target }) => selectedTargetIds.has(target.id))
    .map(({ secret, target }) => {
      const data: Record<string, string> = {};
      const missingKeys: DesiredSecret["missingKeys"] = [];
      const pendingGeneratedKeys: string[] = [];
      for (const item of secret.data) {
        const source = sources.materials.get(item.sourceRef);
        const value = source?.values[item.sourceKey];
        if (value === undefined || value.length === 0) {
          missingKeys.push({ sourceRef: item.sourceRef, sourceKey: item.sourceKey, targetKey: item.targetKey });
        } else {
          data[item.targetKey] = value;
        }
        if (source?.unmaterializedGeneratedKeys.includes(item.sourceKey) === true) pendingGeneratedKeys.push(item.targetKey);
      }
      const keys = Object.keys(data);
      return {
        name: secret.name,
        target,
        secretName: secret.secretName,
        type: secret.type,
        data,
        keySources: secret.data,
        missingKeys,
        pendingGeneratedKeys,
        fingerprint: missingKeys.length === 0 && pendingGeneratedKeys.length === 0 ? fingerprintValues(data, keys) : null,
      };
    });
}

function assertSelectedTargets(config: SecretDistributionConfig, options: SecretsOptions): void {
  const targets = selectedTargets(config, options);
  if (targets.length > 0) return;
  const available = config.targets.filter((target) => target.enabled).map((target) => target.id).sort();
  throw new Error(`no enabled secrets target matches scope=${options.scope ?? "*"} target=${options.targetId ?? "*"}; available targets: ${available.length > 0 ? available.join(", ") : "<none>"}`);
}

function selectedTargets(config: SecretDistributionConfig, options: SecretsOptions): DistributionTarget[] {
  return config.targets
    .filter((target) => target.enabled)
    .filter((target) => options.scope === null || target.scope === options.scope)
    .filter((target) => options.targetId === null || target.id === options.targetId);
}

function selectedSourceRefs(config: SecretDistributionConfig, options: SecretsOptions): Set<string> {
  const targetIds = new Set(selectedTargets(config, options).map((target) => target.id));
  return new Set(config.kubernetesSecrets
    .filter((secret) => targetIds.has(secret.targetId))
    .flatMap((secret) => secret.data.map((item) => item.sourceRef)));
}

async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
  if (secrets.length === 0) return { ok: true, target: targetSummary(target), mode: "skipped-no-secrets" };
  const yaml = renderSecretManifest(target, secrets);
  const result = await capture(config, target.route, ["sh"], applySecretScript(target, secrets, yaml));
  const parsed = parseJsonOutput(result.stdout);
  const applied = result.exitCode === 0 && boolField(parsed, "ok", false);
  const consumerRolloutStatus = applied && target.consumerRollout !== null
    ? await waitForConsumerRollout(config, target)
    : null;
  return {
    ok: applied && (consumerRolloutStatus === null || consumerRolloutStatus.ok === true),
    target: targetSummary(target),
    summary: {
      ...parsed,
      ...(consumerRolloutStatus === null ? {} : { consumerRolloutStatus }),
    },
    remote: secretCaptureSummary(result),
    ...(options.raw ? { rawCaptureOmitted: true, rawPolicy: "Secret distribution never returns raw SSH capture because remote output can contain credential-bearing diagnostics." } : {}),
  };
}

async function statusTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
  const result = await capture(config, target.route, ["sh"], statusSecretScript(target, secrets));
  const parsed = parseJsonOutput(result.stdout);
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false),
    target: targetSummary(target),
    summary: parsed,
    remote: secretCaptureSummary(result),
    ...(options.raw ? { rawCaptureOmitted: true, rawPolicy: "Secret distribution never returns raw SSH capture because remote output can contain credential-bearing diagnostics." } : {}),
  };
}

function secretCaptureSummary(result: SshCaptureResult): Record<string, unknown> {
  return {
    exitCode: result.exitCode,
    stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
    stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
    stdoutTailOmitted: true,
    stderrTailOmitted: true,
    valuesPrinted: false,
  };
}

function renderSecretManifest(target: DistributionTarget, secrets: DesiredSecret[]): string {
  return secrets.map((secret) => `apiVersion: v1
kind: Secret
metadata:
  name: ${secret.secretName}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/managed-by: unidesk
    app.kubernetes.io/part-of: ${target.scope}
type: ${secret.type}
data:
${Object.entries(secret.data).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `  ${key}: ${Buffer.from(value, "utf8").toString("base64")}`).join("\n")}
`).join("---\n");
}

function applySecretScript(target: DistributionTarget, secrets: DesiredSecret[], yaml: string): string {
  const manifestB64 = Buffer.from(yaml, "utf8").toString("base64");
  const summaryB64 = Buffer.from(JSON.stringify(secrets.map(remoteSecretSummary)), "utf8").toString("base64");
  const rollout = target.consumerRollout;
  const rolloutDeployments = rollout?.deployments.join(" ") ?? "";
  const rolloutSummaryB64 = Buffer.from(JSON.stringify(rollout), "utf8").toString("base64");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/secrets.yaml"
printf '%s' '${manifestB64}' | base64 -d >"$manifest"
kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$tmp/ns.out" 2>"$tmp/ns.err"
ns_rc=$?
if [ "$ns_rc" -eq 0 ]; then
  kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
  apply_rc=$?
else
  : >"$tmp/apply.out"
  printf '%s\\n' 'skipped because namespace sync failed' >"$tmp/apply.err"
  apply_rc=1
fi
rollout_restart_rc=0
: >"$tmp/rollout-restart.out"
: >"$tmp/rollout-restart.err"
if [ "$apply_rc" -eq 0 ] && [ -n '${rolloutDeployments}' ]; then
  for deployment in ${rolloutDeployments}; do
    kubectl -n ${target.namespace} rollout restart "deployment/$deployment" >>"$tmp/rollout-restart.out" 2>>"$tmp/rollout-restart.err" || {
      rollout_restart_rc=$?
      break
    }
  done
elif [ "$apply_rc" -ne 0 ] && [ -n '${rolloutDeployments}' ]; then
  rollout_restart_rc=1
  printf '%s\\n' 'skipped because Secret apply failed' >"$tmp/rollout-restart.err"
fi
python3 - "$ns_rc" "$apply_rc" "$rollout_restart_rc" "$tmp/ns.out" "$tmp/ns.err" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout-restart.out" "$tmp/rollout-restart.err" <<'PY'
import base64, json, os, sys
ns_rc, apply_rc, rollout_restart_rc = [int(value) for value in sys.argv[1:4]]
def byte_count(path):
    try:
        return os.path.getsize(path)
    except FileNotFoundError:
        return 0
payload = {
    "ok": ns_rc == 0 and apply_rc == 0 and rollout_restart_rc == 0,
    "namespace": "${target.namespace}",
    "secrets": json.loads(base64.b64decode("${summaryB64}").decode("utf-8")),
    "consumerRollout": json.loads(base64.b64decode("${rolloutSummaryB64}").decode("utf-8")),
    "valuesPrinted": False,
    "steps": {
        "namespace": {"exitCode": ns_rc, "stdoutBytes": byte_count(sys.argv[4]), "stderrBytes": byte_count(sys.argv[5]), "outputOmitted": True},
        "apply": {"exitCode": apply_rc, "stdoutBytes": byte_count(sys.argv[6]), "stderrBytes": byte_count(sys.argv[7]), "outputOmitted": True},
        "consumerRolloutRestart": {"exitCode": rollout_restart_rc, "stdoutBytes": byte_count(sys.argv[8]), "stderrBytes": byte_count(sys.argv[9]), "outputOmitted": True},
    },
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

async function waitForConsumerRollout(config: UniDeskConfig, target: DistributionTarget): Promise<Record<string, unknown>> {
  const rollout = target.consumerRollout;
  if (rollout === null) return { ok: true, mode: "not-declared", mutation: false };
  const startedAt = Date.now();
  const deadline = startedAt + rollout.timeoutSeconds * 1000;
  let attempts = 0;
  let lastSummary: Record<string, unknown> | null = null;
  let lastRemote: Record<string, unknown> | null = null;
  while (true) {
    attempts += 1;
    const result = await capture(config, target.route, ["sh"], consumerRolloutStatusScript(target));
    const parsed = parseJsonOutput(result.stdout);
    lastSummary = parsed;
    lastRemote = secretCaptureSummary(result);
    if (result.exitCode === 0 && boolField(parsed, "ok", false)) {
      return {
        ok: true,
        mutation: false,
        attempts,
        elapsedSeconds: Math.ceil((Date.now() - startedAt) / 1000),
        summary: parsed,
        remote: lastRemote,
        valuesPrinted: false,
      };
    }
    const remainingMs = deadline - Date.now();
    if (remainingMs <= 0) {
      return {
        ok: false,
        mutation: false,
        attempts,
        elapsedSeconds: Math.ceil((Date.now() - startedAt) / 1000),
        reason: "consumer-rollout-timeout",
        summary: lastSummary,
        remote: lastRemote,
        valuesPrinted: false,
      };
    }
    await Bun.sleep(Math.min(rollout.pollIntervalSeconds * 1000, remainingMs));
  }
}

function consumerRolloutStatusScript(target: DistributionTarget): string {
  const rollout = target.consumerRollout;
  if (rollout === null) throw new Error(`target ${target.id} does not declare consumerRollout`);
  const commands = rollout.deployments.map((deployment, index) => [
    `kubectl -n ${target.namespace} get deployment ${deployment} -o json >"$tmp/deployment.${index}.json" 2>"$tmp/deployment.${index}.err"`,
    `printf '%s' "$?" >"$tmp/deployment.${index}.rc"`,
  ].join("\n")).join("\n");
  const expectedB64 = Buffer.from(JSON.stringify(rollout.deployments), "utf8").toString("base64");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
${commands}
python3 - "$tmp" <<'PY'
import base64, json, os, sys
tmp = sys.argv[1]
expected = json.loads(base64.b64decode("${expectedB64}").decode("utf-8"))
items = []
ok = True
for index, name in enumerate(expected):
    try:
        rc = int(open(os.path.join(tmp, f"deployment.{index}.rc"), encoding="utf-8").read() or "1")
    except FileNotFoundError:
        rc = 1
    try:
        observed = json.load(open(os.path.join(tmp, f"deployment.{index}.json"), encoding="utf-8"))
    except Exception:
        observed = None
    metadata = (observed or {}).get("metadata") or {}
    spec = (observed or {}).get("spec") or {}
    status = (observed or {}).get("status") or {}
    generation = int(metadata.get("generation") or 0)
    observed_generation = int(status.get("observedGeneration") or 0)
    desired = int(spec.get("replicas") or 0)
    updated = int(status.get("updatedReplicas") or 0)
    ready = int(status.get("readyReplicas") or 0)
    available = int(status.get("availableReplicas") or 0)
    item_ok = rc == 0 and desired > 0 and observed_generation >= generation and updated >= desired and ready >= desired and available >= desired
    ok = ok and item_ok
    items.append({
        "name": name,
        "exists": rc == 0,
        "generation": generation,
        "observedGeneration": observed_generation,
        "desiredReplicas": desired,
        "updatedReplicas": updated,
        "readyReplicas": ready,
        "availableReplicas": available,
        "ready": item_ok,
    })
payload = {"ok": ok, "namespace": "${target.namespace}", "deployments": items, "valuesPrinted": False}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if ok else 1)
PY
`;
}

function statusSecretScript(target: DistributionTarget, secrets: DesiredSecret[]): string {
  const summaryB64 = Buffer.from(JSON.stringify(secrets.map(remoteSecretSummary)), "utf8").toString("base64");
  const commands = secrets.map((secret, index) => [
    `kubectl -n ${target.namespace} get secret ${secret.secretName} -o json >"$tmp/secret.${index}.json" 2>"$tmp/secret.${index}.err"`,
    `printf '%s' "$?" >"$tmp/secret.${index}.rc"`,
  ].join("\n")).join("\n");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
${commands}
python3 - "$tmp" <<'PY'
import base64, json, os, sys
tmp = sys.argv[1]
expected = json.loads(base64.b64decode("${summaryB64}").decode("utf-8"))
items = []
ok = True
for index, item in enumerate(expected):
    try:
        rc = int(open(os.path.join(tmp, f"secret.{index}.rc"), encoding="utf-8").read() or "1")
    except FileNotFoundError:
        rc = 1
    try:
        observed = json.load(open(os.path.join(tmp, f"secret.{index}.json"), encoding="utf-8"))
    except Exception:
        observed = None
    data = (observed or {}).get("data") or {}
    observed_keys = sorted(data.keys())
    expected_keys = item.get("keys") or []
    missing = [key for key in expected_keys if key not in observed_keys]
    exists = rc == 0
    item_ok = exists and len(missing) == 0
    ok = ok and item_ok
    items.append({
        "name": item.get("name"),
        "secretName": item.get("secretName"),
        "exists": exists,
        "keys": observed_keys,
        "expectedKeys": expected_keys,
        "missingKeys": missing,
        "ok": item_ok,
        "valuesPrinted": False,
    })
payload = {"ok": ok, "namespace": "${target.namespace}", "secrets": items, "valuesPrinted": False}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if ok else 1)
PY
`;
}

function groupDesiredSecretsByTarget(secrets: DesiredSecret[]): Array<{ target: DistributionTarget; secrets: DesiredSecret[] }> {
  const groups = new Map<string, { target: DistributionTarget; secrets: DesiredSecret[] }>();
  for (const secret of secrets) {
    const existing = groups.get(secret.target.id);
    if (existing === undefined) groups.set(secret.target.id, { target: secret.target, secrets: [secret] });
    else existing.secrets.push(secret);
  }
  return Array.from(groups.values());
}

function configSummary(config: SecretDistributionConfig, options: SecretsOptions): Record<string, unknown> {
  const sourceRefs = selectedSourceRefs(config, options);
  return {
    path: config.configPath,
    metadata: config.metadata,
    root: redactRepoPath(secretRoot(config)),
    scope: options.scope,
    targetId: options.targetId,
    sources: [
      ...config.sources.files.filter((item) => sourceRefs.has(item.sourceRef)).map((item) => ({ sourceRef: item.sourceRef, type: item.type, requiredKeys: item.requiredKeys, createIfMissing: item.createIfMissing.enabled })),
      ...config.sources.externalFiles.filter((item) => sourceRefs.has(item.sourceRef)).map((item) => ({ sourceRef: item.sourceRef, type: item.type, required: item.required, createIfMissing: item.createIfMissing.enabled })),
    ],
    targets: config.targets.filter((target) => (options.scope === null || target.scope === options.scope) && (options.targetId === null || target.id === options.targetId)).map(targetSummary),
    valuesPrinted: false,
  };
}

function sourceSummary(sources: SourceInspection): Record<string, unknown> {
  return { ok: sources.ok, root: sources.root, entries: sources.entries, valuesPrinted: false };
}

function compactSourceSummary(sources: SourceInspection): Record<string, unknown> {
  return {
    ok: sources.ok,
    entries: sources.entries.map((entry) => ({
      sourceRef: entry.sourceRef,
      type: entry.type,
      presence: entry.presence ?? entry.exists,
      missingKeys: entry.missingKeys ?? [],
      action: entry.action ?? "none",
      fingerprint: entry.fingerprint ?? null,
      valuesPrinted: false,
    })),
    valuesPrinted: false,
  };
}

function desiredSecretSummary(secret: DesiredSecret): Record<string, unknown> {
  return {
    name: secret.name,
    target: targetSummary(secret.target),
    secretName: secret.secretName,
    keys: Object.keys(secret.data).sort(),
    keySources: secret.keySources,
    missingKeys: secret.missingKeys,
    pendingGeneratedKeys: secret.pendingGeneratedKeys,
    fingerprint: secret.fingerprint,
    valuesPrinted: false,
  };
}

function compactDesiredSecretSummary(secret: DesiredSecret): Record<string, unknown> {
  return {
    name: secret.name,
    secretName: secret.secretName,
    keys: Object.keys(secret.data).sort(),
    missingKeys: secret.missingKeys,
    fingerprint: secret.fingerprint,
    valuesPrinted: false,
  };
}

function remoteSecretSummary(secret: DesiredSecret): Record<string, unknown> {
  return {
    name: secret.name,
    secretName: secret.secretName,
    keys: Object.keys(secret.data).sort(),
    fingerprint: secret.fingerprint,
    valuesPrinted: false,
  };
}

function targetSummary(target: DistributionTarget): Record<string, unknown> {
  return { id: target.id, route: target.route, namespace: target.namespace, scope: target.scope, consumerRollout: target.consumerRollout };
}

function readOptionValue(args: string[], index: number, option: string): string {
  const value = args[index + 1];
  if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${option} requires a value`);
  return value;
}

function resolveConfigPath(pathArg: string): string {
  if (pathArg.startsWith("/") || pathArg.includes("\0")) throw new Error("--config must be a repo-relative YAML path");
  if (pathArg.includes("..")) throw new Error("--config must not contain ..");
  return rootPath(pathArg);
}

function displayConfigPath(pathArg: string): string {
  if (pathArg.startsWith("/") || pathArg.includes("..")) throw new Error("--config must be a repo-relative YAML path without ..");
  return pathArg;
}

function secretRoot(config: SecretDistributionConfig): string {
  const root = config.sources.root;
  return isAbsolute(root) ? root : rootPath(root);
}

export function readTextFile(path: string): string {
  if (!existsSync(path)) throw new Error(`required secret source ${redactRepoPath(path)} is missing`);
  return readFileSync(path, "utf8");
}

export function readEnvSourceFile(params: { root: string; sourceRef: string; rawKey?: string; missingMessage?: (sourcePath: string) => string }): EnvSourceFileMaterial {
  const sourcePath = join(params.root, params.sourceRef);
  if (!existsSync(sourcePath)) {
    throw new Error(params.missingMessage?.(sourcePath) ?? `required secret source ${redactRepoPath(sourcePath)} is missing`);
  }
  const text = readFileSync(sourcePath, "utf8");
  return {
    sourceRef: params.sourceRef,
    sourcePath,
    sourcePathRedacted: redactRepoPath(sourcePath),
    values: params.rawKey === undefined ? parseEnvFile(text) : { [params.rawKey]: text.trim() },
    valuesPrinted: false,
  };
}

export function requiredEnvValue(values: Record<string, string>, key: string, sourceRef: string): string {
  const value = values[key];
  if (value === undefined || value.length === 0) throw new Error(`${sourceRef} is missing required key ${key}`);
  return value;
}

export function fingerprintSecretValues(values: Record<string, string>, keys: string[]): string {
  return fingerprintValues(values, keys);
}

export function readDeclaredSecretValues(params: {
  configPath: string;
  targetId: string;
  secretName: string;
  targetKeys: readonly string[];
}): DeclaredSecretValuesMaterial {
  const config = readSecretDistributionConfig(params.configPath);
  const secret = config.kubernetesSecrets.find((item) => item.targetId === params.targetId && item.name === params.secretName);
  if (secret === undefined) {
    throw new Error(`${config.configPath} does not declare kubernetesSecrets name=${params.secretName} targetId=${params.targetId}`);
  }
  const mappings = params.targetKeys.map((targetKey) => {
    const mapping = secret.data.find((item) => item.targetKey === targetKey);
    if (mapping === undefined) throw new Error(`${config.configPath}.kubernetesSecrets.${params.secretName} does not map targetKey ${targetKey}`);
    return mapping;
  });
  const sources = inspectSources(config, false, new Set(mappings.map((item) => item.sourceRef)));
  const values: Record<string, string> = {};
  const keys = mappings.map((mapping) => {
    const value = sources.materials.get(mapping.sourceRef)?.values[mapping.sourceKey];
    const present = typeof value === "string" && value.length > 0;
    if (!present) throw new Error(`${mapping.sourceRef} is missing required key ${mapping.sourceKey}`);
    values[mapping.targetKey] = value;
    return {
      sourceRef: mapping.sourceRef,
      sourceKey: mapping.sourceKey,
      targetKey: mapping.targetKey,
      present,
    };
  });
  return {
    values,
    summary: {
      configRef: config.configPath,
      targetId: params.targetId,
      secretName: params.secretName,
      keys,
      fingerprint: fingerprintValues(values, [...params.targetKeys]),
      valuesPrinted: false,
    },
  };
}

function writeEnvFile(path: string, values: Record<string, string>): void {
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
  const lines = Object.keys(values).sort().map((key) => `${key}=${quoteEnv(values[key])}`);
  writeFileSync(path, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 });
  chmodSync(path, 0o600);
}

function writeRawFile(path: string, value: string): void {
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
  writeFileSync(path, value, { encoding: "utf8", mode: 0o600 });
  chmodSync(path, 0o600);
}

function fingerprintRawFileContent(value: string): string {
  return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
}

function quoteEnv(value: string): string {
  if (/^[A-Za-z0-9_./:@%?&=+-]+$/u.test(value)) return value;
  return `'${value.replaceAll("'", "'\"'\"'")}'`;
}

export function redactRepoPath(path: string): string {
  const root = rootPath();
  return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
}

function boolField(value: Record<string, unknown> | null, key: string, fallback: boolean): boolean {
  return typeof value?.[key] === "boolean" ? value[key] : fallback;
}

function asRecord(value: unknown, path: string): Record<string, unknown> {
  return yamlRecord(value, path);
}

function objectField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
  return yamlObjectField(obj, key, path, "");
}

function arrayOfRecords(value: unknown, path: string): Record<string, unknown>[] {
  if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
  return value.map((item, index) => asRecord(item, `${path}[${index}]`));
}

function stringField(obj: Record<string, unknown>, key: string, path: string): string {
  return yamlStringField(obj, key, path, "");
}

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

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

function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
  return yamlStringArrayField(obj, key, path, "");
}

function numberArrayField(obj: Record<string, unknown>, key: string, path: string): number[] {
  return yamlIntegerArrayField(obj, key, path, "");
}

function stringMapField(obj: Record<string, unknown>, key: string, path: string): Record<string, string> {
  const record = objectField(obj, key, path);
  const result: Record<string, string> = {};
  for (const [itemKey, itemValue] of Object.entries(record)) {
    result[envKeyValue(itemKey, `${path}.${key}`)] = stringValue(itemValue, `${path}.${key}.${itemKey}`);
  }
  return result;
}

function numberMapField(obj: Record<string, unknown>, key: string, path: string): Record<string, number> {
  const record = objectField(obj, key, path);
  const result: Record<string, number> = {};
  for (const [itemKey, itemValue] of Object.entries(record)) {
    result[envKeyValue(itemKey, `${path}.${key}`)] = randomByteCount(itemValue, `${path}.${key}.${itemKey}`);
  }
  return result;
}

function randomBase64UrlSpec(value: unknown, path: string): { bytes: number; prefix: string } {
  const record = asRecord(value, path);
  return {
    bytes: randomByteCount(record.bytes, `${path}.bytes`),
    prefix: record.prefix === undefined ? "" : stringValue(record.prefix, `${path}.prefix`),
  };
}

function stringValue(value: unknown, path: string): string {
  if (typeof value !== "string") throw new Error(`${path} must be a string`);
  return value;
}

function randomByteCount(value: unknown, path: string): number {
  if (typeof value !== "number" || !Number.isInteger(value) || value < 16 || value > 128) throw new Error(`${path} must be an integer in 16..128`);
  return value;
}

function sourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (value.startsWith("/") || value.includes("..") || value.includes("\0") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${path}.${key} must be a relative source ref without ..`);
  return value;
}

function externalSourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (value.includes("\0") || value.split("/").includes("..")) throw new Error(`${path}.${key} must not contain NUL or parent traversal`);
  if (value.startsWith("~/") && value.length > 2) return value;
  if (isAbsolute(value)) return value;
  throw new Error(`${path}.${key} must be an absolute path or a ~/ home path`);
}

function declaredSourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (value.startsWith("~/") || isAbsolute(value)) return externalSourceRefField(obj, key, path);
  return sourceRefField(obj, key, path);
}

function externalSourcePath(sourceRef: string): string {
  if (sourceRef.startsWith("~/")) return join(homedir(), sourceRef.slice(2));
  if (isAbsolute(sourceRef)) return sourceRef;
  throw new Error(`external sourceRef ${sourceRef} must be an absolute path or a ~/ home path`);
}

function sourcePathFor(root: string, source: SourceFileConfig): string {
  return source.sourceRef.startsWith("~/") || isAbsolute(source.sourceRef)
    ? externalSourcePath(source.sourceRef)
    : join(root, source.sourceRef);
}

function sourceDataKeyField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (!/^[A-Za-z_][A-Za-z0-9._-]*$/u.test(value)) throw new Error(`${path}.${key} must be a source data key`);
  return value;
}

function envKeyValue(value: string, path: string): string {
  if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${path} must be an uppercase env key`);
  return value;
}

function kubernetesNameField(obj: Record<string, unknown>, key: string, path: string): string {
  return yamlKubernetesNameField(obj, key, path, "");
}

function kubernetesNameValue(value: string, path: string): string {
  return yamlKubernetesNameField({ value }, "value", path, "");
}

function kubernetesSecretKeyField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path}.${key} must be a Kubernetes Secret key`);
  return value;
}

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