import { EventEmitter } from "node:events";
import { execFile, spawn, type ChildProcess } from "node:child_process";
import { appendFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import WebSocket from "ws";
import { buildGatewayConnectParams } from "@qjclaw/gateway-client";
import type {
  LogEntry,
  RuntimeCloudFetchAction,
  RuntimeHealth,
  RuntimeMode,
  RuntimeModePreference,
  RuntimeProcessState,
  RuntimeStatus
} from "@qjclaw/shared-types";

const execFileAsync = promisify(execFile);
const GATEWAY_CONNECT_REQUEST_ID = "runtime-manager-connect";
const GATEWAY_STATUS_REQUEST_ID = "runtime-manager-status";
const GATEWAY_HEALTH_REQUEST_ID = "runtime-manager-health";
const GATEWAY_PROBE_TIMEOUT_MS = 4_000;
const GATEWAY_REUSE_READY_TIMEOUT_MS = 12_000;
const GATEWAY_READY_TIMEOUT_MS = 90_000;
const GATEWAY_READY_POLL_INTERVAL_MS = 500;
const MANAGED_CHILD_PID_PREFIX = "__QJC_MANAGED_CHILD_PID__=";

export interface RuntimeResolvedPaths {
  runtimeDir: string;
  nodeExecutable: string;
  openClawEntry: string;
  packagedOpenClawEntry: string;
  runtimeManifestPath: string;
  defaultConfigPath: string;
  pythonExecutable: string;
  pythonManifestPath: string;
  ffmpegExecutable?: string;
  ffprobeExecutable?: string;
  playwrightBrowsersPath: string;
  managedConfigPath: string;
  readmePath: string;
  runtimeDataDir: string;
  runtimeStateDir: string;
  runtimeLogsDir: string;
  logFilePath: string;
}

export interface ManagedConfigResolverInput {
  action: RuntimeCloudFetchAction;
  defaultConfig: Record<string, unknown>;
  paths: RuntimeResolvedPaths;
}

export type ManagedConfigResolver = (input: ManagedConfigResolverInput) => Promise<Record<string, unknown>>;

export interface RuntimeManagerOptions {
  vendorRuntimeDir: string;
  runtimeDataDir: string;
  logFilePath: string;
  requestedMode?: RuntimeModePreference;
  managedConfigResolver?: ManagedConfigResolver;
  strictBundledRuntime?: boolean;
}

interface RuntimeGatewayConnection {
  url?: string;
  token?: string;
}

interface RuntimeDecision {
  selectedMode: RuntimeMode;
  modeReason: string;
  message: string;
}

interface OpenClawGatewayConfigShape {
  auth?: {
    mode?: string;
    token?: string;
  };
  mode?: string;
  bind?: string;
  host?: string;
  port?: number;
}

interface OpenClawConfigShape {
  agents?: {
    defaults?: {
      workspace?: string;
    };
  };
  gateway?: OpenClawGatewayConfigShape;
  [key: string]: unknown;
}

interface PythonPayloadProbeResult {
  ready: boolean;
  pythonVersion?: string;
  installedPackages: string[];
  missingModules: string[];
  importErrors?: Record<string, string>;
  error?: string;
}

interface GatewayProbeResult {
  ready: boolean;
  checkedAt: string;
  lastError?: string;
  version?: string;
  availableMethods?: string[];
}

interface GatewayProbeErrorShape {
  code?: string;
  message?: string;
  details?: {
    code?: string;
    reason?: string;
    recommendedNextStep?: string;
  };
}

async function pathExists(targetPath: string): Promise<boolean> {
  try {
    await access(targetPath);
    return true;
  } catch {
    return false;
  }
}

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

function buildGatewayUrl(config?: OpenClawGatewayConfigShape): string | undefined {
  if (!config) {
    return undefined;
  }

  const port = typeof config.port === "number" ? config.port : 18789;
  const host = typeof config.host === "string" && config.host.trim()
    ? config.host
    : config.bind === "loopback"
      ? "127.0.0.1"
      : "127.0.0.1";

  return `ws://${host}:${port}`;
}

const PYTHON_RUNTIME_IMPORTS = [
  ["openpyxl", "openpyxl"],
  ["pandas", "pandas"],
  ["requests", "requests"],
  ["beautifulsoup4", "bs4"],
  ["lxml", "lxml"],
  ["urllib3", "urllib3"],
  ["pypdf", "pypdf"],
  ["python-docx", "docx"],
  ["charset-normalizer", "charset_normalizer"],
  ["pyyaml", "yaml"],
  ["pillow", "PIL"],
  ["loguru", "loguru"],
  ["pyexecjs", "execjs"],
  ["python-dotenv", "dotenv"],
  ["certifi", "certifi"],
  ["openai", "openai"],
  ["retry", "retry"],
  ["fastapi", "fastapi"],
  ["uvicorn", "uvicorn"],
  ["python-multipart", "multipart"],
  ["pdfplumber", "pdfplumber"],
  ["greenlet", "greenlet"],
  ["playwright", "playwright"],
  ["edge-tts", "edge_tts"],
  ["imageio-ffmpeg", "imageio_ffmpeg"],
  ["qiniu", "qiniu"]
] as const;

const PYTHON_RUNTIME_DIRECT_IMPORT_PROBES = [
  ["loguru", "from loguru import logger"],
  ["execjs", "import execjs"],
  ["openai.AsyncOpenAI", "from openai import AsyncOpenAI"],
  ["greenlet", "import greenlet"],
  ["playwright.async_api", "from playwright.async_api import async_playwright"]
] as const;

const PYTHON_RUNTIME_PROBE_NAMES = [
  ...new Set([
    ...PYTHON_RUNTIME_IMPORTS.map(([packageName]) => packageName),
    ...PYTHON_RUNTIME_DIRECT_IMPORT_PROBES.map(([probeName]) => probeName)
  ])
];

function formatExecError(error: unknown): string {
  if (error instanceof Error) {
    const typedError = error as Error & { code?: number | string; stderr?: string };
    const details = [
      typedError.message,
      typeof typedError.code !== "undefined" ? `code=${typedError.code}` : undefined,
      typeof typedError.stderr === "string" && typedError.stderr.trim()
        ? `stderr=${typedError.stderr.trim()}`
        : undefined
    ].filter((value): value is string => Boolean(value));
    return details.join("; ");
  }

  return String(error);
}

function trimTrailingPunctuation(value: string): string {
  return value.trim().replace(/[.\s]+$/u, "");
}

async function execPythonInlineScript(pythonExecutable: string, inlineScript: string): Promise<string> {
  const { stdout } = await execFileAsync(pythonExecutable, ["-c", inlineScript]);
  return stdout;
}

function formatGatewayProbeError(error: GatewayProbeErrorShape | undefined): string {
  const parts = [
    error?.message,
    error?.details?.reason,
    error?.details?.recommendedNextStep,
    error?.code,
    error?.details?.code
  ].filter((value): value is string => Boolean(value && value.trim()));
  return parts.join(" | ") || "Gateway rejected the readiness probe.";
}

function classifyMacSecurityBlockMessage(message?: string): string | undefined {
  if (!message) {
    return undefined;
  }

  const normalized = message.toLowerCase();
  if (normalized.includes("macsecurityblock")) {
    return trimTrailingPunctuation(message) + ".";
  }

  const blocked = normalized.includes("operation not permitted")
    || normalized.includes("code signature")
    || normalized.includes("codesign")
    || normalized.includes("quarantine")
    || normalized.includes("cannot be opened")
    || normalized.includes("is damaged")
    || normalized.includes("malware")
    || normalized.includes("not opened because apple cannot check it");
  if (!blocked) {
    return undefined;
  }

  return `macSecurityBlock: macOS blocked bundled runtime execution (${trimTrailingPunctuation(message)}).`;
}

async function probeGatewayReadiness(
  url: string,
  token?: string,
  timeoutMs = GATEWAY_PROBE_TIMEOUT_MS,
  options: { requireStatusHealth?: boolean; probeLabel?: "bundled" | "reusable" } = {}
): Promise<GatewayProbeResult> {
  return new Promise<GatewayProbeResult>((resolve) => {
    let settled = false;
    let handshakeSent = false;
    let connectAccepted = false;
    let statusAccepted = false;
    let gatewayVersion: string | undefined;
    let availableMethods: string[] | undefined;

    const finish = (result: Omit<GatewayProbeResult, "checkedAt">) => {
      if (settled) {
        return;
      }

      settled = true;
      clearTimeout(timer);
      try {
        if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
          ws.close();
        }
      } catch {
        // Ignore cleanup failures while finishing the probe.
      }

      resolve({
        checkedAt: new Date().toISOString(),
        ...result
      });
    };

    const timer = setTimeout(() => {
      finish({
        ready: false,
        lastError: options.requireStatusHealth
          ? `Timed out while probing ${options.probeLabel ?? "reusable"} Gateway status and health at ${url}.`
          : `Timed out while probing bundled Gateway readiness at ${url}.`
      });
    }, timeoutMs);

    const ws = new WebSocket(url);

    ws.on("message", (data: WebSocket.RawData) => {
      try {
        const frame = JSON.parse(data.toString()) as Record<string, unknown>;
        const eventName = String(frame.event ?? "");

        if (eventName === "connect.challenge" && !handshakeSent) {
          handshakeSent = true;
          const nonce = String((frame.payload as Record<string, unknown> | undefined)?.nonce ?? "");
          if (!nonce) {
            finish({
              ready: false,
              lastError: "Gateway did not provide a challenge nonce during the readiness probe."
            });
            return;
          }

          ws.send(JSON.stringify({
            type: "req",
            id: GATEWAY_CONNECT_REQUEST_ID,
            method: "connect",
            params: buildGatewayConnectParams({ token })
          }));
          return;
        }

        if (frame.type !== "res") {
          return;
        }

        if (frame.id === GATEWAY_CONNECT_REQUEST_ID) {
          if (frame.ok === false) {
            finish({
              ready: false,
              lastError: formatGatewayProbeError(frame.error as GatewayProbeErrorShape | undefined)
            });
            return;
          }

          connectAccepted = true;
          const payload = (frame.payload ?? frame.result ?? {}) as {
            server?: { version?: unknown };
            features?: { methods?: unknown };
          };
          gatewayVersion = typeof payload.server?.version === "string" ? payload.server.version : undefined;
          availableMethods = Array.isArray(payload.features?.methods)
            ? payload.features.methods.filter((method): method is string => typeof method === "string")
            : undefined;
          if (!options.requireStatusHealth) {
            finish({
              ready: true,
              version: gatewayVersion,
              availableMethods
            });
            return;
          }

          ws.send(JSON.stringify({
            type: "req",
            id: GATEWAY_STATUS_REQUEST_ID,
            method: "status",
            params: {}
          }));
          return;
        }

        if (frame.id === GATEWAY_STATUS_REQUEST_ID) {
          if (frame.ok === false) {
            finish({
              ready: false,
              lastError: formatGatewayProbeError(frame.error as GatewayProbeErrorShape | undefined)
            });
            return;
          }

          statusAccepted = true;
          const payload = (frame.payload ?? frame.result ?? {}) as { runtimeVersion?: unknown };
          if (typeof payload.runtimeVersion === "string" && payload.runtimeVersion.trim()) {
            gatewayVersion = payload.runtimeVersion;
          }
          ws.send(JSON.stringify({
            type: "req",
            id: GATEWAY_HEALTH_REQUEST_ID,
            method: "health",
            params: {}
          }));
          return;
        }

        if (frame.id === GATEWAY_HEALTH_REQUEST_ID) {
          if (frame.ok === false) {
            finish({
              ready: false,
              lastError: formatGatewayProbeError(frame.error as GatewayProbeErrorShape | undefined)
            });
            return;
          }

          const payload = (frame.payload ?? frame.result ?? {}) as { ok?: unknown; details?: unknown };
          if (payload.ok === false) {
            finish({
              ready: false,
              lastError: typeof payload.details === "string" && payload.details.trim()
                ? `Gateway health probe reported not ok: ${payload.details.trim()}`
                : "Gateway health probe reported not ok."
            });
            return;
          }

          finish({
            ready: true,
            version: gatewayVersion,
            availableMethods
          });
        }
      } catch (error) {
        finish({
          ready: false,
          lastError: `Gateway readiness probe returned malformed data: ${formatExecError(error)}`
        });
      }
    });

    ws.on("error", (error) => {
      finish({
        ready: false,
        lastError: `Failed to connect to ${url}. Check that OpenClaw Gateway is reachable. ${formatExecError(error)}`
      });
    });

    ws.on("close", (code: number) => {
      if (settled) {
        return;
      }

      finish({
        ready: false,
        lastError: connectAccepted
          ? statusAccepted
            ? `Gateway closed before health probe completed (${code}).`
            : `Gateway closed before readiness probe completed (${code}).`
          : `Gateway closed during readiness probe (${code}).`
      });
    });
  });
}

function isGatewayProbeNoListener(message?: string): boolean {
  if (!message) {
    return false;
  }

  const normalized = message.toLowerCase();
  return normalized.includes("econnrefused")
    || normalized.includes("enotfound")
    || normalized.includes("ehostunreach")
    || normalized.includes("enetunreach");
}

function isGatewayProbeStartupTransient(message?: string): boolean {
  if (!message) {
    return false;
  }

  const normalized = message.toLowerCase();
  return isGatewayProbeNoListener(message)
    || normalized.includes("gateway closed during readiness probe")
    || normalized.includes("gateway closed before readiness probe completed")
    || normalized.includes("gateway closed before health probe completed")
    || normalized.includes("timed out while probing reusable gateway")
    || normalized.includes("timed out while probing bundled gateway")
    || normalized.includes("gateway health probe reported not ok")
    || normalized.includes("gateway starting; retry shortly")
    || normalized.includes("startup-sidecars")
    || normalized.includes("unavailable");
}

function formatPayloadIssue(
  missingFiles: string[],
  pythonProbe: PythonPayloadProbeResult,
  pythonExecutable: string
): string | undefined {
  if (missingFiles.length > 0) {
    const missingLabels = missingFiles.map((targetPath) => path.basename(targetPath)).join(", ");
    return `required payload files are missing (${missingLabels})`;
  }

  if (pythonProbe.error) {
    return `the bundled Python executable could not be launched at ${pythonExecutable}: ${pythonProbe.error}`;
  }

  if (pythonProbe.missingModules.length > 0) {
    const importErrors = Object.entries(pythonProbe.importErrors ?? {});
    if (importErrors.length > 0) {
      const formattedImportErrors = importErrors
        .map(([probeName, message]) => `${probeName} (${message})`)
        .join("; ");
      return `the bundled Python dependency probe failed. Missing or broken imports: ${pythonProbe.missingModules.join(", ")}. Import errors: ${formattedImportErrors}`;
    }
    return `the bundled Python dependency probe failed. Missing modules: ${pythonProbe.missingModules.join(", ")}`;
  }

  return undefined;
}

async function probePythonPayload(pythonExecutable: string): Promise<PythonPayloadProbeResult> {
  const inlineScript = [
    "import importlib.util, json, sys",
    `modules = ${JSON.stringify(PYTHON_RUNTIME_IMPORTS)}`,
    `direct_imports = ${JSON.stringify(PYTHON_RUNTIME_DIRECT_IMPORT_PROBES)}`,
    "installed = []",
    "missing = []",
    "import_errors = {}",
    "for package_name, module_name in modules:",
    "    spec = importlib.util.find_spec(module_name)",
    "    if spec is None:",
    "        if package_name not in missing:",
    "            missing.append(package_name)",
    "    else:",
    "        installed.append(package_name)",
    "for probe_name, statement in direct_imports:",
    "    try:",
    "        exec(statement, {})",
    "    except Exception as exc:",
    "        if probe_name not in missing:",
    "            missing.append(probe_name)",
    "        import_errors[probe_name] = f\"{type(exc).__name__}: {exc}\"",
    "print(json.dumps({",
    "    'ready': len(missing) == 0,",
    "    'pythonVersion': sys.version.split()[0],",
    "    'installedPackages': installed,",
    "    'missingModules': missing,",
    "    'importErrors': import_errors",
    "}))"
  ].join("\n");

  try {
    const stdout = await execPythonInlineScript(pythonExecutable, inlineScript);
    const parsed = JSON.parse(stdout.trim()) as PythonPayloadProbeResult;
    return {
      ready: parsed.ready,
      pythonVersion: parsed.pythonVersion,
      installedPackages: parsed.installedPackages ?? [],
      missingModules: parsed.missingModules ?? [],
      importErrors: parsed.importErrors ?? {},
      error: parsed.error
    };
  } catch (error) {
    return {
      ready: false,
      installedPackages: [],
      missingModules: [...PYTHON_RUNTIME_PROBE_NAMES],
      importErrors: {},
      error: formatExecError(error)
    };
  }
}

function resolveExecutableOnPath(commandNames: readonly string[]): string | undefined {
  const pathEntries = (process.env.PATH ?? "")
    .split(path.delimiter)
    .map((entry) => entry.trim())
    .filter(Boolean);

  for (const commandName of commandNames) {
    const trimmed = commandName.trim();
    if (!trimmed) {
      continue;
    }

    if (path.isAbsolute(trimmed) && existsSync(trimmed)) {
      return trimmed;
    }

    for (const directory of pathEntries) {
      const candidate = path.join(directory, trimmed);
      if (existsSync(candidate)) {
        return candidate;
      }
    }
  }

  return undefined;
}

function resolveBundledFfmpegExecutable(runtimeDir: string): string | undefined {
  const stablePath = path.join(runtimeDir, "ffmpeg", "bin", "ffmpeg");
  if (existsSync(stablePath)) {
    return stablePath;
  }

  const imageioBinariesDir = path.join(runtimeDir, "python", "lib");
  if (existsSync(imageioBinariesDir)) {
    try {
      for (const pythonEntry of readdirSync(imageioBinariesDir, { withFileTypes: true })) {
        if (!pythonEntry.isDirectory()) continue
        const binariesDir = path.join(imageioBinariesDir, pythonEntry.name, "site-packages", "imageio_ffmpeg", "binaries")
        if (!existsSync(binariesDir)) continue
        const imageioCandidate = readdirSync(binariesDir, { withFileTypes: true })
          .filter((entry) => entry.isFile() && entry.name.startsWith("ffmpeg-") && !entry.name.endsWith(".exe") && !entry.name.endsWith(".zip"))
          .map((entry) => path.join(binariesDir, entry.name))
          .sort((left, right) => left.localeCompare(right, "en"))[0];
        if (imageioCandidate && existsSync(imageioCandidate)) {
          return imageioCandidate;
        }
      }
    } catch {
      // Ignore bundled imageio lookup failures and continue to other fallback locations.
    }
  }

  const playwrightBrowsersDir = path.join(runtimeDir, "playwright-browsers");
  if (existsSync(playwrightBrowsersDir)) {
    try {
      const candidates: string[] = [];
      for (const browserEntry of readdirSync(playwrightBrowsersDir, { withFileTypes: true })) {
        if (!browserEntry.isDirectory() || !browserEntry.name.startsWith("ffmpeg-")) {
          continue;
        }

        const browserDir = path.join(playwrightBrowsersDir, browserEntry.name);
        for (const executableEntry of readdirSync(browserDir, { withFileTypes: true })) {
          if (!executableEntry.isFile()) {
            continue;
          }
          if (executableEntry.name === "ffmpeg" || executableEntry.name.startsWith("ffmpeg-")) {
            candidates.push(path.join(browserDir, executableEntry.name));
          }
        }
      }

      const playwrightCandidate = candidates.sort((left, right) => left.localeCompare(right, "en"))[0];
      if (playwrightCandidate && existsSync(playwrightCandidate)) {
        return playwrightCandidate;
      }
    } catch {
      // Ignore bundled Playwright lookup failures and continue without an ffmpeg fallback.
    }
  }

  return undefined;
}

function resolveBundledFfprobeExecutable(runtimeDir: string, ffmpegExecutable?: string): string | undefined {
  const stablePath = path.join(runtimeDir, "ffmpeg", "bin", "ffprobe");
  if (existsSync(stablePath)) {
    return stablePath;
  }

  if (ffmpegExecutable) {
    const candidate = path.join(path.dirname(ffmpegExecutable), "ffprobe");
    if (existsSync(candidate)) {
      return candidate;
    }
  }

  return undefined;
}

function resolveInjectedBinaryPath(
  envName: string,
  bundledPath: string | undefined,
  commandNames: readonly string[]
): string | undefined {
  const configured = process.env[envName]?.trim();
  if (configured) {
    return configured;
  }
  if (bundledPath && existsSync(bundledPath)) {
    return bundledPath;
  }
  return resolveExecutableOnPath(commandNames);
}

interface PythonManifestPackageShape {
  name?: unknown;
}

interface PythonManifestShape {
  pythonVersion?: unknown;
  requestedPackages?: unknown;
  resolvedPackages?: unknown;
}

function parseManifestPackages(value: unknown): string[] {
  if (!Array.isArray(value)) {
    return [];
  }

  return value
    .map((entry) => {
      if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
        return null;
      }
      const name = (entry as PythonManifestPackageShape).name;
      return typeof name === "string" && name.trim() ? name.trim().toLowerCase() : null;
    })
    .filter((entry): entry is string => Boolean(entry));
}

async function probePythonPayloadFromManifest(pythonManifestPath: string): Promise<PythonPayloadProbeResult | null> {
  try {
    const raw = await readFile(pythonManifestPath, "utf8");
    const parsed = JSON.parse(raw.replace(/^\uFEFF/, "")) as PythonManifestShape;
    const installedPackages = [
      ...new Set([
        ...parseManifestPackages(parsed.requestedPackages),
        ...parseManifestPackages(parsed.resolvedPackages)
      ])
    ];

    if (installedPackages.length === 0) {
      return null;
    }

    const missingModules = PYTHON_RUNTIME_IMPORTS
      .map(([packageName]) => packageName)
      .filter((packageName) => !installedPackages.includes(packageName));

    return {
      ready: missingModules.length === 0,
      pythonVersion: typeof parsed.pythonVersion === "string" ? parsed.pythonVersion : undefined,
      installedPackages,
      missingModules,
      importErrors: {},
      error: undefined
    };
  } catch {
    return null;
  }
}

function decideSelectedMode(
  requestedMode: RuntimeModePreference,
  payloadState: RuntimeStatus["payloadState"],
  payloadIssue?: string
): RuntimeDecision {
  const issueClause = payloadIssue ? trimTrailingPunctuation(payloadIssue) : undefined;

  if (requestedMode === "external-gateway") {
    return {
      selectedMode: "external-gateway",
      modeReason: "Runtime mode is pinned to external-gateway by configuration.",
      message: "Bundled runtime detection completed, but current configuration is forcing external Gateway mode."
    };
  }

  if (payloadState === "ready" && requestedMode === "bundled-runtime") {
    return {
      selectedMode: "bundled-runtime",
      modeReason: "Bundled runtime was explicitly requested and the payload layout is ready. Electron Main can now attempt managed startup.",
      message: "Bundled runtime payload is ready and selected. Start the managed runtime to activate it."
    };
  }

  if (payloadState === "ready") {
    return {
      selectedMode: "bundled-runtime",
      modeReason: "Auto mode selected bundled-runtime because the payload layout is ready.",
      message: "Bundled runtime payload is ready and preferred in auto mode. Start the managed runtime to activate it."
    };
  }

  if (requestedMode === "bundled-runtime") {
    return {
      selectedMode: "external-gateway",
      modeReason: issueClause
        ? `Bundled runtime was requested, but ${issueClause}, so Electron Main is falling back to external-gateway mode.`
        : "Bundled runtime was requested, but the payload layout is incomplete, so Electron Main is falling back to external-gateway mode.",
      message: issueClause
        ? `Bundled runtime was requested, but ${issueClause}, so the app is staying in external Gateway mode.`
        : "Bundled runtime was requested, but the payload layout is incomplete, so the app is staying in external Gateway mode."
    };
  }

  return {
    selectedMode: "external-gateway",
    modeReason: issueClause
      ? `Auto mode selected external-gateway because ${issueClause}.`
      : payloadState === "placeholder"
        ? "Auto mode selected external-gateway because the bundled runtime directory exists but the payload is incomplete."
        : "Auto mode selected external-gateway because no bundled runtime payload is present.",
    message: issueClause
      ? `Bundled runtime is not ready because ${issueClause}, so the app is staying in external Gateway mode.`
      : payloadState === "placeholder"
        ? "Bundled runtime directory is present, but the executable payload is incomplete, so the app is staying in external Gateway mode."
        : "No bundled runtime payload is present yet, so the app is staying in external Gateway mode."
  };
}

export class RuntimeManager extends EventEmitter {
  private readonly vendorRuntimeDir: string;
  private readonly runtimeDataDir: string;
  private readonly logFilePath: string;
  private readonly managedConfigResolver?: ManagedConfigResolver;
  private readonly strictBundledRuntime: boolean;
  private requestedMode: RuntimeModePreference;
  private readonly logs: LogEntry[] = [];
  private runtimeStatus: RuntimeStatus;
  private child?: ChildProcess;
  private managedChildPid?: number;
  private payloadState: RuntimeStatus["payloadState"] = "missing";
  private detectedFiles: string[] = [];
  private missingFiles: string[] = [];
  private gatewayConnection: RuntimeGatewayConnection = {};
  private pythonReady = false;
  private pythonVersion?: string;
  private installedPythonPackages: string[] = [];
  private pythonMissingModules: string[] = [];
  private payloadIssue?: string;
  private checkedAt?: string;
  private lastStoppedAt?: string;
  private lastStartedAt?: string;
  private lastExitCode?: number | null;
  private lastError?: string;
  private lastStderrLines: string[] = [];
  private startPromise?: Promise<RuntimeStatus>;
  private reusedExistingGateway = false;
  constructor(options: RuntimeManagerOptions) {
    super();
    this.vendorRuntimeDir = options.vendorRuntimeDir;
    this.runtimeDataDir = options.runtimeDataDir;
    this.logFilePath = options.logFilePath;
    this.requestedMode = options.requestedMode ?? "auto";
    this.managedConfigResolver = options.managedConfigResolver;
    this.strictBundledRuntime = options.strictBundledRuntime ?? false;

    const resolved = this.resolveBundledPaths();
    this.runtimeStatus = {
      requestedMode: this.requestedMode,
      selectedMode: "external-gateway",
      activeMode: "external-gateway",
      payloadState: "missing",
      processState: "stopped",
      runtimeDir: resolved.runtimeDir,
      nodeExecutable: resolved.nodeExecutable,
      openClawEntry: resolved.openClawEntry,
      defaultConfigPath: resolved.defaultConfigPath,
      pythonExecutable: resolved.pythonExecutable,
      pythonManifestPath: resolved.pythonManifestPath,
      pythonReady: false,
      pythonVersion: undefined,
      installedPythonPackages: [],
      pythonMissingModules: [...PYTHON_RUNTIME_PROBE_NAMES],
      runtimeDataDir: resolved.runtimeDataDir,
      runtimeStateDir: resolved.runtimeStateDir,
      runtimeLogsDir: resolved.runtimeLogsDir,
      logFilePath: resolved.logFilePath,
      gatewayUrl: undefined,
      gatewayTokenConfigured: false,
      pid: undefined,
      startedAt: undefined,
      stoppedAt: undefined,
      lastExitCode: undefined,
      lastError: undefined,
      message: "Bundled runtime has not been probed yet.",
      modeReason: "Runtime mode decision has not been evaluated yet.",
      detectedFiles: [],
      missingFiles: [resolved.nodeExecutable, resolved.openClawEntry, resolved.packagedOpenClawEntry, resolved.runtimeManifestPath, resolved.defaultConfigPath, resolved.pythonExecutable, resolved.pythonManifestPath],
      checkedAt: undefined
    };

    this.appendLog("info", `Runtime manager initialized with requested mode ${this.requestedMode}.`);
  }

  async configure(): Promise<void> {
    await this.detectRuntime();
  }

  async setRequestedMode(requestedMode: RuntimeModePreference): Promise<RuntimeStatus> {
    this.requestedMode = requestedMode;
    this.appendLog("info", `Runtime mode preference updated to ${requestedMode}.`);
    await this.detectRuntime();
    return this.status();
  }

  resolveBundledPaths(): RuntimeResolvedPaths {
    const ffmpegExecutable = resolveBundledFfmpegExecutable(this.vendorRuntimeDir);
    const ffprobeExecutable = resolveBundledFfprobeExecutable(this.vendorRuntimeDir, ffmpegExecutable);
    return {
      runtimeDir: this.vendorRuntimeDir,
      nodeExecutable: path.join(this.vendorRuntimeDir, "node", "bin", "node"),
      openClawEntry: path.join(this.vendorRuntimeDir, "openclaw", "index.js"),
      packagedOpenClawEntry: path.join(this.vendorRuntimeDir, "openclaw", "package", "openclaw.mjs"),
      runtimeManifestPath: path.join(this.vendorRuntimeDir, "runtime-manifest.json"),
      defaultConfigPath: path.join(this.vendorRuntimeDir, "config", "openclaw.json"),
      pythonExecutable: path.join(this.vendorRuntimeDir, "python", "bin", "python3"),
      pythonManifestPath: path.join(this.vendorRuntimeDir, "python", "python-manifest.json"),
      ffmpegExecutable,
      ffprobeExecutable,
      playwrightBrowsersPath: path.join(this.vendorRuntimeDir, "playwright-browsers"),
      managedConfigPath: path.join(this.runtimeDataDir, "state", "openclaw.runtime.json"),
      readmePath: path.join(this.vendorRuntimeDir, "README.md"),
      runtimeDataDir: this.runtimeDataDir,
      runtimeStateDir: path.join(this.runtimeDataDir, "state"),
      runtimeLogsDir: path.join(this.runtimeDataDir, "logs"),
      logFilePath: this.logFilePath
    };
  }

  async detectRuntime(): Promise<boolean> {
    const paths = this.resolveBundledPaths();
    const [
      runtimeDirExists,
      nodeExists,
      openClawExists,
      packagedOpenClawExists,
      runtimeManifestExists,
      configExists,
      pythonExists,
      pythonManifestExists,
      readmeExists,
      gatewayConnection
    ] = await Promise.all([
      pathExists(paths.runtimeDir),
      pathExists(paths.nodeExecutable),
      pathExists(paths.openClawEntry),
      pathExists(paths.packagedOpenClawEntry),
      pathExists(paths.runtimeManifestPath),
      pathExists(paths.defaultConfigPath),
      pathExists(paths.pythonExecutable),
      pathExists(paths.pythonManifestPath),
      pathExists(paths.readmePath),
      this.readGatewayConnection(paths.defaultConfigPath)
    ]);

    let pythonProbe = pythonExists && pythonManifestExists
      ? await probePythonPayload(paths.pythonExecutable)
      : {
          ready: false,
          installedPackages: [],
          missingModules: [...PYTHON_RUNTIME_PROBE_NAMES],
          importErrors: {},
          error: undefined
        };

    if (!pythonProbe.ready && pythonManifestExists) {
      const manifestProbe = await probePythonPayloadFromManifest(paths.pythonManifestPath);
      if (manifestProbe?.ready) {
        pythonProbe = manifestProbe;
        this.appendLog(
          "warn",
          "Bundled Python direct probe failed; using python-manifest fallback for payload validation."
        );
      }
    }

    this.pythonReady = pythonProbe.ready;
    this.pythonVersion = pythonProbe.pythonVersion;
    this.installedPythonPackages = pythonProbe.installedPackages;
    this.pythonMissingModules = pythonProbe.missingModules;

    this.detectedFiles = [
      runtimeDirExists ? paths.runtimeDir : null,
      nodeExists ? paths.nodeExecutable : null,
      openClawExists ? paths.openClawEntry : null,
      packagedOpenClawExists ? paths.packagedOpenClawEntry : null,
      runtimeManifestExists ? paths.runtimeManifestPath : null,
      configExists ? paths.defaultConfigPath : null,
      pythonExists ? paths.pythonExecutable : null,
      pythonManifestExists ? paths.pythonManifestPath : null,
      paths.ffmpegExecutable ?? null,
      paths.ffprobeExecutable ?? null,
      readmeExists ? paths.readmePath : null
    ].filter((value): value is string => Boolean(value));

    this.missingFiles = [
      runtimeDirExists ? null : paths.runtimeDir,
      nodeExists ? null : paths.nodeExecutable,
      openClawExists ? null : paths.openClawEntry,
      packagedOpenClawExists ? null : paths.packagedOpenClawEntry,
      runtimeManifestExists ? null : paths.runtimeManifestPath,
      configExists ? null : paths.defaultConfigPath,
      pythonExists ? null : paths.pythonExecutable,
      pythonManifestExists ? null : paths.pythonManifestPath
    ].filter((value): value is string => Boolean(value));

    const hasCoreFiles = nodeExists
      && openClawExists
      && packagedOpenClawExists
      && runtimeManifestExists
      && configExists
      && pythonExists
      && pythonManifestExists;

    this.payloadState = hasCoreFiles && pythonProbe.ready
      ? "ready"
      : runtimeDirExists || readmeExists
        ? "placeholder"
        : "missing";

    this.payloadIssue = this.payloadState === "ready"
      ? undefined
      : formatPayloadIssue(this.missingFiles, pythonProbe, paths.pythonExecutable);
    this.gatewayConnection = gatewayConnection;
    this.checkedAt = new Date().toISOString();

    if (this.payloadState === "ready") {
      this.appendLog("info", `Bundled runtime payload is ready at ${this.vendorRuntimeDir}.`);
    } else if (this.payloadState === "placeholder") {
      const issue = this.payloadIssue
        ? trimTrailingPunctuation(this.payloadIssue)
        : "the bundled runtime payload did not pass validation";
      this.appendLog("warn", `Bundled runtime directory exists at ${this.vendorRuntimeDir}, but ${issue}.`);
    } else {
      const issue = this.payloadIssue
        ? trimTrailingPunctuation(this.payloadIssue)
        : `Bundled runtime payload is missing at ${this.vendorRuntimeDir}`;
      this.appendLog("warn", `${issue}.`);
    }

    this.refreshStatus();
    return this.payloadState === "ready";
  }

  async start(): Promise<RuntimeStatus> {
    if (this.startPromise) {
      this.appendLog("info", "Bundled runtime startup is already in progress.");
      return this.startPromise;
    }

    const startOperation = this.performStart().finally(() => {
      if (this.startPromise === startOperation) {
        this.startPromise = undefined;
      }
    });
    this.startPromise = startOperation;
    return startOperation;
  }

  private async performStart(): Promise<RuntimeStatus> {
    await this.detectRuntime();

    if (this.child && this.child.exitCode === null && !this.child.killed) {
      this.appendLog(
        "info",
        this.runtimeStatus.processState === "running"
          ? "Managed bundled runtime is already running."
          : "Managed bundled runtime process is already active."
      );
      this.lastError = undefined;
      this.refreshStatus(this.runtimeStatus.processState === "running" ? "running" : undefined);
      return this.status();
    }

    if (this.strictBundledRuntime && this.payloadState !== "ready") {
      this.lastError = this.payloadIssue
        ? `Bundled runtime is required for packaged builds, but ${trimTrailingPunctuation(this.payloadIssue)}.`
        : "Bundled runtime is required for packaged builds, but the payload is not ready.";
      this.appendLog("error", this.lastError);
      this.refreshStatus("error");
      return this.status();
    }

    const decision = decideSelectedMode(this.requestedMode, this.payloadState, this.payloadIssue);
    if (decision.selectedMode !== "bundled-runtime") {
      this.lastError = decision.message;
      this.appendLog("warn", decision.message);
      this.refreshStatus("error");
      return this.status();
    }

    if (this.payloadState !== "ready") {
      this.lastError = this.payloadIssue
        ? `Bundled runtime cannot start because ${trimTrailingPunctuation(this.payloadIssue)}.`
        : "Bundled runtime cannot start because the payload layout is incomplete.";
      this.appendLog("warn", this.lastError);
      this.refreshStatus("error");
      return this.status();
    }

    const paths = this.resolveBundledPaths();
    await mkdir(paths.runtimeDataDir, { recursive: true });
    await mkdir(paths.runtimeStateDir, { recursive: true });
    await mkdir(paths.runtimeLogsDir, { recursive: true });

    const childStdoutLogPath = path.join(paths.runtimeLogsDir, "runtime-child.stdout.log");
    const childStderrLogPath = path.join(paths.runtimeLogsDir, "runtime-child.stderr.log");
    await Promise.all([
      writeFile(childStdoutLogPath, "", "utf8"),
      writeFile(childStderrLogPath, "", "utf8")
    ]);

    let managedConfigPath: string;
    try {
      managedConfigPath = await this.prepareManagedConfig(paths, "init");
      this.gatewayConnection = await this.readGatewayConnection(managedConfigPath);
    } catch (error) {
      this.lastError = error instanceof Error ? error.message : String(error);
      this.appendLog("error", this.lastError);
      this.refreshStatus("error");
      return this.status();
    }

    const reusableGateway = await this.waitForReusableExistingGateway();
    if (reusableGateway.ready) {
      this.reusedExistingGateway = true;
      this.child = undefined;
      this.managedChildPid = undefined;
      this.lastError = undefined;
      this.lastExitCode = undefined;
      this.lastStartedAt = this.lastStartedAt ?? reusableGateway.checkedAt;
      this.appendLog(
        "info",
        `Reusing existing OpenClaw Gateway at ${this.gatewayConnection.url}${reusableGateway.version ? ` (version ${reusableGateway.version})` : ""}; status and health probes succeeded.`
      );
      this.refreshStatus("running");
      return this.status();
    }

    if (reusableGateway.lastError && !isGatewayProbeNoListener(reusableGateway.lastError)) {
      this.reusedExistingGateway = false;
      this.lastError = `Port already has a Gateway or service at ${this.gatewayConnection.url ?? "unknown"}, but it is not reusable: ${reusableGateway.lastError}`;
      this.appendLog("error", this.lastError);
      this.refreshStatus("error");
      return this.status();
    }

    this.reusedExistingGateway = false;
    if (reusableGateway.lastError) {
      this.appendLog("info", `No reusable Gateway is active at ${this.gatewayConnection.url ?? "unknown"}: ${reusableGateway.lastError}`);
    }

    this.lastError = undefined;
    this.lastExitCode = undefined;
    this.lastStartedAt = new Date().toISOString();
    this.refreshStatus("starting");

    const childArgs = [
      paths.openClawEntry,
      "gateway",
      "run",
      "--bind",
      "loopback",
      ...(this.gatewayConnection.url
        ? ["--port", String(new URL(this.gatewayConnection.url).port || 18789)]
        : []),
      ...(this.gatewayConnection.token
        ? ["--token", this.gatewayConnection.token]
        : [])
    ];

    this.appendLog("info", `Bundled runtime managed config: ${managedConfigPath}.`);
    this.appendLog("info", `Bundled runtime stdout log: ${childStdoutLogPath}.`);
    this.appendLog("info", `Bundled runtime stderr log: ${childStderrLogPath}.`);
    this.appendLog("info", `Launching bundled runtime command: ${paths.nodeExecutable} ${childArgs.join(" ")}.`);
    const childEnv = this.buildManagedChildEnv(paths, managedConfigPath);
    const spawnOptions = {
      cwd: paths.runtimeDir,
      env: childEnv,
      stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"]
    };

    this.managedChildPid = undefined;

    let child: ChildProcess;
    try {
      child = spawn(paths.nodeExecutable, childArgs, spawnOptions);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.lastError = classifyMacSecurityBlockMessage(message) ?? `Bundled runtime failed to spawn: ${message}`;
      this.appendLog("error", this.lastError);
      this.refreshStatus("error");
      return this.status();
    }

    this.child = child;
    this.lastStderrLines = [];
    child.stdout?.on("data", (chunk: Buffer) => {
      this.appendChunk("info", chunk, childStdoutLogPath);
    });
    child.stderr?.on("data", (chunk: Buffer) => {
      this.appendChunk("warn", chunk, childStderrLogPath);
      const lines = chunk.toString().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
      this.lastStderrLines.push(...lines);
      if (this.lastStderrLines.length > 50) {
        this.lastStderrLines = this.lastStderrLines.slice(-50);
      }
    });
    child.once("error", (error) => {
      this.lastError = classifyMacSecurityBlockMessage(error.message) ?? `Bundled runtime failed to start: ${error.message}`;
      this.lastStoppedAt = new Date().toISOString();
      this.child = undefined;
      this.managedChildPid = undefined;
      this.reusedExistingGateway = false;
      this.appendLog("error", this.lastError);
      this.refreshStatus("error");
    });
    child.once("exit", (code, signal) => {
      this.lastExitCode = code;
      this.lastStoppedAt = new Date().toISOString();
      const wasStopping = this.runtimeStatus.processState === "stopping";
      this.child = undefined;
      this.managedChildPid = undefined;
      this.reusedExistingGateway = false;
      if (!wasStopping && code !== 0) {
        const stderrHint = this.buildStderrHint();
        const exitMessage = `Bundled runtime exited unexpectedly with code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}${stderrHint ? `: ${stderrHint}` : ""}.`;
        this.lastError = classifyMacSecurityBlockMessage(stderrHint) ?? exitMessage;
        this.appendLog("error", this.lastError);
        this.refreshStatus("error");
        return;
      }
      this.appendLog("info", `Bundled runtime process exited${code != null ? ` with code ${code}` : ""}.`);
      this.refreshStatus("stopped");
    });

    this.appendLog("info", `Bundled runtime process started with pid ${this.child.pid ?? "unknown"}.`);
    this.appendLog("info", `Waiting for bundled Gateway readiness at ${this.gatewayConnection.url ?? "unknown"}.`);

    const readiness = await this.waitForGatewayReady();
    if (!readiness.ready) {
      const startupError = readiness.lastError
        ?? `Timed out while waiting for bundled Gateway readiness at ${this.gatewayConnection.url ?? "unknown"}.`;
      this.appendLog("error", startupError);
      await this.stop();
      this.lastError = startupError;
      this.refreshStatus("error");
      return this.status();
    }

    this.lastError = undefined;
    this.refreshStatus("running");
    return this.status();
  }

  async stop(): Promise<RuntimeStatus> {
    if ((!this.child || this.child.exitCode !== null || this.child.killed) && !this.managedChildPid) {
      this.lastError = undefined;
      this.reusedExistingGateway = false;
      this.refreshStatus("stopped");
      return this.status();
    }

    this.reusedExistingGateway = false;
    const child = this.child;
    const managedPid = this.managedChildPid;
    this.appendLog("info", `Stopping bundled runtime process ${managedPid ?? child?.pid ?? "unknown"}.`);
    this.refreshStatus("stopping");

    try {
      if (child) {
        child.kill("SIGTERM");
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.lastError = `Failed to stop bundled runtime: ${message}`;
      this.appendLog("error", this.lastError);
      this.refreshStatus("error");
      return this.status();
    }

    await delay(500);
    if (this.child && this.child.exitCode === null && !this.child.killed) {
      this.appendLog("warn", "Bundled runtime did not exit after SIGTERM; sending SIGKILL.");
      this.child.kill("SIGKILL");
      await delay(200);
    }
    if (!this.child) {
      this.lastError = undefined;
    }
    this.refreshStatus(this.child ? "stopping" : "stopped");
    return this.status();
  }

  async restart(): Promise<RuntimeStatus> {
    await this.stop();
    await delay(300);
    return this.start();
  }

  async syncManagedConfig(action: RuntimeCloudFetchAction = "sync"): Promise<RuntimeStatus> {
    await this.detectRuntime();
    if (this.payloadState !== "ready") {
      return this.status();
    }

    const paths = this.resolveBundledPaths();
    try {
      const managedConfigPath = await this.prepareManagedConfig(paths, action);
      this.gatewayConnection = await this.readGatewayConnection(managedConfigPath);
      this.appendLog("info", `Managed runtime config refreshed using ${action}.`);
      this.lastError = undefined;
    } catch (error) {
      this.lastError = error instanceof Error ? error.message : String(error);
      this.appendLog("error", `Managed runtime config refresh failed: ${this.lastError}`);
    }

    this.refreshStatus();
    return this.status();
  }

  async health(): Promise<RuntimeHealth> {
    this.refreshStatus();
    const checkedAt = new Date().toISOString();
    const startedAt = this.runtimeStatus.startedAt ? Date.parse(this.runtimeStatus.startedAt) : undefined;
    return {
      ok: this.runtimeStatus.processState === "running",
      checkedAt,
      processState: this.runtimeStatus.processState,
      selectedMode: this.runtimeStatus.selectedMode,
      activeMode: this.runtimeStatus.activeMode,
      pid: this.runtimeStatus.pid,
      gatewayUrl: this.runtimeStatus.gatewayUrl,
      uptimeMs: startedAt ? Math.max(0, Date.now() - startedAt) : undefined,
      details: this.runtimeStatus.message,
      lastError: this.runtimeStatus.lastError
    };
  }

  async getGatewayConnection(): Promise<RuntimeGatewayConnection> {
    return { ...this.gatewayConnection };
  }

  async status(): Promise<RuntimeStatus> {
    this.refreshStatus();
    return {
      ...this.runtimeStatus,
      detectedFiles: [...this.runtimeStatus.detectedFiles],
      missingFiles: [...this.runtimeStatus.missingFiles]
    };
  }

  async tailLogs(limit = 200): Promise<LogEntry[]> {
    return this.logs.slice(-limit);
  }

  private async waitForGatewayReady(): Promise<GatewayProbeResult> {
    if (!this.gatewayConnection.url) {
      return {
        ready: false,
        checkedAt: new Date().toISOString(),
        lastError: "Managed bundled runtime config did not define a Gateway URL."
      };
    }

    const deadline = Date.now() + GATEWAY_READY_TIMEOUT_MS;
    let lastProbeError: string | undefined;
    let lastLoggedProbeError: string | undefined;

    while (Date.now() < deadline) {
      if (!this.child || this.child.exitCode !== null || this.child.killed) {
        return {
          ready: false,
          checkedAt: new Date().toISOString(),
          lastError: this.lastError ?? "Bundled runtime exited before Gateway became ready."
        };
      }

      const probe = await probeGatewayReadiness(this.gatewayConnection.url, this.gatewayConnection.token, GATEWAY_PROBE_TIMEOUT_MS, {
        requireStatusHealth: true,
        probeLabel: "bundled"
      });
      if (probe.ready) {
        return probe;
      }

      lastProbeError = probe.lastError;
      if (lastProbeError && !isGatewayProbeStartupTransient(lastProbeError)) {
        return {
          ready: false,
          checkedAt: new Date().toISOString(),
          lastError: lastProbeError
        };
      }
      if (lastProbeError && lastProbeError !== lastLoggedProbeError) {
        this.appendLog("warn", `Bundled Gateway is not ready yet: ${lastProbeError}`);
        lastLoggedProbeError = lastProbeError;
      }

      await delay(GATEWAY_READY_POLL_INTERVAL_MS);
    }

    return {
      ready: false,
      checkedAt: new Date().toISOString(),
      lastError: lastProbeError
        ? `Bundled Gateway cold-start deadline expired at ${this.gatewayConnection.url}; last probe did not complete successfully.`
        : `Bundled Gateway cold-start deadline expired at ${this.gatewayConnection.url}.`
    };
  }

  private async waitForReusableExistingGateway(): Promise<GatewayProbeResult> {
    if (!this.gatewayConnection.url) {
      return {
        ready: false,
        checkedAt: new Date().toISOString(),
        lastError: "Managed bundled runtime config did not define a Gateway URL."
      };
    }

    const firstProbe = await probeGatewayReadiness(this.gatewayConnection.url, this.gatewayConnection.token, GATEWAY_PROBE_TIMEOUT_MS, {
      requireStatusHealth: true,
      probeLabel: "reusable"
    });
    if (firstProbe.ready || isGatewayProbeNoListener(firstProbe.lastError) || !isGatewayProbeStartupTransient(firstProbe.lastError)) {
      return firstProbe;
    }

    this.appendLog("info", `Existing Gateway at ${this.gatewayConnection.url} is starting; waiting for status and health probes to pass.`);
    const deadline = Date.now() + GATEWAY_REUSE_READY_TIMEOUT_MS;
    let lastProbe = firstProbe;
    let lastLoggedProbeError = firstProbe.lastError;

    while (Date.now() < deadline) {
      await delay(GATEWAY_READY_POLL_INTERVAL_MS);
      const probe = await probeGatewayReadiness(this.gatewayConnection.url, this.gatewayConnection.token, GATEWAY_PROBE_TIMEOUT_MS, {
        requireStatusHealth: true,
        probeLabel: "reusable"
      });
      if (probe.ready || isGatewayProbeNoListener(probe.lastError) || !isGatewayProbeStartupTransient(probe.lastError)) {
        return probe;
      }

      lastProbe = probe;
      if (probe.lastError && probe.lastError !== lastLoggedProbeError) {
        this.appendLog("warn", `Existing Gateway is not reusable yet: ${probe.lastError}`);
        lastLoggedProbeError = probe.lastError;
      }
    }

    return {
      ...lastProbe,
      ready: false,
      checkedAt: new Date().toISOString(),
      lastError: lastProbe.lastError ?? `Timed out while waiting for reusable Gateway at ${this.gatewayConnection.url}.`
    };
  }

  private async readGatewayConnection(configPath: string): Promise<RuntimeGatewayConnection> {
    try {
      const raw = await readFile(configPath, "utf8");
      const parsed = JSON.parse(raw.replace(/^\uFEFF/, "")) as OpenClawConfigShape;
      const gateway = parsed.gateway;
      return {
        url: buildGatewayUrl(gateway),
        token: typeof gateway?.auth?.token === "string" && gateway.auth.token.trim() ? gateway.auth.token : undefined
      };
    } catch {
      return {};
    }
  }

  private async prepareManagedConfig(paths: RuntimeResolvedPaths, action: RuntimeCloudFetchAction): Promise<string> {
    const raw = await readFile(paths.defaultConfigPath, "utf8");
    const parsed = JSON.parse(raw.replace(/^\uFEFF/, "")) as OpenClawConfigShape;
    const workspacePath = path.join(paths.runtimeDataDir, "workspace");
    await mkdir(workspacePath, { recursive: true });

    let nextConfig: OpenClawConfigShape = parsed;
    if (this.managedConfigResolver) {
      const resolvedConfig = await this.managedConfigResolver({
        action,
        defaultConfig: parsed as Record<string, unknown>,
        paths
      });
      nextConfig = resolvedConfig as OpenClawConfigShape;
    }

    const gateway = nextConfig.gateway ?? {};
    nextConfig.gateway = {
      ...gateway,
      mode: "local",
      bind: "loopback",
      port: typeof gateway.port === "number" ? gateway.port : 18889,
      auth: {
        ...(gateway.auth ?? {}),
        mode: "token",
        token: typeof gateway.auth?.token === "string" && gateway.auth.token.trim()
          ? gateway.auth.token
          : "qjc-bundled-runtime-token"
      }
    };

    nextConfig.agents = {
      ...(nextConfig.agents ?? {}),
      defaults: {
        ...(nextConfig.agents?.defaults ?? {}),
        workspace: workspacePath
      }
    };

    await mkdir(path.dirname(paths.managedConfigPath), { recursive: true });
    await writeFile(paths.managedConfigPath, JSON.stringify(nextConfig, null, 2), "utf8");
    return paths.managedConfigPath;
  }

  private refreshStatus(processState?: RuntimeProcessState): void {
    const paths = this.resolveBundledPaths();
    const currentProcessState = processState ?? this.inferProcessState();
    const decision = decideSelectedMode(this.requestedMode, this.payloadState, this.payloadIssue);
    const selectedMode = decision.selectedMode;
    const activeMode = selectedMode === "bundled-runtime" && (currentProcessState === "starting" || currentProcessState === "running" || currentProcessState === "stopping")
      ? "bundled-runtime"
      : "external-gateway";

    let message = decision.message;
    let modeReason = decision.modeReason;

    if (currentProcessState === "starting") {
      message = "Starting managed bundled runtime process.";
      modeReason = "Electron Main is launching the bundled runtime process. Gateway readiness is still pending.";
    } else if (currentProcessState === "running" && this.reusedExistingGateway) {
      message = "Reusing an existing local OpenClaw Gateway; status and health checks passed.";
      modeReason = "Bundled runtime is selected, and Electron Main found a compatible Gateway already listening on the configured port.";
    } else if (currentProcessState === "running") {
      message = "Managed bundled runtime process is running and Gateway is ready.";
      modeReason = "Bundled runtime is selected, the managed process is alive, and Gateway readiness has completed.";
    } else if (currentProcessState === "stopping") {
      message = "Stopping managed bundled runtime process.";
      modeReason = "Electron Main is shutting down the bundled runtime process.";
    } else if (currentProcessState === "error" && this.lastError) {
      message = this.lastError;
    }

    this.runtimeStatus = {
      requestedMode: this.requestedMode,
      selectedMode,
      activeMode,
      payloadState: this.payloadState,
      processState: currentProcessState,
      runtimeDir: paths.runtimeDir,
      nodeExecutable: paths.nodeExecutable,
      openClawEntry: paths.openClawEntry,
      defaultConfigPath: paths.defaultConfigPath,
      pythonExecutable: paths.pythonExecutable,
      pythonManifestPath: paths.pythonManifestPath,
      pythonReady: this.pythonReady,
      pythonVersion: this.pythonVersion,
      installedPythonPackages: [...this.installedPythonPackages],
      pythonMissingModules: [...this.pythonMissingModules],
      runtimeDataDir: paths.runtimeDataDir,
      runtimeStateDir: paths.runtimeStateDir,
      runtimeLogsDir: paths.runtimeLogsDir,
      logFilePath: paths.logFilePath,
      gatewayUrl: this.gatewayConnection.url,
      gatewayTokenConfigured: Boolean(this.gatewayConnection.token),
      pid: this.managedChildPid ?? this.child?.pid,
      startedAt: this.lastStartedAt,
      stoppedAt: this.lastStoppedAt,
      lastExitCode: this.lastExitCode,
      lastError: this.lastError,
      message,
      modeReason,
      detectedFiles: [...this.detectedFiles],
      missingFiles: [...this.missingFiles],
      checkedAt: this.checkedAt
    };
  }

  private buildStderrHint(): string | undefined {
    const recentRaw = this.lastStderrLines.join(" ");
    const securityBlock = classifyMacSecurityBlockMessage(recentRaw);
    if (securityBlock) {
      return securityBlock;
    }

    const recent = recentRaw.toLowerCase();
    if (recent.includes("name conflict") || recent.includes("hostname conflict") || recent.includes("bonjour")) {
      return "gateway name/hostname conflict detected (another OpenClaw instance is running)";
    }
    if (recent.includes("eaddrinuse") || recent.includes("address already in use")) {
      return "port already in use";
    }
    const lastLine = this.lastStderrLines[this.lastStderrLines.length - 1];
    return lastLine ?? undefined;
  }

  private inferProcessState(): RuntimeProcessState {
    if (this.runtimeStatus.processState === "stopping" && this.child) {
      return "stopping";
    }
    if (this.reusedExistingGateway && this.runtimeStatus.processState === "running" && !this.lastError) {
      return "running";
    }
    if (this.child && this.child.exitCode === null && !this.child.killed) {
      if (this.runtimeStatus.processState === "starting") {
        return "starting";
      }
      return "running";
    }
    if (this.lastError) {
      return "error";
    }
    return "stopped";
  }

  private buildManagedChildEnv(paths: RuntimeResolvedPaths, managedConfigPath: string): NodeJS.ProcessEnv {
    const childEnv: NodeJS.ProcessEnv = {};
    const inheritedKeys = [
      "HOME",
      "USER",
      "TMPDIR",
      "TEMP",
      "TMP",
      "SHELL",
      "LANG",
      "LC_ALL",
      "PATH",
      "XDG_CACHE_HOME",
      "XDG_CONFIG_HOME",
      "XDG_DATA_HOME",
      "XDG_RUNTIME_DIR",
      "SSL_CERT_FILE"
    ] as const;

    for (const key of inheritedKeys) {
      const value = process.env[key];
      if (typeof value === "string" && value.length > 0) {
        childEnv[key] = value;
      }
    }

    childEnv.HOME = this.runtimeDataDir;
    childEnv.OPENCLAW_HOME = this.runtimeDataDir;
    childEnv.OPENCLAW_STATE_DIR = paths.runtimeStateDir;
    childEnv.OPENCLAW_CONFIG_PATH = managedConfigPath;
    childEnv.QJCLAW_BUNDLED_RUNTIME_DIR = paths.runtimeDir;
    childEnv.PLAYWRIGHT_BROWSERS_PATH = paths.playwrightBrowsersPath;
    childEnv.PYTHONUTF8 = "1";
    childEnv.PYTHONIOENCODING = "utf-8";
    const resolvedFfmpeg = resolveInjectedBinaryPath("FFMPEG_BIN", paths.ffmpegExecutable, ["ffmpeg"]);
    const resolvedFfprobe = resolveInjectedBinaryPath("FFPROBE_BIN", paths.ffprobeExecutable, ["ffprobe"]);
    if (resolvedFfmpeg) {
      childEnv.FFMPEG_BIN = resolvedFfmpeg;
    }
    if (resolvedFfprobe) {
      childEnv.FFPROBE_BIN = resolvedFfprobe;
    }
    childEnv.PATH = [
      path.join(paths.runtimeDir, "node", "bin"),
      path.dirname(paths.pythonExecutable),
      paths.ffmpegExecutable ? path.dirname(paths.ffmpegExecutable) : null,
      paths.ffprobeExecutable ? path.dirname(paths.ffprobeExecutable) : null,
      process.env.PATH ?? ""
    ].filter(Boolean).join(path.delimiter);

    return childEnv;
  }

  private captureManagedChildPid(chunk: Buffer): void {
    const text = chunk.toString("utf8");
    const match = text.match(/__QJC_MANAGED_CHILD_PID__=(\d+)/);
    if (!match) {
      return;
    }

    const nextPid = Number(match[1]);
    if (!Number.isFinite(nextPid) || nextPid <= 0 || nextPid === this.managedChildPid) {
      return;
    }

    this.managedChildPid = nextPid;
    this.appendLog("info", `Bundled runtime child pid resolved to ${nextPid}.`);
    this.refreshStatus();
  }

  private appendChunk(level: LogEntry["level"], chunk: Buffer, processLogPath?: string): void {
    if (processLogPath) {
      try {
        mkdirSync(path.dirname(processLogPath), { recursive: true });
        appendFileSync(processLogPath, chunk);
      } catch {
        // Ignore child log persistence failures to avoid affecting runtime startup/shutdown.
      }
    }

    const text = chunk.toString("utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
    for (const line of text) {
      this.appendLog(level, line);
    }
  }

  private appendLog(level: LogEntry["level"], message: string): void {
    const entry: LogEntry = {
      ts: new Date().toISOString(),
      level,
      source: "runtime",
      message
    };
    this.logs.push(entry);
    try {
      mkdirSync(path.dirname(this.logFilePath), { recursive: true });
      appendFileSync(this.logFilePath, `[${entry.ts}] [${entry.level}] ${entry.message}\n`, "utf8");
    } catch {
      // Ignore log persistence failures to avoid affecting runtime startup/shutdown.
    }
    this.emit("log", entry);
  }
}
