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

// Moved mechanically from scripts/src/platform-infra.ts:1588-1852 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 { Sub2ApiAccountLocalProxyConfig, Sub2ApiEgressProxyConfig, Sub2ApiPublicExposureConfig } from "./entry";
import type { ApplyOptions, DisclosureOptions, TargetOptions } from "./options";
import { applyScript, dryRunScript } from "./apply-script";
import { readSub2ApiConfig } from "./config";
import { configPath, fieldManager, manifestPath, serviceName } from "./entry";
import { hostDockerApplyScript, hostDockerDryRunScript, hostDockerPlanSummary, hostDockerStatusScript } from "./host-docker";
import { imageRef, isExternalTarget, isHostDockerTarget, managedResourceCleanupPlan, manifest, resolveTarget, targetDatabase, targetDependencyImages, targetHasSentinel, targetImage } from "./manifest";
import { applyPk01PublicExposure } from "./pk01-public-exposure";
import { policyChecks } from "./policy";
import { prepareAccountLocalProxySecret, prepareEgressProxySecret, prepareExternalActiveSecret, preparePublicExposureSecret } from "./secrets-and-egress";
import { statusScript } from "./status-script";
import { boolField } from "./utils";

export function publicExposureSummary(exposure: Sub2ApiPublicExposureConfig): Record<string, unknown> {
  return {
    enabled: exposure.enabled,
    mode: exposure.mode,
    publicBaseUrl: exposure.publicBaseUrl,
    dns: exposure.dns,
    local: exposure.local,
    frpc: exposure.frpc === null
      ? null
      : {
          deploymentName: exposure.frpc.deploymentName,
          secretName: exposure.frpc.secretName,
          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,
          valuesPrinted: false,
        },
    localHttps: exposure.localHttps === null
      ? null
      : {
          route: exposure.localHttps.route,
          mode: "node-local-docker-caddy",
          upstreamMode: exposure.localHttps.upstreamMode,
          composePath: exposure.localHttps.composePath,
          projectName: exposure.localHttps.projectName,
          serviceName: exposure.localHttps.serviceName,
          containerName: exposure.localHttps.containerName,
          image: exposure.localHttps.image,
          bind: `${exposure.localHttps.hostBind}:${exposure.localHttps.httpsPort}`,
          tlsMode: exposure.localHttps.tlsMode,
          valuesPrinted: false,
        },
    pk01: exposure.pk01 === null
      ? null
      : {
          route: exposure.pk01.route,
          mode: "caddy-edge",
          caddyBinaryPath: exposure.pk01.caddyBinaryPath,
          caddyDownloadProxyUrl: exposure.pk01.caddyDownloadProxyUrl,
          caddyConfigPath: exposure.pk01.caddyConfigPath,
          caddyServiceName: exposure.pk01.caddyServiceName,
          pikanodeHttpHostPort: exposure.pk01.pikanodeHttpHostPort,
        },
  };
}

export function egressProxySummary(proxy: Sub2ApiEgressProxyConfig): Record<string, unknown> {
  return {
    enabled: proxy.enabled,
    mode: proxy.sourceType === "master-shadowsocks" ? "master-shadowsocks-http-proxy" : "master-vpn-subscription-http-proxy",
    deploymentName: proxy.deploymentName,
    serviceName: proxy.serviceName,
    secretName: proxy.secretName,
    image: proxy.image,
    imagePullPolicy: proxy.imagePullPolicy,
    listenPort: proxy.listenPort,
    hostNetwork: proxy.hostNetwork,
    sourceConfigRef: proxy.sourceConfigRef,
    sourceFingerprint: proxy.sourceFingerprint,
    sourceRef: proxy.sourceRef,
    sourceType: proxy.sourceType,
    preferredOutbound: proxy.preferredOutbound,
    masterShadowsocks: proxy.masterShadowsocks === null
      ? null
      : {
          serverHost: proxy.masterShadowsocks.serverHost,
          serverPort: proxy.masterShadowsocks.serverPort,
          method: proxy.masterShadowsocks.method,
        },
    applyToSub2Api: proxy.applyToSub2Api,
    applyToSentinel: proxy.applyToSentinel,
    healthProbeUrl: proxy.healthProbeUrl,
    noProxy: proxy.noProxy,
    valuesPrinted: false,
  };
}

export function accountLocalProxySummary(proxy: Sub2ApiAccountLocalProxyConfig): Record<string, unknown> {
  return {
    enabled: proxy.enabled,
    mode: "sub2api-pod-loopback-account-proxy",
    containerName: proxy.containerName,
    secretName: proxy.secretName,
    image: proxy.image,
    imagePullPolicy: proxy.imagePullPolicy,
    listen: `${proxy.listenHost}:${proxy.listenPort}`,
    sourceConfigRef: proxy.sourceConfigRef,
    sourceFingerprint: proxy.sourceFingerprint,
    sourceRef: proxy.sourceRef,
    sourceType: proxy.sourceType,
    healthProbeUrl: proxy.healthProbeUrl,
    valuesPrinted: false,
  };
}

export function plan(options: TargetOptions): Record<string, unknown> {
  const sub2api = readSub2ApiConfig();
  const target = resolveTarget(sub2api, options.targetId);
  const yaml = manifest(sub2api, target);
  const policy = policyChecks(sub2api, yaml, target);
  const database = targetDatabase(sub2api, target);
  const hostDocker = isHostDockerTarget(target) ? hostDockerPlanSummary(sub2api, target) : null;
  return {
    ok: policy.every((check) => check.ok),
    action: "platform-infra-sub2api-plan",
    target: {
      id: target.id,
      route: target.route,
      namespace: target.namespace,
      runtimeMode: target.runtimeMode,
      role: target.role,
      manifestPath,
      configPath,
      fieldManager,
      serviceDns: isHostDockerTarget(target) ? `host-docker:127.0.0.1:${target.hostDocker?.appPort ?? 0}` : `${serviceName}.${target.namespace}.svc.cluster.local:8080`,
    },
    config: {
      path: configPath,
      version: sub2api.version,
      kind: sub2api.kind,
      metadata: sub2api.metadata,
      image: imageRef(sub2api, target),
      pullPolicy: targetImage(sub2api, target).pullPolicy,
      dependencyImages: targetDependencyImages(sub2api, target),
      security: sub2api.security,
      target: {
        runtimeMode: target.runtimeMode,
        runtime: target.runtime,
        databaseMode: target.databaseMode,
        redisMode: target.redisMode,
        appReplicas: target.appReplicas,
        redisReplicas: target.redisReplicas,
        hostDocker,
        publicExposure: target.publicExposure === null ? null : publicExposureSummary(target.publicExposure),
        egressProxy: target.egressProxy === null ? null : egressProxySummary(target.egressProxy),
        accountLocalProxy: target.accountLocalProxy === null ? null : accountLocalProxySummary(target.accountLocalProxy),
      },
      externalDatabase: isExternalTarget(target) ? {
        status: target.databaseMode === "external-active" ? "external-db-active" : "pending-external-db",
        sourceRef: sub2api.runtime.database.sourceRef,
        sourceKeys: sub2api.runtime.database.sourceKeys,
        secretName: sub2api.runtime.database.secretName,
        passwordKey: sub2api.runtime.database.passwordKey,
        host: database.host,
        port: database.port,
        user: database.user,
        dbName: database.dbName,
        sslMode: database.sslMode,
        pendingAllowed: database.pendingAllowed,
      } : null,
    },
    decision: {
      owner: "UniDesk",
      namespace: target.namespace,
      reason: target.databaseMode === "external-pending"
        ? `${target.id} is a standby Sub2API platform-infra target prepared through YAML; it stays scaled to zero until this target is promoted in YAML.`
        : isHostDockerTarget(target)
          ? `${target.id} is activated as a PK01 host-Docker Sub2API target against PK01 local PostgreSQL without k3s or sentinel.`
        : target.databaseMode === "external-active"
          ? `${target.id} is activated as a platform-infra Sub2API target against the external PK01 PostgreSQL runtime while keeping app state outside the k3s node.`
          : `${target.id} is a bundled active Sub2API platform-infra target.`,
      exposure: target.publicExposure?.enabled
        ? isHostDockerTarget(target)
          ? `Public HTTPS ${target.publicExposure.publicBaseUrl} through PK01 Caddy to PK01 local Docker; no D601 egress, no k3s Ingress/NodePort/LoadBalancer.`
          : target.publicExposure.mode === "node-local-https"
            ? `Node-local HTTPS ${target.publicExposure.publicBaseUrl} through ${target.id} Docker Caddy to the ClusterIP Sub2API Service; no PK01 Caddy/FRP and no Kubernetes Ingress/NodePort/LoadBalancer.`
            : `Public HTTPS ${target.publicExposure.publicBaseUrl} through PK01 Caddy and ${target.id} frpc; no master server forwarding and no Kubernetes Ingress/NodePort/LoadBalancer.`
        : "ClusterIP only; no public ingress or node-level exposure.",
      resourcePolicy: isHostDockerTarget(target)
        ? "PK01 host-Docker compose is controlled by YAML; no Kubernetes resources are rendered."
        : target.resourceProtection === null
          ? "No explicit Kubernetes resource protection is declared for this target."
          : `PriorityClass ${target.resourceProtection.priorityClassName} and app/Redis requests and limits are controlled by config/platform-infra/sub2api.yaml.`,
      imageVersionControl: "Sub2API image repository/tag/pullPolicy are controlled by config/platform-infra/sub2api.yaml in the UniDesk repository.",
      urlAllowlistControl: "Sub2API upstream URL validation options are controlled by config/platform-infra/sub2api.yaml and rendered to SECURITY_URL_ALLOWLIST_* env vars.",
      networkPolicy: isHostDockerTarget(target) ? "Not applicable for PK01 host-Docker deployment." : "NetworkPolicy/allow-all is rendered with the deployment so kube-router cannot silently default-deny Sub2API cross-pod traffic.",
      publicExposure: target.publicExposure?.enabled
        ? {
            mode: target.publicExposure.mode === "pk01-local"
              ? "pk01-caddy-local-docker"
              : target.publicExposure.mode === "node-local-https"
                ? "node-local-docker-caddy"
                : "pk01-caddy-frp-direct",
            dataPath: target.publicExposure.mode === "pk01-local"
              ? `client -> PK01 Caddy -> ${target.publicExposure.local?.upstreamHost}:${target.publicExposure.local?.upstreamPort} -> PK01 Docker Sub2API`
              : target.publicExposure.mode === "node-local-https"
                ? `client -> ${target.id} local HTTPS port ${target.publicExposure.localHttps?.httpsPort} -> ${target.id} Docker Caddy -> ${serviceName}.${target.namespace}.svc.cluster.local:8080`
                : `client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API`,
            pikanodeRole: target.publicExposure.mode === "node-local-https"
              ? "not involved; this target does not touch PK01 pikanode"
              : `pikapython.com upstream only; ${target.publicExposure.dns.hostname} does not pass through pikanode Express`,
            publicBaseUrl: target.publicExposure.publicBaseUrl,
            hostname: target.publicExposure.dns.hostname,
            expectedA: target.publicExposure.dns.expectedA,
          }
        : null,
      egressProxy: target.egressProxy?.enabled
        ? {
            mode: `${target.id} in-cluster HTTP proxy client to ${target.egressProxy.sourceType}`,
            service: `${target.egressProxy.serviceName}.${target.namespace}.svc.cluster.local:${target.egressProxy.listenPort}`,
            hostNetwork: target.egressProxy.hostNetwork,
            sourceRef: target.egressProxy.sourceRef,
            sourceType: target.egressProxy.sourceType,
            applyToSub2Api: target.egressProxy.applyToSub2Api,
            applyToSentinel: target.egressProxy.applyToSentinel,
            valuesPrinted: false,
          }
        : null,
      accountLocalProxy: target.accountLocalProxy?.enabled
        ? {
            mode: `${target.id} Sub2API pod loopback proxy for manually configured account proxy URLs`,
            listen: `${target.accountLocalProxy.listenHost}:${target.accountLocalProxy.listenPort}`,
            sourceRef: target.accountLocalProxy.sourceRef,
            sourceType: target.accountLocalProxy.sourceType,
            valuesPrinted: false,
          }
        : null,
      dataStores: isExternalTarget(target)
        ? [
            isHostDockerTarget(target) ? `PK01 local PostgreSQL through ${database.host}:${database.port}` : target.databaseMode === "external-active" ? "External PostgreSQL active from platform-db/PK01" : "External PostgreSQL pending from platform-db/Pika01",
            target.redisReplicas === 0 ? `${target.id} local Redis 8 ephemeral cache, scaled to zero until activation` : `${target.id} local Redis 8 ephemeral cache`,
          ]
        : ["PostgreSQL 18", "Redis 8"],
      appPoolCaps: {
        databaseMaxOpenConns: 10,
        databaseMaxIdleConns: 2,
        redisPoolSize: 32,
        redisMinIdleConns: 2,
      },
      standbyActivation: target.databaseMode === "external-pending"
        ? {
            target: target.id,
            currentMode: "predeployed-standby",
            enableByYaml: `change target ${target.id} databaseMode to external-active and set appReplicas/redisReplicas to 1, then apply --target ${target.id} --confirm`,
            sharedExternalDb: `${sub2api.runtime.database.host}:${sub2api.runtime.database.port}/${sub2api.runtime.database.dbName}`,
            sentinelEnabled: targetHasSentinel(sub2api, target),
          }
        : null,
      cleanup: managedResourceCleanupPlan(sub2api, target),
    },
    policy,
    next: {
      dryRun: `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --dry-run`,
      apply: target.databaseMode === "external-pending" && target.appReplicas === 0
        ? `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm # predeploy only; app replicas=0 until DB is ready`
        : `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm`,
      status: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`,
      validate: `bun scripts/cli.ts platform-infra sub2api validate --target ${target.id}`,
    },
  };
}

export async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
  const sub2api = readSub2ApiConfig();
  const target = resolveTarget(sub2api, options.targetId);
  const yaml = manifest(sub2api, target);
  const policy = policyChecks(sub2api, yaml, target);
  if (!policy.every((check) => check.ok)) {
    return {
      ok: false,
      action: "platform-infra-sub2api-apply",
      mode: "policy-blocked",
      target: target.id,
      policy,
    };
  }
  if (options.confirm && !options.wait) {
    const job = startJob(
      `platform_infra_sub2api_apply_${target.id.toLowerCase()}`,
      ["bun", "scripts/cli.ts", "platform-infra", "sub2api", "apply", "--target", target.id, "--confirm", "--wait"],
      `Apply ${target.id} ${isHostDockerTarget(target) ? "host-Docker" : "k3s"} platform-infra Sub2API desired state through the controlled UniDesk CLI`,
    );
    return {
      ok: true,
      action: "platform-infra-sub2api-apply",
      mode: "async-job",
      target: {
        id: target.id,
        route: target.route,
        namespace: target.namespace,
      },
      job,
      statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
      next: {
        status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
        rollout: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`,
        validate: `bun scripts/cli.ts platform-infra sub2api validate --target ${target.id}`,
      },
    };
  }
  if (options.dryRun) {
    const accountLocalProxySecretMaterial = prepareAccountLocalProxySecret(sub2api, target);
    const result = await capture(
      config,
      target.route,
      ["sh"],
      isHostDockerTarget(target)
        ? hostDockerDryRunScript(sub2api, target)
        : dryRunScript(yaml, target, accountLocalProxySecretMaterial),
    );
    const parsed = parseJsonOutput(result.stdout);
    return {
      ok: result.exitCode === 0 && boolField(parsed, "ok", false),
      action: "platform-infra-sub2api-apply",
      mode: "dry-run",
      target: {
        id: target.id,
        route: target.route,
        namespace: target.namespace,
      },
      policy,
      remote: parsed ?? compactCapture(result, { full: true }),
    };
  }
  const secretMaterial = target.databaseMode === "external-active" ? prepareExternalActiveSecret(sub2api) : null;
  const publicExposureSecretMaterial = preparePublicExposureSecret(sub2api, target);
  const egressProxySecretMaterial = prepareEgressProxySecret(sub2api, target);
  const accountLocalProxySecretMaterial = prepareAccountLocalProxySecret(sub2api, target);
  const result = await capture(
    config,
    target.route,
    ["sh"],
    isHostDockerTarget(target)
      ? hostDockerApplyScript(sub2api, target, secretMaterial)
      : applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial, accountLocalProxySecretMaterial),
  );
  const parsed = parseJsonOutput(result.stdout);
  const pk01Exposure = target.publicExposure === null || !target.publicExposure.enabled || target.publicExposure.mode === "node-local-https"
    ? null
    : await applyPk01PublicExposure(config, target);
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false) && (pk01Exposure === null || pk01Exposure.ok === true),
    action: "platform-infra-sub2api-apply",
    mode: "confirmed",
    target: {
      id: target.id,
      route: target.route,
      namespace: target.namespace,
    },
    policy,
    remote: parsed ?? compactCapture(result, { full: true }),
    pk01Exposure,
    next: {
      status: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`,
      validate: `bun scripts/cli.ts platform-infra sub2api validate --target ${target.id}`,
    },
  };
}

export async function status(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
  const sub2api = readSub2ApiConfig();
  const target = resolveTarget(sub2api, options.targetId);
  const result = await capture(config, target.route, ["sh"], isHostDockerTarget(target) ? hostDockerStatusScript(sub2api, target) : statusScript(sub2api, target));
  const parsed = parseJsonOutput(result.stdout);
  if (options.raw) {
    return {
      ok: result.exitCode === 0 && boolField(parsed, "ok", false),
      action: "platform-infra-sub2api-status",
      target: {
        id: target.id,
        route: target.route,
        namespace: target.namespace,
      },
      remote: compactCapture(result, { full: true }),
      parsed,
    };
  }
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false),
    action: "platform-infra-sub2api-status",
    target: {
      id: target.id,
      route: target.route,
      namespace: target.namespace,
    },
    summary: parsed,
    remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
  };
}
