import fs from "node:fs";
import { request } from "node:http";
import path from "node:path";
import type { Command } from "commander";
import type {
  ConfigFileSnapshot,
  GatewayAuthMode,
  GatewayBindMode,
  GatewayTailscaleMode,
  ReadConfigFileSnapshotWithPluginMetadataResult,
} from "../../config/config.js";
import { CONFIG_PATH, resolveGatewayPort, resolveStateDir } from "../../config/paths.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { hasConfiguredSecretInput } from "../../config/types.secrets.js";
import {
  defaultGatewayBindMode,
  isContainerEnvironment,
  isLoopbackHost,
  resolveGatewayBindHost,
} from "../../gateway/net.js";
import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setVerbose } from "../../globals.js";
import { isTruthyEnvValue } from "../../infra/env.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { GatewayLockError } from "../../infra/gateway-lock.js";
import type { RespawnSupervisor } from "../../infra/supervisor-markers.js";
import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js";
import { withDiagnosticPhase } from "../../logging/diagnostic-phase.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { defaultRuntime } from "../../runtime.js";
import {
  normalizeOptionalLowercaseString,
  normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { formatCliCommand } from "../command-format.js";
import { inheritOptionFromParent } from "../command-options.js";
import { formatInvalidConfigPort, formatInvalidPortOption } from "../error-format.js";
import { withProgress } from "../progress.js";
import { parsePort } from "../shared/parse-port.js";
import { installQaParentWatchdog } from "./qa-parent-watchdog.js";
import { runGatewayLoop } from "./run-loop.js";

type GatewayRunOpts = {
  port?: unknown;
  bind?: unknown;
  token?: unknown;
  auth?: unknown;
  password?: unknown;
  passwordFile?: unknown;
  tailscale?: unknown;
  tailscaleResetOnExit?: boolean;
  allowUnconfigured?: boolean;
  force?: boolean;
  verbose?: boolean;
  cliBackendLogs?: boolean;
  /** @deprecated Use cliBackendLogs. */
  claudeCliLogs?: boolean;
  wsLog?: unknown;
  compact?: boolean;
  rawStream?: boolean;
  rawStreamPath?: unknown;
  dev?: boolean;
  reset?: boolean;
};

const gatewayLog = createSubsystemLogger("gateway");

const GATEWAY_RUN_VALUE_KEYS = [
  "port",
  "bind",
  "token",
  "auth",
  "password",
  "passwordFile",
  "tailscale",
  "wsLog",
  "rawStreamPath",
] as const;

const GATEWAY_RUN_BOOLEAN_KEYS = [
  "tailscaleResetOnExit",
  "allowUnconfigured",
  "dev",
  "reset",
  "force",
  "verbose",
  "cliBackendLogs",
  "claudeCliLogs",
  "compact",
  "rawStream",
] as const;

const SUPERVISED_GATEWAY_LOCK_RETRY_MS = 5000;
const SUPERVISED_GATEWAY_LOCK_RETRY_TIMEOUT_MS = 30_000;
const SUPERVISED_GATEWAY_HEALTH_PROBE_TIMEOUT_MS = 1000;

type Awaitable<T> = T | Promise<T>;
type GatewayRunLogger = Pick<ReturnType<typeof createSubsystemLogger>, "info" | "warn">;

/**
 * EX_CONFIG (78) from sysexits.h — used for configuration errors so systemd
 * (via RestartPreventExitStatus=78) stops restarting instead of entering a
 * restart storm that can render low-resource hosts unresponsive.
 */
const EXIT_CONFIG_ERROR = 78;

const GATEWAY_AUTH_MODES: readonly GatewayAuthMode[] = [
  "none",
  "token",
  "password",
  "trusted-proxy",
];
const GATEWAY_TAILSCALE_MODES: readonly GatewayTailscaleMode[] = ["off", "serve", "funnel"];

const toOptionString = (value: unknown): string | undefined => {
  if (typeof value === "string") {
    return value;
  }
  if (typeof value === "number" || typeof value === "bigint") {
    return value.toString();
  }
  return undefined;
};

function extractGatewayMiskeys(parsed: unknown): {
  hasGatewayToken: boolean;
  hasRemoteToken: boolean;
} {
  if (!parsed || typeof parsed !== "object") {
    return { hasGatewayToken: false, hasRemoteToken: false };
  }
  const gateway = (parsed as Record<string, unknown>).gateway;
  if (!gateway || typeof gateway !== "object") {
    return { hasGatewayToken: false, hasRemoteToken: false };
  }
  const hasGatewayToken = "token" in (gateway as Record<string, unknown>);
  const remote = (gateway as Record<string, unknown>).remote;
  const hasRemoteToken =
    remote && typeof remote === "object" ? "token" in (remote as Record<string, unknown>) : false;
  return { hasGatewayToken, hasRemoteToken };
}

function createGatewayCliStartupTrace() {
  const enabled = isTruthyEnvValue(process.env.OPENCLAW_GATEWAY_STARTUP_TRACE);
  const started = performance.now();
  let last = started;
  const emit = (name: string, durationMs: number, totalMs: number) => {
    if (enabled) {
      gatewayLog.info(
        `startup trace: ${name} ${durationMs.toFixed(1)}ms total=${totalMs.toFixed(1)}ms`,
      );
    }
  };
  return {
    mark(name: string) {
      const now = performance.now();
      emit(name, now - last, now - started);
      last = now;
    },
    async measure<T>(name: string, run: () => Awaitable<T>): Promise<T> {
      const before = performance.now();
      try {
        return await withDiagnosticPhase(name, run);
      } finally {
        const now = performance.now();
        emit(name, now - before, now - started);
        last = now;
      }
    },
  };
}

function warnInlinePasswordFlag() {
  defaultRuntime.error(
    "Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.",
  );
}

async function resolveGatewayPasswordOption(opts: GatewayRunOpts): Promise<string | undefined> {
  const direct = toOptionString(opts.password);
  const file = toOptionString(opts.passwordFile);
  if (direct && file) {
    throw new Error("Use either --password or --password-file.");
  }
  if (file) {
    const { readSecretFromFile } = await import("../../acp/secret-file.js");
    return readSecretFromFile(file, "Gateway password");
  }
  return direct;
}

function parseEnumOption<T extends string>(
  raw: string | undefined,
  allowed: readonly T[],
): T | null {
  if (!raw) {
    return null;
  }
  return (allowed as readonly string[]).includes(raw) ? (raw as T) : null;
}

function formatModeChoices(modes: readonly string[]): string {
  return modes.map((mode) => `"${mode}"`).join("|");
}

function formatModeErrorList(modes: readonly string[]): string {
  const quoted = modes.map((mode) => `"${mode}"`);
  if (quoted.length === 0) {
    return "";
  }
  if (quoted.length === 1) {
    return quoted[0];
  }
  if (quoted.length === 2) {
    return `${quoted[0]} or ${quoted[1]}`;
  }
  return `${quoted.slice(0, -1).join(", ")}, or ${quoted[quoted.length - 1]}`;
}

function shouldBlockGatewayBindWithoutExplicitAuth(params: {
  bindHost: string;
  hasSharedSecret: boolean;
  resolvedAuthMode: GatewayAuthMode;
}): boolean {
  return (
    !isLoopbackHost(params.bindHost) &&
    !params.hasSharedSecret &&
    params.resolvedAuthMode !== "trusted-proxy"
  );
}

async function maybeLogPendingControlUiBuild(cfg: OpenClawConfig): Promise<void> {
  if (cfg.gateway?.controlUi?.enabled === false) {
    return;
  }
  if (toOptionString(cfg.gateway?.controlUi?.root)) {
    return;
  }
  const { resolveControlUiRootSync } = await import("../../infra/control-ui-assets.js");
  if (
    resolveControlUiRootSync({
      moduleUrl: import.meta.url,
      argv1: process.argv[1],
      cwd: process.cwd(),
    })
  ) {
    return;
  }
  gatewayLog.info(
    "Control UI assets are missing; first startup may spend a few seconds building them before the gateway binds. `pnpm gateway:watch` does not rebuild Control UI assets, so rerun `pnpm ui:build` after UI changes or use `pnpm ui:dev` while developing the Control UI. For a full local dist, run `pnpm build && pnpm ui:build`.",
  );
}

function getGatewayStartGuardErrors(params: {
  allowUnconfigured?: boolean;
  configExists: boolean;
  configAuditPath: string;
  mode: string | undefined;
}): string[] {
  if (params.allowUnconfigured || params.mode === "local") {
    return [];
  }
  if (!params.configExists) {
    return [
      `Missing config. Run \`${formatCliCommand("openclaw setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`,
    ];
  }
  if (params.mode === undefined) {
    return [
      [
        "Gateway start blocked: existing config is missing gateway.mode.",
        "Treat this as suspicious or clobbered config.",
        `Re-run \`${formatCliCommand("openclaw onboard --mode local")}\` or \`${formatCliCommand("openclaw setup")}\`, set gateway.mode=local manually, or pass --allow-unconfigured.`,
      ].join(" "),
      `Config write audit: ${params.configAuditPath}`,
    ];
  }
  return [
    `Gateway start blocked: set gateway.mode=local (current: ${params.mode}) or pass --allow-unconfigured.`,
    `Config write audit: ${params.configAuditPath}`,
  ];
}

async function readGatewayStartupConfig(params: {
  startupTrace: ReturnType<typeof createGatewayCliStartupTrace>;
}): Promise<{
  cfg: OpenClawConfig;
  snapshot: ConfigFileSnapshot | null;
  startupConfigSnapshotRead?: ReadConfigFileSnapshotWithPluginMetadataResult;
}> {
  const { readConfigFileSnapshotWithPluginMetadata } = await import("../../config/config.js");
  const snapshotRead: ReadConfigFileSnapshotWithPluginMetadataResult | null =
    await params.startupTrace.measure("cli.config-snapshot", () =>
      readConfigFileSnapshotWithPluginMetadata().catch(() => null),
    );
  const snapshot: ConfigFileSnapshot | null = snapshotRead?.snapshot ?? null;
  const cfg = snapshot?.config ?? {};
  return {
    cfg,
    snapshot,
    ...(snapshotRead ? { startupConfigSnapshotRead: snapshotRead } : {}),
  };
}

function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): GatewayRunOpts {
  const resolved: GatewayRunOpts = { ...opts };

  for (const key of GATEWAY_RUN_VALUE_KEYS) {
    const inherited = inheritOptionFromParent(command, key);
    if (key === "wsLog") {
      // wsLog has a child default ("auto"), so prefer inherited parent CLI value when present.
      resolved[key] = inherited ?? resolved[key];
      continue;
    }
    resolved[key] = resolved[key] ?? inherited;
  }

  for (const key of GATEWAY_RUN_BOOLEAN_KEYS) {
    const inherited = inheritOptionFromParent<boolean>(command, key);
    resolved[key] = Boolean(resolved[key] || inherited);
  }

  return resolved;
}

function isGatewayLockError(err: unknown): err is GatewayLockError {
  return (
    err instanceof GatewayLockError ||
    (!!err && typeof err === "object" && (err as { name?: string }).name === "GatewayLockError")
  );
}

function isGatewayAlreadyRunningLockError(err: unknown): boolean {
  if (!isGatewayLockError(err) || typeof err.message !== "string") {
    return false;
  }
  return (
    err.message.includes("gateway already running") ||
    err.message.includes("another gateway instance is already listening")
  );
}

function isHealthyGatewayLockError(err: unknown): boolean {
  return isGatewayAlreadyRunningLockError(err);
}

function resolveGatewayLockErrorExitCode(
  err: unknown,
  supervisor: RespawnSupervisor | null,
): number {
  if (supervisor === "systemd" && isGatewayAlreadyRunningLockError(err)) {
    return EXIT_CONFIG_ERROR;
  }
  return isHealthyGatewayLockError(err) ? 0 : 1;
}

function normalizeGatewayHealthProbeHost(host: string): string {
  if (host === "0.0.0.0" || host === "::") {
    return "127.0.0.1";
  }
  return host;
}

async function probeGatewayHealthz(params: {
  host: string;
  port: number;
  timeoutMs?: number;
}): Promise<boolean> {
  const timeoutMs = params.timeoutMs ?? SUPERVISED_GATEWAY_HEALTH_PROBE_TIMEOUT_MS;
  return await new Promise<boolean>((resolve) => {
    const req = request(
      {
        hostname: normalizeGatewayHealthProbeHost(params.host),
        port: params.port,
        path: "/healthz",
        method: "GET",
        timeout: timeoutMs,
      },
      (res) => {
        res.resume();
        resolve(typeof res.statusCode === "number" && res.statusCode < 500);
      },
    );
    req.once("timeout", () => {
      req.destroy();
      resolve(false);
    });
    req.once("error", () => {
      resolve(false);
    });
    req.end();
  });
}

async function runGatewayLoopWithSupervisedLockRecovery(params: {
  startLoop: () => Promise<void>;
  supervisor: RespawnSupervisor | null;
  port: number;
  healthHost: string;
  log: GatewayRunLogger;
  now?: () => number;
  sleep?: (ms: number) => Promise<void>;
  probeHealth?: (params: { host: string; port: number }) => Promise<boolean>;
  retryMs?: number;
  timeoutMs?: number;
}) {
  const supervisor = params.supervisor;
  if (!supervisor) {
    await params.startLoop();
    return;
  }

  const now = params.now ?? Date.now;
  const sleep =
    params.sleep ?? (async (ms: number) => await new Promise((resolve) => setTimeout(resolve, ms)));
  const probeHealth = params.probeHealth ?? ((probeParams) => probeGatewayHealthz(probeParams));
  const retryMs = params.retryMs ?? SUPERVISED_GATEWAY_LOCK_RETRY_MS;
  const timeoutMs = params.timeoutMs ?? SUPERVISED_GATEWAY_LOCK_RETRY_TIMEOUT_MS;
  const startedAt = now();

  for (;;) {
    try {
      await params.startLoop();
      return;
    } catch (err) {
      if (!isGatewayAlreadyRunningLockError(err)) {
        throw err;
      }

      if (await probeHealth({ host: params.healthHost, port: params.port })) {
        if (supervisor === "systemd") {
          throw new GatewayLockError(
            "gateway already running under systemd; existing gateway is healthy, exiting with code 78 to prevent a systemd Restart=always loop",
            err,
          );
        }
        params.log.info(
          `gateway already running under ${supervisor}; existing gateway is healthy, leaving it in control`,
        );
        return;
      }

      const elapsedMs = now() - startedAt;
      if (elapsedMs >= timeoutMs) {
        throw new GatewayLockError(
          `gateway already running under ${supervisor}; existing gateway did not become healthy after ${timeoutMs}ms`,
          err,
        );
      }

      const waitMs = Math.min(retryMs, Math.max(0, timeoutMs - elapsedMs));
      params.log.warn(
        `gateway already running under ${supervisor}; waiting ${waitMs}ms before retrying startup`,
      );
      await sleep(waitMs);
    }
  }
}

async function maybeWriteGatewayStartupFailureBundle(err: unknown): Promise<void> {
  const { writeDiagnosticStabilityBundleForFailureSync } =
    await import("../../logging/diagnostic-stability-bundle.js");
  const result = writeDiagnosticStabilityBundleForFailureSync("gateway.startup_failed", err);
  if ("message" in result) {
    gatewayLog.warn(result.message);
  }
}

async function runGatewayCommand(opts: GatewayRunOpts) {
  installQaParentWatchdog();
  const isDevProfile = normalizeOptionalLowercaseString(process.env.OPENCLAW_PROFILE) === "dev";
  const devMode = Boolean(opts.dev) || isDevProfile;
  if (opts.reset && !devMode) {
    defaultRuntime.error("Use --reset with --dev.");
    defaultRuntime.exit(1);
    return;
  }

  setVerbose(Boolean(opts.verbose));
  if (opts.cliBackendLogs || opts.claudeCliLogs) {
    setConsoleSubsystemFilter(["agent/cli-backend"]);
    process.env.OPENCLAW_CLI_BACKEND_LOG_OUTPUT = "1";
  }
  const wsLogRaw = (opts.compact ? "compact" : opts.wsLog) as string | undefined;
  const wsLogStyle: GatewayWsLogStyle =
    wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto";
  if (
    wsLogRaw !== undefined &&
    wsLogRaw !== "auto" &&
    wsLogRaw !== "compact" &&
    wsLogRaw !== "full"
  ) {
    defaultRuntime.error('Invalid --ws-log. Use "auto", "full", or "compact".');
    defaultRuntime.exit(1);
  }
  setGatewayWsLogStyle(wsLogStyle);

  if (opts.rawStream) {
    process.env.OPENCLAW_RAW_STREAM = "1";
  }
  const rawStreamPath = toOptionString(opts.rawStreamPath);
  if (rawStreamPath) {
    process.env.OPENCLAW_RAW_STREAM_PATH = rawStreamPath;
  }

  const startupTrace = createGatewayCliStartupTrace();

  // The heaviest part of gateway startup is loading the server module tree
  // (channels, plugins, HTTP stack, etc.). Show a spinner so the user sees
  // progress instead of a silent 15-20 s pause (especially on Windows/NTFS).
  const { startGatewayServer } = await startupTrace.measure("cli.server-import", () =>
    withProgress(
      { label: "Loading gateway modules…", indeterminate: true },
      async () => import("../../gateway/server.js"),
    ),
  );

  setConsoleTimestampPrefix(true);

  if (devMode) {
    const { ensureDevGatewayConfig } = await import("./dev.js");
    await startupTrace.measure("cli.dev-config", () =>
      ensureDevGatewayConfig({ reset: Boolean(opts.reset) }),
    );
  }

  gatewayLog.info("loading configuration…");
  const { cfg, snapshot, startupConfigSnapshotRead } = await readGatewayStartupConfig({
    startupTrace,
  });
  void maybeLogPendingControlUiBuild(cfg).catch((err) => {
    gatewayLog.warn(`Control UI asset check failed: ${String(err)}`);
  });
  const portOverride = parsePort(opts.port);
  if (opts.port !== undefined && portOverride === null) {
    defaultRuntime.error(formatInvalidPortOption("--port"));
    defaultRuntime.exit(1);
    return;
  }
  const port = portOverride ?? resolveGatewayPort(cfg);
  if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
    defaultRuntime.error(formatInvalidConfigPort("gateway.port"));
    defaultRuntime.exit(1);
    return;
  }
  const { formatFutureConfigActionBlock, resolveFutureConfigActionBlock } =
    await import("../../config/future-version-guard.js");
  const futureStartupBlock = resolveFutureConfigActionBlock({
    action: "start the gateway service",
    snapshot,
  });
  if (futureStartupBlock && process.env.OPENCLAW_SERVICE_MARKER?.trim()) {
    defaultRuntime.error(formatFutureConfigActionBlock(futureStartupBlock));
    defaultRuntime.exit(78);
    return;
  }
  const futureForceBlock = opts.force
    ? resolveFutureConfigActionBlock({
        action: "force-kill gateway port listeners",
        snapshot,
      })
    : null;
  if (futureForceBlock) {
    defaultRuntime.error(formatFutureConfigActionBlock(futureForceBlock));
    defaultRuntime.exit(1);
    return;
  }
  // Only capture the *explicit* bind value here.  The container-aware
  // default is deferred until after Tailscale mode is known (see below)
  // so that Tailscale's loopback constraint is respected.
  const VALID_BIND_MODES = new Set<string>(["loopback", "lan", "auto", "custom", "tailnet"]);
  const bindExplicitRawStr = normalizeOptionalString(
    toOptionString(opts.bind) ?? cfg.gateway?.bind,
  );
  if (bindExplicitRawStr !== undefined && !VALID_BIND_MODES.has(bindExplicitRawStr)) {
    defaultRuntime.error('Invalid --bind. Use "loopback", "lan", "tailnet", "auto", or "custom".');
    defaultRuntime.exit(1);
    return;
  }
  const bindExplicitRaw = bindExplicitRawStr as GatewayBindMode | undefined;
  if (process.env.OPENCLAW_SERVICE_MARKER?.trim()) {
    const { cleanStaleGatewayProcessesSync } = await import("../../infra/restart-stale-pids.js");
    const stale = cleanStaleGatewayProcessesSync(port);
    if (stale.length > 0) {
      gatewayLog.info(
        `service-mode: cleared ${stale.length} stale gateway pid(s) before bind on port ${port}`,
      );
    }
  }
  if (opts.force) {
    try {
      const { forceFreePortAndWait, waitForPortBindable } = await import("../ports.js");
      const { killed, waitedMs, escalatedToSigkill } = await forceFreePortAndWait(port, {
        timeoutMs: 2000,
        intervalMs: 100,
        sigtermTimeoutMs: 700,
      });
      if (killed.length === 0) {
        gatewayLog.info(`force: no listeners on port ${port}`);
      } else {
        for (const proc of killed) {
          gatewayLog.info(
            `force: killed pid ${proc.pid}${proc.command ? ` (${proc.command})` : ""} on port ${port}`,
          );
        }
        if (escalatedToSigkill) {
          gatewayLog.info(`force: escalated to SIGKILL while freeing port ${port}`);
        }
        if (waitedMs > 0) {
          gatewayLog.info(`force: waited ${waitedMs}ms for port ${port} to free`);
        }
      }
      // After killing, verify the port is actually bindable (handles TIME_WAIT).
      const bindProbeHost =
        bindExplicitRaw === "loopback"
          ? "127.0.0.1"
          : bindExplicitRaw === "lan"
            ? "0.0.0.0"
            : bindExplicitRaw === "custom"
              ? toOptionString(cfg.gateway?.customBindHost)
              : undefined;
      const bindWaitMs = await waitForPortBindable(port, {
        timeoutMs: 3000,
        intervalMs: 150,
        host: bindProbeHost,
      });
      if (bindWaitMs > 0) {
        gatewayLog.info(`force: waited ${bindWaitMs}ms for port ${port} to become bindable`);
      }
    } catch (err) {
      defaultRuntime.error(
        `Could not free port ${port}: ${formatErrorMessage(err)}. Run ${formatCliCommand("openclaw gateway status --deep")} to inspect the listener.`,
      );
      defaultRuntime.exit(1);
      return;
    }
  }
  if (opts.token) {
    const token = toOptionString(opts.token);
    if (token) {
      process.env.OPENCLAW_GATEWAY_TOKEN = token;
    }
  }
  const authModeRaw = toOptionString(opts.auth);
  const authMode = parseEnumOption(authModeRaw, GATEWAY_AUTH_MODES);
  if (authModeRaw && !authMode) {
    defaultRuntime.error(`Invalid --auth. Use ${formatModeErrorList(GATEWAY_AUTH_MODES)}.`);
    defaultRuntime.exit(1);
    return;
  }
  const tailscaleRaw = toOptionString(opts.tailscale);
  const tailscaleMode = parseEnumOption(tailscaleRaw, GATEWAY_TAILSCALE_MODES);
  if (tailscaleRaw && !tailscaleMode) {
    defaultRuntime.error(
      `Invalid --tailscale. Use ${formatModeErrorList(GATEWAY_TAILSCALE_MODES)}.`,
    );
    defaultRuntime.exit(1);
    return;
  }
  // Now that Tailscale mode is known, compute the effective bind mode.
  const effectiveTailscaleMode = tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off";
  const bind = (bindExplicitRaw ?? defaultGatewayBindMode(effectiveTailscaleMode)) as
    | "loopback"
    | "lan"
    | "auto"
    | "custom"
    | "tailnet";

  let passwordRaw: string | undefined;
  try {
    passwordRaw = await resolveGatewayPasswordOption(opts);
  } catch (err) {
    defaultRuntime.error(formatErrorMessage(err));
    defaultRuntime.exit(1);
    return;
  }
  if (toOptionString(opts.password)) {
    warnInlinePasswordFlag();
  }
  const tokenRaw = toOptionString(opts.token);

  gatewayLog.info("resolving authentication…");
  const configExists = snapshot?.exists ?? fs.existsSync(CONFIG_PATH);
  const configAuditPath = path.join(resolveStateDir(process.env), "logs", "config-audit.jsonl");
  const effectiveCfg = snapshot?.valid ? snapshot.config : cfg;
  const mode = effectiveCfg.gateway?.mode;
  const guardErrors = getGatewayStartGuardErrors({
    allowUnconfigured: opts.allowUnconfigured,
    configExists,
    configAuditPath,
    mode,
  });
  if (guardErrors.length > 0) {
    for (const error of guardErrors) {
      defaultRuntime.error(error);
    }
    defaultRuntime.exit(EXIT_CONFIG_ERROR);
    return;
  }
  const miskeys = extractGatewayMiskeys(snapshot?.parsed);
  const authOverride =
    authMode || passwordRaw || tokenRaw || authModeRaw
      ? {
          ...(authMode ? { mode: authMode } : {}),
          ...(tokenRaw ? { token: tokenRaw } : {}),
          ...(passwordRaw ? { password: passwordRaw } : {}),
        }
      : undefined;
  const { resolveGatewayAuth } = await import("../../gateway/auth.js");
  const resolvedAuth = await startupTrace.measure("cli.auth-resolve", () =>
    resolveGatewayAuth({
      authConfig: cfg.gateway?.auth,
      authOverride,
      env: process.env,
      tailscaleMode: tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off",
    }),
  );
  const resolvedAuthMode = resolvedAuth.mode;
  const tokenValue = resolvedAuth.token;
  const passwordValue = resolvedAuth.password;
  const hasToken = typeof tokenValue === "string" && tokenValue.trim().length > 0;
  const hasPassword = typeof passwordValue === "string" && passwordValue.trim().length > 0;
  const tokenConfigured =
    hasToken ||
    hasConfiguredSecretInput(
      authOverride?.token ?? cfg.gateway?.auth?.token,
      cfg.secrets?.defaults,
    );
  const passwordConfigured =
    hasPassword ||
    hasConfiguredSecretInput(
      authOverride?.password ?? cfg.gateway?.auth?.password,
      cfg.secrets?.defaults,
    );
  const hasSharedSecret =
    (resolvedAuthMode === "token" && tokenConfigured) ||
    (resolvedAuthMode === "password" && passwordConfigured);
  const authHints: string[] = [];
  if (miskeys.hasGatewayToken) {
    authHints.push('Found "gateway.token" in config. Use "gateway.auth.token" instead.');
  }
  if (miskeys.hasRemoteToken) {
    authHints.push(
      '"gateway.remote.token" is for remote CLI calls; it does not enable local gateway auth.',
    );
  }
  if (resolvedAuthMode === "password" && !passwordConfigured) {
    defaultRuntime.error(
      [
        "Gateway auth is set to password, but no password is configured.",
        "Set gateway.auth.password (or OPENCLAW_GATEWAY_PASSWORD), or pass --password.",
        ...authHints,
      ]
        .filter(Boolean)
        .join("\n"),
    );
    defaultRuntime.exit(EXIT_CONFIG_ERROR);
    return;
  }
  if (resolvedAuthMode === "none") {
    gatewayLog.warn(
      "Gateway auth mode=none explicitly configured; all gateway connections are unauthenticated.",
    );
  }
  const healthHost = await resolveGatewayBindHost(bind, cfg.gateway?.customBindHost);
  if (
    shouldBlockGatewayBindWithoutExplicitAuth({
      bindHost: healthHost,
      hasSharedSecret,
      resolvedAuthMode,
    })
  ) {
    defaultRuntime.error(
      [
        `Refusing to bind gateway to ${bind} without auth.`,
        ...(isContainerEnvironment()
          ? [
              "Container environment detected \u2014 the gateway defaults to bind=auto (0.0.0.0) for port-forwarding compatibility.",
              "Set OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD, or pass --token/--password to start with auth.",
            ]
          : [
              "Set gateway.auth.token/password (or OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD) or pass --token/--password.",
            ]),
        ...authHints,
      ]
        .filter(Boolean)
        .join("\n"),
    );
    defaultRuntime.exit(EXIT_CONFIG_ERROR);
    return;
  }
  const tailscaleOverride =
    tailscaleMode || opts.tailscaleResetOnExit
      ? {
          ...(tailscaleMode ? { mode: tailscaleMode } : {}),
          ...(opts.tailscaleResetOnExit ? { resetOnExit: true } : {}),
        }
      : undefined;

  gatewayLog.info("starting...");
  startupTrace.mark("cli.gateway-loop");
  let startupConfigSnapshotReadForNextStart = startupConfigSnapshotRead;
  const startLoop = async () =>
    await runGatewayLoop({
      runtime: defaultRuntime,
      lockPort: port,
      healthHost,
      start: async ({ startupStartedAt } = {}) => {
        const startupConfigSnapshotReadForThisStart = startupConfigSnapshotReadForNextStart;
        startupConfigSnapshotReadForNextStart = undefined;
        return await startGatewayServer(port, {
          bind,
          auth: authOverride,
          tailscale: tailscaleOverride,
          startupStartedAt,
          ...(startupConfigSnapshotReadForThisStart
            ? { startupConfigSnapshotRead: startupConfigSnapshotReadForThisStart }
            : {}),
        });
      },
    });

  const { detectRespawnSupervisor } = await import("../../infra/supervisor-markers.js");
  const supervisor = detectRespawnSupervisor(process.env);
  try {
    await runGatewayLoopWithSupervisedLockRecovery({
      startLoop,
      supervisor,
      port,
      healthHost,
      log: gatewayLog,
    });
  } catch (err) {
    if (isGatewayLockError(err)) {
      const errMessage = formatErrorMessage(err);
      defaultRuntime.error(
        `Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: ${formatCliCommand("openclaw gateway stop")}`,
      );
      try {
        const { formatPortDiagnostics, inspectPortUsage } = await import("../../infra/ports.js");
        const diagnostics = await inspectPortUsage(port);
        if (diagnostics.status === "busy") {
          for (const line of formatPortDiagnostics(diagnostics)) {
            defaultRuntime.error(line);
          }
        }
      } catch {
        // ignore diagnostics failures
      }
      const { maybeExplainGatewayServiceStop } = await import("./shared.js");
      await maybeExplainGatewayServiceStop();
      defaultRuntime.exit(resolveGatewayLockErrorExitCode(err, supervisor));
      return;
    }
    await maybeWriteGatewayStartupFailureBundle(err);
    defaultRuntime.error(
      `Gateway failed to start: ${formatErrorMessage(err)}. Run ${formatCliCommand("openclaw gateway status --deep")} for diagnostics.`,
    );
    defaultRuntime.exit(1);
  }
}

export const __testing = {
  normalizeGatewayHealthProbeHost,
  resolveGatewayLockErrorExitCode,
  runGatewayLoopWithSupervisedLockRecovery,
};

export function addGatewayRunCommand(cmd: Command): Command {
  return cmd
    .option("--port <port>", "Port for the gateway WebSocket")
    .option(
      "--bind <mode>",
      'Bind mode ("loopback"|"lan"|"tailnet"|"auto"|"custom"). Defaults to config gateway.bind (or loopback).',
    )
    .option(
      "--token <token>",
      "Shared token required in connect.params.auth.token (default: OPENCLAW_GATEWAY_TOKEN env if set)",
    )
    .option("--auth <mode>", `Gateway auth mode (${formatModeChoices(GATEWAY_AUTH_MODES)})`)
    .option("--password <password>", "Password for auth mode=password")
    .option("--password-file <path>", "Read gateway password from file")
    .option(
      "--tailscale <mode>",
      `Tailscale exposure mode (${formatModeChoices(GATEWAY_TAILSCALE_MODES)})`,
    )
    .option(
      "--tailscale-reset-on-exit",
      "Reset Tailscale serve/funnel configuration on shutdown",
      false,
    )
    .option(
      "--allow-unconfigured",
      "Allow gateway start without enforcing gateway.mode=local in config (does not repair config)",
      false,
    )
    .option("--dev", "Create a dev config + workspace if missing (no BOOTSTRAP.md)", false)
    .option(
      "--reset",
      "Reset dev config + credentials + sessions + workspace (requires --dev)",
      false,
    )
    .option("--force", "Kill any existing listener on the target port before starting", false)
    .option("--verbose", "Verbose logging to stdout/stderr", false)
    .option(
      "--cli-backend-logs",
      "Only show CLI backend logs in the console (includes stdout/stderr)",
      false,
    )
    .option("--claude-cli-logs", "Deprecated alias for --cli-backend-logs", false)
    .option("--ws-log <style>", 'WebSocket log style ("auto"|"full"|"compact")', "auto")
    .option("--compact", 'Alias for "--ws-log compact"', false)
    .option("--raw-stream", "Log raw model stream events to jsonl", false)
    .option("--raw-stream-path <path>", "Raw stream jsonl path")
    .action(async (opts, command) => {
      await runGatewayCommand(resolveGatewayRunOptions(opts, command));
    });
}
