// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. utils module for scripts/src/agentrun.ts.

// Moved mechanically from scripts/src/agentrun.ts:7651-8200 for #903.

// SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0.
// Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI.
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { rootPath, type UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { applyLocalCaddyManagedSite } from "../pk01-caddy";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import { runRemoteSshCommandCapture } from "../remote";
import { startJob } from "../jobs";
import {
  AGENTRUN_CONFIG_PATH,
  agentRunLaneSummary,
  agentRunPipelineRunName,
  agentRunProviderCredentialRefs,
  resolveAgentRunLaneTarget,
  type AgentRunCancelLifecycleSpec,
  type AgentRunLaneSpec,
} from "../agentrun-lanes";
import {
  agentRunImageArtifact,
  placeholderAgentRunImage,
  renderedFilesDigest,
  renderedObjectsDigest,
  renderAgentRunControlPlaneManifests,
  renderAgentRunGitopsFiles,
  type AgentRunArtifactService,
} from "../agentrun-manifests";
import { sha256Fingerprint } from "../platform-infra-ops-library";

import type { TimedValue } from "./options";
import { status } from "./control-plane";
import { displayValue, truncateOneLine } from "./options";
import { renderedCliResult } from "./render";

export interface AgentRunResolvedAuth {
  source: "env" | "file";
  value: string;
}

export type AgentRunHttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

export type AgentRunFailureKind = "auth-missing" | "auth-failed" | "agentrun-connect-failed" | "agentrun-proxy-exec-failed" | "agentrun-manager-fetch-failed" | "agentrun-timeout" | "schema-mismatch" | "unsupported-version" | "validation-failed" | "not-found";

export type AgentRunRestCompatGroup = "queue" | "sessions" | "aipod-specs" | "aipods" | "runs" | "commands" | "runner";

export type AgentRunResourceVerb = "get" | "describe" | "events" | "logs" | "result" | "ack" | "cancel" | "dispatch" | "create" | "apply" | "send" | "explain";

export type AgentRunResourceKind = "task" | "run" | "command" | "runnerjob" | "session" | "aipodspec";

export type AgentRunOutputMode = "human" | "wide" | "name" | "json" | "yaml";

export interface AgentRunResourceRef {
  kind: AgentRunResourceKind;
  name: string;
}

export interface AgentRunResourceOptions {
  output: AgentRunOutputMode;
  full: boolean;
  raw: boolean;
  debug: boolean;
  limit: number;
  queue: string | null;
  state: string | null;
  unread: boolean;
  readerId: string;
  taskId: string | null;
  runId: string | null;
  commandId: string | null;
  sessionId: string | null;
  afterSeq: number | null;
  tail: number | null;
  fullText: boolean;
  reason: string | null;
  dryRun: boolean;
  file: string | null;
  aipod: string | null;
  idempotencyKey: string | null;
  promptStdin: boolean;
  node: string | null;
  lane: string | null;
  passthroughArgs: string[];
}

export interface AgentRunRestTargetOptions {
  node: string | null;
  lane: string | null;
}

export interface AgentRunRestCompatOptions extends AgentRunRestTargetOptions {
  output: AgentRunOutputMode;
  full: boolean;
  raw: boolean;
}

export interface AgentRunRestTarget {
  config: UniDeskConfig;
  configPath: string;
  spec: AgentRunLaneSpec;
}

export interface AgentRunSessionPolicyTarget {
  configPath: string;
  spec: AgentRunLaneSpec;
  source: "selected-lane" | "default-lane";
  transport: "direct-http" | "lane-k8s-service-proxy";
}

export type AgentRunBridgeCaptureBackend = "local-backend-core-broker" | "remote-frontend-websocket";

export interface LocalBackendCoreStatus {
  dockerExecutable: boolean;
  backendCoreContainer: boolean;
  error: string | null;
}

export interface AgentRunBridgeCapturePlan {
  backend: AgentRunBridgeCaptureBackend;
  route: string;
  reason: string;
  remoteHost: string | null;
  localBackendCore: LocalBackendCoreStatus;
}

export type AgentRunBridgeCaptureResult = SshCaptureResult & { bridgeExecution?: AgentRunBridgeCapturePlan };

export let localBackendCoreStatusCache: LocalBackendCoreStatus | null = null;

export async function capture(config: UniDeskConfig, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
  const plan = agentRunBridgeCapturePlan(config, target);
  if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) {
    return await captureRemote(config, plan, target, args);
  }
  const local = attachBridgeExecution(await runSshCommandCapture(config, target, args), plan);
  const fallbackHost = agentRunBridgeRemoteHost(config);
  if (local.exitCode !== 0 && fallbackHost !== null && bridgeExecutionFailureKind(local)?.degradedReason === "capture-backend-unavailable") {
    const fallbackPlan: AgentRunBridgeCapturePlan = {
      ...plan,
      backend: "remote-frontend-websocket",
      remoteHost: fallbackHost,
      reason: "local-capture-backend-unavailable",
    };
    return await captureRemote(config, fallbackPlan, target, args);
  }
  return local;
}

export async function captureRemote(config: UniDeskConfig, plan: AgentRunBridgeCapturePlan, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
  if (plan.remoteHost === null) return attachBridgeExecution(remoteBridgeCaptureFailure(new Error("remote host is not configured")), plan);
  try {
    return attachBridgeExecution(await runRemoteSshCommandCapture(config, plan.remoteHost, target, args), plan);
  } catch (error) {
    return attachBridgeExecution(remoteBridgeCaptureFailure(error), plan);
  }
}

export function remoteBridgeCaptureFailure(error: unknown): SshCaptureResult {
  const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
  return {
    exitCode: 255,
    stdout: "",
    stderr: `unidesk remote frontend ssh bridge failed: ${message}\n`,
  };
}

export function attachBridgeExecution(result: SshCaptureResult, plan: AgentRunBridgeCapturePlan): AgentRunBridgeCaptureResult {
  return { ...result, bridgeExecution: plan };
}

export function agentRunBridgeCapturePlan(config: UniDeskConfig, target: string, env: NodeJS.ProcessEnv = process.env): AgentRunBridgeCapturePlan {
  const localBackendCore = detectLocalBackendCoreStatus();
  const remoteHost = agentRunBridgeRemoteHost(config, env);
  const runnerEnv = isAgentRunRunnerEnvironment(env);
  if (runnerEnv && remoteHost !== null) {
    return { backend: "remote-frontend-websocket", route: target, reason: "runner-environment", remoteHost, localBackendCore };
  }
  if (!localBackendCore.backendCoreContainer && remoteHost !== null) {
    return { backend: "remote-frontend-websocket", route: target, reason: "local-backend-core-unavailable", remoteHost, localBackendCore };
  }
  return { backend: "local-backend-core-broker", route: target, reason: "main-server-local-backend-core", remoteHost: null, localBackendCore };
}

export function detectLocalBackendCoreStatus(): LocalBackendCoreStatus {
  if (localBackendCoreStatusCache !== null) return localBackendCoreStatusCache;
  const result = spawnSync("docker", ["ps", "--format", "{{.Names}}"], { encoding: "utf8", timeout: 2000 });
  if (result.error !== undefined) {
    localBackendCoreStatusCache = {
      dockerExecutable: false,
      backendCoreContainer: false,
      error: result.error.message,
    };
    return localBackendCoreStatusCache;
  }
  const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
  localBackendCoreStatusCache = {
    dockerExecutable: result.status === 0,
    backendCoreContainer: result.status === 0 && String(result.stdout ?? "").split(/\r?\n/u).includes("unidesk-backend-core"),
    error: result.status === 0 ? null : output || `docker ps exited ${result.status ?? "unknown"}`,
  };
  return localBackendCoreStatusCache;
}

export function isAgentRunRunnerEnvironment(env: NodeJS.ProcessEnv): boolean {
  return Boolean(
    env.AGENTRUN_BOOT_MODE
      || env.AGENTRUN_RUN_ID
      || env.AGENTRUN_K8S_JOB_NAME
      || env.CODE_QUEUE_SERVICE_ROLE
      || env.CODE_QUEUE_INSTANCE_ID
      || env.KUBERNETES_SERVICE_HOST,
  );
}

export function agentRunBridgeRemoteHost(config: UniDeskConfig, env: NodeJS.ProcessEnv = process.env): string | null {
  return normalizeRemoteHostHint(env.UNIDESK_MAIN_SERVER_IP)
    ?? normalizeRemoteHostHint(env.UNIDESK_MAIN_SERVER_HOST)
    ?? normalizeRemoteHostHint(env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST)
    ?? normalizeRemoteHostHint(config.network.publicHost);
}

export function normalizeRemoteHostHint(raw: string | undefined): string | null {
  const value = raw?.trim() ?? "";
  if (value.length === 0 || value === "localhost" || value === "127.0.0.1" || value === "::1") return null;
  return value.replace(/\/+$/u, "");
}

export function captureBridgeMetadata(result: SshCaptureResult): Record<string, unknown> | null {
  const bridgeExecution = (result as AgentRunBridgeCaptureResult).bridgeExecution;
  if (bridgeExecution === undefined) return null;
  return {
    backend: bridgeExecution.backend,
    route: bridgeExecution.route,
    reason: bridgeExecution.reason,
    remoteHost: bridgeExecution.remoteHost,
    localBackendCore: bridgeExecution.localBackendCore,
  };
}

export function bridgeExecutionFailureKind(result: SshCaptureResult): { degradedReason: string; failureKind: string; recoveryActions: string[] } | null {
  if (result.exitCode === 0) return null;
  const stderr = result.stderr;
  if (/No such container: unidesk-backend-core|failed to start broker|Executable not found.*"docker"|docker: not found|Cannot connect to the Docker daemon/iu.test(stderr)) {
    return {
      degradedReason: "capture-backend-unavailable",
      failureKind: "bridge-execution-environment",
      recoveryActions: [
        "Run the same command from a healthy UniDesk main-server CLI, or provide UNIDESK_MAIN_SERVER_IP/UNIDESK_MAIN_SERVER_HOST so the bridge can use the frontend WebSocket SSH backend.",
        "For AgentRun runner jobs, request the unidesk-ssh tool credential SecretRef instead of embedding secret values in prompts or payloads.",
        "After restoring the bridge backend, retry the same bun scripts/cli.ts agentrun ... command; do not use direct kubectl or GitHub API bypasses.",
      ],
    };
  }
  if (/frontend login failed|remote frontend ssh bridge|websocket error|timed out waiting for provider session|route-not-allowed/iu.test(stderr)) {
    return {
      degradedReason: "bridge-execution-environment-unavailable",
      failureKind: "bridge-execution-environment",
      recoveryActions: [
        "Verify the UniDesk frontend/backend-core SSH bridge is reachable from this runner and that UNIDESK_MAIN_SERVER_IP or UNIDESK_MAIN_SERVER_HOST points at the main server.",
        "Verify scoped UNIDESK_SSH_CLIENT_TOKEN route allowlist includes the requested AgentRun route, without printing token values.",
        "Retry with the same bun scripts/cli.ts agentrun ... command after the controlled bridge path is restored.",
      ],
    };
  }
  return null;
}

export function compactCapture(result: SshCaptureResult, options: { full?: boolean; stdoutTailChars?: number; stderrTailChars?: number } = {}): Record<string, unknown> {
  const stdoutTailChars = options.stdoutTailChars ?? 8000;
  const stderrTailChars = options.stderrTailChars ?? 4000;
  const full = options.full ?? true;
  const payload: Record<string, unknown> = {
    exitCode: result.exitCode,
    stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
    stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
    stdoutTailOmitted: !full && result.exitCode === 0,
    stderrTailOmitted: !full && result.exitCode === 0,
  };
  const bridgeExecution = captureBridgeMetadata(result);
  if (bridgeExecution !== null) payload.bridgeExecution = bridgeExecution;
  const failureKind = bridgeExecutionFailureKind(result);
  if (failureKind !== null) {
    payload.degradedReason = failureKind.degradedReason;
    payload.failureKind = failureKind.failureKind;
    payload.recoveryActions = failureKind.recoveryActions;
  }
  if (full || result.exitCode !== 0) {
    payload.stdoutTail = tail(result.stdout, stdoutTailChars);
    payload.stderrTail = tail(result.stderr, stderrTailChars);
    payload.stdoutTruncated = result.stdout.length > stdoutTailChars;
    payload.stderrTruncated = result.stderr.length > stderrTailChars;
  }
  return payload;
}

export async function timedStatusStage<T>(stage: string, action: () => Promise<T>): Promise<TimedValue<T>> {
  const startedAtMs = Date.now();
  progressEvent("agentrun.control-plane.status.progress", { stage, status: "started" });
  try {
    const value = await action();
    const exitCode = isCaptureResult(value) ? value.exitCode : typeof record(value).ok === "boolean" && record(value).ok !== true ? 1 : 0;
    progressEvent("agentrun.control-plane.status.progress", {
      stage,
      status: exitCode === 0 ? "succeeded" : "failed",
      exitCode,
      elapsedMs: Date.now() - startedAtMs,
    });
    return { value, elapsedMs: Date.now() - startedAtMs };
  } catch (error) {
    progressEvent("agentrun.control-plane.status.progress", {
      stage,
      status: "failed",
      elapsedMs: Date.now() - startedAtMs,
      error: error instanceof Error ? error.message : String(error),
    });
    throw error;
  }
}

export function progressEvent(event: string, payload: Record<string, unknown>): void {
  const stage = displayValue(payload.stage ?? event);
  const status = displayValue(payload.status ?? "-");
  const details = [
    payload.exitCode === undefined ? null : `exit=${displayValue(payload.exitCode)}`,
    payload.elapsedMs === undefined ? null : `elapsed=${displayValue(payload.elapsedMs)}ms`,
    payload.error === undefined ? null : `error=${truncateOneLine(displayValue(payload.error), 160)}`,
  ].filter((item): item is string => item !== null);
  process.stderr.write(`status ${stage} ${status}${details.length === 0 ? "" : ` ${details.join(" ")}`}\n`);
}

export function isCaptureResult(value: unknown): value is SshCaptureResult {
  return typeof value === "object" && value !== null && "exitCode" in value && typeof (value as { exitCode?: unknown }).exitCode === "number";
}

export function isAgentRunPipelineRunName(value: string): boolean {
  return /^agentrun-[a-z0-9-]*ci-[0-9a-f]{12,40}(?:-[a-z0-9-]+)?$/u.test(value);
}

export function captureJsonPayload(result: SshCaptureResult): Record<string, unknown> {
  const trimmed = result.stdout.trim();
  if (trimmed.length === 0) return {};
  try {
    return record(JSON.parse(trimmed) as unknown);
  } catch {
    const lastJsonLine = trimmed.split(/\r?\n/u).reverse().find((line) => line.trim().startsWith("{") && line.trim().endsWith("}"));
    if (lastJsonLine === undefined) return {};
    try {
      return record(JSON.parse(lastJsonLine) as unknown);
    } catch {
      return {};
    }
  }
}

export function record(value: unknown): Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

export function stringOrNull(value: unknown): string | null {
  return typeof value === "string" && value.length > 0 ? value : null;
}

export function nonNegativeIntegerOrNull(value: unknown): number | null {
  if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
  if (typeof value === "string" && /^\d+$/u.test(value)) return Number(value);
  return null;
}

export function isGitSha(value: string): boolean {
  return /^[0-9a-f]{40}$/u.test(value);
}

export function tail(text: string, maxChars: number): string {
  return text.length > maxChars ? text.slice(-maxChars) : text;
}

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

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

export function unsupported(args: string[]): RenderedCliResult {
  const command = `agentrun ${args.join(" ")}`.trim();
  return renderedCliResult(false, command, [
    `Error: unsupported AgentRun command: ${command}`,
    "",
    "Supported resource commands:",
    "  agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send",
    "",
    "Compatibility bridge groups:",
    "  agentrun aipod-specs|queue|runs|commands|runner|sessions",
    "",
    "Operations:",
    "  agentrun control-plane status|trigger-current|refresh",
    "  agentrun git-mirror status|sync|flush",
  ].join("\n"));
}
