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

// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:7231-7600 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 { codexPoolRuntimeTarget } from "./runtime-target";
import { remoteJobDir, remoteJobPollMs, remoteJobTimeoutMs } from "./types";

export async function capture(config: UniDeskConfig, target: string, args: string[], input?: string): Promise<SshCaptureResult> {
  return await runSshCommandCapture(config, target, args, input);
}

export type RemoteCodexPoolMode = "sync" | "validate" | "runtime" | "faults" | "profit" | "credits" | "error-passthrough" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build";

export async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget()): Promise<SshCaptureResult> {
  const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63);
  const startedAtMs = Date.now();
  const start = await capture(config, target.route, ["sh"], remoteJobStartScript(jobName, script));
  const started = parseJsonOutput(start.stdout);
  if (start.exitCode !== 0 || boolField(started, "ok", false) !== true) return start;

  let latest: RemoteCodexPoolJobStatus | null = null;
  while (Date.now() - startedAtMs <= remoteJobTimeoutMs) {
    await sleep(remoteJobPollMs);
    const probe = await capture(config, target.route, ["sh"], remoteJobStatusScript(jobName));
    const parsed = parseJsonOutput(probe.stdout);
    latest = normalizeRemoteJobStatus(parsed);
    process.stderr.write(`${JSON.stringify({
      event: "platform-infra.sub2api.codex-pool.progress",
      at: new Date().toISOString(),
      mode,
      jobName,
      status: latest?.status ?? "unknown",
      exitCode: latest?.exitCode ?? null,
      elapsedMs: Date.now() - startedAtMs,
      stdoutBytes: latest?.stdoutBytes ?? null,
      stderrBytes: latest?.stderrBytes ?? null,
    })}\n`);
    if (probe.exitCode !== 0) {
      return {
        exitCode: probe.exitCode,
        stdout: probe.stdout,
        stderr: `${start.stderr}${probe.stderr}`,
      };
    }
    if (latest?.status === "succeeded" || latest?.status === "failed") {
      return {
        exitCode: latest.exitCode ?? (latest.status === "succeeded" ? 0 : 1),
        stdout: latest.stdout ?? "",
        stderr: latest.stderr ?? "",
      };
    }
  }

  return {
    exitCode: 124,
    stdout: latest?.stdout ?? "",
    stderr: [
      latest?.stderr ?? "",
      `remote codex-pool ${mode} job ${jobName} did not finish within ${remoteJobTimeoutMs}ms`,
      `status command: ${codexPoolModeCommand(mode)}`,
    ].filter(Boolean).join("\n"),
  };
}

export function codexPoolModeCommand(mode: RemoteCodexPoolMode): string {
  if (mode === "sync") return "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm";
  if (mode === "runtime") return "bun scripts/cli.ts platform-infra sub2api codex-pool runtime list";
  if (mode === "profit") return "bun scripts/cli.ts platform-infra sub2api codex-pool profit --since 24h";
  if (mode === "credits") return "bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-since 24h --amount-usd 20 --reason <text>";
  if (mode === "error-passthrough") return "bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough list";
  if (mode === "sentinel-probe") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account <accountName> --confirm";
  if (mode === "sentinel-image-status") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status";
  if (mode === "sentinel-image-build") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm";
  return "bun scripts/cli.ts platform-infra sub2api codex-pool validate";
}

export interface RemoteCodexPoolJobStatus {
  status: "running" | "succeeded" | "failed" | "unknown";
  exitCode: number | null;
  stdout: string | null;
  stderr: string | null;
  stdoutBytes: number | null;
  stderrBytes: number | null;
}

export function normalizeRemoteJobStatus(parsed: Record<string, unknown> | null): RemoteCodexPoolJobStatus | null {
  if (parsed === null) return null;
  const status = parsed.status === "running" || parsed.status === "succeeded" || parsed.status === "failed" || parsed.status === "unknown"
    ? parsed.status
    : "unknown";
  return {
    status,
    exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : null,
    stdout: typeof parsed.stdout === "string" ? parsed.stdout : null,
    stderr: typeof parsed.stderr === "string" ? parsed.stderr : null,
    stdoutBytes: typeof parsed.stdoutBytes === "number" ? parsed.stdoutBytes : null,
    stderrBytes: typeof parsed.stderrBytes === "number" ? parsed.stderrBytes : null,
  };
}

export function remoteJobStartScript(jobName: string, script: string): string {
  const scriptB64 = Buffer.from(script, "utf8").toString("base64");
  return `
set -eu
job=${shQuote(jobName)}
dir=${shQuote(remoteJobDir)}
mkdir -p "$dir"
chmod 700 "$dir"
script_path="$dir/$job.sh"
stdout_path="$dir/$job.stdout"
stderr_path="$dir/$job.stderr"
exit_path="$dir/$job.exit"
done_path="$dir/$job.done"
pid_path="$dir/$job.pid"
rm -f "$script_path" "$stdout_path" "$stderr_path" "$exit_path" "$done_path" "$pid_path"
base64 -d > "$script_path" <<'UNIDESK_REMOTE_SCRIPT_B64'
${scriptB64}
UNIDESK_REMOTE_SCRIPT_B64
chmod 700 "$script_path"
(
  set +e
  trap '' HUP
  sh "$script_path" > "$stdout_path" 2> "$stderr_path"
  code=$?
  printf '%s' "$code" > "$exit_path"
  date -u +%Y-%m-%dT%H:%M:%SZ > "$done_path"
  rm -f "$script_path"
) >/dev/null 2>&1 &
pid=$!
printf '%s' "$pid" > "$pid_path"
python3 - <<PY
import json
print(json.dumps({"ok": True, "jobName": ${JSON.stringify(jobName)}, "pid": int("${"${pid}"}"), "dir": ${JSON.stringify(remoteJobDir)}}))
PY
`;
}

export function remoteJobStatusScript(jobName: string): string {
  return `
set -eu
python3 - <<'PY'
import json
import os
import signal

job = ${JSON.stringify(jobName)}
directory = ${JSON.stringify(remoteJobDir)}
stdout_path = os.path.join(directory, job + ".stdout")
stderr_path = os.path.join(directory, job + ".stderr")
exit_path = os.path.join(directory, job + ".exit")
done_path = os.path.join(directory, job + ".done")
pid_path = os.path.join(directory, job + ".pid")

def read_text(path, limit=None):
    try:
        with open(path, "rb") as handle:
            data = handle.read()
    except FileNotFoundError:
        return None
    if limit is not None and len(data) > limit:
        data = data[-limit:]
    return data.decode("utf-8", errors="replace")

def file_size(path):
    try:
        return os.path.getsize(path)
    except FileNotFoundError:
        return 0

pid_text = read_text(pid_path)
done = os.path.exists(done_path)
exit_text = read_text(exit_path)
exit_code = None
if exit_text is not None:
    try:
        exit_code = int(exit_text.strip())
    except ValueError:
        exit_code = None
running = False
if pid_text is not None and not done:
    try:
        os.kill(int(pid_text.strip()), 0)
        running = True
    except Exception:
        running = False
status = "succeeded" if done and exit_code == 0 else "failed" if done else "running" if running else "unknown"
print(json.dumps({
    "ok": True,
    "jobName": job,
    "status": status,
    "pid": int(pid_text.strip()) if pid_text and pid_text.strip().isdigit() else None,
    "exitCode": exit_code,
    "stdoutBytes": file_size(stdout_path),
    "stderrBytes": file_size(stderr_path),
    "stdout": read_text(stdout_path, 500000) if done else read_text(stdout_path, 12000),
    "stderr": read_text(stderr_path, 120000) if done else read_text(stderr_path, 12000),
    "doneAt": read_text(done_path),
}, ensure_ascii=False))
PY
`;
}

export function shQuote(value: string): string {
  return `'${value.replace(/'/gu, `'\\''`)}'`;
}

export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export function parseJsonOutput(stdout: string): Record<string, unknown> | null {
  const trimmed = stdout.trim();
  if (trimmed.length === 0) return null;
  const end = trimmed.lastIndexOf("}");
  if (end === -1) return null;
  const starts = [trimmed.indexOf("{")];
  for (let index = trimmed.lastIndexOf("\n{"); index !== -1; index = trimmed.lastIndexOf("\n{", index - 1)) {
    starts.push(index + 1);
  }
  for (const start of starts.filter((item) => item !== -1 && item < end).sort((left, right) => right - left)) {
    try {
      const parsed = JSON.parse(trimmed.slice(start, end + 1)) as unknown;
      return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
    } catch {
      continue;
    }
  }
  return null;
}

export function boolField(value: Record<string, unknown> | null, key: string, defaultValue: boolean): boolean {
  if (value === null) return defaultValue;
  const field = value[key];
  return typeof field === "boolean" ? field : defaultValue;
}

export function compactCapture(result: SshCaptureResult, options: { full?: boolean } = {}): Record<string, unknown> {
  const full = options.full ?? false;
  return {
    exitCode: result.exitCode,
    stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
    stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
    stdoutTail: full || result.exitCode !== 0 ? result.stdout.slice(-8000) : "",
    stderrTail: full || result.exitCode !== 0 ? result.stderr.slice(-4000) : "",
  };
}
