"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.YcmdServer = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const net = __importStar(require("net"));
const config_1 = require("../../../config");
const child_process_1 = require("child_process");
const detection_1 = require("./detection");
const client_1 = require("./client");
class YcmdServer {
    process = null;
    serverInfo = null;
    hmacSecret;
    ycmdPath;
    configuredPort = 0;
    detectedPort = null;
    constructor() {
        this.setupExitHandler();
        this.hmacSecret = crypto.randomBytes(16).toString("base64");
        process.on("exit", () => this.forceCleanup());
        const installations = detection_1.YcmdDetection.findInstallations();
        if (installations.length === 0) {
            throw new Error("No ycmd installation found. Please install ycmd first.");
        }
        const knowhowConfig = require("../../../config").getConfigSync();
        const ycmdConfig = knowhowConfig.ycmd || {};
        const configuredPath = ycmdConfig.installPath;
        const knowhowPath = path.join(require("os").homedir(), ".knowhow/ycmd");
        this.ycmdPath =
            (configuredPath && installations.find((p) => p === configuredPath)) ||
                installations.find((p) => p === knowhowPath) ||
                installations[0];
    }
    async start(workspaceRoot, port) {
        return this.startWithRetry(0, workspaceRoot, port);
    }
    async startWithRetry(retryCount, workspaceRoot, port) {
        const maxRetries = 5;
        if (retryCount >= maxRetries) {
            throw new Error(`Failed to start ycmd server after ${maxRetries} attempts`);
        }
        try {
            return await this.doStart(workspaceRoot, port);
        }
        catch (error) {
            if (error.message === "PORT_IN_USE" ||
                error.message.includes("PORT_IN_USE")) {
                console.log(`Retrying with incremented port (attempt ${retryCount + 1}/${maxRetries})`);
                const nextPort = (port || 8080) + retryCount + 1;
                return this.startWithRetry(retryCount + 1, workspaceRoot, nextPort);
            }
            throw error;
        }
    }
    async doStart(workspaceRoot, port) {
        if (this.isRunning()) {
            throw new Error("ycmd server is already running");
        }
        console.log("Starting ycmd server...");
        try {
            const knowhowConfig = await (0, config_1.getConfig)();
            const ycmdConfig = knowhowConfig.ycmd || {};
            if (ycmdConfig.enabled === false) {
                throw new Error("ycmd is disabled in configuration. Set ycmd.enabled to true in .knowhow/knowhow.json");
            }
            const serverConfig = this.createServerConfig(workspaceRoot, port);
            const availablePort = await this.findAvailablePort(serverConfig.port);
            this.configuredPort = availablePort;
            const configPath = await this.writeServerConfig(serverConfig);
            const pythonCmd = detection_1.YcmdDetection.getPythonCommand();
            const ycmdScript = path.join(this.ycmdPath, "ycmd", "__main__.py");
            const args = ["--options_file", configPath];
            args.push("--port", availablePort.toString());
            this.process = (0, child_process_1.spawn)(pythonCmd, [ycmdScript, ...args], {
                stdio: ["pipe", "pipe", "pipe"],
                cwd: this.ycmdPath,
            });
            try {
                await this.setupProcessHandlers();
            }
            catch (error) {
                if (error.message === "PORT_IN_USE") {
                    throw error;
                }
            }
            const serverInfo = await this.waitForServerStart();
            this.serverInfo = {
                ...serverInfo,
                pid: this.process.pid,
                status: "running",
            };
            console.log(`ycmd server started on ${serverInfo.host}:${serverInfo.port}`);
            return this.serverInfo;
        }
        catch (error) {
            this.cleanup();
            if (error.message === "PORT_IN_USE") {
                throw error;
            }
            throw new Error(`Failed to start ycmd server: ${error.message}`);
        }
    }
    async stop() {
        if (!this.isRunning()) {
            console.log("ycmd server is not running");
            return;
        }
        console.log("Stopping ycmd server...");
        return new Promise((resolve, reject) => {
            if (!this.process) {
                resolve();
                return;
            }
            const timeout = setTimeout(() => {
                this.process?.kill("SIGKILL");
                reject(new Error("ycmd server failed to stop gracefully"));
            }, 5000);
            this.process.once("exit", () => {
                clearTimeout(timeout);
                this.cleanup();
                console.log("ycmd server stopped");
                resolve();
            });
            this.process.kill("SIGTERM");
        });
    }
    isRunning() {
        return (this.process !== null &&
            !this.process.killed &&
            this.serverInfo?.status === "running");
    }
    getServerInfo() {
        return this.serverInfo;
    }
    setExternalServerInfo(serverInfo) {
        this.serverInfo = serverInfo;
        this.hmacSecret = serverInfo.hmacSecret || "";
    }
    async healthCheck() {
        if (!this.isRunning() || !this.serverInfo) {
            return false;
        }
        try {
            const client = new client_1.YcmdClient(this.serverInfo);
            return await client.isReady();
        }
        catch {
            return false;
        }
    }
    createServerConfig(workspaceRoot, port) {
        const knowhowConfig = require("../../../config").getConfigSync();
        const ycmdConfig = knowhowConfig.ycmd || {};
        return {
            hmac_secret: this.hmacSecret,
            port: port || ycmdConfig.port || 0,
            host: "127.0.0.1",
            server_keep_logfiles: true,
            server_use_vim_stdout: false,
            log_level: ycmdConfig.logLevel || "info",
            max_diagnostics_to_display: 30,
            auto_trigger_completion: true,
            completion_timeout: (ycmdConfig.completionTimeout || 5000) / 1000,
            global_ycm_extra_conf: workspaceRoot
                ? path.join(workspaceRoot, ".ycm_extra_conf.py")
                : undefined,
            confirm_extra_conf: false,
            auto_start_csharp_server: true,
            auto_stop_csharp_server: true,
            use_clangd: true,
            clangd_binary_path: "",
            clangd_args: [],
            java_jdtls_workspace_root_path: workspaceRoot || "",
            python_binary_path: detection_1.YcmdDetection.getPythonCommand(),
        };
    }
    async writeServerConfig(config) {
        const configPath = path.join(require("os").tmpdir(), `ycmd_config_${Date.now()}.json`);
        await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
        return configPath;
    }
    setupProcessHandlers() {
        if (!this.process) {
            return Promise.resolve(false);
        }
        return new Promise((resolve, reject) => {
            let stdoutBuffer = "";
            this.process.on("error", (error) => {
                console.error("ycmd server process error:", error);
                if (this.serverInfo) {
                    this.serverInfo.status = "error";
                }
            });
            this.process.on("exit", (code, signal) => {
                console.log(`ycmd server exited with code ${code}, signal ${signal}`);
                this.cleanup();
            });
            this.process.stdout?.on("data", (data) => {
                const output = data.toString();
                stdoutBuffer += output;
                const serverReadyMatch = output.match(/serving on http:\/\/127\.0\.0\.1:(\d+)/i) ||
                    output.match(/server running at .*:(\d+)/i) ||
                    output.match(/listening on port (\d+)/i) ||
                    output.match(/port:\s*(\d+)/i);
                if (serverReadyMatch) {
                    const port = parseInt(serverReadyMatch[1], 10);
                    console.log(`ycmd server detected on port ${port} from stdout`);
                    this.detectedPort = port;
                }
                console.log("ycmd stdout:", output);
            });
            this.process.stderr?.on("data", (data) => {
                const output = data.toString();
                if (output.includes("Address already in use") ||
                    output.includes("EADDRINUSE") ||
                    output.includes("bind: Address already in use")) {
                    console.log(`Port ${this.configuredPort} is already in use, will retry with different port`);
                    reject(new Error("PORT_IN_USE"));
                    return;
                }
            });
            setTimeout(() => resolve(true), 1000);
        });
    }
    async waitForServerStart() {
        return new Promise((resolve, reject) => {
            let timedOut = false;
            const timeout = setTimeout(() => {
                console.log("ycmd server startup timeout - server failed to start within 30 seconds");
                reject(new Error("ycmd server failed to start within timeout"));
                timedOut = true;
            }, 30000);
            const host = "127.0.0.1";
            const getPort = () => this.configuredPort;
            const checkReady = async () => {
                try {
                    if (timedOut) {
                        return;
                    }
                    const port = getPort();
                    console.log(`Checking ycmd server health on port ${port}`);
                    const tempServerInfo = {
                        host,
                        port,
                        hmacSecret: this.hmacSecret,
                        status: "starting",
                    };
                    const client = new client_1.YcmdClient(tempServerInfo);
                    const isReady = await client.isReady();
                    if (isReady) {
                        const finalPort = getPort();
                        console.log(`ycmd server is ready on port ${finalPort}`);
                        clearTimeout(timeout);
                        resolve({
                            host,
                            port: finalPort,
                            hmacSecret: this.hmacSecret,
                            status: "starting",
                        });
                        return;
                    }
                    setTimeout(checkReady, 1000);
                }
                catch (error) {
                    const currentPort = getPort();
                    console.log(`Health check failed for port ${currentPort}: ${error.message}`);
                    setTimeout(checkReady, 1000);
                }
            };
            setTimeout(checkReady, 2000);
        });
    }
    cleanup() {
        this.process = null;
        if (this.serverInfo) {
            this.serverInfo.status = "stopped";
        }
    }
    forceCleanup() {
        if (this.process && !this.process.killed) {
            console.log("Force killing ycmd server process on parent exit");
            this.process.kill("SIGKILL");
        }
        this.process = null;
        this.serverInfo = null;
    }
    setupExitHandler() {
        process.on("exit", () => this.forceCleanup());
        process.on("SIGINT", () => {
            console.log("\nReceived SIGINT (Ctrl+C), shutting down ycmd server gracefully...");
            this.gracefulShutdown().finally(() => {
                process.exit(0);
            });
        });
        process.on("SIGTERM", () => {
            console.log("\nReceived SIGTERM, shutting down ycmd server gracefully...");
            this.gracefulShutdown().finally(() => {
                process.exit(0);
            });
        });
    }
    async gracefulShutdown() {
        try {
            await this.stop();
        }
        catch (error) {
            console.error("Error during graceful shutdown:", error);
        }
    }
    async findAvailablePort(preferredPort) {
        if (preferredPort === 0) {
            preferredPort = 8080;
        }
        if (await this.isPortAvailable(preferredPort)) {
            return preferredPort;
        }
        for (let port = 8080; port <= 8090; port++) {
            if (await this.isPortAvailable(port)) {
                console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
                return port;
            }
        }
        throw new Error(`No available ports found in range 8080-8090`);
    }
    async isPortAvailable(port) {
        return new Promise((resolve) => {
            const server = net.createServer();
            server.listen(port, () => {
                server.once("close", () => resolve(true));
                server.close();
            });
            server.on("error", () => resolve(false));
        });
    }
    async restart(workspaceRoot, port) {
        if (this.isRunning()) {
            await this.stop();
        }
        return this.start(workspaceRoot, port);
    }
}
exports.YcmdServer = YcmdServer;
//# sourceMappingURL=server.js.map