// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. apply-script module for scripts/src/platform-infra.ts.

// Moved mechanically from scripts/src/platform-infra.ts:2703-3090 for #903.

import { createHash, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, join } from "node:path";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import { startJob } from "../jobs";
import type { RenderedCliResult } from "../output";
import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy";
import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service";
import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library";
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";

import type { AccountLocalProxySecretMaterial, EgressProxySecretMaterial, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
import { fieldManager, requiredSecretKeys, serviceName, sub2apiCaddyManagedMarker } from "./entry";
import { managedResourceCleanupPlan } from "./manifest";

export function renderPk01Caddyfile(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
  if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when rendering PK01 Caddy`);
  const apexHost = baseDomain(exposure.dns.hostname);
  const apiBlock = renderCaddyManagedBlock(
    sub2apiCaddyManagedMarker(target),
    renderSimpleReverseProxyCaddySiteBlock({
      hostname: exposure.dns.hostname,
      upstream: publicExposureUpstream(target, exposure),
      responseHeaderTimeoutSeconds: exposure.pk01.responseHeaderTimeoutSeconds,
    }),
  );
  return `{
  email ${exposure.pk01.caddyEmail}
  storage file_system {
    root ${exposure.pk01.caddyStorageDir}
  }
}

${apexHost}, www.${apexHost} {
  reverse_proxy 127.0.0.1:${exposure.pk01.pikanodeHttpHostPort}
}

${apiBlock.trim()}
`;
}

export function publicExposureUpstream(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
  if (exposure.mode === "pk01-local") {
    if (exposure.local === null) throw new Error(`publicExposure.local is required for target ${target.id} when mode=pk01-local`);
    return `${exposure.local.upstreamHost}:${exposure.local.upstreamPort}`;
  }
  if (exposure.mode === "node-local-https") {
    if (exposure.localHttps === null) throw new Error(`publicExposure.localHttps is required for target ${target.id} when mode=node-local-https`);
    return `${exposure.dns.hostname}:${exposure.localHttps.httpsPort}`;
  }
  if (exposure.frpc === null) throw new Error(`publicExposure.frpc is required for target ${target.id} when mode=frp`);
  return `127.0.0.1:${exposure.frpc.remotePort}`;
}

export function renderPk01CaddyService(exposure: Sub2ApiPublicExposureConfig): string {
  if (exposure.pk01 === null) throw new Error("publicExposure.pk01 is required when rendering PK01 Caddy service");
  return `[Unit]
Description=UniDesk PK01 Caddy edge for PikaPython and Sub2API
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=${exposure.pk01.caddyBinaryPath} run --environ --config ${exposure.pk01.caddyConfigPath}
ExecReload=${exposure.pk01.caddyBinaryPath} reload --config ${exposure.pk01.caddyConfigPath} --force
Restart=always
RestartSec=3
LimitNOFILE=1048576
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
Environment=XDG_DATA_HOME=${exposure.pk01.caddyStorageDir}
Environment=XDG_CONFIG_HOME=/etc/caddy

[Install]
WantedBy=multi-user.target
`;
}

export function baseDomain(hostname: string): string {
  const parts = hostname.split(".");
  return parts.length <= 2 ? hostname : parts.slice(-2).join(".");
}

export function secretRoot(sub2api: Sub2ApiConfig): string {
  const root = sub2api.runtime.secrets.root;
  return isAbsolute(root) ? root : rootPath(root);
}

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

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

export function dryRunScript(
  yaml: string,
  target: Sub2ApiTargetConfig,
  accountLocalProxySecretMaterial: AccountLocalProxySecretMaterial | null,
): string {
  const encoded = Buffer.from(yaml, "utf8").toString("base64");
  const accountLocalProxySecretName = accountLocalProxySecretMaterial?.secretName ?? "sub2api-account-local-proxy-config";
  const accountLocalProxySecretKey = accountLocalProxySecretMaterial?.secretKey ?? "config.json";
  const accountLocalProxySecretFile = accountLocalProxySecretMaterial === null
    ? ""
    : `printf '%s' '${Buffer.from(accountLocalProxySecretMaterial.configJson, "utf8").toString("base64")}' | base64 -d >"$tmp/account-local-proxy-config.json"`;
  const accountLocalProxySecretSummary = accountLocalProxySecretMaterial === null
    ? "None"
    : `json.loads(${JSON.stringify(JSON.stringify({
        sourceRef: accountLocalProxySecretMaterial.sourceRef,
        sourcePath: accountLocalProxySecretMaterial.sourcePath,
        secretName: accountLocalProxySecretMaterial.secretName,
        secretKey: accountLocalProxySecretMaterial.secretKey,
        fingerprint: accountLocalProxySecretMaterial.fingerprint,
        sourceBytes: accountLocalProxySecretMaterial.sourceBytes,
        selectedOutbound: accountLocalProxySecretMaterial.selectedOutbound,
        proxyUrl: accountLocalProxySecretMaterial.proxyUrl,
        valuesPrinted: false,
      }))})`;
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/sub2api.k8s.yaml"
printf '%s' '${encoded}' | base64 -d > "$manifest"
client_out="$tmp/client.out"
client_err="$tmp/client.err"
server_out="$tmp/server.out"
server_err="$tmp/server.err"
account_local_proxy_secret_yaml="$tmp/account-local-proxy-secret.yaml"
account_local_proxy_secret_out="$tmp/account-local-proxy-secret.out"
account_local_proxy_secret_err="$tmp/account-local-proxy-secret.err"
account_local_proxy_secret_action="not-enabled"
account_local_proxy_secret_rc=0
kubectl apply --dry-run=client -f "$manifest" >"$client_out" 2>"$client_err"
client_rc=$?
if kubectl get namespace ${target.namespace} >/dev/null 2>&1; then
  namespace_exists=true
  kubectl apply --server-side --dry-run=server --field-manager=${fieldManager} -f "$manifest" >"$server_out" 2>"$server_err"
  server_rc=$?
else
  namespace_exists=false
  server_rc=0
  printf '%s\\n' 'server dry-run skipped because namespace does not exist yet; first apply creates it before namespaced resources' >"$server_out"
  : >"$server_err"
fi
if [ "${accountLocalProxySecretMaterial === null ? "false" : "true"}" = "true" ]; then
  ${accountLocalProxySecretFile}
  kubectl -n ${target.namespace} create secret generic ${accountLocalProxySecretName} \\
    --from-file=${accountLocalProxySecretKey}="$tmp/account-local-proxy-config.json" \\
    --dry-run=client -o yaml >"$account_local_proxy_secret_yaml" 2>"$account_local_proxy_secret_err"
  account_local_proxy_secret_rc=$?
  account_local_proxy_secret_action="client-dry-run"
  if [ "$account_local_proxy_secret_rc" -eq 0 ]; then
    if [ "$namespace_exists" = "true" ]; then
      kubectl apply --server-side --dry-run=server --field-manager=${fieldManager} -f "$account_local_proxy_secret_yaml" >"$account_local_proxy_secret_out" 2>>"$account_local_proxy_secret_err"
      account_local_proxy_secret_rc=$?
      account_local_proxy_secret_action="server-dry-run"
    else
      printf '%s\\n' 'server dry-run skipped because namespace does not exist yet; client dry-run succeeded' >"$account_local_proxy_secret_out"
    fi
  else
    : >"$account_local_proxy_secret_out"
  fi
else
  : >"$account_local_proxy_secret_out"
  : >"$account_local_proxy_secret_err"
fi
python3 - "$client_rc" "$server_rc" "$account_local_proxy_secret_rc" "$namespace_exists" "$account_local_proxy_secret_action" "$client_out" "$client_err" "$server_out" "$server_err" "$account_local_proxy_secret_out" "$account_local_proxy_secret_err" <<'PY'
import json
import sys
client_rc = int(sys.argv[1])
server_rc = int(sys.argv[2])
account_local_proxy_secret_rc = int(sys.argv[3])
namespace_exists = sys.argv[4] == "true"
account_local_proxy_secret_action = sys.argv[5]
paths = sys.argv[6:]
def text(path):
    try:
        return open(path, encoding="utf-8").read()
    except FileNotFoundError:
        return ""
payload = {
    "ok": client_rc == 0 and server_rc == 0 and account_local_proxy_secret_rc == 0,
    "target": "${target.id}",
    "namespace": "${target.namespace}",
    "namespaceExistsBeforeDryRun": namespace_exists,
    "clientDryRun": {
        "exitCode": client_rc,
        "stdout": text(paths[0])[-4000:],
        "stderr": text(paths[1])[-4000:],
    },
    "serverDryRun": {
        "exitCode": server_rc,
        "disposition": "executed" if namespace_exists else "skipped-namespace-missing",
        "stdout": text(paths[2])[-4000:],
        "stderr": text(paths[3])[-4000:],
    },
    "accountLocalProxy": ${accountLocalProxySecretSummary},
    "accountLocalProxySecretDryRun": {
        "exitCode": account_local_proxy_secret_rc,
        "action": account_local_proxy_secret_action,
        "stdout": text(paths[4])[-4000:],
        "stderr": text(paths[5])[-4000:],
    },
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

export function applyScript(
  sub2api: Sub2ApiConfig,
  yaml: string,
  target: Sub2ApiTargetConfig,
  secretMaterial: ExternalActiveSecretMaterial | null,
  publicExposureSecretMaterial: PublicExposureSecretMaterial | null,
  egressProxySecretMaterial: EgressProxySecretMaterial | null,
  accountLocalProxySecretMaterial: AccountLocalProxySecretMaterial | null,
): string {
  const encoded = Buffer.from(yaml, "utf8").toString("base64");
  const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
  const localHttpsExposure = target.publicExposure?.enabled === true && target.publicExposure.mode === "node-local-https" ? target.publicExposure : null;
  const localHttps = localHttpsExposure?.localHttps ?? null;
  const appSecretName = sub2api.runtime.database.secretName;
  const publicExposureSecretName = publicExposureSecretMaterial?.secretName ?? sub2api.defaults.cleanup.publicExposure.secretName;
  const publicExposureSecretKey = publicExposureSecretMaterial?.secretKey ?? "frpc.toml";
  const egressProxySecretName = egressProxySecretMaterial?.secretName ?? sub2api.defaults.cleanup.egressProxy.secretName;
  const egressProxySecretKey = egressProxySecretMaterial?.secretKey ?? "config.json";
  const accountLocalProxySecretName = accountLocalProxySecretMaterial?.secretName ?? "sub2api-account-local-proxy-config";
  const accountLocalProxySecretKey = accountLocalProxySecretMaterial?.secretKey ?? "config.json";
  if (target.databaseMode === "external-active" && secretMaterial === null) throw new Error("external-active apply requires secret material");
  const externalSecretFiles = secretMaterial === null
    ? ""
    : requiredSecretKeys.map((key) => {
        const value = Buffer.from(secretMaterial.values[key], "utf8").toString("base64");
        return `    printf '%s' '${value}' | base64 -d >"$tmp/secret.${key}"`;
      }).join("\n");
  const secretSourceSummary = secretMaterial === null
    ? "None"
    : `json.loads(${JSON.stringify(JSON.stringify({
        sourceRef: secretMaterial.sourceRef,
        sourcePath: secretMaterial.sourcePath,
        appSourceRef: secretMaterial.appSourceRef,
        appSourcePath: secretMaterial.appSourcePath,
        sourceAction: secretMaterial.action,
        fingerprint: secretMaterial.fingerprint,
        adminEmail: secretMaterial.adminEmail,
        valuesPrinted: false,
      }))})`;
  const exposureSecretFile = publicExposureSecretMaterial === null
    ? ""
    : `    printf '%s' '${Buffer.from(publicExposureSecretMaterial.frpcToml, "utf8").toString("base64")}' | base64 -d >"$tmp/frpc.toml"`;
  const exposureSecretSummary = publicExposureSecretMaterial === null
    ? "None"
    : `json.loads(${JSON.stringify(JSON.stringify({
        sourceRef: publicExposureSecretMaterial.sourceRef,
        sourcePath: publicExposureSecretMaterial.sourcePath,
        secretName: publicExposureSecretMaterial.secretName,
        secretKey: publicExposureSecretMaterial.secretKey,
        fingerprint: publicExposureSecretMaterial.fingerprint,
        valuesPrinted: false,
      }))})`;
  const egressProxySecretFile = egressProxySecretMaterial === null
    ? ""
    : `    printf '%s' '${Buffer.from(egressProxySecretMaterial.configJson, "utf8").toString("base64")}' | base64 -d >"$tmp/egress-proxy-config.json"`;
  const egressProxySecretSummary = egressProxySecretMaterial === null
    ? "None"
    : `json.loads(${JSON.stringify(JSON.stringify({
        sourceRef: egressProxySecretMaterial.sourceRef,
        sourcePath: egressProxySecretMaterial.sourcePath,
        secretName: egressProxySecretMaterial.secretName,
        secretKey: egressProxySecretMaterial.secretKey,
          fingerprint: egressProxySecretMaterial.fingerprint,
          sourceBytes: egressProxySecretMaterial.sourceBytes,
          selectedOutbound: egressProxySecretMaterial.selectedOutbound,
          sourceDiagnostics: egressProxySecretMaterial.sourceDiagnostics,
          proxyUrl: egressProxySecretMaterial.proxyUrl,
          noProxy: egressProxySecretMaterial.noProxy,
          valuesPrinted: false,
        }))})`;
  const accountLocalProxySecretFile = accountLocalProxySecretMaterial === null
    ? ""
    : `    printf '%s' '${Buffer.from(accountLocalProxySecretMaterial.configJson, "utf8").toString("base64")}' | base64 -d >"$tmp/account-local-proxy-config.json"`;
  const accountLocalProxySecretSummary = accountLocalProxySecretMaterial === null
    ? "None"
    : `json.loads(${JSON.stringify(JSON.stringify({
        sourceRef: accountLocalProxySecretMaterial.sourceRef,
        sourcePath: accountLocalProxySecretMaterial.sourcePath,
        secretName: accountLocalProxySecretMaterial.secretName,
        secretKey: accountLocalProxySecretMaterial.secretKey,
        fingerprint: accountLocalProxySecretMaterial.fingerprint,
        sourceBytes: accountLocalProxySecretMaterial.sourceBytes,
        selectedOutbound: accountLocalProxySecretMaterial.selectedOutbound,
        proxyUrl: accountLocalProxySecretMaterial.proxyUrl,
        valuesPrinted: false,
      }))})`;
  const localHttpsSummary = localHttps === null
    ? "None"
    : `json.loads(${JSON.stringify(JSON.stringify({
        publicBaseUrl: localHttpsExposure?.publicBaseUrl,
        hostname: localHttpsExposure?.dns.hostname,
        expectedA: localHttpsExposure?.dns.expectedA,
        mode: "node-local-docker-caddy",
        route: localHttps.route,
        upstreamMode: localHttps.upstreamMode,
        composePath: localHttps.composePath,
        projectName: localHttps.projectName,
        serviceName: localHttps.serviceName,
        containerName: localHttps.containerName,
        image: localHttps.image,
        bind: `${localHttps.hostBind}:${localHttps.httpsPort}`,
        tlsMode: localHttps.tlsMode,
        valuesPrinted: false,
      }))})`;
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/sub2api.k8s.yaml"
printf '%s' '${encoded}' | base64 -d > "$manifest"
ns_out="$tmp/ns.out"
ns_err="$tmp/ns.err"
secret_out="$tmp/secret.out"
secret_err="$tmp/secret.err"
exposure_secret_out="$tmp/exposure-secret.out"
exposure_secret_err="$tmp/exposure-secret.err"
egress_secret_out="$tmp/egress-secret.out"
egress_secret_err="$tmp/egress-secret.err"
account_local_proxy_secret_out="$tmp/account-local-proxy-secret.out"
account_local_proxy_secret_err="$tmp/account-local-proxy-secret.err"
apply_out="$tmp/apply.out"
apply_err="$tmp/apply.err"
cleanup_out="$tmp/cleanup.out"
cleanup_err="$tmp/cleanup.err"
local_https_out="$tmp/local-https.out"
local_https_err="$tmp/local-https.err"
kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$ns_out" 2>"$ns_err"
ns_rc=$?
secret_action="unknown"
secret_rc=0
exposure_secret_action="not-enabled"
exposure_secret_rc=0
egress_secret_action="not-enabled"
egress_secret_rc=0
account_local_proxy_secret_action="not-enabled"
account_local_proxy_secret_rc=0
local_https_action="not-enabled"
local_https_rc=0
if [ "$ns_rc" -eq 0 ]; then
  if [ "${target.databaseMode}" = "external-pending" ]; then
    secret_action="external-pending-not-managed"
    : >"$secret_out"
    printf '%s\\n' 'external DB target expects its DB credential Secret from the platform DB handoff; predeploy does not create placeholder secrets' >"$secret_err"
  elif [ "${target.databaseMode}" = "external-active" ]; then
${externalSecretFiles}
    kubectl -n ${target.namespace} create secret generic ${appSecretName} \\
      --from-file=POSTGRES_PASSWORD="$tmp/secret.POSTGRES_PASSWORD" \\
      --from-file=ADMIN_PASSWORD="$tmp/secret.ADMIN_PASSWORD" \\
      --from-file=JWT_SECRET="$tmp/secret.JWT_SECRET" \\
      --from-file=TOTP_ENCRYPTION_KEY="$tmp/secret.TOTP_ENCRYPTION_KEY" \\
      --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err"
    secret_rc=$?
    secret_action="external-active-synced"
  elif kubectl -n ${target.namespace} get secret ${appSecretName} >/dev/null 2>&1; then
    secret_action="kept-existing"
    : >"$secret_out"
    : >"$secret_err"
  else
    rand_hex() {
      bytes="$1"
      if command -v openssl >/dev/null 2>&1; then
        openssl rand -hex "$bytes"
      else
        dd if=/dev/urandom bs="$bytes" count=1 2>/dev/null | od -An -tx1 | tr -d ' \\n'
      fi
    }
    kubectl -n ${target.namespace} create secret generic ${appSecretName} \\
      --from-literal=POSTGRES_PASSWORD="$(rand_hex 32)" \\
      --from-literal=ADMIN_PASSWORD="$(rand_hex 16)" \\
      --from-literal=JWT_SECRET="$(rand_hex 32)" \\
      --from-literal=TOTP_ENCRYPTION_KEY="$(rand_hex 32)" \\
      --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err"
    secret_rc=$?
    secret_action="created"
  fi
  if [ "${publicExposureSecretMaterial === null ? "false" : "true"}" = "true" ]; then
${exposureSecretFile}
    kubectl -n ${target.namespace} create secret generic ${publicExposureSecretName} \\
      --from-file=${publicExposureSecretKey}="$tmp/frpc.toml" \\
      --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$exposure_secret_out" 2>"$exposure_secret_err"
    exposure_secret_rc=$?
    exposure_secret_action="synced"
  else
    : >"$exposure_secret_out"
    : >"$exposure_secret_err"
  fi
  if [ "${egressProxySecretMaterial === null ? "false" : "true"}" = "true" ]; then
${egressProxySecretFile}
    kubectl -n ${target.namespace} create secret generic ${egressProxySecretName} \\
      --from-file=${egressProxySecretKey}="$tmp/egress-proxy-config.json" \\
      --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$egress_secret_out" 2>"$egress_secret_err"
    egress_secret_rc=$?
    egress_secret_action="synced"
  else
    : >"$egress_secret_out"
    : >"$egress_secret_err"
  fi
  if [ "${accountLocalProxySecretMaterial === null ? "false" : "true"}" = "true" ]; then
${accountLocalProxySecretFile}
    kubectl -n ${target.namespace} create secret generic ${accountLocalProxySecretName} \\
      --from-file=${accountLocalProxySecretKey}="$tmp/account-local-proxy-config.json" \\
      --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$account_local_proxy_secret_out" 2>"$account_local_proxy_secret_err"
    account_local_proxy_secret_rc=$?
    account_local_proxy_secret_action="synced"
  else
    : >"$account_local_proxy_secret_out"
    : >"$account_local_proxy_secret_err"
  fi
fi
apply_rc=1
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ] && [ "$exposure_secret_rc" -eq 0 ] && [ "$egress_secret_rc" -eq 0 ] && [ "$account_local_proxy_secret_rc" -eq 0 ]; then
  kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err"
  apply_rc=$?
else
  : >"$apply_out"
  printf '%s\\n' 'skipped because namespace, app secret, public exposure secret, egress proxy secret, or account-local proxy secret step failed' >"$apply_err"
fi
cleanup_rc=0
if [ "$apply_rc" -eq 0 ]; then
  python3 - "$cleanup_out" "$cleanup_err" <<'PY'
import json
import subprocess
import sys
import time

out_path, err_path = sys.argv[1:3]
namespace = ${JSON.stringify(target.namespace)}
plan = json.loads(${JSON.stringify(JSON.stringify(cleanupPlan))})

def run(args):
    proc = subprocess.run(["kubectl", "-n", namespace, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    return {
        "args": ["kubectl", "-n", namespace, *args],
        "exitCode": proc.returncode,
        "stdout": proc.stdout.decode("utf-8", errors="replace")[-2000:],
        "stderr": proc.stderr.decode("utf-8", errors="replace")[-2000:],
    }

def delete(kind, name):
    return run(["delete", kind, name, "--ignore-not-found=true", "--wait=false"])

items = []
external_db_state = plan["externalDbState"]
redis_persistent_state = plan["redisPersistentState"]
if external_db_state["enabled"]:
    items.append({"name": "legacy-postgres-statefulset", **delete("statefulset", external_db_state["postgresStatefulSetName"])})
    items.append({"name": "legacy-postgres-service", **delete("service", external_db_state["postgresServiceName"])})
    items.append({"name": "legacy-postgres-pvc", **delete("pvc", external_db_state["postgresPvcName"])})
    items.append({"name": "legacy-app-data-pvc", **delete("pvc", external_db_state["appDataPvcName"])})
if redis_persistent_state["enabled"]:
    items.append({"name": "legacy-redis-pvc", **delete("pvc", redis_persistent_state["pvcName"])})
if not plan["publicExposure"]["enabled"]:
    items.append({"name": "disabled-public-frpc-deployment", **delete("deployment", plan["publicExposure"]["deploymentName"])})
    items.append({"name": "disabled-public-frpc-configmap", **delete("configmap", plan["publicExposure"]["configMapName"])})
    items.append({"name": "disabled-public-frpc-secret", **delete("secret", plan["publicExposure"]["secretName"])})
if not plan["egressProxy"]["enabled"]:
    items.append({"name": "disabled-egress-proxy-deployment", **delete("deployment", plan["egressProxy"]["deploymentName"])})
    items.append({"name": "disabled-egress-proxy-service", **delete("service", plan["egressProxy"]["serviceName"])})
    items.append({"name": "disabled-egress-proxy-secret", **delete("secret", plan["egressProxy"]["secretName"])})
if not plan["sentinel"]["enabled"]:
    items.append({"name": "disabled-sentinel-cronjob", **delete("cronjob", plan["sentinel"]["cronJobName"])})
    items.append({"name": "disabled-sentinel-jobs", **run(["delete", "job", "-l", f"app.kubernetes.io/name={plan['sentinel']['cronJobName']}", "--ignore-not-found=true", "--wait=false"])})
    items.append({"name": "disabled-sentinel-configmap", **delete("configmap", plan["sentinel"]["configMapName"])})
    items.append({"name": "disabled-sentinel-credentials-secret", **delete("secret", plan["sentinel"]["credentialsSecretName"])})
    items.append({"name": "disabled-sentinel-serviceaccount", **delete("serviceaccount", plan["sentinel"]["serviceAccountName"])})
    items.append({"name": "disabled-sentinel-role", **delete("role", plan["sentinel"]["roleName"])})
    items.append({"name": "disabled-sentinel-rolebinding", **delete("rolebinding", plan["sentinel"]["roleBindingName"])})

watch = []
if external_db_state["enabled"]:
    watch.extend([
        ("statefulset", external_db_state["postgresStatefulSetName"]),
        ("service", external_db_state["postgresServiceName"]),
        ("pvc", external_db_state["postgresPvcName"]),
        ("pvc", external_db_state["appDataPvcName"]),
    ])
if redis_persistent_state["enabled"]:
    watch.append(("pvc", redis_persistent_state["pvcName"]))
if not plan["publicExposure"]["enabled"]:
    watch.append(("deployment", plan["publicExposure"]["deploymentName"]))
if not plan["egressProxy"]["enabled"]:
    watch.append(("deployment", plan["egressProxy"]["deploymentName"]))
    watch.append(("service", plan["egressProxy"]["serviceName"]))
if not plan["sentinel"]["enabled"]:
    watch.append(("cronjob", plan["sentinel"]["cronJobName"]))

deadline = time.time() + 90
remaining = []
while True:
    remaining = []
    for kind, name in watch:
        result = run(["get", kind, name])
        if result["exitCode"] == 0:
            remaining.append({"kind": kind, "name": name})
    if not remaining or time.time() >= deadline:
        break
    time.sleep(3)

payload = {
    "ok": all(item["exitCode"] == 0 for item in items) and not remaining,
    "plan": plan,
    "items": items,
    "remainingAfterWait": remaining,
    "valuesPrinted": False,
}
open(out_path, "w", encoding="utf-8").write(json.dumps(payload, ensure_ascii=False, indent=2))
open(err_path, "w", encoding="utf-8").write("")
sys.exit(0 if payload["ok"] else 1)
PY
  cleanup_rc=$?
else
  : >"$cleanup_out"
  printf '%s\\n' 'skipped because apply failed' >"$cleanup_err"
  cleanup_rc=1
fi
if [ "${localHttps === null ? "false" : "true"}" = "true" ]; then
  if [ "$apply_rc" -eq 0 ] && [ "$cleanup_rc" -eq 0 ]; then
    edge_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.workDir)}
    compose_path=${localHttps === null ? "\"\"" : shQuote(localHttps.composePath)}
    caddyfile_path=${localHttps === null ? "\"\"" : shQuote(localHttps.caddyfilePath)}
    data_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.dataDir)}
    config_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.configDir)}
    mkdir -p "$edge_dir" "$(dirname "$compose_path")" "$(dirname "$caddyfile_path")" "$data_dir" "$config_dir" >"$local_https_out" 2>"$local_https_err"
    local_https_rc=$?
    if [ "$local_https_rc" -eq 0 ]; then
      service_cluster_ip="$(kubectl -n ${target.namespace} get service ${serviceName} -o jsonpath='{.spec.clusterIP}' 2>>"$local_https_err" || true)"
      if [ -z "$service_cluster_ip" ] || [ "$service_cluster_ip" = "None" ]; then
        printf '%s\\n' 'sub2api service clusterIP is empty; cannot render node-local HTTPS edge' >>"$local_https_err"
        local_https_rc=1
      fi
    fi
    if [ "$local_https_rc" -eq 0 ]; then
      cat >"$caddyfile_path" <<'CADDY'
https://${localHttpsExposure?.dns.hostname}:${localHttps?.httpsPort} {
  tls internal
  reverse_proxy __SUB2API_SERVICE_CLUSTER_IP__:8080 {
    transport http {
      response_header_timeout ${localHttps?.responseHeaderTimeoutSeconds}s
    }
  }
}
CADDY
      sed -i "s/__SUB2API_SERVICE_CLUSTER_IP__/$service_cluster_ip/g" "$caddyfile_path"
      cat >"$compose_path" <<'COMPOSE'
name: ${localHttps?.projectName}

services:
  ${localHttps?.serviceName}:
    image: ${localHttps?.image}
    container_name: ${localHttps?.containerName}
    restart: unless-stopped
    ports:
      - "${localHttps?.hostBind}:${localHttps?.httpsPort}:${localHttps?.httpsPort}"
    volumes:
      - ${localHttps?.caddyfilePath}:/etc/caddy/Caddyfile:ro
      - ${localHttps?.dataDir}:/data
      - ${localHttps?.configDir}:/config
COMPOSE
      chmod 0644 "$caddyfile_path" "$compose_path" >>"$local_https_out" 2>>"$local_https_err" || local_https_rc=$?
    fi
    if [ "$local_https_rc" -eq 0 ]; then
      docker compose -f "$compose_path" -p ${localHttps === null ? "\"\"" : shQuote(localHttps.projectName)} up -d --force-recreate --remove-orphans >>"$local_https_out" 2>>"$local_https_err"
      local_https_rc=$?
    fi
    if [ "$local_https_rc" -eq 0 ]; then
      sleep 2
      curl --noproxy '*' -kfsS --connect-timeout 5 --max-time 20 --resolve ${localHttpsExposure === null || localHttps === null ? "\"\"" : shQuote(`${localHttpsExposure.dns.hostname}:${localHttps.httpsPort}:127.0.0.1`)} ${localHttpsExposure === null ? "\"\"" : shQuote(`${localHttpsExposure.publicBaseUrl}/health`)} >>"$local_https_out" 2>>"$local_https_err"
      local_https_rc=$?
    fi
    local_https_action="synced"
  else
    printf '%s\\n' 'skipped because Kubernetes apply or cleanup failed' >"$local_https_err"
    : >"$local_https_out"
    local_https_action="skipped-after-k8s-failure"
  fi
else
  : >"$local_https_out"
  : >"$local_https_err"
fi
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$account_local_proxy_secret_rc" "$apply_rc" "$cleanup_rc" "$local_https_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$account_local_proxy_secret_action" "$local_https_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$account_local_proxy_secret_out" "$account_local_proxy_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_err" "$local_https_out" "$local_https_err" <<'PY'
import json
import sys
ns_rc = int(sys.argv[1])
secret_rc = int(sys.argv[2])
exposure_secret_rc = int(sys.argv[3])
egress_secret_rc = int(sys.argv[4])
account_local_proxy_secret_rc = int(sys.argv[5])
apply_rc = int(sys.argv[6])
cleanup_rc = int(sys.argv[7])
local_https_rc = int(sys.argv[8])
secret_action = sys.argv[9]
exposure_secret_action = sys.argv[10]
egress_secret_action = sys.argv[11]
account_local_proxy_secret_action = sys.argv[12]
local_https_action = sys.argv[13]
paths = sys.argv[14:]
def text(path):
    try:
        return open(path, encoding="utf-8").read()
    except FileNotFoundError:
        return ""
def parsed(path):
    try:
        return json.load(open(path, encoding="utf-8"))
    except Exception:
        return None
payload = {
    "ok": ns_rc == 0 and secret_rc == 0 and exposure_secret_rc == 0 and egress_secret_rc == 0 and account_local_proxy_secret_rc == 0 and apply_rc == 0 and cleanup_rc == 0 and local_https_rc == 0,
    "target": "${target.id}",
    "namespace": "${target.namespace}",
    "databaseMode": "${target.databaseMode}",
    "appReplicas": ${target.appReplicas},
    "redisReplicas": ${target.redisReplicas},
    "secret": {
        "name": "${appSecretName}",
        "action": secret_action,
        "requiredKeys": ${JSON.stringify(requiredSecretKeys)},
        "managedByThisApply": ${target.databaseMode === "external-pending" ? "False" : "True"},
        "externalActiveSource": ${secretSourceSummary},
        "valuesPrinted": False,
    },
    "publicExposure": ${exposureSecretSummary},
    "localHttpsExposure": ${localHttpsSummary},
    "egressProxy": ${egressProxySecretSummary},
    "accountLocalProxy": ${accountLocalProxySecretSummary},
    "steps": {
        "namespace": {"exitCode": ns_rc, "stdout": text(paths[0])[-4000:], "stderr": text(paths[1])[-4000:]},
        "secret": {"exitCode": secret_rc, "stdout": text(paths[2])[-4000:], "stderr": text(paths[3])[-4000:]},
        "publicExposureSecret": {"exitCode": exposure_secret_rc, "action": exposure_secret_action, "stdout": text(paths[4])[-4000:], "stderr": text(paths[5])[-4000:]},
        "egressProxySecret": {"exitCode": egress_secret_rc, "action": egress_secret_action, "stdout": text(paths[6])[-4000:], "stderr": text(paths[7])[-4000:]},
        "accountLocalProxySecret": {"exitCode": account_local_proxy_secret_rc, "action": account_local_proxy_secret_action, "stdout": text(paths[8])[-4000:], "stderr": text(paths[9])[-4000:]},
        "apply": {"exitCode": apply_rc, "stdout": text(paths[10])[-8000:], "stderr": text(paths[11])[-4000:]},
        "cleanup": {"exitCode": cleanup_rc, "summary": parsed(paths[12]), "stdout": text(paths[12])[-8000:], "stderr": text(paths[13])[-4000:]},
        "localHttpsExposure": {"exitCode": local_https_rc, "action": local_https_action, "stdout": text(paths[14])[-4000:], "stderr": text(paths[15])[-4000:]},
    },
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
