/**
 * MCP Manager —— 把外部 MCP server（开源 Notion / GitHub / Filesystem 等几百个 server）
 * 接入 FF AI ToolRegistry。
 *
 * 启动流程（onModuleInit）：
 *   1. 列出 enabled MCP servers（agent_mcp_servers 表）
 *   2. 每个 server spawn 子进程（stdio）或建 SSE 连接
 *   3. 握手 → tools/list 拿可用工具
 *   4. 包装成 AgentTool 注入 ToolRegistry（name 前缀 mcp:<serverId>:<toolName>）
 *
 * 调用流程：LLM 调 mcp:xxx:yyy → ToolRegistry 走 adapter → 转发 tools/call 给 server
 *
 * 失败：连接失败写 lastError + 该 server 工具不注册；不阻塞主流程。
 */

import { Injectable, OnModuleInit, Logger, NotFoundException, ForbiddenException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';
import type { AgentMcpServer } from '@prisma/client';
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 { ToolRegistry } from '../tools/tool-registry.service';
import type { AgentTool, ToolDescriptor, ToolInvocation, ToolResult } from '../tools/tool.types';
import { assertNonEmptyString } from '../utils/validation.util';

interface ActiveConnection {
  serverId: string;
  serverName: string;
  client: Client;
  toolNames: string[];
}

/**
 * MCP 连接配置（admin 通过 API 维护）。
 */
export interface CreateMcpServerInput {
  organizationId: string;
  createdById: string;
  name: string;
  transport: 'stdio' | 'sse';
  endpoint: string;
  args?: string[];
  env?: Record<string, string>;
}

@Injectable()
export class McpManagerService implements OnModuleInit {
  private readonly logger = new Logger(McpManagerService.name);
  /** 当前活跃连接 map：serverId → ActiveConnection */
  private readonly connections = new Map<string, ActiveConnection>();

  constructor(
    private readonly prisma: PrismaService,
    private readonly toolRegistry: ToolRegistry,
  ) {}

  /**
   * MCP boot 不阻塞 Nest bootstrap：fire-and-forget。
   * 慢连接 / 不通的 server 都不会拖长 HTTP server 就绪时间；启动后几秒钟内 MCP 工具
   * 逐个上线（每个 server 的 listTools 完成时注册）。
   */
  onModuleInit(): void {
    if (process.env.FFAI_MCP_ENABLED !== 'true') {
      this.logger.log('MCP disabled (set FFAI_MCP_ENABLED=true to enable)');
      return;
    }
    void this.bootAll();
  }

  /** 并行启动所有 enabled servers 并注入工具；单个失败隔离，不影响其他 */
  async bootAll(): Promise<void> {
    const servers = await this.prisma.agentMcpServer.findMany({ where: { enabled: true } });
    this.logger.log(`booting ${servers.length} MCP server(s) in parallel`);
    await Promise.allSettled(
      servers.map((s) =>
        this.bootOne(s).catch((err) => {
          this.logger.warn(`MCP boot failed for ${s.name}: ${(err as Error).message}`);
        }),
      ),
    );
  }

  @SkipAssertAccess('private 启动方法，无 user 上下文；server 来自 prisma findMany 已隐含 org 隔离的列表')
  private async bootOne(server: AgentMcpServer): Promise<void> {
    const transport = this.buildTransport(server);
    const client = new Client(
      { name: 'ffai-agent', version: '1.0.0' },
      { capabilities: {} },
    );
    try {
      await client.connect(transport);
      const toolsRes = await client.listTools();
      const toolNames: string[] = [];
      for (const t of toolsRes.tools ?? []) {
        const adapter = this.buildToolAdapter(server.id, server.name, t.name, t.description ?? '', client);
        try {
          this.toolRegistry.register(adapter);
          toolNames.push(t.name);
        } catch (err) {
          this.logger.warn(`tool register conflict ${adapter.descriptor.name}: ${(err as Error).message}`);
        }
      }
      this.connections.set(server.id, { serverId: server.id, serverName: server.name, client, toolNames });
      await this.prisma.agentMcpServer.update({
        where: { id: server.id },
        data: { lastConnectedAt: new Date(), lastError: null },
      });
      this.logger.log(`MCP ${server.name} connected; ${toolNames.length} tool(s) registered`);
    } catch (err) {
      await this.prisma.agentMcpServer.update({
        where: { id: server.id },
        data: { lastError: (err as Error).message.slice(0, 500) },
      });
      throw err;
    }
  }

  private buildTransport(server: AgentMcpServer): StdioClientTransport | SSEClientTransport {
    if (server.transport === 'stdio') {
      // args/env 是入库时序列化的 JSON 文本；损坏/手改数据时给出明确错误而不是 raw SyntaxError
      const args = parseJsonField<string[]>(server.args, 'args', server.name) ?? [];
      const env = parseJsonField<Record<string, string>>(server.env, 'env', server.name) ?? {};
      return new StdioClientTransport({
        command: server.endpoint,
        args,
        env: { ...process.env, ...env } as Record<string, string>,
      });
    }
    if (server.transport === 'sse') {
      return new SSEClientTransport(new URL(server.endpoint));
    }
    throw new BadRequestException(`unsupported transport: ${server.transport}`);
  }

  private buildToolAdapter(
    serverId: string,
    serverName: string,
    toolName: string,
    description: string,
    client: Client,
  ): AgentTool {
    const descriptor: ToolDescriptor = {
      name: `mcp:${serverName.replace(/[^a-zA-Z0-9_-]/g, '_')}:${toolName}`,
      description: `[MCP] ${description || toolName}`,
      // 简化：input schema 留空（MCP server 自己有 jsonschema 验证）；
      // 未来可把 server 上报的 schema 转换成 ToolDescriptor.inputSchema
      inputSchema: {},
      availability: { surface: ['web', 'desktop', 'mobile', 'teams', 'cli'] },
      writeAction: true, // 保守标记，避免 read-only mode 下泄漏
    };
    return {
      descriptor,
      async invoke(inv: ToolInvocation): Promise<ToolResult> {
        try {
          const result = await client.callTool({
            name: toolName,
            arguments: inv.input as Record<string, unknown>,
          });
          return { ok: !result.isError, output: result.content, errorMessage: result.isError ? 'mcp server returned error' : undefined };
        } catch (err) {
          return { ok: false, errorMessage: `MCP ${serverId}/${toolName}: ${(err as Error).message}` };
        }
      },
    };
  }

  // ─────────── CRUD（admin） ───────────

  /**
   * list 出口对 env 做 redaction：env 通常含 API token / OAuth secret，
   * P2 走 envelope encryption 前用 [REDACTED] 兜底，避免 admin 互相窃取凭据。
   * args 也可能含敏感参数（如 --token=xxx），一并 redact。
   * 编辑场景：admin update 时传完整新 env，不读老 env。
   */
  async list(organizationId: string): Promise<AgentMcpServer[]> {
    const rows = await this.prisma.agentMcpServer.findMany({
      where: { organizationId },
      orderBy: { updatedAt: 'desc' },
    });
    return rows.map((r) => ({
      ...r,
      env: r.env === null ? null : '[REDACTED]',
      args: r.args === null ? null : '[REDACTED]',
    }));
  }

  @SkipAssertAccess('create 入参 organizationId 来自 controller resolveActor，已含 org 校验；create 后调 bootOne 也已 SkipAssert')
  async create(input: CreateMcpServerInput): Promise<AgentMcpServer> {
    const name = assertNonEmptyString(input.name, 'name', 120);
    if (!['stdio', 'sse'].includes(input.transport)) {
      throw new BadRequestException(`unsupported transport: ${input.transport}`);
    }
    const endpoint = assertNonEmptyString(input.endpoint, 'endpoint', 2000);
    const created = await this.prisma.agentMcpServer.create({
      data: {
        organizationId: input.organizationId,
        createdById: input.createdById,
        name,
        transport: input.transport,
        endpoint,
        args: input.args ? JSON.stringify(input.args) : null,
        env: input.env ? JSON.stringify(input.env) : null,
        enabled: true,
      },
    });
    if (process.env.FFAI_MCP_ENABLED === 'true') {
      await this.bootOne(created).catch((err) => {
        this.logger.warn(`new MCP ${name} boot failed: ${(err as Error).message}`);
      });
    }
    return created;
  }

  @SkipAssertAccess('入口 list/connectionStatus 仅 admin，已通过 controller resolveActor 校验 org')
  async remove(id: string, organizationId: string): Promise<{ ok: true }> {
    const existing = await this.prisma.agentMcpServer.findUnique({ where: { id } });
    if (!existing) throw new NotFoundException('mcp server not found');
    if (existing.organizationId !== organizationId) throw new ForbiddenException('cross-org access denied');
    const conn = this.connections.get(id);
    if (conn) {
      try {
        await conn.client.close();
      } catch {
        // close 失败不阻塞 remove
      }
      this.connections.delete(id);
    }
    // 同步清 ToolRegistry 里的 mcp:<server>:* descriptor，避免 LLM 仍看到已下线 server 的工具
    const safeName = existing.name.replace(/[^a-zA-Z0-9_-]/g, '_');
    const removedTools = this.toolRegistry.unregisterPrefix(`mcp:${safeName}:`);
    if (removedTools > 0) {
      this.logger.log(`MCP ${existing.name} removed; ${removedTools} tool(s) unregistered from ToolRegistry`);
    }
    await this.prisma.agentMcpServer.delete({ where: { id } });
    return { ok: true };
  }

  /** 连接健康状态（admin 用） */
  connectionStatus(): Array<{ serverId: string; serverName: string; connected: boolean; toolCount: number }> {
    return Array.from(this.connections.values()).map((c) => ({
      serverId: c.serverId,
      serverName: c.serverName,
      connected: true,
      toolCount: c.toolNames.length,
    }));
  }
}

function parseJsonField<T>(value: string | null, field: string, serverName: string): T | null {
  if (!value) return null;
  try {
    return JSON.parse(value) as T;
  } catch (err) {
    throw new BadRequestException(
      `MCP server '${serverName}' has corrupted ${field} (invalid JSON): ${(err as Error).message}`,
    );
  }
}
