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

// Moved mechanically from scripts/src/agentrun.ts:1653-2021 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 { AgentRunResourceRef, AgentRunRestTargetOptions } from "./utils";
import { renderAgentRunSessionPolicyExplanation } from "./config";
import { optionValue, parseConfirmOptions, validateOptions } from "./control-plane";
import { firstKnownString, parseResourceKind } from "./render";
import { isAgentRunPipelineRunName, isGitSha, record, stringOrNull } from "./utils";

export function renderFailureLines(value: Record<string, unknown>): string[] {
  const terminalClassification = record(value.terminalClassification);
  const nestedTerminalClassification = Object.keys(terminalClassification).length > 0
    ? terminalClassification
    : record(record(value.liveness).terminalClassification);
  const failureKind = stringOrNull(value.failureKind)
    ?? stringOrNull(nestedTerminalClassification.failureKind)
    ?? stringOrNull(value.degradedReason);
  const failureMessage = stringOrNull(value.failureMessage)
    ?? stringOrNull(nestedTerminalClassification.failureMessage)
    ?? stringOrNull(value.message)
    ?? stringOrNull(value.reason);
  const category = stringOrNull(nestedTerminalClassification.category);
  const timeoutKind = stringOrNull(nestedTerminalClassification.timeoutKind);
  const timeoutState = stringOrNull(nestedTerminalClassification.timeoutState);
  const providerInterruption = stringOrNull(nestedTerminalClassification.providerInterruption);
  const lastActivity = stringOrNull(nestedTerminalClassification.lastActivityKind);
  const lastActivitySeq = nestedTerminalClassification.lastActivitySeq;
  const lines: string[] = [];
  if (failureKind !== null) lines.push(`Failure: ${failureKind}`);
  if (failureMessage !== null) lines.push(`Message: ${failureMessage}`);
  if (category !== null) lines.push(`Category: ${category}`);
  if (timeoutKind !== null || timeoutState !== null) lines.push(`Timeout: ${displayValue(timeoutKind)} / ${displayValue(timeoutState)}`);
  if (providerInterruption !== null) lines.push(`Provider: ${providerInterruption}`);
  if (lastActivity !== null) lines.push(`LastActivity: ${lastActivity}${lastActivitySeq === undefined ? "" : ` seq=${String(lastActivitySeq)}`}`);
  return lines;
}

export function renderResourceNextLines(ref: AgentRunResourceRef, value: Record<string, unknown>): string[] {
  const supervisor = record(value.supervisor);
  const runId = firstKnownString(value.runId, value.activeRunId, value.lastRunId, supervisor.runId, supervisor.activeRunId, supervisor.lastRunId);
  const commandId = firstKnownString(value.commandId, value.activeCommandId, value.lastCommandId, supervisor.commandId, supervisor.activeCommandId, supervisor.lastCommandId);
  const runnerJobId = stringOrNull(value.runnerJobId) ?? (ref.kind === "runnerjob" ? ref.name : null);
  const sessionId = firstKnownString(value.sessionId, supervisor.sessionId);
  const lines = [
    runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
    sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`,
    runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`,
    runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun describe command/${commandId} --run ${runId} --full`,
    runId === null || runnerJobId === null ? null : `bun scripts/cli.ts agentrun describe runnerjob/${runnerJobId} --run ${runId} --full`,
    ref.kind !== "session" || sessionId === null ? null : `bun scripts/cli.ts agentrun send session/${sessionId} --prompt-stdin`,
  ];
  const unique: string[] = [];
  for (const line of lines) {
    if (line !== null && !unique.includes(line)) unique.push(line);
  }
  return unique.slice(0, 5);
}

export function parseTaskManifest(raw: string, source: string): Record<string, unknown> {
  const parsed = source.endsWith(".json") ? JSON.parse(raw) as unknown : Bun.YAML.parse(raw) as unknown;
  const input = record(parsed);
  const kind = stringOrNull(input.kind);
  if (kind !== null && kind.toLowerCase() !== "task") throw new Error("apply currently supports kind: Task only");
  const spec = record(input.spec);
  const payload = Object.keys(spec).length > 0 ? spec : input;
  if (!isRecord(payload.payload) && !isRecord(payload.executionPolicy) && stringOrNull(payload.title) === null) {
    throw new Error("task manifest must contain spec with an AgentRun task payload");
  }
  return payload;
}

export function nextPagedResourceCommand(command: string, nextAfterSeq: string, limit: number): string {
  const parts = command
    .replace(/\s+--after-seq\s+\S+/gu, "")
    .replace(/\s+--tail(?:\s+\S+)?/gu, "")
    .replace(/\s+--limit\s+\S+/gu, "")
    .trim();
  return `${parts} --after-seq ${nextAfterSeq} --limit ${limit}`;
}

export function agentRunExplain(kindRaw: string, args: string[] = [], options: AgentRunRestTargetOptions = { node: null, lane: null }): string {
  if (kindRaw === "session-policy" || kindRaw === "provider-policy") return renderAgentRunSessionPolicyExplanation(args, options);
  const kind = parseResourceKind(kindRaw);
  if (kind === "task") {
    return [
      "KIND: task",
      "FIELDS:",
      "  metadata.name: optional client name/idempotency hint",
      "  spec.tenantId, spec.projectId, spec.queue, spec.title",
      "  spec.payload.prompt or spec.payload.<structured fields>",
      "  spec.executionPolicy, spec.resourceBundleRef, spec.workspaceRef",
      "",
      "Apply example:",
      "  kind: Task",
      "  spec:",
      "    tenantId: unidesk",
      "    projectId: pikasTech/unidesk",
      "    queue: commander",
      "    title: Example",
      "    payload:",
      "      prompt: Do the work",
    ].join("\n");
  }
  return `KIND: ${kindRaw}\nUse get/describe with -o json to inspect the current stable UniDesk resource schema.`;
}

export function arrayRecords(value: unknown): Record<string, unknown>[] {
  if (Array.isArray(value)) return value.map(record).filter((item) => Object.keys(item).length > 0);
  if (typeof value === "object" && value !== null && Array.isArray((value as { items?: unknown }).items)) return arrayRecords((value as { items: unknown }).items);
  return [];
}

export function renderTable(headers: string[], rows: string[][]): string {
  const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
  const line = (values: string[]): string => values.map((value, index) => value.padEnd(widths[index] ?? value.length)).join("  ").trimEnd();
  return [line(headers), ...rows.map(line)].join("\n");
}

export function resourceName(item: Record<string, unknown>): string {
  const kind = String(item.kind ?? "resource");
  return `${kind}/${shortId(displayValue(item.name))}`;
}

export function shortId(value: string): string {
  if (value === "-" || value.length <= 18) return value;
  const known = ["qt_", "run_", "cmd_", "rjob_"];
  const prefix = known.find((item) => value.startsWith(item));
  if (prefix !== undefined) return `${prefix}${value.slice(prefix.length, prefix.length + 8)}...`;
  if (/^\w+-\d+/u.test(value)) return value.length > 28 ? `${value.slice(0, 25)}...` : value;
  return `${value.slice(0, 14)}...`;
}

export function displayValue(value: unknown): string {
  if (value === undefined || value === null || value === "") return "-";
  return String(value);
}

export function stringOrDash(value: unknown): string {
  return displayValue(value);
}

export function truncateOneLine(value: string, maxChars: number): string {
  const oneLine = value.replace(/\s+/gu, " ").trim();
  return oneLine.length <= maxChars ? oneLine : `${oneLine.slice(0, Math.max(0, maxChars - 3))}...`;
}

export function truncateMultiline(value: string, maxChars: number): string {
  return value.length <= maxChars ? value : `${value.slice(0, maxChars)}\n...[truncated ${value.length - maxChars} chars; rerun with --full-text or --full]`;
}

export function relativeAge(iso: string | null): string {
  if (iso === null) return "-";
  const ms = Date.now() - Date.parse(iso);
  if (!Number.isFinite(ms) || ms < 0) return "-";
  const minutes = Math.floor(ms / 60000);
  if (minutes < 60) return `${minutes}m`;
  const hours = Math.floor(minutes / 60);
  if (hours < 48) return `${hours}h`;
  return `${Math.floor(hours / 24)}d`;
}

export function pickCompact(value: Record<string, unknown>, keys: string[]): Record<string, unknown> {
  const picked: Record<string, unknown> = {};
  for (const key of keys) {
    if (value[key] !== undefined) picked[key] = value[key];
  }
  return picked;
}

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

export interface TriggerOptions extends DisclosureOptions {
  confirm: boolean;
  dryRun: boolean;
  node: string | null;
  lane: string | null;
  wait: boolean;
}

export interface ConfirmOptions {
  confirm: boolean;
  dryRun: boolean;
}

export interface SecretSyncOptions extends ConfirmOptions, DisclosureOptions {
  node: string | null;
  lane: string | null;
  secretIds: string[];
  output: "human" | "json" | "yaml";
}

export interface LaneConfirmOptions extends ConfirmOptions, DisclosureOptions {
  node: string | null;
  lane: string | null;
}

export interface RefreshOptions extends ConfirmOptions, DisclosureOptions {
  node: string | null;
  lane: string | null;
}

export interface GitMirrorStatusOptions extends DisclosureOptions {
  node: string | null;
  lane: string | null;
}

export interface GitMirrorOptions extends ConfirmOptions {
  node: string | null;
  lane: string | null;
  timeoutSeconds: number;
  wait: boolean;
}

export interface CleanupRunnersOptions extends ConfirmOptions, DisclosureOptions {
  node: string | null;
  lane: string | null;
  timeoutSeconds: number;
}

export interface CleanupRunsOptions extends ConfirmOptions {
  node: string | null;
  lane: string | null;
  minAgeMinutes: number;
  limit: number;
  timeoutSeconds: number;
}

export interface CleanupReleasedPvOptions extends ConfirmOptions {
  node: string | null;
  lane: string | null;
  limit: number;
  timeoutSeconds: number;
}

export interface CleanupSessionPvcsOptions extends ConfirmOptions {
  node: string | null;
  lane: string | null;
  limit: number;
  timeoutSeconds: number;
}

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

export interface StatusOptions extends DisclosureOptions {
  node: string | null;
  lane: string | null;
  sourceCommit: string | null;
  pipelineRun: string | null;
  targetMode: "latest-source-head" | "source-commit" | "pipeline-run";
}

export interface TimedValue<T> {
  value: T;
  elapsedMs: number;
}

export function parseDisclosureOptions(args: string[]): DisclosureOptions {
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--node" || arg === "--lane") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      index += 1;
      continue;
    }
    if (arg !== "--full" && arg !== "--raw") throw new Error(`unsupported status option: ${arg}`);
  }
  const raw = args.includes("--raw");
  return { full: raw || args.includes("--full"), raw };
}

export function parseGitMirrorStatusOptions(args: string[]): GitMirrorStatusOptions {
  validateOptions(args, new Set(["--full", "--raw"]), new Set(["--node", "--lane"]));
  const raw = args.includes("--raw");
  return {
    full: raw || args.includes("--full"),
    raw,
    node: optionValue(args, "--node") ?? null,
    lane: optionValue(args, "--lane") ?? null,
  };
}

export function parseStatusOptions(args: string[]): StatusOptions {
  let node: string | null = null;
  let lane: string | null = null;
  let sourceCommit: string | null = null;
  let pipelineRun: string | null = null;
  let full = false;
  let raw = false;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--full") {
      full = true;
      continue;
    }
    if (arg === "--raw") {
      raw = true;
      full = true;
      continue;
    }
    if (arg === "--node") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
      node = value;
      index += 1;
      continue;
    }
    if (arg === "--lane") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
      lane = value;
      index += 1;
      continue;
    }
    if (arg === "--source-commit") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--source-commit requires a value");
      if (!isGitSha(value)) throw new Error("--source-commit must be a full 40-character git SHA");
      sourceCommit = value;
      index += 1;
      continue;
    }
    if (arg === "--pipeline-run") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--pipeline-run requires a value");
      if (!isAgentRunPipelineRunName(value)) throw new Error("--pipeline-run must be a YAML lane AgentRun PipelineRun name ending in ci-<12+ hex>");
      pipelineRun = value;
      index += 1;
      continue;
    }
    throw new Error(`unsupported status option: ${arg}`);
  }
  if (sourceCommit !== null && pipelineRun !== null) throw new Error("control-plane status accepts only one of --source-commit or --pipeline-run");
  return {
    full,
    raw,
    node,
    lane,
    sourceCommit,
    pipelineRun,
    targetMode: pipelineRun !== null ? "pipeline-run" : sourceCommit !== null ? "source-commit" : "latest-source-head",
  };
}

export function parseTriggerOptions(args: string[]): TriggerOptions {
  const base = parseConfirmOptions(args);
  let node: string | null = null;
  let lane: string | null = null;
  let wait = false;
  let full = false;
  let raw = false;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--confirm" || arg === "--dry-run") continue;
    if (arg === "--full") {
      full = true;
      continue;
    }
    if (arg === "--raw") {
      raw = true;
      full = true;
      continue;
    }
    if (arg === "--wait") {
      wait = true;
      continue;
    }
    if (arg === "--node") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
      node = value;
      index += 1;
      continue;
    }
    if (arg === "--lane") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
      lane = value;
      index += 1;
      continue;
    }
    throw new Error(`unsupported trigger-current option: ${arg}`);
  }
  return { ...base, node, lane, wait, full, raw };
}
