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

// Moved mechanically from scripts/src/platform-infra.ts:1974-2225 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 { AccountLocalProxySecretMaterial, EgressProxySecretMaterial, EgressProxySubscriptionDiagnostics, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiAccountLocalProxyConfig, Sub2ApiConfig, Sub2ApiEgressProxyConfig, Sub2ApiTargetConfig } from "./entry";
import { secretRoot, writeEnvFile } from "./apply-script";
import { configPath, requiredSecretKeys } from "./entry";
import { analyzeEgressProxySubscription, renderSingBoxConfig, renderSingBoxMasterShadowsocksConfig } from "./pk01-public-exposure";

export function hasAllowAllNetworkPolicy(yaml: string, namespaceName: string): boolean {
  return yaml.split(/^---\s*$/mu).some((document) => {
    return /^\s*kind:\s*NetworkPolicy\s*$/mu.test(document)
      && /^\s*name:\s*allow-all\s*$/mu.test(document)
      && new RegExp(`^\\s*namespace:\\s*${escapeRegExp(namespaceName)}\\s*$`, "mu").test(document)
      && /^\s*podSelector:\s*\{\}\s*$/mu.test(document)
      && /^\s*-\s*Ingress\s*$/mu.test(document)
      && /^\s*-\s*Egress\s*$/mu.test(document)
      && /^\s*ingress:\s*\n\s*-\s*\{\}\s*$/mu.test(document)
      && /^\s*egress:\s*\n\s*-\s*\{\}\s*$/mu.test(document);
  });
}

export function hasDeploymentReplicas(yaml: string, name: string, replicas: number): boolean {
  return yaml.split(/^---\s*$/mu).some((document) => {
    return /^\s*kind:\s*Deployment\s*$/mu.test(document)
      && new RegExp(`^\\s*name:\\s*${escapeRegExp(name)}\\s*$`, "mu").test(document)
      && new RegExp(`^\\s*replicas:\\s*${replicas}\\s*$`, "mu").test(document);
  });
}

export function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}

export function prepareExternalActiveSecret(sub2api: Sub2ApiConfig): ExternalActiveSecretMaterial {
  const database = sub2api.runtime.database;
  const root = secretRoot(sub2api);
  const dbSource = readEnvSourceFile({
    root,
    sourceRef: database.sourceRef,
    missingMessage: (sourcePath) => `external-active requires ${redactRepoPath(sourcePath)}; run platform-db postgres apply/status first to materialize the PK01 DB credential source`,
  });
  const dbValues = dbSource.values;
  const dbUser = requiredEnvValue(dbValues, database.sourceKeys.user, database.sourceRef);
  const dbPassword = requiredEnvValue(dbValues, database.sourceKeys.password, database.sourceRef);
  const dbName = requiredEnvValue(dbValues, database.sourceKeys.dbName, database.sourceRef);
  if (dbUser !== database.user) throw new Error(`${database.sourceRef}.${database.sourceKeys.user} does not match ${configPath}.runtime.database.user`);
  if (dbName !== database.dbName) throw new Error(`${database.sourceRef}.${database.sourceKeys.dbName} does not match ${configPath}.runtime.database.dbName`);

  const appSourcePath = join(root, sub2api.runtime.secrets.appSourceRef);
  const existedBefore = existsSync(appSourcePath);
  const existing = existedBefore ? parseEnvFile(readFileSync(appSourcePath, "utf8")) : {};
  const next = { ...existing };
  next.POSTGRES_PASSWORD = dbPassword;
  if (next.ADMIN_EMAIL === undefined || next.ADMIN_EMAIL.length === 0) next.ADMIN_EMAIL = "admin@sub2api.platform-infra.local";
  if (next.ADMIN_PASSWORD === undefined || next.ADMIN_PASSWORD.length === 0) next.ADMIN_PASSWORD = randomBytes(16).toString("hex");
  if (next.JWT_SECRET === undefined || next.JWT_SECRET.length === 0) next.JWT_SECRET = randomBytes(32).toString("hex");
  if (next.TOTP_ENCRYPTION_KEY === undefined || next.TOTP_ENCRYPTION_KEY.length === 0) next.TOTP_ENCRYPTION_KEY = randomBytes(32).toString("hex");
  const values: Record<typeof requiredSecretKeys[number], string> = {
    POSTGRES_PASSWORD: next.POSTGRES_PASSWORD,
    ADMIN_PASSWORD: next.ADMIN_PASSWORD,
    JWT_SECRET: next.JWT_SECRET,
    TOTP_ENCRYPTION_KEY: next.TOTP_ENCRYPTION_KEY,
  };
  const missing = requiredSecretKeys.filter((key) => values[key].length === 0);
  if (missing.length > 0) throw new Error(`external-active app secret source is missing ${missing.join(", ")}`);
  const managedAppSourceKeys = [...requiredSecretKeys, "ADMIN_EMAIL"] as const;
  const action = !existedBefore
    ? "create"
    : managedAppSourceKeys.some((key) => existing[key] !== next[key])
      ? "update"
      : "none";
  if (action !== "none") writeEnvFile(appSourcePath, { ...existing, ...values, ADMIN_EMAIL: next.ADMIN_EMAIL });
  return {
    sourceRef: database.sourceRef,
    sourcePath: dbSource.sourcePathRedacted,
    appSourceRef: sub2api.runtime.secrets.appSourceRef,
    appSourcePath: redactRepoPath(appSourcePath),
    action,
    fingerprint: fingerprintSecretValues(values, [...requiredSecretKeys]),
    adminEmail: next.ADMIN_EMAIL,
    values,
  };
}

export function preparePublicExposureSecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): PublicExposureSecretMaterial | null {
  const exposure = target.publicExposure;
  if (exposure === null || !exposure.enabled) return null;
  if (exposure.mode === "pk01-local") return null;
  if (exposure.mode === "node-local-https") return null;
  if (exposure.frpc === null) throw new Error(`publicExposure.frpc is required for target ${target.id} when mode=frp`);
  return prepareFrpcSecret({
    secretRoot: secretRoot(sub2api),
    exposure,
    sourcePathRedactor: redactRepoPath,
    parseEnvFile,
    requiredEnvValue,
    readTextFile: (sourcePath) => {
      if (!existsSync(sourcePath)) throw new Error(`publicExposure requires ${redactRepoPath(sourcePath)} with ${exposure.frpc.tokenSourceKey}; copy the PK01 frps token into the local secret source first`);
      return readTextFile(sourcePath);
    },
  });
}

export function prepareEgressProxySecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): EgressProxySecretMaterial | null {
  const proxy = target.egressProxy;
  if (proxy === null || !proxy.enabled) return null;
  const source = readEnvSourceFile({
    root: secretRoot(sub2api),
    sourceRef: proxy.sourceRef,
    missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`,
  });
  const values = source.values;
  if (proxy.sourceType === "master-shadowsocks") {
    const password = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef);
    const configJson = renderSingBoxMasterShadowsocksConfig(proxy, password);
    const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
    const noProxy = proxy.noProxy.join(",");
    return {
      sourceRef: proxy.sourceRef,
      sourcePath: source.sourcePathRedacted,
      secretName: proxy.secretName,
      secretKey: proxy.secretKey,
      fingerprint: fingerprintSecretValues({ password, configJson }, ["password", "configJson"]),
      sourceBytes: Buffer.byteLength(password, "utf8"),
      selectedOutbound: "shadowsocks",
      sourceDiagnostics: masterShadowsocksDiagnostics(proxy),
      configJson,
      proxyUrl,
      noProxy,
      valuesPrinted: false,
    };
  }
  if (proxy.preferredOutbound === null) throw new Error(`egressProxy preferredOutbound is required for sourceType=${proxy.sourceType}`);
  const subscriptionUrl = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef);
  const subscription = fetchSubscription(subscriptionUrl);
  const decoded = decodeSubscription(subscription.body);
  const nodes = decoded.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean);
  const analyzed = analyzeEgressProxySubscription(nodes, proxy.preferredOutbound);
  if (analyzed.selected === null) throw new Error(analyzed.diagnostics.error ?? `egressProxy preferredOutbound=${proxy.preferredOutbound} is absent from subscription; no fallback outbound was selected`);
  const selected = analyzed.selected;
  const configJson = renderSingBoxConfig(selected, proxy);
  const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
  const noProxy = proxy.noProxy.join(",");
  return {
    sourceRef: proxy.sourceRef,
    sourcePath: source.sourcePathRedacted,
    secretName: proxy.secretName,
    secretKey: proxy.secretKey,
      fingerprint: fingerprintSecretValues({ subscription: subscription.body, configJson }, ["subscription", "configJson"]),
      sourceBytes: Buffer.byteLength(subscription.body, "utf8"),
      selectedOutbound: selected.kind,
      sourceDiagnostics: analyzed.diagnostics,
      configJson,
      proxyUrl,
      noProxy,
      valuesPrinted: false,
    };
}

export function prepareAccountLocalProxySecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): AccountLocalProxySecretMaterial | null {
  const proxy = target.accountLocalProxy;
  if (proxy === null || !proxy.enabled) return null;
  const source = readEnvSourceFile({
    root: secretRoot(sub2api),
    sourceRef: proxy.sourceRef,
    missingMessage: (sourcePath) => `accountLocalProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`,
  });
  const password = requiredEnvValue(source.values, proxy.sourceKey, proxy.sourceRef);
  const configJson = renderAccountLocalProxyConfig(proxy, password);
  return {
    sourceRef: proxy.sourceRef,
    sourcePath: source.sourcePathRedacted,
    secretName: proxy.secretName,
    secretKey: proxy.secretKey,
    fingerprint: fingerprintSecretValues({ password, configJson }, ["password", "configJson"]),
    sourceBytes: Buffer.byteLength(password, "utf8"),
    selectedOutbound: "shadowsocks",
    configJson,
    proxyUrl: `http://${proxy.listenHost}:${proxy.listenPort}`,
    valuesPrinted: false,
  };
}

export function renderAccountLocalProxyConfig(proxy: Sub2ApiAccountLocalProxyConfig, password: string): string {
  return `${JSON.stringify({
    log: { level: "info", timestamp: true },
    inbounds: [
      {
        type: "mixed",
        tag: "account-local",
        listen: proxy.listenHost,
        listen_port: proxy.listenPort,
      },
    ],
    outbounds: [
      {
        type: "shadowsocks",
        tag: "master-vpn",
        server: proxy.masterShadowsocks.serverHost,
        server_port: proxy.masterShadowsocks.serverPort,
        method: proxy.masterShadowsocks.method,
        password,
      },
      { 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",
    },
  }, null, 2)}\n`;
}

export function fetchSubscription(subscriptionUrl: string): { body: string } {
  const shellUrl = shQuote(subscriptionUrl);
  const command = `curl -fsSL --connect-timeout 10 --max-time 30 ${shellUrl}`;
  const proc = Bun.spawnSync(["bash", "-lc", command], { stdout: "pipe", stderr: "pipe" });
  if (proc.exitCode !== 0) {
    const stderr = new TextDecoder().decode(proc.stderr).slice(-500);
    throw new Error(`failed to fetch master VPN subscription: exit=${proc.exitCode} stderr=${stderr}`);
  }
  const body = new TextDecoder().decode(proc.stdout).trim();
  if (body.length === 0) throw new Error("master VPN subscription is empty");
  return { body };
}

export function decodeSubscription(raw: string): string {
  if (raw.includes("://")) return raw;
  const normalized = raw.replace(/\s+/gu, "");
  const padding = "=".repeat((4 - (normalized.length % 4)) % 4);
  try {
    return Buffer.from(`${normalized}${padding}`, "base64").toString("utf8");
  } catch (error) {
    throw new Error(`master VPN subscription is neither URI list nor base64 URI list: ${String(error)}`);
  }
}

export function safeEgressProxySubscriptionDiagnostics(sub2api: Sub2ApiConfig, proxy: Sub2ApiEgressProxyConfig): EgressProxySubscriptionDiagnostics {
  try {
    if (proxy.sourceType === "master-shadowsocks") return masterShadowsocksDiagnostics(proxy);
    const source = readEnvSourceFile({
      root: secretRoot(sub2api),
      sourceRef: proxy.sourceRef,
      missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`,
    });
    if (proxy.preferredOutbound === null) throw new Error(`egressProxy preferredOutbound is required for sourceType=${proxy.sourceType}`);
    const subscriptionUrl = requiredEnvValue(source.values, proxy.sourceKey, proxy.sourceRef);
    const subscription = fetchSubscription(subscriptionUrl);
    const decoded = decodeSubscription(subscription.body);
    const nodes = decoded.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean);
    return analyzeEgressProxySubscription(nodes, proxy.preferredOutbound).diagnostics;
  } catch (error) {
    return {
      ok: false,
      sourceType: proxy.sourceType,
      preferredOutbound: proxy.preferredOutbound,
      candidateCount: 0,
      supportedCount: 0,
      unsupportedCount: 0,
      byKind: {},
      selectedOutbound: null,
      selectedFingerprint: null,
      candidates: [],
      error: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500),
      valuesPrinted: false,
    };
  }
}

export function masterShadowsocksDiagnostics(proxy: Sub2ApiEgressProxyConfig): EgressProxySubscriptionDiagnostics {
  const shadowsocks = proxy.masterShadowsocks;
  if (shadowsocks === null) {
    return {
      ok: false,
      sourceType: proxy.sourceType,
      preferredOutbound: proxy.preferredOutbound,
      candidateCount: 0,
      supportedCount: 0,
      unsupportedCount: 0,
      byKind: {},
      selectedOutbound: null,
      selectedFingerprint: null,
      candidates: [],
      error: "masterShadowsocks is missing",
      valuesPrinted: false,
    };
  }
  const fingerprint = fingerprintSecretValues({ outbound: `${shadowsocks.serverHost}:${shadowsocks.serverPort}:${shadowsocks.method}` }, ["outbound"]);
  return {
    ok: true,
    sourceType: "master-shadowsocks",
    preferredOutbound: null,
    candidateCount: 1,
    supportedCount: 1,
    unsupportedCount: 0,
    byKind: { shadowsocks: 1 },
    selectedOutbound: "shadowsocks",
    selectedFingerprint: fingerprint,
    candidates: [
      {
        sourceLine: 1,
        kind: "shadowsocks",
        fingerprint,
        paramKeys: ["serverHost", "serverPort", "method"],
        selected: true,
      },
    ],
    valuesPrinted: false,
  };
}
