import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

export interface McpToolClientConfig {
  name: string;
  mcpUrl: string;
  bearerToken: string;
}

export interface McpToolCaller {
  callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
  listTools(): Promise<string[]>;
  close(): Promise<void>;
}

export class McpToolClient implements McpToolCaller {
  private client?: Client;

  constructor(private readonly config: McpToolClientConfig) {}

  async connect(): Promise<void> {
    const transport = new StreamableHTTPClientTransport(new URL(this.config.mcpUrl), {
      requestInit: {
        headers: { authorization: `Bearer ${this.config.bearerToken}` },
      },
    });
    const client = new Client(
      { name: `exult-voice-agent-${this.config.name}`, version: "0.1.0" },
      { capabilities: {} },
    );
    await client.connect(transport);
    this.client = client;
  }

  async listTools(): Promise<string[]> {
    const result = await this.withReconnect((client) => client.listTools());
    return result.tools.map((tool) => tool.name);
  }

  async callTool(name: string, args: Record<string, unknown> = {}): Promise<unknown> {
    const result = await this.withReconnect((client) =>
      client.callTool({ name, arguments: args }),
    );
    return parseMcpToolContent(result.content);
  }

  async close(): Promise<void> {
    await this.client?.close();
    this.client = undefined;
  }

  private async ensureClient(): Promise<Client> {
    if (!this.client) await this.connect();
    return this.client!;
  }

  private async withReconnect<T>(fn: (client: Client) => Promise<T>): Promise<T> {
    const client = await this.ensureClient();
    try {
      return await fn(client);
    } catch (err) {
      if (!isReconnectableMcpError(err)) throw err;
      await this.close();
      const retry = await this.ensureClient();
      return fn(retry);
    }
  }
}

export function isReconnectableMcpError(err: unknown): boolean {
  const message =
    err instanceof Error ? err.message : typeof err === "string" ? err : "";
  return /connection|transport|socket|network|closed|terminated|econnreset|fetch failed/i.test(
    message,
  );
}

export function parseMcpToolContent(content: unknown): unknown {
  if (!Array.isArray(content)) return content;
  const textParts = content
    .filter((part): part is { type: string; text: string } => {
      return (
        part !== null &&
        typeof part === "object" &&
        (part as { type?: unknown }).type === "text" &&
        typeof (part as { text?: unknown }).text === "string"
      );
    })
    .map((part) => part.text);

  if (textParts.length === 0) return content;
  if (textParts.length === 1) {
    try {
      return JSON.parse(textParts[0]!);
    } catch {
      return textParts[0];
    }
  }
  return textParts;
}
