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

// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:621-819 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 { Sub2ApiRuntimeConfig } from "./options";
import type { CodexPoolRuntimeTarget } from "./types";
import { validatePort, validateProxyName } from "./config";
import { booleanValue, isRecord, normalizeBaseUrl, numberValue, stringValue, validateKubernetesName } from "./config-utils";
import { serviceName, sub2apiConfigPath } from "./types";

export function readSub2ApiRuntimeConfig(): Sub2ApiRuntimeConfig {
  const parsed = Bun.YAML.parse(readFileSync(sub2apiConfigPath, "utf8")) as unknown;
  if (!isRecord(parsed)) throw new Error(`${sub2apiConfigPath} must contain a YAML object`);
  const defaults = isRecord(parsed.defaults) ? parsed.defaults : null;
  const defaultTargetId = defaults === null ? null : stringValue(defaults.targetId);
  if (defaultTargetId === null || !/^[A-Za-z0-9._-]+$/u.test(defaultTargetId)) throw new Error(`${sub2apiConfigPath}.defaults.targetId must be a simple target id`);
  const runtime = isRecord(parsed.runtime) ? parsed.runtime : null;
  const database = runtime !== null && isRecord(runtime.database) ? runtime.database : null;
  const appSecretName = database === null ? null : stringValue(database.secretName);
  if (appSecretName === null) throw new Error(`${sub2apiConfigPath}.runtime.database.secretName is required`);
  validateKubernetesName(appSecretName, `${sub2apiConfigPath}.runtime.database.secretName`, true);
  const secrets = runtime !== null && isRecord(runtime.secrets) ? runtime.secrets : null;
  const secretsRoot = secrets === null ? null : stringValue(secrets.root);
  if (secretsRoot === null || !secretsRoot.startsWith("/")) throw new Error(`${sub2apiConfigPath}.runtime.secrets.root must be an absolute path`);
  const sentinel = runtime !== null && isRecord(runtime.sentinel) ? runtime.sentinel : null;
  const enabledOnTargets = Array.isArray(sentinel?.enabledOnTargets)
    ? sentinel.enabledOnTargets.map((entry) => stringValue(entry)).filter((entry): entry is string => entry !== null && entry.length > 0)
    : [];
  if (!Array.isArray(parsed.targets) || !parsed.targets.every(isRecord)) throw new Error(`${sub2apiConfigPath}.targets must be a list`);
  return {
    defaultTargetId,
    appSecretName,
    secretsRoot,
    sentinelEnabledOnTargets: enabledOnTargets,
    targets: parsed.targets,
  };
}

export function defaultCodexPoolRuntimeTargetId(): string {
  return readSub2ApiRuntimeConfig().defaultTargetId;
}

export function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarget {
  const runtimeConfig = readSub2ApiRuntimeConfig();
  const resolvedTargetId = targetId ?? runtimeConfig.defaultTargetId;
  const raw = runtimeConfig.targets.find((item) => String(item.id ?? "").toLowerCase() === resolvedTargetId.toLowerCase());
  if (!isRecord(raw)) throw new Error(`${sub2apiConfigPath}.targets does not contain target ${targetId}`);
  const id = stringValue(raw.id) ?? resolvedTargetId;
  const route = stringValue(raw.route) ?? "";
  const targetNamespace = stringValue(raw.namespace);
  if (route.length === 0) throw new Error(`${sub2apiConfigPath}.targets[${id}].route is required`);
  if (targetNamespace === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].namespace is required`);
  validateKubernetesName(targetNamespace, `${sub2apiConfigPath}.targets[${id}].namespace`, true);
  const sentinelImageBuild = readTargetSentinelImageBuild(raw, id);

  let egressProxy: CodexPoolRuntimeTarget["egressProxy"] = null;
  if (isRecord(raw.egressProxy) && raw.egressProxy.enabled === true) {
    const proxyServiceName = stringValue(raw.egressProxy.serviceName);
    const listenPort = numberValue(raw.egressProxy.listenPort);
    if (proxyServiceName === null || listenPort === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName/listenPort are required`);
    validateKubernetesName(proxyServiceName, `${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName`, true);
    if (!Number.isInteger(listenPort) || listenPort < 1 || listenPort > 65535) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.listenPort must be a TCP port`);
    const noProxyRaw = Array.isArray(raw.egressProxy.noProxy) ? raw.egressProxy.noProxy : [];
    const noProxy = noProxyRaw.map((entry) => stringValue(entry)).filter((entry): entry is string => entry !== null && entry.length > 0).join(",");
    egressProxy = {
      enabled: true,
      applyToSentinel: raw.egressProxy.applyToSentinel === undefined ? true : raw.egressProxy.applyToSentinel === true,
      serviceName: proxyServiceName,
      listenPort,
      httpProxy: `http://${proxyServiceName}.${targetNamespace}.svc.cluster.local:${listenPort}`,
      noProxy,
    };
  }

  let publicBaseUrl: string | null = null;
  const publicExposure = readTargetPublicExposure(raw, id);
  if (publicExposure !== null && publicExposure.enabled) publicBaseUrl = publicExposure.publicBaseUrl;

  return {
    id,
    route,
    namespace: targetNamespace,
    serviceName,
    serviceDns: `${serviceName}.${targetNamespace}.svc.cluster.local:8080`,
    publicBaseUrl,
    publicExposure,
    appSecretName: runtimeConfig.appSecretName,
    secretsRoot: runtimeConfig.secretsRoot,
    sentinelEnabled: runtimeConfig.sentinelEnabledOnTargets.some((entry) => entry.toLowerCase() === id.toLowerCase()),
    sentinelImageBuild,
    egressProxy,
  };
}

export function readTargetSentinelImageBuild(raw: Record<string, unknown>, targetId: string): CodexPoolRuntimeTarget["sentinelImageBuild"] {
  const codexPool = isRecord(raw.codexPool) ? raw.codexPool : null;
  const imageBuild = codexPool !== null && isRecord(codexPool.sentinelImageBuild) ? codexPool.sentinelImageBuild : null;
  if (imageBuild === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild must be a YAML object`);
  const policy = stringValue(imageBuild.baseImageCachePolicy);
  if (policy !== "pull" && policy !== "local-if-present") {
    throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.baseImageCachePolicy must be pull or local-if-present`);
  }
  return {
    baseImageCachePolicy: policy,
    noProxy: readTargetNoProxy(imageBuild.noProxy, `${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.noProxy`),
  };
}

export function readTargetPublicExposure(raw: Record<string, unknown>, targetId: string): PublicServiceExposure | null {
  if (raw.publicExposure === undefined || raw.publicExposure === null) return null;
  if (!isRecord(raw.publicExposure)) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure must be a YAML object`);
  const exposure = raw.publicExposure;
  const enabled = readRequiredTargetBoolean(exposure.enabled, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.enabled`);
  const dns = readRequiredTargetRecord(exposure.dns, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns`);
  const frpc = readRequiredTargetRecord(exposure.frpc, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc`);
  const pk01 = readRequiredTargetRecord(exposure.pk01, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01`);
  const publicBaseUrl = readTargetBaseUrl(exposure.publicBaseUrl, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.publicBaseUrl`, "https:");
  const hostname = readRequiredTargetString(dns.hostname, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.hostname`);
  if (new URL(publicBaseUrl).hostname !== hostname) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure publicBaseUrl hostname must match dns.hostname`);
  const config: PublicServiceExposure = {
    enabled,
    publicBaseUrl,
    dns: {
      hostname,
      expectedA: readRequiredTargetString(dns.expectedA, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.expectedA`),
      resolvers: readTargetStringArray(dns.resolvers, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.resolvers`),
    },
    frpc: {
      deploymentName: readRequiredTargetString(frpc.deploymentName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.deploymentName`),
      secretName: readRequiredTargetString(frpc.secretName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretName`),
      secretKey: readRequiredTargetString(frpc.secretKey, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretKey`),
      image: readRequiredTargetString(frpc.image, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.image`),
      serverAddr: readRequiredTargetString(frpc.serverAddr, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverAddr`),
      serverPort: readTargetPort(frpc.serverPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverPort`),
      proxyName: readRequiredTargetString(frpc.proxyName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.proxyName`),
      remotePort: readTargetPort(frpc.remotePort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.remotePort`),
      localIP: readRequiredTargetString(frpc.localIP, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localIP`),
      localPort: readTargetPort(frpc.localPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localPort`),
      tokenSourceRef: readRequiredTargetString(frpc.tokenSourceRef, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.tokenSourceRef`),
      tokenSourceKey: readRequiredTargetString(frpc.tokenSourceKey, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.tokenSourceKey`),
    },
    pk01: {
      route: readRequiredTargetString(pk01.route, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.route`),
      caddyConfigPath: readAbsoluteTargetPath(pk01.caddyConfigPath, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.caddyConfigPath`),
      caddyServiceName: readRequiredTargetString(pk01.caddyServiceName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.caddyServiceName`),
      responseHeaderTimeoutSeconds: readPositiveTargetInteger(pk01.responseHeaderTimeoutSeconds, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.responseHeaderTimeoutSeconds`),
    },
  };
  validateKubernetesName(config.frpc.deploymentName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.deploymentName`, true);
  validateKubernetesName(config.frpc.secretName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretName`, true);
  validateProxyName(config.frpc.proxyName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.proxyName`);
  validatePort(config.frpc.serverPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverPort`);
  validatePort(config.frpc.remotePort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.remotePort`);
  validatePort(config.frpc.localPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localPort`);
  return config;
}

export function readRequiredTargetRecord(value: unknown, path: string): Record<string, unknown> {
  if (!isRecord(value)) throw new Error(`${path} must be a YAML object`);
  return value;
}

export function readRequiredTargetString(value: unknown, path: string): string {
  const text = stringValue(value);
  if (text === null) throw new Error(`${path} must be a non-empty string`);
  if (/[\r\n]/u.test(text)) throw new Error(`${path} must not contain newlines`);
  return text;
}

export function readRequiredTargetBoolean(value: unknown, path: string): boolean {
  const parsed = booleanValue(value);
  if (parsed === null) throw new Error(`${path} must be a boolean`);
  return parsed;
}

export function readTargetBaseUrl(value: unknown, path: string, protocol: "http:" | "https:" | null = null): string {
  const baseUrl = normalizeBaseUrl(stringValue(value));
  if (baseUrl === null) throw new Error(`${path} must be a valid http(s) URL`);
  if (protocol !== null && new URL(baseUrl).protocol !== protocol) throw new Error(`${path} must use ${protocol}//`);
  return baseUrl;
}

export function readTargetPort(value: unknown, path: string): number {
  const port = numberValue(value);
  if (port === null || !Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${path} must be an integer TCP port`);
  return port;
}

export function readPositiveTargetInteger(value: unknown, path: string): number {
  const number = numberValue(value);
  if (number === null || !Number.isInteger(number) || number < 1) throw new Error(`${path} must be a positive integer`);
  return number;
}

export function readAbsoluteTargetPath(value: unknown, path: string): string {
  const text = readRequiredTargetString(value, path);
  if (!text.startsWith("/")) throw new Error(`${path} must be an absolute path`);
  return text;
}

export function readTargetStringArray(value: unknown, path: string): string[] {
  if (!Array.isArray(value)) throw new Error(`${path} must be a YAML array`);
  const result = value.map((item, index) => readRequiredTargetString(item, `${path}[${index}]`));
  if (result.length === 0) throw new Error(`${path} must not be empty`);
  return result;
}

export function readTargetNoProxy(value: unknown, path: string): string {
  return readTargetStringArray(value, path).join(",");
}

export function targetFlag(target: CodexPoolRuntimeTarget): string {
  return target.id === defaultCodexPoolRuntimeTargetId() ? "" : ` --target ${target.id}`;
}
