"use client";
import { useState, type RefObject } from "react";
import { useT } from "@/hooks/i18n/useT";
import Link from "next/link";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { JanelaSelo } from "@/components/inbox/JanelaSelo";
import { ChannelLogo } from "@/components/inbox/ChannelLogo";
import { Phone, MagnifyingGlass, DotsThree, Pause, Archive, EyeSlash, UserCircle, UsersThree, Robot, Check, DownloadSimple } from "@/lib/ui/icons";
import { useAuth } from "@/hooks/auth/AuthProvider";
import { useClaimConversation } from "@/hooks/inbox/useClaimConversation";
import { useReleaseConversation } from "@/hooks/inbox/useReleaseConversation";
import {
  useMarkPersonalContact,
  useUnmarkPersonalContact,
} from "@/hooks/contacts/usePersonalContact";
import { ROLE_RANK } from "@/lib/auth/types";
import {
  useArchiveConversation,
  useCloseConversation,
  useReopenConversation,
} from "@/hooks/inbox/useCloseConversation";
import { useResumeAiAttendance } from "@/hooks/inbox/useResumeAiAttendance";
import { usePauseAiAttendance } from "@/hooks/inbox/usePauseAiAttendance";
import { useIaComDono } from "@/hooks/inbox/useIaComDono";
import { useAutomaticoAtivo } from "@/hooks/ai/useAutomaticoAtivo";
import { OwnerBadge } from "@/components/kanban/OwnerBadge";
import { FaixaDaCarteira } from "@/components/inbox/FaixaDaCarteira";
import { comandoDaConversa, ROTULO_DO_MOTIVO } from "@/lib/inbox/comando-da-conversa";
import { ReassignDialog } from "@/components/inbox/ReassignDialog";
import { SnoozeButton } from "@/components/inbox/SnoozeButton";
import { DialogoDeConvite, ParticipantesDaConversa } from "@/components/inbox/ParticipantesDaConversa";
import { DialogoMoverParaFila, ItemMoverParaFila } from "@/components/inbox/MoverParaFila";
import { DialButton } from "@/components/voice/DialButton";
import { VideoCallButton } from "@/components/inbox/VideoCallButton";
import { ItemReservarServico, ReservarServicoNaConversa } from "@/components/agenda/ReservarServicoNaConversa";
import { motivoDoContato } from "@/lib/inbox/motivo-do-envio-bloqueado";
import type { ConversationWithContact } from "@/hooks/inbox/useConversationsRealtime";
import { rotuloParaAEquipe } from "@/lib/contacts/rotulo-do-contato";
import { phoneForDisplay } from "@/lib/channels/phone-variants";
import { useEnderecoDaResposta } from "@/hooks/inbox/useEnderecoDaResposta";
import { rotuloDoCanalDaConversa } from "@/lib/channels/estado";

interface Props {
  conversation: ConversationWithContact;
  /**
   * A busca DENTRO da conversa (#1793): abre um campo que filtra só as
   * mensagens já carregadas. O ref devolve o foco a este botão quando o campo
   * fecha — senão o Esc largava o foco no `body`.
   */
  onBuscar?: () => void;
  buscaAberta?: boolean;
  botaoBuscaRef?: RefObject<HTMLButtonElement | null>;
  /** Seleciona outra conversa no Inbox — a aba Número do Transferir abre a do outro número. */
  onAbrirConversa?: (id: string) => void;
}

/**
 * O CHIP NOMEIA CICLO DE VIDA, NÃO COMANDO.
 *
 * Ele afirmava quem manda — "Automático atendendo", "Aguardando atendente" — a
 * 20px de um selo que responde a MESMA pergunta por outra fonte, e as duas se
 * contradiziam na tela: `conversations.status` não acompanha silêncio, trava de
 * contato nem atribuição, e o motor nunca o lê. Medido em 2026-08-30 num print
 * do dono: "Aguardando atendente" e "Automático" no mesmo cabeçalho.
 *
 * Quem responde "quem manda" é o `OwnerBadge`, que vem de `comandoDaConversa`.
 * Aqui fica só o que o status realmente sabe: o episódio está aberto ou acabou.
 *
 * Cobre os SETE valores do CHECK de propósito — o call site é
 * `t(STATUS_LABEL[status] ?? status)`, e um buraco imprime o token cru em inglês
 * no rosto do atendente. Vigiado pelo invariante de espelho.
 */
const STATUS_LABEL: Record<string, string> = {
  open: "Aberta",
  pending: "Aberta",
  claimed: "Aberta",
  ai_handling: "Aberta",
  resolved: "Resolvida",
  closed: "Fechada",
  archived: "Arquivada",
};

export function ConversationHeader({
  conversation,
  onAbrirConversa,
  onBuscar,
  buscaAberta,
  botaoBuscaRef,
}: Props) {
  const t = useT();
  const { user, activeOrg } = useAuth();
  const claim = useClaimConversation();
  const release = useReleaseConversation();
  const close = useCloseConversation();
  const reopen = useReopenConversation();
  const arquivar = useArchiveConversation();
  const retomar = useResumeAiAttendance();
  const pausar = usePauseAiAttendance();
  const iaComDono = useIaComDono();
  // "Existe automático nesta org?" — sem isto o selo afirmava que o robô estava
  // atendendo em instalação que nunca configurou agente nenhum.
  const automaticoDaOrg = useAutomaticoAtivo();
  const [reassignOpen, setReassignOpen] = useState(false);
  const [confirmFecharOpen, setConfirmFecharOpen] = useState(false);
  /**
   * A ESCOLHA AO FECHAR (#3313, item 1), e ela começa MARCADA em "devolver".
   *
   * O padrão não é preferência de produto: é o desfecho que a operação espera
   * ao fechar. Medido na issue — a equipe fechava conversas achando que a trava
   * de humano tinha ido embora com elas, e a próxima mensagem daquele cliente,
   * dias depois, ficava sem resposta da IA. "Manter com a equipe" continua a um
   * clique e é relembrado a cada abertura do diálogo (quem escolheu manter na
   * conversa anterior não deve ver a escolha já virada na seguinte).
   */
  const [devolverAoFechar, setDevolverAoFechar] = useState(true);
  const [confirmArquivarOpen, setConfirmArquivarOpen] = useState(false);
  const [confirmPessoalOpen, setConfirmPessoalOpen] = useState(false);
  // O diálogo do convite é do CABEÇALHO, não do item do menu (#3003): um
  // `dialog` modal dentro de um `menu` modal deixa a página inteira aria-hidden
  // enquanto o menu está aberto, e o que vem depois do convite — o avatar de
  // quem entrou, por exemplo — fica invisível para leitores de tela e para o
  // e2e. Aqui o item só levanta a bandeira e o menu fecha como qualquer outro.
  const [conviteAberto, setConviteAberto] = useState(false);
  // A folha de reserva segue o MESMO desenho do convite: o item do ⋯ só levanta
  // a bandeira, a folha mora fora do menu e o menu fecha como qualquer outro.
  const [reservaAberta, setReservaAberta] = useState(false);
  // A FILA (#3876): o item do ⋯ só levanta a bandeira — o diálogo mora fora do
  // menu, pelo mesmo motivo do convite acima.
  const [filaAberta, setFilaAberta] = useState(false);

  const c = conversation.contacts ?? null;
  /**
   * A TRAVA DE HUMANO MORA NO CONTATO, não na conversa — e é por isso que
   * fechar a conversa não a solta. Enquanto `force_human` for `true`, os três
   * guards do motor (`skip("force_human")`, `isLeadInHandoff`, `before-send`)
   * continuam vetando a IA. Sem esta leitura, a escolha do fechamento apareceria
   * em conversa nenhuma (ou em todas, que é pior: perguntaria sobre uma trava
   * que não existe).
   */
  const contatoTravadoComEquipe = c?.force_human === true;
  const displayName = rotuloParaAEquipe(c, t);
  /**
   * POR ONDE ESTA CONVERSA ENTROU — o mesmo rótulo da lista e da tela de
   * canais (#2383). `null` quando não há canal, e aí o badge não existe.
   */
  const rotuloCanal = rotuloDoCanalDaConversa(conversation.channel_sessions, t);
  const phone = c?.phone_number ? phoneForDisplay(c.phone_number) : null;
  const status = conversation.status;
  const isMineAssigned = conversation.assigned_to_user_id === user.id;
  const isOpen = status === "open" || conversation.assigned_to_user_id == null;
  /**
   * "ASSUMIR" NÃO SE OFERECE A QUEM JÁ É O DONO (#3003).
   *
   * A condição era `isOpen`, e com a conversa JÁ assumida por mim ela é
   * verdadeira (status `open`) — então o cabeçalho desenhava "Assumir" e
   * "Liberar" lado a lado, dois botões para o mesmo slot, sendo o primeiro um
   * no-op: `fn_conversation_assign` com `expected_assignee` igual ao dono não
   * muda nada. É exatamente a pergunta da issue ("o que acontece se eu clicar
   * aqui?") respondida com "nada".
   *
   * O #3012 continua de pé e é o que a condição preserva: com a conversa SEM
   * dono o botão existe (é a saída da trava do compositor), e com ela assumida
   * por OUTRA pessoa também (é o gesto de reatribuir). O que sai é só o botão
   * que não faz nada.
   */
  const podeAssumir = isOpen && !isMineAssigned;

  // Marcar/desmarcar pessoal (spec 21, etapa 15 — botão no cabeçalho).
  //
  // GERENTE pode (spec decisão 1, D3): pessoal é decisão operacional — esconder
  // uma conversa da operação. Desbloquear exige ADMIN porque reabre um canal
  // que o cliente fechou (direito do titular, LGPD): dois gestos parecidos,
  // dois gates diferentes, cada um com seu motivo.
  //
  // Os hooks ficam AQUI, antes de qualquer early return (a ordem dos hooks não
  // pode mudar entre renderizações); com contato ausente o id vazio nunca
  // dispara porque o botão nem é desenhado.
  const ehPessoal = c?.is_personal === true;
  const podeMarcarPessoal =
    (user.is_platform_admin && !user.support) ||
    (activeOrg != null && ROLE_RANK[activeOrg.role] >= ROLE_RANK.manager);
  /**
   * ATENDIMENTO CONJUNTO (#2542): quem pode CONVIDAR é o dono da conversa ou um
   * gestor+ — exatamente o gate da rota. A tela não oferece o que a rota recusa:
   * um botão que devolve 403 é pior que um botão que não existe, porque a pessoa
   * descobre o limite depois de escolher o colega na lista. Não é gate de
   * SEGURANÇA (esse mora na rota, que revalida papel e organização): é o mesmo
   * desenho de `podeMarcarPessoal`, que também espelha a régua do servidor.
   */
  const podeConvidar =
    isMineAssigned ||
    (user.is_platform_admin && !user.support) ||
    (activeOrg != null && ROLE_RANK[activeOrg.role] >= ROLE_RANK.manager);
  // Reservar exige papel de atendente (o mesmo gate da rota); grupo não é pessoa.
  const podeReservar =
    !conversation.is_group && !!c?.id && activeOrg != null && ROLE_RANK[activeOrg.role] >= ROLE_RANK.agent;
  /**
   * EXPORTAR CONVERSA (#1982, F1) — o MESMO gate da rota, e nada além dele.
   *
   * `app/api/v1/conversations/[id]/export/route.ts` abre com
   * `requireRole("agent")` e ainda dá passagem ao admin de plataforma
   * (`full`), exatamente como as demais rotas de `conversations/[id]`. Repetir
   * a régua aqui é o desenho que este componente já declarou: não oferecer o
   * que a rota recusa — um `viewer` que clica e recebe 403 num download
   * descobre o limite DEPOIS de pedir o arquivo. É a mesma comparação de
   * `ChatThread` (`podeEnviar`, :112), sem hook novo e sem `usePermission`.
   */
  const podeExportar =
    (user.is_platform_admin && !user.support) ||
    (activeOrg != null && ROLE_RANK[activeOrg.role] >= ROLE_RANK.agent);
  const marcarPessoal = useMarkPersonalContact(c?.id ?? "");
  const desmarcarPessoal = useUnmarkPersonalContact(c?.id ?? "");

  // Para qual número a resposta sai (Spec 31, B2). Fica ANTES do retorno de
  // leitura-só, porque a ordem dos hooks não pode mudar entre renderizações.
  const enderecoDaResposta = useEnderecoDaResposta(conversation.id, conversation.last_inbound_at ?? null);
  const endereco = enderecoDaResposta.data?.data ?? null;

  /**
   * QUEM MANDA, uma pergunta com uma resposta.
   *
   * Este bloco era três leituras parciais. O selo e o botão de volta liam duas
   * travas (`bot_silenced_until || force_human`); a linha da lista lia uma, por
   * COR; o painel não lia nenhuma. Desde a 0173 há uma quarta situação —
   * "alguém assumiu" — e continuar somando condições à mão aqui é como as três
   * leituras divergiram em primeiro lugar. A regra mora em `lib/inbox`,
   * espelhando os gates que o MOTOR lê, e esta tela só a consome.
   */
  const fatosDoComando = {
    status,
    assigned_to_user_id: conversation.assigned_to_user_id,
    assigned_to_user_name: conversation.assigned_to_user_name ?? null,
    assignee_kind: conversation.assignee_kind ?? null,
    bot_silenced_until: conversation.bot_silenced_until ?? null,
    last_handoff_reason: conversation.last_handoff_reason ?? null,
    force_human: c?.force_human ?? null,
    is_blocked: conversation.contacts?.is_blocked ?? null,
    is_group: conversation.is_group ?? false,
    automaticoDaOrg: automaticoDaOrg.data,
    // O fato por NÚMERO (#4012): o mesmo que o banco já usou para rebaixar
    // `comando_da_conversa` a `aguardando`. Ausente = cache anterior, e a
    // regra cai no comportamento org-wide de antes.
    automaticoDaConversa: conversation.automatico_da_conversa ?? undefined,
  };
  const { comando, automaticoAtivo, travaVigente, motivo } = comandoDaConversa(fatosDoComando);
  /**
   * O QUE "LIBERAR" FAZ DEPENDE DO ESTADO, e a guia do botão (#3003) não pode
   * prometer o automático de volta quando ele não volta.
   *
   * `fn_conversation_assign` com dono `null` deixa a conversa aberta e sem dono,
   * mas só limpa `bot_silenced_until` quando NÃO houve escalação
   * (`last_handoff_at is null`). Depois de uma escalação, com o contato travado
   * (`force_human`), descadastrado, em grupo ou numa org sem automático, a
   * conversa cai na fila esperando gente. A pergunta é a mesma regra de quem
   * manda, aplicada ao estado que a RPC vai gravar.
   */
  const liberarDevolveAoAutomatico =
    comandoDaConversa({
      ...fatosDoComando,
      status: "open",
      assigned_to_user_id: null,
      assigned_to_user_name: null,
      assignee_kind: null,
      bot_silenced_until: conversation.last_handoff_at ? fatosDoComando.bot_silenced_until : null,
    }).comando.quem === "automatico";

  const encerrada = status === "closed" || status === "archived" || status === "resolved";
  /**
   * A VOLTA aparece sempre que há algo a devolver — inclusive em conversa
   * ENCERRADA. Antes ela era condicionada a `status !== "closed"`, e o resultado
   * era um beco sem saída medido: atendente assume, fecha, sai de férias; a
   * conversa fica com o automático parado e, para qualquer colega, sem NENHUMA
   * porta — "Liberar" só existe para o próprio dono e a rota recusa quem não é.
   * `devolverAtendimentoAoAgente` funciona nesse estado (o status fechado está na
   * lista de reativáveis), então esconder o botão escondia uma ação que existe.
   *
   * A condição é `travaVigente`, e NÃO `!automaticoAtivo`: conversa encerrada tem
   * o automático inativo sem ter trava nenhuma, e sair do segundo faria o botão
   * aparecer em toda conversa fechada — clicá-lo REABRIRIA uma conversa que
   * ninguém pediu para reabrir. Oferecer uma ação que não deveria acontecer é
   * pior que não oferecer nenhuma.
   */
  const podeDevolver = travaVigente;
  /**
   * PAUSAR só aparece quando pausar é um gesto DIFERENTE de assumir.
   *
   * Desde a 0173 "Assumir" já cala o automático (a RPC grava o silêncio). Numa
   * conversa sem dono, portanto, "Assumir" e "Pausar o automático" teriam
   * exatamente o mesmo efeito — dois botões para um ato é a confusão que esta
   * entrega existe para acabar, não para dobrar.
   *
   * Sobra o caso em que ele é próprio: a conversa JÁ tem dono e o automático
   * continua de pé. Isso é real e não é raro — o rodízio (`reason='routing'`)
   * distribui sem calar, de propósito, senão uma org em round_robin ficaria sem
   * automático nenhum.
   */
  const podePausar =
    automaticoAtivo && !encerrada && conversation.assigned_to_user_id !== null;

  /**
   * O INTERRUPTOR DO MODO "IA COM O DONO" (#3967).
   *
   * Aparece quando a pergunta faz sentido: a conversa TEM dono humano e não está
   * encerrada. É o único estado em que ligar o modo muda alguma coisa — sem dono
   * não há dono a preservar, e a rota recusa (o automático já responde ali).
   *
   * Aparece TAMBÉM com o automático parado: "voltar com a IA sem soltar o dono"
   * é literalmente o caso de uso da issue (a conversa foi pausada, o atendente
   * quer a IA de apoio e não quer sair da carteira). Ligar limpa o silêncio —
   * está no cabeçalho da rota, e é o que o botão promete.
   *
   * Escondido quando o CONTATO está travado/bloqueado: ali o modo não pode
   * valer (a rota devolve 409) e um botão que só leva a erro é pior que nenhum.
   */
  const modoLigado = conversation.metadata?.ai_com_dono === true;
  const podeModoIaComDono =
    !encerrada &&
    conversation.assigned_to_user_id !== null &&
    fatosDoComando.force_human !== true &&
    fatosDoComando.is_blocked !== true;

  if (user.support?.access_mode === "support_readonly") return <header className="flex items-center justify-between border-b p-4">
    <strong>{displayName}</strong><span className="text-sm text-muted-foreground">{STATUS_LABEL[status] ?? status} · Somente leitura</span>
  </header>;
  return (
    // `flex-wrap` porque este header travava a LARGURA DA TELA INTEIRA. Ele
    // media 707px de `min-content` — a identidade do contato encolhia bem
    // (`min-w-0` + `truncate`), mas a barra de ações era `shrink-0` e não
    // quebrava. Como a coluna do meio do inbox é `1fr`, que é
    // `minmax(auto, 1fr)`, ela não podia ficar menor que esses 707px, e o
    // painel de CRM era empurrado 311px para fora da viewport em 1280px.
    //
    // Reorganizar em vez de esconder: acima de ~1440px o header fica IDÊNTICO ao
    // de antes (uma linha), e quando aperta a barra desce para a linha de baixo.
    // Nenhuma ação some da conversa (doc de decisão 251, 03/11): as do dia a
    // dia ficam à vista e o resto fica no menu ⋯ (#3003).
    //
    // NO CELULAR a quebra vira o problema oposto: com ~390px de largura a barra
    // descia em três ou quatro fileiras de botões e comia metade da tela,
    // deixando quase nada para as mensagens. Abaixo do `md` a barra é UMA
    // fileira que rola na horizontal — a mesma régua de não esconder ação, sem
    // pagar em altura. `py-2` pelo mesmo motivo: cada pixel de altura ali é
    // tirado da conversa.
    <div className="flex flex-wrap items-center justify-between gap-2 border-b border-border bg-background px-3 py-2 md:gap-3 md:px-4 md:py-3">
      {/* FAIXA DA CARTEIRA (#2591, regra 2): "este cliente pertence à carteira de
          A". `w-full` a põe numa linha própria acima da identidade, sem disputar
          espaço com a barra de ações — a faixa só existe quando o dono é OUTRA
          pessoa (o componente decide; sem carteira ele não desenha nada). */}
      {c?.id && (
        <FaixaDaCarteira contactId={c.id} quemAtendeId={conversation.assigned_to_user_id} />
      )}
      <div className="min-w-0">
        <div className="flex min-w-0 items-center gap-1.5">
          <ChannelLogo channel={conversation.channel_sessions} size={20} />
          <h2 className="min-w-0 truncate text-sm font-semibold" title={displayName}>{displayName}</h2>
          <Badge variant="outline" className="h-4 shrink-0 px-1.5 text-[10px]">
            {t(STATUS_LABEL[status] ?? status)}
          </Badge>
          {/* Ao lado do estado, não escondido num painel: a pergunta "dá para
              escrever agora?" se faz ANTES de digitar, não depois de receber um
              `failed` com um código de cinco dígitos. */}
          <JanelaSelo
            provider={conversation.channel_sessions?.provider ?? null}
            lastInboundAt={conversation.last_inbound_at}
          />
          {/* #2383: O CANAL DESTA CONVERSA, NO CABEÇALHO — a acepção que faltava
              da issue: a lista já dizia por onde a conversa entrou, a conversa
              ABERTA não dizia. O logo ao lado nomeia o provedor, não o número:
              com "Peças" e "Vendas" conectados, ele é idêntico nos dois casos.

              O TEXTO é o mesmo da lista e da tela `/app/connections`
              (`rotuloDoCanalDaConversa` → `nomeDoCanal`): uma segunda cadeia de
              regras aqui seria como as leituras de "quem manda" divergiram —
              três telas, um fato, uma fonte.

              `max-w` + `truncate` respondem ao critério de aceite "layout
              utilizável com nomes de canal maiores": o badge encolhe e o `title`
              devolve o texto inteiro. Sem canal (grupo, conversa sem sessão,
              não-WA) não há rótulo nenhum e esta barra fica exatamente como
              estava — o badge é acréscimo, não substituição. */}
          {rotuloCanal && (
            <Badge
              variant="outline"
              className="h-4 min-w-0 max-w-[9rem] truncate px-1.5 text-[10px]"
              title={`${t("Entrou por")} ${rotuloCanal}`}
              data-testid="canal-da-conversa"
            >
              <span className="truncate">{rotuloCanal}</span>
            </Badge>
          )}
        </div>

        {/* QUEM ESTÁ NO COMANDO, com nome e por GEOMETRIA — disco cheio para
            pessoa, anel vazado para o automático. É o mesmo componente do card do
            funil e do dossiê: um terceiro jeito de dizer "quem manda", por cor ou
            por texto, faria a mesma pergunta ter três respostas diferentes na
            mesma tela. Cor não sobrevive ao daltonismo nem ao teste do metro. */}
        <div className="mt-1 flex items-center gap-2" data-testid="comando-da-conversa">
          {comando.quem === "humano" ? (
            <OwnerBadge ownerKind="user" ownerName={comando.nome ?? t("Atendente")} />
          ) : comando.quem === "automatico" ? (
            <OwnerBadge ownerKind="ai" ownerName={t("Automático")} />
          ) : (
            // `ninguem`, `aguardando` e `encerrada` sem dono caem aqui: o disco
            // TRACEJADO do OwnerBadge, que é como o funil já desenha "ninguém".
            <OwnerBadge ownerKind={null} ownerName={null} />
          )}
        </div>
        {phone && (
          // No celular o telefone sai: cada linha do cabeçalho é altura tirada
          // do fio, e o número continua na Ficha.
          <p className="mt-0.5 flex min-w-0 items-center gap-1 truncate text-xs text-muted-foreground max-md:hidden">
            <Phone size={11} weight="regular" aria-hidden /> {phone}
          </p>
        )}
        {/* Contato com telefone extra: diz por qual número a resposta sai. Sem
            extra, nada muda na tela. No canal que responde pela última mensagem
            recebida, a resposta vai para o número por onde ela chegou; nos outros
            segue pelo principal, e a linha não aparece. */}
        {endereco?.has_extras && c?.phone_number && endereco.follows_last_inbound && endereco.phone_number && (
          <p className="mt-0.5 min-w-0 truncate text-xs text-muted-foreground max-md:hidden" data-testid="resposta-sai-por">
            {t("A resposta sai por:")} {phoneForDisplay(endereco.phone_number)}
          </p>
        )}
      </div>

      {/* `shrink-0` saiu daqui: era ele que impunha o piso de largura. Agora a
          barra pode encolher e quebrar internamente, e os botões continuam
          todos visíveis e clicáveis — só que em duas linhas quando preciso.
          Esta coluna existe para o selo do automático morar ABAIXO da barra
          (#1625): na linha do nome ele alargava a identidade e empurrava a
          barra inteira para baixo. Ela também não é `shrink-0`, pelo mesmo
          motivo da barra. */}
      {/* NO CELULAR (F2 da ADR-0006): com o teclado aberto a coluna sai e o fio
          fica com a altura (`data-teclado-aberto`, de `useAlturaVisivel`); a
          máscara apaga a borda direita para dizer que a fileira rola, e o `pr-8`
          deixa o último botão fora da máscara quando rolado até o fim. Tudo
          `max-md:` — no computador encolher a janela não é teclado. */}
      <div className="flex min-w-0 flex-col items-end gap-1 max-md:w-full max-md:items-stretch max-md:group-data-[teclado-aberto]/conversa:hidden">
      <div className="flex min-w-0 flex-wrap items-center gap-1.5 max-md:flex-nowrap max-md:overflow-x-auto max-md:pb-1 max-md:pr-8 max-md:[&>*]:shrink-0 max-md:[mask-image:linear-gradient(to_right,black_calc(100%_-_2rem),transparent)]">
        {/* Primeira da barra e sem rótulo escrito: é ferramenta de LEITURA, não
            ação de atendimento, e não muda de lugar com o estado da conversa.
            Só o ícone porque a barra já quebrou a caixa útil em 1280px uma vez
            (ver o comentário do interruptor abaixo). */}
        {onBuscar && (
          <Button
            ref={botaoBuscaRef}
            size="sm"
            variant="ghost"
            className="w-11 px-0 lg:w-8"
            onClick={onBuscar}
            aria-label={t("Buscar nesta conversa")}
            title={t("Buscar nesta conversa")}
            data-testid="buscar-na-conversa"
            aria-expanded={buscaAberta}
          >
            <MagnifyingGlass size={16} aria-hidden />
          </Button>
        )}
        {/* QUEM MAIS ESTÁ AQUI (#2542) fica na BARRA, e só a PRESENÇA (#3003).
            Os avatares são informação — "quem mais está comigo nesta conversa?"
            —, e informação dentro de um menu deixa de ser lida. O convite é
            AÇÃO e mudou de lugar: mora no ⋯ (`modo="convite"`), com ícone e
            nome, como as outras ações secundárias. */}
        <ParticipantesDaConversa
          conversationId={conversation.id}
          podeConvidar={podeConvidar}
          atendenteId={user.id}
          modo="presenca"
        />
        {podeAssumir && (
          <Button
            size="sm"
            variant="default"
            // No celular, a ação que a pessoa veio fazer abre a fileira — sem
            // rolar para achá-la. Só CSS: a ordem do DOM não muda.
            className="max-md:order-first"
            data-testid="acao-assumir"
            disabled={claim.isPending}
            // O rótulo NÃO muda (é contrato: `inbox-header-nao-trava` e o
            // dicionário de espanhol o citam). O que faltava era a consequência
            // dita: desde a 0173 assumir também para o atendimento automático, e
            // um botão que muda duas coisas precisa anunciar as duas.
            title={t("Você passa a responder esta conversa e o atendimento automático para aqui.")}
            onClick={() =>
              claim.mutate({
                conversation_id: conversation.id,
                expected_assignee: conversation.assigned_to_user_id,
              })
            }
          >
            {t("Assumir")}
          </Button>
        )}
        {isMineAssigned && (
          <Button
            size="sm"
            variant="outline"
            className="max-md:order-first"
            data-testid="acao-liberar"
            disabled={release.isPending}
            // GUIA CURTA (#3003): o rótulo diz o GESTO, não a consequência. "Liberar"
            // não conta para onde a conversa vai — de volta ao automático ou para a
            // fila (ver `liberarDevolveAoAutomatico`) — e essa é exatamente a
            // pergunta "o que acontece se eu clicar aqui?".
            //
            // A guia vai em `title`, e NÃO em `aria-label`: o `aria-label`
            // SUBSTITUI o nome acessível, e `getByRole("button", { name: "Liberar" })`
            // de `inbox-header-nao-trava` e de `ConversationHeader.test` deixaria de
            // achar o botão. É o mesmo caminho já usado por "Assumir", "Devolver ao
            // automático" e "Pausar o automático" neste arquivo.
            title={
              liberarDevolveAoAutomatico
                ? t("Solta esta conversa: ela fica sem dono e o atendimento automático volta a responder.")
                : t("Solta esta conversa: ela fica sem dono e volta para a fila, esperando um atendente.")
            }
            onClick={() => release.mutate({ conversation_id: conversation.id })}
          >
            {t("Liberar")}
          </Button>
        )}
        {/* O INTERRUPTOR. Um botão, dois rótulos, um slot.
            Fica ANTES de transferir/fechar porque é a ação que a pessoa procura
            quando terminou o que tinha para fazer aqui.

            Dois botões lado a lado foi medido e recusado: a barra de ações já
            estourou a caixa útil de 392px em 1280px uma vez (ver o comentário no
            topo do JSX), e um botão a mais custa ~85px — o cabeçalho ganharia uma
            segunda fileira justo na largura mais apertada. Os dois estados são
            mutuamente exclusivos, então nunca precisam existir juntos.

            O `data-testid` do lado de VOLTA é o mesmo de antes: `escalacao-ciclo`
            o clica, e rótulo/testid visível é contrato. */}
        {podeDevolver && (
          <Button
            size="sm"
            variant="outline"
            disabled={retomar.isPending}
            data-testid="devolver-ao-automatico"
            // O ALCANCE DA VOLTA NÃO É SEMPRE O MESMO, e a tela precisa dizer qual é.
            //
            // `devolverAtendimentoAoAgente` limpa `contacts.force_human`, que é do
            // CLIENTE e não desta conversa: quando foi ela que travou, o clique
            // religa o automático para TODAS as conversas daquela pessoa. Um botão
            // que às vezes faz mais do que o nome promete precisa dizer quando.
            title={
              motivo === "contato_travado"
                ? t("Religa o atendimento automático para este cliente — vale para todas as conversas dele.")
                : t("Devolve esta conversa ao atendimento automático.")
            }
            onClick={() => retomar.mutate({ conversation_id: conversation.id })}
          >
            {retomar.isPending ? t("Devolvendo...") : t("Devolver ao automático")}
          </Button>
        )}
        {!encerrada && (
          <Button
            size="sm"
            variant="outline"
            data-testid="acao-transferir"
            // GUIA CURTA (#3003): "Transferir" não diz PARA QUEM. A aba Número do
            // diálogo é a outra metade da resposta — mas ela aparece quando a org
            // tem outro número com telefone, inclusive fora do ar (com a opção
            // desabilitada; `outrosNumerosDoContato`), e ela não move esta
            // conversa: continua o atendimento do contato por lá.
            title={t("Passa esta conversa para outro atendente. Se houver outro número conectado, dá para continuar o atendimento por ele.")}
            onClick={() => setReassignOpen(true)}
          >
            {t("Transferir")}
          </Button>
        )}
        {!encerrada && (
          <Button
            size="sm"
            variant="outline"
            data-testid="acao-fechar"
            disabled={close.isPending}
            // GUIA CURTA (#3003): "Fechar" sozinho não distingue encerrar de
            // arquivar — que é o clique errado relatado na issue. A guia diz o que
            // muda e o que continua disponível depois (reabrir).
            title={t("Encerra o atendimento. Depois você pode reabrir a conversa.")}
            onClick={() => {
              // Padrão marcado a cada abertura: a escolha é DESTA conversa.
              setDevolverAoFechar(true);
              setConfirmFecharOpen(true);
            }}
          >
            {t("Fechar")}
          </Button>
        )}
        {encerrada && <Button size="sm" variant="outline" data-testid="acao-reabrir" disabled={reopen.isPending}
          // GUIA CURTA (#3003): o par de "Fechar" precisa da mesma clareza — o
          // rótulo não diz que a conversa volta a ser atendida.
          title={t("Reabre o atendimento desta conversa, que estava encerrada.")}
          onClick={() => reopen.mutate({ conversation_id: conversation.id, expected_revision: conversation.service_revision })}>
          {t("Reabrir")}
        </Button>}
        {/* ─── O MENU ⋯ (#3003): o que NÃO é do dia a dia ──────────────────────
            A issue relata a confusão de quem está começando: a barra oferecia
            doze controles ao mesmo tempo e nenhum deles respondia "o que
            acontece se eu clicar aqui?". Agora a barra guarda as ações de todo
            atendimento — responder (assumir/devolver), transferir e encerrar —
            e o resto mora aqui, com ícone, nome e a MESMA guia de uma linha
            que os botões visíveis têm.

            O QUE ESTE MENU NÃO PODE FAZER: esconder ação. Cada item mantém a
            condição que já o governava (o `podePausar`, o `status !==
            "archived"`, o gate de `podeMarcarPessoal`, o `null` que o
            `VideoCallButton` devolve sem servidor) — nada aqui é recalculado
            pela barra, senão a regra viveria em dois lugares e divergiria na
            primeira mudança. `tests/unit/inbox-barra-enxuta-3003.test.tsx`
            falha se uma ação sair da tela sem estar aqui, e falha de novo se
            sair daqui sem estar na tela.

            O nome acessível é "Mais ações", o mesmo da barra de ações em lote.
            No celular o gatilho tem 44px (`w-11`, como a busca ao lado); o
            `lg:w-8` só vale onde há ponteiro fino. */}
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button
              size="sm"
              variant="ghost"
              className="w-11 px-0 lg:w-8"
              aria-label={t("Mais ações")}
              title={t("Mais ações desta conversa")}
              data-testid="menu-de-acoes"
            >
              <DotsThree size={16} weight="bold" aria-hidden />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" className="w-64" data-testid="menu-de-acoes-conteudo">
            {/* Por onde entrar em contato com a pessoa. Era `xl:hidden` na barra
                porque a partir de 1280px o painel de CRM tem um "Ver contato"
                próprio; dentro do menu a duplicata não custa fileira nenhuma,
                então o item aparece em qualquer largura. */}
            {c?.id && (
              <DropdownMenuItem asChild>
                <Link
                  href={`/app/contacts/${c.id}`}
                  className="flex w-full items-center gap-2"
                  data-testid="acao-ver-contato"
                  // GUIA CURTA (#3003): o rótulo promete "contato" e entrega uma
                  // página inteira; a guia diz qual.
                  title={t("Abre a ficha completa deste contato.")}
                >
                  <UserCircle size={16} aria-hidden />
                  {t("Ver contato")}
                </Link>
              </DropdownMenuItem>
            )}
            {/* A chamada usa o telefone da ficha, mesmo quando o contato chegou
                por outro canal. Grupos não representam uma pessoa para ligar. */}
            {!conversation.is_group && c?.id && (
              <DialButton contactId={c.id} hasPhone={!!c.phone_number} comoItemDeMenu />
            )}
            {podeReservar && <ItemReservarServico onSelect={() => setReservaAberta(true)} />}
            {/* Vídeo: sem JITSI_SERVER_URL o componente devolve `null` e o item
                não existe — o menu não promete o que a instalação não tem. */}
            <VideoCallButton
              conversationId={conversation.id}
              provider={conversation.channel_sessions?.provider ?? null}
              lastInboundAt={conversation.last_inbound_at}
              bloqueio={motivoDoContato(c, t)}
              encerrada={status === "closed"}
              comoItemDeMenu
            />
            {/* ATENDIMENTO CONJUNTO (#2542): o convite é ação, a presença é
                informação — a presença ficou na barra (`modo="presenca"`) e o
                convite vira item daqui. O diálogo NÃO mora dentro do menu: ver
                `DialogoDeConvite`, montado no fim deste componente. */}
            {podeConvidar && (
              <DropdownMenuItem
                data-testid="convidar-participante"
                title={t("Convida um colega para atender junto — o responsável pela conversa não muda.")}
                onSelect={() => setConviteAberto(true)}
              >
                <UsersThree size={16} aria-hidden className="mr-2" />
                {t("Convidar")}
              </DropdownMenuItem>
            )}
            {/* ─── A FILA (#3876) ──────────────────────────────────────────
                Uma ação que a conversa já tinha e a tela não oferecia: o
                `PUT /conversations/[id]/sector` (9062) estava sem consumidor.
                Fica no ⋯ porque classificar é correção de rota, não é do dia a
                dia. `ItemMoverParaFila` devolve `null` quando a rota recusaria
                (viewer, módulo desligado, conversa encerrada).
                O export em PDF (#1982) NÃO entra aqui: o PR #4207, aberto
                antes, é quem entrega essa tela — duas entregas da mesma fatia
                viram dois itens no menu.
                O separador de cima vem de dentro do item: solto aqui, ele
                dobraria com o de baixo quando o item não aparece. */}
            <ItemMoverParaFila encerrada={encerrada} onAbrir={() => setFilaAberta(true)} />
            <DropdownMenuSeparator />
            {podePausar && (
              <DropdownMenuItem
                data-testid="pausar-o-automatico"
                disabled={pausar.isPending}
                // `podePausar` já exige dono != null, então este botão NUNCA aparece
                // sem dono — prometer "você assume" aqui seria prometer o que a rota
                // não faz: com dono, ela só cala, nunca rouba a conversa de quem a tem.
                title={t("O atendimento automático para nesta conversa. O dono não muda.")}
                onSelect={() => pausar.mutate({ conversation_id: conversation.id })}
              >
                <Pause size={16} aria-hidden className="mr-2" />
                {pausar.isPending ? t("Pausando...") : t("Pausar o automático")}
              </DropdownMenuItem>
            )}
            {/* #3967 — "IA com o dono": um interruptor, um rótulo, dois estados.
                A conversa ganha a IA de APOIO sem perder o dono: nada de `release`,
                nada de rodízio, nada de fila. É o par do "Pausar o automático"
                (que cala sem devolver) e existe porque, com dono humano, a IA
                ficava calada por definição — e o único caminho de volta soltava
                a conversa de quem a estava acompanhando. Clicar fecha o menu; o
                estado ligado fica à vista no selo abaixo da barra. */}
            {podeModoIaComDono && (
              <DropdownMenuItem
                role="menuitemcheckbox"
                aria-checked={modoLigado}
                disabled={iaComDono.isPending}
                data-testid="ia-com-o-dono"
                title={
                  modoLigado
                    ? t("Desliga a IA nesta conversa. Ela volta a ficar calada e continua sua.")
                    : t("A IA responde esta conversa sem tirar o dono de você. Se ela passar para uma pessoa, a conversa continua com o mesmo atendente.")
                }
                onSelect={() =>
                  iaComDono.mutate({ conversation_id: conversation.id, ligado: !modoLigado })
                }
              >
                <Robot size={16} aria-hidden className="mr-2" />
                {iaComDono.isPending ? t("Salvando...") : t("IA com o dono")}
                {modoLigado && <Check size={16} aria-hidden className="ml-auto" />}
              </DropdownMenuItem>
            )}
            {!encerrada && (
              <SnoozeButton
                conversationId={conversation.id}
                snoozeUntil={conversation.snooze_until ?? null}
                modo="submenu"
              />
            )}
            <DropdownMenuSeparator />
            {/* ARQUIVAR (#923): tira da frente sem destruir.
                A conversa já arquivada não mostra o item — arquivar duas vezes
                não é um gesto que exista, e o item só reapareceria como um
                clique que não muda nada. Fechada E resolvida mostram: são
                exatamente as que se quer mandar para o arquivo depois de
                encerradas, e é o caminho que faz a aba "Arquivadas" deixar de
                ser uma pasta morta.
                A permissão é a mesma de fechar (a rota `/conversations/[id]` é
                `requireSupportWrite`): quem pode encerrar, pode arquivar. */}
            {status !== "archived" && (
              <DropdownMenuItem
                data-testid="acao-arquivar"
                disabled={arquivar.isPending}
                // GUIA CURTA (#3003): "Arquivar" parece "guardar e pronto", e não é —
                // numa conversa ainda aberta ele ENCERRA o atendimento (o diálogo de
                // confirmação diz isso com todas as letras, e a guia diz antes dele).
                // Ela NÃO some do inbox: sai da Fila e de Minhas, e segue em Todas e
                // em Arquivadas (`tabToFilter` em InboxLayout).
                title={t("Guarda a conversa na aba Arquivadas. Se ainda estiver aberta, o atendimento é encerrado.")}
                onSelect={() => setConfirmArquivarOpen(true)}
              >
                <Archive size={16} aria-hidden className="mr-2" />
                {arquivar.isPending ? t("Arquivando...") : t("Arquivar")}
              </DropdownMenuItem>
            )}
            {/* PESSOAL (spec 21, etapa 15): marcar tira a conversa do inbox na
                hora; desmarcar relista. O `data-testid` é contrato do e2e da
                spec (`contato-pessoal-sai-da-operacao.spec.ts`). */}
            {podeMarcarPessoal && c?.id && !ehPessoal && (
              <DropdownMenuItem
                data-testid="marcar-pessoal"
                disabled={marcarPessoal.isPending}
                title={t("Tira este contato da operação: a conversa sai do inbox.")}
                onSelect={() => setConfirmPessoalOpen(true)}
              >
                <EyeSlash size={16} aria-hidden className="mr-2" />
                {t("Marcar como pessoal")}
              </DropdownMenuItem>
            )}
            {podeMarcarPessoal && c?.id && ehPessoal && (
              <DropdownMenuItem
                data-testid="desmarcar-pessoal"
                disabled={desmarcarPessoal.isPending}
                title={t("Devolve este contato à operação: a conversa volta ao inbox.")}
                onSelect={() => desmarcarPessoal.mutate()}
              >
                <EyeSlash size={16} aria-hidden className="mr-2" />
                {desmarcarPessoal.isPending ? t("Desmarcando...") : t("Desmarcar pessoal")}
              </DropdownMenuItem>
            )}
            {/* EXPORTAR CONVERSA (#1982, F1) — o PDF do histórico desta conversa.
                A rota já existia (`app/api/v1/conversations/[id]/export`,
                `?formato=pdf`) e nenhuma tela a chamava: o operador não tinha
                por onde baixar. Aqui é ÂNCORA de download, não `fetch`: o
                browser faz o GET com o cookie da sessão e grava o arquivo com o
                nome que o `Content-Disposition` da rota manda (`no-store`).
                É o mesmo gesto do "Exportar CSV" da auditoria.

                O QUE ENTRA: o histórico legível, datado e em ordem, para o
                operador arquivar no caso. O QUE NÃO ENTRA: nota interna — o
                PDF é do histórico do cliente, e a rota não lê a tabela de
                notas. O `title` diz isso a quem está começando; o gate é o da
                rota (`podeExportar`), então o item não aparece para `viewer`. */}
            {podeExportar && (
              <>
                <DropdownMenuSeparator />
                <DropdownMenuItem asChild>
                  <a
                    href={`/api/v1/conversations/${conversation.id}/export?formato=pdf`}
                    download
                    data-testid="exportar-conversa"
                    className="flex w-full items-center gap-2"
                    title={t("Baixa o histórico desta conversa em PDF, datado e em ordem, para arquivar. Notas internas não entram.")}
                  >
                    <DownloadSimple size={16} aria-hidden />
                    {t("Exportar conversa")}
                  </a>
                </DropdownMenuItem>
              </>
            )}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
        {/* O aviso pertence à operação automática. Abaixo da barra ele não
            alarga a ficha do contato nem muda a posição dos botões.
            Sem esta marca, a conversa em que o robô está calado tem exatamente
            a mesma cara de uma conversa normal. O testid é contrato:
            `escalacao-ciclo.spec.ts` o clica. */}
        {motivo !== null && (
          <Badge variant="outline" className="h-4 w-fit max-w-full truncate px-1.5 text-[10px]"
            title={t(ROTULO_DO_MOTIVO[motivo])} data-testid="badge-atendimento-humano">
            {t(ROTULO_DO_MOTIVO[motivo])}
          </Badge>
        )}
        {/* O modo ligado é informação, e informação dentro de um menu deixa de ser
            lida: sem este selo a conversa em que a IA responde ao lado do dono
            tem a cara de qualquer outra. Com o automático calado por outro
            motivo, quem fala é o selo de cima. */}
        {podeModoIaComDono && modoLigado && motivo === null && (
          <Badge variant="outline" className="h-4 w-fit max-w-full truncate px-1.5 text-[10px]"
            data-testid="badge-ia-com-o-dono">
            {t("IA com o dono")}
          </Badge>
        )}
      </div>
      <ReassignDialog
        conversationId={conversation.id}
        open={reassignOpen}
        onOpenChange={setReassignOpen}
        numero={
          onAbrirConversa && c?.id
            ? {
                contactId: c.id,
                contactPhone: c.phone_number ?? null,
                channelSessionId: conversation.channel_session_id,
                onAbrirConversa,
              }
            : undefined
        }
      />
      {/* O diálogo do convite, FORA do menu ⋯ (ver `DialogoDeConvite`): ele é
          modal, e modal dentro de modal esconde a página de quem usa leitor de
          tela. O item do menu só levanta `conviteAberto`. */}
      <DialogoDeConvite
        conversationId={conversation.id}
        aberto={conviteAberto}
        onAbertoChange={setConviteAberto}
      />
      {/* O diálogo da fila, FORA do menu ⋯ — o mesmo desenho do convite acima —
          e montado só quando abre. Aberto é o único estado em que ele existe, e
          um diálogo fechado que consultasse setores em toda conversa seria uma
          requisição por cabeçalho montado para quem nunca tocou no ⋯. */}
      {filaAberta && (
        <DialogoMoverParaFila
          conversationId={conversation.id}
          filaAtual={conversation.sector_id ?? null}
          aberto
          onAbertoChange={setFilaAberta}
        />
      )}
      {podeReservar && c?.id && activeOrg && (
        <ReservarServicoNaConversa
          key={`${activeOrg.orgId}:${conversation.id}`}
          contactId={c.id}
          conversationId={conversation.id}
          contactName={displayName}
          aberto={reservaAberta}
          onAbertoChange={setReservaAberta}
        />
      )}
      <AlertDialog open={confirmFecharOpen} onOpenChange={setConfirmFecharOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>{t("Fechar esta conversa?")}</AlertDialogTitle>
            <AlertDialogDescription>
              {t("O atendimento é encerrado. Se o cliente escrever de novo, você pode reabrir.")}
            </AlertDialogDescription>
          </AlertDialogHeader>
          {/* A PERGUNTA QUE FALTAVA (#3313, item 1). A trava de humano fica no
              CONTATO: fechar a conversa não a solta, e a equipe descobria isso
              dias depois, com o cliente sem resposta. Só aparece com a trava
              ligada — sem ela não há o que perguntar, e fechar continua sendo
              só fechar (nenhuma chave nova viaja no corpo).

              Rádio nativo, e não um `Switch`: as duas opções são excludentes e
              o estado atual PRECISA estar visível na abertura, que é justamente
              o que o item pede ("padrão marcado"). */}
          {contatoTravadoComEquipe ? (
            <div className="space-y-2" data-testid="escolha-ao-fechar">
              <p className="text-sm font-medium">
                {t("Este contato está com a equipe. Ao fechar, o que acontece?")}
              </p>
              <label className="flex cursor-pointer items-start gap-3">
                <input
                  type="radio"
                  name="devolver-ao-fechar"
                  data-testid="fechar-devolver-ao-automatico"
                  checked={devolverAoFechar}
                  onChange={() => setDevolverAoFechar(true)}
                  className="mt-1 h-4 w-4 shrink-0 accent-primary"
                />
                <span className="space-y-1">
                  <span className="block text-sm font-medium">
                    {t("Devolver ao automático ao fechar")}
                  </span>
                  <span className="block text-xs text-muted-foreground">
                    {t(
                      "A IA volta a atender este cliente na próxima mensagem dele. Se o contato estiver marcado como sempre atendimento humano, ele continua com a equipe.",
                    )}
                  </span>
                </span>
              </label>
              <label className="flex cursor-pointer items-start gap-3">
                <input
                  type="radio"
                  name="devolver-ao-fechar"
                  data-testid="fechar-manter-com-a-equipe"
                  checked={!devolverAoFechar}
                  onChange={() => setDevolverAoFechar(false)}
                  className="mt-1 h-4 w-4 shrink-0 accent-primary"
                />
                <span className="space-y-1">
                  <span className="block text-sm font-medium">{t("Manter com a equipe")}</span>
                  <span className="block text-xs text-muted-foreground">
                    {t(
                      "A conversa fecha e a IA continua calada para este cliente: a próxima mensagem dele espera uma pessoa.",
                    )}
                  </span>
                </span>
              </label>
            </div>
          ) : null}
          <AlertDialogFooter>
            <AlertDialogCancel>{t("Cancelar")}</AlertDialogCancel>
            <AlertDialogAction
              onClick={() => {
                // A chave só entra no corpo quando a escolha existe: sem contato
                // travado a tela não pergunta nada, e o fechamento é o de sempre.
                const escolha: {
                  conversation_id: string;
                  expected_revision?: number;
                  devolver_ao_automatico?: boolean;
                } = {
                  conversation_id: conversation.id,
                  expected_revision: conversation.service_revision,
                };
                if (contatoTravadoComEquipe) {
                  escolha.devolver_ao_automatico = devolverAoFechar;
                }
                close.mutate(escolha);
              }}
            >
              {t("Fechar")}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      {/* A confirmação precisa dizer o que ACONTECE, e o que acontece depende
          do estado. `fn_conversation_set_status` trata `archived` como
          terminal: encerra o atendimento (grava `service_closed_at`,
          incrementa a revisão) e, com isso, desfaz a pausa do automático. Um
          atendente que leia "arquivar = tirar da vista, volto depois"
          encerraria o atendimento sem saber — e o robô voltaria a responder
          no próximo "oi" do cliente. Por isso a descrição só aparece quando
          `!encerrada`: quando já está encerrada, arquivar não muda o
          atendimento, só o lugar onde a conversa mora. */}
      <AlertDialog open={confirmArquivarOpen} onOpenChange={setConfirmArquivarOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>{t("Arquivar esta conversa?")}</AlertDialogTitle>
            {!encerrada && (
              <AlertDialogDescription>
                {t(
                  "Arquivar encerra este atendimento e guarda a conversa no histórico. Se o cliente escrever de novo, ela volta.",
                )}
              </AlertDialogDescription>
            )}
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>{t("Cancelar")}</AlertDialogCancel>
            <AlertDialogAction
              onClick={() =>
                arquivar.mutate({
                  conversation_id: conversation.id,
                  expected_revision: conversation.service_revision,
                })
              }
            >
              {t("Arquivar")}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      {/* A confirmação diz o que ACONTECE ao marcar: a conversa fecha e sai do
          atendente, e o contato fica inutilizado para envio — sem apagar nada.
          Desmarcar é direto no botão porque só relista o que já está lá. */}
      <AlertDialog open={confirmPessoalOpen} onOpenChange={setConfirmPessoalOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>{t("Marcar este contato como pessoal?")}</AlertDialogTitle>
            <AlertDialogDescription>
              {t(
                "A conversa sai do inbox e o contato fica fora da operação: sem IA, sem follow-up, sem campanha e sem envio. O histórico continua no banco e volta à vista ao desmarcar; follow-ups e campanhas cancelados não voltam.",
              )}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>{t("Cancelar")}</AlertDialogCancel>
            <AlertDialogAction onClick={() => marcarPessoal.mutate()}>
              {t("Marcar como pessoal")}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  );
}
