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

// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:3097-3421 for #903.

import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import type { RenderedCliResult } from "../output";
import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service";
import { shortSha256Fingerprint } from "../platform-infra-ops-library";
import {
  codexPoolSentinelSummary,
  codexPoolSentinelRuntimeImage,
  readCodexPoolSentinelConfig,
  renderCodexPoolSentinelManifest,
  type CodexPoolSentinelConfig,
  type CodexPoolSentinelProfileSecret,
} from "../platform-infra-sub2api-codex-sentinel";
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";

import type { CodexPoolConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProxyBinding, CodexPoolManualBindingSource, CodexPoolRuntimeTarget, CodexProfile } from "./types";
import { desiredAccountCapacityTotal } from "./accounts";
import { readCodexPoolConfig } from "./config";
import { codexConsumerBaseUrl } from "./local-codex";
import { codexPoolRuntimeTarget } from "./runtime-target";
import { codexPoolConfigPath, fieldManager, serviceName, sub2apiConfigPath } from "./types";

export function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRuntimeTarget()): Record<string, unknown> {
  return {
    id: target.id,
    route: target.route,
    namespace: target.namespace,
    service: serviceName,
    serviceDns: target.serviceDns,
    publicBaseUrl: target.publicBaseUrl,
    consumerBaseUrl: target.publicBaseUrl === null ? null : codexConsumerBaseUrl(pool, target),
    configPath: codexPoolConfigPath,
    groupName: pool.groupName,
    apiKeyName: pool.apiKeyName,
    apiKeySecret: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
    publicExposure: targetPublicExposureSummary(target),
    sentinelImageBuild: {
      source: `${sub2apiConfigPath}.targets[${target.id}].codexPool.sentinelImageBuild`,
      baseImageCachePolicy: target.sentinelImageBuild.baseImageCachePolicy,
      noProxy: target.sentinelImageBuild.noProxy,
    },
    minOwnerConcurrency: pool.minOwnerConcurrency,
    minOwnerConcurrencySource: pool.minOwnerConcurrencySource,
    accountCapacityTotal: desiredAccountCapacityTotal(pool),
    defaultAccountPriority: pool.defaultAccountPriority,
    defaultAccountCapacity: pool.defaultAccountCapacity,
    defaultAccountLoadFactor: pool.defaultAccountLoadFactor,
    defaultSentinelProtect: pool.defaultSentinelProtect,
    sentinel: {
      monitorEnabled: pool.sentinel.monitor.enabled,
      actionsEnabled: pool.sentinel.actions.enabled,
      cronJobName: pool.sentinel.cronJobName,
      stateConfigMapName: pool.sentinel.stateConfigMapName,
    },
    egressProxy: target.egressProxy === null ? null : {
      enabled: target.egressProxy.enabled,
      applyToSentinel: target.egressProxy.applyToSentinel,
      serviceName: target.egressProxy.serviceName,
      listenPort: target.egressProxy.listenPort,
      httpProxy: target.egressProxy.httpProxy,
      noProxy: target.egressProxy.noProxy,
    },
    valuesPrinted: false,
  };
}

export function sentinelProfileSecrets(profiles: CodexProfile[]): CodexPoolSentinelProfileSecret[] {
  return profiles
    .filter((profile) => profile.ok && profile.apiKey !== null && profile.apiKey.length > 0)
    .map((profile) => ({
      accountName: profile.accountName,
      profile: profile.profile,
      baseUrl: profile.baseUrl,
      apiKey: profile.apiKey ?? "",
      upstreamUserAgent: profile.upstreamUserAgent,
      trustUpstream: profile.trustUpstream,
      sentinelProtect: profile.sentinelProtect,
    }));
}

export function resolvedManualAccountProtections(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown>[] {
  return pool.manualAccounts.protected.map((account) => ({
    accountName: account.accountName,
    reason: account.reason,
    proxyBinding: resolveManualProxyBinding(account.proxyBinding, pool, target),
    groupBinding: resolveManualGroupBinding(account.groupBinding, pool),
  }));
}

export function resolveManualProxyBinding(binding: CodexPoolManualAccountProxyBinding | null, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown> | null {
  if (binding === null) return null;
  const source = manualBindingSource(pool, binding.source);
  if (source.kind !== "proxy") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=proxy`);
  if (binding.enabled && source.provider === "target-egress-proxy" && (target.egressProxy === null || target.egressProxy.enabled !== true)) {
    throw new Error(`${codexPoolConfigPath}.manualAccounts.bindingSources.${source.id} requires ${sub2apiConfigPath}.targets[${target.id}].egressProxy.enabled=true`);
  }
  return {
    enabled: binding.enabled,
    source: source.id,
    proxyName: binding.proxyName,
    sourcePlan: {
      ...manualBindingSourcePlan(source),
      targetEgressProxy: source.provider === "target-egress-proxy" && target.egressProxy !== null
        ? {
          targetId: target.id,
          namespace: target.namespace,
          serviceName: target.egressProxy.serviceName,
          listenPort: target.egressProxy.listenPort,
          httpProxy: target.egressProxy.httpProxy,
          noProxy: target.egressProxy.noProxy,
        }
        : null,
    },
  };
}

export function resolveManualGroupBinding(binding: CodexPoolManualAccountGroupBinding | null, pool: CodexPoolConfig): Record<string, unknown> | null {
  if (binding === null) return null;
  const source = manualBindingSource(pool, binding.source);
  if (source.kind !== "group") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=group`);
  return {
    enabled: binding.enabled,
    source: source.id,
    sourcePlan: {
      ...manualBindingSourcePlan(source),
      poolGroupName: source.provider === "pool-group" ? pool.groupName : null,
    },
  };
}

export function manualBindingSource(pool: CodexPoolConfig, sourceId: string): CodexPoolManualBindingSource {
  const source = pool.manualAccounts.bindingSources.byId[sourceId];
  if (source === undefined) throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${sourceId} is not declared`);
  return source;
}

export function manualBindingSourcePlan(source: CodexPoolManualBindingSource): Record<string, unknown> {
  return {
    id: source.id,
    enabled: source.enabled,
    kind: source.kind,
    provider: source.provider,
    description: source.description,
  };
}

export function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Record<string, unknown> | null {
  const exposure = target.publicExposure;
  if (exposure === null) return null;
  return {
    source: `${sub2apiConfigPath}.targets[${target.id}].publicExposure`,
    enabled: exposure.enabled,
    target: {
      id: target.id,
      route: target.route,
      namespace: target.namespace,
      serviceDns: target.serviceDns,
    },
    urls: {
      publicBaseUrl: exposure.publicBaseUrl,
      consumerBaseUrl: target.publicBaseUrl === null ? null : `${target.publicBaseUrl.replace(/\/+$/u, "")}/`,
    },
    dns: exposure.dns,
    frpc: {
      deploymentName: exposure.frpc.deploymentName,
      secretName: exposure.frpc.secretName,
      secretKey: exposure.frpc.secretKey,
      image: exposure.frpc.image,
      serverAddr: exposure.frpc.serverAddr,
      serverPort: exposure.frpc.serverPort,
      proxyName: exposure.frpc.proxyName,
      remotePort: exposure.frpc.remotePort,
      localIP: exposure.frpc.localIP,
      localPort: exposure.frpc.localPort,
      tokenSourceRef: exposure.frpc.tokenSourceRef,
      tokenSourceKey: exposure.frpc.tokenSourceKey,
    },
    pk01: exposure.pk01,
    valuesPrinted: false,
  };
}

export function prepareTargetPublicExposureSecret(target: CodexPoolRuntimeTarget): ReturnType<typeof prepareFrpcSecret> {
  if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`);
  return prepareFrpcSecret({
    secretRoot: target.secretsRoot,
    exposure: target.publicExposure,
    sourcePathRedactor: redactRepoPath,
    parseEnvFile,
    requiredEnvValue,
    readTextFile: (sourcePath) => {
      if (!existsSync(sourcePath)) throw new Error(`publicExposure requires ${redactRepoPath(sourcePath)} with ${target.publicExposure?.frpc.tokenSourceKey}; materialize the PK01 frps token source first`);
      return readTextFile(sourcePath);
    },
  });
}

export function secretMaterialSummary(secret: ReturnType<typeof prepareFrpcSecret>): Record<string, unknown> {
  return {
    sourceRef: secret.sourceRef,
    sourcePath: secret.sourcePath,
    secretName: secret.secretName,
    secretKey: secret.secretKey,
    fingerprint: secret.fingerprint,
    valuesPrinted: false,
  };
}

export function targetPublicExposureApplyScript(target: CodexPoolRuntimeTarget, secret: ReturnType<typeof prepareFrpcSecret>): string {
  if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`);
  const manifest = renderFrpcManifest({
    id: target.id,
    route: target.route,
    namespace: target.namespace,
    replicas: 1,
    publicExposure: target.publicExposure,
  } satisfies PublicServiceTarget);
  const manifestB64 = Buffer.from(manifest, "utf8").toString("base64");
  const frpcTomlB64 = Buffer.from(secret.frpcToml, "utf8").toString("base64");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/sub2api-public-exposure.yaml"
frpc_toml="$tmp/frpc.toml"
printf '%s' '${manifestB64}' | base64 -d > "$manifest"
printf '%s' '${frpcTomlB64}' | base64 -d > "$frpc_toml"
ns_out="$tmp/ns.out"
ns_err="$tmp/ns.err"
secret_out="$tmp/secret.out"
secret_err="$tmp/secret.err"
apply_out="$tmp/apply.out"
apply_err="$tmp/apply.err"
rollout_out="$tmp/rollout.out"
rollout_err="$tmp/rollout.err"
pods_json="$tmp/pods.json"
pods_err="$tmp/pods.err"
logs_out="$tmp/logs.out"
logs_err="$tmp/logs.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_rc=1
apply_rc=1
rollout_rc=1
if [ "$ns_rc" -eq 0 ]; then
  kubectl -n ${target.namespace} create secret generic ${secret.secretName} \\
    --from-file=${secret.secretKey}="$frpc_toml" \\
    --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err"
  secret_rc=$?
else
  : >"$secret_out"
  printf '%s\\n' 'skipped because namespace apply failed' >"$secret_err"
fi
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ]; then
  kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err"
  apply_rc=$?
  if [ "$apply_rc" -eq 0 ]; then
    kubectl -n ${target.namespace} rollout status deployment/${target.publicExposure.frpc.deploymentName} --timeout=60s >"$rollout_out" 2>"$rollout_err"
    rollout_rc=$?
  else
    printf '%s\\n' 'skipped because apply failed' >"$rollout_err"
  fi
else
  : >"$apply_out"
  printf '%s\\n' 'skipped because namespace or secret step failed' >"$apply_err"
  : >"$rollout_out"
  printf '%s\\n' 'skipped because namespace or secret step failed' >"$rollout_err"
fi
kubectl -n ${target.namespace} get pods -l app.kubernetes.io/name=${target.publicExposure.frpc.deploymentName} -o json >"$pods_json" 2>"$pods_err"
pods_rc=$?
kubectl -n ${target.namespace} logs deployment/${target.publicExposure.frpc.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err"
logs_rc=$?
python3 - "$tmp" "$ns_rc" "$secret_rc" "$apply_rc" "$rollout_rc" "$pods_rc" "$logs_rc" <<'PY'
import json
import os
import sys

tmp = sys.argv[1]
ns_rc, secret_rc, apply_rc, rollout_rc, pods_rc, logs_rc = [int(value) for value in sys.argv[2:]]

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

def load_json(name):
    path = os.path.join(tmp, name)
    try:
        return json.load(open(path, encoding="utf-8"))
    except Exception:
        return None

pods_obj = load_json("pods.json")
pods = []
if isinstance(pods_obj, dict):
    for item in pods_obj.get("items") or []:
        status = item.get("status") or {}
        statuses = status.get("containerStatuses") or []
        pods.append({
            "name": item.get("metadata", {}).get("name"),
            "phase": status.get("phase"),
            "ready": all(cs.get("ready") is True for cs in statuses) if statuses else False,
            "restarts": sum(int(cs.get("restartCount") or 0) for cs in statuses),
        })

logs = text("logs.out", 4000)
payload = {
    "ok": ns_rc == 0 and secret_rc == 0 and apply_rc == 0 and rollout_rc == 0 and pods_rc == 0 and len(pods) > 0 and all(p["ready"] for p in pods),
    "target": "${target.id}",
    "namespace": "${target.namespace}",
    "source": "${sub2apiConfigPath}.targets[${target.id}].publicExposure",
    "proxy": {
        "name": "${target.publicExposure.frpc.proxyName}",
        "remotePort": ${JSON.stringify(target.publicExposure.frpc.remotePort)},
        "publicBaseUrl": "${target.publicExposure.publicBaseUrl}",
        "localIP": "${target.publicExposure.frpc.localIP}",
        "localPort": ${JSON.stringify(target.publicExposure.frpc.localPort)},
    },
    "resources": {
        "secret": "${secret.secretName}",
        "secretKey": "${secret.secretKey}",
        "deployment": "${target.publicExposure.frpc.deploymentName}",
        "image": "${target.publicExposure.frpc.image}",
    },
    "steps": {
        "namespace": {"exitCode": ns_rc, "stdout": text("ns.out"), "stderr": text("ns.err")},
        "secret": {"exitCode": secret_rc, "stdout": text("secret.out"), "stderr": text("secret.err"), "valuesPrinted": False},
        "apply": {"exitCode": apply_rc, "stdout": text("apply.out"), "stderr": text("apply.err")},
        "rollout": {"exitCode": rollout_rc, "stdout": text("rollout.out"), "stderr": text("rollout.err")},
        "pods": {"exitCode": pods_rc, "items": pods, "stderr": text("pods.err")},
        "logs": {
            "exitCode": logs_rc,
            "startProxySuccess": "start proxy success" in logs,
            "stdoutTail": logs,
            "stderrTail": text("logs.err"),
        },
    },
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
