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

// Moved mechanically from scripts/src/agentrun.ts:2747-3186 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 { AgentRunPublicExposure } from "./config";
import type { ConfirmOptions, LaneConfirmOptions, SecretSyncOptions, TriggerOptions } from "./options";
import { triggerCurrentYamlLaneConfirmed } from "./cleanup-scripts";
import { readAgentRunClientConfig } from "./config";
import { displayValue } from "./options";
import { pathValue } from "./render";
import { startAsyncAgentRunJob } from "./rest-bridge";
import { collectLaneSecretSources, readSecretSourceValue, restartYamlLaneScript, secretSyncScript } from "./secrets";
import { capture, captureJsonPayload, compactCapture, isGitSha, stringOrNull } from "./utils";
import { yamlLaneSourceBootstrapProbeScript } from "./yaml-lane";

export function renderNextObjectLines(next: Record<string, unknown>): string[] {
  return Object.values(next)
    .filter((value): value is string => typeof value === "string" && value.length > 0)
    .slice(0, 5)
    .map((line) => `  ${line}`);
}

export function yesNo(value: unknown): string {
  if (value === true) return "yes";
  if (value === false) return "no";
  if (value === null || value === undefined) return "-";
  return displayValue(value);
}

export function shortSha(value: unknown): string {
  const text = displayValue(value);
  if (/^[0-9a-f]{40}$/u.test(text)) return text.slice(0, 12);
  return text;
}

export function compactLaneSecretsStatus(secrets: Record<string, unknown>): Record<string, unknown> {
  const items = Array.isArray(secrets.items) ? secrets.items.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
  const missing = items
    .filter((item) => item.present !== true || item.keyPresent !== true)
    .map((item) => ({
      namespace: item.namespace ?? null,
      name: item.name ?? null,
      key: item.key ?? null,
      present: item.present ?? false,
      keyPresent: item.keyPresent ?? false,
    }));
  return {
    ready: secrets.ready ?? false,
    count: secrets.count ?? items.length,
    missingCount: missing.length,
    missing,
    valuesPrinted: false,
  };
}

export function compactAgentRunLaneStatusTarget(spec: AgentRunLaneSpec): Record<string, unknown> {
  return {
    node: {
      id: spec.nodeId,
      route: spec.nodeRoute,
      kubeRoute: spec.nodeKubeRoute,
    },
    lane: spec.lane,
    version: spec.version,
    source: {
      repository: spec.source.repository,
      branch: spec.source.branch,
    },
    runtime: {
      namespace: spec.runtime.namespace,
      managerDeployment: spec.runtime.managerDeployment,
      managerService: spec.runtime.managerService,
    },
    ci: {
      namespace: spec.ci.namespace,
      pipeline: spec.ci.pipeline,
      pipelineRunPrefix: spec.ci.pipelineRunPrefix,
    },
    gitops: {
      branch: spec.gitops.branch,
      argoApplication: spec.gitops.argoApplication,
    },
    gitMirror: {
      namespace: spec.gitMirror.namespace,
      readService: spec.gitMirror.readService,
      writeService: spec.gitMirror.writeService,
      repositoryCount: spec.gitMirror.repositories.length,
    },
    database: {
      mode: spec.database.mode,
      provider: spec.database.provider,
      configRef: spec.database.configRef,
      localPostgresExpectedAbsent: spec.database.localPostgresExpectedAbsent,
      valuesPrinted: false,
    },
    secretCount: spec.secrets.length,
    valuesPrinted: false,
  };
}

export async function restartYamlLane(config: UniDeskConfig, options: LaneConfirmOptions): Promise<Record<string, unknown>> {
  const { configPath, spec } = resolveAgentRunLaneTarget(options);
  const plan = {
    node: spec.nodeId,
    kubeRoute: spec.nodeKubeRoute,
    lane: spec.lane,
    namespace: spec.runtime.namespace,
    deployment: spec.runtime.managerDeployment,
    annotation: "kubectl.kubernetes.io/restartedAt",
    reason: "reload-lane-secrets-or-runtime-env",
    valuesPrinted: false,
  };
  if (options.dryRun || !options.confirm) {
    return {
      ok: true,
      command: "agentrun control-plane restart",
      mode: "dry-run",
      mutation: false,
      configPath,
      target: agentRunLaneSummary(spec),
      plan,
      next: {
        confirm: `bun scripts/cli.ts agentrun control-plane restart --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
        status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
      },
      valuesPrinted: false,
    };
  }
  const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", restartYamlLaneScript(spec)]);
  const payload = captureJsonPayload(result);
  return {
    ok: result.exitCode === 0 && payload.ok !== false,
    command: "agentrun control-plane restart",
    mode: "confirmed-rollout-restart",
    mutation: true,
    configPath,
    target: agentRunLaneSummary(spec),
    plan,
    result: payload,
    capture: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 4000, stderrTailChars: 4000 }),
    next: {
      status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
    },
    valuesPrinted: false,
  };
}

export async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Promise<Record<string, unknown>> {
  const { configPath, spec } = resolveAgentRunLaneTarget(options);
  const sources = collectLaneSecretSources(spec);
  if (sources.length === 0) {
    return {
      ok: false,
      command: "agentrun control-plane secret-sync",
      mode: "yaml-declared-node-lane",
      configPath,
      target: agentRunLaneSummary(spec),
      degradedReason: "lane-secret-sources-not-declared",
      valuesPrinted: false,
    };
  }
  const values = sources.map((source) => ({ spec: source, value: readSecretSourceValue(spec, source) }));
  const plan = values.map(({ spec: item, value }) => ({
    id: item.id,
    namespace: item.targetRef.namespace,
    secret: item.targetRef.name,
    key: item.targetRef.key,
    sourceRef: item.sourceRef,
    sourceMode: item.sourceMode,
    sourceKey: item.sourceKey,
    sourcePath: value.redactedPath,
    fingerprint: value.fingerprint,
    valueBytes: value.valueBytes,
    valuesPrinted: false,
  }));
  if (options.dryRun || !options.confirm) {
    return {
      ok: true,
      command: "agentrun control-plane secret-sync",
      mode: "dry-run",
      mutation: false,
      configPath,
      target: agentRunLaneSummary(spec),
      plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
      next: {
        confirm: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
        status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
      },
      valuesPrinted: false,
    };
  }
  const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", secretSyncScript(spec, values.map(({ spec: item, value }) => ({ targetRef: item.targetRef, value: value.value })))]);
  const payload = captureJsonPayload(result);
  return {
    ok: result.exitCode === 0 && payload.ok !== false,
    command: "agentrun control-plane secret-sync",
    mode: "confirmed-sync",
    mutation: true,
    configPath,
    target: agentRunLaneSummary(spec),
    plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
    result: payload,
    capture: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
    next: {
      status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
    },
    valuesPrinted: false,
  };
}

export async function exposeAgentRun(_config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
  const clientConfig = readAgentRunClientConfig();
  const exposure = clientConfig.publicExposure;
  if (!exposure?.enabled) {
    return {
      ok: true,
      command: "agentrun control-plane expose",
      action: "disabled-by-yaml",
      publicExposure: exposureSummary(exposure),
      valuesPrinted: false,
    };
  }
  if (options.dryRun) {
    return {
      ok: true,
      command: "agentrun control-plane expose",
      dryRun: true,
      mutation: false,
      publicExposure: exposureSummary(exposure),
      plan: {
        masterFrps: {
          action: "ensure-allow-port",
          configPath: exposure.masterFrps.configPath,
          containerName: exposure.masterFrps.containerName,
          remotePort: exposure.remotePort,
        },
        masterCaddy: {
          action: "ensure-https-vhost",
          domain: exposure.masterCaddy.domain,
          configPath: exposure.masterCaddy.configPath,
          serviceName: exposure.masterCaddy.serviceName,
          upstreamBaseUrl: exposure.masterCaddy.upstreamBaseUrl,
          responseHeaderTimeoutSeconds: exposure.masterCaddy.responseHeaderTimeoutSeconds,
        },
      },
      next: { confirm: "bun scripts/cli.ts agentrun control-plane expose --confirm" },
      valuesPrinted: false,
    };
  }
  const masterFrps = applyAgentRunMasterFrpsAllowPort(exposure);
  const masterCaddy = applyAgentRunMasterCaddySite(exposure);
  const ok = masterFrps.ok === true && masterCaddy.ok === true;
  return {
    ok,
    command: "agentrun control-plane expose",
    mutation: true,
    publicExposure: exposureSummary(exposure),
    masterFrps,
    masterCaddy,
    validation: {
      publicProbe: `curl -fsS ${exposure.publicBaseUrl.replace(/\/+$/u, "")}/health/readiness`,
      directClient: "bun scripts/cli.ts agentrun get tasks --queue commander -o json",
      valuesPrinted: false,
    },
    valuesPrinted: false,
  };
}

export function exposureSummary(exposure: AgentRunPublicExposure | null | undefined): Record<string, unknown> {
  if (!exposure) return { enabled: false, valuesPrinted: false };
  return {
    enabled: exposure.enabled,
    proxyName: exposure.proxyName,
    remotePort: exposure.remotePort,
    publicBaseUrl: exposure.publicBaseUrl,
    masterBaseUrl: exposure.masterBaseUrl,
    frps: {
      configPath: exposure.masterFrps.configPath,
      containerName: exposure.masterFrps.containerName,
    },
    caddy: {
      enabled: exposure.masterCaddy.enabled,
      domain: exposure.masterCaddy.domain,
      configPath: exposure.masterCaddy.configPath,
      serviceName: exposure.masterCaddy.serviceName,
      upstreamBaseUrl: exposure.masterCaddy.upstreamBaseUrl,
      responseHeaderTimeoutSeconds: exposure.masterCaddy.responseHeaderTimeoutSeconds,
    },
    valuesPrinted: false,
  };
}

export function applyAgentRunMasterFrpsAllowPort(exposure: AgentRunPublicExposure): Record<string, unknown> {
  const pathValue = exposure.masterFrps.configPath;
  const port = exposure.remotePort;
  const container = exposure.masterFrps.containerName;
  if (!existsSync(pathValue)) return { ok: false, error: "master-frps-config-missing", path: pathValue, valuesPrinted: false };
  const before = readFileSync(pathValue, "utf8");
  const alreadyAllowed = frpsAllowPortExists(before, port);
  let backupPath: string | null = null;
  let action = "kept-existing";
  if (!alreadyAllowed) {
    const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
    backupPath = `${pathValue}.bak-agentrun-${stamp}`;
    copyFileSync(pathValue, backupPath);
    writeFileSync(pathValue, `${before.replace(/\s*$/u, "")}\n\n[[allowPorts]]\nstart = ${port}\nend = ${port}\n`, "utf8");
    chmodSync(pathValue, statSync(backupPath).mode & 0o777);
    action = "added-allow-port";
  }
  const restart = alreadyAllowed ? null : spawnSync("docker", ["restart", container], { encoding: "utf8" });
  const inspect = spawnSync("docker", ["inspect", "-f", "{{.State.Running}}", container], { encoding: "utf8" });
  const ok = alreadyAllowed || (restart?.status === 0 && inspect.status === 0 && String(inspect.stdout).trim() === "true");
  return {
    ok,
    action,
    path: pathValue,
    backupPath,
    remotePort: port,
    containerName: container,
    restart: restart === null ? null : { exitCode: restart.status, stdoutTail: String(restart.stdout).slice(-1000), stderrTail: String(restart.stderr).slice(-1000) },
    inspect: { exitCode: inspect.status, running: String(inspect.stdout).trim() === "true", stderrTail: String(inspect.stderr).slice(-1000) },
    valuesPrinted: false,
  };
}

export function applyAgentRunMasterCaddySite(exposure: AgentRunPublicExposure): Record<string, unknown> {
  const caddy = exposure.masterCaddy;
  if (!caddy.enabled) return { ok: true, action: "disabled-by-yaml", valuesPrinted: false };
  const pathValue = caddy.configPath;
  if (!existsSync(pathValue)) return { ok: false, error: "master-caddy-config-missing", path: pathValue, valuesPrinted: false };
  const desiredBlock = renderAgentRunCaddySiteBlock(caddy.domain, caddy.upstreamBaseUrl, caddy.responseHeaderTimeoutSeconds);
  const applied = applyLocalCaddyManagedSite({
    serviceId: "agentrun",
    markerName: "agentrun",
    configPath: pathValue,
    serviceName: caddy.serviceName,
    siteBlock: desiredBlock,
    legacySiteDomain: caddy.domain,
  });
  return {
    ...applied,
    domain: caddy.domain,
    upstreamBaseUrl: caddy.upstreamBaseUrl,
    responseHeaderTimeoutSeconds: caddy.responseHeaderTimeoutSeconds,
    valuesPrinted: false,
  };
}

export function renderAgentRunCaddySiteBlock(domain: string, upstreamBaseUrl: string, responseHeaderTimeoutSeconds: number): string {
  const upstream = new URL(upstreamBaseUrl);
  const upstreamHost = `${upstream.hostname}${upstream.port ? `:${upstream.port}` : ""}`;
  return `${domain} {
    encode zstd gzip
    reverse_proxy ${upstreamHost} {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        transport http {
            dial_timeout 5s
            response_header_timeout ${responseHeaderTimeoutSeconds}s
        }
    }
}`;
}

export function frpsAllowPortExists(toml: string, port: number): boolean {
  const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
  return sections.some((section) => {
    if (!section.includes("[[allowPorts]]")) return false;
    const start = section.match(/^\s*start\s*=\s*(\d+)\s*$/mu);
    const end = section.match(/^\s*end\s*=\s*(\d+)\s*$/mu);
    return Number(start?.[1]) === port && Number(end?.[1]) === port;
  });
}

export async function triggerCurrent(config: UniDeskConfig, options: TriggerOptions): Promise<Record<string, unknown>> {
  return await triggerCurrentYamlLane(config, options, resolveAgentRunLaneTarget(options));
}

export async function triggerCurrentYamlLane(config: UniDeskConfig, options: TriggerOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
  const spec = target.spec;
  const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapProbeScript(spec)]);
  const source = captureJsonPayload(probe);
  const sourceCommit = stringOrNull(source.sourceCommit);
  const remoteBranchExists = source.remoteBranchExists === true;
  const pipelineRun = sourceCommit !== null && isGitSha(sourceCommit) ? agentRunPipelineRunName(spec, sourceCommit) : null;
  const placeholderImage = sourceCommit === null ? null : placeholderAgentRunImage(spec, sourceCommit);
  const renderedFiles = placeholderImage === null ? [] : renderAgentRunGitopsFiles(spec, { sourceCommit, image: placeholderImage });
  const plan = {
    node: spec.nodeId,
    lane: spec.lane,
    version: spec.version,
    source: {
      workspace: spec.source.workspace,
      remote: spec.source.remote,
      branch: spec.source.branch,
      bootstrapFromBranch: spec.source.bootstrapFromBranch,
      bootstrapTimeoutSeconds: spec.source.bootstrapTimeoutSeconds,
      bootstrapPollSeconds: spec.source.bootstrapPollSeconds,
      remoteBranchExists,
      sourceCommit,
    },
    deploymentFormat: spec.deployment.format,
    deploymentTruth: "config/agentrun.yaml",
    removedServiceDeployJson: true,
    pipelineRun,
    imageBuild: {
      repository: `${spec.ci.registryPrefix}/${spec.deployment.manager.imageBuild.repository}`,
      containerfile: spec.deployment.manager.imageBuild.containerfile,
      buildArgNames: Object.keys(spec.deployment.manager.imageBuild.buildArgs).sort(),
      timeoutSeconds: spec.deployment.manager.imageBuild.timeoutSeconds,
      pollSeconds: spec.deployment.manager.imageBuild.pollSeconds,
      proxyConfigured: spec.deployment.manager.imageBuild.httpProxy !== null || spec.deployment.manager.imageBuild.httpsProxy !== null,
    },
    gitops: {
      branch: spec.gitops.branch,
      path: spec.gitops.path,
      renderedFileCount: renderedFiles.length,
      renderedFilesDigest: renderedFiles.length === 0 ? null : renderedFilesDigest(renderedFiles),
    },
    valuesPrinted: false,
  };
  if (options.dryRun || !options.confirm) {
    return {
      ok: true,
      command: "agentrun control-plane trigger-current",
      mode: "dry-run",
      mutation: false,
      configPath: target.configPath,
      target: agentRunLaneSummary(spec),
      plan,
      sourceProbe: compactCapture(probe, { full: probe.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
      next: {
        confirm: `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
        status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
      },
      valuesPrinted: false,
    };
  }
  if (options.wait) {
    return await triggerCurrentYamlLaneConfirmed(config, spec, target.configPath, true);
  }
  return startAsyncAgentRunJob(
    `agentrun_${spec.lane}_trigger_current`,
    ["bun", "scripts/cli.ts", "agentrun", "control-plane", "trigger-current", "--node", spec.nodeId, "--lane", spec.lane, "--confirm", "--wait"],
    `Run AgentRun ${spec.version} YAML-only trigger-current on ${spec.nodeId}`,
  );
}
