#!/usr/bin/env bun
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { resolve } from "node:path";

function required(value, path) {
  if (typeof value !== "string" || value.length === 0) throw new Error(`${path} must be a non-empty string`);
  return value;
}

function record(value, path) {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
  return value;
}

function positiveInteger(value, path) {
  if (!Number.isInteger(value) || value < 1) throw new Error(`${path} must be a positive integer`);
  return value;
}

function readEnvFile(path) {
  const values = {};
  const text = readFileSync(path, "utf8");
  for (const [index, rawLine] of text.split(/\r?\n/u).entries()) {
    const line = rawLine.trim();
    if (line.length === 0 || line.startsWith("#")) continue;
    const eq = line.indexOf("=");
    if (eq <= 0) throw new Error(`${path}:${index + 1} must be KEY=value`);
    values[line.slice(0, eq)] = line.slice(eq + 1);
  }
  return values;
}

function readLinePairCredential(path, usernameLine, passwordLine) {
  const lines = readFileSync(path, "utf8").split(/\r?\n/u);
  const username = (lines[usernameLine - 1] || "").trim();
  const password = (lines[passwordLine - 1] || "").trim();
  if (!username) throw new Error(`missing username line ${usernameLine} in ${path}`);
  if (!password) throw new Error(`missing password line ${passwordLine} in ${path}`);
  return { username, password };
}

function sha(value) {
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
}

function yamlScalar(value) {
  return JSON.stringify(String(value));
}

function indent(text, spaces) {
  const pad = " ".repeat(spaces);
  return text.split("\n").map((line) => line.length > 0 ? `${pad}${line}` : line).join("\n");
}

function labels(config) {
  const name = required(record(config.metadata, "metadata").name, "metadata.name");
  return [
    "app.kubernetes.io/part-of: unidesk",
    `unidesk.ai/deployment: ${name}`,
    `unidesk.ai/target: ${required(record(config.target, "target").id, "target.id")}`,
  ].join("\n");
}

function envFromSecret(secretName, keyName, envName) {
  return [
    `- name: ${envName}`,
    "  valueFrom:",
    "    secretKeyRef:",
    `      name: ${secretName}`,
    `      key: ${keyName}`,
  ].join("\n");
}

function envFromConfig(configName, keyName, envName) {
  return [
    `- name: ${envName}`,
    "  valueFrom:",
    "    configMapKeyRef:",
    `      name: ${configName}`,
    `      key: ${keyName}`,
  ].join("\n");
}

function optionalRecord(value, path) {
  if (value === undefined || value === null) return null;
  return record(value, path);
}

function optionalString(value, fallback) {
  return typeof value === "string" && value.length > 0 ? value : fallback;
}

function removeUrlSearchParam(value, paramName) {
  if (typeof value !== "string" || value.length === 0) return value;
  try {
    const url = new URL(value);
    url.searchParams.delete(paramName);
    return url.toString();
  } catch {
    return value
      .replace(new RegExp(`([?&])${paramName}=true(&|$)`, "g"), (_match, prefix, suffix) => (prefix === "?" && suffix ? "?" : ""))
      .replace(/[?&]$/, "");
  }
}

function resourceBlock(resources) {
  const requests = record(record(resources, "resources").requests, "resources.requests");
  const limits = record(resources.limits, "resources.limits");
  return [
    "resources:",
    "  requests:",
    `    cpu: ${yamlScalar(required(requests.cpu, "resources.requests.cpu"))}`,
    `    memory: ${yamlScalar(required(requests.memory, "resources.requests.memory"))}`,
    "  limits:",
    ...(limits.cpu ? [`    cpu: ${yamlScalar(limits.cpu)}`] : []),
    `    memory: ${yamlScalar(required(limits.memory, "resources.limits.memory"))}`,
  ].join("\n");
}

const configPath = process.argv[2] ?? "config/unidesk-host-k8s.yaml";
const config = Bun.YAML.parse(readFileSync(configPath, "utf8"));
const target = record(config.target, "target");
const runtime = record(config.runtime, "runtime");
const delivery = config.delivery === undefined ? null : record(config.delivery, "delivery");
const images = record(config.images, "images");
const database = record(config.database, "database");
const services = record(config.services, "services");
const backend = record(services.backendCore, "services.backendCore");
const frontend = record(services.frontend, "services.frontend");
const decisionCenter = optionalRecord(services.decisionCenter, "services.decisionCenter");
const todoNote = optionalRecord(services.todoNote, "services.todoNote");
const runtimeConfig = record(config.runtimeConfig, "runtimeConfig");
const runtimeAuth = record(config.runtimeAuth, "runtimeAuth");
const runtimeAuthSourceRef = record(runtimeAuth.sourceRef, "runtimeAuth.sourceRef");
const runtimeSecrets = record(config.runtimeSecrets, "runtimeSecrets");
const secretTarget = record(runtimeSecrets.target, "runtimeSecrets.target");
const secretKeys = record(secretTarget.keys, "runtimeSecrets.target.keys");
const namespace = required(target.namespace, "target.namespace");
const secretName = required(secretTarget.name, "runtimeSecrets.target.name");
const authSourcePath = resolve(required(runtimeAuthSourceRef.path, "runtimeAuth.sourceRef.path"));
if (required(runtimeAuthSourceRef.format, "runtimeAuth.sourceRef.format") !== "linePair") throw new Error("runtimeAuth.sourceRef.format must be linePair");
const authCredential = readLinePairCredential(
  authSourcePath,
  positiveInteger(runtimeAuthSourceRef.usernameLine, "runtimeAuth.sourceRef.usernameLine"),
  positiveInteger(runtimeAuthSourceRef.passwordLine, "runtimeAuth.sourceRef.passwordLine"),
);
const sourcePath = resolve(required(record(runtimeSecrets.sourceRef, "runtimeSecrets.sourceRef").path, "runtimeSecrets.sourceRef.path"));
const secrets = readEnvFile(sourcePath);
secrets[secretKeys.authPassword] = authCredential.password;
const requiredSecretKeys = Object.values(secretKeys);
for (const key of requiredSecretKeys) {
  if (typeof secrets[key] !== "string" || secrets[key].length === 0) throw new Error(`missing ${key} in ${sourcePath}`);
}
const secretFingerprint = sha(requiredSecretKeys.map((key) => `${key}=${secrets[key]}`).join("\n"));
const runtimeConfigName = "unidesk-runtime-config";
const deliveryServiceRefs = new Set();
if (delivery !== null) {
  if (!Array.isArray(delivery.services) || delivery.services.length === 0) throw new Error("delivery.services must be a non-empty array");
  for (const [index, value] of delivery.services.entries()) {
    const item = record(value, `delivery.services[${index}]`);
    deliveryServiceRefs.add(required(item.serviceRef, `delivery.services[${index}].serviceRef`));
  }
}
if (delivery !== null && delivery.enabled !== true && delivery.enabled !== false) {
  throw new Error("delivery.enabled must be boolean");
}
const databaseMode = database.mode === undefined ? "k8s-postgres" : required(database.mode, "database.mode");
if (databaseMode !== "k8s-postgres" && databaseMode !== "host-native") throw new Error("database.mode must be k8s-postgres or host-native");
const dbService = required(database.serviceName, "database.serviceName");
const dbHost = databaseMode === "host-native" ? required(database.host, "database.host") : `${dbService}.${namespace}.svc.cluster.local`;
const dbPort = required(String(database.port), "database.port");
const dbName = secrets[secretKeys.postgresDb];
if (secrets[secretKeys.databaseUrl] !== `postgres://${secrets[secretKeys.postgresUser]}:${secrets[secretKeys.postgresPassword]}@${dbHost}:${dbPort}/${dbName}`) {
  throw new Error(`DATABASE_URL in ${sourcePath} must target ${dbHost}:${dbPort}/${dbName}`);
}

function readServiceDatabase(service, servicePath) {
  if (service === null) return null;
  const database = record(service.database, `${servicePath}.database`);
  const sourceRef = record(database.sourceRef, `${servicePath}.database.sourceRef`);
  const sourcePath = resolve(required(sourceRef.path, `${servicePath}.database.sourceRef.path`));
  const sourceKey = required(sourceRef.key, `${servicePath}.database.sourceRef.key`);
  const sourceValues = readEnvFile(sourcePath);
  const value = sourceValues[sourceKey];
  if (typeof value !== "string" || value.length === 0) throw new Error(`missing ${sourceKey} in ${sourcePath}`);
  return {
    sourcePath,
    sourceRefPath: required(sourceRef.path, `${servicePath}.database.sourceRef.path`),
    sourceKey,
    secretName: required(database.secretName, `${servicePath}.database.secretName`),
    secretKey: required(database.secretKey, `${servicePath}.database.secretKey`),
    value: removeUrlSearchParam(value, "uselibpqcompat"),
    fingerprint: sha(`${sourceKey}=${removeUrlSearchParam(value, "uselibpqcompat")}`),
  };
}

function readFileSource(sourceRef, path) {
  const sourcePath = resolve(required(sourceRef.path, `${path}.path`));
  const value = readFileSync(sourcePath, "utf8");
  if (value.length === 0) throw new Error(`missing file content in ${sourcePath}`);
  return {
    sourcePath,
    sourceRefPath: required(sourceRef.path, `${path}.path`),
    value,
    fingerprint: sha(value),
  };
}

function readServiceStorage(service, servicePath) {
  if (service === null) return null;
  const storage = optionalRecord(service.storage, `${servicePath}.storage`);
  if (storage === null) return null;
  const primary = required(storage.primary, `${servicePath}.storage.primary`);
  if (primary !== "postgres" && primary !== "github-repo") throw new Error(`${servicePath}.storage.primary must be postgres or github-repo`);
  const healthRefreshSeconds = storage.healthRefreshSeconds === undefined
    ? null
    : positiveInteger(storage.healthRefreshSeconds, `${servicePath}.storage.healthRefreshSeconds`);
  const resolved = {
    primary,
    ...(healthRefreshSeconds === null ? {} : { healthRefreshSeconds }),
  };
  const github = primary === "github-repo" ? record(storage.github, `${servicePath}.storage.github`) : null;
  if (github === null) return resolved;
  const ssh = record(github.ssh, `${servicePath}.storage.github.ssh`);
  const privateKey = record(ssh.privateKey, `${servicePath}.storage.github.ssh.privateKey`);
  const knownHosts = record(ssh.knownHosts, `${servicePath}.storage.github.ssh.knownHosts`);
  const privateKeySource = readFileSource(record(privateKey.sourceRef, `${servicePath}.storage.github.ssh.privateKey.sourceRef`), `${servicePath}.storage.github.ssh.privateKey.sourceRef`);
  const knownHostsSource = readFileSource(record(knownHosts.sourceRef, `${servicePath}.storage.github.ssh.knownHosts.sourceRef`), `${servicePath}.storage.github.ssh.knownHosts.sourceRef`);
  return {
    ...resolved,
    github: {
      repo: required(github.repo, `${servicePath}.storage.github.repo`),
      sshUrl: required(github.sshUrl, `${servicePath}.storage.github.sshUrl`),
      branch: required(github.branch, `${servicePath}.storage.github.branch`),
      basePath: required(github.basePath, `${servicePath}.storage.github.basePath`),
      worktreePath: required(github.worktreePath, `${servicePath}.storage.github.worktreePath`),
      commitMessagePrefix: required(github.commitMessagePrefix, `${servicePath}.storage.github.commitMessagePrefix`),
      author: {
        name: required(record(github.author, `${servicePath}.storage.github.author`).name, `${servicePath}.storage.github.author.name`),
        email: required(record(github.author, `${servicePath}.storage.github.author`).email, `${servicePath}.storage.github.author.email`),
      },
      ssh: {
        secretName: required(ssh.secretName, `${servicePath}.storage.github.ssh.secretName`),
        mountPath: required(ssh.mountPath, `${servicePath}.storage.github.ssh.mountPath`),
        privateKeyKey: required(privateKey.secretKey, `${servicePath}.storage.github.ssh.privateKey.secretKey`),
        knownHostsKey: required(knownHosts.secretKey, `${servicePath}.storage.github.ssh.knownHosts.secretKey`),
        privateKeySource,
        knownHostsSource,
        fingerprint: sha(`${privateKeySource.fingerprint}\n${knownHostsSource.fingerprint}`),
      },
    },
  };
}

function managedMicroservice(service, servicePath, id, name, description) {
  return {
    id,
    name,
    providerId: required(target.id, "target.id"),
    description,
    repository: {
      url: required(record(service.repository, `${servicePath}.repository`).url, `${servicePath}.repository.url`),
      commitId: required(runtime.commit, "runtime.commit"),
      dockerfile: required(record(service.repository, `${servicePath}.repository`).dockerfile, `${servicePath}.repository.dockerfile`),
      composeFile: required(runtime.deployRef, "runtime.deployRef"),
      composeService: required(service.deploymentName, `${servicePath}.deploymentName`),
      containerName: required(service.containerName, `${servicePath}.containerName`),
    },
    backend: {
      nodeBaseUrl: `http://${required(service.serviceName, `${servicePath}.serviceName`)}.${namespace}.svc.cluster.local:${required(String(service.containerPort), `${servicePath}.containerPort`)}`,
      nodeBindHost: `${required(service.serviceName, `${servicePath}.serviceName`)}.${namespace}.svc.cluster.local`,
      nodePort: positiveInteger(service.containerPort, `${servicePath}.containerPort`),
      proxyMode: "cluster-service-http",
      frontendOnly: true,
      public: false,
      allowedMethods: ["GET", "HEAD", "POST", "PUT", "DELETE"],
      allowedPathPrefixes: ["/health", "/live", "/logs", "/api/"],
      healthPath: required(service.healthPath, `${servicePath}.healthPath`),
      timeoutMs: 30000,
    },
    development: {
      providerId: required(target.id, "target.id"),
      sshPassthrough: true,
      worktreePath: required(record(service.repository, `${servicePath}.repository`).worktreePath, `${servicePath}.repository.worktreePath`),
    },
    frontend: {
      route: required(record(service.frontend, `${servicePath}.frontend`).route, `${servicePath}.frontend.route`),
      integrated: record(service.frontend, `${servicePath}.frontend`).integrated !== false,
    },
    deployment: {
      mode: "internal-sidecar",
      namespace,
      expectedNodeIds: [required(target.id, "target.id")],
      activeNodeId: required(target.id, "target.id"),
    },
  };
}

const decisionCenterDatabase = readServiceDatabase(decisionCenter, "services.decisionCenter");
const decisionCenterStorage = readServiceStorage(decisionCenter, "services.decisionCenter");
const todoNoteDatabase = readServiceDatabase(todoNote, "services.todoNote");
const todoNoteStorage = readServiceStorage(todoNote, "services.todoNote");
const configuredMicroservices = JSON.parse(required(runtimeConfig.microservicesJson, "runtimeConfig.microservicesJson"));
if (!Array.isArray(configuredMicroservices)) throw new Error("runtimeConfig.microservicesJson must encode an array");
const managedMicroservices = [
  ...(decisionCenter === null ? [] : [managedMicroservice(decisionCenter, "services.decisionCenter", "decision-center", "Decision Center", "NC01 YAML-first Decision Center for decisions, requirements, meetings, and work diaries.")]),
  ...(todoNote === null ? [] : [managedMicroservice(todoNote, "services.todoNote", "todo-note", "Todo Note", "NC01 YAML-first Todo Note backed by the shared decision-center-data GitHub repository.")]),
];
const managedIds = new Set(managedMicroservices.map((service) => service.id));
const effectiveMicroservicesJson = JSON.stringify([
  ...configuredMicroservices.filter((service) => typeof service !== "object" || service === null || !managedIds.has(service.id)),
  ...managedMicroservices,
]);
const runtimeConfigFingerprint = sha(JSON.stringify({ ...runtimeConfig, microservicesJson: effectiveMicroservicesJson }));

const docs = [];
docs.push(`apiVersion: v1
kind: Namespace
metadata:
  name: ${namespace}
  labels:
${indent(labels(config), 4)}`);
docs.push(`apiVersion: v1
kind: Secret
metadata:
  name: ${secretName}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
  annotations:
    unidesk.ai/source-ref: ${yamlScalar(required(record(runtimeSecrets.sourceRef, "runtimeSecrets.sourceRef").path, "runtimeSecrets.sourceRef.path"))}
    unidesk.ai/fingerprint: ${yamlScalar(secretFingerprint)}
type: Opaque
stringData:
${indent(requiredSecretKeys.map((key) => `${key}: ${yamlScalar(secrets[key])}`).join("\n"), 2)}`);
docs.push(`apiVersion: v1
kind: ConfigMap
metadata:
  name: ${runtimeConfigName}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
data:
  UNIDESK_ENV: ${yamlScalar(required(runtime.environment, "runtime.environment"))}
  UNIDESK_NAMESPACE: ${yamlScalar(namespace)}
  UNIDESK_DEPLOY_REF: ${yamlScalar(required(runtime.deployRef, "runtime.deployRef"))}
  UNIDESK_DEPLOY_REPO: ${yamlScalar(required(runtime.repository, "runtime.repository"))}
  UNIDESK_DEPLOY_COMMIT: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
  UNIDESK_DEPLOY_REQUESTED_COMMIT: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
  UNIDESK_DATABASE_NAME: ${yamlScalar(dbName)}
  UNIDESK_DATABASE_MODE: ${yamlScalar(databaseMode)}
  UNIDESK_DATABASE_HOST: ${yamlScalar(dbHost)}
  UNIDESK_DATABASE_PORT: ${yamlScalar(dbPort)}
  DATABASE_VOLUME_NAME: ${yamlScalar(required(database.pvcName, "database.pvcName"))}
  DATABASE_VOLUME_SIZE: ${yamlScalar(required(database.storageSize, "database.storageSize"))}
  HEARTBEAT_TIMEOUT_MS: ${yamlScalar(required(runtimeConfig.heartBeatTimeoutMs, "runtimeConfig.heartBeatTimeoutMs"))}
  TASK_PENDING_TIMEOUT_MS: ${yamlScalar(required(runtimeConfig.taskPendingTimeoutMs, "runtimeConfig.taskPendingTimeoutMs"))}
  DATABASE_POOL_MAX: ${yamlScalar(required(runtimeConfig.databasePoolMax, "runtimeConfig.databasePoolMax"))}
  SESSION_TTL_SECONDS: ${yamlScalar(required(runtimeConfig.sessionTtlSeconds, "runtimeConfig.sessionTtlSeconds"))}
  AUTH_USERNAME: ${yamlScalar(authCredential.username)}
  UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST: ${yamlScalar(required(runtimeConfig.sshClientRouteAllowlist, "runtimeConfig.sshClientRouteAllowlist"))}
  MICROSERVICES_JSON: ${yamlScalar(effectiveMicroservicesJson)}
  NO_PROXY: ${yamlScalar(required(runtime.noProxy, "runtime.noProxy"))}
  no_proxy: ${yamlScalar(required(runtime.noProxy, "runtime.noProxy"))}`);
const serviceDatabaseSecrets = new Map();
for (const databaseSecret of [decisionCenterDatabase, todoNoteDatabase].filter(Boolean)) {
  const existing = serviceDatabaseSecrets.get(databaseSecret.secretName);
  if (existing !== undefined && (existing.secretKey !== databaseSecret.secretKey || existing.value !== databaseSecret.value)) {
    throw new Error(`conflicting managed service database Secret: ${databaseSecret.secretName}`);
  }
  serviceDatabaseSecrets.set(databaseSecret.secretName, databaseSecret);
}
for (const databaseSecret of serviceDatabaseSecrets.values()) {
  docs.push(`apiVersion: v1
kind: Secret
metadata:
  name: ${databaseSecret.secretName}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
  annotations:
    unidesk.ai/source-ref: ${yamlScalar(databaseSecret.sourceRefPath)}
    unidesk.ai/fingerprint: ${yamlScalar(databaseSecret.fingerprint)}
type: Opaque
stringData:
  ${databaseSecret.secretKey}: ${yamlScalar(databaseSecret.value)}`);
}
const serviceGitSecrets = new Map();
for (const storage of [decisionCenterStorage, todoNoteStorage]) {
  if (storage?.github === undefined) continue;
  const gitSecret = storage.github.ssh;
  const existing = serviceGitSecrets.get(gitSecret.secretName);
  if (existing !== undefined && (
    existing.privateKeyKey !== gitSecret.privateKeyKey
    || existing.knownHostsKey !== gitSecret.knownHostsKey
    || existing.privateKeySource.value !== gitSecret.privateKeySource.value
    || existing.knownHostsSource.value !== gitSecret.knownHostsSource.value
  )) {
    throw new Error(`conflicting managed service Git SSH Secret: ${gitSecret.secretName}`);
  }
  serviceGitSecrets.set(gitSecret.secretName, gitSecret);
}
for (const gitSecret of serviceGitSecrets.values()) {
  docs.push(`apiVersion: v1
kind: Secret
metadata:
  name: ${gitSecret.secretName}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
  annotations:
    unidesk.ai/private-key-source-ref: ${yamlScalar(gitSecret.privateKeySource.sourceRefPath)}
    unidesk.ai/known-hosts-source-ref: ${yamlScalar(gitSecret.knownHostsSource.sourceRefPath)}
    unidesk.ai/fingerprint: ${yamlScalar(gitSecret.fingerprint)}
type: Opaque
stringData:
  ${gitSecret.privateKeyKey}: ${yamlScalar(gitSecret.privateKeySource.value)}
  ${gitSecret.knownHostsKey}: ${yamlScalar(gitSecret.knownHostsSource.value)}`);
}
if (databaseMode === "k8s-postgres") {
docs.push(`apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ${required(database.pvcName, "database.pvcName")}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ${required(database.storageClassName, "database.storageClassName")}
  resources:
    requests:
      storage: ${required(database.storageSize, "database.storageSize")}`);
docs.push(`apiVersion: v1
kind: ConfigMap
metadata:
  name: unidesk-db-init
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
data:
  001_unidesk_init.sql: |
${indent(readFileSync("src/components/database/init/001_unidesk_init.sql", "utf8"), 4)}`);
docs.push(`apiVersion: v1
kind: Service
metadata:
  name: ${dbService}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: database
  ports:
    - name: pg
      port: ${database.port}
      targetPort: pg`);
docs.push(`apiVersion: apps/v1
kind: Deployment
metadata:
  name: database
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: database
  template:
    metadata:
      labels:
        app.kubernetes.io/name: database
        app.kubernetes.io/part-of: unidesk
      annotations:
        unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
        unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
    spec:
      securityContext:
        fsGroup: 70
      containers:
        - name: postgres
          image: ${required(images.postgres, "images.postgres")}
          imagePullPolicy: IfNotPresent
          ports:
            - name: pg
              containerPort: ${database.port}
          env:
${indent(envFromSecret(secretName, secretKeys.postgresUser, "POSTGRES_USER"), 12)}
${indent(envFromSecret(secretName, secretKeys.postgresPassword, "POSTGRES_PASSWORD"), 12)}
${indent(envFromSecret(secretName, secretKeys.postgresDb, "POSTGRES_DB"), 12)}
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
            - name: init
              mountPath: /docker-entrypoint-initdb.d
              readOnly: true
          readinessProbe:
            exec:
              command: ["sh", "-ec", "pg_isready -U \\"$POSTGRES_USER\\" -d \\"$POSTGRES_DB\\""]
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 18
          startupProbe:
            exec:
              command: ["sh", "-ec", "pg_isready -U \\"$POSTGRES_USER\\" -d \\"$POSTGRES_DB\\""]
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 60
${indent(resourceBlock(database.resources), 10)}
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: ${required(database.pvcName, "database.pvcName")}
        - name: init
          configMap:
            name: unidesk-db-init`);
}
docs.push(`apiVersion: v1
kind: Service
metadata:
  name: ${required(backend.serviceName, "services.backendCore.serviceName")}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: backend-core
  ports:
    - name: http
      port: ${backend.containerPort}
      targetPort: http
    - name: provider
      port: ${backend.providerPort}
      targetPort: provider
    - name: provider-data
      port: ${backend.providerDataPort}
      targetPort: provider-data`);
docs.push(`apiVersion: v1
kind: Service
metadata:
  name: ${required(backend.providerPublicServiceName, "services.backendCore.providerPublicServiceName")}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: backend-core
  ports:
    - name: provider
      port: ${backend.providerPublicPort}
      targetPort: provider
    - name: provider-data
      port: ${backend.providerDataPublicPort}
      targetPort: provider-data`);
docs.push(`apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${required(backend.deploymentName, "services.backendCore.deploymentName")}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: backend-core
  template:
    metadata:
      labels:
        app.kubernetes.io/name: backend-core
        app.kubernetes.io/part-of: unidesk
      annotations:
        unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
        unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
    spec:
      initContainers:
        - name: wait-database
          image: ${required(images.postgres, "images.postgres")}
          imagePullPolicy: IfNotPresent
          env:
            - name: PGHOST
              value: ${yamlScalar(dbHost)}
            - name: PGPORT
              value: ${yamlScalar(dbPort)}
${indent(envFromSecret(secretName, secretKeys.postgresUser, "PGUSER"), 12)}
${indent(envFromSecret(secretName, secretKeys.postgresPassword, "PGPASSWORD"), 12)}
${indent(envFromSecret(secretName, secretKeys.postgresDb, "PGDATABASE"), 12)}
          command: ["sh", "-ec", "until pg_isready -h \\"$PGHOST\\" -p \\"$PGPORT\\" -U \\"$PGUSER\\" -d \\"$PGDATABASE\\" >/dev/null 2>&1; do sleep 2; done"]
      containers:
        - name: backend-core
          image: ${required(images.backendCore, "images.backendCore")}
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: ${backend.containerPort}
            - name: provider
              containerPort: ${backend.providerPort}
            - name: provider-data
              containerPort: ${backend.providerDataPort}
          envFrom:
            - configMapRef:
                name: ${runtimeConfigName}
          env:
            - name: PORT
              value: ${yamlScalar(backend.containerPort)}
            - name: PROVIDER_PORT
              value: ${yamlScalar(backend.providerPort)}
            - name: PROVIDER_DATA_PORT
              value: ${yamlScalar(backend.providerDataPort)}
${indent(envFromSecret(secretName, secretKeys.databaseUrl, "DATABASE_URL"), 12)}
${indent(envFromSecret(secretName, secretKeys.providerToken, "PROVIDER_TOKEN"), 12)}
            - name: LOG_FILE
              value: ${yamlScalar(required(backend.logFile, "services.backendCore.logFile"))}
          volumeMounts:
            - name: logs
              mountPath: /var/log/unidesk
          readinessProbe:
            httpGet:
              path: /health
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 20
          startupProbe:
            httpGet:
              path: /health
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 60
${indent(resourceBlock(backend.resources), 10)}
      volumes:
        - name: logs
          emptyDir: {}`);
function managedStorageEnv(envPrefix, storage) {
  const lines = [
    `- name: ${envPrefix}_STORAGE_PRIMARY`,
    `  value: ${yamlScalar(storage?.primary ?? "postgres")}`,
  ];
  if (storage?.healthRefreshSeconds !== undefined) {
    lines.push(
      `- name: ${envPrefix}_STORAGE_HEALTH_REFRESH_MS`,
      `  value: ${yamlScalar(positiveInteger(storage.healthRefreshSeconds, "storage.healthRefreshSeconds") * 1000)}`,
    );
  }
  if (storage?.github === undefined) return lines.join("\n");
  lines.push(
    `- name: ${envPrefix}_GIT_REPO`,
    `  value: ${yamlScalar(storage.github.repo)}`,
    `- name: ${envPrefix}_GIT_REPO_SSH_URL`,
    `  value: ${yamlScalar(storage.github.sshUrl)}`,
    `- name: ${envPrefix}_GIT_BRANCH`,
    `  value: ${yamlScalar(storage.github.branch)}`,
    `- name: ${envPrefix}_GIT_BASE_PATH`,
    `  value: ${yamlScalar(storage.github.basePath)}`,
    `- name: ${envPrefix}_GIT_WORKTREE`,
    `  value: ${yamlScalar(storage.github.worktreePath)}`,
    `- name: ${envPrefix}_GIT_AUTHOR_NAME`,
    `  value: ${yamlScalar(storage.github.author.name)}`,
    `- name: ${envPrefix}_GIT_AUTHOR_EMAIL`,
    `  value: ${yamlScalar(storage.github.author.email)}`,
    `- name: ${envPrefix}_GIT_COMMIT_PREFIX`,
    `  value: ${yamlScalar(storage.github.commitMessagePrefix)}`,
    "- name: GIT_SSH_COMMAND",
    `  value: ${yamlScalar(`ssh -i ${storage.github.ssh.mountPath}/${storage.github.ssh.privateKeyKey} -o UserKnownHostsFile=${storage.github.ssh.mountPath}/${storage.github.ssh.knownHostsKey} -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes`)}`,
  );
  return lines.join("\n");
}

function appendManagedServiceDeployment({ service, servicePath, serviceId, imageKey, envPrefix, databaseConfig, storageConfig }) {
if (service === null) return;
if (deliveryServiceRefs.has(servicePath)) return;
if (databaseConfig === null) throw new Error(`${servicePath}.database is required`);
const github = storageConfig?.github;
docs.push(`apiVersion: v1
kind: Service
metadata:
  name: ${required(service.serviceName, `${servicePath}.serviceName`)}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
    app.kubernetes.io/name: ${serviceId}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: ${serviceId}
  ports:
    - name: http
      port: ${positiveInteger(service.containerPort, `${servicePath}.containerPort`)}
      targetPort: http`);
docs.push(`apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${required(service.deploymentName, `${servicePath}.deploymentName`)}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
    app.kubernetes.io/name: ${serviceId}
spec:
  replicas: ${positiveInteger(service.replicas, `${servicePath}.replicas`)}
  selector:
    matchLabels:
      app.kubernetes.io/name: ${serviceId}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ${serviceId}
        app.kubernetes.io/part-of: unidesk
      annotations:
        unidesk.ai/secret-fingerprint: ${yamlScalar(databaseConfig.fingerprint)}
        unidesk.ai/storage-secret-fingerprint: ${yamlScalar(github?.ssh.fingerprint ?? "")}
        unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
    spec:
      initContainers:
        - name: wait-database
          image: ${required(images.postgres, "images.postgres")}
          imagePullPolicy: IfNotPresent
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: ${databaseConfig.secretName}
                  key: ${databaseConfig.secretKey}
          command: ["sh", "-ec", "db_url=\\"$(printf '%s' \\"$DATABASE_URL\\" | sed 's/[?&]uselibpqcompat=true//g')\\"; until psql \\"$db_url\\" -Atqc 'SELECT 1' >/dev/null 2>&1; do sleep 2; done"]
      containers:
        - name: ${required(service.containerName, `${servicePath}.containerName`)}
          image: ${required(images[imageKey], `images.${imageKey}`)}
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: ${positiveInteger(service.containerPort, `${servicePath}.containerPort`)}
          env:
            - name: HOST
              value: "0.0.0.0"
            - name: PORT
              value: ${yamlScalar(service.containerPort)}
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: ${databaseConfig.secretName}
                  key: ${databaseConfig.secretKey}
            - name: DATABASE_POOL_MAX
              value: ${yamlScalar(required(runtimeConfig.databasePoolMax, "runtimeConfig.databasePoolMax"))}
            - name: UNIDESK_DEPLOY_SERVICE_ID
              value: ${yamlScalar(serviceId)}
            - name: UNIDESK_DEPLOY_REPO
              value: ${yamlScalar(required(record(service.repository, `${servicePath}.repository`).url, `${servicePath}.repository.url`))}
            - name: UNIDESK_DEPLOY_COMMIT
              value: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
            - name: UNIDESK_DEPLOY_REQUESTED_COMMIT
              value: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
            - name: LOG_FILE
              value: ${yamlScalar(required(service.logFile, `${servicePath}.logFile`))}
${indent(managedStorageEnv(envPrefix, storageConfig), 12)}
          volumeMounts:
            - name: logs
              mountPath: /var/log/unidesk
${github === undefined ? "" : indent(`- name: github-ssh
  mountPath: ${yamlScalar(github.ssh.mountPath)}
  readOnly: true`, 12)}
          readinessProbe:
            httpGet:
              path: ${required(service.healthPath, `${servicePath}.healthPath`)}
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 18
          livenessProbe:
            httpGet:
              path: /live
              port: http
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          startupProbe:
            httpGet:
              path: /live
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 30
${indent(resourceBlock(service.resources), 10)}
      volumes:
        - name: logs
          emptyDir: {}
${github === undefined ? "" : indent(`- name: github-ssh
  secret:
    secretName: ${github.ssh.secretName}
    defaultMode: 0400`, 8)}`);
}

appendManagedServiceDeployment({
  service: decisionCenter,
  servicePath: "services.decisionCenter",
  serviceId: "decision-center",
  imageKey: "decisionCenter",
  envPrefix: "DECISION_CENTER",
  databaseConfig: decisionCenterDatabase,
  storageConfig: decisionCenterStorage,
});
appendManagedServiceDeployment({
  service: todoNote,
  servicePath: "services.todoNote",
  serviceId: "todo-note",
  imageKey: "todoNote",
  envPrefix: "TODO_NOTE",
  databaseConfig: todoNoteDatabase,
  storageConfig: todoNoteStorage,
});
docs.push(`apiVersion: v1
kind: Service
metadata:
  name: ${required(frontend.serviceName, "services.frontend.serviceName")}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: frontend
  ports:
    - name: http
      port: ${frontend.publicPort}
      targetPort: http`);
docs.push(`apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${required(frontend.deploymentName, "services.frontend.deploymentName")}
  namespace: ${namespace}
  labels:
${indent(labels(config), 4)}
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: frontend
  template:
    metadata:
      labels:
        app.kubernetes.io/name: frontend
        app.kubernetes.io/part-of: unidesk
      annotations:
        unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
        unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
    spec:
      containers:
        - name: frontend
          image: ${required(images.frontend, "images.frontend")}
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: ${frontend.containerPort}
          envFrom:
            - configMapRef:
                name: ${runtimeConfigName}
          env:
            - name: PORT
              value: ${yamlScalar(frontend.containerPort)}
            - name: CORE_INTERNAL_URL
              value: ${yamlScalar(`http://${backend.serviceName}.${namespace}.svc.cluster.local:${backend.containerPort}`)}
            - name: FRONTEND_PUBLIC_URL
              value: ${yamlScalar(`http://${runtime.publicHost}:${frontend.publicPort}`)}
            - name: PROVIDER_INGRESS_PUBLIC_URL
              value: ${yamlScalar(`ws://${runtime.publicHost}:${backend.providerPort}/ws/provider`)}
${indent(envFromConfig(runtimeConfigName, "AUTH_USERNAME", "AUTH_USERNAME"), 12)}
${indent(envFromSecret(secretName, secretKeys.authPassword, "AUTH_PASSWORD"), 12)}
${indent(envFromSecret(secretName, secretKeys.providerToken, "PROVIDER_TOKEN"), 12)}
${indent(envFromSecret(secretName, secretKeys.sshClientToken, "UNIDESK_SSH_CLIENT_TOKEN"), 12)}
${indent(envFromSecret(secretName, secretKeys.sessionSecret, "SESSION_SECRET"), 12)}
            - name: LOG_FILE
              value: ${yamlScalar(required(frontend.logFile, "services.frontend.logFile"))}
          volumeMounts:
            - name: logs
              mountPath: /var/log/unidesk
          readinessProbe:
            httpGet:
              path: /health
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 20
          startupProbe:
            httpGet:
              path: /health
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 60
${indent(resourceBlock(frontend.resources), 10)}
      volumes:
        - name: logs
          emptyDir: {}`);

process.stdout.write(`${docs.join("\n---\n")}\n`);
