/**
 * Production ChatToolDispatcher — bridges ChatService to McpProxyService.
 *
 * For an agent's chat turn, the model sees the union of all tools exposed by
 * the agent's project's MCP servers. We list them by sending each server an
 * MCP `tools/list` JSON-RPC request, then translate the result into the
 * OpenAI function-tool shape with namespaced names (`<server>__<tool>`). Tool
 * dispatch reverses the namespace and sends an `tools/call` to the right
 * server through the same proxy path the regular MCP client traffic uses.
 *
 * Listing is best-effort: a single server failing to respond does not abort
 * the whole list — its tools just won't appear that turn. Errors from the
 * dispatch path of an actual call do propagate to the chat loop, which
 * persists them as `error` tool turns and lets the model recover.
 */
import type { McpProxyService } from './mcp-proxy-service.js';
import type { IProjectRepository } from '../repositories/project.repository.js';
import type { ChatTool, ChatToolDispatcher } from './chat.service.js';
import { TOOL_NAME_SEPARATOR } from './chat.service.js';

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

export interface McpCallToolResult {
  content?: Array<{ type: string; text?: string; [k: string]: unknown }>;
  isError?: boolean;
  [k: string]: unknown;
}

export interface ChatToolDispatcherDeps {
  proxy: McpProxyService;
  projects: IProjectRepository;
  /** Optional logger for "server X failed to list" lines. */
  logger?: { warn(obj: Record<string, unknown>, msg: string): void };
}

export class ChatToolDispatcherImpl implements ChatToolDispatcher {
  constructor(private readonly deps: ChatToolDispatcherDeps) {}

  async listTools(projectId: string | null): Promise<ChatTool[]> {
    if (projectId === null) return [];
    const project = await this.deps.projects.findById(projectId);
    if (project === null) return [];
    const out: ChatTool[] = [];
    for (const ps of project.servers) {
      try {
        const res = await this.deps.proxy.execute({
          serverId: ps.serverId,
          method: 'tools/list',
        });
        if (res.error !== undefined) {
          this.deps.logger?.warn({ serverId: ps.serverId, error: res.error }, 'tools/list failed');
          continue;
        }
        const result = res.result as McpListToolsResult | undefined;
        if (result?.tools === undefined) continue;
        for (const t of result.tools) {
          out.push({
            name: `${ps.server.name}${TOOL_NAME_SEPARATOR}${t.name}`,
            description: t.description ?? '',
            parameters: t.inputSchema ?? { type: 'object', properties: {} },
          });
        }
      } catch (err) {
        this.deps.logger?.warn(
          { serverId: ps.serverId, error: (err as Error).message },
          'tools/list threw',
        );
      }
    }
    return out;
  }

  async callTool(args: {
    projectId: string;
    serverName: string;
    toolName: string;
    args: Record<string, unknown>;
  }): Promise<unknown> {
    const project = await this.deps.projects.findById(args.projectId);
    if (project === null) {
      throw new Error(`Project ${args.projectId} not found`);
    }
    const projectServer = project.servers.find((ps) => ps.server.name === args.serverName);
    if (projectServer === undefined) {
      throw new Error(`Server '${args.serverName}' is not attached to project '${project.name}'`);
    }
    const res = await this.deps.proxy.execute({
      serverId: projectServer.serverId,
      method: 'tools/call',
      params: { name: args.toolName, arguments: args.args },
    });
    if (res.error !== undefined) {
      throw new Error(`tools/call ${args.serverName}/${args.toolName} failed: ${res.error.message}`);
    }
    return res.result as McpCallToolResult;
  }
}
