/**
 * Audit log dedicado para tool calls MCP.
 *
 * Spec 11 §6: cada tool call gera 1 entrada em `api_audit_log` com
 * `action='mcp.tool_called'`, `actor_type='ai_agent'` (quando aplicavel),
 * `actor_api_token_id=<token>`, `resource_type='mcp_tool'`, `resource_id=<tool_name>`.
 *
 * Fire-and-forget: falha de write nunca bloqueia retorno da tool.
 */
import { audit } from "@/lib/audit";
import type { McpContext } from "./types";

interface AuditMcpToolCallInput {
  ctx: McpContext;
  toolName: string;
  args: Record<string, unknown>;
  durationMs: number;
  success: boolean;
  errorMessage?: string;
  resultSummary?: string;
  /**
   * Tamanho, em bytes, do JSON devolvido ao modelo (`tamanhoDoResultado`). Só o
   * NÚMERO: é ele que custa token, e o conteúdo pode ter dado pessoal. Ausente =
   * a chamada não teve resposta a medir (erro, recusa) ou não deu para medir.
   */
  resultBytes?: number;
  /**
   * O DESFECHO da chamada quando não é sucesso puro:
   *
   *   - `sem_resultado` — a tool declarou um vazio ("não achei"), e o `motivo`
   *     diz qual vazio foi;
   *   - `recusa` — a ferramenta RECUSOU sem efeito (`{ erro, mensagem }`,
   *     `{ permitido: false, motivo }`, `{ error }`, `{ ok: false }`), e o
   *     `motivo` é o CÓDIGO da recusa (`sem_destino`). Sem esta marca a trilha
   *     afirmava uma mutação que não aconteceu (#4342/#4343).
   *
   * O painel conta a chamada como falha por `metadata->>'success' = 'false'`
   * (`fn_agent_tool_usage`, migration 0103) e não precisa saber mais nada para
   * parar de dizer "nenhuma falha". Estes dois campos são o que separa
   * "a busca não achou" de "a ferramenta recusou" e de "a busca quebrou" no
   * dado gravado; separar os três na TELA pede um contador novo naquela
   * função — migration, trabalho do mantenedor.
   */
  desfecho?: "sem_resultado" | "recusa";
  motivo?: string;
  /**
   * O TERMO desta chamada, quando a tool declara um (`termoBuscado`). É o que
   * permite a leitura por termo (`fn_agent_tool_usage_por_termo`) contar "não
   * achei por termo" em vez de só "falhou por tool" (#484).
   *
   * O termo já viaja em `metadata.args` (`redactArgs`, linha 71), mas sob o nome
   * do argumento de cada tool — o leitor no banco teria de gravar esse nome. Um
   * campo próprio dá à leitura uma chave única, independente da tool, e sobrevive
   * a uma tool que redija os próprios args (`redigirParaAuditoria`).
   */
  termo?: string;
}

const ARGS_REDACT_KEYS = new Set([
  "authorization",
  "api_key",
  "token",
  "password",
  "cpf",
]);

function redactArgs(args: Record<string, unknown>): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(args)) {
    if (ARGS_REDACT_KEYS.has(k.toLowerCase())) {
      out[k] = "[redacted]";
    } else if (typeof v === "string" && v.length > 500) {
      out[k] = `${v.slice(0, 500)}...[truncated]`;
    } else {
      out[k] = v;
    }
  }
  return out;
}

export async function auditMcpToolCall(input: AuditMcpToolCallInput): Promise<void> {
  const { ctx, toolName, args, durationMs, success, errorMessage, resultSummary, resultBytes, desfecho, motivo, termo } =
    input;

  const metadata: Record<string, unknown> = {
    actor_type: ctx.actor.type,
    actor_id: ctx.actor.id,
    tool_name: toolName,
    args: redactArgs(args),
    duration_ms: durationMs,
    success,
  };

  if (resultSummary) metadata.result_summary = resultSummary.slice(0, 280);
  if (typeof resultBytes === "number" && Number.isFinite(resultBytes) && resultBytes >= 0) {
    metadata.result_bytes = Math.floor(resultBytes);
  }
  if (errorMessage) metadata.error = errorMessage.slice(0, 500);
  if (desfecho) metadata.desfecho = desfecho;
  if (motivo) metadata.motivo = motivo.slice(0, 200);
  // O termo, para a contagem "não achei POR TERMO" (`fn_agent_tool_usage_por_termo`).
  // Mesmo corte do motivo: é texto do cliente e o audit não guarda romance.
  if (termo) metadata.termo = termo.slice(0, 200);
  // O contato da conversa, quando o turno o injetou no ctx (hoje: ferramenta
  // de servidor MCP externo em turno de atendimento, #2147).
  if (ctx.contatoDoTurno) metadata.contact_id = ctx.contatoDoTurno;
  if (ctx.actor.type === "ai_agent" && ctx.actor.api_token_id) {
    metadata.actor_api_token_id = ctx.actor.api_token_id;
  }

  await audit({
    action: "mcp.tool_called",
    // Quem age via MCP é um TOKEN, nunca uma linha de auth.users: para um token
    // comum, ctx.actor.id é o id do próprio token (lib/mcp/auth.ts), e mandá-lo
    // como actorUserId estourava a FK api_audit_log_actor_user_id_fkey. O ator
    // já fica registrado em actorApiTokenId e em metadata.actor_id.
    actorUserId: null,
    actorApiTokenId: ctx.apiTokenId,
    organizationId: ctx.organizationId,
    resourceType: "mcp_tool",
    // `resource_id` é uuid no banco; o nome da tool ia aqui como texto e o
    // insert morria com "invalid input syntax for type uuid: crm_create_lead".
    // O nome já viaja em metadata.tool_name, que é jsonb.
    resourceId: null,
    requestId: ctx.requestId,
    metadata,
  });
}
