// 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 { EgressProxySecretMaterial, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
import { fieldManager, requiredSecretKeys, sub2apiCaddyManagedMarker } from "./entry";
import { managedResourceCleanupPlan } from "./manifest";

export function renderPk01Caddyfile(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
  const apexHost = baseDomain(exposure.dns.hostname);
  const apiBlock = renderCaddyManagedBlock(
    sub2apiCaddyManagedMarker(target),
    renderSimpleReverseProxyCaddySiteBlock({
      hostname: exposure.dns.hostname,
      upstream: `127.0.0.1:${exposure.frpc.remotePort}`,
      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 renderPk01CaddyService(exposure: Sub2ApiPublicExposureConfig): string {
  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): string {
  const encoded = Buffer.from(yaml, "utf8").toString("base64");
  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"
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
python3 - "$client_rc" "$server_rc" "$namespace_exists" "$client_out" "$client_err" "$server_out" "$server_err" <<'PY'
import json
import sys
client_rc = int(sys.argv[1])
server_rc = int(sys.argv[2])
namespace_exists = sys.argv[3] == "true"
paths = sys.argv[4:]
def text(path):
    try:
        return open(path, encoding="utf-8").read()
    except FileNotFoundError:
        return ""
payload = {
    "ok": client_rc == 0 and server_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:],
    },
}
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,
): string {
  const encoded = Buffer.from(yaml, "utf8").toString("base64");
  const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
  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";
  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,
        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,
        }))})`;
  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"
apply_out="$tmp/apply.out"
apply_err="$tmp/apply.err"
cleanup_out="$tmp/cleanup.out"
cleanup_err="$tmp/cleanup.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
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
fi
apply_rc=1
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ] && [ "$exposure_secret_rc" -eq 0 ] && [ "$egress_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, or egress 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
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$apply_rc" "$cleanup_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_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])
apply_rc = int(sys.argv[5])
cleanup_rc = int(sys.argv[6])
secret_action = sys.argv[7]
exposure_secret_action = sys.argv[8]
egress_secret_action = sys.argv[9]
paths = sys.argv[10:]
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 apply_rc == 0 and cleanup_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},
    "egressProxy": ${egressProxySecretSummary},
    "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:]},
        "apply": {"exitCode": apply_rc, "stdout": text(paths[8])[-8000:], "stderr": text(paths[9])[-4000:]},
        "cleanup": {"exitCode": cleanup_rc, "summary": parsed(paths[10]), "stdout": text(paths[10])[-8000:], "stderr": text(paths[11])[-4000:]},
    },
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
