import fs from "node:fs";
import { CONFIG_PATH, loadConfig, readConfigFileSnapshot, resolveGatewayPort, } from "../../config/config.js";
import { resolveGatewayAuth } from "../../gateway/auth.js";
import { startGatewayServer } from "../../gateway/server.js";
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setVerbose } from "../../globals.js";
import { GatewayLockError } from "../../infra/gateway-lock.js";
import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { forceFreePortAndWait } from "../ports.js";
import { ensureDevGatewayConfig } from "./dev.js";
import { runGatewayLoop } from "./run-loop.js";
import { describeUnknownError, extractGatewayMiskeys, maybeExplainGatewayServiceStop, parsePort, toOptionString, } from "./shared.js";
const gatewayLog = createSubsystemLogger("gateway");
async function runGatewayCommand(opts) {
    const isDevProfile = process.env.OPENCLAW_PROFILE?.trim().toLowerCase() === "dev";
    const devMode = Boolean(opts.dev) || isDevProfile;
    if (opts.reset && !devMode) {
        defaultRuntime.error("Use --reset with --dev.");
        defaultRuntime.exit(1);
        return;
    }
    setConsoleTimestampPrefix(true);
    setVerbose(Boolean(opts.verbose));
    if (opts.claudeCliLogs) {
        setConsoleSubsystemFilter(["agent/claude-cli"]);
        process.env.OPENCLAW_CLAUDE_CLI_LOG_OUTPUT = "1";
    }
    const wsLogRaw = (opts.compact ? "compact" : opts.wsLog);
    const wsLogStyle = wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto";
    if (wsLogRaw !== undefined &&
        wsLogRaw !== "auto" &&
        wsLogRaw !== "compact" &&
        wsLogRaw !== "full") {
        defaultRuntime.error('Invalid --ws-log (use "auto", "full", "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;
    }
    if (devMode) {
        await ensureDevGatewayConfig({ reset: Boolean(opts.reset) });
    }
    const cfg = loadConfig();
    const portOverride = parsePort(opts.port);
    if (opts.port !== undefined && portOverride === null) {
        defaultRuntime.error("Invalid port");
        defaultRuntime.exit(1);
    }
    const port = portOverride ?? resolveGatewayPort(cfg);
    if (!Number.isFinite(port) || port <= 0) {
        defaultRuntime.error("Invalid port");
        defaultRuntime.exit(1);
    }
    if (opts.force) {
        try {
            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`);
                }
            }
        }
        catch (err) {
            defaultRuntime.error(`Force: ${String(err)}`);
            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 = authModeRaw === "token" || authModeRaw === "password" ? authModeRaw : null;
    if (authModeRaw && !authMode) {
        defaultRuntime.error('Invalid --auth (use "token" or "password")');
        defaultRuntime.exit(1);
        return;
    }
    const tailscaleRaw = toOptionString(opts.tailscale);
    const tailscaleMode = tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel"
        ? tailscaleRaw
        : null;
    if (tailscaleRaw && !tailscaleMode) {
        defaultRuntime.error('Invalid --tailscale (use "off", "serve", or "funnel")');
        defaultRuntime.exit(1);
        return;
    }
    const passwordRaw = toOptionString(opts.password);
    const tokenRaw = toOptionString(opts.token);
    const configExists = fs.existsSync(CONFIG_PATH);
    const mode = cfg.gateway?.mode;
    if (!opts.allowUnconfigured && mode !== "local") {
        if (!configExists) {
            defaultRuntime.error(`Missing config. Run \`${formatCliCommand("openclaw-cn setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`);
        }
        else {
            defaultRuntime.error(`Gateway start blocked: set gateway.mode=local (current: ${mode ?? "unset"}) or pass --allow-unconfigured.`);
        }
        defaultRuntime.exit(1);
        return;
    }
    const bindRaw = toOptionString(opts.bind) ?? cfg.gateway?.bind ?? "loopback";
    const bind = bindRaw === "loopback" ||
        bindRaw === "lan" ||
        bindRaw === "auto" ||
        bindRaw === "custom" ||
        bindRaw === "tailnet"
        ? bindRaw
        : null;
    if (!bind) {
        defaultRuntime.error('Invalid --bind (use "loopback", "lan", "tailnet", "auto", or "custom")');
        defaultRuntime.exit(1);
        return;
    }
    const snapshot = await readConfigFileSnapshot().catch(() => null);
    const miskeys = extractGatewayMiskeys(snapshot?.parsed);
    const authConfig = {
        ...cfg.gateway?.auth,
        ...(authMode ? { mode: authMode } : {}),
        ...(passwordRaw ? { password: passwordRaw } : {}),
        ...(tokenRaw ? { token: tokenRaw } : {}),
    };
    const resolvedAuth = resolveGatewayAuth({
        authConfig,
        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 hasSharedSecret = (resolvedAuthMode === "token" && hasToken) || (resolvedAuthMode === "password" && hasPassword);
    const authHints = [];
    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 === "token" && !hasToken && !resolvedAuth.allowTailscale) {
        defaultRuntime.error([
            "Gateway auth is set to token, but no token is configured.",
            "Set gateway.auth.token (or OPENCLAW_GATEWAY_TOKEN), or pass --token.",
            ...authHints,
        ]
            .filter(Boolean)
            .join("\n"));
        defaultRuntime.exit(1);
        return;
    }
    if (resolvedAuthMode === "password" && !hasPassword) {
        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(1);
        return;
    }
    if (bind !== "loopback" && !hasSharedSecret) {
        defaultRuntime.error([
            `Refusing to bind gateway to ${bind} without auth.`,
            "Set gateway.auth.token/password (or OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD) or pass --token/--password.",
            ...authHints,
        ]
            .filter(Boolean)
            .join("\n"));
        defaultRuntime.exit(1);
        return;
    }
    try {
        await runGatewayLoop({
            runtime: defaultRuntime,
            start: async () => await startGatewayServer(port, {
                bind,
                auth: authMode || passwordRaw || tokenRaw || authModeRaw
                    ? {
                        mode: authMode ?? undefined,
                        token: tokenRaw,
                        password: passwordRaw,
                    }
                    : undefined,
                tailscale: tailscaleMode || opts.tailscaleResetOnExit
                    ? {
                        mode: tailscaleMode ?? undefined,
                        resetOnExit: Boolean(opts.tailscaleResetOnExit),
                    }
                    : undefined,
            }),
        });
    }
    catch (err) {
        if (err instanceof GatewayLockError ||
            (err && typeof err === "object" && err.name === "GatewayLockError")) {
            const errMessage = describeUnknownError(err);
            defaultRuntime.error(`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: ${formatCliCommand("openclaw-cn gateway stop")}`);
            try {
                const diagnostics = await inspectPortUsage(port);
                if (diagnostics.status === "busy") {
                    for (const line of formatPortDiagnostics(diagnostics)) {
                        defaultRuntime.error(line);
                    }
                }
            }
            catch {
                // ignore diagnostics failures
            }
            await maybeExplainGatewayServiceStop();
            defaultRuntime.exit(1);
            return;
        }
        defaultRuntime.error(`Gateway failed to start: ${String(err)}`);
        defaultRuntime.exit(1);
    }
}
export function addGatewayRunCommand(cmd) {
    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 ("token"|"password")')
        .option("--password <password>", "Password for auth mode=password")
        .option("--tailscale <mode>", 'Tailscale exposure mode ("off"|"serve"|"funnel")')
        .option("--tailscale-reset-on-exit", "Reset Tailscale serve/funnel configuration on shutdown", false)
        .option("--allow-unconfigured", "Allow gateway start without gateway.mode=local in 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("--claude-cli-logs", "Only show claude-cli logs in the console (includes stdout/stderr)", 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) => {
        await runGatewayCommand(opts);
    });
}
