/**
 * O protocolo MCP, na parte que a Ktalogu fala.
 *
 * MCP é JSON-RPC 2.0 por cima de HTTP: o assistente manda um POST com um
 * pedido e recebe um JSON de resposta. Aqui estão só os quatro pedidos que um
 * servidor de ferramentas precisa atender: apresentar-se (`initialize`),
 * responder que está vivo (`ping`), listar as ferramentas (`tools/list`) e
 * executar uma (`tools/call`).
 *
 * O servidor não guarda sessão: cada pedido chega com a chave e é resolvido
 * sozinho. Por isso não há fluxo de eventos aberto nem notificação saindo
 * daqui, e a resposta é sempre um JSON simples, que todo cliente MCP aceita.
 *
 * Este arquivo não sabe o que é uma chave nem o que é um orçamento. Recebe um
 * `Servidor` com as ferramentas já filtradas para quem está chamando. É o que
 * deixa o teste ao lado exercitar o protocolo sem banco.
 */

/* As versões do protocolo que este servidor atende. Quando o cliente pede uma
   delas, a resposta devolve a mesma; quando pede outra, devolve a de 2025-06. */
export const VERSOES_ACEITAS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"] as const;
export const VERSAO_PADRAO = "2025-06-18";

export type FerramentaAnunciada = {
  name: string;
  title?: string;
  description: string;
  inputSchema: Record<string, unknown>;
  annotations?: {
    title?: string;
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    openWorldHint?: boolean;
  };
};

/* `imagens`: fotos que vão junto da resposta (a prévia de uma arte), em base64. */
export type ResultadoDaFerramenta = { texto: string; erro?: boolean; imagens?: Array<{ dados: string; tipo: string }> };

export type Servidor = {
  nome: string;
  titulo: string;
  versao: string;
  instrucoes: string;
  ferramentas(): FerramentaAnunciada[];
  /* `null` = ferramenta que não existe para quem está chamando. */
  chamar(nome: string, argumentos: unknown): Promise<ResultadoDaFerramenta | null>;
};

export type Resposta = {
  jsonrpc: "2.0";
  id: string | number | null;
  result?: unknown;
  error?: { code: number; message: string };
};

const ERRO = {
  pedidoInvalido: -32600,
  metodoDesconhecido: -32601,
  parametrosInvalidos: -32602,
  interno: -32603,
} as const;

function ehObjeto(valor: unknown): valor is Record<string, unknown> {
  return Boolean(valor) && typeof valor === "object" && !Array.isArray(valor);
}

function idDe(mensagem: Record<string, unknown>): string | number | null | undefined {
  const id = mensagem.id;
  if (typeof id === "string" || typeof id === "number") return id;
  if (id === null) return null;
  return undefined;
}

function erro(id: string | number | null, code: number, message: string): Resposta {
  return { jsonrpc: "2.0", id, error: { code, message } };
}

/** O nome do programa que está do outro lado, quando ele se apresenta. */
export function clienteDoInitialize(corpo: unknown): string | null {
  const mensagens = Array.isArray(corpo) ? corpo : [corpo];
  for (const mensagem of mensagens) {
    if (!ehObjeto(mensagem) || mensagem.method !== "initialize" || !ehObjeto(mensagem.params)) continue;
    const info = mensagem.params.clientInfo;
    if (ehObjeto(info) && typeof info.name === "string" && info.name.trim()) {
      return info.name.trim().slice(0, 60);
    }
  }
  return null;
}

/** Responde uma mensagem. `null` = era uma notificação, não há o que responder. */
export async function tratarMensagem(mensagem: unknown, servidor: Servidor): Promise<Resposta | null> {
  if (!ehObjeto(mensagem) || mensagem.jsonrpc !== "2.0") {
    return erro(null, ERRO.pedidoInvalido, "Pedido inválido: esperava uma mensagem JSON-RPC 2.0.");
  }

  const id = idDe(mensagem);
  const metodo = typeof mensagem.method === "string" ? mensagem.method : "";

  /* Resposta do cliente a algo que o servidor teria pedido: não pedimos nada. */
  if (!metodo) {
    if (id === undefined || "result" in mensagem || "error" in mensagem) return null;
    return erro(id, ERRO.pedidoInvalido, "Pedido inválido: falta o método.");
  }

  /* Notificação (sem id) não tem resposta, conheça o servidor ou não. */
  if (id === undefined) return null;

  try {
    switch (metodo) {
      case "initialize": {
        const pedida = ehObjeto(mensagem.params) ? String(mensagem.params.protocolVersion ?? "") : "";
        const versao = (VERSOES_ACEITAS as readonly string[]).includes(pedida) ? pedida : VERSAO_PADRAO;
        return {
          jsonrpc: "2.0",
          id,
          result: {
            protocolVersion: versao,
            capabilities: { tools: { listChanged: false } },
            serverInfo: { name: servidor.nome, title: servidor.titulo, version: servidor.versao },
            instructions: servidor.instrucoes,
          },
        };
      }

      case "ping":
        return { jsonrpc: "2.0", id, result: {} };

      case "tools/list":
        return { jsonrpc: "2.0", id, result: { tools: servidor.ferramentas() } };

      case "tools/call": {
        const params = ehObjeto(mensagem.params) ? mensagem.params : null;
        const nome = params && typeof params.name === "string" ? params.name : "";
        if (!nome) return erro(id, ERRO.parametrosInvalidos, "Diga qual ferramenta chamar, no campo `name`.");
        const resultado = await servidor.chamar(nome, params?.arguments);
        if (!resultado) return erro(id, ERRO.parametrosInvalidos, `Ferramenta desconhecida: ${nome}.`);
        return {
          jsonrpc: "2.0",
          id,
          result: {
            content: [{ type: "text", text: resultado.texto }, ...(resultado.imagens ?? []).map((i) => ({ type: "image", data: i.dados, mimeType: i.tipo }))],
            isError: Boolean(resultado.erro),
          },
        };
      }

      default:
        return erro(id, ERRO.metodoDesconhecido, `Método não atendido: ${metodo}.`);
    }
  } catch (falha) {
    console.error("[mcp] falha ao atender", metodo, falha instanceof Error ? falha.message : falha);
    return erro(id, ERRO.interno, "Erro interno ao atender o pedido.");
  }
}

/**
 * Responde o corpo de um POST: uma mensagem ou um lote delas.
 * `corpo: null` = só havia notificações; a resposta HTTP é 202, sem conteúdo.
 */
export async function tratarCorpo(corpo: unknown, servidor: Servidor): Promise<{ corpo: Resposta | Resposta[] | null }> {
  if (Array.isArray(corpo)) {
    if (corpo.length === 0) {
      return { corpo: erro(null, ERRO.pedidoInvalido, "Pedido inválido: lote vazio.") };
    }
    if (corpo.length > 20) {
      return { corpo: erro(null, ERRO.pedidoInvalido, "Pedido inválido: lote grande demais.") };
    }
    /* O limite de uso conta pedidos. Um lote com vários comandos seria um jeito
       de multiplicar esse limite por vinte, então cada pedido executa no máximo
       um comando. Os programas de hoje já mandam um por vez. */
    const comandos = corpo.filter((mensagem) => ehObjeto(mensagem) && mensagem.method === "tools/call").length;
    if (comandos > 1) {
      return { corpo: erro(null, ERRO.pedidoInvalido, "Pedido inválido: envie um comando (tools/call) por pedido.") };
    }
    const respostas: Resposta[] = [];
    for (const mensagem of corpo) {
      const resposta = await tratarMensagem(mensagem, servidor);
      if (resposta) respostas.push(resposta);
    }
    return { corpo: respostas.length ? respostas : null };
  }
  return { corpo: await tratarMensagem(corpo, servidor) };
}
