/**
 * Core do MCP server (Spec 11 §5.3).
 *
 * `createMcpServer(authResult, requestId)` retorna instancia `McpServer` com
 * todas as tools desta wave registradas. Cada tool e exposta com:
 *   - Zod raw shape como inputSchema (registerTool aceita ZodRawShape).
 *   - Handler async que (a) checa role + scope, (b) chama o handler da
 *     wave 2, (c) audita em api_audit_log, (d) retorna content[] padrao
 *     MCP. Erros viram `{ isError: true, content: [...] }` (e o codigo
 *     MCP fica no metadata, nao no JSON-RPC error envelope).
 */
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { z } from "zod";

import type { ModuloOpcional } from "@/lib/instalacao/modulos";
import type { CapacidadeDaOrganizacao } from "@/lib/organizacao/capacidades";
import { createAdminClient } from "@/lib/supabase/admin";
import { auditMcpToolCall } from "./audit";
import { McpAuthError, ensureRole, ensureScope, type McpAuthResult } from "./auth";
import { recusaDaChamada, rotuloDaRecusa } from "./desfecho-da-chamada";
import { verificarTetoMcp } from "./rate-limit";
import { tamanhoDoResultado } from "./resultado-bytes";
import { allTools } from "./tools";
import { deCapacidadeDesligada, deModuloDesligado } from "./tools/catalog";
import { higienizarUuidsDeAterro } from "./uuid-de-aterro";
import type { McpContext } from "./types";

const SERVER_NAME = "deskcomm-crm";
const SERVER_VERSION = "0.1.0";

function summarizeResult(result: unknown): string | undefined {
  if (!result || typeof result !== "object") return undefined;
  const r = result as Record<string, unknown>;
  if (Array.isArray(r.contacts)) return `${r.contacts.length} contacts`;
  if (Array.isArray(r.conversations)) return `${r.conversations.length} conversations`;
  if (Array.isArray(r.messages)) return `${r.messages.length} messages`;
  if (typeof r.id === "string") return `id=${r.id}`;
  return undefined;
}

/**
 * `modulosLigados`: os módulos opcionais ligados na instalação. Capacidade de
 * módulo desligado nem é registrada — o cliente externo não a vê na lista.
 * Ausente vale como nenhum, pela mesma razão de `pickToolsFromMcp`.
 */
export function createMcpServer(
  auth: McpAuthResult,
  requestId: string,
  modulosLigados: readonly ModuloOpcional[] = [],
  capacidadesLigadas: readonly CapacidadeDaOrganizacao[] = [],
  idempotencyKey?: string,
): McpServer {
  const server = new McpServer({
    name: SERVER_NAME,
    version: SERVER_VERSION,
  });

  const supabase = createAdminClient();

  for (const tool of allTools) {
    if (deModuloDesligado(tool.name, modulosLigados)) continue;
    if (deCapacidadeDesligada(tool.name, capacidadesLigadas)) continue;
    server.registerTool(
      tool.name,
      {
        description: tool.description,
        inputSchema: tool.inputSchema,
      },
      async (rawArgs) => {
        const startedAt = Date.now();
        // A MESMA higiene do outro ingresso (`lib/ai/runtime/tools.ts`), pela
        // mesma razão: um uuid de aterro em campo opcional vira filtro por um
        // id que não existe, e o resultado vazio é lido como "não há". Aqui é o
        // caminho do MCP externo; lá é o do agente. Os dois entram no mesmo
        // handler, então os dois higienizam — deixar um de fora seria fechar a
        // porta e esquecer a janela. Ver `lib/mcp/uuid-de-aterro.ts`.
        const higiene = higienizarUuidsDeAterro(
          tool.inputSchema as Record<string, z.ZodTypeAny>,
          (rawArgs ?? {}) as Record<string, unknown>,
        );
        const args = higiene.limpos;
        const argsAudit = tool.redigirParaAuditoria ? tool.redigirParaAuditoria(args) : args;
        const ctx: McpContext = {
          ...(idempotencyKey !== undefined ? { idempotencyKey } : {}),
          organizationId: auth.organizationId,
          role: auth.role,
          actor: auth.actor,
          apiTokenId: auth.apiTokenId,
          tokenScopes: auth.scopes,
          requestId,
          supabase,
        };

        try {
          // ANTES de escopo e papel: quem está em laço estourando o teto não
          // deve pagar o custo de mais nada. Dentro do `try` de propósito — o
          // `catch` abaixo é quem AUDITA, e recusa sem rastro em
          // `api_audit_log` faria "o agente parou" virar mistério.
          await verificarTetoMcp(auth, tool.category);
          // Empresa suspensa: só a privacidade responde (LGPD nunca é
          // bloqueada). Depois do teto — a integração em laço não escreve
          // auditoria sem freio — e antes de tudo que custa.
          if (auth.orgSuspensa && !tool.permiteOrgSuspensa) {
            throw new McpAuthError(
              -32002,
              403,
              "Organization suspended: only privacy (LGPD) tools answer until the account is reactivated.",
              "org_suspended",
            );
          }
          ensureScope(auth.scopes, tool.requiresScope);
          ensureRole(auth.role, tool.requiresRole);

          const result = await tool.handler(args as never, ctx);
          const durationMs = Date.now() - startedAt;
          // Mesma regra do ingresso do agente (`lib/ai/runtime/tools.ts`, #484):
          // o vazio que a tool declara não é sucesso. Sem isto, a mesma busca
          // sem achado era `success: true` por aqui e `false` por lá.
          const motivoDoVazio = tool.motivoDoVazio?.(result) ?? null;

          // RECUSA TAMBÉM NÃO É SUCESSO (#4343). Medido na prova do #3328:
          // `crm_create_lead` recusou com `{ erro: "sem_destino" }`, nenhum
          // negócio nasceu, e esta linha gravava `success: true` — a trilha
          // afirmava uma mutação que não aconteceu. A régua é a mesma dos
          // outros dois ingressos (`lib/mcp/desfecho-da-chamada.ts`), não uma
          // lista de ferramentas: qualquer recusa no formato do catálogo conta.
          const recusa = recusaDaChamada(result);

          await auditMcpToolCall({
            ctx,
            toolName: tool.name,
            args: argsAudit,
            durationMs,
            success: motivoDoVazio === null && recusa === null,
            resultSummary: summarizeResult(result),
            // Mede o texto de `content` (o que a ferramenta devolve ao modelo). O `structuredContent`,
            // mais abaixo, leva o mesmo objeto para o cliente que prefere dado estruturado e NÃO entra
            // nesta medida: um cliente que lê os dois campos recebe cerca do dobro do que a coluna diz.
            resultBytes: tamanhoDoResultado(result),
            ...(recusa !== null
              ? {
                  errorMessage: rotuloDaRecusa(recusa),
                  desfecho: "recusa" as const,
                  ...(recusa.codigo === null ? {} : { motivo: recusa.codigo }),
                }
              : motivoDoVazio === null
                ? {}
                : { desfecho: "sem_resultado" as const, motivo: motivoDoVazio }),
          });

          return {
            content: [{ type: "text", text: JSON.stringify(result) }],
            structuredContent: result as Record<string, unknown>,
          };
        } catch (err) {
          const message = err instanceof Error ? err.message : "unknown_error";
          const durationMs = Date.now() - startedAt;

          await auditMcpToolCall({
            ctx,
            toolName: tool.name,
            args: argsAudit,
            durationMs,
            success: false,
            errorMessage: message,
          });

          return {
            isError: true,
            content: [{ type: "text", text: message }],
          };
        }
      },
    );
  }

  return server;
}
