import { Injectable, Logger } from '@nestjs/common';

export interface McpToolDef {
  name: string;
  description?: string;
  inputSchema: Record<string, unknown>;
}

/**
 * McpClientService — 极简 HTTP MCP 客户端(raw JSON-RPC over Streamable HTTP)。
 *
 * 助手作为"外部 agent"通过此客户端**真连** PAC 自己的 MCP 端点(dogfood 验证契约)。
 * 转发调用方(登录用户)的 Bearer token → MCP 工具在该用户的 tenant scope 下执行。
 *
 * 无状态:PAC MCP server enableJsonResponse + 无 session,tools/list / tools/call 可直接 POST,无需握手。
 */
@Injectable()
export class McpClientService {
  private readonly logger = new Logger(McpClientService.name);
  // 与 MCP server 同进程,走 loopback;PORT 即 main.ts 的监听端口(默认 3001,与之一致)。
  // 异常拓扑(反代/独立部署)时用 PAC_MCP_URL 覆盖。
  private readonly url =
    process.env.PAC_MCP_URL ?? `http://127.0.0.1:${process.env.PORT ?? '3001'}/pac/v1/mcp`;
  /**
   * 工具清单缓存 —— **按能力分桶**,不是一个全局清单。
   *
   * ⚠️ 原实现是 `McpToolDef[] | null` 单例。工具清单当时确实与调用人无关(全只读、全员一样),
   * 但分配功能要按 `plan:dispatch` **条件注册**工具(主管多几个)——
   * 单例缓存这时会当场串号:进程重启后第一个进来的若是客服,
   * 缓存下客服的短清单,**全公司的主管在缓存失效前都拿不到分配工具**;
   * 反过来更糟,客服会拿到主管的工具清单。
   * 而且两种错法都**不报错**:模型只会说"我没有这个能力",或者调了工具被服务端拒绝。
   *
   * key 用调用方给的**能力指纹**(如 'dispatch' / 'basic')。
   * ⚠️ 刻意不在本类里解 JWT 算指纹:这是个纯 HTTP 客户端,让它认识权限模型
   * 就等于把权限判定散到第二个地方去。谁调谁负责给 key。
   */
  private readonly toolsCache = new Map<string, McpToolDef[]>();

  private async rpc(token: string, method: string, params?: unknown): Promise<Record<string, unknown>> {
    const res = await fetch(this.url, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        accept: 'application/json, text/event-stream',
        authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
    });
    const text = await res.text();
    const msg = parseRpc(text);
    if (msg.error) {
      throw new Error(`MCP ${method} 失败: ${(msg.error as { message?: string }).message ?? 'unknown'}`);
    }
    return (msg.result ?? {}) as Record<string, unknown>;
  }

  /**
   * @param capabilityKey 能力指纹 —— **相同 key 的人必须拿到相同的工具清单**。
   *   由调用方按权限算(见 assistant.service.ts 的 mcpCapabilityKey)。
   *   传 `undefined` 会退回单一全局桶,只在确定工具清单与权限无关时才这么用。
   */
  async listTools(token: string, capabilityKey = 'default'): Promise<McpToolDef[]> {
    const hit = this.toolsCache.get(capabilityKey);
    if (hit) return hit;
    const r = await this.rpc(token, 'tools/list');
    const tools = (r.tools as McpToolDef[]) ?? [];
    this.toolsCache.set(capabilityKey, tools);
    return tools;
  }

  /** 调工具 → 返回纯文本结果(MCP content[].text 拼接);isError 时抛出供 agent 看到。 */
  async callTool(token: string, name: string, args: unknown): Promise<string> {
    const r = await this.rpc(token, 'tools/call', { name, arguments: args ?? {} });
    const content = (r.content as Array<{ type: string; text?: string }>) ?? [];
    const text = content.map((c) => c.text ?? '').join('\n');
    if (r.isError) throw new Error(text || `工具 ${name} 返回错误`);
    return text;
  }
}

/** 解析 MCP 响应:plain JSON 或 SSE 帧(data: {...})。 */
function parseRpc(text: string): Record<string, unknown> {
  const t = text.trim();
  if (t.startsWith('{')) return JSON.parse(t) as Record<string, unknown>;
  const dataLine = t.split('\n').find((l) => l.startsWith('data:'));
  if (!dataLine) throw new Error(`无法解析 MCP 响应: ${t.slice(0, 120)}`);
  return JSON.parse(dataLine.slice('data:'.length).trim()) as Record<string, unknown>;
}
