import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, watch } from "node:fs";
import path from "node:path";
import process from "node:process";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
const JSONRPC_VERSION = "2.0";
const TOOL_LIST_CHANGED_METHOD = "notifications/tools/list_changed";
const RECONNECT_REQUIRED_CODE = -32070;
export function stableJson(value) {
    if (value === null || typeof value !== "object")
        return JSON.stringify(value);
    if (Array.isArray(value))
        return `[${value.map((item) => stableJson(item)).join(",")}]`;
    const record = value;
    const entries = Object.keys(record)
        .sort()
        .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`);
    return `{${entries.join(",")}}`;
}
export function computeToolInterfaceHash(tools) {
    return createHash("sha256").update(stableJson(tools)).digest("hex");
}
export function extractToolListHash(message) {
    if (!message || typeof message !== "object" || !("result" in message))
        return null;
    const result = message.result;
    if (!result || !("tools" in result))
        return null;
    return computeToolInterfaceHash(result.tools);
}
export function toolListChangedNotification() {
    return {
        jsonrpc: JSONRPC_VERSION,
        method: TOOL_LIST_CHANGED_METHOD,
    };
}
export function reconnectRequiredError(id, reason) {
    return {
        jsonrpc: JSONRPC_VERSION,
        id,
        error: {
            code: RECONNECT_REQUIRED_CODE,
            message: `MCP tool interface changed; reconnect or reinitialize this MCP server. ${reason}`,
        },
    };
}
function repoRoot() {
    return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
}
function parseArgsFromEnv(name) {
    const raw = process.env[name];
    if (!raw)
        return null;
    try {
        const parsed = JSON.parse(raw);
        if (Array.isArray(parsed))
            return parsed.map((item) => String(item));
    }
    catch {
        // Fall through to shell-like split for simple env overrides.
    }
    return raw.split(" ").filter(Boolean);
}
function defaultWorkerCommand(root) {
    const distEntrypoint = path.join(root, "dist", "index.js");
    if (existsSync(distEntrypoint)) {
        return { command: process.execPath, args: [distEntrypoint] };
    }
    return { command: "npx", args: ["tsx", "src/index.ts"] };
}
function resolveWorkerCommand(root) {
    const defaults = defaultWorkerCommand(root);
    return {
        command: process.env.NEOTOMA_MCP_DEV_WORKER_CMD || process.env.MCP_DEV_WORKER_CMD || defaults.command,
        args: parseArgsFromEnv("NEOTOMA_MCP_DEV_WORKER_ARGS") ??
            parseArgsFromEnv("MCP_DEV_WORKER_ARGS") ??
            defaults.args,
    };
}
function watchedPaths(root) {
    const raw = process.env.NEOTOMA_MCP_DEV_WATCH_PATHS;
    if (raw) {
        return raw
            .split(path.delimiter)
            .map((entry) => entry.trim())
            .filter(Boolean)
            .map((entry) => (path.isAbsolute(entry) ? entry : path.join(root, entry)));
    }
    return [
        path.join(root, "src"),
        path.join(root, "openapi.yaml"),
        path.join(root, "docs", "developer", "mcp", "tool_descriptions.yaml"),
    ];
}
function writeJson(message) {
    process.stdout.write(`${JSON.stringify(message)}\n`);
}
function log(message) {
    process.stderr.write(`[neotoma-mcp-dev-shim] ${message}\n`);
}
class McpDevShim {
    root;
    workerCommand;
    worker = null;
    workerReady = false;
    restartTimer = null;
    queue = [];
    suppressedIds = new Set();
    watchers = [];
    lastInitializeRequest = null;
    lastInitializedNotification = null;
    lastToolHash = null;
    interfaceChanged = false;
    syntheticRequestSeq = 0;
    pendingClientMethods = new Map();
    constructor(root, workerCommand) {
        this.root = root;
        this.workerCommand = workerCommand;
    }
    start() {
        this.spawnWorker();
        this.startWatching();
        this.startInput();
    }
    spawnWorker() {
        this.workerReady = false;
        const env = { ...process.env, NEOTOMA_ACTIONS_DISABLE_AUTOSTART: "1" };
        const child = spawn(this.workerCommand.command, this.workerCommand.args, {
            cwd: this.root,
            env,
            stdio: ["pipe", "pipe", "pipe"],
        });
        this.worker = child;
        child.stderr.on("data", (chunk) => process.stderr.write(chunk));
        child.stdout.on("data", (chunk) => this.handleWorkerOutput(chunk.toString()));
        child.on("exit", (code, signal) => {
            if (this.worker === child) {
                this.worker = null;
                this.workerReady = false;
            }
            log(`worker exited (code=${code ?? "null"}, signal=${signal ?? "null"})`);
        });
        if (this.lastInitializeRequest) {
            this.reinitializeWorker();
        }
        else {
            this.workerReady = true;
            this.flushQueue();
        }
    }
    startInput() {
        const rl = readline.createInterface({ input: process.stdin });
        rl.on("line", (line) => {
            if (!line.trim())
                return;
            try {
                this.handleClientMessage(JSON.parse(line));
            }
            catch (error) {
                log(`failed to parse client message: ${error.message}`);
            }
        });
    }
    startWatching() {
        for (const watchPath of watchedPaths(this.root)) {
            if (!existsSync(watchPath))
                continue;
            try {
                const watcher = watch(watchPath, { recursive: true }, () => this.scheduleRestart(`change under ${path.relative(this.root, watchPath)}`));
                this.watchers.push(watcher);
            }
            catch (error) {
                log(`watch unavailable for ${watchPath}: ${error.message}`);
            }
        }
    }
    scheduleRestart(reason) {
        if (this.restartTimer)
            clearTimeout(this.restartTimer);
        this.restartTimer = setTimeout(() => this.restartWorker(reason), 150);
    }
    restartWorker(reason) {
        log(`restarting worker after ${reason}`);
        const oldWorker = this.worker;
        this.worker = null;
        this.workerReady = false;
        oldWorker?.kill("SIGTERM");
        setTimeout(() => {
            if (oldWorker && !oldWorker.killed)
                oldWorker.kill("SIGKILL");
        }, 1_000);
        this.spawnWorker();
    }
    handleClientMessage(message) {
        if (message.method === "initialize") {
            this.lastInitializeRequest = message;
            this.interfaceChanged = false;
        }
        else if (message.method === "notifications/initialized") {
            this.lastInitializedNotification = message;
        }
        else if (this.interfaceChanged && message.method === "tools/call") {
            writeJson(reconnectRequiredError(message.id ?? null, "The shim detected a changed tools/list surface after worker reload."));
            return;
        }
        this.forwardOrQueue(message);
    }
    reinitializeWorker() {
        if (!this.lastInitializeRequest) {
            this.workerReady = true;
            this.flushQueue();
            return;
        }
        const replay = { ...this.lastInitializeRequest, id: this.nextSyntheticId() };
        this.suppressedIds.add(replay.id ?? null);
        this.forwardToWorker(replay);
    }
    requestToolListForHash() {
        const id = this.nextSyntheticId();
        this.suppressedIds.add(id);
        this.forwardToWorker({ jsonrpc: JSONRPC_VERSION, id, method: "tools/list", params: {} });
    }
    nextSyntheticId() {
        this.syntheticRequestSeq += 1;
        return `neotoma-dev-shim:${this.syntheticRequestSeq}`;
    }
    forwardOrQueue(message) {
        if ("id" in message && message.id !== undefined && message.method) {
            this.pendingClientMethods.set(message.id, message.method);
        }
        if (!this.worker || !this.workerReady) {
            this.queue.push(message);
            return;
        }
        this.forwardToWorker(message);
    }
    forwardToWorker(message) {
        if (!this.worker || !this.worker.stdin.writable) {
            this.queue.push(message);
            return;
        }
        this.worker.stdin.write(`${JSON.stringify(message)}\n`);
    }
    flushQueue() {
        while (this.worker && this.workerReady && this.queue.length > 0) {
            this.forwardToWorker(this.queue.shift());
        }
    }
    workerBuffer = "";
    handleWorkerOutput(chunk) {
        this.workerBuffer += chunk;
        const lines = this.workerBuffer.split("\n");
        this.workerBuffer = lines.pop() ?? "";
        for (const line of lines) {
            if (!line.trim())
                continue;
            try {
                this.handleWorkerMessage(JSON.parse(line));
            }
            catch (error) {
                log(`non-JSON worker stdout ignored: ${error.message}`);
            }
        }
    }
    handleWorkerMessage(message) {
        const id = "id" in message ? (message.id ?? null) : null;
        const isSuppressed = this.suppressedIds.delete(id);
        const clientMethod = this.pendingClientMethods.get(id);
        this.pendingClientMethods.delete(id);
        const newHash = extractToolListHash(message);
        if (newHash)
            this.handleToolHash(newHash, isSuppressed);
        if (!isSuppressed && clientMethod === "tools/list")
            this.interfaceChanged = false;
        if (isSuppressed) {
            if (!this.workerReady) {
                if (this.lastInitializedNotification)
                    this.forwardToWorker(this.lastInitializedNotification);
                this.workerReady = true;
                this.requestToolListForHash();
                return;
            }
            this.flushQueue();
            return;
        }
        writeJson(message);
    }
    handleToolHash(newHash, isSuppressed) {
        if (!this.lastToolHash) {
            this.lastToolHash = newHash;
            return;
        }
        if (this.lastToolHash === newHash)
            return;
        this.lastToolHash = newHash;
        this.interfaceChanged = true;
        writeJson(toolListChangedNotification());
        log("tool interface changed; sent notifications/tools/list_changed. Reconnect or reinitialize if the MCP client does not refresh tools.");
        if (!isSuppressed)
            this.interfaceChanged = false;
    }
    close() {
        for (const watcher of this.watchers)
            watcher.close();
        this.worker?.kill("SIGTERM");
    }
}
export function runMcpDevShim() {
    const root = repoRoot();
    const shim = new McpDevShim(root, resolveWorkerCommand(root));
    const cleanup = () => {
        shim.close();
        process.exit(0);
    };
    process.on("SIGINT", cleanup);
    process.on("SIGTERM", cleanup);
    shim.start();
}
if (import.meta.url === `file://${process.argv[1]}`) {
    runMcpDevShim();
}
//# sourceMappingURL=mcp_dev_shim.js.map