import { spawn, spawnSync } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { type UniDeskConfig, repoRoot } from "./config";
import {
  ApplyPatchV2Error,
  applyPatchV2RemoteCommand,
  decodeApplyPatchV2BulkRead,
  formatApplyPatchV2BulkReplacementPayload,
  runApplyPatchV2,
  type ApplyPatchV2BulkReplacementWritePlan,
  type ApplyPatchV2Executor,
  type ApplyPatchV2FileSystem,
  type ApplyPatchV2RemoteOperation,
} from "./apply-patch-v2";
import {
  isSshFileTransferOperation,
  runSshFileTransferOperation,
  type SshRemoteCommandExecutor,
  type SshRemoteCommandStreamHandlers,
} from "./ssh-file-transfer";
import { readTransSshBackendConfig, type TransSshBackendConfig } from "./trans-config";
import { readCliOutputPolicy } from "./output";
import { readHostK8sPublicHost } from "./host-k8s-config";
import {
  buildK3sTargetCommand,
  buildWindowsPowerShellInvocation,
  effectiveApplyPatchV2Invocation,
  normalizeSshOperationArgs,
  parseSshInvocation,
  powerShellSingleQuote,
  safeProviderId,
  shellArgv,
  shellQuote,
  sshRoutePayloadCwd,
  sshRouteSeparatorCompatibilityHint,
  wrapSshRemoteCommand,
  type ParsedSshArgs,
  type ParsedSshInvocation,
  type ParsedSshRoute,
  type SshCaptureBackend,
  type SshCaptureBackendPlan,
  type SshCaptureResult,
  type SshFailureHint,
  type SshInvocationKind,
  type SshRuntimeTimeoutHint,
  type SshRuntimeTimingHint,
  type SshStdoutTruncationHint,
  type SshStreamTruncationSummary,
  type SshTcpPoolFailureKind,
  type SshTcpPoolHint,
  type SshTruncationCompletionSummary,
} from "./ssh";

const defaultSshSlowWarningMs = 10_000;
const defaultSshRuntimeTimeoutMs = 60_000;
const maxSshRuntimeTimeoutMs = 60_000;
const maxExplicitSshCaptureRuntimeTimeoutMs = 61 * 60_000;
const defaultSshBackendCoreDetectTimeoutMs = 15_000;
const maxSshBackendCoreDetectTimeoutMs = 60_000;
const minSshStdoutStreamMaxBytes = 4 * 1024;
const maxSshStdoutStreamMaxBytes = 16 * 1024 * 1024;
function classifySshLikeFailure(exitCode: number, stderrText: string): SshFailureHint["trigger"] | null {
  const normalized = stderrText.toLowerCase();
  if (
    normalized.includes("kex_exchange_identification")
    || normalized.includes("ssh_exchange_identification")
    || normalized.includes("connection closed by remote host")
    || normalized.includes("connection reset by peer")
    || normalized.includes("connection timed out")
    || normalized.includes("operation timed out")
    || normalized.includes("timed out waiting for provider session")
    || normalized.includes("the operation was aborted")
  ) {
    return "timeout-or-kex";
  }
  return exitCode === 255 ? "exit-255" : null;
}

export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCode: number, stderrText: string): SshFailureHint | null {
  if (parsed.invocationKind !== "ssh-like") return null;
  const trigger = classifySshLikeFailure(exitCode, stderrText);
  if (trigger === null) return null;
  const shownProviderId = safeProviderId(providerId);
  return {
    code: "ssh-like-command-friction",
    providerId: shownProviderId,
    trigger,
    exitCode,
    message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or explicit sh/bash stdin passthrough for non-interactive commands.",
    try: `trans ${shownProviderId} sh <<'SH'`,
    triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error '<ssh-like timeout or kex failure>'`,
    note: "This hint intentionally does not echo the original remote command.",
  };
}

export function formatSshFailureHint(hint: SshFailureHint): string {
  return `UNIDESK_SSH_HINT ${JSON.stringify(hint)}\n`;
}

export function sshWindowsPlaneHint(invocation: ParsedSshInvocation, exitCode: number, stderrText: string): string {
  if (invocation.route.plane !== "host" || exitCode !== 127) return "";
  const workspace = invocation.route.workspace ?? "";
  const match = workspace.match(/^\/mnt\/([a-z])(?:\/(.*))?$/iu);
  if (match === null || !/command not found|not found/iu.test(stderrText)) return "";
  const drive = (match[1] ?? "c").toLowerCase();
  const suffix = (match[2] ?? "").replace(/^\/+|\/+$/gu, "");
  const windowsRoute = `${invocation.providerId}:win/${drive}${suffix.length > 0 ? `/${suffix}` : ""}`;
  return `UNIDESK_SSH_HINT ${JSON.stringify({
    code: "wsl-command-not-found-check-windows-plane",
    providerId: invocation.providerId,
    route: invocation.route.raw,
    exitCode,
    message: "The command was not found on the WSL/host plane; this does not prove it is absent from the same provider's Windows PATH.",
    try: `trans ${windowsRoute} cmd <command> ...`,
    windowsRoute,
  })}\n`;
}

export function sshWindowsPowerShellPipelineHint(invocation: ParsedSshInvocation, exitCode: number, stderrText: string): string {
  if (invocation.route.plane !== "win" || exitCode === 0 || !/An empty pipe element is not allowed/iu.test(stderrText)) return "";
  return `UNIDESK_SSH_HINT ${JSON.stringify({
    code: "windows-powershell-statement-pipeline-boundary",
    providerId: safeProviderId(invocation.providerId),
    route: invocation.route.raw,
    exitCode,
    message: "PowerShell cannot pipe a statement such as foreach directly at this syntax boundary.",
    action: "Wrap the statement in @(...), then pipe the resulting values, or use a multiline quoted PowerShell heredoc.",
    examples: ["@(foreach (...) { ... }) | Format-List", "trans <route> ps <<'PS'"],
    note: "This hint intentionally does not echo the submitted PowerShell source.",
  })}\n`;
}

export function sshSlowWarningThresholdMs(env: NodeJS.ProcessEnv = process.env): number {
  const raw = env.UNIDESK_SSH_SLOW_WARNING_MS;
  const parsed = raw === undefined ? NaN : Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshSlowWarningMs;
  return Math.max(1000, Math.trunc(parsed));
}

export function sshRuntimeTimingHint(options: {
  invocation: ParsedSshInvocation;
  transport: SshRuntimeTimingHint["transport"];
  exitCode: number;
  startedAtMs: number;
  finishedAtMs?: number;
  thresholdMs?: number;
}): SshRuntimeTimingHint {
  const finishedAtMs = options.finishedAtMs ?? Date.now();
  const thresholdMs = options.thresholdMs ?? sshSlowWarningThresholdMs();
  const elapsedMs = Math.max(0, Math.round(finishedAtMs - options.startedAtMs));
  const elapsedSeconds = Number((elapsedMs / 1000).toFixed(3));
  const slow = elapsedMs > thresholdMs;
  const thresholdSeconds = Number((thresholdMs / 1000).toFixed(3));
  return {
    code: "ssh-runtime-timing",
    level: slow ? "warning" : "info",
    providerId: safeProviderId(options.invocation.providerId),
    route: options.invocation.route.raw,
    transport: options.transport,
    invocationKind: options.invocation.parsed.invocationKind,
    exitCode: options.exitCode,
    elapsedMs,
    elapsedSeconds,
    thresholdMs,
    slow,
    message: slow
      ? `ssh operation took ${elapsedSeconds}s, above the ${thresholdSeconds}s warning threshold; consider checking provider/session latency, remote command cost, helper bootstrap, or tran/apply-patch optimization before repeating high-frequency operations.`
      : `ssh operation completed in ${elapsedSeconds}s.`,
    note: "Timing hint is written to stderr and intentionally does not echo the original remote command.",
  };
}

export function formatSshRuntimeTimingHint(hint: SshRuntimeTimingHint): string {
  if (!hint.slow) return "";
  return `UNIDESK_SSH_TIMING ${JSON.stringify(hint)}\n`;
}

export function sshRuntimeTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
  const raw = env.UNIDESK_SSH_RUNTIME_TIMEOUT_MS ?? env.UNIDESK_TRAN_RUNTIME_TIMEOUT_MS;
  const parsed = raw === undefined ? NaN : Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshRuntimeTimeoutMs;
  return Math.min(maxSshRuntimeTimeoutMs, Math.max(1000, Math.trunc(parsed)));
}

export function sshCaptureRuntimeTimeoutMs(explicitTimeoutMs?: number, env: NodeJS.ProcessEnv = process.env): number {
  if (explicitTimeoutMs === undefined) return sshRuntimeTimeoutMs(env);
  if (!Number.isSafeInteger(explicitTimeoutMs) || explicitTimeoutMs <= 0) throw new Error("SSH capture runtimeTimeoutMs must be a positive integer");
  return Math.min(maxExplicitSshCaptureRuntimeTimeoutMs, explicitTimeoutMs);
}

export function sshRuntimeTimeoutHint(options: {
  invocation: ParsedSshInvocation;
  transport: SshRuntimeTimeoutHint["transport"];
  timeoutMs: number;
}): SshRuntimeTimeoutHint {
  const timeoutSeconds = Number((options.timeoutMs / 1000).toFixed(3));
  return {
    code: "ssh-runtime-timeout",
    level: "warning",
    providerId: safeProviderId(options.invocation.providerId),
    route: options.invocation.route.raw,
    transport: options.transport,
    invocationKind: options.invocation.parsed.invocationKind,
    timeoutMs: options.timeoutMs,
    timeoutSeconds,
    message: `ssh/tran operation exceeded the ${timeoutSeconds}s top-level runtime limit and was disconnected.`,
    action: "Use short query plus poll semantics; do not keep tran open waiting for long CI/CD, trace, logs, or build progress.",
    note: "Timeout hint is written to stderr and intentionally does not echo the original remote command.",
  };
}

export function formatSshRuntimeTimeoutHint(hint: SshRuntimeTimeoutHint): string {
  return `UNIDESK_SSH_RUNTIME_TIMEOUT ${JSON.stringify(hint)}\n`;
}

export function sshStdoutStreamMaxBytes(env: NodeJS.ProcessEnv = process.env): number {
  const raw = env.UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES ?? env.UNIDESK_TRAN_STDOUT_STREAM_MAX_BYTES;
  const parsed = raw === undefined ? NaN : Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return readCliOutputPolicy().maxStdoutBytes;
  return Math.min(maxSshStdoutStreamMaxBytes, Math.max(minSshStdoutStreamMaxBytes, Math.trunc(parsed)));
}

export function sshStderrStreamMaxBytes(env: NodeJS.ProcessEnv = process.env): number {
  const raw = env.UNIDESK_SSH_STDERR_STREAM_MAX_BYTES ?? env.UNIDESK_TRAN_STDERR_STREAM_MAX_BYTES;
  const parsed = raw === undefined ? NaN : Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return readCliOutputPolicy().maxStdoutBytes;
  return Math.min(maxSshStdoutStreamMaxBytes, Math.max(minSshStdoutStreamMaxBytes, Math.trunc(parsed)));
}

export function sshStdoutTruncationHint(options: {
  invocation: ParsedSshInvocation;
  transport: SshStdoutTruncationHint["transport"];
  stream?: SshStdoutTruncationHint["stream"];
  thresholdBytes: number;
  observedBytesAtTruncation: number;
  forwardedBytes?: number;
  dumpPath: string | null;
  dumpError?: string | null;
}): SshStdoutTruncationHint {
  const stream = options.stream ?? "stdout";
  const policy = readCliOutputPolicy();
  const dumpCreated = options.dumpPath !== null;
  return {
    code: stream === "stdout" ? "ssh-stdout-truncated" : "ssh-stderr-truncated",
    level: "warning",
    stream,
    providerId: safeProviderId(options.invocation.providerId),
    route: options.invocation.route.raw,
    transport: options.transport,
    invocationKind: options.invocation.parsed.invocationKind,
    thresholdBytes: options.thresholdBytes,
    observedBytesAtTruncation: options.observedBytesAtTruncation,
    forwardedBytes: options.forwardedBytes ?? Math.min(options.thresholdBytes, options.observedBytesAtTruncation),
    dumpPath: options.dumpPath,
    dumpError: options.dumpError ?? null,
    disclosurePolicy: {
      name: "bounded-stream-explicit-dump",
      configPath: policy.configPath,
      maxPreviewBytes: policy.maxStdoutBytes,
      dumpDir: policy.dumpDir,
      defaultCreatesDump: false,
      dumpCreated,
    },
    recommendedRerun: [
      "Use rg -m/--max-count, sed -n, head, tail, --limit, --tail-bytes, or an id-specific query.",
      "Use --full, --raw, --tail-bytes, --limit, or UNIDESK_*_STREAM_MAX_BYTES only when you intentionally need a wider one-off disclosure.",
      ...(dumpCreated
        ? ["Read dumpPath only because explicit stream dump capture was requested."]
        : ["Default truncation creates no dump; set UNIDESK_TRAN_STREAM_DUMP=1 only for an explicit one-off complete capture."]),
    ],
    message: dumpCreated
      ? `ssh ${stream} exceeded the YAML-configured preview budget; explicit capture wrote the complete stream to a protected local dump.`
      : `ssh ${stream} exceeded the YAML-configured preview budget; output is bounded and no temporary dump was created by default.`,
    action: dumpCreated
      ? "Inspect dumpPath for this explicit one-off capture, then prefer a narrower remote command next time."
      : "Rerun with cat/head/tail/rg, --limit, --tail-bytes, or an id-specific query instead of recovering through a temporary dump.",
    note: "This hint is written to stderr and intentionally does not echo the original remote command.",
  };
}

export function formatSshStdoutTruncationHint(hint: SshStdoutTruncationHint): string {
  const marker = hint.stream === "stderr" ? "UNIDESK_SSH_STDERR_TRUNCATED" : "UNIDESK_SSH_STDOUT_TRUNCATED";
  return `${marker} ${JSON.stringify(hint)}\n`;
}

export function sshTruncationCompletionSummary(options: {
  invocation: ParsedSshInvocation;
  transport: SshTruncationCompletionSummary["transport"];
  exitCode: number;
  timedOut: boolean;
  startedAtMs: number;
  stdout: SshStreamTruncationSummary | null;
  stderr: SshStreamTruncationSummary | null;
}): SshTruncationCompletionSummary | null {
  if (options.stdout === null && options.stderr === null) return null;
  const elapsedMs = Math.max(0, Date.now() - options.startedAtMs);
  return {
    code: "ssh-truncation-summary",
    level: "info",
    providerId: safeProviderId(options.invocation.providerId),
    route: options.invocation.route.raw,
    transport: options.transport,
    invocationKind: options.invocation.parsed.invocationKind,
    exitCode: options.exitCode,
    timedOut: options.timedOut,
    elapsedMs,
    elapsedSeconds: Math.round(elapsedMs / 100) / 10,
    commandOmitted: true,
    stdout: options.stdout,
    stderr: options.stderr,
    message: "SSH output was truncated, but the command has finished; use exitCode/timedOut for closeout and rerun a semantic narrow query when more evidence is needed.",
    action: options.stdout?.dumpPath != null || options.stderr?.dumpPath != null
      ? "An explicit stream dump was requested; inspect its dumpPath only when the complete one-off payload is required."
      : "No dump was created by default; use cat/head/tail/rg, --limit, --tail-bytes, or an id-specific query.",
  };
}

export function formatSshTruncationCompletionSummary(summary: SshTruncationCompletionSummary | null): string {
  return summary === null ? "" : `UNIDESK_SSH_TRUNCATION_SUMMARY ${JSON.stringify(summary)}\n`;
}

export function classifySshTcpPoolFailure(text: string): SshTcpPoolFailureKind | null {
  const normalized = text.toLowerCase();
  if (normalized.includes("ssh tcp data channel closed")) return "provider-data-channel-closed";
  if (
    normalized.includes("requested ssh tcp data channel is not ready")
    || normalized.includes("ssh tcp data channel is not available")
    || normalized.includes('"failurekind":"provider-data-channel-missing"')
  ) {
    return "provider-data-channel-missing";
  }
  if (
    normalized.includes("provider ssh tcp data pool has no idle channel")
    || normalized.includes('"failurekind":"provider-data-pool-exhausted"')
  ) {
    return "provider-data-pool-exhausted";
  }
  return null;
}

export function sshTcpPoolHint(options: {
  invocation: ParsedSshInvocation;
  transport: SshTcpPoolHint["transport"];
  exitCode: number;
  stderrText: string;
}): SshTcpPoolHint | null {
  if (options.exitCode === 0) return null;
  const failureKind = classifySshTcpPoolFailure(options.stderrText);
  if (failureKind === null) return null;
  const providerId = safeProviderId(options.invocation.providerId);
  return {
    code: "ssh-tcp-pool-transient",
    level: "warning",
    providerId,
    route: options.invocation.route.raw,
    transport: options.transport,
    invocationKind: options.invocation.parsed.invocationKind,
    failureKind,
    exitCode: options.exitCode,
    message: "host.ssh.tcp-pool data channel failed during an SSH operation; classify this as transport/data-pool transient until pool labels and a retry prove otherwise.",
    action: "Inspect providerGatewaySshData* labels, then rerun the same idempotent command through the controlled CLI. Do not treat this alone as HWLAB runtime configuration failure.",
    retry: `trans ${providerId} argv true`,
    diagnostics: {
      poolStatus: `bun scripts/cli.ts debug ssh-pool ${providerId}`,
      fullHealth: "bun scripts/cli.ts debug health",
    },
    note: "Hint is written to stderr and intentionally does not echo the original remote command.",
  };
}

export function formatSshTcpPoolHint(hint: SshTcpPoolHint | null): string {
  return hint === null ? "" : `UNIDESK_SSH_TCP_POOL_HINT ${JSON.stringify(hint)}\n`;
}

function sshStreamDumpPath(invocation: ParsedSshInvocation, stream: SshStdoutTruncationHint["stream"]): string {
  const policy = readCliOutputPolicy();
  mkdirSync(policy.dumpDir, { recursive: true, mode: 0o700 });
  const timestamp = new Date().toISOString().replace(/[:.]/gu, "-");
  const suffix = randomBytes(4).toString("hex");
  const slug = `${invocation.providerId}-${invocation.route.raw}-${invocation.route.plane}`
    .replace(/[^A-Za-z0-9._-]+/gu, "-")
    .replace(/^-+|-+$/gu, "")
    .slice(0, 80) || "ssh";
  return join(policy.dumpDir, `${timestamp}-${process.pid}-${suffix}-${slug}.${stream}.bin`);
}

export interface SshStreamForwarder {
  write: (chunk: Buffer) => string | null;
  summary: () => SshStreamTruncationSummary | null;
}

export function createSshStdoutForwarder(options: {
  invocation: ParsedSshInvocation;
  transport: SshStdoutTruncationHint["transport"];
  maxBytes?: number;
  stdout?: NodeJS.WritableStream;
  persistDump?: boolean;
}): SshStreamForwarder {
  return createSshStreamForwarder({
    invocation: options.invocation,
    transport: options.transport,
    stream: "stdout",
    maxBytes: options.maxBytes ?? sshStdoutStreamMaxBytes(),
    target: options.stdout ?? process.stdout,
    persistDump: options.persistDump ?? sshStreamDumpExplicitlyRequested(),
  });
}

export function createSshStderrForwarder(options: {
  invocation: ParsedSshInvocation;
  transport: SshStdoutTruncationHint["transport"];
  maxBytes?: number;
  stderr?: NodeJS.WritableStream;
  persistDump?: boolean;
}): SshStreamForwarder {
  return createSshStreamForwarder({
    invocation: options.invocation,
    transport: options.transport,
    stream: "stderr",
    maxBytes: options.maxBytes ?? sshStderrStreamMaxBytes(),
    target: options.stderr ?? process.stderr,
    persistDump: options.persistDump ?? sshStreamDumpExplicitlyRequested(),
  });
}

function createSshStreamForwarder(options: {
  invocation: ParsedSshInvocation;
  transport: SshStdoutTruncationHint["transport"];
  stream: SshStdoutTruncationHint["stream"];
  maxBytes: number;
  target: NodeJS.WritableStream;
  persistDump: boolean;
}): SshStreamForwarder {
  let observedBytes = 0;
  let forwardedBytes = 0;
  let truncated = false;
  let dumpPath: string | null = null;
  let dumpError: string | null = null;
  let lastHint: SshStdoutTruncationHint | null = null;
  const bufferedChunks: Buffer[] = [];

  const appendDump = (chunk: Buffer): void => {
    if (!options.persistDump) return;
    if (dumpError !== null) return;
    try {
      if (dumpPath === null) {
        dumpPath = sshStreamDumpPath(options.invocation, options.stream);
        writeFileSync(dumpPath, Buffer.alloc(0), { mode: 0o600 });
        for (const buffered of bufferedChunks) appendFileSync(dumpPath, buffered);
        bufferedChunks.length = 0;
      }
      appendFileSync(dumpPath, chunk);
    } catch (error) {
      dumpError = error instanceof Error ? error.message : String(error);
      dumpPath = null;
    }
  };

  return {
    write(chunk: Buffer): string | null {
      observedBytes += chunk.length;
      if (!truncated && observedBytes <= options.maxBytes) {
        if (options.persistDump) bufferedChunks.push(Buffer.from(chunk));
        options.target.write(chunk);
        forwardedBytes += chunk.length;
        return null;
      }

      if (!truncated) {
        truncated = true;
        const remaining = Math.max(0, options.maxBytes - forwardedBytes);
        if (remaining > 0) {
          const forwarded = chunk.subarray(0, remaining);
          options.target.write(forwarded);
          forwardedBytes += remaining;
        }
        appendDump(chunk);
        lastHint = sshStdoutTruncationHint({
          invocation: options.invocation,
          transport: options.transport,
          stream: options.stream,
          thresholdBytes: options.maxBytes,
          observedBytesAtTruncation: observedBytes,
          forwardedBytes,
          dumpPath,
          dumpError,
        });
        return formatSshStdoutTruncationHint(lastHint);
      }

      appendDump(chunk);
      return null;
    },
    summary(): SshStreamTruncationSummary | null {
      if (lastHint === null) return null;
      return {
        stream: lastHint.stream,
        thresholdBytes: lastHint.thresholdBytes,
        observedBytesAtTruncation: lastHint.observedBytesAtTruncation,
        forwardedBytes: lastHint.forwardedBytes,
        dumpPath,
        dumpError,
      };
    },
  };
}

export function sshStreamDumpExplicitlyRequested(env: NodeJS.ProcessEnv = process.env): boolean {
  return env.UNIDESK_TRAN_STREAM_DUMP === "1";
}

export function sshBrokerSource(): string {
  return String.raw`
const open = JSON.parse(process.argv[2] || process.argv[1] || "{}");
const token = process.env.PROVIDER_TOKEN || process.env.UNIDESK_PROVIDER_TOKEN || "";
const baseUrl = process.env.UNIDESK_SSH_BROKER_URL || "ws://backend-core:8080/ws/ssh";
const url = baseUrl + "?token=" + encodeURIComponent(token);
const ws = new WebSocket(url);
let exitCode = 255;
let canSend = false;
let sessionReady = false;
let opened = false;
let pendingWrites = 0;
let exitRequested = false;
const pending = [];
const pendingInput = [];
function finishIfReady() {
  if (!exitRequested || pendingWrites > 0) return;
  process.exit(exitCode);
}
function writeOutput(stream, chunk) {
  pendingWrites += 1;
  stream.write(chunk, () => {
    pendingWrites -= 1;
    finishIfReady();
  });
}
function requestExit(code) {
  exitCode = code;
  exitRequested = true;
  clearTimeout(openTimer);
  clearRuntimeTimer();
  try { ws.close(); } catch {}
  finishIfReady();
}
const openTimer = setTimeout(() => {
  if (opened) return;
  writeOutput(process.stderr, "unidesk ssh bridge timed out waiting for provider session\n");
  requestExit(255);
}, Number(open.openTimeoutMs || 15000));
const runtimeTimeoutMs = Number(open.runtimeTimeoutMs || 60000);
const runtimeTimeoutMode = open.runtimeTimeoutMode === "inactivity" ? "inactivity" : "wall-clock";
let runtimeTimer = null;
function armRuntimeTimer() {
  if (runtimeTimer !== null) clearTimeout(runtimeTimer);
  runtimeTimer = setTimeout(() => {
    const noun = runtimeTimeoutMode === "inactivity" ? "inactivity timeout" : "runtime timeout";
    writeOutput(process.stderr, "unidesk ssh bridge " + noun + "; use short query plus poll semantics for long non-streaming work\n");
    requestExit(124);
  }, runtimeTimeoutMs);
}
function clearRuntimeTimer() {
  if (runtimeTimer !== null) clearTimeout(runtimeTimer);
  runtimeTimer = null;
}
armRuntimeTimer();

function send(value) {
  const text = JSON.stringify(value);
  if (!canSend || ws.readyState !== WebSocket.OPEN) {
    pending.push(text);
    return;
  }
  ws.send(text);
}

function flush() {
  while (pending.length > 0 && ws.readyState === WebSocket.OPEN) {
    ws.send(pending.shift());
  }
}

function sendInput(value) {
  const text = JSON.stringify(value);
  if (!sessionReady || ws.readyState !== WebSocket.OPEN) {
    pendingInput.push(text);
    return;
  }
  ws.send(text);
}

function flushInput() {
  if (!sessionReady || ws.readyState !== WebSocket.OPEN) return;
  while (pendingInput.length > 0) {
    ws.send(pendingInput.shift());
  }
}

function decodeData(data) {
  return typeof data === "string" ? data : Buffer.from(data).toString("utf8");
}

ws.addEventListener("open", () => {
  canSend = true;
  send({
    type: "ssh.open",
    providerId: open.providerId,
    command: open.command || undefined,
    cwd: open.cwd || undefined,
    tty: open.tty === true,
    cols: open.cols || 100,
    rows: open.rows || 30,
  });
  flush();
});

ws.addEventListener("message", (event) => {
  const message = JSON.parse(decodeData(event.data));
  if (runtimeTimeoutMode === "inactivity") armRuntimeTimer();
  if (message.type === "ssh.data") {
    opened = true;
    const chunk = Buffer.from(message.data || "", "base64");
    if (message.stream === "stderr") writeOutput(process.stderr, chunk);
    else writeOutput(process.stdout, chunk);
    return;
  }
  if (message.type === "ssh.opened") {
    opened = true;
    sessionReady = true;
    clearTimeout(openTimer);
    if (open.stdinEotOnEnd === true) setTimeout(flushInput, 200);
    else flushInput();
    return;
  }
  if (message.type === "ssh.dispatched") {
    return;
  }
  if (message.type === "ssh.error") {
    clearTimeout(openTimer);
    clearRuntimeTimer();
    writeOutput(process.stderr, String(message.message || "ssh bridge error") + "\n");
    if (message.failureKind || message.providerId || message.dataChannelId || message.dataPool) {
      writeOutput(process.stderr, "UNIDESK_SSH_ERROR " + JSON.stringify({
        failureKind: message.failureKind || null,
        providerId: message.providerId || null,
        dataChannelId: message.dataChannelId || null,
        dataPool: message.dataPool || null,
        transport: message.transport || null,
        controlFallback: message.controlFallback === true
      }) + "\n");
    }
    requestExit(255);
    return;
  }
  if (message.type === "ssh.exit") {
    requestExit(Number.isInteger(message.exitCode) ? message.exitCode : 255);
  }
});

ws.addEventListener("close", () => {
  requestExit(exitCode);
});

ws.addEventListener("error", () => {
  writeOutput(process.stderr, "unidesk ssh bridge websocket error\n");
  requestExit(255);
});

process.stdin.on("data", (chunk) => {
  sendInput({ type: "ssh.input", data: Buffer.from(chunk).toString("base64"), encoding: "base64" });
});
process.stdin.on("end", () => {
  if (open.stdinEotOnEnd === true) {
    sendInput({ type: "ssh.input", data: Buffer.from([4]).toString("base64"), encoding: "base64" });
  }
  sendInput({ type: "ssh.eof" });
});
`;
}

function terminalSize(): { cols: number; rows: number } {
  return {
    cols: Number(process.stdout.columns) > 0 ? Number(process.stdout.columns) : 100,
    rows: Number(process.stdout.rows) > 0 ? Number(process.stdout.rows) : 30,
  };
}

export function remoteCommandForRoute(route: ParsedSshRoute, command: string[], options: { stdin?: boolean } = {}): string {
  if (route.plane === "k3s") return buildK3sTargetCommand(route, command, options);
  if (route.plane === "win") throw new Error(`ssh apply-patch does not support win routes yet: ${route.raw}`);
  return shellArgv(command);
}

type WindowsApplyPatchFsOperation =
  | "stat"
  | "read-b64-block"
  | "read-bulk-b64"
  | "apply-replacements-bulk-stdin"
  | "write-b64-stdin"
  | "write-b64-begin"
  | "write-b64-append-stdin"
  | "write-b64-commit"
  | "delete";

const windowsApplyPatchWriteB64ChunkChars = 12_000;
const posixApplyPatchWriteB64ChunkChars = 12_000;

type PosixApplyPatchFsOperation = Exclude<ApplyPatchV2RemoteOperation, "write-b64-argv">;

export function createPosixApplyPatchFileSystem(invocation: ParsedSshInvocation, runRemoteCommand: (command: string[], input?: string) => Promise<SshCaptureResult>): ApplyPatchV2FileSystem {
  async function checked(operation: PosixApplyPatchFsOperation, args: string[], input?: string): Promise<SshCaptureResult> {
    const startedAtMs = Date.now();
    let result: SshCaptureResult;
    try {
      result = await runRemoteCommand(applyPatchV2RemoteCommand(operation, args), input);
    } catch (error) {
      throw new ApplyPatchV2Error(`posix apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
        operation,
        args,
        input,
        invocation,
        remoteElapsedMs: Math.max(0, Date.now() - startedAtMs),
        cause: error instanceof Error ? { name: error.name, message: error.message } : { message: String(error) },
      }));
    }
    if (result.exitCode === 0) return result;
    throw new ApplyPatchV2Error(`posix apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
      operation,
      args,
      input,
      invocation,
      result,
      remoteElapsedMs: Math.max(0, Date.now() - startedAtMs),
    }));
  }

  return {
    async stat(filePath) {
      const result = await checked("stat", [filePath]);
      const [bytesText, sha256] = result.stdout.trim().split(/\s+/u);
      const bytes = Number(bytesText);
      if (!Number.isSafeInteger(bytes) || bytes < 0 || !/^[0-9a-f]{64}$/u.test(sha256 ?? "")) {
        throw new Error(`posix apply-patch fs stat returned invalid metadata: ${JSON.stringify({ filePath, stdout: result.stdout.slice(0, 500) })}`);
      }
      return { bytes, sha256: sha256! };
    },
    async readBlock(filePath, blockIndex, blockBytes) {
      const result = await checked("read-b64-block", [filePath, String(blockIndex), String(blockBytes)]);
      const encoded = result.stdout
        .split(/\r?\n/u)
        .filter((line) => !line.startsWith("UNIDESK_APPLY_PATCH_V2_BLOCK "))
        .join("")
        .replace(/\s+/gu, "");
      return encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64");
    },
    async writeFile(filePath, content) {
      const encoded = content.toString("base64");
      const expectedBytes = String(content.length);
      const expectedSha256 = createHash("sha256").update(content).digest("hex");
      try {
        await checked("write-b64-stdin", [filePath, expectedBytes, expectedSha256], encoded);
        return;
      } catch {
        const token = `${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}-${expectedSha256.slice(0, 12)}`;
        await checked("write-b64-begin", [filePath, token]);
        for (const chunk of chunkString(encoded, posixApplyPatchWriteB64ChunkChars)) {
          await checked("write-b64-append", [filePath, token, chunk]);
        }
        await checked("write-b64-commit", [filePath, token, expectedBytes, expectedSha256]);
      }
    },
    async deleteFile(filePath) {
      await checked("delete", [filePath]);
    },
    async readFiles(filePaths) {
      const result = await checked("read-bulk-b64", filePaths);
      return decodeApplyPatchV2BulkRead(result.stdout, filePaths);
    },
    async applyReplacementsBulk(filePaths, plans: Map<string, ApplyPatchV2BulkReplacementWritePlan>) {
      const { targets, payload } = formatApplyPatchV2BulkReplacementPayload(filePaths, plans);
      if (targets.length === 0) return;
      await checked("apply-replacements-bulk-stdin", [String(targets.length)], payload);
    },
  };
}

export function createWindowsApplyPatchFileSystem(invocation: ParsedSshInvocation, runRemoteCommand: (remoteCommand: string, input?: string) => Promise<SshCaptureResult>): ApplyPatchV2FileSystem {
  async function checked(operation: WindowsApplyPatchFsOperation, args: string[], input?: string): Promise<SshCaptureResult> {
    const command = buildWindowsPowerShellInvocation(windowsApplyPatchFsScript(invocation.route.workspace, operation, args));
    const startedAtMs = Date.now();
    let result: SshCaptureResult;
    try {
      result = await runRemoteCommand(command, input);
    } catch (error) {
      throw new ApplyPatchV2Error(`windows apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
        operation,
        args,
        input,
        invocation,
        remoteElapsedMs: Math.max(0, Date.now() - startedAtMs),
        cause: error instanceof Error ? { name: error.name, message: error.message } : { message: String(error) },
      }));
    }
    if (result.exitCode === 0) return result;
    throw new ApplyPatchV2Error(`windows apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
      operation,
      args,
      input,
      invocation,
      result,
      remoteElapsedMs: Math.max(0, Date.now() - startedAtMs),
    }));
  }

  return {
    async stat(filePath) {
      const result = await checked("stat", [filePath]);
      const [bytesText, sha256] = result.stdout.trim().split(/\s+/u);
      const bytes = Number(bytesText);
      if (!Number.isSafeInteger(bytes) || bytes < 0 || !/^[0-9a-f]{64}$/u.test(sha256 ?? "")) {
        throw new Error(`windows apply-patch fs stat returned invalid metadata: ${JSON.stringify({ filePath, stdout: result.stdout.slice(0, 500) })}`);
      }
      return { bytes, sha256: sha256! };
    },
    async readBlock(filePath, blockIndex, blockBytes) {
      const result = await checked("read-b64-block", [filePath, String(blockIndex), String(blockBytes)]);
      const encoded = result.stdout.replace(/\s+/gu, "");
      return encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64");
    },
    async writeFile(filePath, content) {
      const encoded = content.toString("base64");
      const expectedBytes = String(content.length);
      const expectedSha256 = createHash("sha256").update(content).digest("hex");
      try {
        await checked("write-b64-stdin", [filePath, expectedBytes, expectedSha256], encoded);
        return;
      } catch {
        const token = `${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}-${expectedSha256.slice(0, 12)}`;
        await checked("write-b64-begin", [filePath, token]);
        for (const chunk of chunkString(encoded, windowsApplyPatchWriteB64ChunkChars)) {
          await checked("write-b64-append-stdin", [filePath, token], chunk);
        }
        await checked("write-b64-commit", [filePath, token, expectedBytes, expectedSha256]);
      }
    },
    async deleteFile(filePath) {
      await checked("delete", [filePath]);
    },
    async readFiles(filePaths) {
      const result = await checked("read-bulk-b64", [String(filePaths.length)], `${filePaths.join("\n")}\n`);
      return decodeApplyPatchV2BulkRead(result.stdout, filePaths);
    },
    async applyReplacementsBulk(filePaths, plans: Map<string, ApplyPatchV2BulkReplacementWritePlan>) {
      const { targets, payload } = formatApplyPatchV2BulkReplacementPayload(filePaths, plans);
      if (targets.length === 0) return;
      await checked("apply-replacements-bulk-stdin", [String(targets.length)], payload);
    },
  };
}

function applyPatchFsFailureDetails(options: {
  operation: string;
  args: string[];
  input?: string;
  invocation: ParsedSshInvocation;
  result?: SshCaptureResult;
  remoteElapsedMs: number;
  cause?: Record<string, unknown>;
}): Record<string, unknown> {
  const { operation, args, input, invocation, result } = options;
  const inputBytes = input === undefined ? 0 : Buffer.byteLength(input, "utf8");
  const expected = applyPatchFsExpectedWrite(operation, args);
  const targetCount = applyPatchFsBulkTargetCount(operation, args);
  return {
    operation,
    route: invocation.route.raw,
    ...(operation === "read-bulk-b64" || operation === "apply-replacements-bulk-stdin" ? {} : { filePath: args[0] ?? "" }),
    ...(targetCount === null ? {} : { targetCount }),
    args: args.slice(0, 4),
    inputBytes,
    ...(inputBytes === 0 ? {} : { inputSha256: createHash("sha256").update(input, "utf8").digest("hex") }),
    ...(expected.expectedBytes === undefined ? {} : { expectedBytes: expected.expectedBytes }),
    ...(expected.expectedSha256 === undefined ? {} : { expectedSha256: expected.expectedSha256 }),
    remoteElapsedMs: options.remoteElapsedMs,
    landed: false,
    ...(result === undefined ? {} : {
      exitCode: result.exitCode,
      stdoutTail: result.stdout.slice(-2000),
      stderrTail: result.stderr.slice(-4000),
    }),
    ...(options.cause === undefined ? {} : { cause: options.cause }),
  };
}

function applyPatchFsExpectedWrite(operation: string, args: string[]): { expectedBytes?: string; expectedSha256?: string } {
  if (operation === "write-b64-stdin") return { expectedBytes: args[1], expectedSha256: args[2] };
  if (operation === "write-b64-commit") return { expectedBytes: args[2], expectedSha256: args[3] };
  return {};
}

function applyPatchFsBulkTargetCount(operation: string, args: string[]): number | null {
  if (operation === "read-bulk-b64") {
    const count = Number(args[0]);
    return Number.isSafeInteger(count) && count >= 0 ? count : args.length;
  }
  if (operation !== "apply-replacements-bulk-stdin") return null;
  const count = Number(args[0]);
  return Number.isSafeInteger(count) && count >= 0 ? count : null;
}

function windowsApplyPatchFsScript(basePath: string | null, operation: WindowsApplyPatchFsOperation, args: string[]): string {
  const target = args[0] ?? "";
  const arg1 = args[1] ?? "";
  const arg2 = args[2] ?? "";
  const arg3 = args[3] ?? "";
  const scriptParts = [
    "$ErrorActionPreference = 'Stop';",
    "$ProgressPreference = 'SilentlyContinue';",
    "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();",
    "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();",
    "$OutputEncoding = [System.Text.UTF8Encoding]::new();",
    `$basePath = ${powerShellSingleQuote(basePath ?? "")};`,
    `$operation = ${powerShellSingleQuote(operation)};`,
    `$targetArg = ${powerShellSingleQuote(target)};`,
    `$arg1 = ${powerShellSingleQuote(arg1)};`,
    `$arg2 = ${powerShellSingleQuote(arg2)};`,
    `$arg3 = ${powerShellSingleQuote(arg3)};`,
    "function Fail([string]$Message, [int]$Code) { [Console]::Error.WriteLine($Message); exit $Code }",
    "function Resolve-UnideskPath([string]$Raw) { if ([string]::IsNullOrWhiteSpace($Raw)) { Fail 'empty apply-patch path' 2 }; if ([System.IO.Path]::IsPathRooted($Raw)) { return [System.IO.Path]::GetFullPath($Raw) }; if (-not [string]::IsNullOrWhiteSpace($basePath)) { return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($basePath, $Raw)) }; return [System.IO.Path]::GetFullPath($Raw) }",
    "function Ensure-Parent([string]$Target) { $parent = [System.IO.Path]::GetDirectoryName($Target); if (-not [string]::IsNullOrWhiteSpace($parent)) { [System.IO.Directory]::CreateDirectory($parent) | Out-Null } }",
    "function Get-Sha256([string]$Path) { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }",
    "function Get-Sha256Bytes([byte[]]$Bytes) { $sha = [System.Security.Cryptography.SHA256]::Create(); try { return ([System.BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } }",
    "function Decode-Utf8([string]$Encoded) { return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Encoded)) }",
    "function Encode-Utf8([string]$Value) { return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value)) }",
    "function Set-TmpPaths([string]$Target, [string]$Token) { if ($Token -notmatch '^[A-Za-z0-9_.-]+$') { Fail 'invalid apply-patch temp token' 2 }; $dir = [System.IO.Path]::GetDirectoryName($Target); if ([string]::IsNullOrWhiteSpace($dir)) { $dir = (Get-Location).ProviderPath }; $base = [System.IO.Path]::GetFileName($Target); $script:tmp = [System.IO.Path]::Combine($dir, '.' + $base + '.unidesk-v2-' + $Token + '.tmp'); $script:tmpB64 = $script:tmp + '.b64' }",
    "function Verify-Temp([string]$Target, [string]$Tmp, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { $actualBytes = ([System.IO.FileInfo]$Tmp).Length; if ($actualBytes -ne $ExpectedBytes) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('apply-patch byte count mismatch for ' + $Target + ': expected=' + $ExpectedBytes + ' actual=' + $actualBytes) 23 }; $actualSha256 = Get-Sha256 $Tmp; if ($actualSha256 -ne $ExpectedSha256) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('apply-patch sha256 mismatch for ' + $Target + ': expected=' + $ExpectedSha256 + ' actual=' + $actualSha256) 24 } }",
    "function Decode-ToTarget([string]$Target, [string]$Encoded, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { Ensure-Parent $Target; Set-TmpPaths $Target ([guid]::NewGuid().ToString('N')); try { $bytes = [Convert]::FromBase64String(($Encoded -replace '\\s','')) } catch { Fail ('apply-patch base64 decode failed for ' + $Target + ': ' + $_.Exception.Message) 22 }; [System.IO.File]::WriteAllBytes($script:tmp, $bytes); Verify-Temp $Target $script:tmp $ExpectedBytes $ExpectedSha256; Move-Item -LiteralPath $script:tmp -Destination $Target -Force; $actualSha256 = Get-Sha256 $Target; if ($actualSha256 -ne $ExpectedSha256) { Fail ('apply-patch final sha256 mismatch for ' + $Target) 25 } }",
    "$target = Resolve-UnideskPath $targetArg;",
    "switch ($operation) {",
    "  'stat' { if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('file not found: ' + $target) 1 }; $bytes = ([System.IO.FileInfo]$target).Length; $digest = Get-Sha256 $target; [Console]::Out.WriteLine(([string]$bytes) + ' ' + $digest); break }",
    "  'read-b64-block' { $blockIndex = [Int64]$arg1; $blockSize = [Int32]$arg2; if ($blockIndex -lt 0 -or $blockSize -le 0) { Fail 'invalid read block args' 2 }; $fs = [System.IO.File]::Open($target, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite); try { [void]$fs.Seek($blockIndex * $blockSize, [System.IO.SeekOrigin]::Begin); $buffer = New-Object byte[] $blockSize; $read = $fs.Read($buffer, 0, $blockSize); if ($read -gt 0) { [Console]::Out.Write([Convert]::ToBase64String($buffer, 0, $read)) } } finally { $fs.Dispose() }; break }",
    "  'read-bulk-b64' { $expectedCount = [Int32]$targetArg; $items = @([Console]::In.ReadToEnd() -split \"`r?`n\" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }); if ($items.Count -ne $expectedCount) { Fail ('bulk read record count mismatch: expected=' + $expectedCount + ' actual=' + $items.Count) 23 }; [Console]::Out.WriteLine('UNIDESK_APPLY_PATCH_V2_BULK_READ ' + $items.Count); foreach ($item in $items) { $path = Resolve-UnideskPath $item; if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail ('file not found: ' + $path) 1 }; $bytes = [System.IO.File]::ReadAllBytes($path); $pathB64 = Encode-Utf8 $item; $digest = Get-Sha256Bytes $bytes; [Console]::Out.Write($pathB64 + ' ' + $bytes.Length + ' ' + $digest + ' '); [Console]::Out.Write([System.Convert]::ToBase64String($bytes)); [Console]::Out.WriteLine() }; break }",
    "  'apply-replacements-bulk-stdin' { $expectedCount = [Int32]$targetArg; $records = @([Console]::In.ReadToEnd() -split \"`r?`n\" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }); if ($records.Count -ne $expectedCount) { Fail ('bulk replacement record count mismatch: expected=' + $expectedCount + ' actual=' + $records.Count) 23 }; $utf8 = [System.Text.UTF8Encoding]::new($false); $plans = New-Object System.Collections.Generic.List[object]; $index = 0; foreach ($record in $records) { $index += 1; $fields = $record -split '\\s+'; if ($fields.Count -ne 6) { Fail 'bulk replacement malformed record' 23 }; $targetRelative = Decode-Utf8 $fields[0]; $resolved = Resolve-UnideskPath $targetRelative; $originalBytes = [Int64]$fields[1]; $originalSha256 = $fields[2]; $finalBytes = [Int64]$fields[3]; $finalSha256 = $fields[4]; $replacementsText = $fields[5]; if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { Fail ('file not found: ' + $resolved) 1 }; $source = [System.IO.File]::ReadAllBytes($resolved); if ($source.Length -ne $originalBytes -or (Get-Sha256Bytes $source) -ne $originalSha256) { Fail ('bulk replacement original integrity mismatch for ' + $resolved) 23 }; $lines = New-Object System.Collections.Generic.List[string]; $sourceText = [System.Text.Encoding]::UTF8.GetString($source); foreach ($line in ($sourceText -split \"`n\", -1)) { $lines.Add($line) | Out-Null }; if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { $lines.RemoveAt($lines.Count - 1) }; $replacements = New-Object System.Collections.Generic.List[object]; if (-not [string]::IsNullOrEmpty($replacementsText)) { foreach ($item in ($replacementsText -split ';')) { if ([string]::IsNullOrWhiteSpace($item)) { continue }; $parts = $item -split ',', 3; if ($parts.Count -ne 3) { Fail 'bulk replacement malformed replacement' 23 }; $newText = Decode-Utf8 $parts[2]; $newLines = New-Object System.Collections.Generic.List[string]; foreach ($line in ($newText -split \"`n\", -1)) { $newLines.Add($line) | Out-Null }; if ($newLines.Count -gt 0 -and $newLines[$newLines.Count - 1] -eq '') { $newLines.RemoveAt($newLines.Count - 1) }; $replacements.Add([pscustomobject]@{ start = [Int32]$parts[0]; oldLength = [Int32]$parts[1]; newLines = [string[]]$newLines }) | Out-Null } }; foreach ($replacement in @($replacements | Sort-Object -Property start -Descending)) { if ($replacement.start -lt 0 -or $replacement.oldLength -lt 0 -or ($replacement.start + $replacement.oldLength) -gt $lines.Count) { Fail ('bulk replacement out of bounds for ' + $resolved) 23 }; $removeCount = $replacement.oldLength; if ($removeCount -gt 0) { $lines.RemoveRange($replacement.start, $removeCount) }; if ($replacement.newLines.Count -gt 0) { $lines.InsertRange($replacement.start, [string[]]$replacement.newLines) } }; $outputText = if ($lines.Count -eq 0) { '' } else { ([string]::Join(\"`n\", [string[]]$lines) + \"`n\") }; $outputBytes = [System.Text.Encoding]::UTF8.GetBytes($outputText); if ($outputBytes.Length -ne $finalBytes -or (Get-Sha256Bytes $outputBytes) -ne $finalSha256) { Fail ('bulk replacement final integrity mismatch for ' + $resolved) 23 }; $tmp = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($resolved), ('.' + [System.IO.Path]::GetFileName($resolved) + '.unidesk-v2-bulk-' + [guid]::NewGuid().ToString('N') + '.tmp')); [System.IO.File]::WriteAllText($tmp, $outputText, $utf8); $plans.Add([pscustomobject]@{ target = $resolved; tmp = $tmp; sha256 = $finalSha256 }) | Out-Null }; foreach ($plan in $plans) { Ensure-Parent $plan.target; Move-Item -LiteralPath $plan.tmp -Destination $plan.target -Force; if ((Get-Sha256 $plan.target) -ne $plan.sha256) { Fail ('bulk replacement final sha256 mismatch for ' + $plan.target) 25 } }; break }",
    "  'write-b64-stdin' { Decode-ToTarget $target ([Console]::In.ReadToEnd()) ([Int64]$arg1) $arg2; break }",
    "  'write-b64-begin' { Ensure-Parent $target; Set-TmpPaths $target $arg1; [System.IO.File]::WriteAllText($script:tmpB64, '', [System.Text.Encoding]::ASCII); break }",
    "  'write-b64-append-stdin' { Set-TmpPaths $target $arg1; $chunk = ([Console]::In.ReadToEnd()) -replace '\\s',''; [System.IO.File]::AppendAllText($script:tmpB64, $chunk, [System.Text.Encoding]::ASCII); break }",
    "  'write-b64-commit' { Set-TmpPaths $target $arg1; $encoded = [System.IO.File]::ReadAllText($script:tmpB64, [System.Text.Encoding]::ASCII); Remove-Item -LiteralPath $script:tmpB64 -Force -ErrorAction SilentlyContinue; Decode-ToTarget $target $encoded ([Int64]$arg2) $arg3; break }",
    "  'delete' { Remove-Item -LiteralPath $target -Force -ErrorAction SilentlyContinue; break }",
    "  default { Fail ('unsupported apply-patch fs op: ' + $operation) 2 }",
    "}",
  ];
  return scriptParts
    .map((line) => line.startsWith("  'apply-replacements-bulk-stdin' ") ? windowsBulkReplacementScript() : line)
    .join(" ");
}

function windowsBulkReplacementScript(): string {
  return [
    "  'apply-replacements-bulk-stdin' {",
    "    $expectedCount = [Int32]$targetArg;",
    "    $records = @([Console]::In.ReadToEnd() -split \"`r?`n\" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) });",
    "    if ($records.Count -ne $expectedCount) { Fail ('bulk replacement record count mismatch: expected=' + $expectedCount + ' actual=' + $records.Count) 23 };",
    "    $plans = New-Object System.Collections.Generic.List[object];",
    "    foreach ($record in $records) {",
    "      $fields = $record -split '\\s+'; if ($fields.Count -ne 7) { Fail 'bulk replacement malformed record' 23 };",
    "      $targetRelative = Decode-Utf8 $fields[0]; $resolved = Resolve-UnideskPath $targetRelative;",
    "      $originalBytes = [Int64]$fields[1]; $originalSha256 = $fields[2]; $finalBytes = [Int64]$fields[3]; $finalSha256 = $fields[4]; $encodingStyle = $fields[5]; $replacementsText = $fields[6];",
    "      if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { Fail ('file not found: ' + $resolved) 1 };",
    "      $source = [System.IO.File]::ReadAllBytes($resolved); $actualOriginalBytes = [Int64]$source.Length; $actualOriginalSha256 = Get-Sha256Bytes $source;",
    "      if ($actualOriginalBytes -ne $originalBytes) { Fail ('bulk replacement original integrity mismatch for ' + $resolved) 23 };",
    "      if (-not [string]::Equals($actualOriginalSha256, $originalSha256, [StringComparison]::OrdinalIgnoreCase)) { Fail ('bulk replacement original integrity mismatch for ' + $resolved) 23 };",
    "      $textEncoding = if ($encodingStyle -eq 'legacy-single-byte') { [System.Text.Encoding]::GetEncoding(28591) } else { [System.Text.UTF8Encoding]::new($false, $true) };",
    "      try { $sourceText = $textEncoding.GetString($source) } catch { Fail ('bulk replacement source decode failed encoding=' + $encodingStyle + ' for ' + $resolved) 25 };",
    "      $lines = New-Object System.Collections.Generic.List[string]; foreach ($line in ($sourceText -split \"`n\", -1)) { $lines.Add($line) | Out-Null }; if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { $lines.RemoveAt($lines.Count - 1) };",
    "      $replacements = New-Object System.Collections.Generic.List[object];",
    "      if (-not [string]::IsNullOrEmpty($replacementsText)) { foreach ($item in ($replacementsText -split ';')) { if ([string]::IsNullOrWhiteSpace($item)) { continue }; $parts = $item -split ',', 3; if ($parts.Count -ne 3) { Fail 'bulk replacement malformed replacement' 23 }; $newText = Decode-Utf8 $parts[2]; $newLines = New-Object System.Collections.Generic.List[string]; foreach ($line in ($newText -split \"`n\", -1)) { $newLines.Add($line) | Out-Null }; if ($newLines.Count -gt 0 -and $newLines[$newLines.Count - 1] -eq '') { $newLines.RemoveAt($newLines.Count - 1) }; $replacements.Add([pscustomobject]@{ start = [Int32]$parts[0]; oldLength = [Int32]$parts[1]; newLines = [string[]]$newLines }) | Out-Null } };",
    "      foreach ($replacement in @($replacements | Sort-Object -Property start -Descending)) { if ($replacement.start -lt 0 -or $replacement.oldLength -lt 0 -or ($replacement.start + $replacement.oldLength) -gt $lines.Count) { Fail ('bulk replacement out of bounds for ' + $resolved) 23 }; if ($replacement.oldLength -gt 0) { $lines.RemoveRange($replacement.start, $replacement.oldLength) }; if ($replacement.newLines.Count -gt 0) { $lines.InsertRange($replacement.start, [string[]]$replacement.newLines) } };",
    "      $outputText = if ($lines.Count -eq 0) { '' } else { ([string]::Join(\"`n\", [string[]]$lines) + \"`n\") };",
    "      try { $outputBytes = $textEncoding.GetBytes($outputText) } catch { Fail ('bulk replacement final encode failed encoding=' + $encodingStyle + ' for ' + $resolved) 25 };",
    "      if ($outputBytes.Length -ne $finalBytes -or (Get-Sha256Bytes $outputBytes) -ne $finalSha256) { Fail ('bulk replacement final integrity mismatch for ' + $resolved) 23 };",
    "      $tmp = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($resolved), ('.' + [System.IO.Path]::GetFileName($resolved) + '.unidesk-v2-bulk-' + [guid]::NewGuid().ToString('N') + '.tmp')); [System.IO.File]::WriteAllBytes($tmp, $outputBytes); $plans.Add([pscustomobject]@{ target = $resolved; tmp = $tmp; sha256 = $finalSha256 }) | Out-Null;",
    "    };",
    "    foreach ($plan in $plans) { Ensure-Parent $plan.target; Move-Item -LiteralPath $plan.tmp -Destination $plan.target -Force; if ((Get-Sha256 $plan.target) -ne $plan.sha256) { Fail ('bulk replacement final sha256 mismatch for ' + $plan.target) 25 } };",
    "    break",
    "  }",
  ].join(" ");
}

function chunkString(value: string, chunkSize: number): string[] {
  const chunks: string[] = [];
  for (let index = 0; index < value.length; index += chunkSize) {
    chunks.push(value.slice(index, index + chunkSize));
  }
  return chunks.length > 0 ? chunks : [""];
}

async function runSshCaptureCommand(config: UniDeskConfig, invocation: ParsedSshInvocation, command: string[], input?: string): Promise<SshCaptureResult> {
  const remoteCommand = remoteCommandForRoute(invocation.route, command, { stdin: input !== undefined });
  return await runSshCaptureRemoteCommand(config, invocation, remoteCommand, input);
}

async function runSshCaptureRemoteCommand(config: UniDeskConfig, invocation: ParsedSshInvocation, remoteCommand: string, input?: string, options: { runtimeTimeoutMs?: number } = {}): Promise<SshCaptureResult> {
  const startedAtMs = Date.now();
  const size = terminalSize();
  const runtimeTimeoutMs = sshCaptureRuntimeTimeoutMs(options.runtimeTimeoutMs);
  const payload = {
    providerId: invocation.providerId,
    command: wrapSshRemoteCommand(remoteCommand),
    cwd: sshRoutePayloadCwd(invocation.route),
    tty: false,
    stdinEotOnEnd: false,
    openTimeoutMs: Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)),
    runtimeTimeoutMs,
    cols: size.cols,
    rows: size.rows,
  };
  const payloadJson = JSON.stringify(payload);
  const encodedBrokerSource = Buffer.from(sshBrokerSource(), "utf8").toString("base64");
  const script = [
    "set -eu",
    `payload=${shellQuote(payloadJson)}`,
    "if command -v backend-core >/dev/null 2>&1; then",
    '  exec backend-core --ssh-broker "$payload"',
    "fi",
    `export UNIDESK_SSH_BROKER_URL=${shellQuote("ws://127.0.0.1:8080/ws/ssh")}`,
    'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"',
    'trap \'rm -f "$broker_js"\' EXIT',
    `printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`,
    'bun "$broker_js" "$payload"',
  ].join("\n");
  const child = spawn("docker", ["exec", "-i", "unidesk-backend-core", "sh", "-c", script], {
    cwd: repoRoot,
    stdio: ["pipe", "pipe", "pipe"],
  });
  if (input !== undefined) {
    writeChunkedStdin(child.stdin, input);
  } else {
    child.stdin.end();
  }
  let stdout = "";
  let stderr = "";
  child.stdout.on("data", (chunk: Buffer) => {
    stdout += chunk.toString("utf8");
  });
  child.stderr.on("data", (chunk: Buffer) => {
    stderr += chunk.toString("utf8");
  });
  return await new Promise<SshCaptureResult>((resolve) => {
    let settled = false;
    let killTimer: NodeJS.Timeout | null = null;
    const finish = (exitCode: number): void => {
      if (settled) return;
      settled = true;
      clearTimeout(runtimeTimer);
      if (killTimer !== null) clearTimeout(killTimer);
      const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({
        invocation,
        transport: "backend-core-broker",
        exitCode,
        startedAtMs,
      }));
      if (timingHint) stderr += timingHint;
      stderr += formatSshTcpPoolHint(sshTcpPoolHint({
        invocation,
        transport: "backend-core-broker",
        exitCode,
        stderrText: stderr,
      }));
      stderr += sshWindowsPlaneHint(invocation, exitCode, stderr);
      stderr += sshWindowsPowerShellPipelineHint(invocation, exitCode, stderr);
      resolve({ exitCode, stdout, stderr });
    };
    const runtimeTimer = setTimeout(() => {
      const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({
        invocation,
        transport: "backend-core-broker",
        timeoutMs: runtimeTimeoutMs,
      }));
      stderr += hint;
      try {
        child.kill("SIGTERM");
      } catch {
        // Ignore.
      }
      killTimer = setTimeout(() => {
        try {
          child.kill("SIGKILL");
        } catch {
          // Ignore.
        }
      }, 2000);
      finish(124);
    }, runtimeTimeoutMs);
    child.on("error", (error) => {
      stderr += `unidesk ssh failed to start broker: ${error.message}\n`;
      finish(255);
    });
    child.on("close", (code) => finish(code ?? 255));
  });
}

async function runSshStreamRemoteCommand(
  config: UniDeskConfig,
  invocation: ParsedSshInvocation,
  remoteCommand: string,
  handlers: SshRemoteCommandStreamHandlers,
  input?: string,
  options: { inactivityTimeoutMs?: number } = {},
): Promise<SshCaptureResult> {
  const streamInvocation = {
    ...invocation,
    parsed: { ...invocation.parsed, remoteCommand, requiresStdin: input !== undefined, invocationKind: "helper" as const },
  };
  const startedAtMs = Date.now();
  const size = terminalSize();
  const inactivityTimeoutMs = options.inactivityTimeoutMs ?? sshRuntimeTimeoutMs();
  const runtimeTimeoutMs = options.inactivityTimeoutMs === undefined ? inactivityTimeoutMs : Math.max(inactivityTimeoutMs * 4, 60 * 60_000);
  const payload = {
    providerId: invocation.providerId,
    command: wrapSshRemoteCommand(remoteCommand),
    cwd: sshRoutePayloadCwd(invocation.route),
    tty: false,
    stdinEotOnEnd: false,
    openTimeoutMs: Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)),
    runtimeTimeoutMs,
    runtimeTimeoutMode: options.inactivityTimeoutMs === undefined ? "wall-clock" : "inactivity",
    cols: size.cols,
    rows: size.rows,
  };
  const payloadJson = JSON.stringify(payload);
  const encodedBrokerSource = Buffer.from(sshBrokerSource(), "utf8").toString("base64");
  const script = [
    "set -eu",
    `payload=${shellQuote(payloadJson)}`,
    "if command -v backend-core >/dev/null 2>&1; then",
    '  exec backend-core --ssh-broker "$payload"',
    "fi",
    `export UNIDESK_SSH_BROKER_URL=${shellQuote("ws://127.0.0.1:8080/ws/ssh")}`,
    'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"',
    'trap \'rm -f "$broker_js"\' EXIT',
    `printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`,
    'bun "$broker_js" "$payload"',
  ].join("\n");
  const child = spawn("docker", ["exec", "-i", "unidesk-backend-core", "sh", "-c", script], {
    cwd: repoRoot,
    stdio: ["pipe", "pipe", "pipe"],
  });
  if (input !== undefined) {
    writeChunkedStdin(child.stdin, input);
  } else {
    child.stdin.end();
  }
  let stdout = "";
  let stderr = "";
  let streamError: unknown = null;
  let streamWrites = Promise.resolve();
  const queueStreamWrite = (chunk: Buffer, stream: "stdout" | "stderr"): void => {
    streamWrites = streamWrites.then(async () => {
      if (stream === "stdout") await handlers.onStdout(chunk);
      else if (handlers.onStderr !== undefined) await handlers.onStderr(chunk);
    }).catch((error) => {
      streamError = error;
      stderr += `unidesk ssh stream sink failed: ${error instanceof Error ? error.message : String(error)}\n`;
      try {
        child.kill("SIGTERM");
      } catch {
        // Ignore kill failures after a local stream sink error.
      }
    });
  };
  child.stdout.on("data", (chunk: Buffer) => {
    queueStreamWrite(chunk, "stdout");
  });
  child.stderr.on("data", (chunk: Buffer) => {
    stderr += chunk.toString("utf8");
    queueStreamWrite(chunk, "stderr");
  });
  return await new Promise<SshCaptureResult>((resolve) => {
    let settled = false;
    let killTimer: NodeJS.Timeout | null = null;
    let inactivityTimer: NodeJS.Timeout | null = null;
    const refreshActivityTimer = (): void => {
      if (settled || options.inactivityTimeoutMs === undefined) return;
      if (inactivityTimer !== null) clearTimeout(inactivityTimer);
      inactivityTimer = setTimeout(() => {
        const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({
          invocation: streamInvocation,
          transport: "backend-core-broker",
          timeoutMs: inactivityTimeoutMs,
        }));
        stderr += hint;
        try {
          child.kill("SIGTERM");
        } catch {
          // Ignore.
        }
        killTimer = setTimeout(() => {
          try {
            child.kill("SIGKILL");
          } catch {
            // Ignore.
          }
        }, 2000);
        finish(124);
      }, inactivityTimeoutMs);
    };
    const finish = (exitCode: number): void => {
      if (settled) return;
      settled = true;
      clearTimeout(runtimeTimer);
      if (inactivityTimer !== null) clearTimeout(inactivityTimer);
      if (killTimer !== null) clearTimeout(killTimer);
      void streamWrites.then(() => {
        const finalCode = streamError === null ? exitCode : 255;
        const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({
          invocation: streamInvocation,
          transport: "backend-core-broker",
          exitCode: finalCode,
          startedAtMs,
        }));
        if (timingHint) stderr += timingHint;
        stderr += formatSshTcpPoolHint(sshTcpPoolHint({
          invocation: streamInvocation,
          transport: "backend-core-broker",
          exitCode: finalCode,
          stderrText: stderr,
        }));
        resolve({ exitCode: finalCode, stdout, stderr });
      });
    };
    const runtimeTimer = setTimeout(() => {
      const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({
        invocation: streamInvocation,
        transport: "backend-core-broker",
        timeoutMs: runtimeTimeoutMs,
      }));
      stderr += hint;
      try {
        child.kill("SIGTERM");
      } catch {
        // Ignore.
      }
      killTimer = setTimeout(() => {
        try {
          child.kill("SIGKILL");
        } catch {
          // Ignore.
        }
      }, 2000);
      finish(124);
    }, runtimeTimeoutMs);
    refreshActivityTimer();
    child.on("error", (error) => {
      stderr += `unidesk ssh failed to start broker: ${error.message}\n`;
      finish(255);
    });
    child.on("close", (code) => finish(code ?? 255));
    child.stdout.on("data", refreshActivityTimer);
    child.stderr.on("data", refreshActivityTimer);
  });
}

async function runRemoteSsh(config: UniDeskConfig, host: string, providerId: string, args: string[]): Promise<number> {
  const { runRemoteSshCli } = await import("./remote-ssh");
  return await runRemoteSshCli({
    host,
    user: "root",
    port: 22,
    projectRoot: repoRoot,
    identityFile: null,
    transport: "frontend",
    args: ["ssh", providerId, ...args],
  }, config);
}

export async function runSshCommandCapture(config: UniDeskConfig, target: string, args: string[], input?: string, options: { runtimeTimeoutMs?: number } = {}): Promise<SshCaptureResult> {
  const invocation = parseSshInvocation(target, args);
  const parsed = invocation.parsed;
  if (parsed.remoteCommand === null) throw new Error(`ssh ${target} capture requires a non-interactive operation`);
  const stdin = parsed.stdinPrefix !== undefined || parsed.stdinSuffix !== undefined
    ? `${parsed.stdinPrefix ?? ""}${input ?? ""}${parsed.stdinSuffix ?? ""}`
    : input;
  const plan = sshCaptureBackendPlan(config, process.env);
  if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) {
    return await runRemoteSshCapture(config, plan.remoteHost, target, args, stdin, options);
  }
  const local = await runSshCaptureRemoteCommand(config, invocation, parsed.remoteCommand, stdin, options);
  const fallbackHost = sshCaptureRemoteHost(config, process.env);
  if (local.exitCode !== 0 && fallbackHost !== null && isLocalSshCaptureBackendUnavailable(local)) {
    return await runRemoteSshCapture(config, fallbackHost, target, args, stdin, options);
  }
  return local;
}

async function runRemoteSshCapture(config: UniDeskConfig, host: string, target: string, args: string[], input?: string, options: { runtimeTimeoutMs?: number } = {}): Promise<SshCaptureResult> {
  const { runRemoteSshCommandCapture } = await import("./remote-ssh");
  return await runRemoteSshCommandCapture(config, host, target, args, input, process.env, options);
}

export function sshCaptureBackendPlan(config: UniDeskConfig, env: NodeJS.ProcessEnv = process.env): SshCaptureBackendPlan {
  const configuredBackend = readTransSshBackendConfig(env);
  const remoteHost = sshCaptureRemoteHost(config, env);
  if (configuredBackend !== null) return configuredSshCaptureBackendPlan(configuredBackend, remoteHost);
  const localBackendCore = detectLocalBackendCoreStatus(env);
  const runnerEnv = isRunnerEnvironment(env);
  if (runnerEnv && remoteHost !== null) return { backend: "remote-frontend-websocket", remoteHost, reason: "runner-environment", localBackendCore };
  if (!localBackendCore.backendCoreContainer && remoteHost !== null) return { backend: "remote-frontend-websocket", remoteHost, reason: "local-backend-core-unavailable", localBackendCore };
  return { backend: "local-backend-core-broker", remoteHost: null, reason: "main-server-local-backend-core", localBackendCore };
}

function configuredSshCaptureBackendPlan(configuredBackend: TransSshBackendConfig, remoteHost: string | null): SshCaptureBackendPlan {
  if (configuredBackend.backend === "local-backend-core-broker") {
    return {
      backend: "local-backend-core-broker",
      remoteHost: null,
      reason: `configured:${configuredBackend.sourceRef}`,
      localBackendCore: {
        dockerExecutable: true,
        backendCoreContainer: true,
        error: null,
        source: configuredBackend.sourceRef,
      },
    };
  }
  if (remoteHost === null) {
    throw new Error(`${configuredBackend.sourceRef} selects ${configuredBackend.raw}, but no remote host is configured for frontend websocket SSH`);
  }
  return {
    backend: "remote-frontend-websocket",
    remoteHost,
    reason: `configured:${configuredBackend.sourceRef}`,
    localBackendCore: {
      dockerExecutable: false,
      backendCoreContainer: false,
      error: null,
      source: configuredBackend.sourceRef,
    },
  };
}

function sshCaptureRemoteHost(config: UniDeskConfig, env: NodeJS.ProcessEnv): string | null {
  return normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_IP)
    ?? normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_HOST)
    ?? normalizeRemoteHost(env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST)
    ?? normalizeRemoteHost(readHostK8sPublicHost() ?? undefined)
    ?? normalizeRemoteHost(config.network.publicHost);
}

function normalizeRemoteHost(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, "");
}

function isRunnerEnvironment(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,
  );
}

function detectLocalBackendCoreStatus(env: NodeJS.ProcessEnv = process.env): SshCaptureBackendPlan["localBackendCore"] {
  const timeout = sshBackendCoreDetectTimeoutMs(env);
  const inspect = spawnSync("docker", ["inspect", "unidesk-backend-core", "--format", "{{.State.Running}}"], { encoding: "utf8", timeout });
  if (inspect.error !== undefined) return { dockerExecutable: false, backendCoreContainer: false, error: inspect.error.message, source: "docker-inspect" };
  const inspectOutput = `${inspect.stdout ?? ""}\n${inspect.stderr ?? ""}`.trim();
  if (inspect.status === 0) {
    const running = String(inspect.stdout ?? "").trim() === "true";
    return {
      dockerExecutable: true,
      backendCoreContainer: running,
      error: running ? null : inspectOutput || "docker inspect unidesk-backend-core reported not running",
      source: "docker-inspect",
    };
  }
  const result = spawnSync("docker", ["ps", "--format", "{{.Names}}"], { encoding: "utf8", timeout });
  if (result.error !== undefined) return { dockerExecutable: false, backendCoreContainer: false, error: result.error.message, source: "docker-ps" };
  const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
  return {
    dockerExecutable: result.status === 0,
    backendCoreContainer: result.status === 0 && String(result.stdout ?? "").split(/\r?\n/u).includes("unidesk-backend-core"),
    error: result.status === 0 ? null : inspectOutput || output || `docker ps exited ${result.status ?? "unknown"}`,
    source: "docker-ps",
  };
}

function sshBackendCoreDetectTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
  const parsed = Number(env.UNIDESK_SSH_BACKEND_CORE_DETECT_TIMEOUT_MS);
  if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshBackendCoreDetectTimeoutMs;
  return Math.min(maxSshBackendCoreDetectTimeoutMs, Math.max(2000, Math.trunc(parsed)));
}

function isLocalSshCaptureBackendUnavailable(result: SshCaptureResult): boolean {
  return /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(result.stderr);
}

function writeChunkedStdin(stdin: NodeJS.WritableStream, input: string): void {
  const buffer = Buffer.from(input, "utf8");
  const chunkSize = 32 * 1024;
  for (let offset = 0; offset < buffer.length; offset += chunkSize) {
    stdin.write(buffer.subarray(offset, Math.min(buffer.length, offset + chunkSize)));
  }
  stdin.end();
}

export async function runSsh(config: UniDeskConfig, providerId: string, args: string[]): Promise<number> {
  const normalizedArgs = normalizeSshOperationArgs(args);
  process.stderr.write(sshRouteSeparatorCompatibilityHint(args, normalizedArgs));
  // 在选择或连接 transport 前完成解析，保证可预测的 route/operation 错误始终在本地失败。
  const invocation = parseSshInvocation(providerId, normalizedArgs);
  const plan = sshCaptureBackendPlan(config, process.env);
  if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) {
    return await runRemoteSsh(config, plan.remoteHost, providerId, normalizedArgs);
  }
  const parsed = invocation.parsed;
  const operationName = normalizedArgs[0] ?? "";
  if (isSshFileTransferOperation(normalizedArgs)) {
    const executor: SshRemoteCommandExecutor = {
      streamInactivityTimeoutMs: sshRuntimeTimeoutMs(),
      runRemoteCommand: (remoteCommand, input, options) => runSshCaptureRemoteCommand(config, invocation, remoteCommand, input, options),
      streamRemoteCommand: (remoteCommand, handlers, input, options) => runSshStreamRemoteCommand(config, invocation, remoteCommand, handlers, input, options),
    };
    return await runSshFileTransferOperation(invocation, normalizedArgs, executor, {
      buildRouteCommand: remoteCommandForRoute,
      buildWindowsPowerShellCommand: buildWindowsPowerShellInvocation,
    });
  }
  if (operationName === "apply-patch") {
    const applyPatch = effectiveApplyPatchV2Invocation(invocation, normalizedArgs.slice(1));
    const executor: ApplyPatchV2Executor = applyPatch.invocation.route.plane === "win"
      ? { fs: createWindowsApplyPatchFileSystem(applyPatch.invocation, (remoteCommand, input) => runSshCaptureRemoteCommand(config, applyPatch.invocation, remoteCommand, input)) }
      : applyPatch.invocation.route.plane === "host"
        ? { fs: createPosixApplyPatchFileSystem(applyPatch.invocation, (command, input) => runSshCaptureCommand(config, applyPatch.invocation, command, input)) }
        : { run: (command, input) => runSshCaptureCommand(config, applyPatch.invocation, command, input) };
    return await runApplyPatchV2({
      executor,
      stdin: process.stdin,
      stdout: process.stdout,
      stderr: process.stderr,
      argv: applyPatch.argv,
      timing: {
        providerId: applyPatch.invocation.providerId,
        route: applyPatch.invocation.route.raw,
        transport: "backend-core-broker",
      },
    });
  }
  const startedAtMs = Date.now();
  const size = terminalSize();
  const openTimeoutMs = Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000));
  const runtimeTimeoutMs = sshRuntimeTimeoutMs();
  const payload = {
    providerId: invocation.providerId,
    command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers),
    cwd: sshRoutePayloadCwd(invocation.route),
    tty: parsed.remoteCommand === null,
    stdinEotOnEnd: parsed.remoteCommand !== null && parsed.requiresStdin !== true,
    openTimeoutMs,
    runtimeTimeoutMs,
    cols: size.cols,
    rows: size.rows,
  };
  const payloadJson = JSON.stringify(payload);
  const encodedBrokerSource = Buffer.from(sshBrokerSource(), "utf8").toString("base64");
  const script = [
    "set -eu",
    `payload=${shellQuote(payloadJson)}`,
    "if command -v backend-core >/dev/null 2>&1; then",
    '  exec backend-core --ssh-broker "$payload"',
    "fi",
    `export UNIDESK_SSH_BROKER_URL=${shellQuote("ws://127.0.0.1:8080/ws/ssh")}`,
    'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"',
    'trap \'rm -f "$broker_js"\' EXIT',
    `printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`,
    'bun "$broker_js" "$payload"',
  ].join("\n");
  const child = spawn("docker", [
    "exec",
    "-i",
    "unidesk-backend-core",
    "sh",
    "-c",
    script,
  ], {
    cwd: repoRoot,
    stdio: ["pipe", "pipe", "pipe"],
  });

  const rawMode = parsed.remoteCommand === null && process.stdin.isTTY;
  if (rawMode) process.stdin.setRawMode(true);
  process.stdin.resume();
  if (parsed.stdinPrefix !== undefined || parsed.stdinSuffix !== undefined) {
    if (parsed.stdinPrefix) child.stdin.write(parsed.stdinPrefix);
    process.stdin.pipe(child.stdin, { end: false });
    process.stdin.once("end", () => {
      if (parsed.stdinSuffix) child.stdin.write(parsed.stdinSuffix);
      child.stdin.end();
    });
  } else {
    process.stdin.pipe(child.stdin);
  }
  let stderrTail = "";
  const appendStderrTail = (chunk: Buffer | string): void => {
    const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
    stderrTail = (stderrTail + text).slice(-16_384);
  };
  let stdoutForwarder: SshStreamForwarder | null = null;
  let stderrForwarder: SshStreamForwarder | null = null;
  if (parsed.remoteCommand === null) {
    child.stdout.pipe(process.stdout);
  } else {
    stdoutForwarder = createSshStdoutForwarder({
      invocation,
      transport: "backend-core-broker",
    });
    stderrForwarder = createSshStderrForwarder({
      invocation,
      transport: "backend-core-broker",
    });
    child.stdout.on("data", (chunk: Buffer) => {
      const hint = stdoutForwarder.write(chunk);
      if (hint !== null) {
        appendStderrTail(hint);
        process.stderr.write(hint);
      }
    });
    child.stderr.on("data", (chunk: Buffer) => {
      appendStderrTail(chunk);
      const hint = stderrForwarder.write(chunk);
      if (hint !== null) {
        appendStderrTail(hint);
        process.stderr.write(hint);
      }
    });
  }
  if (parsed.remoteCommand === null) {
    child.stderr.on("data", (chunk: Buffer) => {
      appendStderrTail(chunk);
      process.stderr.write(chunk);
    });
  }

  return await new Promise<number>((resolve) => {
    let settled = false;
    let timedOut = false;
    let killTimer: NodeJS.Timeout | null = null;
    const runtimeTimer = setTimeout(() => {
      if (settled) return;
      timedOut = true;
      const hint = sshRuntimeTimeoutHint({
        invocation,
        transport: "backend-core-broker",
        timeoutMs: runtimeTimeoutMs,
      });
      const formatted = formatSshRuntimeTimeoutHint(hint);
      appendStderrTail(formatted);
      process.stderr.write(formatted);
      try {
        child.stdin.destroy();
      } catch {
        // Ignore stdin teardown failures on the timeout path.
      }
      try {
        child.kill("SIGTERM");
      } catch {
        // Ignore kill failures and fall through to finish.
      }
      killTimer = setTimeout(() => {
        try {
          child.kill("SIGKILL");
        } catch {
          // Process may have already exited.
        }
      }, 2000);
      finish(124);
    }, runtimeTimeoutMs);
    const restore = (): void => {
      clearTimeout(runtimeTimer);
      if (killTimer && !timedOut) clearTimeout(killTimer);
      process.stdin.unpipe(child.stdin);
      if (rawMode) process.stdin.setRawMode(false);
    };
    const finish = (exitCode: number): void => {
      if (settled) return;
      settled = true;
      restore();
      const hint = timedOut ? null : sshFailureHint(invocation.providerId, parsed, exitCode, stderrTail);
      if (hint !== null) process.stderr.write(formatSshFailureHint(hint));
      if (!timedOut) {
        const windowsPlaneHint = sshWindowsPlaneHint(invocation, exitCode, stderrTail);
        if (windowsPlaneHint) process.stderr.write(windowsPlaneHint);
        const pipelineHint = sshWindowsPowerShellPipelineHint(invocation, exitCode, stderrTail);
        if (pipelineHint) process.stderr.write(pipelineHint);
      }
      if (!timedOut) {
        const tcpPoolHint = formatSshTcpPoolHint(sshTcpPoolHint({
          invocation,
          transport: "backend-core-broker",
          exitCode,
          stderrText: stderrTail,
        }));
        if (tcpPoolHint) process.stderr.write(tcpPoolHint);
      }
      const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({
        invocation,
        transport: "backend-core-broker",
        exitCode,
        startedAtMs,
      }));
      if (timingHint) process.stderr.write(timingHint);
      const truncationSummary = formatSshTruncationCompletionSummary(sshTruncationCompletionSummary({
        invocation,
        transport: "backend-core-broker",
        exitCode,
        timedOut,
        startedAtMs,
        stdout: stdoutForwarder?.summary() ?? null,
        stderr: stderrForwarder?.summary() ?? null,
      }));
      if (truncationSummary) process.stderr.write(truncationSummary);
      resolve(exitCode);
    };
    child.on("error", (error) => {
      const message = `unidesk ssh failed to start broker: ${error.message}\n`;
      appendStderrTail(message);
      process.stderr.write(message);
      finish(255);
    });
    child.on("close", (code) => {
      finish(code ?? 255);
    });
  });
}
