import net from "node:net";
import { findExecutable } from "../../../../utils/executable.js";
import { spawnProcess } from "../../../../utils/spawn.js";
import { terminateWithTreeKill } from "../../../../utils/tree-kill.js";
import { createProviderEnvSpec, resolveProviderCommandPrefix, } from "../../provider-launch-config.js";
const OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5000;
const OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1000;
export class OpenCodeServerManager {
    constructor(logger, runtimeSettings) {
        this.currentServer = null;
        this.retiredServers = new Set();
        this.startPromise = null;
        this.forcedRefreshPromise = null;
        this.logger = logger;
        this.runtimeSettings = runtimeSettings;
        this.runtimeSettingsKey = JSON.stringify(runtimeSettings ?? {});
    }
    static getInstance(logger, runtimeSettings) {
        const nextSettingsKey = JSON.stringify(runtimeSettings ?? {});
        if (!OpenCodeServerManager.instance) {
            OpenCodeServerManager.instance = new OpenCodeServerManager(logger, runtimeSettings);
            OpenCodeServerManager.registerExitHandler();
        }
        else if (OpenCodeServerManager.instance.runtimeSettingsKey !== nextSettingsKey) {
            logger.warn({
                existingRuntimeSettings: OpenCodeServerManager.instance.runtimeSettingsKey,
                requestedRuntimeSettings: nextSettingsKey,
            }, "OpenCode server manager already initialized with different runtime settings");
        }
        return OpenCodeServerManager.instance;
    }
    static registerExitHandler() {
        if (OpenCodeServerManager.exitHandlerRegistered) {
            return;
        }
        OpenCodeServerManager.exitHandlerRegistered = true;
        const cleanup = () => {
            const instance = OpenCodeServerManager.instance;
            void instance?.shutdown();
        };
        process.on("exit", cleanup);
        process.on("SIGTERM", cleanup);
        process.on("SIGINT", cleanup);
    }
    async ensureRunning() {
        const acquisition = await this.acquire({ force: false });
        acquisition.release();
        return acquisition.server;
    }
    async acquire(options) {
        if (options.env) {
            const server = await this.startDedicatedServer(options.env);
            return this.acquireServer(server);
        }
        const server = options.force
            ? await this.getForcedRefreshServer()
            : await this.getCurrentServer();
        return this.acquireServer(server);
    }
    acquireServer(server) {
        server.refCount += 1;
        let released = false;
        return {
            server: { port: server.port, url: server.url },
            release: () => {
                if (released) {
                    return;
                }
                released = true;
                server.refCount -= 1;
                this.cleanupRetiredServers();
            },
        };
    }
    async getForcedRefreshServer() {
        if (this.forcedRefreshPromise) {
            return this.forcedRefreshPromise;
        }
        this.forcedRefreshPromise = Promise.resolve()
            .then(async () => {
            await this.rotateCurrentServer();
            return this.getCurrentServer();
        })
            .finally(() => {
            this.forcedRefreshPromise = null;
        });
        return this.forcedRefreshPromise;
    }
    async getCurrentServer() {
        if (this.startPromise) {
            return this.startPromise;
        }
        if (this.currentServer && !this.currentServer.process.killed) {
            return this.currentServer;
        }
        this.startPromise = this.startServer();
        try {
            const result = await this.startPromise;
            if (!result.retired) {
                this.currentServer = result;
            }
            return result;
        }
        finally {
            this.startPromise = null;
        }
    }
    async rotateCurrentServer() {
        const existing = this.currentServer;
        if (existing) {
            existing.retired = true;
            this.retiredServers.add(existing);
            this.currentServer = null;
            this.cleanupRetiredServers();
        }
        if (this.startPromise) {
            const pending = await this.startPromise;
            pending.retired = true;
            this.retiredServers.add(pending);
            this.currentServer = null;
            this.cleanupRetiredServers();
        }
    }
    async startDedicatedServer(env) {
        const server = await this.startServer(env);
        server.retired = true;
        this.retiredServers.add(server);
        return server;
    }
    async startServer(launchEnv) {
        const port = await findAvailablePort();
        const url = `http://127.0.0.1:${port}`;
        const launchPrefix = await resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveOpenCodeBinary);
        return new Promise((resolve, reject) => {
            const serverProcess = spawnProcess(launchPrefix.command, [...launchPrefix.args, "serve", "--port", String(port)], {
                detached: process.platform !== "win32",
                stdio: ["ignore", "pipe", "pipe"],
                ...createProviderEnvSpec({
                    runtimeSettings: this.runtimeSettings,
                    overlays: [launchEnv],
                }),
            });
            let started = false;
            let stderrBuffer = "";
            let stdoutBuffer = "";
            const STARTUP_BUFFER_CAP = 8192;
            const appendCapped = (current, chunk) => {
                if (current.length >= STARTUP_BUFFER_CAP) {
                    return current;
                }
                const remaining = STARTUP_BUFFER_CAP - current.length;
                return current + chunk.slice(0, remaining);
            };
            const buildStartupErrorMessage = (headline) => {
                const sections = [headline];
                const stderrTrimmed = stderrBuffer.trim();
                if (stderrTrimmed.length > 0) {
                    sections.push(`stderr: ${stderrTrimmed}`);
                }
                const stdoutTrimmed = stdoutBuffer.trim();
                if (stdoutTrimmed.length > 0) {
                    sections.push(`stdout: ${stdoutTrimmed}`);
                }
                return sections.join("\n");
            };
            const timeout = setTimeout(() => {
                if (!started) {
                    reject(new Error(buildStartupErrorMessage("OpenCode server startup timeout")));
                }
            }, 30000);
            serverProcess.stdout?.on("data", (data) => {
                const output = data.toString();
                stdoutBuffer = appendCapped(stdoutBuffer, output);
                if (output.includes("listening on") && !started) {
                    started = true;
                    clearTimeout(timeout);
                    resolve({
                        process: serverProcess,
                        port,
                        url,
                        refCount: 0,
                        retired: false,
                    });
                }
            });
            serverProcess.stderr?.on("data", (data) => {
                const output = data.toString();
                stderrBuffer = appendCapped(stderrBuffer, output);
                this.logger.error({ stderr: output.trim() }, "OpenCode server stderr");
            });
            serverProcess.on("error", (error) => {
                clearTimeout(timeout);
                const headline = error instanceof Error ? error.message : String(error);
                reject(new Error(buildStartupErrorMessage(headline)));
            });
            serverProcess.on("exit", (code) => {
                if (!started) {
                    clearTimeout(timeout);
                    reject(new Error(buildStartupErrorMessage(`OpenCode server exited with code ${code}`)));
                }
                if (this.currentServer?.process === serverProcess) {
                    this.currentServer = null;
                }
                for (const retired of Array.from(this.retiredServers)) {
                    if (retired.process === serverProcess) {
                        this.retiredServers.delete(retired);
                    }
                }
            });
        });
    }
    async shutdown() {
        const servers = [
            ...(this.currentServer ? [this.currentServer] : []),
            ...Array.from(this.retiredServers),
        ];
        await Promise.all(servers.map((server) => this.killServer(server)));
        this.currentServer = null;
        this.retiredServers.clear();
    }
    cleanupRetiredServers() {
        for (const server of Array.from(this.retiredServers)) {
            if (server.refCount === 0) {
                this.retiredServers.delete(server);
                void this.killServer(server);
            }
        }
    }
    async killServer(server) {
        if ((server.process.exitCode !== null && server.process.exitCode !== undefined) ||
            (server.process.signalCode !== null && server.process.signalCode !== undefined)) {
            return;
        }
        const result = await terminateWithTreeKill(server.process, {
            gracefulTimeoutMs: OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS,
            forceTimeoutMs: OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS,
            onForceSignal: () => {
                this.logger.warn({ timeoutMs: OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS }, "OpenCode server did not exit after SIGTERM; sending SIGKILL");
            },
        });
        if (result === "kill-timeout") {
            this.logger.warn({ timeoutMs: OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS }, "OpenCode server did not report exit after SIGKILL");
        }
    }
}
OpenCodeServerManager.instance = null;
OpenCodeServerManager.exitHandlerRegistered = false;
async function resolveOpenCodeBinary() {
    const found = await findExecutable("opencode");
    if (found) {
        return found;
    }
    throw new Error("OpenCode binary not found. Install OpenCode (https://github.com/opencode-ai/opencode) and ensure it is available in your shell PATH.");
}
function findAvailablePort() {
    return new Promise((resolve, reject) => {
        const server = net.createServer();
        server.listen(0, "127.0.0.1", () => {
            const address = server.address();
            server.close(() => {
                if (typeof address === "object" && address) {
                    resolve(address.port);
                }
                else {
                    reject(new Error("Failed to allocate port"));
                }
            });
        });
        server.on("error", reject);
    });
}
//# sourceMappingURL=server-manager.js.map