import { spawn, type ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import type {
  MCPServerConfig,
  MCPToolDefinition,
  MCPResource,
  MCPPrompt,
  MCPServerInfo,
  MCPToolResult,
  JsonRpcRequest,
  JsonRpcResponse,
} from './types.js';

/**
 * MCP client for communicating with MCP servers via JSON-RPC over stdio
 */
export class MCPClient extends EventEmitter {
  private process: ChildProcess | null = null;
  private config: MCPServerConfig;
  private serverName: string;
  private requestId = 0;
  private pendingRequests = new Map<
    number | string,
    { resolve: (value: unknown) => void; reject: (error: Error) => void }
  >();
  private buffer = '';
  private serverInfo: MCPServerInfo | null = null;
  private connected = false;

  constructor(serverName: string, config: MCPServerConfig) {
    super();
    this.serverName = serverName;
    this.config = config;
  }

  /**
   * Connect to the MCP server
   */
  async connect(): Promise<MCPServerInfo> {
    if (this.connected) {
      return this.serverInfo!;
    }

    return new Promise((resolve, reject) => {
      const timeout = this.config.timeout ?? 30000;
      const timer = setTimeout(() => {
        this.disconnect();
        reject(new Error(`MCP server ${this.serverName} connection timed out`));
      }, timeout);

      try {
        this.process = spawn(this.config.command, this.config.args ?? [], {
          cwd: this.config.cwd,
          env: { ...process.env, ...this.config.env },
          stdio: ['pipe', 'pipe', 'pipe'],
        });

        this.process.stdout?.on('data', (data: Buffer) => {
          this.handleData(data.toString());
        });

        this.process.stderr?.on('data', (data: Buffer) => {
          this.emit('stderr', data.toString());
        });

        this.process.on('error', (err) => {
          clearTimeout(timer);
          reject(new Error(`Failed to start MCP server ${this.serverName}: ${err.message}`));
        });

        this.process.on('exit', (code) => {
          this.connected = false;
          this.emit('exit', code);
        });

        // Initialize the connection
        this.initialize()
          .then((info) => {
            clearTimeout(timer);
            this.connected = true;
            this.serverInfo = info;
            resolve(info);
          })
          .catch((err) => {
            clearTimeout(timer);
            this.disconnect();
            reject(err);
          });
      } catch (err) {
        clearTimeout(timer);
        reject(err);
      }
    });
  }

  /**
   * Disconnect from the MCP server
   */
  disconnect(): void {
    if (this.process) {
      this.process.kill();
      this.process = null;
    }
    this.connected = false;
    this.pendingRequests.clear();
    this.buffer = '';
  }

  /**
   * Check if connected
   */
  isConnected(): boolean {
    return this.connected;
  }

  /**
   * Get server info
   */
  getServerInfo(): MCPServerInfo | null {
    return this.serverInfo;
  }

  /**
   * List available tools
   */
  async listTools(): Promise<MCPToolDefinition[]> {
    const response = await this.request<{ tools: MCPToolDefinition[] }>('tools/list', {});
    return response.tools ?? [];
  }

  /**
   * Call a tool
   */
  async callTool(name: string, args: Record<string, unknown>): Promise<MCPToolResult> {
    return this.request<MCPToolResult>('tools/call', { name, arguments: args });
  }

  /**
   * List available resources
   */
  async listResources(): Promise<MCPResource[]> {
    const response = await this.request<{ resources: MCPResource[] }>('resources/list', {});
    return response.resources ?? [];
  }

  /**
   * Read a resource
   */
  async readResource(uri: string): Promise<{ contents: Array<{ uri: string; text?: string; blob?: string }> }> {
    return this.request('resources/read', { uri });
  }

  /**
   * List available prompts
   */
  async listPrompts(): Promise<MCPPrompt[]> {
    const response = await this.request<{ prompts: MCPPrompt[] }>('prompts/list', {});
    return response.prompts ?? [];
  }

  /**
   * Get a prompt
   */
  async getPrompt(
    name: string,
    args?: Record<string, string>
  ): Promise<{ description?: string; messages: Array<{ role: string; content: unknown }> }> {
    return this.request('prompts/get', { name, arguments: args });
  }

  /**
   * Send a JSON-RPC request
   */
  private async request<T>(method: string, params: unknown): Promise<T> {
    if (!this.connected || !this.process) {
      throw new Error(`MCP server ${this.serverName} is not connected`);
    }

    const id = ++this.requestId;
    const request: JsonRpcRequest = {
      jsonrpc: '2.0',
      id,
      method,
      params,
    };

    return new Promise<T>((resolve, reject) => {
      this.pendingRequests.set(id, {
        resolve: resolve as (value: unknown) => void,
        reject,
      });

      const message = JSON.stringify(request) + '\n';
      this.process!.stdin?.write(message);
    });
  }

  /**
   * Initialize the MCP connection
   */
  private async initialize(): Promise<MCPServerInfo> {
    const response = await this.request<{
      protocolVersion: string;
      serverInfo: { name: string; version: string };
      capabilities: Record<string, unknown>;
    }>('initialize', {
      protocolVersion: '2024-11-05',
      clientInfo: {
        name: 'emdash-core-ts',
        version: '0.1.0',
      },
      capabilities: {},
    });

    // Send initialized notification
    const notification = JSON.stringify({
      jsonrpc: '2.0',
      method: 'notifications/initialized',
    }) + '\n';
    this.process!.stdin?.write(notification);

    return {
      name: response.serverInfo.name,
      version: response.serverInfo.version,
      capabilities: {
        tools: !!response.capabilities.tools,
        resources: !!response.capabilities.resources,
        prompts: !!response.capabilities.prompts,
      },
    };
  }

  /**
   * Handle incoming data from the server
   */
  private handleData(data: string): void {
    this.buffer += data;

    // Process complete JSON-RPC messages (newline-delimited)
    const lines = this.buffer.split('\n');
    this.buffer = lines.pop() ?? '';

    for (const line of lines) {
      if (!line.trim()) continue;

      try {
        const message = JSON.parse(line) as JsonRpcResponse;
        this.handleMessage(message);
      } catch {
        // Ignore malformed messages
        this.emit('error', new Error(`Invalid JSON from MCP server: ${line}`));
      }
    }
  }

  /**
   * Handle a JSON-RPC message
   */
  private handleMessage(message: JsonRpcResponse): void {
    // Handle response to a request
    if (message.id !== undefined) {
      const pending = this.pendingRequests.get(message.id);
      if (pending) {
        this.pendingRequests.delete(message.id);

        if (message.error) {
          pending.reject(new Error(`MCP error: ${message.error.message}`));
        } else {
          pending.resolve(message.result);
        }
      }
    }

    // Handle notifications (no id)
    if (message.id === undefined && 'method' in message) {
      this.emit('notification', message);
    }
  }
}
