/**
 * MCP Client — wraps the MCP SDK to manage a single server connection.
 *
 * Handles stdio transport lifecycle, tool discovery, and tool invocation.
 * Each McpClient instance maps to one MCP server process.
 */

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

import type { McpServerConfig, McpToolInfo, McpServerStatus } from './types.js';

export class McpClient {
  readonly serverName: string;

  private client: Client | null = null;
  private transport: StdioClientTransport | null = null;
  private _status: McpServerStatus = 'disconnected';
  private _tools: McpToolInfo[] = [];
  private _error?: string;
  private config: McpServerConfig;

  get status(): McpServerStatus {
    return this._status;
  }

  get tools(): McpToolInfo[] {
    return this._tools;
  }

  get error(): string | undefined {
    return this._error;
  }

  constructor(config: McpServerConfig) {
    this.config = config;
    this.serverName = config.name;
  }

  /**
   * Connect to the MCP server: spawn process, initialize protocol, discover tools.
   */
  async connect(): Promise<void> {
    if (this._status === 'connected' || this._status === 'connecting') {
      return;
    }

    this._status = 'connecting';
    this._error = undefined;

    try {
      // Create stdio transport — spawns the server process
      this.transport = new StdioClientTransport({
        command: this.config.command,
        args: this.config.args,
        env: this.config.env,
        cwd: this.config.cwd,
        stderr: 'pipe',
      });

      // Create MCP client
      this.client = new Client(
        { name: 'flynn', version: '0.1.0' },
        { capabilities: {} },
      );

      // Connect — performs MCP initialize handshake
      await this.client.connect(this.transport);

      // Discover tools
      await this.refreshTools();

      this._status = 'connected';
      console.log(`MCP server '${this.serverName}' connected (${this._tools.length} tools)`);
    } catch (error) {
      this._status = 'error';
      this._error = error instanceof Error ? error.message : String(error);
      console.error(`MCP server '${this.serverName}' failed to connect:`, this._error);

      // Clean up partial state
      await this.cleanup();
      throw error;
    }
  }

  /**
   * Disconnect from the MCP server and kill the process.
   */
  async disconnect(): Promise<void> {
    await this.cleanup();
    this._status = 'disconnected';
    this._tools = [];
    this._error = undefined;
  }

  /**
   * Re-discover tools from the server (e.g. after a tools/list_changed notification).
   */
  async refreshTools(): Promise<McpToolInfo[]> {
    if (!this.client) {
      throw new Error(`MCP server '${this.serverName}' is not connected`);
    }

    const result = await this.client.listTools();

    this._tools = result.tools.map((t) => ({
      name: t.name,
      description: t.description ?? '',
      inputSchema: {
        type: 'object' as const,
        properties: (t.inputSchema.properties ?? {}) as Record<string, unknown>,
        required: t.inputSchema.required,
      },
    }));

    return this._tools;
  }

  /**
   * Call a tool on this MCP server by its original (unprefixed) name.
   *
   * Returns the text content of the result, or throws on error.
   */
  async callTool(toolName: string, args: Record<string, unknown>): Promise<{ content: string; isError: boolean }> {
    if (!this.client) {
      throw new Error(`MCP server '${this.serverName}' is not connected`);
    }

    const result = await this.client.callTool({ name: toolName, arguments: args });

    // Extract text content from the result
    const textParts: string[] = [];
    let isError = false;

    if ('isError' in result && result.isError) {
      isError = true;
    }

    if ('content' in result && Array.isArray(result.content)) {
      for (const block of result.content) {
        if ('text' in block && typeof block.text === 'string') {
          textParts.push(block.text);
        }
      }
    }

    // Fallback: if toolResult format is returned instead
    if ('toolResult' in result && textParts.length === 0) {
      textParts.push(String(result.toolResult));
    }

    return {
      content: textParts.join('\n') || '(no output)',
      isError,
    };
  }

  /** Clean up transport and client without changing status. */
  private async cleanup(): Promise<void> {
    try {
      if (this.transport) {
        await this.transport.close();
      }
    } catch {
      // Ignore close errors
    }
    this.client = null;
    this.transport = null;
  }
}
