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

// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:1-319 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 { readCodexPoolConfig } from "./config";
import { codexPoolRuntimeTarget } from "./runtime-target";

export const serviceName = "sub2api";

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

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

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

export const sentinelImageDockerfilePath = rootPath("src", "components", "platform-infra", "sub2api", "sentinel.Dockerfile");

export const remoteJobDir = "/tmp/unidesk-platform-infra-sub2api-codex-pool";

export const remoteJobTimeoutMs = 15 * 60_000;

export const remoteJobPollMs = 5_000;

export interface DisclosureOptions {
  full: boolean;
  raw: boolean;
  targetId: string;
}

export interface SyncOptions extends DisclosureOptions {
  confirm: boolean;
  pruneRemoved: boolean;
}

export interface ConfirmOptions extends DisclosureOptions {
  confirm: boolean;
}

export interface SentinelProbeOptions extends ConfirmOptions {
  accounts: string[];
}

export interface SentinelReportOptions extends DisclosureOptions {
  events: number;
}

export interface TraceOptions extends DisclosureOptions {
  requestId: string | null;
  since: string;
  tail: number;
  contextSeconds: number;
  showLines: boolean;
}

export interface FeedbackOptions {
  user: string;
  window: "5m" | "30m" | "1h" | "6h" | "24h" | "7d" | "30d";
  requestId: string | null;
  pageToken: string | null;
  json: boolean;
  targetId: string;
}

export interface SentinelImageOptions extends DisclosureOptions {
  action: "status" | "build";
  confirm: boolean;
  dryRun: boolean;
}

export interface CodexPoolRuntimeTarget {
  id: string;
  route: string;
  namespace: string;
  runtimeMode: "k3s" | "host-docker";
  serviceName: string;
  serviceDns: string;
  publicBaseUrl: string | null;
  publicExposure: CodexPoolRuntimePublicExposure | null;
  appSecretName: string;
  secretsRoot: string;
  appSourceRef: string;
  hostDockerAppPort: number | null;
  hostDockerEnvPath: string | null;
  sentinelEnabled: boolean;
  sentinelImageBuild: {
    baseImageCachePolicy: "pull" | "local-if-present";
    noProxy: string;
    proxyEnvPath: string | null;
  };
  egressProxy: {
    enabled: boolean;
    applyToSentinel: boolean;
    serviceName: string;
    listenPort: number;
    httpProxy: string;
    noProxy: string;
  } | null;
}

export interface CodexPoolRuntimePublicExposure {
  enabled: boolean;
  mode: "frp" | "pk01-local";
  publicBaseUrl: string;
  dns: { hostname: string; expectedA: string; resolvers: string[] };
  local: {
    upstreamHost: string;
    upstreamPort: number;
  } | null;
  frpc: PublicServiceExposure["frpc"] | null;
  pk01: PublicServiceExposure["pk01"];
}

export interface CodexProfile {
  profile: string;
  accountName: string;
  configFile: string;
  authFile: string;
  provider: string;
  baseUrl: string;
  wireApi: string | null;
  model: string | null;
  envKey: string | null;
  apiKey: string | null;
  apiKeySource: "auth-json" | "env" | null;
  openaiResponsesWebSocketsV2Mode: OpenAIResponsesWebSocketsV2Mode | null;
  upstreamUserAgent: string | null;
  trustUpstream: boolean;
  sentinelProtect: CodexSentinelProtectPolicy;
  priority: number;
  capacity: number;
  loadFactor: number;
  tempUnschedulableTemplate: string;
  tempUnschedulable: CodexTempUnschedulablePolicy;
  authOpenAIKeyShape: string;
  ok: boolean;
  error: string | null;
}

export type OpenAIResponsesWebSocketsV2Mode = "off" | "ctx_pool" | "passthrough";

export interface CodexTempUnschedulableRule {
  statusCode: number;
  keywords: string[];
  durationMinutes: number;
  description: string | null;
}

export interface CodexTempUnschedulablePolicy {
  enabled: boolean;
  rules: CodexTempUnschedulableRule[];
}

export interface CodexRuntimeTemplate {
  id: string;
  kind: "temp-unschedulable";
  description: string;
  tempUnschedulable: CodexTempUnschedulablePolicy;
}

export interface CodexErrorPassthroughRule {
  id: string;
  name: string;
  enabled: boolean;
  priority: number;
  errorCodes: number[];
  keywords: string[];
  matchMode: "any" | "all";
  platforms: string[];
  passthroughCode: boolean;
  responseCode: number | null;
  passthroughBody: boolean;
  customMessage: string | null;
  skipMonitoring: boolean;
  description: string;
}

export interface CodexRuntimeConfig {
  templates: CodexRuntimeTemplate[];
  templatesById: Record<string, CodexRuntimeTemplate>;
  errorPassthroughRules: CodexErrorPassthroughRule[];
  errorPassthroughRulesById: Record<string, CodexErrorPassthroughRule>;
}

export interface CodexPoolProfitConfig {
  defaultWindow: string;
  usageCostField: "actual_cost";
  accountCostSource: "account-name-trailing-cny-per-api-usd";
  saleRateCnyPerApiUsd: number;
  periodCosts: Array<{
    id: string;
    period: string;
    accountName: string;
    amountCny: number;
    description: string;
  }>;
  groups: {
    profitable: string[];
    selfUse: string[];
    excluded: string[];
  };
}

export interface CodexPoolCreditsConfig {
  timezone: string;
  defaultRankTiers: CodexPoolCreditRankTier[];
  excludedUsers: string[];
}

export interface CodexPoolCreditRankTier {
  count: number | null;
  amountUsd: number;
}

export interface CodexPoolConfig {
  version: number;
  kind: string;
  metadata: {
    id: string;
    owner: string;
    relatedIssues: number[];
  };
  groupName: string;
  groupDescription: string;
  apiKeyName: string;
  apiKeySecretName: string;
  apiKeySecretKey: string;
  adminEmailDefault: string;
  minOwnerBalanceUsd: number;
  minOwnerConcurrency: number;
  minOwnerConcurrencySource: "auto" | "yaml";
  defaultAccountPriority: number;
  defaultAccountCapacity: number;
  defaultAccountLoadFactor: number;
  defaultTempUnschedulableTemplate: string;
  defaultTempUnschedulable: CodexTempUnschedulablePolicy;
  defaultSentinelProtect: CodexSentinelProtectPolicy;
  profiles: CodexPoolProfileConfig[];
  runtime: CodexRuntimeConfig;
  profit: CodexPoolProfitConfig;
  credits: CodexPoolCreditsConfig;
  manualAccounts: CodexPoolManualAccountsConfig;
  publicExposure: CodexPoolPublicExposureConfig;
  localCodex: CodexPoolLocalCodexConfig;
  sentinel: CodexPoolSentinelConfig;
}

export interface CodexPoolManualAccountsConfig {
  bindingSources: CodexPoolManualBindingSourcesConfig;
  protected: CodexPoolManualAccountProtection[];
}

export type CodexPoolManualBindingKind = "proxy" | "group";

export type CodexPoolManualBindingProvider = "target-egress-proxy" | "fixed-http-proxy" | "pool-group";

export interface CodexPoolManualFixedProxyConfig {
  protocol: "http";
  host: string;
  port: number;
}

export interface CodexPoolManualBindingSource {
  id: string;
  enabled: boolean;
  kind: CodexPoolManualBindingKind;
  provider: CodexPoolManualBindingProvider;
  description: string | null;
  hostProxyRef: string | null;
  fixedProxy: CodexPoolManualFixedProxyConfig | null;
}

export interface CodexPoolManualBindingSourcesConfig {
  items: CodexPoolManualBindingSource[];
  byId: Record<string, CodexPoolManualBindingSource>;
}

export interface CodexPoolManualAccountProxyBinding {
  enabled: boolean;
  source: string;
  proxyName: string;
}

export interface CodexPoolManualAccountGroupBinding {
  enabled: boolean;
  source: string;
}

export interface CodexPoolManualAccountProtection {
  accountName: string;
  reason: string | null;
  targetIds: string[] | null;
  proxyBinding: CodexPoolManualAccountProxyBinding | null;
  groupBinding: CodexPoolManualAccountGroupBinding | null;
}

export interface CodexPoolProfileConfig {
  profile: string;
  accountName: string | null;
  configFile: string;
  authFile: string;
  fallbackConfigFile: string | null;
  fallbackAuthFile: string | null;
  openaiResponsesWebSocketsV2Mode: OpenAIResponsesWebSocketsV2Mode | null;
  upstreamUserAgent: string | null;
  trustUpstream: boolean;
  sentinelProtect: CodexSentinelProtectPolicy;
  priority: number;
  capacity: number | null;
  loadFactor: number | null;
  tempUnschedulableTemplate: string;
  tempUnschedulable: CodexTempUnschedulablePolicy;
}

export interface CodexSentinelProtectPolicy {
  enabled: boolean;
  consecutiveFailures: number;
  initialRetryDelaySeconds: number;
  maxRetryDelaySeconds: number;
  backoffMultiplier: number;
}

export interface CodexPoolPublicExposureConfig {
  enabled: boolean;
  proxyName: string;
  configMapName: string;
  deploymentName: string;
  frpcImage: string;
  serverAddr: string;
  serverPort: number;
  remotePort: number;
  localIP: string;
  localPort: number;
  publicBaseUrl: string;
  masterBaseUrl: string;
  masterFrps: {
    configPath: string;
    containerName: string;
  };
  masterCaddy: {
    enabled: boolean;
    domain: string;
    configPath: string;
    serviceName: string;
    upstreamBaseUrl: string;
    responseHeaderTimeoutSeconds: number;
    edgeRetry: CodexPoolCaddyEdgeRetryConfig;
  };
}

export interface CodexPoolCaddyEdgeRetryConfig {
  enabled: boolean;
  tryDurationSeconds: number;
  tryIntervalMilliseconds: number;
  retryMatch: {
    methods: string[];
    paths: string[];
  };
}

export interface CodexPoolLocalCodexConfig {
  backupSuffix: string;
  providerName: string;
  wireApi: string;
  modelContextWindow: number;
  modelAutoCompactTokenLimit: number;
  supportsWebSockets: boolean;
  responsesWebSocketsV2: boolean;
  responsesSmokeModel: string;
}

export interface CodexLocalConsumerTomlOptions {
  providerName: string;
  baseUrl: string;
  wireApi: string;
  modelContextWindow: number;
  modelAutoCompactTokenLimit: number;
  supportsWebSockets: boolean;
  responsesWebSocketsV2: boolean;
}

export function codexPoolTraceHelp(): Record<string, unknown> {
  return {
    command: "platform-infra sub2api codex-pool trace",
    usage: "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <id> [options]",
    options: {
      "--request-id <id>": "Required client or internal request ID; --id is an alias.",
      "--target <id>": "YAML-selected Sub2API target.",
      "--since <duration>": "Search window such as 2h or 24h; default 24h.",
      "--tail <count>": "Bounded runtime log tail from 100 to 200000; default 20000.",
      "--context-seconds <count>": "Context around indexed events from 0 to 3600; default 300.",
      "--show-lines": "Include bounded matched runtime log lines.",
      "--full | --raw": "Explicit expanded or machine-readable disclosure.",
    },
    state: {
      mutation: false,
      remoteJobSubmitted: false,
      helpSource: "local",
    },
  };
}

export function codexPoolHelp(): unknown {
  const pool = readCodexPoolConfig();
  const runtimeTarget = codexPoolRuntimeTarget();
  return {
    command: "platform-infra sub2api codex-pool plan|sync|validate|runtime|faults|profit|credits|announcements|error-passthrough|users|trace|feedback|sentinel-image|sentinel-probe|sentinel-report|cleanup-probes|expose|configure-local",
    output: "json, except trace, feedback, and sentinel-report default to low-noise text tables",
    usage: [
      "bun scripts/cli.ts platform-infra sub2api codex-pool plan",
      "bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sync [--target D601] --confirm [--prune-removed]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool validate [--target D601] [--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime list [--recent 24h] [--target PK01] [--json|--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account <name-or-id> [--target PK01] [--json|--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <group-id>] [--account <name-or-id>] [--since 24h] [--tail 50000] [--target PK01] [--json|--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime events [--since 24h] [--tail 50000] [--target PK01] [--json|--raw] (目标侧仅筛选 policy marker，聚合由调用方完成)",
      "bun scripts/cli.ts platform-infra sub2api codex-pool faults [--level P0|P1|P2] [--group <name-or-id>] [--account <name-or-id>] [--model <model>] [--stream sync|stream] [--endpoint <path>] [--request-id <id>] [--page-token <token>] [--target PK01] [--json]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--sale-rate <CNY/API_USD>] [--scenario-without-account <id-or-exact-name> ...] [--accounts [--page-token <token>]|--missing-costs] [--json]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool credits grant (--active-since 24h|--active-start <ISO> --active-end <ISO>|--users <selectors>|--allocations-json <object>) [--amount-usd <amount>|--rank-tiers <count:amount,...,*:amount>] --reason <text> [--target PK01] [--confirm] [--json]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool announcements list|get|create [--target PK01] [--confirm for create] [--json]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough list|apply|delete [--template <id>] [--target PK01] [--confirm] [--json]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool users ensure --email <email> --username <name> --api-key-name <name> [--target PK01] [--confirm] [--json]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --template <id> [--target PK01] [--confirm]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --template <id> [--target PK01] [--confirm] [--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --template <id> --select-configured-policy --account-type apikey [--exclude-accounts <name-or-id,...>] [--target PK01] [--confirm] [--json|--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --kind priority --priority <0-1000> [--target PK01] [--confirm]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --kind capacity --capacity <1-100000> [--target PK01] [--confirm]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --kind account-name --name <new-name> [--target PK01] [--confirm]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --account <name-or-id> --kind temp-unschedulable [--target PK01] [--confirm]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --accounts <name-or-id,...> --kind temp-unschedulable [--target PK01] [--confirm] [--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool trace [--target D601] --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool feedback --user <email-or-id> [--window 30m] [--page-token <token>|--request-id <client-or-internal-id>] [--target PK01] [--json]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status [--target D601]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build [--target D601] --confirm",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe [--target D601] --account unidesk-codex-hy --confirm",
      "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report [--target D601] [--events 20|--full|--raw]",
      "bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes [--target D601] --confirm",
      "bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm",
      "bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
    ],
    description: "Import YAML-selected ~/.codex API-key profiles into one Sub2API OpenAI pool, expose one unified API_KEY, and optionally configure this master server's ~/.codex consumer endpoint.",
    target: {
      default: runtimeTarget.id,
      route: runtimeTarget.route,
      namespace: runtimeTarget.namespace,
      serviceDns: runtimeTarget.serviceDns,
      configPath: codexPoolConfigPath,
      poolGroupName: pool.groupName,
      poolApiKeySecretName: pool.apiKeySecretName,
      poolApiKeySecretKey: pool.apiKeySecretKey,
      publicBaseUrl: runtimeTarget.publicBaseUrl,
      publicExposureSource: `${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure`,
      sentinelMonitorEnabled: pool.sentinel.monitor.enabled,
      sentinelActionsEnabled: pool.sentinel.actions.enabled,
      secretValuesPrinted: false,
    },
  };
}
