import { createHash } from "node:crypto";
import type { UniDeskConfig } from "./config";
import {
  applyPk01CaddyBlock,
  capture,
  compactCapture,
  parseJsonOutput,
  publicDnsProbe,
  publicHttpProbe,
  publicServicePolicyChecks,
  publicServiceStatusScript,
  renderEnvSecretFrpcManifest,
  type PublicServiceRuntimeResources,
} from "./platform-infra-public-service";
import { parseOpsApplyOptions, parseOpsCommonOptions, type OpsApplyOptions, type OpsCommonOptions } from "./platform-infra-ops-library";
import { readSub2RankConfig, resolveSub2RankTarget, sub2RankConfigLabel, type Sub2RankConfig, type Sub2RankTarget } from "./platform-infra-sub2rank-config";

const serviceId = "sub2rank";

export const sub2RankImagePlaceholder = "__SUB2RANK_IMAGE_REF__";
export const sub2RankConfigBase64Placeholder = "__SUB2RANK_CONFIG_BASE64__";
export const sub2RankConfigSha256Placeholder = "__SUB2RANK_CONFIG_SHA256__";
export const sub2RankSourceCommitPlaceholder = "__SUB2RANK_SOURCE_COMMIT__";

export function sub2RankHelp(): Record<string, unknown> {
  return {
    command: "platform-infra sub2rank plan|public-exposure|status|validate",
    output: "json",
    usage: [
      "bun scripts/cli.ts platform-infra sub2rank plan [--target NC01]",
      "bun scripts/cli.ts platform-infra sub2rank public-exposure [--target NC01] --dry-run",
      "bun scripts/cli.ts platform-infra sub2rank public-exposure [--target NC01] --confirm",
      "bun scripts/cli.ts platform-infra sub2rank status [--target NC01] [--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2rank validate [--target NC01] [--full|--raw]",
    ],
    configTruth: sub2RankConfigLabel,
    applicationConfigRef: "application.configRef in config/platform-infra/sub2rank.yaml",
    artifactPolicy: "Application PR merge is the sole delivery trigger; PaC/Tekton builds the image and publishes a digest-pinned GitOps manifest.",
    secretPolicy: "Resolves the existing Secret declaration from config/secrets-distribution.yaml and never reads or prints runtime Secret values.",
  };
}

export async function runSub2RankCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
  const [action = "plan"] = args;
  if (action === "plan") return plan(parseOpsCommonOptions(args.slice(1)));
  if (action === "public-exposure") return await publicExposure(config, parseOpsApplyOptions(args.slice(1)));
  if (action === "status") return await status(config, parseOpsCommonOptions(args.slice(1)));
  if (action === "validate") return await validate(config, parseOpsCommonOptions(args.slice(1)));
  return { ok: false, error: "unsupported-platform-infra-sub2rank-command", args, help: sub2RankHelp() };
}

function plan(options: OpsCommonOptions): Record<string, unknown> {
  const sub2rank = readSub2RankConfig();
  const target = resolveSub2RankTarget(sub2rank, options.targetId);
  const delivery = deliveryForTarget(sub2rank, target);
  const yaml = renderSub2RankManifest(sub2rank, target);
  const policy = policyChecks(sub2rank, target, yaml);
  return {
    ok: policy.every((item) => item.ok),
    action: "platform-infra-sub2rank-plan",
    mutation: false,
    warnings: sub2rank.warnings,
    config: configSummary(sub2rank, target),
    policy,
    artifact: {
      imageRepository: sub2rank.image.repository,
      buildDisposition: "pac-tekton-on-application-pr-merge",
      sourceRoot: sub2rank.application.sourceRoot,
      sourceCommit: sub2rank.application.sourceCommit,
      sourceClean: sub2rank.application.sourceClean,
      sourceRemotePresent: sub2rank.application.sourceRemotePresent,
      sourceRemoteMatches: sub2rank.application.sourceRemoteMatches,
      publication: {
        supported: sub2rank.delivery.enabled,
        authority: "GitHub PR merge -> Gitea snapshot -> CLI release plan -> CLI manual PaC webhook -> Tekton -> GitOps -> Argo",
        branch: delivery.sourceBranch,
        pipeline: delivery.pipeline.name,
      },
    },
    topology: target.publicExposure.enabled
      ? `client -> PK01 Caddy -> PK01 frps:${target.publicExposure.frpc.remotePort} -> ${target.id} frpc -> ${sub2rank.runtime.service.name}.${target.namespace}.svc.cluster.local:${sub2rank.runtime.service.port}`
      : target.publicEdgeEnabled
        ? `client -> ${target.id} shared public-edge -> ${sub2rank.runtime.service.name}.${target.namespace}.svc.cluster.local:${sub2rank.runtime.service.port}`
        : `CLI in ${target.route} -> ${sub2rank.runtime.service.name}.${target.namespace}.svc.cluster.local:${sub2rank.runtime.service.port}`,
    next: {
      secrets: `bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope ${target.secret.scope} --confirm`,
      publicExposureDryRun: target.publicExposure.enabled ? `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --dry-run` : null,
      publicExposureApply: target.publicExposure.enabled ? `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --confirm` : null,
      sharedPublicEdge: target.publicExposure.enabled || !target.publicEdgeEnabled ? null : `bun scripts/cli.ts platform-infra public-edge plan --target ${target.id}`,
      status: `bun scripts/cli.ts platform-infra sub2rank status --target ${target.id}`,
      validate: `bun scripts/cli.ts platform-infra sub2rank validate --target ${target.id}`,
    },
  };
}

async function publicExposure(config: UniDeskConfig, options: OpsApplyOptions): Promise<Record<string, unknown>> {
  const sub2rank = readSub2RankConfig();
  const target = resolveSub2RankTarget(sub2rank, options.targetId);
  if (!target.publicExposure.enabled) {
    return {
      ok: true,
      action: "platform-infra-sub2rank-public-exposure",
      mode: "disabled",
      mutation: false,
      target: targetSummary(target),
      reason: "legacy-public-exposure-disabled",
      next: `bun scripts/cli.ts platform-infra public-edge plan --target ${target.id}`,
    };
  }
  if (options.dryRun) {
    return {
      ok: true,
      action: "platform-infra-sub2rank-public-exposure",
      mode: "dry-run",
      mutation: false,
      target: targetSummary(target),
      exposure: {
        publicBaseUrl: target.publicExposure.publicBaseUrl,
        upstream: `127.0.0.1:${target.publicExposure.frpc.remotePort}`,
        managedBlock: serviceId,
      },
      next: `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --confirm`,
    };
  }
  const caddy = await applyPk01CaddyBlock(config, serviceId, target.publicExposure);
  return {
    ok: caddy.ok === true,
    action: "platform-infra-sub2rank-public-exposure",
    mode: "confirmed",
    mutation: true,
    target: targetSummary(target),
    pk01Caddy: caddy,
    next: `Merge the application PR only after the PaC repository and source snapshot chain are ready.`,
  };
}

async function status(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
  const sub2rank = readSub2RankConfig();
  const target = resolveSub2RankTarget(sub2rank, options.targetId);
  const resources = runtimeResources(sub2rank, target);
  const remote = await capture(config, target.route, ["sh"], publicServiceStatusScript({
    target,
    resources,
    healthPath: sub2rank.runtime.probes.healthPath,
    servicePort: sub2rank.runtime.service.port,
    includeFrpc: target.publicExposure.enabled,
  }));
  const parsed = parseJsonOutput(remote.stdout);
  return {
    ok: remote.exitCode === 0 && parsed?.ok === true,
    action: "platform-infra-sub2rank-status",
    mutation: false,
    target: targetSummary(target),
    configRefs: configRefsSummary(sub2rank, target),
    summary: parsed,
    remote: compactCapture(remote, { full: options.full || remote.exitCode !== 0 }),
    ...(options.raw ? { raw: remote } : {}),
  };
}

async function validate(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
  const sub2rank = readSub2RankConfig();
  const target = resolveSub2RankTarget(sub2rank, options.targetId);
  const runtime = await status(config, options);
  const hasPublicEntry = target.publicExposure.enabled || target.publicEdgeEnabled;
  const dns = hasPublicEntry ? await publicDnsProbe(target.publicExposure.dns) : { ok: true, skipped: true, reason: "internal-only-target" };
  const publicHttps = hasPublicEntry ? publicHttpProbe(target.publicExposure.publicBaseUrl, sub2rank.runtime.probes.healthPath) : { ok: true, skipped: true, reason: "internal-only-target" };
  return {
    ok: runtime.ok === true && dns.ok === true && publicHttps.ok === true,
    action: "platform-infra-sub2rank-validate",
    mutation: false,
    target: targetSummary(target),
    runtime,
    dns,
    publicHttps,
    automaticCredit: { enabled: sub2rank.application.automaticCreditEnabled, source: sub2rank.application.configRef },
  };
}

export interface Sub2RankManifestRenderOptions {
  imageRef?: string;
  appConfigBase64?: string;
  appConfigSha256?: string;
  sourceCommit?: string;
}

export function renderSub2RankManifest(config: Sub2RankConfig, target: Sub2RankTarget, options: Sub2RankManifestRenderOptions = {}): string {
  const runtime = config.runtime;
  const image = options.imageRef ?? `${config.image.repository}:pac-preview`;
  const appConfigBase64 = options.appConfigBase64 ?? Buffer.from(config.application.configText, "utf8").toString("base64");
  const appConfigSha256 = options.appConfigSha256 ?? config.application.configSha256;
  const sourceCommit = options.sourceCommit ?? config.application.sourceCommit;
  const configHash = createHash("sha256").update(JSON.stringify({ deployment: deploymentHashInput(config), target: manifestTargetHashInput(config, target) })).digest("hex").slice(0, 16);
  const appEnv = runtime.secret.appEnvTargetKeys.map((key) => `            - name: ${key}\n              valueFrom:\n                secretKeyRef:\n                  name: ${target.secret.secretName}\n                  key: ${key}`).join("\n");
  const hostVolumeMounts = runtime.hostMounts.map((mount, index) => `            - name: host-source-${index}\n              mountPath: ${mount.target}\n              readOnly: true`).join("\n");
  const hostVolumes = runtime.hostMounts.map((mount, index) => `        - name: host-source-${index}\n          hostPath:\n            path: ${mount.source}\n            type: Directory`).join("\n");
  const runtimeCommand = (values: string[]): string[] => values.map((value, index) => values[index - 1] === "--runtime" ? target.applicationRuntimeTarget : value);
  const command = runtimeCommand(runtime.command).map((value) => `            - ${yamlQuoted(value)}`).join("\n");
  const workerCommand = runtimeCommand(runtime.workerCommand).map((value) => `            - ${yamlQuoted(value)}`).join("\n");
  const namespace = target.manageNamespace ? `apiVersion: v1
kind: Namespace
metadata:
  name: ${target.namespace}
  labels:
    app.kubernetes.io/managed-by: unidesk
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ${runtime.sharedNetworkPolicyRef.name}
  namespace: ${target.namespace}
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - {}
  egress:
    - {}
---
` : "";
  return `${namespace}apiVersion: v1
kind: ConfigMap
metadata:
  name: ${runtime.configMap.name}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: ${runtime.service.name}
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
binaryData:
  ${runtime.configMap.key}: ${yamlQuoted(appConfigBase64)}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ${runtime.storage.claimName}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: ${runtime.service.name}
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: ${runtime.storage.size}
---
apiVersion: v1
kind: Service
metadata:
  name: ${runtime.service.name}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: ${runtime.service.name}
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: ${runtime.service.name}
  ports:
    - name: http
      port: ${runtime.service.port}
      targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${runtime.service.name}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: ${runtime.service.name}
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  replicas: ${target.replicas}
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app.kubernetes.io/name: ${runtime.service.name}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ${runtime.service.name}
        app.kubernetes.io/part-of: platform-infra
      annotations:
        unidesk.ai/sub2rank-config-sha256: "${appConfigSha256}"
        unidesk.ai/sub2rank-deployment-hash: "${configHash}"
        unidesk.ai/source-commit: "${sourceCommit}"
        unidesk.ai/public-base-url: "${target.publicExposure.publicBaseUrl}"
    spec:
      securityContext:
        fsGroup: 1000
      containers:
        - name: ${runtime.service.name}
          image: ${yamlQuoted(image)}
          imagePullPolicy: ${config.image.pullPolicy}
          command:
${command}
          ports:
            - name: http
              containerPort: ${runtime.service.port}
          env:
${appEnv}
            - name: ${runtime.temporal.addressEnv}
              value: ${yamlQuoted(runtime.temporal.address)}
          readinessProbe:
            httpGet:
              path: ${runtime.probes.healthPath}
              port: http
            initialDelaySeconds: ${runtime.probes.initialDelaySeconds}
            periodSeconds: ${runtime.probes.periodSeconds}
            timeoutSeconds: ${runtime.probes.timeoutSeconds}
            failureThreshold: ${runtime.probes.failureThreshold}
          livenessProbe:
            httpGet:
              path: ${runtime.probes.healthPath}
              port: http
            initialDelaySeconds: ${runtime.probes.initialDelaySeconds}
            periodSeconds: ${runtime.probes.periodSeconds}
            timeoutSeconds: ${runtime.probes.timeoutSeconds}
            failureThreshold: ${runtime.probes.failureThreshold}
          volumeMounts:
            - name: app-config
              mountPath: ${runtime.configMap.mountPath}
              subPath: ${runtime.configMap.key}
              readOnly: true
            - name: data
              mountPath: ${runtime.storage.mountPath}
${hostVolumeMounts.length > 0 ? hostVolumeMounts : ""}
      volumes:
        - name: app-config
          configMap:
            name: ${runtime.configMap.name}
        - name: data
          persistentVolumeClaim:
            claimName: ${runtime.storage.claimName}
${hostVolumes.length > 0 ? hostVolumes : ""}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${runtime.worker.name}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: ${runtime.worker.name}
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  replicas: ${target.replicas}
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app.kubernetes.io/name: ${runtime.worker.name}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ${runtime.worker.name}
        app.kubernetes.io/part-of: platform-infra
      annotations:
        unidesk.ai/sub2rank-config-sha256: "${appConfigSha256}"
        unidesk.ai/sub2rank-deployment-hash: "${configHash}"
        unidesk.ai/source-commit: "${sourceCommit}"
    spec:
      securityContext:
        fsGroup: 1000
      containers:
        - name: ${runtime.worker.name}
          image: ${yamlQuoted(image)}
          imagePullPolicy: ${config.image.pullPolicy}
          command:
${workerCommand}
          ports:
            - name: health
              containerPort: ${runtime.worker.healthPort}
          env:
${appEnv}
            - name: ${runtime.temporal.addressEnv}
              value: ${yamlQuoted(runtime.temporal.address)}
          readinessProbe:
            httpGet:
              path: ${runtime.probes.healthPath}
              port: health
            initialDelaySeconds: ${runtime.probes.initialDelaySeconds}
            periodSeconds: ${runtime.probes.periodSeconds}
            timeoutSeconds: ${runtime.probes.timeoutSeconds}
            failureThreshold: ${runtime.probes.failureThreshold}
          livenessProbe:
            httpGet:
              path: ${runtime.probes.healthPath}
              port: health
            initialDelaySeconds: ${runtime.probes.initialDelaySeconds}
            periodSeconds: ${runtime.probes.periodSeconds}
            timeoutSeconds: ${runtime.probes.timeoutSeconds}
            failureThreshold: ${runtime.probes.failureThreshold}
          volumeMounts:
            - name: app-config
              mountPath: ${runtime.configMap.mountPath}
              subPath: ${runtime.configMap.key}
              readOnly: true
            - name: data
              mountPath: ${runtime.storage.mountPath}
${hostVolumeMounts.length > 0 ? hostVolumeMounts : ""}
      volumes:
        - name: app-config
          configMap:
            name: ${runtime.configMap.name}
        - name: data
          persistentVolumeClaim:
            claimName: ${runtime.storage.claimName}
${hostVolumes.length > 0 ? hostVolumes : ""}
${renderEnvSecretFrpcManifest(target, {
    configMapName: target.publicExposure.frpc.configMapName,
    configKey: target.publicExposure.frpc.configKey,
    tokenSecretName: target.secret.secretName,
    tokenSecretKey: target.publicExposure.frpc.tokenTargetKey,
    tokenEnvName: target.publicExposure.frpc.tokenEnvName,
  })}`;
}

function policyChecks(config: Sub2RankConfig, target: Sub2RankTarget, yaml: string): Array<{ name: string; ok: boolean; detail: string }> {
  return [
    ...publicServicePolicyChecks(yaml, target, serviceId, { requireAllowAllManifest: target.manageNamespace }).map((item) => ({ name: String(item.name), ok: item.ok === true, detail: String(item.detail) })),
    {
      name: "shared-runtime-resources-not-owned",
      ok: target.manageNamespace ? /^kind: Namespace$/mu.test(yaml) : !/^kind:\s*(?:Namespace|NetworkPolicy)$/mu.test(yaml),
      detail: target.manageNamespace
        ? `Sub2Rank owns the dedicated development namespace ${target.namespace} and its allow-all NetworkPolicy.`
        : `Sub2Rank references ${target.namespace}/NetworkPolicy/${config.runtime.sharedNetworkPolicyRef.name} without seizing shared runtime resources.`,
    },
    {
      name: "existing-secret-source-ref",
      ok: config.runtime.secret.requiredTargetKeys.every((key) => target.secret.mappings.some((mapping) => mapping.targetKey === key)),
      detail: `Runtime keys resolve through ${config.runtime.secret.configRef} declaration ${target.secret.declarationId}; valuesPrinted=false.`,
    },
  ];
}

function configSummary(config: Sub2RankConfig, target: Sub2RankTarget): Record<string, unknown> {
  const delivery = deliveryForTarget(config, target);
  return {
    configRefs: configRefsSummary(config, target),
    target: targetSummary(target),
    image: { repository: config.image.repository, pullPolicy: config.image.pullPolicy, authority: "PaC digest pin" },
    delivery: {
      sourceBranch: delivery.sourceBranch,
      pipeline: delivery.pipeline.name,
      gitopsBranch: delivery.gitops.branch,
      gitopsManifestPath: delivery.gitops.manifestPath,
      argoApplication: delivery.gitops.application.name,
    },
    runtime: {
      service: `${config.runtime.service.name}.${target.namespace}.svc.cluster.local:${config.runtime.service.port}`,
      worker: {
        deployment: config.runtime.worker.name,
        healthPort: config.runtime.worker.healthPort,
        publicService: false,
      },
      temporal: {
        addressEnv: config.runtime.temporal.addressEnv,
        address: config.runtime.temporal.address,
        namespace: config.application.temporalNamespace,
        taskQueue: target.temporalTaskQueue,
        scoreScheduleWorkflowId: target.scoreScheduleWorkflowId,
      },
      storage: { claimName: config.runtime.storage.claimName, size: config.runtime.storage.size, mountPath: config.runtime.storage.mountPath },
      applicationRuntimeTarget: target.applicationRuntimeTarget,
      secret: secretSummary(config, target),
    },
    publicExposure: {
      enabled: target.publicExposure.enabled,
      publicBaseUrl: target.publicExposure.publicBaseUrl,
      expectedA: target.publicExposure.dns.expectedA,
      remotePort: target.publicExposure.frpc.remotePort,
      marker: serviceId,
    },
    automaticCredit: { enabled: config.application.automaticCreditEnabled, source: config.application.configRef },
  };
}

function configRefsSummary(config: Sub2RankConfig, target?: Sub2RankTarget): Record<string, unknown> {
  return {
    warnings: config.warnings,
    deployment: { path: sub2RankConfigLabel, kind: config.kind },
    application: {
      path: config.application.configRef,
      sha256: config.application.configSha256,
      deploymentBlockPresent: config.application.deploymentBlockPresent,
      sourceCommit: config.application.sourceCommit,
      sourceClean: config.application.sourceClean,
      sourceRemotePresent: config.application.sourceRemotePresent,
      sourceRemoteMatches: config.application.sourceRemoteMatches,
      sourceConfigPathMatches: config.application.sourceConfigPathMatches,
      configMatchesSourceCommit: config.application.configMatchesSourceCommit,
    },
    secret: { path: config.runtime.secret.configRef, declarationId: target?.secret.declarationId ?? config.runtime.secret.declarationId },
  };
}

function secretSummary(config: Sub2RankConfig, target: Sub2RankTarget): Record<string, unknown> {
  return {
    name: target.secret.secretName,
    declarationId: target.secret.declarationId,
    mappings: target.secret.mappings.map((item) => ({ sourceRef: item.sourceRef, sourceKey: item.sourceKey, targetKey: item.targetKey })),
    requiredTargetKeys: config.runtime.secret.requiredTargetKeys,
    valuesPrinted: false,
  };
}

function targetSummary(target: Sub2RankTarget): Record<string, unknown> {
  return {
    id: target.id,
    route: target.route,
    namespace: target.namespace,
    replicas: target.replicas,
    manageNamespace: target.manageNamespace,
    applicationRuntimeTarget: target.applicationRuntimeTarget,
    temporalTaskQueue: target.temporalTaskQueue,
    scoreScheduleWorkflowId: target.scoreScheduleWorkflowId,
    publicEdgeEnabled: target.publicEdgeEnabled,
    publicBaseUrl: target.publicExposure.publicBaseUrl,
  };
}

function runtimeResources(config: Sub2RankConfig, target: Sub2RankTarget): PublicServiceRuntimeResources {
  return {
    appDeploymentName: config.runtime.service.name,
    additionalDeploymentNames: [config.runtime.worker.name],
    serviceName: config.runtime.service.name,
    persistentVolumeClaimName: config.runtime.storage.claimName,
    configMapName: config.runtime.configMap.name,
    secretName: target.secret.secretName,
    requiredSecretKeys: config.runtime.secret.requiredTargetKeys,
    sharedNetworkPolicyName: config.runtime.sharedNetworkPolicyRef.name,
  };
}

function deliveryForTarget(config: Sub2RankConfig, target: Sub2RankTarget): {
  sourceBranch: string;
  pipeline: Sub2RankConfig["delivery"]["pipeline"];
  gitops: Sub2RankConfig["delivery"]["gitops"];
} {
  const profile = Object.values(config.delivery.profiles).find((candidate) => candidate.targetId === target.id);
  return profile ?? { sourceBranch: config.application.branch, pipeline: config.delivery.pipeline, gitops: config.delivery.gitops };
}

function deploymentHashInput(config: Sub2RankConfig): Record<string, unknown> {
  const { scope: _scope, ...legacySecret } = config.runtime.secret;
  return {
    application: {
      repository: config.application.repository,
      branch: config.application.branch,
      sourceConfigPath: config.application.sourceConfigPath,
      dockerfile: config.application.dockerfile,
      runtimeTarget: config.application.runtimeTarget,
    },
    image: config.image,
    runtime: { ...config.runtime, secret: legacySecret },
  };
}

function manifestTargetHashInput(config: Sub2RankConfig, target: Sub2RankTarget): Record<string, unknown> {
  const legacyShape = {
    id: target.id,
    route: target.route,
    namespace: target.namespace,
    enabled: target.enabled,
    replicas: target.replicas,
    publicExposure: target.publicExposure,
  };
  if (!target.manageNamespace
    && target.applicationRuntimeTarget === config.application.runtimeTarget
    && target.secret.declarationId === config.runtime.secret.declarationId) return legacyShape;
  return {
    ...legacyShape,
    manageNamespace: target.manageNamespace,
    applicationRuntimeTarget: target.applicationRuntimeTarget,
    secretDeclarationId: target.secret.declarationId,
  };
}

function yamlQuoted(value: string): string {
  return JSON.stringify(value);
}
