import { currentExecutionBoundary, currentExecutionJob } from '@/lib/atendimento/fronteira-server';
import { claimOfJob } from '@/lib/agent-engine/queue/claim';
/**
 * Tools MCP habilitadas NA TELA entrando no turno do engine (Fase 2B-tools).
 *
 * Reusa a MESMA ponte in-process do runtime nativo (lib/ai/runtime/tools:
 * pickToolsFromMcp) — audit em api_audit_log, ensureRole/ensureScope e redação
 * de PII idênticos. O actor do audit é o ai_agents.id do agente publicado, com
 * token efêmero mintado pelo padrão do repo (TTL curto, revogado no fim do turno).
 *
 * FILTRO DE SEGURANÇA (inegociável, não é knob):
 *   - crm_send_whatsapp_message NUNCA entra: enviar é SEMPRE a tool send_message
 *     do engine, atrás da cadeia runBeforeSend (CLAUDE.md princípio 2) — uma tool
 *     de envio por fora furaria anti-ban/opt-out/disclosure inteiros;
 *   - crm_request_human_handoff NUNCA entra (e a auto-injeção da ponte fica
 *     desligada): o engine tem a própria request_human_handoff com silêncio
 *     durável + cancelamento de follow-ups — duas tools de handoff confundiriam
 *     o modelo e a variante do CRM não silencia o harness.
 */
import type { Tool } from 'ai';

import { carregarServidorMcpExternoDoTurno } from '@/lib/mcp/servidor-externo/carregar';
import { carregarServidoresDoAgenteNoTurno } from '@/lib/mcp/servidor-externo/do-agente/carregar';
import { pickToolsFromMcp, type RuntimeHandoffSignal } from '@/lib/ai/runtime/tools';
import { mintEphemeralToken, revokeEphemeralToken } from '@/lib/ai/runtime/mcp_token';
import {
  COBERTA_PELA_NATIVA,
  IDS_DO_HARNESS,
  motivoDoHarness,
} from '@/lib/mcp/tools/ferramentas-do-harness';
import type { McpAuthResult } from '@/lib/mcp/auth';
import type { McpContext } from '@/lib/mcp/types';
import { modulosLigados } from '@/lib/instalacao/modulos';
import { capacidadesDaOrganizacao } from '@/lib/organizacao/capacidades';
import { filtrarToolsComCallbackDesabilitado } from '@/lib/followup/callback-policy';

import type { Logger } from '../../obs/logger';
import type { CrmEdgeConfig } from './mcp-client';
import type { PublishedAgentConfig } from '../../agent/agent-config';
import type { EfeitoDasEscrituras } from '@/lib/ai/runtime/efeito-das-escritas';

/**
 * Tools do catálogo que jamais entram num turno do engine (ver doc acima).
 *
 * A lista VIVE em `lib/mcp/tools/ferramentas-do-harness.ts` — com o motivo de
 * cada uma escrito para a tela — e é reexportada aqui porque este é o lado que
 * recusa. Enquanto ela existiu só aqui, o dono marcava `crm_send_whatsapp_message`
 * no pacote "Atender e responder" e o turno descartava sem que a tela soubesse.
 *
 * Exportado para ser ASSERÍVEL: a garantia de que o papel Operador não tem canal
 * (spec 16 §3.2) depende desta lista, e uma garantia que nenhum teste consegue
 * ler é uma garantia que ninguém percebe quando some.
 */
export const BLOCKED_TOOL_IDS: ReadonlySet<string> = IDS_DO_HARNESS;

export interface McpTurnTools {
  tools: Record<string, Tool>;
  /** ids efetivamente montados (para o log do turno — auditável). */
  toolIds: string[];
  /**
   * Capacidades ligadas que ficaram FORA porque a nativa do turno faz o mesmo
   * (`COBERTA_PELA_NATIVA`). A capacidade continua no turno, pela nativa — por
   * isso quem conta "o turno tem ferramenta do catálogo?" (a projeção) as soma.
   */
  cobertasPelaNativa: string[];
  /** revoga o token efêmero — chamar no fim do turno (caminho feliz). */
  cleanup: () => Promise<void>;
  /**
   * Nomes, em `tools`, das remotas do servidor MCP externo montadas como
   * LEITURA — quem decide é `definirFerramentasRemotas`, e a ponte só anota o
   * que montou. O turno registra o resultado delas como evidência da
   * conferência de promessa.
   */
  leiturasRemotas: ReadonlySet<string>;
}

/**
 * O que conta como "o turno tem ferramenta do catálogo" para `turnoProjeta`: as
 * montadas E as cobertas pela nativa. Sem as cobertas, o corte armaria a projeção
 * do contexto (sem os ids) num agente cuja capacidade continua no turno pela
 * nativa — e a variante do retorno usa justamente o `lead_id` do contexto.
 */
export function idsDoCatalogoQueContamParaAProjecao(
  mcp: Pick<McpTurnTools, 'toolIds' | 'cobertasPelaNativa'>,
): string[] {
  return [...mcp.toolIds, ...mcp.cobertasPelaNativa];
}

export async function buildMcpTurnTools(
  cfg: CrmEdgeConfig,
  /**
   * `contactId`: o contato do turno — ver `contatoDoTurno` em `lib/ai/runtime/tools.ts`.
   * Obrigatório de propósito: `null` só onde não há cliente (o ensaio do agente);
   * omiti-lo num turno de conversa abriria as leituras escopadas por ele.
   */
  ids: {
    organizationId: string;
    jobId: string;
    contactId: string | null;
    /** A conversa do turno — o canal que o servidor MCP externo recebe (#2147). */
    conversationId?: string | null;
  },
  agentConfig: PublishedAgentConfig,
  log: Logger,
  options?: {
    readOnly: boolean;
    /**
     * O PREDICADO ÚNICO do efeito do turno (#3333): esta função é quem alimenta, porque é
     * a ponte que executa a escrita e decide o desfecho. Quem lê é o gate
     * `registro_sem_efeito` — o turno passa o MESMO coletor para os dois lados.
     *
     * Ausente = ninguém pergunta (operador, chamador antigo): nenhum desfecho é registrado,
     * e o gate fica desarmado. Nunca o contrário.
     */
    efeitos?: EfeitoDasEscrituras;
    /**
     * As ferramentas NATIVAS que este turno tem. A do catálogo que uma delas
     * cobre (`COBERTA_PELA_NATIVA`) não é montada. Ausente = nenhuma (Operador,
     * chamador antigo): nada sai, o comportamento de antes.
     */
    nativasDoTurno?: readonly string[];
  },
): Promise<McpTurnTools | null> {
  const callbackFiltered = filtrarToolsComCallbackDesabilitado(
    agentConfig.toolIds,
    agentConfig.followup,
  );
  const allowed = callbackFiltered.filter((id) => !BLOCKED_TOOL_IDS.has(id));
  const blocked = agentConfig.toolIds.filter((id) => BLOCKED_TOOL_IDS.has(id));
  if (blocked.length > 0) {
    // A tela não oferece mais estas capacidades (a rota serve `marcavel: false`
    // com o motivo), então chegar aqui significa versão de agente PUBLICADA
    // antes da correção, ou configuração escrita por fora da tela. O log deixou
    // de ser o ÚNICO sinal — a tela mostra o porquê — mas o turno continua
    // recusando em silêncio para o modelo, de propósito.
    log.warn('tools MCP bloqueadas no turno do engine (envio/handoff são do harness)', {
      blocked_tool_ids: blocked,
      motivos: blocked.map((id) => motivoDoHarness(id)),
    });
  }
  // O early-return abaixo continua sobre `allowed` sem o corte: a ponte injeta as
  // ferramentas de proposta mesmo com a lista vazia, e o corte não pode mudar isso.
  const nativas = new Set(options?.nativasDoTurno ?? []);
  const cobertas = allowed.filter((id) => {
    const c = COBERTA_PELA_NATIVA[id];
    return c !== undefined && nativas.has(c.nativa);
  });
  if (cobertas.length > 0) {
    log.info('capacidades do catálogo cobertas pela nativa — fora do turno', { cobertas });
  }
  if (allowed.length === 0) {
    return null;
  }

  const ephemeral = await mintEphemeralToken({
    organizationId: ids.organizationId,
    runId: ids.jobId,
    readOnly: options?.readOnly,
    versionCreatedBy: agentConfig.versionCreatedBy ?? undefined,
    agentCreatedBy: agentConfig.agentCreatedBy ?? undefined,
  });

  const originJob = currentExecutionJob();
  const boundary = currentExecutionBoundary();
  const claim = originJob ? claimOfJob(originJob) : undefined;
  const ctx: McpContext = {
    sourceJobId: ids.jobId,
    ...(originJob?.id === ids.jobId && boundary && claim
      ? { meetingBooking: { sourceJobId: originJob.id, claim, boundary } }
      : {}),
    organizationId: ids.organizationId,
    role: 'ai_operator',
    // `agent_id` explícito porque é ele que vai para colunas com FK (atividade
    // da timeline); `id` continua sendo a identidade de correlação do audit.
    actor: {
      type: 'ai_agent',
      id: agentConfig.agentId,
      agent_id: agentConfig.agentId,
      role: 'ai_operator',
      api_token_id: ephemeral.id,
    },
    apiTokenId: ephemeral.id,
    requestId: ids.jobId,
    supabase: cfg.supabase,
  };
  const auth: McpAuthResult = {
    organizationId: ids.organizationId,
    role: 'ai_operator',
    actor: ctx.actor,
    apiTokenId: ephemeral.id,
    scopes: options?.readOnly
      ? ['mcp:read', 'actor:ai_agent']
      : ['mcp:read', 'mcp:write', 'actor:ai_agent'],
  };
  // O engine não usa o sinal de handoff da ponte (a tool está bloqueada) — dummy.
  const handoffSignal: RuntimeHandoffSignal = { triggered: false };

  // #2147 — servidor MCP externo registrado pela instalação. `null` SEM REDE
  // quando o agente não escolheu nenhuma remota (item 7) ou quando
  // `ids.contactId` está presente, turno de conversa, sem a opção
  // `mcp_externo_no_atendimento` do agente (item 8, escolha (b)). Ligada, o
  // servidor recebe a identidade do contato em cabeçalho (ver `carregar.ts`).
  const servidorExterno = await carregarServidorMcpExternoDoTurno(
    cfg.supabase,
    ids.organizationId,
    allowed,
    {
      ...(ids.contactId
        ? {
            contatoDoTurno: ids.contactId,
            ...(agentConfig.mcpExternoNoAtendimento === true
              ? {
                  noAtendimento: true,
                  ...(ids.conversationId ? { conversaDoTurno: ids.conversationId } : {}),
                }
              : {}),
          }
        : {}),
    },
  );

  // Servidores MCP DO AGENTE (migration 9095) — mesma carga do runtime nativo.
  const servidoresDoAgente = await carregarServidoresDoAgenteNoTurno(
    cfg.supabase,
    ids.organizationId,
    agentConfig.agentId,
    allowed,
    {
      ...(ids.contactId ? { contatoDoTurno: ids.contactId } : {}),
      ...(ids.conversationId ? { conversaDoTurno: ids.conversationId } : {}),
      ...(agentConfig.mcpExternoNoAtendimento === true ? { noAtendimento: true } : {}),
    },
  );

  const leiturasRemotas = new Set<string>();
  const tools = pickToolsFromMcp({
    supabase: cfg.supabase,
    ctx,
    auth,
    toolIds: allowed.filter((id) => !cobertas.includes(id)),
    handoffToolEnabled: false,
    proposalAiDraftEnabled: agentConfig.proposalAiDraftEnabled,
    handoffSignal,
    // "Em que negócios ele pode mexer" — o campo é OPCIONAL na interface, e
    // omiti-lo não é neutro: `escopo ?? []` e vazio significa NENHUM. Este
    // turno, que é o de produção, montava as capacidades de CRM sem escopo, e
    // por isso TODA escrita de lead era recusada — com a capacidade ligada na
    // tela e o card parado. Quem passava era só o dispatcher antigo.
    pipelineIds: agentConfig.pipelineIds,
    modulosLigados: await modulosLigados(cfg.supabase),
    capacidadesLigadas: await capacidadesDaOrganizacao(cfg.supabase, ids.organizationId),
    ...(ids.contactId ? { contatoDoTurno: ids.contactId } : {}),
    ...(servidorExterno ? { servidorMcpExterno: servidorExterno } : {}),
    ...(servidoresDoAgente.length > 0 ? { servidoresDoAgente } : {}),
    ...(options?.efeitos ? { efeitos: options.efeitos } : {}),
    leiturasRemotas,
  });

  return {
    tools,
    toolIds: Object.keys(tools),
    leiturasRemotas,
    cobertasPelaNativa: cobertas,
    // ponytail: revoke só no caminho feliz — em crash do turno o token expira
    // pelo TTL curto do mint (mesmo tradeoff aceito pelo runtime nativo no grace).
    cleanup: async () => {
      try {
        await revokeEphemeralToken(ephemeral.id, ids.organizationId);
      } catch {
        // token expira sozinho; revogação é higiene, não invariante.
      }
    },
  };
}
