/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import { detectIde, DetectedIde, getIdeDisplayName, } from '../ide/detect-ide.js';
import { ideContext, IdeContextNotificationSchema, IdeDiffAcceptedNotificationSchema, IdeDiffClosedNotificationSchema, CloseDiffResponseSchema, } from '../ide/ideContext.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const logger = {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    debug: (...args) => console.debug('[DEBUG] [IDEClient]', ...args),
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    error: (...args) => console.error('[ERROR] [IDEClient]', ...args),
};
export var IDEConnectionStatus;
(function (IDEConnectionStatus) {
    IDEConnectionStatus["Connected"] = "connected";
    IDEConnectionStatus["Disconnected"] = "disconnected";
    IDEConnectionStatus["Connecting"] = "connecting";
})(IDEConnectionStatus || (IDEConnectionStatus = {}));
function getRealPath(path) {
    try {
        return fs.realpathSync(path);
    }
    catch (_e) {
        // If realpathSync fails, it might be because the path doesn't exist.
        // In that case, we can fall back to the original path.
        return path;
    }
}
/**
 * Manages the connection to and interaction with the IDE server.
 */
export class IdeClient {
    static instance;
    client = undefined;
    state = {
        status: IDEConnectionStatus.Disconnected,
        details: 'IDE integration is currently disabled. To enable it, run /ide enable.',
    };
    currentIde;
    currentIdeDisplayName;
    diffResponses = new Map();
    constructor() {
        this.currentIde = detectIde();
        if (this.currentIde) {
            this.currentIdeDisplayName = getIdeDisplayName(this.currentIde);
        }
    }
    static getInstance() {
        if (!IdeClient.instance) {
            IdeClient.instance = new IdeClient();
        }
        return IdeClient.instance;
    }
    async connect() {
        if (!this.currentIde || !this.currentIdeDisplayName) {
            this.setState(IDEConnectionStatus.Disconnected, `IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: ${Object.values(DetectedIde)
                .map((ide) => getIdeDisplayName(ide))
                .join(', ')}`, false);
            return;
        }
        this.setState(IDEConnectionStatus.Connecting);
        if (!this.validateWorkspacePath()) {
            return;
        }
        const port = this.getPortFromEnv();
        if (!port) {
            return;
        }
        await this.establishConnection(port);
    }
    /**
     * A diff is accepted with any modifications if the user performs one of the
     * following actions:
     * - Clicks the checkbox icon in the IDE to accept
     * - Runs `command+shift+p` > "Gemini CLI: Accept Diff in IDE" to accept
     * - Selects "accept" in the CLI UI
     * - Saves the file via `ctrl/command+s`
     *
     * A diff is rejected if the user performs one of the following actions:
     * - Clicks the "x" icon in the IDE
     * - Runs "Gemini CLI: Close Diff in IDE"
     * - Selects "no" in the CLI UI
     * - Closes the file
     */
    async openDiff(filePath, newContent) {
        return new Promise((resolve, reject) => {
            this.diffResponses.set(filePath, resolve);
            this.client
                ?.callTool({
                name: `openDiff`,
                arguments: {
                    filePath,
                    newContent,
                },
            })
                .catch((err) => {
                logger.debug(`callTool for ${filePath} failed:`, err);
                reject(err);
            });
        });
    }
    async closeDiff(filePath) {
        try {
            const result = await this.client?.callTool({
                name: `closeDiff`,
                arguments: {
                    filePath,
                },
            });
            if (result) {
                const parsed = CloseDiffResponseSchema.parse(result);
                return parsed.content;
            }
        }
        catch (err) {
            logger.debug(`callTool for ${filePath} failed:`, err);
        }
        return;
    }
    // Closes the diff. Instead of waiting for a notification,
    // manually resolves the diff resolver as the desired outcome.
    async resolveDiffFromCli(filePath, outcome) {
        const content = await this.closeDiff(filePath);
        const resolver = this.diffResponses.get(filePath);
        if (resolver) {
            if (outcome === 'accepted') {
                resolver({ status: 'accepted', content });
            }
            else {
                resolver({ status: 'rejected', content: undefined });
            }
            this.diffResponses.delete(filePath);
        }
    }
    async disconnect() {
        if (this.state.status === IDEConnectionStatus.Disconnected) {
            return;
        }
        for (const filePath of this.diffResponses.keys()) {
            await this.closeDiff(filePath);
        }
        this.diffResponses.clear();
        this.setState(IDEConnectionStatus.Disconnected, 'IDE integration disabled. To enable it again, run /ide enable.');
        this.client?.close();
    }
    getCurrentIde() {
        return this.currentIde;
    }
    getConnectionStatus() {
        return this.state;
    }
    getDetectedIdeDisplayName() {
        return this.currentIdeDisplayName;
    }
    setState(status, details, logToConsole = false) {
        const isAlreadyDisconnected = this.state.status === IDEConnectionStatus.Disconnected &&
            status === IDEConnectionStatus.Disconnected;
        // Only update details & log to console if the state wasn't already
        // disconnected, so that the first detail message is preserved.
        if (!isAlreadyDisconnected) {
            this.state = { status, details };
            if (details) {
                if (logToConsole) {
                    logger.error(details);
                }
                else {
                    // We only want to log disconnect messages to debug
                    // if they are not already being logged to the console.
                    logger.debug(details);
                }
            }
        }
        if (status === IDEConnectionStatus.Disconnected) {
            ideContext.clearIdeContext();
        }
    }
    validateWorkspacePath() {
        const ideWorkspacePath = process.env['QWEN_CODE_IDE_WORKSPACE_PATH'];
        if (ideWorkspacePath === undefined) {
            this.setState(IDEConnectionStatus.Disconnected, `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try refreshing your terminal. To install the extension, run /ide install.`, true);
            return false;
        }
        if (ideWorkspacePath === '') {
            this.setState(IDEConnectionStatus.Disconnected, `To use this feature, please open a single workspace folder in ${this.currentIdeDisplayName} and try again.`, true);
            return false;
        }
        const idePath = getRealPath(ideWorkspacePath).toLocaleLowerCase();
        const cwd = getRealPath(process.cwd()).toLocaleLowerCase();
        const rel = path.relative(idePath, cwd);
        if (rel.startsWith('..') || path.isAbsolute(rel)) {
            this.setState(IDEConnectionStatus.Disconnected, `Directory mismatch. Qwen Code is running in a different location than the open workspace in ${this.currentIdeDisplayName}. Please run the CLI from the same directory as your project's root folder.`, true);
            return false;
        }
        return true;
    }
    getPortFromEnv() {
        const port = process.env['QWEN_CODE_IDE_SERVER_PORT'];
        if (!port) {
            this.setState(IDEConnectionStatus.Disconnected, `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try restarting your terminal. To install the extension, run /ide install.`, true);
            return undefined;
        }
        return port;
    }
    registerClientHandlers() {
        if (!this.client) {
            return;
        }
        this.client.setNotificationHandler(IdeContextNotificationSchema, (notification) => {
            ideContext.setIdeContext(notification.params);
        });
        this.client.onerror = (_error) => {
            this.setState(IDEConnectionStatus.Disconnected, `IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`, true);
        };
        this.client.onclose = () => {
            this.setState(IDEConnectionStatus.Disconnected, `IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`, true);
        };
        this.client.setNotificationHandler(IdeDiffAcceptedNotificationSchema, (notification) => {
            const { filePath, content } = notification.params;
            const resolver = this.diffResponses.get(filePath);
            if (resolver) {
                resolver({ status: 'accepted', content });
                this.diffResponses.delete(filePath);
            }
            else {
                logger.debug(`No resolver found for ${filePath}`);
            }
        });
        this.client.setNotificationHandler(IdeDiffClosedNotificationSchema, (notification) => {
            const { filePath } = notification.params;
            const resolver = this.diffResponses.get(filePath);
            if (resolver) {
                resolver({ status: 'rejected', content: undefined });
                this.diffResponses.delete(filePath);
            }
            else {
                logger.debug(`No resolver found for ${filePath}`);
            }
        });
    }
    async establishConnection(port) {
        let transport;
        try {
            this.client = new Client({
                name: 'streamable-http-client',
                // TODO(#3487): use the CLI version here.
                version: '1.0.0',
            });
            transport = new StreamableHTTPClientTransport(new URL(`http://${getIdeServerHost()}:${port}/mcp`));
            this.registerClientHandlers();
            await this.client.connect(transport);
            this.registerClientHandlers();
            this.setState(IDEConnectionStatus.Connected);
        }
        catch (_error) {
            this.setState(IDEConnectionStatus.Disconnected, `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try restarting your terminal. To install the extension, run /ide install.`, true);
            if (transport) {
                try {
                    await transport.close();
                }
                catch (closeError) {
                    logger.debug('Failed to close transport:', closeError);
                }
            }
        }
    }
    async init() {
        if (this.state.status === IDEConnectionStatus.Connected) {
            return;
        }
        if (!this.currentIde) {
            this.setState(IDEConnectionStatus.Disconnected, 'Not running in a supported IDE, skipping connection.');
            return;
        }
        this.setState(IDEConnectionStatus.Connecting);
        if (!this.validateWorkspacePath()) {
            return;
        }
        const port = this.getPortFromEnv();
        if (!port) {
            return;
        }
        await this.establishConnection(port);
    }
    dispose() {
        this.client?.close();
    }
    setDisconnected() {
        this.setState(IDEConnectionStatus.Disconnected);
    }
}
function getIdeServerHost() {
    const isInContainer = fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
    return isInContainer ? 'host.docker.internal' : 'localhost';
}
//# sourceMappingURL=ide-client.js.map