import { bindAbortSignal, createRpcError, createRpcPayload, isRpcErrorForId, isRpcResponseForId } from 'react-mcp-hook/client';
const DEFAULT_TIMEOUT_MS = 10000;
const RESTART_DELAY_MS = 1000;
// Lazy-load Node.js modules at runtime
let spawnFn = null;
let createInterfaceFn = null;
async function ensureNodeModules() {
    if (spawnFn && createInterfaceFn) {
        return;
    }
    try {
        // In Electron renderer with nodeIntegration: true, require is available globally
        // Check multiple ways to access require
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        let nodeRequire = null;
        if (typeof window?.require === 'function') {
            nodeRequire = window.require;
            console.log('[STDIO Plugin] Using window.require');
        }
        else if (typeof globalThis?.require === 'function') {
            nodeRequire = globalThis.require;
            console.log('[STDIO Plugin] Using globalThis.require');
        }
        else if (typeof require !== 'undefined') {
            nodeRequire = require;
            console.log('[STDIO Plugin] Using global require');
        }
        if (nodeRequire) {
            console.log('[STDIO Plugin] Loading Node.js modules via require');
            const childProcess = nodeRequire('node:child_process');
            const readline = nodeRequire('node:readline');
            spawnFn = childProcess.spawn;
            createInterfaceFn = readline.createInterface;
            console.log('[STDIO Plugin] Node.js modules loaded successfully via require');
        }
        else {
            console.log('[STDIO Plugin] Attempting dynamic import of Node.js modules');
            // Fallback to dynamic import (may not work in bundled code)
            const childProcess = await import('node:child_process');
            const readline = await import('node:readline');
            spawnFn = childProcess.spawn;
            createInterfaceFn = readline.createInterface;
            console.log('[STDIO Plugin] Node.js modules loaded successfully via import');
        }
    }
    catch (error) {
        console.error('[STDIO Plugin] Failed to load Node.js modules:', error);
        throw new Error(`Failed to load Node.js modules: ${error instanceof Error ? error.message : String(error)}`);
    }
}
class StdioTransportPlugin {
    constructor() {
        Object.defineProperty(this, "name", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 'local-stdio'
        });
        Object.defineProperty(this, "description", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 'Local stdio transport for MCP servers'
        });
        Object.defineProperty(this, "onNotification", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "options", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: null
        });
        Object.defineProperty(this, "process", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: null
        });
        Object.defineProperty(this, "lineReader", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: null
        });
        Object.defineProperty(this, "startPromise", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: null
        });
        Object.defineProperty(this, "restartTimer", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: null
        });
        Object.defineProperty(this, "disposed", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: false
        });
        Object.defineProperty(this, "initialized", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: false
        });
        Object.defineProperty(this, "autoRestart", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: true
        });
        Object.defineProperty(this, "timeoutMs", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: DEFAULT_TIMEOUT_MS
        });
        Object.defineProperty(this, "pendingRequests", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: new Map()
        });
    }
    async initialize(options) {
        console.log('[STDIO Plugin] initialize called with options:', options);
        // Note: We don't check for window here because Electron has window defined
        // The check for Node.js capabilities should be done before loading this module
        if (this.process || this.lineReader) {
            await this.dispose();
        }
        const stdioOptions = options;
        const command = stdioOptions.command?.trim();
        if (!command) {
            throw new Error('STDIO transport requires a command to run');
        }
        this.options = {
            ...stdioOptions,
            command
        };
        this.autoRestart = stdioOptions.autoRestart !== false;
        this.timeoutMs = typeof stdioOptions.timeoutMs === 'number' ? stdioOptions.timeoutMs : DEFAULT_TIMEOUT_MS;
        this.disposed = false;
        console.log('[STDIO Plugin] Calling ensureStarted');
        await this.ensureStarted();
        console.log('[STDIO Plugin] initialize complete');
    }
    isReady() {
        return this.initialized && this.process !== null && !this.process.killed;
    }
    async sendRequest(method, params, options = {}) {
        console.log('[STDIO Plugin] sendRequest called:', method, params);
        if (this.disposed) {
            throw new Error('STDIO transport disposed');
        }
        await this.ensureStarted();
        const proc = this.process;
        if (!proc || !proc.stdin) {
            throw new Error('STDIO transport process is not available');
        }
        const payload = createRpcPayload(method, params ?? {});
        const requestId = payload.id;
        if (options.signal?.aborted) {
            throw new Error('MCP request was aborted');
        }
        return await new Promise((resolve, reject) => {
            let entry;
            const cleanup = () => {
                if (entry.timeout) {
                    clearTimeout(entry.timeout);
                }
                entry.abortCleanup?.();
                this.pendingRequests.delete(requestId);
            };
            entry = {
                resolve: (message) => {
                    cleanup();
                    resolve(message);
                },
                reject: (error) => {
                    cleanup();
                    reject(error);
                },
                onEvent: options.onEvent
            };
            if (options.signal) {
                const controller = new AbortController();
                entry.abortCleanup = bindAbortSignal(options.signal, controller);
                if (options.signal.aborted) {
                    cleanup();
                    reject(new Error('MCP request was aborted'));
                    return;
                }
                controller.signal.addEventListener('abort', () => {
                    cleanup();
                    reject(new Error('MCP request was aborted'));
                }, { once: true });
            }
            entry.timeout = setTimeout(() => {
                cleanup();
                reject(new Error(`MCP request "${method}" timed out after ${this.timeoutMs}ms`));
            }, this.timeoutMs);
            this.pendingRequests.set(requestId, entry);
            try {
                const writable = proc.stdin;
                if (!writable) {
                    throw new Error('STDIO transport stdin is not available');
                }
                const payloadText = `${JSON.stringify(payload)}\n`;
                if (!writable.write(payloadText)) {
                    writable.once('drain', () => {
                        // Request completes when a response is received
                    });
                }
            }
            catch (error) {
                cleanup();
                reject(new Error(`Failed to send MCP request: ${error instanceof Error ? error.message : String(error)}`));
            }
        });
    }
    async dispose() {
        this.disposed = true;
        this.initialized = false;
        if (this.restartTimer) {
            clearTimeout(this.restartTimer);
            this.restartTimer = null;
        }
        this.rejectAllPendingRequests(new Error('STDIO transport disposed'));
        if (this.lineReader) {
            this.lineReader.removeAllListeners();
            this.lineReader.close();
            this.lineReader = null;
        }
        const proc = this.process;
        this.process = null;
        if (proc) {
            await new Promise((resolve) => {
                const timeout = setTimeout(() => {
                    if (!proc.killed) {
                        proc.kill('SIGKILL');
                    }
                    resolve();
                }, 2000);
                proc.once('exit', () => {
                    clearTimeout(timeout);
                    resolve();
                });
                if (!proc.killed) {
                    proc.kill('SIGTERM');
                }
            });
        }
        this.startPromise = null;
        this.options = null;
    }
    async ensureStarted() {
        console.log('[STDIO Plugin] ensureStarted - disposed:', this.disposed, 'startPromise:', !!this.startPromise);
        if (this.disposed) {
            throw new Error('STDIO transport disposed');
        }
        if (!this.startPromise) {
            console.log('[STDIO Plugin] Creating new startProcess promise');
            this.startPromise = this.startProcess();
        }
        await this.startPromise;
        console.log('[STDIO Plugin] ensureStarted complete');
    }
    async startProcess() {
        console.log('[STDIO Plugin] startProcess beginning');
        console.log('[STDIO Plugin] process.platform:', process.platform);
        console.log('[STDIO Plugin] Current process.env.PATH:', process.env.PATH);
        if (!this.options) {
            throw new Error('STDIO transport options are not configured');
        }
        // Ensure Node.js modules are loaded
        await ensureNodeModules();
        if (!spawnFn) {
            throw new Error('Failed to load Node.js child_process module');
        }
        // Fix PATH for macOS packaged apps - the renderer process doesn't inherit shell PATH
        let fixedEnv = { ...process.env, ...this.options.env };
        console.log('[STDIO Plugin] About to check PATH fix. Platform:', process.platform, 'PATH includes nvm:', fixedEnv.PATH?.includes('nvm'));
        if (process.platform === 'darwin') {
            console.log('[STDIO Plugin] Fixing PATH for macOS. Current PATH:', fixedEnv.PATH);
            // Get homedir from os module, not env vars
            const os = globalThis.require?.('node:os') || await import('node:os');
            const homedir = os.homedir();
            let foundNvmPath = false;
            // Try to get PATH from shell with .zprofile sourced
            try {
                const { execSync } = globalThis.require?.('node:child_process') || await import('node:child_process');
                const shell = process.env.SHELL || '/bin/zsh';
                const shellPath = execSync(`${shell} -l -c 'source ~/.zprofile 2>/dev/null; echo $PATH'`, {
                    encoding: 'utf8',
                    env: { HOME: homedir }
                }).trim();
                if (shellPath && shellPath.length > 20 && shellPath.includes('nvm')) {
                    fixedEnv.PATH = shellPath;
                    foundNvmPath = true;
                    console.log('[STDIO Plugin] Fixed PATH from shell profile:', shellPath.substring(0, 150));
                }
            }
            catch (error) {
                console.warn('[STDIO Plugin] Could not get PATH from shell:', error);
            }
            // Fallback: auto-detect NVM installation
            if (!foundNvmPath) {
                console.log('[STDIO Plugin] Attempting to auto-detect NVM installation');
                const nvmDir = process.env.NVM_DIR || `${homedir}/.nvm`;
                let nvmNodePath = null;
                try {
                    const fs = globalThis.require?.('node:fs') || await import('node:fs');
                    const path = globalThis.require?.('node:path') || await import('node:path');
                    const nvmVersionsPath = path.join(nvmDir, 'versions/node');
                    const versions = fs.readdirSync(nvmVersionsPath);
                    if (versions.length > 0) {
                        // Use the latest version (sorted descending)
                        const latestVersion = versions.sort().reverse()[0];
                        nvmNodePath = path.join(nvmVersionsPath, latestVersion, 'bin');
                        console.log('[STDIO Plugin] Found NVM node at:', nvmNodePath);
                    }
                }
                catch (err) {
                    console.warn('[STDIO Plugin] Could not detect NVM installation:', err);
                }
                const commonPaths = [
                    nvmNodePath,
                    '/opt/homebrew/bin',
                    '/usr/local/bin',
                    '/usr/bin',
                    '/bin',
                    '/usr/sbin',
                    '/sbin',
                ].filter(Boolean);
                fixedEnv.PATH = [...commonPaths, fixedEnv.PATH].filter(Boolean).join(':');
                console.log('[STDIO Plugin] Using fallback PATH:', fixedEnv.PATH.substring(0, 150));
            }
        }
        console.log('[STDIO Plugin] Final PATH for spawn:', fixedEnv.PATH?.substring(0, 150));
        console.log('[STDIO Plugin] Spawning process:', this.options.command, this.options.args);
        const child = spawnFn(this.options.command, this.options.args ?? [], {
            stdio: ['pipe', 'pipe', 'pipe'],
            cwd: this.options.cwd,
            env: fixedEnv
        });
        this.process = child;
        if (!child.stdout || !child.stdin) {
            child.kill('SIGKILL');
            this.process = null;
            throw new Error('Failed to create stdio pipes for MCP process');
        }
        this.attachProcessListeners(child);
        await new Promise((resolve, reject) => {
            let settled = false;
            const handleSpawn = () => {
                settled = true;
                cleanup();
                this.initialized = true;
                resolve();
            };
            const handleError = (error) => {
                cleanup();
                if (!settled) {
                    reject(error);
                }
                else {
                    this.handleProcessError(error);
                }
            };
            const cleanup = () => {
                child.off('spawn', handleSpawn);
                child.off('error', handleError);
            };
            child.once('spawn', handleSpawn);
            child.once('error', handleError);
        });
    }
    attachProcessListeners(child) {
        if (this.lineReader) {
            this.lineReader.removeAllListeners();
            this.lineReader.close();
        }
        if (!createInterfaceFn) {
            throw new Error('Node.js readline module not loaded');
        }
        if (child.stdout) {
            this.lineReader = createInterfaceFn({
                input: child.stdout,
                crlfDelay: Infinity
            });
            this.lineReader.on('line', (line) => {
                this.handleMessage(line);
            });
        }
        child.on('error', (error) => {
            this.handleProcessError(error);
        });
        child.on('exit', (code, signal) => {
            this.handleProcessExit(code, signal);
        });
        if (child.stderr) {
            child.stderr.on('data', (data) => {
                console.warn(`MCP stdio stderr: ${data.toString().trim()}`);
            });
        }
    }
    handleMessage(line) {
        const trimmed = line.trim();
        if (!trimmed) {
            return;
        }
        try {
            const message = JSON.parse(trimmed);
            const maybeId = message.id;
            if (typeof maybeId === 'number') {
                const pending = this.pendingRequests.get(maybeId);
                if (pending) {
                    const isError = isRpcErrorForId(message, maybeId);
                    const isResponse = isRpcResponseForId(message, maybeId);
                    const isFinal = isError || isResponse;
                    pending.onEvent?.(message, { requestId: maybeId, isFinal });
                    if (isError) {
                        const { error } = message;
                        pending.reject(createRpcError(error));
                        return;
                    }
                    if (isResponse) {
                        pending.resolve(message);
                        return;
                    }
                }
            }
            if ('method' in message && !('id' in message)) {
                this.onNotification?.(message);
            }
        }
        catch (error) {
            console.warn('Failed to parse MCP stdio message:', error, line);
        }
    }
    handleProcessError(error) {
        if (this.disposed) {
            return;
        }
        console.error('MCP stdio process error:', error);
        this.rejectAllPendingRequests(new Error(`MCP stdio process error: ${error.message}`));
    }
    handleProcessExit(code, signal) {
        this.initialized = false;
        this.lineReader?.removeAllListeners();
        this.lineReader?.close();
        this.lineReader = null;
        this.process = null;
        this.startPromise = null;
        const reason = signal ? `terminated by signal ${signal}` : `exited with code ${code}`;
        const exitError = new Error(`MCP stdio process ${reason}`);
        this.rejectAllPendingRequests(exitError);
        if (!this.disposed && this.autoRestart) {
            if (this.restartTimer) {
                clearTimeout(this.restartTimer);
            }
            this.restartTimer = setTimeout(() => {
                this.restartTimer = null;
                if (this.disposed) {
                    return;
                }
                this.startPromise = this.startProcess().catch((err) => {
                    console.error('Failed to restart MCP stdio process:', err);
                    this.startPromise = null;
                });
            }, RESTART_DELAY_MS);
        }
    }
    rejectAllPendingRequests(error) {
        for (const [requestId, pending] of this.pendingRequests) {
            pending.onEvent?.({
                jsonrpc: '2.0',
                id: requestId,
                error: {
                    code: error.code ?? -32000,
                    message: error.message
                }
            }, { requestId, isFinal: true });
            if (pending.timeout) {
                clearTimeout(pending.timeout);
            }
            pending.abortCleanup?.();
            pending.reject(error);
        }
        this.pendingRequests.clear();
    }
}
// Create a unique plugin instance for each server configuration
// This is necessary because the plugin maintains stateful connections (child process, pending requests)
export const createStdioTransportConfig = (options) => {
    // Create a new plugin instance for this specific server
    const plugin = new StdioTransportPlugin();
    return {
        plugin,
        options
    };
};
