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

// Moved mechanically from scripts/src/platform-infra.ts:1-337 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 { SubscriptionNode } from "./pk01-public-exposure";
import { sub2ApiHelpTargetSummary } from "./options";

export const serviceName = "sub2api";

export const fieldManager = "unidesk-platform-infra";

export const manifestPath = rootPath("src", "components", "platform-infra", "sub2api", "sub2api.k8s.yaml");

export const configPath = rootPath("config", "platform-infra", "sub2api.yaml");

export const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");

export const legacySub2apiCaddyManagedMarker = "sub2api";

export function sub2apiCaddyManagedMarker(target: Sub2ApiTargetConfig): string {
  return target.id.toUpperCase() === "D601" ? legacySub2apiCaddyManagedMarker : `${legacySub2apiCaddyManagedMarker}-${target.id.toLowerCase()}`;
}

export const requiredSecretKeys = ["POSTGRES_PASSWORD", "ADMIN_PASSWORD", "JWT_SECRET", "TOTP_ENCRYPTION_KEY"] as const;

export type DatabaseMode = "bundled" | "external-pending" | "external-active";

export type RedisMode = "bundled-persistent" | "local-ephemeral";

export interface Sub2ApiConfig {
  version: number;
  kind: string;
  metadata: {
    id: string;
    owner: string;
    relatedIssues: number[];
  };
  defaults: Sub2ApiDefaults;
  image: {
    repository: string;
    tag: string;
    pullPolicy: "Always" | "IfNotPresent" | "Never";
  };
  dependencyImages: {
    postgres: string;
    redis: string;
  };
  security: {
    urlAllowlist: {
      enabled: boolean;
      allowInsecureHttp: boolean;
      allowPrivateHosts: boolean;
      upstreamHosts: string[];
    };
  };
  targets: Sub2ApiTargetConfig[];
  runtime: {
    database: ExternalDatabaseConfig;
    secrets: RuntimeSecretsConfig;
    redis: RuntimeRedisConfig;
    appData: {
      mode: "persistent-pvc" | "empty-dir";
    };
    sentinel: {
      mode: "singleton";
      enabledOnTargets: string[];
    };
  };
}

export interface Sub2ApiDefaults {
  targetId: string;
  cleanup: {
    externalDbState: {
      postgresStatefulSetName: string;
      postgresServiceName: string;
      postgresPvcName: string;
      appDataPvcName: string;
    };
    redisPersistentState: {
      pvcName: string;
    };
    publicExposure: {
      deploymentName: string;
      configMapName: string;
      secretName: string;
    };
    egressProxy: {
      deploymentName: string;
      serviceName: string;
      secretName: string;
    };
  };
}

export interface Sub2ApiTargetConfig {
  id: string;
  route: string;
  namespace: string;
  role: string;
  enabled: boolean;
  databaseMode: DatabaseMode;
  redisMode: RedisMode;
  appReplicas: number;
  redisReplicas: number;
  image: Partial<Sub2ApiConfig["image"]>;
  dependencyImages: Partial<Sub2ApiConfig["dependencyImages"]>;
  publicExposure: Sub2ApiPublicExposureConfig | null;
  egressProxy: Sub2ApiEgressProxyConfig | null;
}

export interface Sub2ApiPublicExposureConfig {
  enabled: boolean;
  publicBaseUrl: string;
  dns: {
    hostname: string;
    expectedA: string;
    resolvers: string[];
  };
  frpc: {
    deploymentName: string;
    secretName: string;
    secretKey: string;
    image: string;
    serverAddr: string;
    serverPort: number;
    proxyName: string;
    remotePort: number;
    localIP: string;
    localPort: number;
    tokenSourceRef: string;
    tokenSourceKey: string;
  };
  pk01: {
    route: string;
    caddyBinaryPath: string;
    caddyDownloadUrl: string;
    caddyDownloadProxyUrl: string | null;
    caddyConfigPath: string;
    caddyServiceName: string;
    caddyStorageDir: string;
    caddyEmail: string;
    pikanodeRoot: string;
    pikanodeContainerName: string;
    pikanodeImage: string;
    pikanodeHttpHostPort: number;
    responseHeaderTimeoutSeconds: number;
  };
}

export interface Sub2ApiEgressProxyConfig {
  enabled: boolean;
  deploymentName: string;
  serviceName: string;
  secretName: string;
  secretKey: string;
  image: string;
  imagePullPolicy: "Always" | "IfNotPresent" | "Never";
  listenPort: number;
  hostNetwork: boolean;
  sourceConfigRef: string | null;
  sourceFingerprint: string | null;
  sourceRef: string;
  sourceKey: string;
  sourceType: "subscription-url" | "master-shadowsocks";
  preferredOutbound: "vless-reality" | "hysteria2" | null;
  masterShadowsocks: Sub2ApiMasterShadowsocksConfig | null;
  noProxy: string[];
  applyToSub2Api: boolean;
  applyToSentinel: boolean;
  healthProbeUrl: string;
}

export interface Sub2ApiMasterShadowsocksConfig {
  serverHost: string;
  serverPort: number;
  method: string;
}

export interface ExternalDatabaseConfig {
  mode: "external";
  sourceRef: string;
  sourceKeys: {
    user: string;
    password: string;
    dbName: string;
  };
  secretName: string;
  passwordKey: string;
  host: string;
  port: number;
  user: string;
  dbName: string;
  sslMode: string;
  pendingAllowed: boolean;
}

export interface RuntimeSecretsConfig {
  root: string;
  appSourceRef: string;
}

export interface RuntimeRedisConfig {
  serviceName: string;
  persistence: boolean;
}

export interface ExternalActiveSecretMaterial {
  sourceRef: string;
  sourcePath: string;
  appSourceRef: string;
  appSourcePath: string;
  action: "create" | "update" | "none";
  fingerprint: string;
  values: Record<typeof requiredSecretKeys[number], string>;
}

export interface PublicExposureSecretMaterial {
  sourceRef: string;
  sourcePath: string;
  secretName: string;
  secretKey: string;
  fingerprint: string;
  frpcToml: string;
  valuesPrinted: false;
}

export interface EgressProxySecretMaterial {
  sourceRef: string;
  sourcePath: string;
  secretName: string;
  secretKey: string;
  fingerprint: string;
  sourceBytes: number;
  selectedOutbound: string;
  sourceDiagnostics: EgressProxySubscriptionDiagnostics;
  configJson: string;
  proxyUrl: string;
  noProxy: string;
  valuesPrinted: false;
}

export interface EgressProxySubscriptionCandidateSummary {
  sourceLine: number;
  kind: SubscriptionNode["kind"] | "shadowsocks";
  fingerprint: string;
  paramKeys: string[];
  selected: boolean;
}

export interface EgressProxySubscriptionDiagnostics {
  ok: boolean;
  sourceType: Sub2ApiEgressProxyConfig["sourceType"];
  preferredOutbound: Sub2ApiEgressProxyConfig["preferredOutbound"];
  candidateCount: number;
  supportedCount: number;
  unsupportedCount: number;
  byKind: Record<string, number>;
  selectedOutbound: string | null;
  selectedFingerprint: string | null;
  candidates: EgressProxySubscriptionCandidateSummary[];
  error?: string;
  valuesPrinted: false;
}

export interface ManagedResourceCleanupPlan {
  externalDbState: {
    enabled: boolean;
    postgresStatefulSetName: string;
    postgresServiceName: string;
    postgresPvcName: string;
    appDataPvcName: string;
  };
  redisPersistentState: {
    enabled: boolean;
    pvcName: string;
  };
  publicExposure: {
    enabled: boolean;
    deploymentName: string;
    configMapName: string;
    secretName: string;
  };
  egressProxy: {
    enabled: boolean;
    deploymentName: string;
    serviceName: string;
    secretName: string;
  };
  sentinel: {
    enabled: boolean;
    cronJobName: string;
    configMapName: string;
    credentialsSecretName: string;
    stateConfigMapName: string;
    serviceAccountName: string;
    roleName: string;
    roleBindingName: string;
  };
}

export function platformInfraHelp(): unknown {
  const target = sub2ApiHelpTargetSummary();
  return {
    command: "platform-infra sub2api|langbot|n8n|wechat-archive|observability|secret-plane|kafka ...",
    output: "json",
    usage: [
      "bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]",
      "bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --dry-run",
      "bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --confirm",
      "bun scripts/cli.ts platform-infra sub2api status [--target G14|D601] [--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api validate [--target G14|D601] [--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool plan",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
      "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
      "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm",
      "bun scripts/cli.ts platform-infra egress-proxy traffic --target D601 --sample-seconds 15",
      "bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark --targets D601,D518 --profile no-mirror-600m --dry-run",
      "bun scripts/cli.ts platform-infra langbot plan [--target G14]",
      "bun scripts/cli.ts platform-infra langbot apply [--target G14] --confirm",
      "bun scripts/cli.ts platform-infra langbot status [--target G14] [--full|--raw]",
      "bun scripts/cli.ts platform-infra langbot logs [--target G14] [--component app|plugin-runtime|frpc|all]",
      "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm",
      "bun scripts/cli.ts platform-infra langbot query --path /api/v1/platform/bots",
      "bun scripts/cli.ts platform-infra n8n plan [--target G14]",
      "bun scripts/cli.ts platform-infra n8n apply [--target G14] --confirm",
      "bun scripts/cli.ts platform-infra n8n status [--target G14] [--full|--raw]",
      "bun scripts/cli.ts platform-infra n8n logs [--target G14] [--component app|frpc|all]",
      "bun scripts/cli.ts platform-infra n8n validate [--target G14] [--full|--raw]",
      "bun scripts/cli.ts platform-infra wechat-archive plan [--target G14]",
      "bun scripts/cli.ts platform-infra wechat-archive apply [--target G14] --confirm",
      "bun scripts/cli.ts platform-infra wechat-archive status [--target G14] [--full|--raw]",
      "bun scripts/cli.ts platform-infra wechat-archive validate [--target G14] [--full|--raw]",
      "bun scripts/cli.ts platform-infra wechat-archive pull --remote-path /UniDesk/WeChatArchive/...",
      "bun scripts/cli.ts platform-infra wechat-archive wcf-host-prepare --confirm",
      "bun scripts/cli.ts platform-infra wechat-archive wcf-host-start --confirm",
      "bun scripts/cli.ts platform-infra wechat-archive collector-image-build --confirm",
      "bun scripts/cli.ts platform-infra wechat-archive collector-apply --confirm",
      "bun scripts/cli.ts platform-infra wechat-archive collector-status --full",
      "bun scripts/cli.ts platform-infra observability plan --target D601",
      "bun scripts/cli.ts platform-infra observability apply --target D601 --dry-run",
      "bun scripts/cli.ts platform-infra observability apply --target D601 --confirm",
      "bun scripts/cli.ts platform-infra observability status --target D601",
      "bun scripts/cli.ts platform-infra observability validate --target D601",
      "bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id <traceId> [--grep text] [--limit 40] [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability search --target D601 --grep 'no rollout found' [--lookback-minutes 360] [--candidate-limit 80] [--limit 20]",
      "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D601 --business-trace-id <trc_...> [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D601 --run-id <run_...> [--command-id <cmd_...>] [--runner-job-id <rjob_...>]",
      "bun scripts/cli.ts platform-infra secret-plane plan --target D518",
      "bun scripts/cli.ts platform-infra secret-plane apply --target D518 --dry-run",
      "bun scripts/cli.ts platform-infra secret-plane apply --target D518 --confirm",
      "bun scripts/cli.ts platform-infra secret-plane status --target D518",
      "bun scripts/cli.ts platform-infra secret-plane validate --target D518",
      "bun scripts/cli.ts platform-infra kafka plan --target D518",
      "bun scripts/cli.ts platform-infra kafka apply --target D518 --dry-run",
      "bun scripts/cli.ts platform-infra kafka apply --target D518 --confirm",
      "bun scripts/cli.ts platform-infra kafka status --target D518",
      "bun scripts/cli.ts platform-infra kafka validate --target D518",
    ],
    description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows, OpenTelemetry tracing, the independent target-scoped secret plane, and the D518 Kafka event bus. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.",
    target,
    codexPool: {
      usage: [
        "bun scripts/cli.ts platform-infra sub2api codex-pool plan",
        "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
        "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
        "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>",
        "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
      ],
      module: "scripts/src/platform-infra-sub2api-codex.ts",
    },
  };
}
