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

// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:3422-3685 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 { CodexLocalConsumerTomlOptions, CodexPoolConfig } from "./types";
import { fingerprint, isRecord } from "./config-utils";
import { capture, parseJsonOutput } from "./remote";
import { codexPoolRuntimeTarget } from "./runtime-target";
import { sub2apiConfigPath } from "./types";

export async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): Promise<{ apiKey: string | null; error: string | null }> {
  const result = await capture(config, target.route, ["sh"], `
set -u
kubectl -n ${target.namespace} get secret ${pool.apiKeySecretName} -o json
`);
  if (result.exitCode !== 0) {
    return { apiKey: null, error: `read pool API key secret failed: ${result.stderr.slice(-1000)}` };
  }
  const parsed = parseJsonOutput(result.stdout);
  const data = isRecord(parsed?.data) ? parsed.data : null;
  const encoded = typeof data?.[pool.apiKeySecretKey] === "string" ? data[pool.apiKeySecretKey] : null;
  if (encoded === null) return { apiKey: null, error: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey} missing` };
  try {
    const apiKey = Buffer.from(encoded, "base64").toString("utf8");
    return apiKey.length > 0 ? { apiKey, error: null } : { apiKey: null, error: "decoded API key is empty" };
  } catch (error) {
    return { apiKey: null, error: error instanceof Error ? error.message : String(error) };
  }
}

export function writeLocalCodexConfig(pool: CodexPoolConfig, apiKey: string, target = codexPoolRuntimeTarget()): Record<string, unknown> {
  const codexDir = join(homedir(), ".codex");
  mkdirSync(codexDir, { recursive: true, mode: 0o700 });
  const configPath = join(codexDir, "config.toml");
  const authPath = join(codexDir, "auth.json");
  const backupConfigPath = join(codexDir, `config.toml.${pool.localCodex.backupSuffix}`);
  const backupAuthPath = join(codexDir, `auth.json.${pool.localCodex.backupSuffix}`);
  if (!existsSync(configPath)) throw new Error(`${configPath} missing`);
  if (!existsSync(authPath)) throw new Error(`${authPath} missing`);
  const configBackupAction = copyIfMissing(configPath, backupConfigPath);
  const authBackupAction = copyIfMissing(authPath, backupAuthPath);
  const currentToml = readFileSync(configPath, "utf8");
  const nextToml = updateCodexConfigToml(currentToml, pool, target);
  writeFileSync(configPath, nextToml, { encoding: "utf8", mode: 0o600 });
  chmodSync(configPath, 0o600);
  writeFileSync(authPath, `${JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
  chmodSync(authPath, 0o600);
  return {
    ok: true,
    codexDir,
    config: {
      path: configPath,
      bytes: statSync(configPath).size,
      backupPath: backupConfigPath,
      backupAction: configBackupAction,
      backupBytes: statSync(backupConfigPath).size,
    },
    auth: {
      path: authPath,
      bytes: statSync(authPath).size,
      backupPath: backupAuthPath,
      backupAction: authBackupAction,
      backupBytes: statSync(backupAuthPath).size,
      openaiApiKeyShape: "string",
      openaiApiKeyBytes: Buffer.byteLength(apiKey, "utf8"),
      openaiApiKeyFingerprint: fingerprint(apiKey),
      valuesPrinted: false,
    },
    provider: {
      name: pool.localCodex.providerName,
      baseUrl: codexConsumerBaseUrl(pool, target),
      wireApi: pool.localCodex.wireApi,
      modelContextWindow: pool.localCodex.modelContextWindow,
      modelAutoCompactTokenLimit: pool.localCodex.modelAutoCompactTokenLimit,
      supportsWebSockets: pool.localCodex.supportsWebSockets,
      responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2,
    },
    valuesPrinted: false,
  };
}

export function copyIfMissing(source: string, target: string): "created" | "kept-existing" {
  if (existsSync(target)) return "kept-existing";
  copyFileSync(source, target);
  chmodSync(target, 0o600);
  return "created";
}

export function updateCodexConfigToml(current: string, pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): string {
  return renderCodexLocalConsumerToml(current, {
    providerName: pool.localCodex.providerName,
    baseUrl: codexConsumerBaseUrl(pool, target),
    wireApi: pool.localCodex.wireApi,
    modelContextWindow: pool.localCodex.modelContextWindow,
    modelAutoCompactTokenLimit: pool.localCodex.modelAutoCompactTokenLimit,
    supportsWebSockets: pool.localCodex.supportsWebSockets,
    responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2,
  });
}

export function codexConsumerBaseUrl(pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): string {
  void pool;
  const baseUrl = target.publicBaseUrl;
  if (baseUrl === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure.enabled must be true before resolving the Codex consumer base URL`);
  return `${baseUrl.replace(/\/+$/u, "")}/`;
}

export function renderCodexLocalConsumerToml(current: string, options: CodexLocalConsumerTomlOptions): string {
  let next = upsertTopLevelTomlString(current, "model_provider", options.providerName);
  next = upsertTopLevelTomlInteger(next, "model_context_window", options.modelContextWindow);
  next = upsertTopLevelTomlInteger(next, "model_auto_compact_token_limit", options.modelAutoCompactTokenLimit);
  next = upsertProviderSection(next, options);
  next = upsertTomlSectionBoolean(next, "features", "responses_websockets_v2", options.responsesWebSocketsV2);
  return next.endsWith("\n") ? next : `${next}\n`;
}

export function upsertTopLevelTomlString(text: string, key: string, value: string): string {
  const lines = text.split(/\r?\n/u);
  const firstSection = lines.findIndex((line) => /^\s*\[/.test(line));
  const end = firstSection === -1 ? lines.length : firstSection;
  for (let index = 0; index < end; index += 1) {
    if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) {
      lines[index] = `${key} = ${tomlString(value)}`;
      return lines.join("\n");
    }
  }
  lines.splice(0, 0, `${key} = ${tomlString(value)}`);
  return lines.join("\n");
}

export function upsertTopLevelTomlInteger(text: string, key: string, value: number): string {
  const lines = text.split(/\r?\n/u);
  const firstSection = lines.findIndex((line) => /^\s*\[/.test(line));
  const end = firstSection === -1 ? lines.length : firstSection;
  const assignment = `${key} = ${value}`;
  for (let index = 0; index < end; index += 1) {
    if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) {
      lines[index] = assignment;
      return lines.join("\n");
    }
  }
  lines.splice(end, 0, assignment);
  return lines.join("\n");
}

export function upsertProviderSection(text: string, options: CodexLocalConsumerTomlOptions): string {
  const { providerName, baseUrl, wireApi, supportsWebSockets } = options;
  const header = `[model_providers.${providerName}]`;
  const lines = text.split(/\r?\n/u);
  const start = lines.findIndex((line) => line.trim() === header);
  const canonical = [
    `name = ${tomlString(providerName)}`,
    `base_url = ${tomlString(baseUrl)}`,
    `wire_api = ${tomlString(wireApi)}`,
    "requires_openai_auth = true",
    `supports_websockets = ${tomlBoolean(supportsWebSockets)}`,
  ];
  if (start === -1) {
    const needsBlank = lines.length > 0 && lines[lines.length - 1].trim() !== "";
    return [...lines, ...(needsBlank ? [""] : []), header, ...canonical].join("\n");
  }
  let end = lines.length;
  for (let index = start + 1; index < lines.length; index += 1) {
    if (/^\s*\[/.test(lines[index])) {
      end = index;
      break;
    }
  }
  const preserved = lines.slice(start + 1, end).filter((line) => {
    return !/^\s*(name|base_url|wire_api|requires_openai_auth|supports_websockets|env_key)\s*=/u.test(line);
  });
  const replacement = [header, ...canonical, ...preserved.filter((line) => line.trim() !== "")];
  return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n");
}

export function upsertTomlSectionBoolean(text: string, sectionName: string, key: string, value: boolean): string {
  const header = `[${sectionName}]`;
  const lines = text.split(/\r?\n/u);
  const start = lines.findIndex((line) => line.trim() === header);
  const assignment = `${key} = ${tomlBoolean(value)}`;
  if (start === -1) {
    const needsBlank = lines.length > 0 && lines[lines.length - 1].trim() !== "";
    return [...lines, ...(needsBlank ? [""] : []), header, assignment].join("\n");
  }
  let end = lines.length;
  for (let index = start + 1; index < lines.length; index += 1) {
    if (/^\s*\[/.test(lines[index])) {
      end = index;
      break;
    }
  }
  for (let index = start + 1; index < end; index += 1) {
    if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) {
      lines[index] = assignment;
      return lines.join("\n");
    }
  }
  return [...lines.slice(0, start + 1), assignment, ...lines.slice(start + 1)].join("\n");
}

export async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: string, target = codexPoolRuntimeTarget()): Promise<Record<string, unknown>> {
  const probe = await probePublicModels(pool, "with-api-key", apiKey, target);
  return {
    ...probe,
    ok: probe.ok === true,
    apiKeyFingerprint: fingerprint(apiKey),
    keyPreview: apiKeyPreview(apiKey),
    valuesPrinted: false,
  };
}

export async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string, target = codexPoolRuntimeTarget()): Promise<Record<string, unknown>> {
  void pool;
  if (target.publicBaseUrl === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure.enabled must be true before probing the public gateway`);
  const baseUrl = target.publicBaseUrl;
  const url = `${baseUrl.replace(/\/+$/u, "")}/v1/models`;
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 8000);
  try {
    const response = await fetch(url, {
      method: "GET",
      headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
      signal: controller.signal,
    });
    const body = await response.text();
    let parsed: unknown = null;
    try {
      parsed = body.trim().length > 0 ? JSON.parse(body) : null;
    } catch {
      parsed = null;
    }
    const modelCount = isRecord(parsed) && Array.isArray(parsed.data) ? parsed.data.length : null;
    return {
      ok: response.ok,
      mode,
      method: "GET /v1/models",
      baseUrl,
      httpStatus: response.status,
      modelCount,
      bodyPreview: response.ok ? "" : body.slice(0, 500),
      valuesPrinted: false,
    };
  } catch (error) {
    return {
      ok: false,
      mode,
      method: "GET /v1/models",
      baseUrl,
      httpStatus: 0,
      error: error instanceof Error ? error.message : String(error),
      valuesPrinted: false,
    };
  } finally {
    clearTimeout(timeout);
  }
}

export function apiKeyPreview(apiKey: string): string {
  if (apiKey.length <= 14) return "***";
  return `${apiKey.slice(0, 10)}...${apiKey.slice(-4)}`;
}

export function tomlString(value: string): string {
  return JSON.stringify(value);
}

export function tomlBoolean(value: boolean): string {
  return value ? "true" : "false";
}

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