"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import {
  Bell,
  BellRing,
  FileText,
  MessageCircle,
  Minus,
  Package,
  Paperclip,
  Send,
  ShoppingBag,
  Smile,
  X,
} from "lucide-react";
import { useCart } from "@/context/CartContext";
import { getReadableTextColor } from "@/lib/color-contrast";
import { getSupabaseClient } from "@/lib/supabase";
import { visitaQueNaoConta } from "@/lib/no-editor-da-loja";

/*
  Chat da loja, para quem ainda nao e cliente.

  A duvida na hora da compra ("cabe meu logo nisso?", "entrega ate sexta?") hoje
  vira WhatsApp — que tira a pessoa da loja e leva a conversa para fora do
  sistema. Aqui ela fala sem sair da pagina e sem criar conta: pede-se o nome, e
  so.

  Tres cuidados que a tela precisa ter:

  - o balao nao pode cair em cima do botao do WhatsApp da loja, entao ele mede
    o que ja esta flutuando ali e pousa ao lado, na mesma altura;
  - online e offline mudam na hora, sem recarregar: a loja voltou, o cliente ve;
  - quem quiser pode pedir aviso no aparelho e voltar direto na conversa quando
    a resposta chegar.
*/

type CartaoDaMensagem = {
  tipo: "produto" | "cupom";
  titulo?: string | null;
  subtitulo?: string | null;
  imagem?: string | null;
  preco?: number | null;
  link?: string | null;
  codigo?: string | null;
};

type MensagemDaLoja = {
  id: string;
  minha: boolean;
  quem: string;
  texto: string | null;
  cartao?: CartaoDaMensagem | null;
  anexoUrl?: string | null;
  anexoNome?: string | null;
  anexoTipo?: string | null;
  apagada: boolean;
  em: string;
};

const TAMANHO_MAXIMO = 8 * 1024 * 1024;

/* Os emojis que aparecem de verdade numa conversa de loja. Um punhado resolve —
   um seletor completo pesaria mais do que o balão inteiro. */
const EMOJIS = [
  "😀", "😅", "😊", "😍", "🤔", "😉", "🙏", "👍",
  "👏", "🤝", "💪", "🎉", "🔥", "✨", "❤️", "😂",
  "😢", "😱", "🙌", "👌", "💰", "💳", "🛒", "🎁",
  "📦", "🚚", "⏰", "✅", "❌", "📸", "🖨️", "🎨",
];

/*
  O balão do convite.

  O ícone de contorno fininho que vinha da biblioteca é o mesmo de qualquer
  aplicativo, e num botão colorido ele some: linha de 1,5 px sobre cor forte
  fica lavada. Este é maciço, com a barriga arredondada e a pontinha embaixo, e
  os três pontinhos são furos — mostram a cor do próprio botão, então ele
  funciona em qualquer cor de loja sem ajuste nenhum.
*/
function BalaoDeConversa({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" className={className} aria-hidden="true" focusable="false">
      <path
        fill="currentColor"
        fillRule="evenodd"
        clipRule="evenodd"
        d="M6.6 3.4h10.8A4.6 4.6 0 0 1 22 8v4.6a4.6 4.6 0 0 1-4.6 4.6h-5.2l-4.06 3.02a.95.95 0 0 1-1.51-.9l.44-2.4A4.6 4.6 0 0 1 2 12.6V8a4.6 4.6 0 0 1 4.6-4.6Zm1.6 5.75a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3Zm3.8 0a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3Zm3.8 0a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3Z"
      />
    </svg>
  );
}

const emReais = (valor?: number | null) =>
  new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(Number(valor || 0));

/*
  O produto (ou o cupom) que a loja mandou.

  Link cru ninguem clica: some no meio da conversa e nao diz o que tem do outro
  lado. Com foto, nome e preco, a pessoa decide sem sair do chat — e o cupom
  aparece com o codigo grande, para copiar sem errar letra.
*/
function CartaoDaLoja({ cartao, minha }: { cartao: CartaoDaMensagem; minha: boolean }) {
  const fundo = minha ? "bg-white/15" : "bg-[#f6f5f9]";

  if (cartao.tipo === "cupom") {
    return (
      <div className={`mb-1.5 rounded-xl ${fundo} p-2.5`}>
        <div className="text-[10px] font-bold uppercase tracking-wider opacity-70">Cupom de desconto</div>
        <div className="mt-0.5 text-[17px] font-extrabold tracking-tight">{cartao.codigo || cartao.titulo}</div>
        {cartao.subtitulo ? <div className="text-[11.5px] opacity-80">{cartao.subtitulo}</div> : null}
      </div>
    );
  }

  return (
    <a
      href={cartao.link || "#"}
      className={`mb-1.5 flex items-center gap-2.5 rounded-xl ${fundo} p-2 no-underline`}
      style={{ color: "inherit" }}
    >
      {cartao.imagem ? (
        // eslint-disable-next-line @next/next/no-img-element
        <img
          src={cartao.imagem}
          alt={cartao.titulo || "Produto"}
          className="h-14 w-14 shrink-0 rounded-lg object-cover"
        />
      ) : null}
      <span className="min-w-0">
        <span className="block truncate text-[12.5px] font-bold leading-tight">{cartao.titulo}</span>
        {cartao.preco ? (
          <span className="block text-[14px] font-extrabold tracking-tight">{emReais(cartao.preco)}</span>
        ) : (
          <span className="block text-[11.5px] opacity-80">Ver na loja</span>
        )}
        <span className="block text-[10.5px] underline opacity-80">abrir o produto</span>
      </span>
    </a>
  );
}

type EstadoDoAviso = "verificando" | "indisponivel" | "precisa_instalar" | "desligado" | "ligado" | "negado";

const GUARDA_DA_CHAVE = "ktl:visitante";
const GUARDA_DO_NOME = "ktl:visitante-nome";

function chaveDoVisitante() {
  try {
    const guardada = window.localStorage.getItem(GUARDA_DA_CHAVE);
    if (guardada) return guardada;
    const nova = crypto.randomUUID();
    window.localStorage.setItem(GUARDA_DA_CHAVE, nova);
    return nova;
  } catch {
    /* Navegador sem armazenamento: o cookie do servidor segura a conversa. */
    return crypto.randomUUID();
  }
}

function base64ParaBytes(base64: string) {
  const preenchido = (base64 + "=".repeat((4 - (base64.length % 4)) % 4)).replace(/-/g, "+").replace(/_/g, "/");
  const cru = atob(preenchido);
  return Uint8Array.from([...cru].map((caractere) => caractere.charCodeAt(0)));
}

/* A foto do que a pessoa esta olhando. E o que faz a lista do Chats parecer a
   vitrine da loja, e nao um relatorio de acessos. */
function imagemNaTela() {
  const marcada = document.querySelector<HTMLMetaElement>('meta[property="og:image"]')?.content;
  if (marcada) return marcada;
  const fotos = Array.from(document.querySelectorAll<HTMLImageElement>("img"));
  const grande = fotos.find((foto) => foto.clientWidth > 150 && foto.clientHeight > 150);
  return grande?.currentSrc || grande?.src || null;
}

/* O termo procurado, seja qual for o nome do parametro na loja. */
function buscaNaTela() {
  const parametros = new URLSearchParams(window.location.search);
  return parametros.get("q") || parametros.get("busca") || parametros.get("search") || "";
}

/* O preco que a pessoa esta vendo, lido da propria tela: e o numero que
   importa na hora de oferecer algo. */
function precoNaTela() {
  const texto = document.body.innerText.match(/R\$\s?([\d.]+,\d{2})/);
  if (!texto) return null;
  const numero = Number(texto[1].replace(/\./g, "").replace(",", "."));
  return Number.isFinite(numero) ? numero : null;
}

function hora(iso: string) {
  return new Date(iso).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
}

/* Quando a conversa começou. Sem isso, uma resposta de ontem parece de agora. */
function diaDaConversa(iso: string) {
  const data = new Date(iso);
  const hoje = new Date();
  const zerar = (valor: Date) => new Date(valor.getFullYear(), valor.getMonth(), valor.getDate()).getTime();
  const diferenca = Math.round((zerar(hoje) - zerar(data)) / 86400000);
  const quando = diferenca === 0 ? "Hoje" : diferenca === 1 ? "Ontem" : data.toLocaleDateString("pt-BR");
  return `${quando}, ${hora(iso)}`;
}

/*
  O toque de aviso do visitante.

  Sem som, a loja falava primeiro e a pessoa não percebia: o balão ganhava uma
  bolinha vermelha num canto da tela que ela não estava olhando. Duas notas
  curtas, feitas na hora — nenhum arquivo para baixar na loja de ninguém.

  O navegador só deixa tocar depois de a pessoa interagir com a página. Por isso
  o áudio é destravado no primeiro clique, toque ou rolagem — que numa loja
  acontece bem antes de qualquer mensagem.
*/
let audio: AudioContext | null = null;

function abrirAudio() {
  try {
    const Fabrica =
      window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
    if (!Fabrica) return null;
    if (!audio) audio = new Fabrica();
    if (audio.state === "suspended") void audio.resume();
    return audio;
  } catch {
    return null;
  }
}

function tocarAviso() {
  const contexto = abrirAudio();
  if (!contexto) return;

  try {
    const agora = contexto.currentTime;
    [784, 1046.5].forEach((nota, passo) => {
      const inicio = agora + passo * 0.13;
      const oscilador = contexto.createOscillator();
      const volume = contexto.createGain();
      oscilador.type = "sine";
      oscilador.frequency.value = nota;
      volume.gain.setValueAtTime(0.0001, inicio);
      volume.gain.exponentialRampToValueAtTime(0.16, inicio + 0.02);
      volume.gain.exponentialRampToValueAtTime(0.0001, inicio + 0.24);
      oscilador.connect(volume);
      volume.connect(contexto.destination);
      oscilador.start(inicio);
      oscilador.stop(inicio + 0.26);
    });
  } catch {
    /* Sem som, resta o balão abrindo — que já é o aviso principal. */
  }
}

export default function ChatDaLoja({ slug, primaryColor }: { slug?: string; primaryColor?: string }) {
  const cor = primaryColor || "#4F46E5";
  const corDoTexto = getReadableTextColor(cor);

  const [ligado, setLigado] = useState(false);
  const [online, setOnline] = useState(false);
  const [aberto, setAberto] = useState(false);
  const [chave, setChave] = useState<string | null>(null);
  const [threadId, setThreadId] = useState<string | null>(null);
  const [mensagens, setMensagens] = useState<MensagemDaLoja[]>([]);
  const [nome, setNome] = useState("");
  const [contato, setContato] = useState("");
  const [texto, setTexto] = useState("");
  const [enviando, setEnviando] = useState(false);
  const [erro, setErro] = useState<string | null>(null);
  const [naoLidas, setNaoLidas] = useState(0);
  const [aviso, setAviso] = useState<EstadoDoAviso>("verificando");
  const [ligandoAviso, setLigandoAviso] = useState(false);
  /* Onde o vizinho (o botão do WhatsApp) está: o balão pousa ao lado dele. */
  const [vizinho, setVizinho] = useState<{ bottom: number; right: number } | null>(null);

  const [arquivo, setArquivo] = useState<File | null>(null);
  const [lojaDigitando, setLojaDigitando] = useState(false);
  const [emojisAbertos, setEmojisAbertos] = useState(false);
  /* No celular a conversa é a tela inteira: um cartãozinho no meio do site é
     desconfortável de ler e pior ainda de escrever com o teclado aberto. */
  const [telaPequena, setTelaPequena] = useState(false);

  const fim = useRef<HTMLDivElement | null>(null);
  const campoDeArquivo = useRef<HTMLInputElement | null>(null);
  const campoDeTexto = useRef<HTMLTextAreaElement | null>(null);
  /* Quem fechou no X não quer o balão abrindo sozinho de novo nesta visita. */
  const dispensadoRef = useRef(false);
  const canalDoFio = useRef<ReturnType<ReturnType<typeof getSupabaseClient>["channel"]> | null>(null);
  const ultimoAvisoDeDigitacao = useRef(0);
  /* A última fala da loja que esta aba já conhecia: é o que separa "chegou
     agora" de "já estava aí quando abri". */
  const ultimaDaLojaRef = useRef<string | null>(null);
  const primeiraCargaRef = useRef(true);
  const buscarRef = useRef<((rolar: boolean) => Promise<void>) | null>(null);
  const ultimaVistaRef = useRef<string | null>(null);
  const abertoRef = useRef(false);
  abertoRef.current = aberto;

  /* O carrinho da loja passa por aqui porque o balao vive dentro dele: o que a
     pessoa ja separou e o sinal mais forte antes do pagamento. */
  const { cartCount, cartTotal } = useCart();
  const carrinhoRef = useRef({ itens: 0, total: 0 });
  const carrinhoVistoRef = useRef<number | null>(null);
  const contarRef = useRef<((passo?: string) => void) | null>(null);

  const chavePublica = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;

  useEffect(() => {
    if (!slug) return;
    setChave(chaveDoVisitante());
    try {
      setNome(window.localStorage.getItem(GUARDA_DO_NOME) || "");
    } catch {
      /* sem nome guardado, pergunta de novo */
    }
    /* Chegou pelo aviso do celular: a conversa abre sozinha. */
    const parametros = new URLSearchParams(window.location.search);
    if (parametros.get("chat") === "1") setAberto(true);
  }, [slug]);

  /*
    Onde o balão pousa.

    A loja pode ter o botão do WhatsApp no mesmo canto. Em vez de chutar uma
    posição, o balão mede o que já está flutuando ali e se coloca **ao lado**,
    na mesma altura — empilhar um sobre o outro dava certo na tela grande e
    virava sobreposição no celular, onde a barra de baixo sobe tudo.
  */
  useEffect(() => {
    const medir = () => {
      /* A loja tem varios links de WhatsApp (rodape, ficha do produto). O que
         importa e o que flutua no mesmo canto — os outros nao atrapalham. */
      const candidatos = Array.from(
        document.querySelectorAll<HTMLElement>('a[href*="wa.me"], a[href*="api.whatsapp.com"]')
      );
      const flutuante = candidatos.find((elemento) => {
        if (getComputedStyle(elemento).position !== "fixed") return false;
        const caixa = elemento.getBoundingClientRect();
        const distancia = window.innerHeight - caixa.bottom;
        /*
          Tem que estar visível, e não só "para baixo".

          A conta antiga aceitava distância negativa — ou seja, um botão que
          estava abaixo da dobra —, e então o balão era empurrado para fora da
          tela junto com ele. O sintoma era o balão simplesmente sumir.
        */
        const noCantoDeBaixo = distancia >= -8 && distancia < 220;
        const naDireita = caixa.right > window.innerWidth - 220;
        return caixa.height > 24 && noCantoDeBaixo && naDireita;
      });

      if (!flutuante) return setVizinho(null);
      const caixa = flutuante.getBoundingClientRect();
      const deBaixo = Math.max(0, window.innerHeight - caixa.bottom);
      const daDireita = Math.max(0, window.innerWidth - caixa.right);
      /* O botão do WhatsApp tem um halo pulsante que vaza uns 8 px: o vão de 16
         evita que os dois pareçam encostados. */
      setVizinho({ bottom: Math.round(deBaixo), right: Math.round(daDireita + caixa.width + 16) });
    };

    medir();
    const atrasada = window.setTimeout(medir, 1200);
    window.addEventListener("resize", medir);
    return () => {
      window.clearTimeout(atrasada);
      window.removeEventListener("resize", medir);
    };
  }, [ligado]);

  /*
    Sinal de vida e rota.

    De tempos em tempos a aba avisa a loja que a pessoa esta ali e em que
    pagina — e assim que o balcao enxerga o movimento da vitrine. Quando ela
    troca de pagina o sinal sai na hora e com nome ("viu tal produto"), e vira
    um passo da rota: no Chats a mudanca aparece antes de a pessoa rolar a tela.

    A troca de endereco e vista pelo proprio endereco, e nao pelo roteador: a
    loja tem telas que so mudam a busca na URL, e essas o roteador nao anuncia.
  */
  useEffect(() => {
    if (!slug || !chave || !ligado) return;
    /* O lojista no editor ou na prévia dos temas não aparece como movimento da vitrine. */
    if (visitaQueNaoConta()) return;

    let primeiroSinal = true;
    let enderecoAnterior = "";

    const contar = (passo?: string) => {
      const caminho = window.location.pathname;
      const partes = caminho.split("/").filter(Boolean);
      const ehProduto = partes[1] === "p" && Boolean(partes[2]);
      const tituloDaPagina = document.querySelector("h1")?.textContent?.trim() || document.title;

      void fetch("/api/loja/presenca", {
        method: "POST",
        headers: { "content-type": "application/json" },
        keepalive: true,
        body: JSON.stringify({
          slug,
          chave,
          caminho,
          titulo: tituloDaPagina,
          produtoId: ehProduto ? partes[2] : null,
          produtoNome: ehProduto ? tituloDaPagina : null,
          produtoPreco: ehProduto ? precoNaTela() : null,
          produtoImagem: ehProduto ? imagemNaTela() : null,
          busca: buscaNaTela(),
          /* De onde veio so vale na chegada: depois disso o "anterior" e a
             propria loja. */
          origem: primeiroSinal ? document.referrer : null,
          carrinhoItens: carrinhoRef.current.itens,
          carrinhoTotal: carrinhoRef.current.total,
          passo,
        }),
      }).catch(() => undefined);

      primeiroSinal = false;
    };

    contarRef.current = contar;

    const olharEndereco = () => {
      const agora = window.location.pathname + window.location.search;
      if (agora === enderecoAnterior) return;
      const estreando = enderecoAnterior === "";
      enderecoAnterior = agora;
      const partes = window.location.pathname.split("/").filter(Boolean);
      const ehProduto = partes[1] === "p" && Boolean(partes[2]);
      const noCaixa = /\/(checkout|carrinho|finalizar)/.test(window.location.pathname);
      const passo = estreando
        ? "entrou"
        : noCaixa
          ? "checkout"
          : buscaNaTela()
            ? "busca"
            : ehProduto
              ? "produto"
              : "pagina";
      /* O titulo da pagina nova leva um instante para existir no documento. */
      window.setTimeout(() => contar(passo), estreando ? 0 : 350);
    };

    olharEndereco();
    /* Um olhar por segundo no endereco: e comparacao de texto, nao pesa, e pega
       qualquer navegacao — inclusive as que so mudam a busca. */
    const olho = window.setInterval(() => {
      if (document.visibilityState === "visible") olharEndereco();
    }, 1000);

    const relogio = window.setInterval(() => {
      if (document.visibilityState === "visible") contar();
    }, 45000);

    return () => {
      window.clearInterval(olho);
      window.clearInterval(relogio);
      contarRef.current = null;
    };
  }, [slug, chave, ligado]);

  /*
    Carrinho.

    Quem ja separou dois itens e outra conversa: e o sinal mais forte que existe
    antes do pagamento. Como o balao vive dentro do carrinho da loja, basta
    olhar — nao ha script novo nem espiada no armazenamento.
  */
  useEffect(() => {
    carrinhoRef.current = { itens: cartCount, total: cartTotal };
    if (!ligado) return;
    if (carrinhoVistoRef.current === null) {
      carrinhoVistoRef.current = cartCount;
      return;
    }
    if (carrinhoVistoRef.current === cartCount) return;
    carrinhoVistoRef.current = cartCount;
    if (cartCount > 0) contarRef.current?.("carrinho");
  }, [cartCount, cartTotal, ligado]);

  /* Conversa comecada pela loja (saudacao automatica ou puxada por alguem):
     chega por um canal do proprio visitante, porque ainda nao existe fio. */
  useEffect(() => {
    if (!chave || !ligado) return;
    const supabase = getSupabaseClient();
    const canal = supabase.channel(`visitante-${chave}`);
    canal.on("broadcast", { event: "novidade" }, () => void buscarRef.current?.(false));
    canal.subscribe();
    return () => {
      void supabase.removeChannel(canal);
    };
  }, [chave, ligado]);

  const buscar = useCallback(
    async (rolar: boolean) => {
      if (!slug || !chave) return;
      try {
        const resposta = await fetch(
          `/api/loja/chat?slug=${encodeURIComponent(slug)}&chave=${encodeURIComponent(chave)}`,
          { cache: "no-store" }
        );
        const dados = (await resposta.json().catch(() => null)) as
          | {
              ligado?: boolean;
              online?: boolean;
              threadId?: string;
              nome?: string | null;
              mensagens?: MensagemDaLoja[];
            }
          | null;

        setLigado(Boolean(dados?.ligado));
        setOnline(Boolean(dados?.online));
        if (dados?.threadId) setThreadId(dados.threadId);
        /* O nome NÃO vem da conversa do servidor. Uma thread aberta pelo
           painel guarda o nome que a loja digitou lá ("Teste Painel"), e ele
           reaparecia no formulário do visitante. Só vale o que esta pessoa
           escreveu neste navegador. */

        const lista = Array.isArray(dados?.mensagens) ? dados.mensagens : [];
        setMensagens(lista);

        /*
          A loja falou agora.

          Quando a mensagem é nova — e não o histórico que já estava aí quando a
          página abriu — o balão toca e abre sozinho. É o "posso ajudar?" da
          loja de rua: de nada adianta o vendedor falar de costas para o
          cliente, que era o que acontecia com a bolinha vermelha num canto da
          tela que ninguém estava olhando.
        */
        const ultimaDaLoja = [...lista].reverse().find((item) => !item.minha);
        if (primeiraCargaRef.current) {
          primeiraCargaRef.current = false;
          ultimaDaLojaRef.current = ultimaDaLoja?.id || null;
        } else if (ultimaDaLoja && ultimaDaLoja.id !== ultimaDaLojaRef.current) {
          ultimaDaLojaRef.current = ultimaDaLoja.id;
          tocarAviso();
          if (!abertoRef.current && !dispensadoRef.current) {
            setAberto(true);
            setNaoLidas(0);
            ultimaVistaRef.current = lista[lista.length - 1]?.id || ultimaVistaRef.current;
            window.setTimeout(() => fim.current?.scrollIntoView({ block: "end" }), 120);
          }
        }

        /*
          Nao lidas se contam a partir da ultima mensagem que a pessoa viu, e
          nao somando a cada consulta: duas consultas ao mesmo tempo somavam a
          mesma resposta duas vezes.
        */
        if (abertoRef.current) {
          ultimaVistaRef.current = lista[lista.length - 1]?.id || ultimaVistaRef.current;
          setNaoLidas(0);
        } else {
          const marca = ultimaVistaRef.current;
          const depois = marca ? lista.slice(lista.findIndex((item) => item.id === marca) + 1) : lista;
          setNaoLidas(depois.filter((item) => !item.minha).length);
        }
        if (rolar) window.setTimeout(() => fim.current?.scrollIntoView({ block: "end" }), 40);
      } catch {
        /* Rede caiu: o balao continua onde estava e tenta de novo depois. */
      }
    },
    [slug, chave]
  );

  useEffect(() => {
    buscarRef.current = buscar;
  }, [buscar]);

  useEffect(() => {
    if (!chave) return;
    void buscar(false);
  }, [chave, buscar]);

  /* A loja abrindo ou fechando o atendimento chega na hora, sem recarregar. */
  useEffect(() => {
    if (!slug || !ligado) return;
    const supabase = getSupabaseClient();
    const canal = supabase.channel(`loja-${slug}`);
    canal.on("broadcast", { event: "novidade" }, ({ payload }) => {
      const dados = (payload || {}) as { resumo?: string };
      if (dados.resumo === "online") setOnline(true);
      if (dados.resumo === "offline") setOnline(false);
    });
    canal.subscribe();
    return () => {
      void supabase.removeChannel(canal);
    };
  }, [slug, ligado]);

  /* Enquanto a conversa existe, a resposta da loja chega na hora. */
  useEffect(() => {
    if (!threadId) return;
    const supabase = getSupabaseClient();
    const canal = supabase.channel(`chat-${threadId}`);
    let apagarAviso: number | undefined;

    canal.on("broadcast", { event: "novidade" }, () => {
      setLojaDigitando(false);
      void buscar(abertoRef.current);
    });

    /* A loja escrevendo do outro lado. Esperar olhando uma tela parada é o que
       faz a pessoa desistir e ir para o WhatsApp. */
    canal.on("broadcast", { event: "digitando" }, ({ payload }) => {
      if (String((payload as { quem?: string })?.quem || "") === "visitante") return;
      setLojaDigitando(true);
      window.clearTimeout(apagarAviso);
      apagarAviso = window.setTimeout(() => setLojaDigitando(false), 4000);
    });

    canal.subscribe();
    canalDoFio.current = canal;

    return () => {
      window.clearTimeout(apagarAviso);
      canalDoFio.current = null;
      setLojaDigitando(false);
      void supabase.removeChannel(canal);
    };
  }, [threadId, buscar]);

  useEffect(() => {
    if (!aberto || !threadId) return;
    const relogio = window.setInterval(() => void buscar(false), 15000);
    return () => window.clearInterval(relogio);
  }, [aberto, threadId, buscar]);

  useEffect(() => {
    const medida = window.matchMedia("(max-width: 639px)");
    const conferir = () => setTelaPequena(medida.matches);
    conferir();
    medida.addEventListener("change", conferir);
    return () => medida.removeEventListener("change", conferir);
  }, []);

  /*
    Uma rolagem de cada vez.

    Com a conversa ocupando a tela no celular, o dedo continuava movendo a loja
    atrás dela: dois conteúdos correndo ao mesmo tempo, e a sensação de que a
    tela está escorregando. Enquanto o painel está aberto, a página fica parada
    onde estava — só a conversa rola.
  */
  useEffect(() => {
    if (!aberto || !telaPequena) return;

    /*
      `overflow: hidden` sozinho não segura o iPhone — no Safari o dedo continua
      arrastando a página por baixo. O que segura em todo celular é prender a
      página no lugar e guardar onde ela estava, devolvendo na hora de fechar.
      Sem devolver, a loja voltaria para o topo e a pessoa perderia o produto
      que estava olhando.
    */
    const pagina = document.documentElement;
    const corpo = document.body;
    const onde = window.scrollY;
    const antes = {
      overflowDaPagina: pagina.style.overflow,
      position: corpo.style.position,
      top: corpo.style.top,
      left: corpo.style.left,
      right: corpo.style.right,
      width: corpo.style.width,
    };

    pagina.style.overflow = "hidden";
    corpo.style.position = "fixed";
    corpo.style.top = `-${onde}px`;
    corpo.style.left = "0";
    corpo.style.right = "0";
    corpo.style.width = "100%";

    return () => {
      pagina.style.overflow = antes.overflowDaPagina;
      corpo.style.position = antes.position;
      corpo.style.top = antes.top;
      corpo.style.left = antes.left;
      corpo.style.right = antes.right;
      corpo.style.width = antes.width;
      window.scrollTo(0, onde);
    };
  }, [aberto, telaPequena]);

  /* O painel de emoji fecha ao clicar fora, ao voltar para o campo e no Esc.
     Só fechar no próprio botão era a reclamação certa: ele ficava aberto por
     cima da conversa e a pessoa tinha que caçar de novo a carinha. */
  useEffect(() => {
    if (!emojisAbertos) return;
    const fechar = () => setEmojisAbertos(false);
    const noEsc = (evento: KeyboardEvent) => {
      if (evento.key === "Escape") setEmojisAbertos(false);
    };
    window.addEventListener("pointerdown", fechar);
    window.addEventListener("keydown", noEsc);
    return () => {
      window.removeEventListener("pointerdown", fechar);
      window.removeEventListener("keydown", noEsc);
    };
  }, [emojisAbertos]);

  /* O navegador só libera som depois de a pessoa interagir. Destrava no
     primeiro toque, clique ou rolagem — que numa loja vem antes da conversa. */
  useEffect(() => {
    if (!ligado) return;
    const destravar = () => abrirAudio();
    window.addEventListener("pointerdown", destravar, { once: true });
    window.addEventListener("keydown", destravar, { once: true });
    window.addEventListener("scroll", destravar, { once: true, passive: true });
    return () => {
      window.removeEventListener("pointerdown", destravar);
      window.removeEventListener("keydown", destravar);
      window.removeEventListener("scroll", destravar);
    };
  }, [ligado]);

  /* Estado do aviso neste aparelho. */
  useEffect(() => {
    if (!ligado) return;
    void (async () => {
      if (!chavePublica) return setAviso("indisponivel");
      if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
        const ehApple = /iPad|iPhone|iPod/.test(navigator.userAgent);
        return setAviso(ehApple ? "precisa_instalar" : "indisponivel");
      }
      if (Notification.permission === "denied") return setAviso("negado");
      try {
        const registro = await navigator.serviceWorker.register("/sw.js");
        const atual = await registro.pushManager.getSubscription();
        setAviso(atual ? "ligado" : "desligado");
      } catch {
        setAviso("indisponivel");
      }
    })();
  }, [ligado, chavePublica]);

  async function ligarAviso() {
    if (!chavePublica || !threadId) return;
    setLigandoAviso(true);
    setErro(null);
    try {
      const permissao = await Notification.requestPermission();
      if (permissao !== "granted") {
        setAviso(permissao === "denied" ? "negado" : "desligado");
        return;
      }
      const registro = await navigator.serviceWorker.ready;
      const assinatura = await registro.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: base64ParaBytes(chavePublica),
      });
      const resposta = await fetch("/api/loja/chat/push", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ slug, chave, assinatura: assinatura.toJSON(), user_agent: navigator.userAgent }),
      });
      if (!resposta.ok) {
        throw new Error((await resposta.json().catch(() => null))?.error || "Não consegui ativar.");
      }
      setAviso("ligado");
    } catch (falha) {
      setErro(falha instanceof Error ? falha.message : "Não consegui ativar o aviso.");
    } finally {
      setLigandoAviso(false);
    }
  }

  async function enviar() {
    const mensagem = texto.trim();
    const quem = nome.trim();
    const anexo = arquivo;
    if ((!mensagem && !anexo) || enviando) return;
    if (!quem) {
      setErro("Diga seu nome para a loja saber com quem fala.");
      return;
    }
    if (anexo && anexo.size > TAMANHO_MAXIMO) {
      setErro("Arquivo maior que 8 MB.");
      return;
    }

    setEnviando(true);
    setErro(null);
    const provisoria: MensagemDaLoja = {
      id: `enviando-${Date.now()}`,
      minha: true,
      quem: "Você",
      texto: mensagem || null,
      anexoUrl: anexo && anexo.type.startsWith("image/") ? URL.createObjectURL(anexo) : null,
      anexoNome: anexo?.name || null,
      anexoTipo: anexo ? (anexo.type.startsWith("image/") ? "image" : "file") : null,
      apagada: false,
      em: new Date().toISOString(),
    };
    setMensagens((antes) => [...antes, provisoria]);
    setTexto("");
    setArquivo(null);
    if (campoDeArquivo.current) campoDeArquivo.current.value = "";
    window.setTimeout(() => fim.current?.scrollIntoView({ block: "end" }), 20);

    try {
      /* Com arquivo o envio vira formulário; só texto continua no caminho
         curto, que é o de todo dia. */
      const dados = new FormData();
      dados.set("slug", String(slug || ""));
      dados.set("chave", String(chave || ""));
      dados.set("nome", quem);
      dados.set("contato", contato.trim());
      dados.set("mensagem", mensagem);
      if (anexo) dados.set("arquivo", anexo);

      const resposta = await fetch("/api/loja/chat", {
        method: "POST",
        ...(anexo
          ? { body: dados }
          : {
              headers: { "content-type": "application/json" },
              body: JSON.stringify({ slug, chave, nome: quem, contato: contato.trim(), mensagem }),
            }),
      });
      const retorno = (await resposta.json().catch(() => null)) as { error?: string; threadId?: string } | null;
      if (!resposta.ok) throw new Error(retorno?.error || "Não foi possível enviar.");

      try {
        window.localStorage.setItem(GUARDA_DO_NOME, quem);
      } catch {
        /* sem armazenamento, pergunta de novo na proxima visita */
      }
      if (retorno?.threadId) setThreadId(retorno.threadId);
      await buscar(true);
    } catch (falha) {
      setMensagens((antes) => antes.filter((item) => item.id !== provisoria.id));
      setTexto(mensagem);
      setArquivo(anexo);
      setErro(falha instanceof Error ? falha.message : "Não foi possível enviar.");
    } finally {
      setEnviando(false);
    }
  }

  if (!slug || !ligado) return null;

  const podeOferecerAviso = Boolean(threadId) && (aviso === "desligado" || aviso === "precisa_instalar");
  const estiloDoBotao = vizinho ? { bottom: `${vizinho.bottom}px`, right: `${vizinho.right}px` } : undefined;
  const estiloDoPainel = vizinho ? { bottom: `${vizinho.bottom + 68}px` } : undefined;

  /* Tons da cor da loja: o cabeçalho e os cartões são a mesma cor diluída no
     branco, para a casa continuar sendo a casa sem pesar na leitura. */
  const corSuave = `color-mix(in srgb, ${cor} 9%, #ffffff)`;
  const corSuavissima = `color-mix(in srgb, ${cor} 5%, #ffffff)`;

  /* Os atalhos de abertura só existem antes de a pessoa falar: depois disso ela
     já disse o que queria, e um menu no meio da conversa vira estorvo. */
  const jaFalou = mensagens.some((mensagem) => mensagem.minha);
  const atalhos = [
    { icone: <Package className="h-4 w-4" />, rotulo: "Acompanhar pedido", frase: "Oi! Queria acompanhar meu pedido." },
    { icone: <FileText className="h-4 w-4" />, rotulo: "Fazer orçamento", frase: "Oi! Queria um orçamento." },
    { icone: <MessageCircle className="h-4 w-4" />, rotulo: "Falar com atendente", frase: "Oi! Pode me ajudar?" },
  ];

  return (
    <>
      {/*
        O convite.

        Um círculo com um ícone não diz o que acontece se você clicar. A pastilha
        com "Falar com a loja" diz — e é o que faz alguém que só ia embora
        perguntar antes.
      */}
      <button
        type="button"
        onClick={() => {
          setAberto((antes) => !antes);
          setNaoLidas(0);
          ultimaVistaRef.current = mensagens[mensagens.length - 1]?.id || ultimaVistaRef.current;
          window.setTimeout(() => fim.current?.scrollIntoView({ block: "end" }), 120);
        }}
        aria-label={aberto ? "Fechar conversa" : "Falar com a loja"}
        data-flutuante-da-loja=""
        /* No celular e uma bolinha do tamanho da do WhatsApp (56 px): dois botoes
           lado a lado com formatos diferentes — um comprimido, outro redondo —
           parecem dois assuntos, e o comprido ainda rouba a largura da tela.
           Da tela media para cima o rotulo cabe, e ai ele vira pilula. */
        className={`fixed right-4 z-[70] h-14 w-14 items-center justify-center rounded-full text-[14px] font-semibold shadow-[0_10px_28px_rgba(15,23,42,0.24)] transition active:scale-95 bottom-[calc(5.5rem+env(safe-area-inset-bottom))] sm:h-auto sm:w-auto sm:justify-start sm:gap-2.5 sm:px-5 sm:py-3.5 md:bottom-6 ${
          aberto && telaPequena ? "hidden" : "flex"
        }`}
        /* A posição ao lado do WhatsApp vale nas duas telas — é no celular que
           os dois se atropelavam. Quem não usa a medida é o painel, que no
           celular ocupa tudo. */
        style={{ backgroundColor: cor, color: corDoTexto, ...estiloDoBotao }}
      >
        {aberto ? (
          <X className="h-6 w-6 sm:h-5 sm:w-5" />
        ) : (
          <BalaoDeConversa className="h-[26px] w-[26px] sm:h-[22px] sm:w-[22px]" />
        )}
        <span className="hidden sm:inline">{aberto ? "Fechar" : "Falar com a loja"}</span>
        {!aberto && naoLidas > 0 ? (
          <span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-rose-500 px-1 text-[11px] font-bold text-white">
            {naoLidas}
          </span>
        ) : null}
      </button>

      {aberto ? (
        <section
          /*
            No celular, a tela inteira.

            Um cartãozinho de 380 px flutuando no meio do site é desconfortável
            de ler e pior ainda de escrever com o teclado aberto — a conversa
            fica espremida entre a loja e o teclado. No computador ele volta a
            ser o balão do canto.
          */
          className="fixed inset-0 z-[70] flex w-full flex-col overflow-hidden bg-white sm:inset-auto sm:right-4 sm:w-[min(380px,calc(100vw-2rem))] sm:rounded-[22px] sm:border sm:border-black/[0.06] sm:shadow-[0_18px_50px_rgba(15,23,42,0.18)] sm:bottom-[calc(9.5rem+env(safe-area-inset-bottom))] md:bottom-24"
          style={telaPequena ? undefined : { maxHeight: "min(74vh, 560px)", ...estiloDoPainel }}
        >
          {/*
            O cabeçalho.

            A cor da loja aparece na marquinha e no fundo diluído — chapada atrás
            do texto ela cansa e briga com a conversa. O ponto e a frase dizem a
            mesma coisa de dois jeitos: se tem gente agora, e o que esperar se
            não tiver.
          */}
          <header
            className="flex items-center gap-3 border-b border-black/[0.05] px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] sm:pt-3"
            style={{ backgroundColor: corSuave }}
          >
            <span
              className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[14px]"
              style={{ backgroundColor: cor, color: corDoTexto }}
            >
              <ShoppingBag className="h-[18px] w-[18px]" />
            </span>

            <div className="min-w-0 flex-1">
              <div className="text-[14.5px] font-semibold leading-tight tracking-[-0.01em] text-zinc-900">
                Falar com a loja
              </div>
              <div className="mt-[3px] flex items-center gap-1.5 text-[11.5px] leading-tight text-zinc-500">
                <span
                  className={`inline-block h-[6px] w-[6px] shrink-0 rounded-full ${
                    online ? "bg-emerald-500" : "bg-amber-500"
                  }`}
                />
                <span className="truncate">
                  <span className="font-semibold text-zinc-700">{online ? "Online agora" : "Offline"}</span>
                  {" · "}
                  {online ? "resposta por aqui mesmo" : "respondemos assim que voltarmos"}
                </span>
              </div>
            </div>

            {/* Discretos de propósito: quem abriu veio conversar, e não fechar —
                os botões existem, mas não pedem atenção. */}
            <div className="flex shrink-0 items-center">
              <button
                type="button"
                onClick={() => setAberto(false)}
                aria-label="Minimizar conversa"
                className="flex h-7 w-7 items-center justify-center rounded-full text-black/35 transition hover:bg-black/[0.06] hover:text-black/60"
              >
                <Minus className="h-4 w-4" />
              </button>
              {/* Fechar é diferente de minimizar: quem fecha não quer que o balão
                  abra sozinho de novo nesta visita. */}
              <button
                type="button"
                onClick={() => {
                  setAberto(false);
                  dispensadoRef.current = true;
                }}
                aria-label="Fechar conversa"
                className="flex h-7 w-7 items-center justify-center rounded-full text-black/35 transition hover:bg-black/[0.06] hover:text-black/60"
              >
                <X className="h-4 w-4" />
              </button>
            </div>
          </header>

          {/* `overscroll-contain`: chegando ao fim da conversa, o empurrão não
              passa para a loja atrás — a rolagem morre aqui. */}
          <div
            className="flex-1 overflow-y-auto overscroll-contain px-4 py-3"
            style={{ backgroundColor: corSuavissima }}
          >
            {mensagens.length === 0 ? (
              <div className="px-3 py-8 text-center text-[12.5px] text-zinc-500">
                {online
                  ? "Manda sua dúvida — tem gente do outro lado agora."
                  : "Manda sua dúvida. Respondemos assim que alguém voltar ao atendimento."}
              </div>
            ) : (
              <>
                {/* Quando a conversa começou: sem isso, uma resposta de ontem
                    parece de agora. */}
                <div className="mb-3 flex items-center gap-3 text-[11.5px] text-zinc-400">
                  <span className="h-px flex-1 bg-black/[0.07]" />
                  <span>{diaDaConversa(mensagens[0].em)}</span>
                  <span className="h-px flex-1 bg-black/[0.07]" />
                </div>

                {mensagens.map((mensagem) => (
                  <div
                    key={mensagem.id}
                    className={`mb-3 flex items-start gap-2 ${mensagem.minha ? "justify-end" : "justify-start"}`}
                  >
                    {/* A marquinha da loja em cada fala dela: de relance se sabe
                        quem falou, sem ler. */}
                    {!mensagem.minha ? (
                      <span
                        className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
                        style={{ backgroundColor: corSuave, color: cor }}
                      >
                        <ShoppingBag className="h-4 w-4" />
                      </span>
                    ) : null}

                    <div className={`max-w-[78%] ${mensagem.minha ? "text-right" : ""}`}>
                      <div
                        className={`inline-block rounded-[18px] px-3.5 py-2 text-left text-[13.5px] leading-[1.45] shadow-[0_1px_1px_rgba(16,24,40,0.05)] ${
                          mensagem.minha ? "rounded-br-[6px] text-white" : "rounded-tl-[6px] bg-white text-zinc-800"
                        }`}
                        style={mensagem.minha ? { backgroundColor: cor, color: corDoTexto } : undefined}
                      >
                    {mensagem.apagada ? (
                      <span className="italic opacity-70">Esta mensagem foi apagada</span>
                    ) : (
                      <>
                        {mensagem.cartao ? <CartaoDaLoja cartao={mensagem.cartao} minha={mensagem.minha} /> : null}

                        {mensagem.anexoUrl && mensagem.anexoTipo === "image" ? (
                          <a
                            href={mensagem.anexoUrl}
                            target="_blank"
                            rel="noreferrer"
                            className="mb-1.5 block overflow-hidden rounded-xl"
                          >
                            {/* eslint-disable-next-line @next/next/no-img-element */}
                            <img
                              src={mensagem.anexoUrl}
                              alt={mensagem.anexoNome || "Imagem"}
                              className="max-h-[170px] w-full object-cover"
                            />
                          </a>
                        ) : null}

                        {mensagem.anexoUrl && mensagem.anexoTipo !== "image" ? (
                          <a
                            href={mensagem.anexoUrl}
                            target="_blank"
                            rel="noreferrer"
                            className={`mb-1.5 flex items-center gap-2 rounded-xl px-2.5 py-2 text-[12px] font-semibold ${
                              mensagem.minha ? "bg-white/15" : "bg-[#f2f1f5]"
                            }`}
                          >
                            <Paperclip className="h-3.5 w-3.5 shrink-0" />
                            <span className="truncate">{mensagem.anexoNome || "Arquivo"}</span>
                          </a>
                        ) : null}

                        {mensagem.texto ? <p className="whitespace-pre-wrap">{mensagem.texto}</p> : null}
                      </>
                    )}
                      </div>
                      <div className="mt-1 px-1 text-[11px] text-zinc-400">{hora(mensagem.em)}</div>
                    </div>
                  </div>
                ))}

                {/*
                  Os três caminhos de sempre.

                  Quem abre um chat de loja quer uma de três coisas, e escrever a
                  primeira frase é o que mais trava. Somem assim que a pessoa
                  fala: menu no meio de conversa é estorvo.
                */}
                {!jaFalou ? (
                  <div className="mb-3 ml-10 flex flex-wrap gap-2">
                    {atalhos.map((atalho) => (
                      <button
                        key={atalho.rotulo}
                        type="button"
                        onClick={() => {
                          setTexto(atalho.frase);
                          campoDeTexto.current?.focus();
                        }}
                        className="inline-flex items-center gap-2 rounded-full border border-black/[0.06] bg-white px-3.5 py-2 text-[12.5px] font-medium text-zinc-700 shadow-[0_1px_1px_rgba(16,24,40,0.04)] transition hover:border-black/15 hover:text-zinc-900"
                      >
                        <span style={{ color: cor }}>{atalho.icone}</span>
                        {atalho.rotulo}
                      </button>
                    ))}
                  </div>
                ) : null}
              </>
            )}
            {lojaDigitando ? (
              <div className="mb-2 flex justify-start">
                <div className="rounded-2xl bg-white px-3 py-2 text-[12px] text-zinc-500 shadow-[0_1px_2px_rgba(16,24,40,0.08)]">
                  digitando…
                </div>
              </div>
            ) : null}
            <div ref={fim} />
          </div>

          {/*
            O aviso no aparelho.

            Só faz sentido depois de existir conversa — antes disso não há o que
            avisar — e some assim que a pessoa liga. É o que evita a espera de
            olho na tela: ela fecha a loja e a resposta a encontra.
          */}
          {podeOferecerAviso ? (
            <div className="px-4 pb-3" style={{ backgroundColor: corSuavissima }}>
              <div className="flex items-center gap-3 rounded-2xl px-3.5 py-3" style={{ backgroundColor: corSuave }}>
                <span
                  className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/70"
                  style={{ color: cor }}
                >
                  <Bell className="h-4.5 w-4.5" />
                </span>
                {aviso === "precisa_instalar" ? (
                  <span className="flex-1 text-[12px] leading-snug text-zinc-600">
                    Para receber aviso no iPhone, adicione esta loja à Tela de Início.
                  </span>
                ) : (
                  <>
                    <span className="min-w-0 flex-1">
                      <span className="block text-[13px] font-bold text-zinc-900">Avise-me quando responderem</span>
                      <span className="block text-[11.5px] leading-snug text-zinc-600">
                        Você será notificado assim que a loja responder.
                      </span>
                    </span>
                    <button
                      type="button"
                      onClick={() => void ligarAviso()}
                      disabled={ligandoAviso}
                      className="inline-flex h-9 shrink-0 items-center rounded-xl px-4 text-[13px] font-bold disabled:opacity-60"
                      style={{ backgroundColor: cor, color: corDoTexto }}
                    >
                      Ativar
                    </button>
                  </>
                )}
              </div>
            </div>
          ) : null}

          {aviso === "ligado" && threadId ? (
            <div className="px-4 pb-3" style={{ backgroundColor: corSuavissima }}>
              <div className="flex items-center gap-2 rounded-xl bg-emerald-50 px-3 py-2 text-[11.5px] font-semibold text-emerald-700">
                <BellRing className="h-3.5 w-3.5 shrink-0" />
                Você será avisado neste aparelho quando respondermos.
              </div>
            </div>
          ) : null}

          <div className="border-t border-black/[0.05] bg-white px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-2.5 sm:pb-3">
            {/* Enquanto a pessoa nao mandou nada, pede nome e contato. Antes a
                condicao era "conversa vazia", e a saudacao automatica da loja
                ja enchia a conversa: os campos sumiam e o envio seguia
                exigindo o nome, sem lugar para digita-lo. */}
            {!mensagens.some((m) => m.minha) ? (
              <div className="mb-2 grid grid-cols-2 gap-2">
                <input
                  value={nome}
                  onChange={(evento) => setNome(evento.target.value)}
                  placeholder="Seu nome"
                  className="h-9 rounded-xl bg-[#f5f4f7] px-3 text-[12.5px] outline-none placeholder:text-zinc-400 focus:bg-[#f0eff3]"
                />
                <input
                  value={contato}
                  onChange={(evento) => setContato(evento.target.value)}
                  placeholder="WhatsApp (opcional)"
                  className="h-9 rounded-xl bg-[#f5f4f7] px-3 text-[12.5px] outline-none placeholder:text-zinc-400 focus:bg-[#f0eff3]"
                />
              </div>
            ) : null}

            {erro ? <div className="mb-2 text-[11.5px] font-semibold text-rose-600">{erro}</div> : null}

            {/* O arquivo escolhido fica à vista antes de ir: numa gráfica é a
                logo do cliente, e mandar a errada custa uma reimpressão. */}
            {arquivo ? (
              <div className="mb-2 flex items-center gap-2 rounded-xl bg-[#f4f3f7] px-2.5 py-2 text-[11.5px] text-zinc-700">
                <Paperclip className="h-3.5 w-3.5 shrink-0 text-zinc-500" />
                <span className="flex-1 truncate">{arquivo.name}</span>
                <button
                  type="button"
                  onClick={() => {
                    setArquivo(null);
                    if (campoDeArquivo.current) campoDeArquivo.current.value = "";
                  }}
                  aria-label="Tirar o arquivo"
                  className="text-zinc-400 hover:text-zinc-700"
                >
                  <X className="h-3.5 w-3.5" />
                </button>
              </div>
            ) : null}

            <div className="relative flex items-end gap-2">
              {/*
                Os emojis flutuam por cima da conversa.

                Empurrando o conteúdo, cada abrir e fechar sacudia a tela e
                mudava a rolagem — a mensagem que se estava lendo saltava do
                lugar. Por cima, nada se move.
              */}
              {emojisAbertos ? (
                <div
                  className="absolute bottom-full left-0 right-0 z-10 mb-2 grid grid-cols-8 gap-0.5 rounded-2xl border border-black/[0.06] bg-white p-2 shadow-[0_12px_32px_rgba(16,24,40,0.14)]"
                  onPointerDown={(evento) => evento.stopPropagation()}
                >
                  {EMOJIS.map((emoji) => (
                    <button
                      key={emoji}
                      type="button"
                      onClick={() => {
                        setTexto((antes) => antes + emoji);
                        campoDeTexto.current?.focus();
                      }}
                      className="h-8 rounded-lg text-[17px] leading-none transition hover:bg-black/[0.05]"
                    >
                      {emoji}
                    </button>
                  ))}
                </div>
              ) : null}

              <input
                ref={campoDeArquivo}
                type="file"
                hidden
                onChange={(evento) => {
                  setArquivo(evento.target.files?.[0] || null);
                  setErro(null);
                }}
              />
              <button
                type="button"
                onClick={() => campoDeArquivo.current?.click()}
                aria-label="Anexar arquivo"
                className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-zinc-400 transition hover:bg-black/[0.04] hover:text-zinc-600"
              >
                <Paperclip className="h-[18px] w-[18px]" />
              </button>
              <textarea
                ref={campoDeTexto}
                onFocus={() => setEmojisAbertos(false)}
                value={texto}
                onChange={(evento) => {
                  setTexto(evento.target.value);
                  /* Avisa a loja que estou escrevendo, no máximo de dois em dois
                     segundos. */
                  const agora = Date.now();
                  if (evento.target.value.trim() && canalDoFio.current && agora - ultimoAvisoDeDigitacao.current > 2000) {
                    ultimoAvisoDeDigitacao.current = agora;
                    void canalDoFio.current.send({
                      type: "broadcast",
                      event: "digitando",
                      payload: { quem: nome.trim() || "Visitante" },
                    });
                  }
                }}
                onKeyDown={(evento) => {
                  if (evento.key === "Enter" && !evento.shiftKey) {
                    evento.preventDefault();
                    void enviar();
                  }
                }}
                rows={1}
                placeholder="Escreva sua mensagem..."
                className="max-h-24 min-h-[42px] flex-1 resize-none rounded-[18px] bg-[#f5f4f7] py-[11px] pl-4 pr-10 text-[13.5px] leading-tight outline-none transition placeholder:text-zinc-400 focus:bg-[#f0eff3]"
              />

              {/* O emoji mora dentro do campo, como em todo aplicativo de
                  mensagem — e não numa barra de ferramentas em cima. */}
              <button
                type="button"
                onPointerDown={(evento) => evento.stopPropagation()}
                onClick={() => setEmojisAbertos((antes) => !antes)}
                aria-label="Emojis"
                className="absolute bottom-[11px] right-[62px] h-5 w-5 text-zinc-400 transition hover:text-zinc-600"
              >
                <Smile className="h-5 w-5" />
              </button>

              <button
                type="button"
                onClick={() => void enviar()}
                disabled={enviando || (!texto.trim() && !arquivo)}
                aria-label="Enviar"
                className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition active:scale-95 disabled:opacity-35"
                style={{ backgroundColor: cor, color: corDoTexto }}
              >
                <Send className="h-[17px] w-[17px]" />
              </button>
            </div>

            <div className="mt-2 pl-1 text-[10.5px] text-zinc-400">Pressione Enter para enviar</div>
          </div>
        </section>
      ) : null}
    </>
  );
}
