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

// Moved mechanically from scripts/src/platform-infra.ts:2226-2702 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 { sub2apiCaddyManagedMarker } from "./entry";
import type { EgressProxySubscriptionCandidateSummary, EgressProxySubscriptionDiagnostics, Sub2ApiEgressProxyConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
import { status } from "./actions";
import { publicExposureUpstream, renderPk01CaddyService, renderPk01Caddyfile } from "./apply-script";
import { asRecordOrNull, boolField } from "./utils";

export type SubscriptionNode =
  | { kind: "vless-reality"; uri: string; uuid: string; host: string; port: number; sni: string; flow: string | null; fingerprint: string | null; publicKey: string; shortId: string | null }
  | { kind: "hysteria2"; uri: string; password: string; host: string; port: number; sni: string | null; obfsPassword: string | null; insecure: boolean };

export function selectSubscriptionNode(nodes: string[], preferred: Exclude<Sub2ApiEgressProxyConfig["preferredOutbound"], null>): SubscriptionNode {
  const analyzed = analyzeEgressProxySubscription(nodes, preferred);
  if (analyzed.selected === null) throw new Error(analyzed.diagnostics.error ?? `egressProxy preferredOutbound=${preferred} is absent from subscription; no fallback outbound was selected`);
  return analyzed.selected;
}

export function analyzeEgressProxySubscription(nodes: string[], preferred: Exclude<Sub2ApiEgressProxyConfig["preferredOutbound"], null>): { diagnostics: EgressProxySubscriptionDiagnostics; selected: SubscriptionNode | null } {
  const parsed = nodes
    .map((uri, index) => ({ uri, sourceLine: index, node: parseSubscriptionNode(uri) }))
    .filter((entry): entry is { uri: string; sourceLine: number; node: SubscriptionNode } => entry.node !== null);
  const selectedEntry = parsed.find((entry) => entry.node.kind === preferred) ?? null;
  const byKind: Record<string, number> = {};
  for (const entry of parsed) byKind[entry.node.kind] = (byKind[entry.node.kind] ?? 0) + 1;
  const candidates = parsed.map((entry): EgressProxySubscriptionCandidateSummary => {
    const fingerprint = fingerprintSecretValues({ candidate: entry.uri }, ["candidate"]);
    return {
      sourceLine: entry.sourceLine,
      kind: entry.node.kind,
      fingerprint,
      paramKeys: subscriptionUriParamKeys(entry.uri),
      selected: selectedEntry !== null && entry.sourceLine === selectedEntry.sourceLine,
    };
  });
  const selectedFingerprint = selectedEntry === null ? null : fingerprintSecretValues({ candidate: selectedEntry.uri }, ["candidate"]);
  const diagnostics: EgressProxySubscriptionDiagnostics = {
    ok: selectedEntry !== null,
    sourceType: "subscription-url",
    preferredOutbound: preferred,
    candidateCount: nodes.length,
    supportedCount: parsed.length,
    unsupportedCount: nodes.length - parsed.length,
    byKind,
    selectedOutbound: selectedEntry?.node.kind ?? null,
    selectedFingerprint,
    candidates,
    valuesPrinted: false,
  };
  if (selectedEntry === null) {
    diagnostics.error = `egressProxy preferredOutbound=${preferred} is absent from subscription; no fallback outbound was selected`;
  }
  return { diagnostics, selected: selectedEntry?.node ?? null };
}

export function subscriptionUriParamKeys(uri: string): string[] {
  try {
    return Array.from(new URL(uri).searchParams.keys()).sort();
  } catch {
    return [];
  }
}

export function parseSubscriptionNode(uri: string): SubscriptionNode | null {
  let url: URL;
  try {
    url = new URL(uri);
  } catch {
    return null;
  }
  if (url.protocol === "vless:") {
    const params = url.searchParams;
    if (params.get("security") !== "reality") return null;
    const publicKey = params.get("pbk");
    if (publicKey === null || publicKey.length === 0) return null;
    return {
      kind: "vless-reality",
      uri,
      uuid: decodeURIComponent(url.username),
      host: requiredUrlHost(url, uri),
      port: requiredUrlPort(url, uri),
      sni: params.get("sni") ?? "",
      flow: params.get("flow"),
      fingerprint: params.get("fp"),
      publicKey,
      shortId: params.get("sid"),
    };
  }
  if (url.protocol === "hysteria2:" || url.protocol === "hy2:") {
    const params = url.searchParams;
    return {
      kind: "hysteria2",
      uri,
      password: decodeURIComponent(url.username),
      host: requiredUrlHost(url, uri),
      port: requiredUrlPort(url, uri),
      sni: params.get("sni"),
      obfsPassword: params.get("obfs-password"),
      insecure: params.get("insecure") === "1" || params.get("insecure") === "true",
    };
  }
  return null;
}

export function requiredUrlHost(url: URL, uri: string): string {
  if (url.hostname.length === 0) throw new Error(`VPN subscription node is missing host: ${redactSubscriptionUri(uri)}`);
  return url.hostname;
}

export function requiredUrlPort(url: URL, uri: string): number {
  const port = Number(url.port);
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`VPN subscription node is missing a valid port: ${redactSubscriptionUri(uri)}`);
  return port;
}

export function renderSingBoxConfig(node: SubscriptionNode, proxy: Sub2ApiEgressProxyConfig): string {
  const outbound = node.kind === "vless-reality"
    ? {
        type: "vless",
        tag: "master-vpn",
        server: node.host,
        server_port: node.port,
        uuid: node.uuid,
        flow: node.flow ?? undefined,
        tls: {
          enabled: true,
          server_name: node.sni,
          utls: { enabled: true, fingerprint: node.fingerprint ?? "chrome" },
          reality: { enabled: true, public_key: node.publicKey, short_id: node.shortId ?? "" },
        },
      }
    : {
        type: "hysteria2",
        tag: "master-vpn",
        server: node.host,
        server_port: node.port,
        password: node.password,
        obfs: node.obfsPassword === null ? undefined : { type: "salamander", password: node.obfsPassword },
        tls: { enabled: true, server_name: node.sni ?? node.host, insecure: node.insecure },
      };
  return renderSingBoxProxyConfig(outbound, proxy);
}

export function renderSingBoxMasterShadowsocksConfig(proxy: Sub2ApiEgressProxyConfig, password: string): string {
  const shadowsocks = proxy.masterShadowsocks;
  if (shadowsocks === null) throw new Error("masterShadowsocks config is required");
  return renderSingBoxProxyConfig({
    type: "shadowsocks",
    tag: "master-vpn",
    server: shadowsocks.serverHost,
    server_port: shadowsocks.serverPort,
    method: shadowsocks.method,
    password,
  }, proxy);
}

export function renderSingBoxProxyConfig(outbound: Record<string, unknown>, proxy: Sub2ApiEgressProxyConfig): string {
  const config = stripUndefined({
    log: { level: "info", timestamp: true },
    experimental: {
      clash_api: {
        external_controller: "127.0.0.1:9090",
      },
    },
    inbounds: [
      {
        type: "mixed",
        tag: "mixed-in",
        listen: "0.0.0.0",
        listen_port: proxy.listenPort,
      },
    ],
    outbounds: [
      outbound,
      { type: "direct", tag: "direct" },
      { type: "block", tag: "block" },
    ],
    route: {
      rules: [
        { ip_is_private: true, outbound: "direct" },
        { domain_suffix: ["cluster.local", "svc"], outbound: "direct" },
      ],
      final: "master-vpn",
    },
  });
  return `${JSON.stringify(config, null, 2)}\n`;
}

export function stripUndefined(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(stripUndefined);
  if (value !== null && typeof value === "object") {
    const output: Record<string, unknown> = {};
    for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
      if (child !== undefined) output[key] = stripUndefined(child);
    }
    return output;
  }
  return value;
}

export function redactSubscriptionUri(uri: string): string {
  return uri.replace(/\/\/[^@]+@/u, "//<redacted>@").replace(/password=[^&#]+/giu, "password=<redacted>").replace(/pbk=[^&#]+/giu, "pbk=<redacted>");
}

export async function applyPk01PublicExposure(config: UniDeskConfig, target: Sub2ApiTargetConfig): Promise<Record<string, unknown>> {
  const exposure = target.publicExposure;
  if (exposure === null) return { ok: true, action: "not-configured" };
  if (exposure.mode === "node-local-https") return { ok: true, action: "not-applicable-node-local-https" };
  if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when applying PK01 public exposure`);
  if (!exposure.enabled) return await removePk01PublicExposure(config, target, exposure);
  const start = await startPk01PublicExposureJob(config, target, exposure);
  if (!start.ok || typeof start.remoteJobId !== "string") {
    return {
      ok: false,
      action: "platform-infra-sub2api-pk01-public-exposure",
      route: exposure.pk01.route,
      mode: "remote-job-start-failed",
      remoteJob: start,
    };
  }
  const waited = await waitPk01PublicExposureJob(config, exposure, start.remoteJobId);
  const terminal = asRecordOrNull(waited.terminal);
  const summary = asRecordOrNull(terminal?.summary);
  return {
    ok: waited.ok === true && boolField(summary, "ok", false),
    action: "platform-infra-sub2api-pk01-public-exposure",
    route: exposure.pk01.route,
    mode: "remote-job",
    summary,
    remoteJob: {
      start,
      waited,
    },
  };
}

export async function removePk01PublicExposure(config: UniDeskConfig, target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): Promise<Record<string, unknown>> {
  if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when removing PK01 public exposure`);
  const marker = sub2apiCaddyManagedMarker(target);
  const script = `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
config_path=${shQuote(exposure.pk01.caddyConfigPath)}
service_name=${shQuote(exposure.pk01.caddyServiceName)}
marker=${shQuote(marker)}
hostname=${shQuote(exposure.dns.hostname)}
next="$tmp/Caddyfile.next"
update_out="$tmp/update.out"
update_err="$tmp/update.err"
fmt_out="$tmp/fmt.out"
fmt_err="$tmp/fmt.err"
validate_out="$tmp/validate.out"
validate_err="$tmp/validate.err"
install_out="$tmp/install.out"
install_err="$tmp/install.err"
reload_out="$tmp/reload.out"
reload_err="$tmp/reload.err"
if ! [ -f "$config_path" ]; then
  python3 - "$config_path" "$marker" "$hostname" <<'PY'
import json
import sys
print(json.dumps({
    "ok": True,
    "action": "caddy-config-missing-noop",
    "configPath": sys.argv[1],
    "marker": sys.argv[2],
    "hostname": sys.argv[3],
    "valuesPrinted": False,
}, ensure_ascii=False, indent=2))
PY
  exit 0
fi
sudo python3 - "$config_path" "$next" "$marker" >"$update_out" 2>"$update_err" <<'PY'
import pathlib
import re
import sys

config_path = pathlib.Path(sys.argv[1])
next_path = pathlib.Path(sys.argv[2])
marker = sys.argv[3]
text = config_path.read_text(encoding="utf-8")
pattern = re.compile(r"(?ms)^# BEGIN unidesk managed (?P<name>[^\\n]+)\\n(?P<body>.*?)\\n# END unidesk managed (?P=name)\\n*")
removed = False

def replace(match):
    global removed
    if match.group("name") != marker:
        return match.group(0)
    removed = True
    return ""

next_text = pattern.sub(replace, text)
next_text = re.sub(r"\\n{3,}", "\\n\\n", next_text).rstrip() + "\\n"
next_path.write_text(next_text, encoding="utf-8")
print("removed" if removed else "absent")
PY
update_rc=$?
if [ "$update_rc" -eq 0 ]; then
  sudo caddy fmt --overwrite "$next" >"$fmt_out" 2>"$fmt_err"
  fmt_rc=$?
else
  : >"$fmt_out"; : >"$fmt_err"; fmt_rc=1
fi
if [ "$fmt_rc" -eq 0 ]; then
  sudo caddy validate --config "$next" >"$validate_out" 2>"$validate_err"
  validate_rc=$?
else
  : >"$validate_out"; : >"$validate_err"; validate_rc=1
fi
if [ "$validate_rc" -eq 0 ]; then
  sudo install -m 0644 "$next" "$config_path" >"$install_out" 2>"$install_err"
  install_rc=$?
else
  : >"$install_out"; : >"$install_err"; install_rc=1
fi
if [ "$install_rc" -eq 0 ]; then
  sudo systemctl reload "$service_name" >"$reload_out" 2>"$reload_err" || sudo systemctl restart "$service_name" >>"$reload_out" 2>>"$reload_err"
  reload_rc=$?
else
  : >"$reload_out"; : >"$reload_err"; reload_rc=1
fi
python3 - "$update_rc" "$fmt_rc" "$validate_rc" "$install_rc" "$reload_rc" "$config_path" "$marker" "$hostname" "$update_out" "$update_err" "$fmt_out" "$fmt_err" "$validate_out" "$validate_err" "$install_out" "$install_err" "$reload_out" "$reload_err" <<'PY'
import json
import sys

update_rc, fmt_rc, validate_rc, install_rc, reload_rc = [int(value) for value in sys.argv[1:6]]
config_path, marker, hostname = sys.argv[6:9]
paths = sys.argv[9:]

def text(path, limit=3000):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-limit:]
    except FileNotFoundError:
        return ""

update_stdout = text(paths[0], 1000).strip()
payload = {
    "ok": update_rc == 0 and fmt_rc == 0 and validate_rc == 0 and install_rc == 0 and reload_rc == 0,
    "action": "removed-managed-block" if update_stdout == "removed" else "managed-block-absent",
    "target": "${target.id}",
    "publicBaseUrl": "${exposure.publicBaseUrl}",
    "hostname": hostname,
    "configPath": config_path,
    "managedBlock": {"marker": marker},
    "steps": {
        "update": {"exitCode": update_rc, "stdout": update_stdout, "stderr": text(paths[1])},
        "fmt": {"exitCode": fmt_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
        "validate": {"exitCode": validate_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
        "install": {"exitCode": install_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
        "reload": {"exitCode": reload_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
    },
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
  const result = await capture(config, exposure.pk01.route, ["sh"], script);
  const parsed = parseJsonOutput(result.stdout);
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false),
    action: "platform-infra-sub2api-pk01-public-exposure",
    route: exposure.pk01.route,
    mode: "remove-disabled-managed-block",
    summary: parsed ?? null,
    capture: compactCapture(result, { full: result.exitCode !== 0 }),
  };
}

export async function startPk01PublicExposureJob(config: UniDeskConfig, target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): Promise<Record<string, unknown>> {
  if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when starting PK01 public exposure`);
  const jobId = `sub2api-pk01-exposure-${new Date().toISOString().replace(/[^0-9A-Za-z]/gu, "")}-${randomBytes(3).toString("hex")}`;
  const payload = pk01PublicExposureScript(target, exposure);
  const payloadB64 = Buffer.from(payload, "utf8").toString("base64");
  const script = `
set -eu
job_id=${shQuote(jobId)}
base="$HOME/.unidesk/platform-infra/sub2api-pk01-exposure/jobs"
job_dir="$base/$job_id"
mkdir -p "$job_dir"
payload="$job_dir/payload.sh"
runner="$job_dir/runner.sh"
stdout="$job_dir/stdout.log"
stderr="$job_dir/stderr.log"
state="$job_dir/state.json"
printf '%s' '${payloadB64}' | base64 -d >"$payload"
chmod 0700 "$payload"
cat >"$runner" <<'RUNNER'
#!/bin/sh
set +e
job_dir="$1"
payload="$job_dir/payload.sh"
stdout="$job_dir/stdout.log"
stderr="$job_dir/stderr.log"
state="$job_dir/state.json"
write_state() {
  status="$1"
  stage="$2"
  exit_code="$3"
  python3 - "$state" "$status" "$stage" "$exit_code" <<'PY'
import datetime
import json
import sys

path, status, stage, exit_code = sys.argv[1:5]
payload = {
    "ok": status == "succeeded",
    "status": status,
    "stage": stage,
    "updatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "exitCode": None if exit_code == "" else int(exit_code),
}
if status in ("succeeded", "failed"):
    payload["finishedAt"] = payload["updatedAt"]
with open(path, "w", encoding="utf-8") as handle:
    json.dump(payload, handle, ensure_ascii=False)
PY
}
write_state running start ""
sh "$payload" >"$stdout" 2>"$stderr"
rc=$?
if [ "$rc" -eq 0 ]; then
  write_state succeeded complete "$rc"
else
  write_state failed complete "$rc"
fi
exit "$rc"
RUNNER
chmod 0700 "$runner"
python3 - "$state" <<'PY'
import datetime
import json
import sys

payload = {
    "ok": False,
    "status": "queued",
    "stage": "queued",
    "updatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "exitCode": None,
}
with open(sys.argv[1], "w", encoding="utf-8") as handle:
    json.dump(payload, handle, ensure_ascii=False)
PY
nohup "$runner" "$job_dir" >/dev/null 2>&1 &
printf '%s' "$!" >"$job_dir/pid"
python3 - "$job_id" "$job_dir" <<'PY'
import json
import os
import sys

job_id, job_dir = sys.argv[1:3]
payload = {
    "ok": True,
    "remoteJobId": job_id,
    "statePath": os.path.join(job_dir, "state.json"),
    "stdoutPath": os.path.join(job_dir, "stdout.log"),
    "stderrPath": os.path.join(job_dir, "stderr.log"),
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY
`;
  const result = await capture(config, exposure.pk01.route, ["sh"], script);
  const parsed = parseJsonOutput(result.stdout);
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false),
    remoteJobId: typeof parsed?.remoteJobId === "string" ? parsed.remoteJobId : null,
    parsed,
    capture: compactCapture(result, { full: result.exitCode !== 0 }),
  };
}

export async function waitPk01PublicExposureJob(config: UniDeskConfig, exposure: Sub2ApiPublicExposureConfig, remoteJobId: string): Promise<Record<string, unknown>> {
  if (exposure.pk01 === null) throw new Error("publicExposure.pk01 is required when waiting for PK01 public exposure");
  const observations: Array<Record<string, unknown>> = [];
  const maxPolls = 120;
  let lastProgressKey: string | null = null;
  for (let attempt = 0; attempt < maxPolls; attempt += 1) {
    const observation = await readPk01PublicExposureJob(config, exposure, remoteJobId);
    observations.push(observation.summary);
    const statusValue = observation.status;
    const stage = typeof observation.summary.stage === "string" ? observation.summary.stage : null;
    const progressKey = `${statusValue ?? "unknown"}:${stage ?? "unknown"}`;
    if (attempt === 0 || progressKey !== lastProgressKey || attempt % 6 === 0 || statusValue === "succeeded" || statusValue === "failed") {
      lastProgressKey = progressKey;
      console.error(JSON.stringify({
        event: "platform-infra.sub2api.pk01-exposure.progress",
        at: new Date().toISOString(),
        remoteJobId,
        attempt: attempt + 1,
        status: statusValue,
        stage,
        exitCode: observation.summary.exitCode ?? null,
      }));
    }
    if (statusValue === "succeeded" || statusValue === "failed") {
      return {
        ok: statusValue === "succeeded" && boolField(asRecordOrNull(observation.summary.summary), "ok", false),
        attempts: attempt + 1,
        terminal: observation.summary,
        recent: observations.slice(-10),
      };
    }
    await Bun.sleep(5000);
  }
  return {
    ok: false,
    attempts: maxPolls,
    terminal: null,
    recent: observations.slice(-10),
    error: "PK01 public exposure job did not finish before local polling budget",
  };
}

export async function readPk01PublicExposureJob(config: UniDeskConfig, exposure: Sub2ApiPublicExposureConfig, remoteJobId: string): Promise<{ status: string | null; summary: Record<string, unknown> }> {
  if (exposure.pk01 === null) throw new Error("publicExposure.pk01 is required when reading PK01 public exposure job");
  const safeId = remoteJobId.replace(/[^A-Za-z0-9_.-]/gu, "");
  const script = `
set +e
job_dir="$HOME/.unidesk/platform-infra/sub2api-pk01-exposure/jobs/${safeId}"
state="$job_dir/state.json"
stdout="$job_dir/stdout.log"
stderr="$job_dir/stderr.log"
if [ -f "$state" ]; then cat "$state"; else printf '{"status":"missing","stage":"missing","exitCode":null}'; fi
printf '\\n---STDOUT---\\n'
if [ -f "$stdout" ]; then tail -200 "$stdout"; fi
printf '\\n---STDERR---\\n'
if [ -f "$stderr" ]; then tail -120 "$stderr"; fi
printf '\\n---DOWNLOAD---\\n'
download_part="$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64.part"
download_cache="$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64"
if [ -f "$download_part" ]; then stat -c 'part_bytes=%s part_mtime=%y' "$download_part"; fi
if [ -f "$download_cache" ]; then stat -c 'cache_bytes=%s cache_mtime=%y' "$download_cache"; fi
`;
  const result = await capture(config, exposure.pk01.route, ["sh"], script);
  const [stateText, rest = ""] = result.stdout.split("\n---STDOUT---\n");
  const [stdoutAndMaybeStderr = "", downloadProgress = ""] = rest.split("\n---DOWNLOAD---\n");
  const [stdoutTail = "", stderrTail = ""] = stdoutAndMaybeStderr.split("\n---STDERR---\n");
  const parsed = parseJsonOutput(stateText) ?? {};
  const statusValue = typeof parsed.status === "string" ? parsed.status : null;
  const summary = parseJsonOutput(stdoutTail);
  return {
    status: statusValue,
    summary: {
      ok: result.exitCode === 0,
      remoteJobId,
      status: statusValue,
      stage: parsed.stage ?? null,
      updatedAt: parsed.updatedAt ?? null,
      finishedAt: parsed.finishedAt ?? null,
      exitCode: parsed.exitCode ?? null,
      summary,
      stdoutTail: stdoutTail.slice(-8000),
      stderrTail: stderrTail.slice(-4000),
      downloadProgress: downloadProgress.slice(-1000),
      capture: compactCapture(result, { full: result.exitCode !== 0 }),
    },
  };
}

export function pk01PublicExposureScript(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
  if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when rendering PK01 public exposure script`);
  const caddyfile = renderPk01Caddyfile(target, exposure);
  const serviceUnit = renderPk01CaddyService(exposure);
  const exposureMode = exposure.mode === "pk01-local" ? "pk01-caddy-local-docker" : "pk01-caddy-frp-direct";
  const dataPath = exposure.mode === "pk01-local"
    ? `client -> PK01 Caddy -> ${publicExposureUpstream(target, exposure)} -> PK01 Docker Sub2API`
    : `client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API`;
  const frpSummary = exposure.mode === "frp" && exposure.frpc !== null
    ? {
        server: `${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}`,
        remotePort: exposure.frpc.remotePort,
        proxyName: exposure.frpc.proxyName,
      }
    : null;
  const frpSummaryPython = frpSummary === null ? "None" : `json.loads(${JSON.stringify(JSON.stringify(frpSummary))})`;
  const caddyfileB64 = Buffer.from(caddyfile, "utf8").toString("base64");
  const serviceUnitB64 = Buffer.from(serviceUnit, "utf8").toString("base64");
  const caddyDownloadProxyArgs = exposure.pk01.caddyDownloadProxyUrl === null ? "" : `--proxy ${shQuote(exposure.pk01.caddyDownloadProxyUrl)}`;
  const caddyDownloadProxyUrl = exposure.pk01.caddyDownloadProxyUrl ?? "";
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
caddyfile="$tmp/Caddyfile"
service_unit="$tmp/${exposure.pk01.caddyServiceName}.service"
printf '%s' '${caddyfileB64}' | base64 -d >"$caddyfile"
printf '%s' '${serviceUnitB64}' | base64 -d >"$service_unit"
download_out="$tmp/download.out"
download_err="$tmp/download.err"
install_out="$tmp/install.out"
install_err="$tmp/install.err"
validate_out="$tmp/validate.out"
validate_err="$tmp/validate.err"
pikanode_out="$tmp/pikanode.out"
pikanode_err="$tmp/pikanode.err"
caddy_out="$tmp/caddy.out"
caddy_err="$tmp/caddy.err"
probe_out="$tmp/probe.out"
probe_err="$tmp/probe.err"
download_action="kept-existing"
download_proxy_url=${shQuote(caddyDownloadProxyUrl)}
download_proxy_mode="direct"
if [ -n "$download_proxy_url" ]; then download_proxy_mode="curl-proxy"; fi
download_rc=0
if ! [ -x ${shQuote(exposure.pk01.caddyBinaryPath)} ]; then
  cache_dir="$HOME/.cache/unidesk/platform-infra"
  cache_path="$cache_dir/caddy-linux-amd64"
  cache_part="$cache_path.part"
  mkdir -p "$cache_dir"
  if [ -x "$cache_path" ]; then
    download_action="used-cache"
    "$cache_path" version >"$download_out" 2>"$download_err" || true
  else
    download_action="downloaded"
    resume_args=
    if [ -s "$cache_part" ]; then resume_args="-C -"; fi
    curl -fL --connect-timeout 20 --retry 5 --retry-delay 3 --max-time 600 ${caddyDownloadProxyArgs} $resume_args ${shQuote(exposure.pk01.caddyDownloadUrl)} -o "$cache_part" >"$download_out" 2>"$download_err"
    download_rc=$?
    if [ "$download_rc" -eq 33 ]; then
      rm -f "$cache_part"
      curl -fL --connect-timeout 20 --retry 5 --retry-delay 3 --max-time 600 ${caddyDownloadProxyArgs} ${shQuote(exposure.pk01.caddyDownloadUrl)} -o "$cache_part" >>"$download_out" 2>>"$download_err"
      download_rc=$?
    fi
    if [ "$download_rc" -eq 0 ]; then
      mv "$cache_part" "$cache_path"
      chmod 0755 "$cache_path"
    fi
  fi
  if [ "$download_rc" -eq 0 ]; then
    sudo install -m 0755 "$cache_path" ${shQuote(exposure.pk01.caddyBinaryPath)} >>"$download_out" 2>>"$download_err"
    download_rc=$?
  fi
else
  rm -f "$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64.part"
  ${shQuote(exposure.pk01.caddyBinaryPath)} version >"$download_out" 2>"$download_err" || true
fi
install_rc=1
if [ "$download_rc" -eq 0 ]; then
  sudo mkdir -p "$(dirname ${shQuote(exposure.pk01.caddyConfigPath)})" ${shQuote(exposure.pk01.caddyStorageDir)} /etc/systemd/system
  merged_caddyfile="$tmp/Caddyfile.merged"
  sudo python3 - "$caddyfile" ${shQuote(exposure.pk01.caddyConfigPath)} "$merged_caddyfile" ${shQuote(exposure.dns.hostname)} >"$install_out" 2>"$install_err" <<'PY'
${pk01CaddyMergeManagedBlocksPython()}
PY
  install_rc=$?
  if [ "$install_rc" -eq 0 ]; then
    sudo install -m 0644 "$merged_caddyfile" ${shQuote(exposure.pk01.caddyConfigPath)} >>"$install_out" 2>>"$install_err"
    install_rc=$?
  fi
  if [ "$install_rc" -eq 0 ]; then
    sudo install -m 0644 "$service_unit" /etc/systemd/system/${exposure.pk01.caddyServiceName}.service >>"$install_out" 2>>"$install_err"
    install_rc=$?
  fi
fi
validate_rc=1
if [ "$install_rc" -eq 0 ]; then
  sudo ${shQuote(exposure.pk01.caddyBinaryPath)} validate --config ${shQuote(exposure.pk01.caddyConfigPath)} >"$validate_out" 2>"$validate_err"
  validate_rc=$?
else
  : >"$validate_out"
  : >"$validate_err"
fi
pikanode_rc=1
if [ "$validate_rc" -eq 0 ]; then
  if docker inspect ${shQuote(exposure.pk01.pikanodeContainerName)} >/dev/null 2>&1; then
    docker rm -f ${shQuote(exposure.pk01.pikanodeContainerName)} >"$pikanode_out" 2>"$pikanode_err"
    pikanode_rc=$?
  else
    : >"$pikanode_out"
    : >"$pikanode_err"
    pikanode_rc=0
  fi
  if [ "$pikanode_rc" -eq 0 ]; then
    docker run -d --name ${shQuote(exposure.pk01.pikanodeContainerName)} \\
      --restart=always \\
      -p 127.0.0.1:${exposure.pk01.pikanodeHttpHostPort}:8888 \\
      -p 22000-22099:22000-22099 \\
      -p 7500:7500 \\
      -v ${shQuote(exposure.pk01.pikanodeRoot)}:/usr/src/app \\
      -v /etc/letsencrypt:/etc/letsencrypt:ro \\
      -w /usr/src/app \\
      ${shQuote(exposure.pk01.pikanodeImage)} \\
      sh init.sh >>"$pikanode_out" 2>>"$pikanode_err"
    pikanode_rc=$?
    if [ "$pikanode_rc" -ne 0 ]; then
      printf '%s\\n' '[rollback] restarting pikanode with previous public 80/443 mapping' >>"$pikanode_err"
      docker rm -f ${shQuote(exposure.pk01.pikanodeContainerName)} >>"$pikanode_out" 2>>"$pikanode_err" || true
      docker run -d --name ${shQuote(exposure.pk01.pikanodeContainerName)} \\
        --restart=always \\
        -p 80:8888 \\
        -p 22000-22099:22000-22099 \\
        -p 443:443 \\
        -p 7500:7500 \\
        -v ${shQuote(exposure.pk01.pikanodeRoot)}:/usr/src/app \\
        -v /etc/letsencrypt:/etc/letsencrypt:ro \\
        -w /usr/src/app \\
        ${shQuote(exposure.pk01.pikanodeImage)} \\
        sh init.sh >>"$pikanode_out" 2>>"$pikanode_err" || true
    fi
  fi
else
  : >"$pikanode_out"
  : >"$pikanode_err"
fi
caddy_rc=1
if [ "$pikanode_rc" -eq 0 ]; then
  sudo systemctl daemon-reload >"$caddy_out" 2>"$caddy_err"
  sudo systemctl enable --now ${shQuote(exposure.pk01.caddyServiceName)} >>"$caddy_out" 2>>"$caddy_err"
  caddy_rc=$?
  if [ "$caddy_rc" -eq 0 ]; then
    sudo systemctl reload ${shQuote(exposure.pk01.caddyServiceName)} >>"$caddy_out" 2>>"$caddy_err" || sudo systemctl restart ${shQuote(exposure.pk01.caddyServiceName)} >>"$caddy_out" 2>>"$caddy_err"
    caddy_rc=$?
  fi
fi
probe_rc=1
if [ "$caddy_rc" -eq 0 ]; then
  {
    printf '[pikanode]\\n'
    curl -kfsS --max-time 10 --resolve pikapython.com:443:127.0.0.1 https://pikapython.com/ -o /dev/null -w 'status=%{http_code}\\n'
    printf '[api-health]\\n'
    curl -kfsS --max-time 10 --resolve ${exposure.dns.hostname}:443:127.0.0.1 ${exposure.publicBaseUrl}/health -o /dev/null -w 'status=%{http_code}\\n' || true
  } >"$probe_out" 2>"$probe_err"
  probe_rc=0
else
  : >"$probe_out"
  : >"$probe_err"
fi
python3 - "$download_rc" "$install_rc" "$validate_rc" "$pikanode_rc" "$caddy_rc" "$probe_rc" "$download_action" "$download_proxy_mode" "$download_proxy_url" "$download_out" "$download_err" "$install_out" "$install_err" "$validate_out" "$validate_err" "$pikanode_out" "$pikanode_err" "$caddy_out" "$caddy_err" "$probe_out" "$probe_err" <<'PY'
import json
import sys

download_rc, install_rc, validate_rc, pikanode_rc, caddy_rc, probe_rc = [int(value) for value in sys.argv[1:7]]
download_action = sys.argv[7]
download_proxy_mode = sys.argv[8]
download_proxy_url = sys.argv[9] or None
paths = sys.argv[10:]

def text(path, limit=4000):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-limit:]
    except FileNotFoundError:
        return ""

payload = {
    "ok": download_rc == 0 and install_rc == 0 and validate_rc == 0 and pikanode_rc == 0 and caddy_rc == 0,
    "target": "${target.id}",
    "mode": ${JSON.stringify(exposureMode)},
    "publicBaseUrl": "${exposure.publicBaseUrl}",
    "hostname": "${exposure.dns.hostname}",
    "expectedA": "${exposure.dns.expectedA}",
    "dataPath": ${JSON.stringify(dataPath)},
    "pikanodeRole": "pikapython.com upstream only; ${exposure.dns.hostname} does not pass through pikanode Express",
    "caddy": {
        "binaryPath": "${exposure.pk01.caddyBinaryPath}",
        "configPath": "${exposure.pk01.caddyConfigPath}",
        "serviceName": "${exposure.pk01.caddyServiceName}",
        "downloadProxy": {"mode": download_proxy_mode, "url": download_proxy_url or None},
    },
    "frp": ${frpSummaryPython},
    "steps": {
        "downloadCaddy": {"exitCode": download_rc, "action": download_action, "stdout": text(paths[0]), "stderr": text(paths[1])},
        "installConfig": {"exitCode": install_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
        "validateCaddyConfig": {"exitCode": validate_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
        "restartPikanodeLoopback": {"exitCode": pikanode_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
        "startCaddy": {"exitCode": caddy_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
        "localProbe": {"exitCode": probe_rc, "stdout": text(paths[10]), "stderr": text(paths[11])},
    },
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
