import { MCPClient } from './client.js';
import { loadMCPConfig, getEnabledServers } from './config.js';
import type { MCPServerConfig, MCPToolDefinition, MCPServerInfo } from './types.js';
import type { Logger } from '../utils/logger.js';
import { createLogger } from '../utils/logger.js';

/**
 * Connected server state
 */
interface ConnectedServer {
  client: MCPClient;
  info: MCPServerInfo;
  tools: MCPToolDefinition[];
}

/**
 * MCP Server Manager
 *
 * Manages the lifecycle of multiple MCP servers, including:
 * - Loading configuration
 * - Starting/stopping servers
 * - Tool discovery and registration
 * - Tool name collision handling
 */
export class MCPServerManager {
  private servers = new Map<string, ConnectedServer>();
  private repoRoot: string;
  private logger: Logger;
  private toolNameMap = new Map<string, string>(); // prefixed name -> server name

  constructor(options: { repoRoot: string; logger?: Logger }) {
    this.repoRoot = options.repoRoot;
    this.logger = options.logger ?? createLogger({ name: 'mcp-manager' });
  }

  /**
   * Initialize and connect to all enabled MCP servers
   */
  async initialize(): Promise<void> {
    const config = await loadMCPConfig(this.repoRoot);
    const enabledServers = getEnabledServers(config);

    const serverNames = Object.keys(enabledServers);
    if (serverNames.length === 0) {
      this.logger.debug('No MCP servers configured');
      return;
    }

    this.logger.info({ servers: serverNames }, 'Connecting to MCP servers');

    // Connect to all servers in parallel
    const results = await Promise.allSettled(
      Object.entries(enabledServers).map(([name, serverConfig]) =>
        this.connectServer(name, serverConfig)
      )
    );

    // Log results
    for (let i = 0; i < results.length; i++) {
      const result = results[i];
      const name = serverNames[i];

      if (result.status === 'rejected') {
        this.logger.error({ server: name, error: result.reason }, 'Failed to connect to MCP server');
      }
    }
  }

  /**
   * Connect to a single MCP server
   */
  private async connectServer(name: string, config: MCPServerConfig): Promise<void> {
    this.logger.debug({ server: name }, 'Connecting to MCP server');

    const client = new MCPClient(name, config);

    client.on('stderr', (data: string) => {
      this.logger.debug({ server: name, stderr: data.trim() }, 'MCP server stderr');
    });

    client.on('exit', (code: number) => {
      this.logger.warn({ server: name, code }, 'MCP server exited');
      this.servers.delete(name);
    });

    client.on('error', (err: Error) => {
      this.logger.error({ server: name, error: err.message }, 'MCP server error');
    });

    const info = await client.connect();
    this.logger.info({ server: name, version: info.version }, 'Connected to MCP server');

    // Discover tools
    let tools: MCPToolDefinition[] = [];
    if (info.capabilities.tools) {
      tools = await client.listTools();
      this.logger.info({ server: name, toolCount: tools.length }, 'Discovered MCP tools');

      // Register tool names with collision handling
      for (const tool of tools) {
        const prefixedName = this.registerToolName(name, tool.name);
        if (prefixedName !== tool.name) {
          this.logger.debug(
            { server: name, originalName: tool.name, prefixedName },
            'Tool name prefixed to avoid collision'
          );
        }
      }
    }

    this.servers.set(name, { client, info, tools });
  }

  /**
   * Register a tool name, handling collisions
   */
  private registerToolName(serverName: string, toolName: string): string {
    // Check if tool name already exists
    if (this.toolNameMap.has(toolName)) {
      // Prefix with server name to avoid collision
      const prefixedName = `${serverName}:${toolName}`;
      this.toolNameMap.set(prefixedName, serverName);
      return prefixedName;
    }

    this.toolNameMap.set(toolName, serverName);
    return toolName;
  }

  /**
   * Get all available tools from all connected servers
   */
  getAllTools(): Array<MCPToolDefinition & { serverName: string; prefixedName: string }> {
    const tools: Array<MCPToolDefinition & { serverName: string; prefixedName: string }> = [];

    for (const [serverName, server] of this.servers) {
      for (const tool of server.tools) {
        // Find the prefixed name for this tool
        let prefixedName = tool.name;
        if (this.toolNameMap.get(tool.name) !== serverName) {
          prefixedName = `${serverName}:${tool.name}`;
        }

        tools.push({
          ...tool,
          serverName,
          prefixedName,
        });
      }
    }

    return tools;
  }

  /**
   * Call a tool by name
   */
  async callTool(
    toolName: string,
    args: Record<string, unknown>
  ): Promise<{ success: boolean; result?: unknown; error?: string }> {
    // Find which server owns this tool
    const serverName = this.toolNameMap.get(toolName);
    if (!serverName) {
      return { success: false, error: `Unknown MCP tool: ${toolName}` };
    }

    const server = this.servers.get(serverName);
    if (!server) {
      return { success: false, error: `MCP server ${serverName} is not connected` };
    }

    // Get the actual tool name (remove prefix if present)
    const actualToolName = toolName.includes(':') ? toolName.split(':')[1] : toolName;

    try {
      const result = await server.client.callTool(actualToolName, args);

      if (result.isError) {
        const errorText = result.content
          .filter((c) => c.type === 'text')
          .map((c) => c.text)
          .join('\n');
        return { success: false, error: errorText || 'Tool execution failed' };
      }

      // Extract text content
      const textContent = result.content
        .filter((c) => c.type === 'text')
        .map((c) => c.text)
        .join('\n');

      return { success: true, result: textContent || result.content };
    } catch (err) {
      return { success: false, error: `Tool call failed: ${err}` };
    }
  }

  /**
   * Get a specific server
   */
  getServer(name: string): ConnectedServer | undefined {
    return this.servers.get(name);
  }

  /**
   * Get all connected server names
   */
  getServerNames(): string[] {
    return Array.from(this.servers.keys());
  }

  /**
   * Check if any servers are connected
   */
  hasServers(): boolean {
    return this.servers.size > 0;
  }

  /**
   * Disconnect all servers
   */
  async shutdown(): Promise<void> {
    this.logger.info('Shutting down MCP servers');

    for (const [name, server] of this.servers) {
      try {
        server.client.disconnect();
        this.logger.debug({ server: name }, 'Disconnected MCP server');
      } catch (err) {
        this.logger.error({ server: name, error: err }, 'Error disconnecting MCP server');
      }
    }

    this.servers.clear();
    this.toolNameMap.clear();
  }

  /**
   * Reconnect to a specific server
   */
  async reconnect(name: string): Promise<void> {
    const existing = this.servers.get(name);
    if (existing) {
      existing.client.disconnect();
      this.servers.delete(name);
    }

    const config = await loadMCPConfig(this.repoRoot);
    const serverConfig = config.servers[name];

    if (!serverConfig) {
      throw new Error(`MCP server ${name} not found in configuration`);
    }

    await this.connectServer(name, serverConfig);
  }
}
