/**
 * MCP Client Service
 * 管理 MCP Server 连接、工具发现和工具执行
 *
 * 生命周期：
 *   onModuleInit()   → 连接全局配置的 MCP Servers
 *   onModuleDestroy() → 断开所有连接
 *
 * 关键方法：
 *   getDiscoveredTools() → McpToolDefinition[] (合并到工具列表)
 *   executeTool(prefixedName, input) → { output, isError }
 *   isMcpTool(toolName) → boolean
 */

import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import {
  McpServerConfig,
  McpServerState,
  McpConnectionState,
  McpToolDefinition,
  MCP_TOOL_PREFIX,
} from './mcp.types';
import { McpConfigService } from './mcp-config.service';
import { McpServerConfigRepository } from '../../database/postgres/repositories/mcp-server-config.repository';

@Injectable()
export class McpClientService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(McpClientService.name);
  private servers = new Map<string, McpServerState>();

  constructor(
    private readonly mcpConfig: McpConfigService,
    private readonly configRepo: McpServerConfigRepository,
  ) {}

  async onModuleInit() {
    // 1. Connect env-var configured servers
    if (this.mcpConfig.isGlobalEnabled()) {
      const globalServers = this.mcpConfig.getGlobalServers();
      if (globalServers.length > 0) {
        this.logger.log(`Connecting to ${globalServers.length} global MCP server(s) from env...`);
        await this.connectServers(globalServers);
      }
    }

    // 2. Connect database-configured servers
    try {
      const dbConfigs = await this.configRepo.findEnabled(null);
      if (dbConfigs.length > 0) {
        const mcpConfigs = dbConfigs
          .map(orm => this.configRepo.toMcpServerConfig(orm))
          .filter(c => !this.servers.has(c.id)); // skip already connected from env
        if (mcpConfigs.length > 0) {
          this.logger.log(`Connecting to ${mcpConfigs.length} MCP server(s) from database...`);
          await this.connectServers(mcpConfigs);
        }
      }
    } catch (error) {
      this.logger.warn(`Failed to load MCP configs from database: ${error}`);
    }

    if (this.servers.size === 0) {
      this.logger.log('MCP: No servers configured.');
    }
  }

  async onModuleDestroy() {
    await this.disconnectAll();
  }

  // ============================================================
  // Connection Management
  // ============================================================

  async connectServers(configs: McpServerConfig[]): Promise<void> {
    const results = await Promise.allSettled(
      configs.map(config => this.connectServer(config)),
    );

    const succeeded = results.filter(r => r.status === 'fulfilled').length;
    const failed = results.filter(r => r.status === 'rejected').length;
    if (failed > 0) {
      this.logger.warn(`MCP: ${succeeded} connected, ${failed} failed`);
    }
  }

  private async connectServer(config: McpServerConfig): Promise<void> {
    if (this.servers.has(config.id)) {
      this.logger.warn(`MCP server "${config.id}" already connected, skipping`);
      return;
    }

    const state: McpServerState = {
      config,
      state: McpConnectionState.CONNECTING,
      tools: [],
    };
    this.servers.set(config.id, state);

    try {
      // 1. Create transport
      let transport: StdioClientTransport | SSEClientTransport;
      if (config.transport === 'stdio') {
        transport = new StdioClientTransport({
          command: config.command!,
          args: config.args || [],
          env: { ...process.env, ...(config.env || {}) } as Record<string, string>,
        });
      } else {
        transport = new SSEClientTransport(new URL(config.url!));
      }

      // 2. Create client
      const client = new Client(
        { name: 'iconsulting-agent', version: '1.0.0' },
        { capabilities: {} },
      );

      // 3. Connect with timeout
      const timeoutMs = config.connectTimeoutMs || 10000;
      await Promise.race([
        client.connect(transport),
        new Promise<never>((_, reject) =>
          setTimeout(() => reject(new Error(`Connection timeout (${timeoutMs}ms)`)), timeoutMs),
        ),
      ]);

      // 4. Discover tools
      const toolsResult = await client.listTools();
      const tools: McpToolDefinition[] = (toolsResult.tools || []).map(t => ({
        name: `${MCP_TOOL_PREFIX}${config.id}__${t.name}`,
        originalName: t.name,
        serverId: config.id,
        description: `[MCP:${config.name}] ${t.description || t.name}`,
        input_schema: (t.inputSchema as Record<string, unknown>) || {
          type: 'object',
          properties: {},
        },
        isConcurrencySafe: config.toolsConcurrencySafe ?? true,
      }));

      // 5. Update state
      state.client = client;
      state.transport = transport;
      state.state = McpConnectionState.CONNECTED;
      state.tools = tools;
      state.connectedAt = new Date();

      this.logger.log(
        `MCP server "${config.name}" (${config.id}) connected. ` +
        `Discovered ${tools.length} tool(s): [${tools.map(t => t.originalName).join(', ')}]`,
      );
    } catch (error) {
      const errMsg = error instanceof Error ? error.message : String(error);
      state.state = McpConnectionState.ERROR;
      state.lastError = errMsg;
      this.logger.error(
        `MCP server "${config.name}" (${config.id}) connection failed: ${errMsg}`,
      );
      // 优雅降级：不 throw，仅记录错误
    }
  }

  private async disconnectServer(serverId: string): Promise<void> {
    const state = this.servers.get(serverId);
    if (!state) return;

    try {
      if (state.client) {
        await state.client.close();
      }
    } catch (error) {
      this.logger.warn(`Error disconnecting MCP server "${serverId}": ${error}`);
    } finally {
      this.servers.delete(serverId);
    }
  }

  async disconnectAll(): Promise<void> {
    const ids = Array.from(this.servers.keys());
    if (ids.length === 0) return;

    await Promise.allSettled(ids.map(id => this.disconnectServer(id)));
    this.logger.log(`Disconnected ${ids.length} MCP server(s)`);
  }

  // ============================================================
  // Tool Discovery
  // ============================================================

  /** 获取所有已连接 Server 的工具（带前缀） */
  getDiscoveredTools(): McpToolDefinition[] {
    const tools: McpToolDefinition[] = [];
    for (const state of this.servers.values()) {
      if (state.state === McpConnectionState.CONNECTED) {
        tools.push(...state.tools);
      }
    }
    return tools;
  }

  /** 判断工具名是否是 MCP 工具 */
  isMcpTool(toolName: string): boolean {
    return toolName.startsWith(MCP_TOOL_PREFIX);
  }

  /** 解析 MCP 工具名 → { serverId, originalName } */
  parseMcpToolName(
    prefixedName: string,
  ): { serverId: string; originalName: string } | null {
    if (!prefixedName.startsWith(MCP_TOOL_PREFIX)) return null;
    const rest = prefixedName.slice(MCP_TOOL_PREFIX.length);
    const separatorIndex = rest.indexOf('__');
    if (separatorIndex === -1) return null;
    return {
      serverId: rest.slice(0, separatorIndex),
      originalName: rest.slice(separatorIndex + 2),
    };
  }

  /** 获取 MCP 工具的并发安全声明（用于合并到 TOOL_CONCURRENCY_MAP） */
  getMcpConcurrencyMap(): Record<string, boolean> {
    const map: Record<string, boolean> = {};
    for (const tool of this.getDiscoveredTools()) {
      map[tool.name] = tool.isConcurrencySafe;
    }
    return map;
  }

  // ============================================================
  // Tool Execution
  // ============================================================

  /** 执行 MCP 工具（通过前缀名路由到正确的 Server） */
  async executeTool(
    prefixedName: string,
    input: Record<string, unknown>,
  ): Promise<{ output: string; isError: boolean }> {
    const parsed = this.parseMcpToolName(prefixedName);
    if (!parsed) {
      return {
        output: `<tool_use_error>Invalid MCP tool name: ${prefixedName}</tool_use_error>`,
        isError: true,
      };
    }

    const serverState = this.servers.get(parsed.serverId);
    if (!serverState || serverState.state !== McpConnectionState.CONNECTED) {
      return {
        output: `<tool_use_error>MCP server "${parsed.serverId}" is not connected</tool_use_error>`,
        isError: true,
      };
    }

    const timeoutMs = serverState.config.toolTimeoutMs || 30000;

    try {
      const result = await Promise.race([
        serverState.client!.callTool({
          name: parsed.originalName,
          arguments: input,
        }),
        new Promise<never>((_, reject) =>
          setTimeout(
            () => reject(new Error(`MCP tool timeout after ${timeoutMs}ms`)),
            timeoutMs,
          ),
        ),
      ]);

      // MCP tools/call 返回 { content: Array<{ type, text }>, isError? }
      const outputParts = ((result as any).content || []).map((c: any) => {
        if (c.type === 'text') return c.text;
        if (c.type === 'image') return '[image content]';
        if (c.type === 'resource') return `[resource: ${c.resource?.uri}]`;
        return JSON.stringify(c);
      });
      const output = outputParts.join('\n');

      return {
        output: output || '(empty result)',
        isError: !!(result as any).isError,
      };
    } catch (error) {
      const errMsg = error instanceof Error ? error.message : String(error);
      this.logger.error(`MCP tool execution failed [${prefixedName}]: ${errMsg}`);

      // 检测连接错误 → 标记 Server 为 ERROR 状态
      if (this.isConnectionError(error)) {
        serverState.state = McpConnectionState.ERROR;
        serverState.lastError = errMsg;
        this.logger.warn(
          `MCP server "${parsed.serverId}" marked as errored after tool failure`,
        );
      }

      return {
        output: `<tool_use_error>MCP tool ${parsed.originalName} failed: ${errMsg}</tool_use_error>`,
        isError: true,
      };
    }
  }

  // ============================================================
  // Runtime Management (called by AdminMcpController)
  // ============================================================

  /** Add and connect a new MCP server at runtime */
  async addAndConnectServer(config: McpServerConfig): Promise<void> {
    if (this.servers.has(config.id)) {
      throw new Error(`MCP server "${config.id}" already exists`);
    }
    await this.connectServer(config);
  }

  /** Remove and disconnect an MCP server */
  async removeServer(serverId: string): Promise<void> {
    await this.disconnectServer(serverId);
  }

  /** Reconnect a server (disconnect then connect with new config) */
  async reconnectServer(config: McpServerConfig): Promise<void> {
    if (this.servers.has(config.id)) {
      await this.disconnectServer(config.id);
    }
    await this.connectServer(config);
  }

  /** Test connection without persisting — returns result and disconnects */
  async testConnection(
    config: McpServerConfig,
  ): Promise<{ success: boolean; error?: string; toolCount?: number }> {
    try {
      let transport: StdioClientTransport | SSEClientTransport;
      if (config.transport === 'stdio') {
        transport = new StdioClientTransport({
          command: config.command!,
          args: config.args || [],
          env: { ...process.env, ...(config.env || {}) } as Record<string, string>,
        });
      } else {
        transport = new SSEClientTransport(new URL(config.url!));
      }

      const client = new Client(
        { name: 'iconsulting-agent-test', version: '1.0.0' },
        { capabilities: {} },
      );

      const timeoutMs = config.connectTimeoutMs || 10000;
      await Promise.race([
        client.connect(transport),
        new Promise<never>((_, reject) =>
          setTimeout(() => reject(new Error(`Connection timeout (${timeoutMs}ms)`)), timeoutMs),
        ),
      ]);

      const toolsResult = await client.listTools();
      const toolCount = (toolsResult.tools || []).length;

      await client.close();
      return { success: true, toolCount };
    } catch (error) {
      const errMsg = error instanceof Error ? error.message : String(error);
      return { success: false, error: errMsg };
    }
  }

  /** Get runtime state for a specific server */
  getServerState(serverId: string): McpServerState | null {
    return this.servers.get(serverId) || null;
  }

  // ============================================================
  // Health / Status
  // ============================================================

  getServerStates(): Array<{
    id: string;
    name: string;
    state: McpConnectionState;
    toolCount: number;
    lastError?: string;
  }> {
    return Array.from(this.servers.values()).map(s => ({
      id: s.config.id,
      name: s.config.name,
      state: s.state,
      toolCount: s.tools.length,
      lastError: s.lastError,
    }));
  }

  // ---- Private ----

  private isConnectionError(error: unknown): boolean {
    if (error instanceof Error) {
      const msg = error.message;
      return (
        msg.includes('ECONNREFUSED') ||
        msg.includes('EPIPE') ||
        msg.includes('ENOTCONN') ||
        msg.includes('channel closed') ||
        msg.includes('Connection reset')
      );
    }
    return false;
  }
}
