"use client";

import { estoqueLimitaAVenda, preencherModelo } from "@/lib/loja/regras-da-loja";
import { useEstoqueDaEscolha } from "@/lib/use-estoque-da-escolha";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { usePathname, useRouter } from "next/navigation";
import {
  BadgeCheck,
  Clock,
  CreditCard,
  Headphones,
  Info,
  LogIn,
  MapPin,
  MessageCircle,
  Minus,
  Package,
  Plus,
  ShieldCheck,
  ShoppingCart,
  Truck,
} from "lucide-react";
import { useCart } from "@/context/CartContext";
import CatalogShippingCalculator from "@/components/CatalogShippingCalculator";
import type { ProductCombo, SelectedComboData } from "@/components/ProductCombosWidget";
import { useVisaoDaLoja } from "@/components/VisaoDaLojaContext";
import { normalizeWhatsappForLink } from "@/lib/phone";
import { VendorCompreJunto } from "./VendorCompreJunto";
import { buildShippingPackageFromItems, isPickupShippingOption, type ShippingSelection } from "@/lib/catalog-shipping";
import {
  calculateEffectiveQuantity,
  calculateTierDiscountPct,
  checkSaleQuantityLimits,
  getBestTierDiscount,
  resolveSaleQuantityLimits,
  type ProductPriceTier,
} from "@/lib/price-tiers";
import {
  alternarEscolha,
  escolhidosComGrupo,
  grupoPendente,
  servicoComoOpcao,
  somaDosServicos,
  vitrineDeServicos,
  type GrupoDeServicos,
  type ServicoOpcional,
} from "@/lib/servicos-opcionais";
import { moedaDoVendor, numeroDoVendor } from "./formato";
import { VendorValorMinimo } from "./VendorValorMinimo";
import { SeletorDeVariacoes } from "@/components/SeletorDeVariacoes";
import {
  juntarAdicionais,
  nomeDaMedida,
  precoDaLinha,
  unidadeDoAdicional,
  valorMinimoDoItem,
  vendidoPorMedida,
  type UnidadeDoAdicional,
} from "@/lib/adicionais";

/*
  O card de configuração do Vendor: onde a compra acontece.

  O desenho é todo deste template. A LÓGICA de negócio — faixa de preço,
  contrato de `selected_options`, regra de escolha dos serviços — vem das
  bibliotecas do sistema de propósito: são as mesmas regras que o pedido, o
  orçamento e a produção leem depois. Template define aparência; o que sai
  daqui para o carrinho tem de ser byte a byte igual ao dos outros.

  As regras da loja (lib/loja/regras-da-loja.ts) mandam no botão e no que
  aparece no lugar do preço: entrar para ver o preço, loja em pausa, pedido
  pelo WhatsApp, lista de orçamento ou só vitrine. O produto já chega sem
  preço quando o visitante não pode ver; aqui só se decide como desenhar e
  para onde o botão leva.
*/

export type VendorOpcao = {
  id?: string | null;
  label: string;
  price?: number | null;
  price_applies_per_quantity?: boolean | null;
  price_unit?: string | null;
  is_required?: boolean | null;
  stock?: number | null;
  /* A foto do produto que representa esta opção. */
  image_id?: string | null;
};

export type VendorAtributo = {
  id?: string | null;
  name?: string | null;
  options?: Array<string | VendorOpcao> | null;
  allow_multiple?: boolean | null;
  is_required?: boolean | null;
  position?: number | null;
  show_in_store?: boolean | null;
};

export type VendorProdutoDaPagina = {
  id: string;
  name: string;
  price: number;
  stock: number;
  user_id: string;
  unit_type?: string | null;
  track_inventory?: boolean | null;
  /* "Permitir estoque negativo": o saldo nao limita a venda. */
  allow_negative_stock?: boolean | null;
  /* Estoque v2 (marcado pelo servidor): quem diz o saldo e o banco, por combinação. */
  estoque_v2?: boolean | null;
  estoque_v2_limita?: boolean | null;
  estoque_v2_esgotado?: boolean | null;
  price_on_request?: boolean | null;
  use_variation_price_only?: boolean | null;
  /* Valor mínimo do item (migration 194). */
  valor_minimo_item?: number | string | null;
  valor_minimo_loja?: boolean | null;
  min_sale_qty_enabled?: boolean | null;
  min_sale_qty?: number | null;
  max_sale_qty_enabled?: boolean | null;
  max_sale_qty?: number | null;
  allowed_sale_qty_enabled?: boolean | null;
  allowed_sale_quantities?: number[] | null;
  inventory_scope?: string | null;
  inventory_attribute_name?: string | null;
  shipping_weight_kg?: number | null;
  shipping_width_cm?: number | null;
  shipping_height_cm?: number | null;
  shipping_length_cm?: number | null;
  production_time_value?: number | null;
  production_time_unit?: "hours" | "days" | "business_days" | "weeks" | null;
  /* Os gabaritos para baixar (aba Produção do editor); a página desenha o bloco. */
  production_templates?: Array<{ format?: string | null; url?: string | null; name?: string | null }> | null;
  production_template_url?: string | null;
  production_template_name?: string | null;
  production_template_format?: string | null;
  product_attributes?: VendorAtributo[] | null;
  /* As combinações com preço e saldo próprios, quando a loja controla o
     estoque por variação. */
  product_variations?: Array<{
    id?: string | null;
    combination?: Record<string, string> | null;
    price?: number | null;
    stock?: number | null;
    sku?: string | null;
  }> | null;
  product_images?: Array<{ id?: string | null; image_url?: string | null }> | null;
  product_price_tiers?: ProductPriceTier[] | null;
  custom_fields_config?: Array<{
    id?: string | null;
    title?: string | null;
    field_type?: "text" | "textarea" | "number" | null;
    placeholder?: string | null;
    required?: boolean | null;
    active?: boolean | null;
    /* O campo pode estar preso a uma escolha: "Texto do recorte" só faz
       sentido para quem pediu recorte personalizado. */
    variation_signature?: string | null;
    position?: number | null;
  }> | null;
};

type OpcaoNormalizada = {
  id: string | null;
  rotulo: string;
  preco: number;
  porQuantidade: boolean;
  /* Em que a opção se multiplica: medida (m²), peça ou uma vez no pedido. */
  unidade: UnidadeDoAdicional;
  semEstoque: boolean;
  /* O saldo cadastrado nesta opção, quando o estoque é controlado por opção. */
  saldo: number | null;
};

type AtributoNormalizado = {
  chave: string;
  nome: string;
  multipla: boolean;
  obrigatorio: boolean;
  opcoes: OpcaoNormalizada[];
};

/* As opções normalizadas do produto. O desenho das variações é do seletor do
   sistema; esta leitura existe para o preço e para o que vai ao carrinho —
   preço da opção, se multiplica pela quantidade e a qual atributo pertence. */
function normalizarAtributos(
  atributos: VendorAtributo[] | null | undefined,
  /* Saldo zero só significa "esgotado" no atributo que controla o estoque.
     Nos outros, o número guardado no cadastro é lixo antigo. */
  atributoDoEstoque: string | null,
  /* A unidade de venda do produto decide o que "multiplica na quantidade" quer dizer. */
  unitType: string | null | undefined
): AtributoNormalizado[] {
  if (!Array.isArray(atributos)) return [];
  const chaveDoEstoque = String(atributoDoEstoque || "").trim().toLowerCase();
  return atributos
    .filter((atributo) => atributo && atributo.show_in_store !== false && String(atributo.name || "").trim())
    .map((atributo, indice) => {
      const opcoes = (Array.isArray(atributo.options) ? atributo.options : [])
        .map((opcao) => {
          if (typeof opcao === "string") {
            return {
              id: null, rotulo: opcao.trim(), preco: 0, porQuantidade: true,
              unidade: vendidoPorMedida(unitType) ? "measure" : "piece", semEstoque: false, saldo: null,
            };
          }
          const rotulo = String(opcao?.label || "").trim();
          if (!rotulo) return null;
          return {
            id: typeof opcao.id === "string" && opcao.id ? opcao.id : null,
            rotulo,
            preco: Number(opcao.price) || 0,
            /* Adicional por peça é o padrão histórico do cadastro: só o
               `false` explícito cobra uma vez. */
            porQuantidade: opcao.price_applies_per_quantity !== false,
            unidade: unidadeDoAdicional(opcao, unitType),
            semEstoque:
              Boolean(chaveDoEstoque) &&
              String(atributo.name || "").trim().toLowerCase() === chaveDoEstoque &&
              opcao.stock !== null &&
              opcao.stock !== undefined &&
              Number(opcao.stock) <= 0,
            saldo: opcao.stock === null || opcao.stock === undefined ? null : Number(opcao.stock) || 0,
          } satisfies OpcaoNormalizada;
        })
        .filter((opcao): opcao is OpcaoNormalizada => Boolean(opcao));

      return {
        chave: String(atributo.name || "").trim(),
        nome: String(atributo.name || "").trim().replace(/\s*:\s*$/, ""),
        multipla: atributo.allow_multiple === true,
        obrigatorio: atributo.is_required === true,
        opcoes,
        posicao: typeof atributo.position === "number" ? atributo.position : indice,
      };
    })
    .filter((atributo) => atributo.opcoes.length > 0)
    .sort((a, b) => a.posicao - b.posicao)
    .map(({ chave, nome, multipla, obrigatorio, opcoes }) => ({ chave, nome, multipla, obrigatorio, opcoes }));
}

/* Texto legível sobre a cor da marca, para o seletor de variações saber com
   que cor escrever dentro de uma opção marcada. */
function textoSobreACor(cor: string) {
  const hex = String(cor || "").replace("#", "");
  if (hex.length !== 6) return "#ffffff";
  const r = parseInt(hex.slice(0, 2), 16);
  const g = parseInt(hex.slice(2, 4), 16);
  const b = parseInt(hex.slice(4, 6), 16);
  return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.62 ? "#111827" : "#ffffff";
}

/* Dígitos viram metros com duas casas: "250" vira "2,50". A conta é a mesma
   da vitrine, escrita aqui para o Vendor não depender dela. */
function metrosDeDigitos(digitos: string) {
  const limpo = String(digitos || "").replace(/\D/g, "");
  if (!limpo) return "";
  const preenchido = limpo.padStart(3, "0");
  const inteiro = preenchido.slice(0, -2).replace(/^0+(?=\d)/, "") || "0";
  return `${inteiro},${preenchido.slice(-2)}`;
}

/* O "?" dos serviços: abre no passar do mouse e também no toque.

   O `title` do navegador não serve aqui — demora quase um segundo, some
   sozinho e no celular não existe. Como o cartão inteiro é um botão, o gatilho
   é um <span> com papel de botão: botão dentro de botão não existe em HTML. */
function AjudaDoServico({ texto }: { texto: string }) {
  const [aberto, setAberto] = useState(false);
  const caixa = useRef<HTMLSpanElement | null>(null);

  useEffect(() => {
    if (!aberto) return;
    const foraDaCaixa = (evento: MouseEvent | TouchEvent) => {
      if (!caixa.current?.contains(evento.target as Node)) setAberto(false);
    };
    const comEsc = (evento: KeyboardEvent) => {
      if (evento.key === "Escape") {
        evento.stopPropagation();
        setAberto(false);
      }
    };
    document.addEventListener("mousedown", foraDaCaixa);
    document.addEventListener("touchstart", foraDaCaixa);
    document.addEventListener("keydown", comEsc);
    return () => {
      document.removeEventListener("mousedown", foraDaCaixa);
      document.removeEventListener("touchstart", foraDaCaixa);
      document.removeEventListener("keydown", comEsc);
    };
  }, [aberto]);

  return (
    <span
      ref={caixa}
      className="relative inline-flex"
      onMouseEnter={() => setAberto(true)}
      onMouseLeave={() => setAberto(false)}
    >
      <span
        role="button"
        tabIndex={0}
        aria-label="O que está incluso"
        aria-expanded={aberto}
        onClick={(evento) => {
          evento.preventDefault();
          evento.stopPropagation();
          setAberto((antes) => !antes);
        }}
        onKeyDown={(evento) => {
          if (evento.key !== "Enter" && evento.key !== " ") return;
          evento.preventDefault();
          evento.stopPropagation();
          setAberto((antes) => !antes);
        }}
        className="inline-flex cursor-help items-center text-[var(--vd-muted-fg)] transition-colors hover:text-[var(--vd-foreground)]"
      >
        <Info className="h-3.5 w-3.5" />
      </span>

      {aberto ? (
        <span
          role="tooltip"
          onClick={(evento) => evento.stopPropagation()}
          className="absolute bottom-full left-1/2 z-40 mb-2 w-60 -translate-x-1/2 border bg-[var(--vd-surface)] p-3 text-left text-[12px] font-normal leading-relaxed text-[var(--vd-muted-fg)] shadow-xl"
          style={{ borderRadius: "var(--vd-radius-lg)", borderColor: "var(--vd-border)" }}
        >
          {texto}
        </span>
      ) : null}
    </span>
  );
}

/* Os dois selos de confiança que a loja escolhe para a página do produto.
   Sem escolha, valem os mesmos padrões do resto do sistema. */
const DESENHOS_DA_CONFIANCA: Record<string, typeof ShieldCheck> = {
  truck: Truck,
  shieldCheck: ShieldCheck,
  creditCard: CreditCard,
  package: Package,
  clock: Clock,
  badgeCheck: BadgeCheck,
  mapPin: MapPin,
  headphones: Headphones,
};

function selosDaLoja(itens: Array<{ icon?: string | null; title?: string | null }> | null | undefined) {
  const lista = (itens || [])
    .map((item) => ({ icon: String(item?.icon || "").trim(), title: String(item?.title || "").trim() }))
    .filter((item) => item.icon || item.title)
    .slice(0, 2);
  const padroes = [
    { icon: "truck", title: "Entrega rápida e segura" },
    { icon: "shieldCheck", title: "Garantia de qualidade" },
  ];
  while (lista.length < 2) lista.push(padroes[lista.length]);
  return lista;
}

function semCaixa(valor: string | null | undefined) {
  return String(valor || "").trim().toLowerCase();
}

/* A assinatura de um campo aponta para uma combinação inteira
   ("cor:azul|tamanho:g") ou para uma única opção ("option::cor::azul").
   Campo sem assinatura aparece sempre. */
function campoCombinaComEscolhas(assinatura: string | null | undefined, escolhas: Record<string, string[]>) {
  const alvo = semCaixa(assinatura);
  if (!alvo) return true;

  if (alvo.startsWith("option::")) {
    const [, atributo, valor] = alvo.split("::");
    if (!atributo || !valor) return false;
    const chave = Object.keys(escolhas).find((k) => semCaixa(k) === semCaixa(atributo));
    if (!chave) return false;
    return (escolhas[chave] || []).some((rotulo) => semCaixa(rotulo) === semCaixa(valor));
  }

  const atual = Object.entries(escolhas)
    .map(([chave, valores]) => [semCaixa(chave), semCaixa(valores[0] || "")] as const)
    .filter(([chave, valor]) => Boolean(chave) && Boolean(valor))
    .sort((a, b) => a[0].localeCompare(b[0]))
    .map(([chave, valor]) => `${chave}:${valor}`)
    .join("|");

  return alvo === atual;
}

export function VendorCompra({
  produto,
  slug,
  corDaMarca,
  gruposDeServico,
  servicosDoProduto,
  combos,
  aoPreferirImagem,
  selos,
  whatsapp,
}: {
  produto: VendorProdutoDaPagina;
  slug: string;
  corDaMarca: string;
  gruposDeServico?: GrupoDeServicos[];
  servicosDoProduto?: ServicoOpcional[];
  /* Os produtos que a loja sugere levar junto com este. */
  combos?: ProductCombo[];
  /* A opção escolhida pode ter foto própria: a galeria acompanha. */
  aoPreferirImagem?: (idDaImagem: string | null) => void;
  /* Os selos de confiança configurados no catálogo. */
  selos?: Array<{ icon?: string | null; title?: string | null }> | null;
  /* O WhatsApp da loja (o mesmo do rodapé): é para onde vai o pedido no
     modo "pedir pelo WhatsApp" e a dúvida sobre um produto sob consulta. */
  whatsapp?: string | null;
}) {
  const { addToCart, setShippingSelection } = useCart();
  const router = useRouter();
  const caminho = usePathname();
  /* O que este visitante pode ver e fazer (regras da loja). */
  const visao = useVisaoDaLoja();
  /* O saldo so limita quando o produto controla estoque e NAO permite estoque
     negativo. Com a permissao ligada, a pagina vende como se houvesse saldo:
     sem "Indisponivel", sem teto de quantidade, sem opcao riscada e sem aviso
     de estoque. O estoque fica negativo e o painel marca a reposicao. */
  const estoqueLimitaDoCadastro = estoqueLimitaAVenda(produto);
  const textoSemPreco = visao.textoNoLugarDoPreco;
  /* Sem preço: pelo cadastro ("sob consulta") ou pela regra da loja — nos
     dois casos o produto chega com `price_on_request`. */
  const semPreco = Boolean(produto.price_on_request);
  const digitosDoWhats = normalizeWhatsappForLink(whatsapp);
  const [combosEscolhidos, setCombosEscolhidos] = useState<SelectedComboData[]>([]);
  const atributos = useMemo(
    () =>
      normalizarAtributos(
        produto.product_attributes,
        estoqueLimitaDoCadastro && produto.inventory_scope === "option" ? produto.inventory_attribute_name || null : null,
        produto.unit_type
      ),
    [produto.product_attributes, produto.inventory_scope, produto.inventory_attribute_name, produto.unit_type, estoqueLimitaDoCadastro]
  );

  const ehMetroQuadrado = produto.unit_type === "square_meter";
  const faixas = useMemo(
    () => (produto.product_price_tiers || []).filter((faixa) => faixa && faixa.active !== false),
    [produto.product_price_tiers]
  );

  const quantidadeMinima = useMemo(() => {
    if (produto.allowed_sale_qty_enabled && produto.allowed_sale_quantities?.length) {
      return Math.min(...produto.allowed_sale_quantities.map((q) => Number(q) || 1));
    }
    if (produto.min_sale_qty_enabled && Number(produto.min_sale_qty) > 0) return Number(produto.min_sale_qty);
    if (faixas.length) return Math.min(...faixas.map((faixa) => Number(faixa.min_qty) || 1));
    return 1;
  }, [produto, faixas]);

  const [escolhas, setEscolhas] = useState<Record<string, string[]>>({});
  /* Nasce na quantidade mínima e não é ressincronizada: a página de produto
     sempre remonta ao trocar de produto, então não há o que perseguir. */
  const [quantidade, setQuantidade] = useState(quantidadeMinima);
  /* As medidas entram como dígitos e saem formatadas: quem digita 250 vê
     2,50. Sem isso o cliente tem de acertar a vírgula sozinho. */
  const [digitosDaLargura, setDigitosDaLargura] = useState("");
  const [digitosDaAltura, setDigitosDaAltura] = useState("");
  const largura = metrosDeDigitos(digitosDaLargura);
  const altura = metrosDeDigitos(digitosDaAltura);
  const [campos, setCampos] = useState<Record<string, string>>({});
  const [servicosMarcados, setServicosMarcados] = useState<string[]>([]);
  const [aviso, setAviso] = useState("");
  const [adicionado, setAdicionado] = useState(false);
  /* Onde a barra fixa é montada: a raiz do template, que carrega as cores e
     as fontes da loja. Só existe depois que a página monta no navegador. */
  const [casaDaBarra, setCasaDaBarra] = useState<HTMLElement | null>(null);
  useEffect(() => {
    const achar = () => setCasaDaBarra(document.querySelector<HTMLElement>("[data-vd-raiz]") || document.body);
    const marcar = window.setTimeout(achar, 0);
    return () => window.clearTimeout(marcar);
  }, []);
  const [freteEscolhido, setFreteEscolhido] = useState<ShippingSelection | null>(null);
  /* Guardado para saber se a pessoa já escolheu uma linha da tabela: a
     quantidade nasce na menor faixa, e o total é mostrado desde o começo. */
  const [, setQuantidadeEscolhida] = useState(false);

  const vitrine = useMemo(
    () => vitrineDeServicos(gruposDeServico ?? [], servicosDoProduto ?? []),
    [gruposDeServico, servicosDoProduto]
  );
  const servicoPendente = useMemo(() => grupoPendente(vitrine, servicosMarcados), [vitrine, servicosMarcados]);

  /* Estoque v2: o banco diz quanto dá para vender DESTA combinação (a mesma
     conta do PDV e do orçamento — item pronto da célula escolhida ou os
     insumos controlados da composição, serviços inclusive). Fora do v2 nada
     muda: o saldo vem do produto, como sempre. */
  const ehEstoqueV2 = produto.estoque_v2 === true;
  const escolhaParaOEstoque = useMemo(
    () =>
      ehEstoqueV2
        ? {
            options: [
              ...atributos.flatMap((atributo) =>
                (escolhas[atributo.chave] || []).map((rotulo) => ({
                  option_id: atributo.opcoes.find((o) => o.rotulo === rotulo)?.id ?? null,
                  attribute_name: atributo.chave,
                  label: rotulo,
                }))
              ),
              ...escolhidosComGrupo(vitrine, servicosMarcados).map(({ servico, grupo }) => servicoComoOpcao(servico, grupo.nome)),
            ],
          }
        : null,
    [ehEstoqueV2, atributos, escolhas, vitrine, servicosMarcados]
  );
  const escolhaCompletaParaOEstoque = atributos.every((a) => !a.obrigatorio || (escolhas[a.chave] || []).length > 0);
  const estoqueV2 = useEstoqueDaEscolha({
    ativo: ehEstoqueV2,
    slug,
    productId: produto.id,
    selected: escolhaParaOEstoque,
    largura: produto.unit_type === "square_meter" ? Number(String(largura).replace(",", ".")) || null : null,
    altura: produto.unit_type === "square_meter" ? Number(String(altura).replace(",", ".")) || null : null,
    completa: escolhaCompletaParaOEstoque,
  });
  /* O saldo só limita quando o produto controla e NÃO permite negativo (no
     v2: quando o banco diz que limita). Enquanto a resposta do v2 não chega,
     nada trava: o servidor confere de novo no carrinho e ao fechar — menos o
     produto que o servidor já marcou esgotado: esse nasce indisponível. */
  const esgotadoAntesDaResposta = ehEstoqueV2 && estoqueV2 == null && produto.estoque_v2_esgotado === true;
  const estoqueLimita = ehEstoqueV2
    ? estoqueV2 ? estoqueV2.limita === true : esgotadoAntesDaResposta
    : estoqueLimitaDoCadastro;
  /* Escolha que o estoque v2 não acha (valor fora da grade): não vende. */
  const escolhaForaDoEstoque = ehEstoqueV2 && estoqueV2?.motivo === "falta_escolher" && escolhaCompletaParaOEstoque;

  /* Campo preso a uma variação só aparece quando ela é a escolhida — mostrar
     sempre faz o cliente preencher o que não vale para o pedido dele. */
  /* Se a opção marcada aponta para uma foto do produto, é ela que a galeria
     deve mostrar: escolher "Vinil dourado" e continuar vendo o branco faz o
     cliente duvidar do que está comprando. */
  const imagemPreferida = useMemo(() => {
    const disponiveis = new Set(
      (produto.product_images || []).map((imagem) => String(imagem?.id || "")).filter(Boolean)
    );
    for (const atributo of produto.product_attributes || []) {
      const marcadas = escolhas[String(atributo?.name || "").trim()] || [];
      if (!marcadas.length || !Array.isArray(atributo?.options)) continue;
      for (const rotulo of marcadas) {
        const opcao = atributo.options.find(
          (item) => (typeof item === "string" ? item : item?.label) === rotulo
        );
        if (!opcao || typeof opcao === "string") continue;
        const id = String(opcao.image_id || "");
        if (id && disponiveis.has(id)) return id;
      }
    }
    return null;
  }, [produto.product_images, produto.product_attributes, escolhas]);

  useEffect(() => {
    aoPreferirImagem?.(imagemPreferida);
  }, [aoPreferirImagem, imagemPreferida]);

  const camposAtivos = useMemo(
    () =>
      (produto.custom_fields_config || [])
        .filter((campo) => campo && campo.active !== false && campo.title)
        .filter((campo) => campoCombinaComEscolhas(campo.variation_signature, escolhas))
        .sort((a, b) => (Number(a.position) || 0) - (Number(b.position) || 0)),
    [produto.custom_fields_config, escolhas]
  );

  function escolher(atributo: AtributoNormalizado, rotulo: string) {
    setAviso("");
    setEscolhas((antes) => {
      const atuais = antes[atributo.chave] || [];
      if (!atributo.multipla) return { ...antes, [atributo.chave]: [rotulo] };
      return {
        ...antes,
        [atributo.chave]: atuais.includes(rotulo)
          ? atuais.filter((r) => r !== rotulo)
          : [...atuais, rotulo],
      };
    });
  }

  /* O que as opções escolhidas somam. Separado entre o que multiplica pela
     quantidade e o que entra uma vez só na venda. */
  const somaDasOpcoes = useMemo(() => {
    const soma = { porMedida: 0, porPeca: 0, umaVez: 0 };
    for (const atributo of atributos) {
      for (const rotulo of escolhas[atributo.chave] || []) {
        const opcao = atributo.opcoes.find((o) => o.rotulo === rotulo);
        if (!opcao) continue;
        if (opcao.unidade === "order") soma.umaVez += opcao.preco;
        else if (opcao.unidade === "piece") soma.porPeca += opcao.preco;
        else soma.porMedida += opcao.preco;
      }
    }
    return soma;
  }, [atributos, escolhas]);

  const somaDosServicosEscolhidos = useMemo(
    () => somaDosServicos(vitrine, servicosMarcados),
    [vitrine, servicosMarcados]
  );

  /* Tudo que entra por cima do preço da faixa. Serviço "por unidade" num
     produto por m² conta por peça: ninguém cobra revisão de arte por metro. */
  const adicionais = useMemo(
    () =>
      juntarAdicionais(somaDasOpcoes, {
        porMedida: 0,
        porPeca: somaDosServicosEscolhidos.porQuantidade,
        umaVez: somaDosServicosEscolhidos.umaVez,
      }),
    [somaDasOpcoes, somaDosServicosEscolhidos]
  );

  const area = useMemo(() => {
    const l = Number(String(largura).replace(",", ".")) || 0;
    const a = Number(String(altura).replace(",", ".")) || 0;
    return l > 0 && a > 0 ? l * a : 0;
  }, [largura, altura]);

  /* A combinação escolhida, quando a loja controla estoque por variação. */
  const variacaoCasada = useMemo(() => {
    if ((produto.inventory_scope || "product") !== "variant") return null;
    const escolhido: Record<string, string> = {};
    for (const [atributo, rotulos] of Object.entries(escolhas)) {
      const rotulo = rotulos[0];
      if (rotulo) escolhido[semCaixa(atributo)] = semCaixa(rotulo);
    }
    const chaves = Object.keys(escolhido).sort();
    if (!chaves.length) return null;
    for (const variacao of produto.product_variations || []) {
      const combinacao: Record<string, string> = {};
      for (const [atributo, rotulo] of Object.entries(variacao?.combination || {})) {
        combinacao[semCaixa(atributo)] = semCaixa(rotulo);
      }
      const dela = Object.keys(combinacao).sort();
      if (dela.length !== chaves.length) continue;
      if (chaves.every((chave) => combinacao[chave] === escolhido[chave])) return variacao;
    }
    return null;
  }, [produto.inventory_scope, produto.product_variations, escolhas]);

  /* O saldo que vale depende de onde a loja guarda o estoque: no produto, na
     opção escolhida ou na combinação. Olhar sempre `produto.stock` fazia o
     Vendor vender opção esgotada e barrar produto que tinha saldo na variação. */
  const saldoQueVale = useMemo(() => {
    if (!estoqueLimita) return Number.POSITIVE_INFINITY;
    if (esgotadoAntesDaResposta) return 0;
    if (ehEstoqueV2) {
      /* Ainda falta escolher cor/tamanho: o banco responde 0 porque não sabe
         a célula, e isso não é "esgotado" — o botão pede a escolha. Esgotado
         antes da escolha só quando a grade toda está zerada (o servidor já
         marcou estoque_v2_esgotado, migration 216). Com tudo escolhido e
         fora da grade, vale o 0 (escolhaForaDoEstoque). */
      if (estoqueV2?.motivo === "falta_escolher" && !escolhaCompletaParaOEstoque) return Number.POSITIVE_INFINITY;
      const disponivel = estoqueV2?.disponivel;
      return typeof disponivel === "number" ? Math.max(0, disponivel) : Number.POSITIVE_INFINITY;
    }
    const escopo = produto.inventory_scope || "product";
    if (escopo === "option") {
      const atributoDoEstoque = semCaixa(produto.inventory_attribute_name);
      const saldos = atributos
        .filter((atributo) => !atributoDoEstoque || semCaixa(atributo.nome) === atributoDoEstoque)
        .flatMap((atributo) =>
          (escolhas[atributo.chave] || [])
            .map((rotulo) => atributo.opcoes.find((opcao) => opcao.rotulo === rotulo)?.saldo)
            .filter((saldo): saldo is number => typeof saldo === "number")
        );
      if (saldos.length) return Math.max(0, Math.min(...saldos));
      return Math.max(0, Number(produto.stock) || 0);
    }
    if (escopo === "variant") {
      if (variacaoCasada) return Math.max(0, Number(variacaoCasada.stock) || 0);
      return Math.max(0, Number(produto.stock) || 0);
    }
    return Math.max(0, Number(produto.stock) || 0);
  }, [produto, atributos, escolhas, variacaoCasada, estoqueLimita, ehEstoqueV2, estoqueV2, esgotadoAntesDaResposta, escolhaCompletaParaOEstoque]);

  const medidasOk = !ehMetroQuadrado || area > 0;

  /* O pacote que a calculadora cota: o mesmo item que vai para o carrinho,
     com o peso e as medidas da embalagem do cadastro. */
  const itensDoFrete = useMemo(
    () => [
      {
        product_id: produto.id,
        name: produto.name,
        quantity: quantidade,
        unit_type: produto.unit_type || "unit",
        width_m: ehMetroQuadrado ? Number(String(largura).replace(",", ".")) || 0 : undefined,
        height_m: ehMetroQuadrado ? Number(String(altura).replace(",", ".")) || 0 : undefined,
        shipping_weight_kg: produto.shipping_weight_kg ?? null,
        shipping_width_cm: produto.shipping_width_cm ?? null,
        shipping_height_cm: produto.shipping_height_cm ?? null,
        shipping_length_cm: produto.shipping_length_cm ?? null,
        production_time_value: produto.production_time_value ?? null,
        production_time_unit: produto.production_time_unit ?? null,
        price_on_request: produto.price_on_request ?? false,
      },
    ],
    [produto, quantidade, ehMetroQuadrado, largura, altura]
  );

  /* O frete escolhido aqui vale para o carrinho, como no outro template:
     retirada vale sempre; cotação de transportadora só enquanto o pacote for
     o mesmo que foi cotado (mudou a quantidade ou a medida, cota de novo). */
  const freteValido = useMemo(() => {
    if (!freteEscolhido) return null;
    if (isPickupShippingOption(freteEscolhido.option)) return freteEscolhido;
    const pacote = buildShippingPackageFromItems(itensDoFrete);
    if (!pacote.ok || freteEscolhido.package_signature !== pacote.packageSignature) return null;
    return freteEscolhido;
  }, [freteEscolhido, itensDoFrete]);

  const escolherFrete = useCallback(
    (selecao: ShippingSelection | null) => {
      setFreteEscolhido(selecao);
      setShippingSelection(selecao);
    },
    [setShippingSelection]
  );

  /* As regras de venda do cadastro: mínimo, máximo e a lista de quantidades
     permitidas. O carrinho já barrava o que não obedece, mas só depois do
     clique — o cliente montava o pedido inteiro para ouvir não no fim. */
  const limites = useMemo(() => resolveSaleQuantityLimits(produto), [produto]);
  const usaLista = limites.allowedQuantities.length > 0;

  const conferirQuantidade = useCallback(
    (qtd: number) =>
      checkSaleQuantityLimits({
        min_sale_qty_enabled: produto.min_sale_qty_enabled,
        min_sale_qty: produto.min_sale_qty,
        max_sale_qty_enabled: produto.max_sale_qty_enabled,
        max_sale_qty: produto.max_sale_qty,
        allowed_sale_qty_enabled: produto.allowed_sale_qty_enabled,
        allowed_sale_quantities: produto.allowed_sale_quantities,
        effectiveQuantity: calculateEffectiveQuantity({
          quantity: qtd,
          unit_type: produto.unit_type || "unit",
          width_m: ehMetroQuadrado ? Number(String(largura).replace(",", ".")) || 0 : undefined,
          height_m: ehMetroQuadrado ? Number(String(altura).replace(",", ".")) || 0 : undefined,
        }),
      }),
    [produto, ehMetroQuadrado, largura, altura]
  );

  /* O estoque também se mede em área: 5 peças de 3,75 m² comem 18,75 m² do
     saldo, não 5. */
  const cabeNoEstoque = useCallback(
    (qtd: number) => {
      if (!Number.isFinite(saldoQueVale)) return true;
      /* No v2 o banco já respondeu em peças para a medida escolhida. */
      if (ehEstoqueV2) return qtd <= saldoQueVale + 0.000001;
      return (
        calculateEffectiveQuantity({
          quantity: qtd,
          unit_type: produto.unit_type || "unit",
          width_m: ehMetroQuadrado ? Number(String(largura).replace(",", ".")) || 0 : undefined,
          height_m: ehMetroQuadrado ? Number(String(altura).replace(",", ".")) || 0 : undefined,
        }) <=
        saldoQueVale + 0.000001
      );
    },
    [saldoQueVale, produto.unit_type, ehMetroQuadrado, largura, altura, ehEstoqueV2]
  );

  const conferencia = conferirQuantidade(quantidade);
  const passaDoEstoque = !cabeNoEstoque(quantidade);

  const avisoDaQuantidade = useMemo(() => {
    if (conferencia.isBelowMin && conferencia.minQty !== null) {
      return `Quantidade mínima: ${numeroDoVendor(conferencia.minQty)}.`;
    }
    if (conferencia.isAboveMax && conferencia.maxQty !== null) {
      return `Quantidade máxima: ${numeroDoVendor(conferencia.maxQty)}.`;
    }
    if (conferencia.isNotAllowedValue && conferencia.allowedQuantities.length > 0) {
      return `Quantidades permitidas: ${conferencia.allowedQuantities.map((v) => numeroDoVendor(v)).join(", ")}.`;
    }
    return "";
  }, [conferencia]);

  /* Com lista de quantidades permitidas, o mais e o menos pulam de valor em
     valor: somar 1 cairia num número que a loja não vende. */
  const vizinhaPermitida = (atual: number, direcao: -1 | 1) => {
    const lista = limites.allowedQuantities;
    if (!lista.length) return null;
    if (direcao < 0) {
      for (let i = lista.length - 1; i >= 0; i -= 1) if (lista[i] < atual) return lista[i];
      return null;
    }
    for (let i = 0; i < lista.length; i += 1) if (lista[i] > atual) return lista[i];
    return null;
  };

  const proximaAbaixo = usaLista ? vizinhaPermitida(quantidade, -1) : Math.max(quantidadeMinima, quantidade - 1);
  const proximaAcima = usaLista ? vizinhaPermitida(quantidade, 1) : quantidade + 1;
  const podeDiminuir =
    proximaAbaixo !== null &&
    proximaAbaixo > 0 &&
    (usaLista ? conferirQuantidade(proximaAbaixo).isValid : quantidade > quantidadeMinima);
  const podeAumentar =
    proximaAcima !== null && conferirQuantidade(proximaAcima).isValid && cabeNoEstoque(proximaAcima);

  /* A tabela de faixas de um produto por área fala em metros, não em peças:
     "de 1 a 4,99" ali é m². Chamar de "unidades" fazia o cliente ler peças. */
  const unidadeDaFaixa = (qtd: number) => {
    if (ehMetroQuadrado) return "m²";
    if (produto.unit_type === "meter" || produto.unit_type === "linear_meter") return "m";
    return qtd === 1 ? "unidade" : "unidades";
  };

  /* A quantidade que a faixa enxerga. Num produto por m², cinco peças de
     3,75 m² são 18,75 m² — e é esse número que decide a faixa, igual ao que
     o carrinho e o checkout já faziam. Contar peças fazia a página prometer
     um preço e o carrinho cobrar outro. */
  const quantidadeEfetiva = useMemo(
    () =>
      calculateEffectiveQuantity({
        quantity: quantidade,
        unit_type: produto.unit_type || "unit",
        width_m: ehMetroQuadrado ? Number(String(largura).replace(",", ".")) || 0 : undefined,
        height_m: ehMetroQuadrado ? Number(String(altura).replace(",", ".")) || 0 : undefined,
      }),
    [quantidade, produto.unit_type, ehMetroQuadrado, largura, altura]
  );

  /* A conta da linha inteira, a mesma do carrinho, do checkout e do admin:
     (faixa + adicionais por m²) × área total + adicionais por peça × peças
     + o que entra uma vez. "Preço apenas pela variação" tira o preço base
     da conta: cobrar os dois era vender pelo dobro. */
  /* O valor mínimo do item entra por último, e no produto por m² só depois
     das medidas: sem elas a quantidade conta como m². */
  const valorMinimo = valorMinimoDoItem(produto, "loja");
  const conta = useMemo(
    () =>
      precoDaLinha({
        precoBase: Number(produto.price) || 0,
        faixas,
        pecas: quantidade,
        quantidadeEfetiva,
        adicionais,
        soVariacao: produto.use_variation_price_only,
        sobConsulta: produto.price_on_request,
        valorMinimo: medidasOk ? valorMinimo : null,
      }),
    [produto.price, produto.use_variation_price_only, produto.price_on_request, faixas, quantidade, quantidadeEfetiva, adicionais, medidasOk, valorMinimo]
  );
  /* A etiqueta aparece sempre que o produto tem mínimo na loja; o cliente
     nunca vê quanto o tamanho "daria", só o preço final. */
  const mostraMinimo = valorMinimo !== null && !produto.price_on_request;
  /* O preço do metro quadrado (ou da unidade) na quantidade pedida, com os
     adicionais que também são por medida. */
  const precoDaMedida = conta.precoPorMedidaSemRateio;

  /* Qual faixa pegou e quanto ela abateu. O cliente precisa VER que o preço
     caiu porque ele levou mais — senão o desconto por quantidade não convence
     ninguém a subir a quantidade. */
  const faixaAplicada = conta.faixa;
  const descontoAplicado = produto.use_variation_price_only
    ? 0
    : calculateTierDiscountPct(Number(produto.price) || 0, conta.precoDaFaixa);
  /* E a melhor oferta da tabela, para quem ainda está na quantidade 1. */
  const melhorOferta = useMemo(
    () => getBestTierDiscount(Number(produto.price) || 0, faixas),
    [produto.price, faixas]
  );

  /* A escada de preço do produto medido, só de leitura: a faixa vem da área
     total do pedido, então não há o que "escolher" numa tabela. */
  const porMedida = vendidoPorMedida(produto.unit_type);
  const medida = nomeDaMedida(produto.unit_type);
  const numeroEnxuto = (n: number) => numeroDoVendor(n, Number.isInteger(n) ? 0 : 2);
  const faixasOrdenadas = useMemo(
    () => [...faixas].sort((a, b) => (Number(a.min_qty) || 0) - (Number(b.min_qty) || 0)),
    [faixas]
  );
  const rotuloDaFaixa = (faixa: ProductPriceTier, primeira: boolean) => {
    const de = Number(faixa.min_qty) || 0;
    const ate = faixa.max_qty === null || faixa.max_qty === undefined ? null : Number(faixa.max_qty);
    if (ate === null) return `${numeroEnxuto(de)} ${medida} ou mais`;
    if (primeira && de <= 1) return `Até ${numeroEnxuto(ate)} ${medida}`;
    return `De ${numeroEnxuto(de)} a ${numeroEnxuto(ate)} ${medida}`;
  };
  /* O empurrão: quantas peças faltam para a próxima faixa. */
  const dicaDaProximaFaixa = useMemo(() => {
    if (!porMedida || produto.use_variation_price_only) return "";
    const proxima = faixasOrdenadas.find((faixa) => (Number(faixa.min_qty) || 0) > quantidadeEfetiva + 0.000001);
    if (!proxima) return "";
    const precoLa = (Number(proxima.unit_price) || 0) + adicionais.porMedida;
    if (precoLa >= precoDaMedida) return "";
    const inicio = Number(proxima.min_qty) || 0;
    if (!(area > 0)) return `A partir de ${numeroEnxuto(inicio)} ${medida}, cada ${medida} sai por ${moedaDoVendor(precoLa)}.`;
    const pecasNecessarias = Math.ceil((inicio - 0.000001) / area);
    if (pecasNecessarias <= quantidade) return "";
    return `Leve ${numeroEnxuto(pecasNecessarias)} ${pecasNecessarias === 1 ? "peça" : "peças"} (${numeroDoVendor(pecasNecessarias * area, 2)} ${medida}) e cada ${medida} cai para ${moedaDoVendor(precoLa)}.`;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [porMedida, produto.use_variation_price_only, faixasOrdenadas, quantidadeEfetiva, adicionais.porMedida, precoDaMedida, area, quantidade, medida]);

  /* O que sai uma peça: no produto por m², o preço do metro vezes a área,
     mais o que é cobrado por peça. */
  const precoUnitario = useMemo(
    () => precoDaMedida * (ehMetroQuadrado ? Math.max(area, 0) : 1) + adicionais.porPeca,
    [precoDaMedida, ehMetroQuadrado, area, adicionais.porPeca]
  );

  /* O que os produtos do "compre junto" somam ao pedido. */
  const somaDosCombos = useMemo(
    () =>
      combosEscolhidos.reduce(
        (soma, escolha) => soma + (Number(escolha.discountedPrice) || 0) * (Number(escolha.quantity) || 0),
        0
      ),
    [combosEscolhidos]
  );

  /* Sem medida ainda, o produto por m² não tem preço: só o que entra uma vez. */
  const total = useMemo(
    () => (ehMetroQuadrado && !(area > 0) ? adicionais.umaVez : conta.total) + somaDosCombos,
    [ehMetroQuadrado, area, adicionais.umaVez, conta.total, somaDosCombos]
  );

  /* A entrega escolhida entra no total da página, com o nome dela embaixo:
     o número que o cliente vê aqui é o mesmo que o carrinho vai mostrar. */
  const entregaNoTotal = !semPreco && freteValido ? Math.max(0, Number(freteValido.option.price) || 0) : null;
  const totalNaTela = total + (entregaNoTotal ?? 0);

  const faltaEscolher = useMemo(
    () => atributos.filter((a) => a.obrigatorio && !(escolhas[a.chave] || []).length),
    [atributos, escolhas]
  );
  const faltaPreencher = useMemo(
    () => camposAtivos.filter((c) => c.required && !String(campos[String(c.id || c.title)] || "").trim()),
    [camposAtivos, campos]
  );

  const semEstoque = estoqueLimita && saldoQueVale <= 0;

  /* O que a tela do estoque pode dizer, pela regra da loja: o número exato,
     só o aviso de que está acabando, ou nada. O bloqueio por falta de
     estoque continua em qualquer caso. */
  const estoqueVisivel = visao.regras.estoqueVisivel;
  const ultimasUnidades =
    estoqueVisivel === "low_only" &&
    estoqueLimita &&
    Number.isFinite(saldoQueVale) &&
    saldoQueVale > 0 &&
    saldoQueVale <= visao.regras.limiteEstoqueBaixo;

  /* Produto sob consulta no carrinho comum, com preço visível: não há o que
     somar, então o botão fica parado e a conversa vai para o WhatsApp. Nos
     outros casos (lista de orçamento, preço escondido pela loja) o item
     entra normalmente e a loja põe o valor depois. */
  const sobConsultaSemCarrinho = semPreco && visao.modoDeCompra === "cart" && visao.mostrarPreco;

  /* Produto esgotado que, pela regra da loja, vira pedido de orçamento: o
     botão fica ativo e abre o WhatsApp em vez de travar em "Indisponível". */
  const esgotadoVirouOrcamento =
    visao.regras.esgotado === "quote" && semEstoque && visao.podeComprar && !visao.precisaEntrar;

  /* O produto está pronto para ir ao carrinho: escolhas feitas, quantidade
     dentro das regras, estoque que dá. */
  const prontoParaAdicionar =
    !semEstoque &&
    !escolhaForaDoEstoque &&
    !passaDoEstoque &&
    conferencia.isValid &&
    !faltaEscolher.length &&
    !faltaPreencher.length &&
    !servicoPendente &&
    medidasOk;

  /* Preço só para quem entrou: o botão fica ativo para levar ao login. */
  const podeComprar = visao.precisaEntrar || esgotadoVirouOrcamento
    ? true
    : visao.podeComprar && !sobConsultaSemCarrinho && prontoParaAdicionar;

  const rotuloDoBotao = visao.precisaEntrar
    ? visao.regras.textoPrecoLogin
    : !visao.podeComprar
      ? visao.rotuloDoBotao || visao.motivoDoBloqueio || "Indisponível"
      : sobConsultaSemCarrinho
        ? textoSemPreco
        : esgotadoVirouOrcamento
          ? "Pedir orçamento"
        : semEstoque || escolhaForaDoEstoque
          ? "Indisponível"
          : passaDoEstoque
            ? "Quantidade acima do estoque"
            : avisoDaQuantidade
              ? "Ajuste a quantidade"
              : faltaEscolher.length
                ? `Escolha: ${faltaEscolher[0].nome}`
                : servicoPendente
                  ? `Escolha: ${servicoPendente.grupo.nome}`
                  : !medidasOk
                    ? "Informe as medidas"
                    : faltaPreencher.length
                      ? "Preencha os campos"
                      : visao.rotuloDoBotao || "Adicionar ao carrinho";

  /* O rótulo curto da barra fixa do celular. */
  const rotuloCurto = visao.precisaEntrar
    ? visao.regras.textoPrecoLogin
    : esgotadoVirouOrcamento
      ? "Pedir orçamento"
      : visao.rotuloDoBotao || "Adicionar";
  const rotuloDeAdicionado = visao.modoDeCompra === "quote" ? "Adicionado à lista" : "Adicionado ao carrinho";
  /* Só vitrine: a página mostra o produto e nada mais. */
  const soVitrine = visao.modoDeCompra === "showcase";

  /* As escolhas em texto, como vão para o carrinho e para a mensagem do
     WhatsApp: "Cor: Azul / Prazo: Expressa / Texto: Parabéns". */
  const descricaoDasEscolhas = () => {
    const servicosEscolhidos = escolhidosComGrupo(vitrine, servicosMarcados);
    return [
      ...atributos
        .filter((a) => (escolhas[a.chave] || []).length)
        .map((a) => `${a.nome}: ${(escolhas[a.chave] || []).join(", ")}`),
      ...servicosEscolhidos.map(({ servico, grupo }) => `${grupo.nome}: ${servico.titulo}`),
      ...camposAtivos
        .filter((c) => String(campos[String(c.id || c.title)] || "").trim())
        .map((c) => `${c.title}: ${campos[String(c.id || c.title)]}`),
    ].join(" / ");
  };

  /* A dúvida sobre um produto sob consulta e o pedido no modo WhatsApp vão
     para o mesmo número, cada um com a sua frase. */
  const linkDoWhats = (texto: string) =>
    digitosDoWhats ? `https://wa.me/${digitosDoWhats}?text=${encodeURIComponent(texto)}` : null;
  /* A frase da dúvida vem do modelo que a loja escreveu em WhatsApp >
     Mensagens; sem modelo, fica a frase de sempre do tema. */
  const linkDeDuvida = linkDoWhats(
    visao.regras.mensagensDoWhatsapp.produto
      ? preencherModelo(visao.regras.mensagensDoWhatsapp.produto, { nome_produto: produto.name })
      : `Olá! Quero tirar dúvidas sobre o produto ${produto.name}.`
  );

  function adicionar() {
    /* As regras da loja vêm antes de tudo. */
    if (visao.precisaEntrar) {
      router.push(`/${slug}/cliente/login?next=${encodeURIComponent(caminho || `/${slug}/p/${produto.id}`)}`);
      return;
    }
    if (!visao.podeComprar || sobConsultaSemCarrinho) return;
    if (esgotadoVirouOrcamento) {
      const link = linkDoWhats(
        `Olá! O produto ${produto.name} está sem estoque na loja. Vocês conseguem fazer sob encomenda? Quero um orçamento.`
      );
      if (!link) {
        setAviso("A loja ainda não configurou o WhatsApp.");
        return;
      }
      window.open(link, "_blank", "noopener");
      return;
    }
    if (!podeComprar) {
      setAviso(rotuloDoBotao);
      return;
    }

    /* Pedido pelo WhatsApp: nada vai ao carrinho; a conversa abre com o
       produto, as escolhas e a quantidade. */
    if (visao.modoDeCompra === "whatsapp") {
      if (!digitosDoWhats) {
        setAviso("A loja ainda não configurou o WhatsApp.");
        return;
      }
      const escolhidas = descricaoDasEscolhas();
      const medidas = ehMetroQuadrado && area > 0 ? ` de ${largura}m x ${altura}m` : "";
      const texto = `Olá! Tenho interesse no produto ${produto.name}${escolhidas ? ` (${escolhidas})` : ""}, ${numeroDoVendor(quantidade)} ${
        quantidade === 1 ? (ehMetroQuadrado ? "peça" : "unidade") : ehMetroQuadrado ? "peças" : "unidades"
      }${medidas}.`;
      window.open(linkDoWhats(texto) || "", "_blank", "noopener");
      return;
    }

    const opcoesDoPedido = atributos.flatMap((atributo) =>
      (escolhas[atributo.chave] || []).map((rotulo) => {
        const opcao = atributo.opcoes.find((o) => o.rotulo === rotulo);
        return {
          option_id: opcao?.id ?? null,
          attribute_name: atributo.chave,
          label: rotulo,
          price: opcao?.preco ?? 0,
          price_applies_per_quantity: opcao?.porQuantidade ?? true,
          price_unit: opcao?.unidade ?? (vendidoPorMedida(produto.unit_type) ? "measure" : "piece"),
          affects_stock: false,
        };
      })
    );

    const servicosEscolhidos = escolhidosComGrupo(vitrine, servicosMarcados);
    const descricao = descricaoDasEscolhas();

    const temEscolha = opcoesDoPedido.length > 0 || servicosEscolhidos.length > 0 || descricao.length > 0;

    const resultado = addToCart(
      {
        id: produto.id,
        name: produto.name,
        price: Number(produto.price) || 0,
        /* O saldo que vale para a escolha (a combinacao, a opcao ou o
           produto): o carrinho conferia o total do produto. Sem limite
           (estoque negativo permitido ou sem controle), vai sem saldo. */
        stock: Number.isFinite(saldoQueVale) ? saldoQueVale : null,
        /* Estoque v2: o carrinho confere de novo no servidor (soma por
           célula). Por área o teto local em m² não vale: só o servidor. */
        ...(ehEstoqueV2 ? { estoque_v2: true } : {}),
        user_id: produto.user_id,
        /* O carrinho mostra a miniatura por este campo; sem ele o item sai "sem foto". */
        image_url:
          (produto as { image_url?: string | null }).image_url ||
          produto.product_images?.find((imagem) => imagem?.image_url)?.image_url ||
          undefined,
        unit_type: produto.unit_type || "unit",
        track_inventory: ehEstoqueV2 ? estoqueLimita && !ehMetroQuadrado : produto.track_inventory ?? undefined,
        allow_negative_stock: ehEstoqueV2 ? !estoqueLimita : produto.allow_negative_stock ?? undefined,
        price_on_request: produto.price_on_request ?? undefined,
        use_variation_price_only: produto.use_variation_price_only ?? undefined,
        valor_minimo_item: produto.valor_minimo_item ?? null,
        valor_minimo_loja: produto.valor_minimo_loja ?? null,
        product_price_tiers: faixas,
        min_sale_qty_enabled: produto.min_sale_qty_enabled ?? undefined,
        min_sale_qty: produto.min_sale_qty ?? undefined,
        max_sale_qty_enabled: produto.max_sale_qty_enabled ?? undefined,
        max_sale_qty: produto.max_sale_qty ?? undefined,
        allowed_sale_qty_enabled: produto.allowed_sale_qty_enabled ?? undefined,
        allowed_sale_quantities: produto.allowed_sale_quantities ?? undefined,
        /* Sem peso e medidas o carrinho não tinha como cotar a transportadora:
           a finalização pedia "peso e medidas" de um produto que tem os dois. */
        shipping_weight_kg: produto.shipping_weight_kg ?? null,
        shipping_width_cm: produto.shipping_width_cm ?? null,
        shipping_height_cm: produto.shipping_height_cm ?? null,
        shipping_length_cm: produto.shipping_length_cm ?? null,
        production_time_value: produto.production_time_value ?? null,
        production_time_unit: produto.production_time_unit ?? null,
      } as never,
      quantidade,
      ehMetroQuadrado
        ? {
            width_m: Number(String(largura).replace(",", ".")) || 0,
            height_m: Number(String(altura).replace(",", ".")) || 0,
          }
        : undefined,
      temEscolha
        ? {
            id: "custom-options",
            description: descricao,
            selected_options: {
              inventory_scope: "product",
              inventory_attribute_name: null,
              options: [
                ...opcoesDoPedido,
                ...servicosEscolhidos.map(({ servico, grupo }) => servicoComoOpcao(servico, grupo.nome)),
              ],
              custom_fields: camposAtivos
                .filter((c) => String(campos[String(c.id || c.title)] || "").trim())
                .map((c) => ({ title: c.title, value: campos[String(c.id || c.title)] })),
            },
          }
        : undefined
    );

    /* Os combos entram DEPOIS do item principal e presos a ele: é o
       `combo_parent_id` que faz o carrinho tratar os dois como um pedido só,
       e é por isso que a seleção do frete só pode ser gravada no fim. */
    if (resultado.ok && combosEscolhidos.length) {
      let algumFalhou = false;
      for (const escolha of combosEscolhidos) {
        const configuracao = (escolha.configuration || {}) as {
          unit_type?: string;
          width_m?: number;
          height_m?: number;
          variation?: unknown;
        };
        const retorno = addToCart(
          {
            id: escolha.combo.combo_product.id,
            name: escolha.combo.combo_product.name,
            price: Number(escolha.combo.combo_product.price) || 0,
            image_url: escolha.combo.combo_product.image_url,
            user_id: produto.user_id,
            /* Com o preço escondido pela loja o sugerido vai "sob consulta",
               igual ao item principal: o carrinho não inventa R$ 0. */
            price_on_request: Boolean(escolha.combo.combo_product.price_on_request),
            track_inventory: false,
            unit_type: configuracao.unit_type || "unit",
            shipping_weight_kg: escolha.combo.combo_product.shipping_weight_kg ?? null,
            shipping_width_cm: escolha.combo.combo_product.shipping_width_cm ?? null,
            shipping_height_cm: escolha.combo.combo_product.shipping_height_cm ?? null,
            shipping_length_cm: escolha.combo.combo_product.shipping_length_cm ?? null,
          } as never,
          escolha.quantity,
          {
            price_override: escolha.discountedPrice,
            is_combo_item: true,
            combo_parent_id: resultado.cart_item_id,
            width_m: configuracao.width_m,
            height_m: configuracao.height_m,
          },
          configuracao.variation as never
        );
        if (!retorno.ok) algumFalhou = true;
      }
      if (algumFalhou) setAviso("Alguns itens do compre junto não puderam ser adicionados.");
    }

    /* O frete escolhido nesta página entra no carrinho no fim, depois dos
       combos, como diz o comentário acima: antes ele nunca era gravado e o
       carrinho abria sem entrega, como se o cliente não tivesse escolhido. */
    if (resultado.ok && freteValido) setShippingSelection(freteValido);

    setAdicionado(true);
    window.setTimeout(() => setAdicionado(false), 2200);
  }

  return (
    <div
      className="space-y-4 border bg-[var(--vd-surface)] p-4"
      style={{ borderRadius: "var(--vd-radius-xl)", borderColor: "var(--vd-border)" }}
    >
      <h2 className="text-lg font-semibold">Escolha as opções</h2>

      {/* medidas, quando o produto é vendido por metro quadrado */}
      {ehMetroQuadrado ? (
        <div className="space-y-1.5">
          <p className="text-xs font-medium text-[var(--vd-muted-fg)]">Medidas em metros</p>
          <div className="grid grid-cols-2 gap-2">
            <input
              inputMode="numeric"
              value={largura}
              onKeyDown={(e) => {
                if (e.key !== "Backspace") return;
                if (e.currentTarget.selectionStart !== e.currentTarget.selectionEnd) return;
                e.preventDefault();
                setDigitosDaLargura((d) => d.slice(0, -1));
              }}
              onChange={(e) => setDigitosDaLargura(e.target.value.replace(/\D/g, ""))}
              placeholder="Largura"
              className="h-10 w-full border px-3 text-sm outline-none"
              style={{
                borderRadius: "var(--vd-radius-xl)",
                borderColor: "var(--vd-border)",
                backgroundColor: "var(--vd-muted-bg)",
              }}
            />
            <input
              inputMode="numeric"
              value={altura}
              onKeyDown={(e) => {
                if (e.key !== "Backspace") return;
                if (e.currentTarget.selectionStart !== e.currentTarget.selectionEnd) return;
                e.preventDefault();
                setDigitosDaAltura((d) => d.slice(0, -1));
              }}
              onChange={(e) => setDigitosDaAltura(e.target.value.replace(/\D/g, ""))}
              placeholder="Altura"
              className="h-10 w-full border px-3 text-sm outline-none"
              style={{
                borderRadius: "var(--vd-radius-xl)",
                borderColor: "var(--vd-border)",
                backgroundColor: "var(--vd-muted-bg)",
              }}
            />
          </div>
          {area > 0 ? (
            <p className="text-[11px] text-[var(--vd-muted-fg)]">
              Área: <strong className="tabular-nums">{numeroDoVendor(area, 2)} m²</strong>
              {quantidade > 1 ? (
                <>
                  {" · "}
                  <strong className="tabular-nums">{numeroDoVendor(area * quantidade, 2)} m²</strong> no total
                </>
              ) : null}
            </p>
          ) : null}
        </div>
      ) : null}

      {/* As variações usam o mesmo mecanismo do resto do sistema: bolinha de
          cor, miniatura, botão, lista com busca ou menu, conforme o que o
          lojista escolheu em cada atributo — e recolhendo em chip quando a
          escolha está feita. Duplicar isso aqui seria manter duas verdades
          sobre o mesmo cadastro. */}
      {produto.product_attributes?.length ? (
        <SeletorDeVariacoes
          atributos={produto.product_attributes as never}
          selecionadas={escolhas}
          aoEscolher={(nome, rotulo, multipla) => {
            const atributo = atributos.find((a) => a.chave === nome);
            if (atributo) escolher(atributo, rotulo);
            else {
              setAviso("");
              setEscolhas((antes) => {
                const atuais = antes[nome] || [];
                if (!multipla) return { ...antes, [nome]: [rotulo] };
                return {
                  ...antes,
                  [nome]: atuais.includes(rotulo) ? atuais.filter((r) => r !== rotulo) : [...atuais, rotulo],
                };
              });
            }
          }}
          corDaLoja={corDaMarca}
          corDoTextoNaLoja={textoSobreACor(corDaMarca)}
          corDaLojaNoBranco={corDaMarca}
          formatarDinheiro={moedaDoVendor}
          unidadeDeVenda={produto.unit_type || "unit"}
          imagens={
            (produto.product_images || [])
              .filter((imagem) => imagem?.id && imagem?.image_url)
              .map((imagem) => ({ id: String(imagem.id), image_url: String(imagem.image_url) })) as never
          }
          atributoDeEstoque={
            estoqueLimita && produto.inventory_scope === "option" ? produto.inventory_attribute_name || null : null
          }
        />
      ) : null}

      {/* A escada do produto medido: o preço do m² por faixa de área, só de
          leitura. Quem manda na faixa é a área total do pedido; o que a
          pessoa escolhe é quantas peças leva, no contador lá embaixo.
          Sem preço para este visitante, nem a escada nem a tabela aparecem. */}
      {faixas.length && porMedida && !semPreco ? (
        <div className="space-y-1.5">
          <p className="text-sm font-medium">Desconto progressivo por {medida}</p>
          <p className="text-[11px] text-[var(--vd-muted-fg)]">
            Quanto mais {medida} no pedido, mais barato sai cada {medida}.
          </p>
          <div
            className="max-w-lg overflow-hidden border"
            style={{ borderRadius: "var(--vd-radius-lg)", borderColor: "var(--vd-border)" }}
          >
            {faixasOrdenadas.map((faixa, indice) => {
              const marcada = Boolean(faixaAplicada) && Number(faixaAplicada?.min_qty) === Number(faixa.min_qty);
              return (
                <div
                  key={`${faixa.min_qty}-${faixa.unit_price}`}
                  className={`flex items-center justify-between gap-3 px-3 py-2 text-[12px] sm:text-[13px] ${indice > 0 ? "border-t" : ""}`}
                  style={{
                    borderColor: "var(--vd-border)",
                    backgroundColor: marcada ? "color-mix(in srgb, var(--vd-primary) 8%, transparent)" : undefined,
                  }}
                >
                  <span className={marcada ? "font-semibold" : "text-[var(--vd-muted-fg)]"}>
                    {rotuloDaFaixa(faixa, indice === 0)}
                    {marcada ? (
                      <span className="ml-1.5 text-[10px] font-semibold uppercase" style={{ color: "var(--vd-primary)" }}>
                        seu pedido
                      </span>
                    ) : null}
                  </span>
                  <span className="font-semibold tabular-nums">
                    {moedaDoVendor((Number(faixa.unit_price) || 0) + adicionais.porMedida)}/{medida}
                  </span>
                </div>
              );
            })}
          </div>
          {dicaDaProximaFaixa ? (
            <p className="text-[11px] font-medium" style={{ color: "#047857" }}>{dicaDaProximaFaixa}</p>
          ) : null}
        </div>
      ) : null}

      {/* a tabela de quantidade: o jeito da gráfica de mostrar preço */}
      {faixas.length && !porMedida && !semPreco ? (
        <div className="space-y-1.5">
          <p className="text-sm font-medium">Escolha a quantidade:</p>
          <div
            className="max-w-lg overflow-hidden border"
            style={{ borderRadius: "var(--vd-radius-lg)", borderColor: "var(--vd-border)" }}
          >
            <table className="w-full border-collapse text-[11px] sm:text-[13px]">
              <thead style={{ backgroundColor: "var(--vd-muted-bg)", color: "var(--vd-muted-fg)" }}>
                <tr>
                  <th className="whitespace-nowrap px-2 py-2 text-left sm:px-3">QUANTIDADE</th>
                  <th className="whitespace-nowrap border-l px-2 py-2 text-left sm:px-3" style={{ borderColor: "var(--vd-border)" }}>
                    VALOR POR UNIDADE
                  </th>
                  <th className="whitespace-nowrap border-l px-2 py-2 text-left sm:px-3" style={{ borderColor: "var(--vd-border)" }}>
                    VALOR TOTAL
                  </th>
                </tr>
              </thead>
              <tbody>
                {faixas.map((faixa) => {
                  const qtd = Number(faixa.min_qty) || 1;
                  const unitario =
                    (Number(faixa.unit_price) || 0) + somaDasOpcoes.porMedida + somaDasOpcoes.porPeca + somaDosServicosEscolhidos.porQuantidade;
                  const marcada = quantidade === qtd;
                  return (
                    <tr
                      key={`${faixa.min_qty}-${faixa.unit_price}`}
                      onClick={() => {
                        setQuantidade(qtd);
                        setQuantidadeEscolhida(true);
                      }}
                      className="cursor-pointer border-t transition-colors"
                      style={{
                        borderColor: "var(--vd-border)",
                        backgroundColor: marcada ? "color-mix(in srgb, var(--vd-primary) 8%, transparent)" : undefined,
                      }}
                    >
                      <td className="whitespace-nowrap px-2 py-2.5 sm:px-3">
                        <label className="flex cursor-pointer items-center gap-2">
                          <input
                            type="radio"
                            checked={marcada}
                            onChange={() => {
                              setQuantidade(qtd);
                              setQuantidadeEscolhida(true);
                            }}
                            style={{ accentColor: "var(--vd-primary)" }}
                          />
                          {numeroDoVendor(qtd)} {unidadeDaFaixa(qtd)}
                        </label>
                      </td>
                      <td
                        className="whitespace-nowrap border-l px-2 py-2.5 tabular-nums text-[var(--vd-muted-fg)] sm:px-3"
                        style={{ borderColor: "var(--vd-border)" }}
                      >
                        {moedaDoVendor(unitario)}
                      </td>
                      <td
                        className="whitespace-nowrap border-l px-2 py-2.5 font-medium tabular-nums sm:px-3"
                        style={{ borderColor: "var(--vd-border)" }}
                      >
                        {moedaDoVendor(unitario * qtd + somaDasOpcoes.umaVez + somaDosServicosEscolhidos.umaVez)}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>
      ) : null}

      {/* os serviços da loja */}
      {vitrine.length ? (
        <div className="space-y-3">
          {vitrine.map((caixa) => (
            <div key={caixa.grupo.id || "servicos"} className="space-y-1.5">
              <p className="text-xs font-medium text-[var(--vd-muted-fg)]">
                {caixa.grupo.nome}
                {caixa.grupo.obrigatorio ? (
                  <span className="ml-1.5 text-[10.5px]" style={{ color: "var(--vd-primary)" }}>
                    {caixa.grupo.modo === "uma"
                      ? "selecione uma opção (obrigatório)"
                      : "selecione uma ou mais opções (obrigatório)"}
                  </span>
                ) : null}
              </p>
              <div className="grid gap-2 sm:grid-cols-2">
                {caixa.servicos.map((servico) => {
                  const marcado = servicosMarcados.includes(servico.id);
                  return (
                    <button
                      key={servico.id}
                      type="button"
                      onClick={() => {
                        setAviso("");
                        setServicosMarcados((antes) => alternarEscolha(antes, servico, caixa));
                      }}
                      /* Sem `overflow-hidden`: o balão de ajuda sobe para fora do cartão, e
                         cortado ele não serve para nada. */
                      className="relative border py-2 pl-3 pr-2 text-left transition-all"
                      style={{
                        borderRadius: "var(--vd-radius-lg)",
                        borderColor: marcado ? "var(--vd-primary)" : "var(--vd-border)",
                        backgroundColor: marcado
                          ? "color-mix(in srgb, var(--vd-primary) 6%, transparent)"
                          : "var(--vd-surface)",
                      }}
                    >
                      <span className="flex items-start gap-2.5">
                        <span
                          className="flex h-8 w-8 shrink-0 items-center justify-center"
                          style={{
                            borderRadius: "var(--vd-radius-md)",
                            backgroundColor: marcado
                              ? "color-mix(in srgb, var(--vd-primary) 16%, transparent)"
                              : "var(--vd-muted-bg)",
                          }}
                        >
                          {/* eslint-disable-next-line @next/next/no-img-element */}
                          <img
                            src={`/icones/servicos/${servico.icone}.svg`}
                            alt=""
                            width={20}
                            height={20}
                            style={{ width: 20, height: 20 }}
                          />
                        </span>
                        <span className="min-w-0 flex-1">
                          <span className="flex items-center gap-1">
                            <span className="truncate text-[13px] font-semibold">{servico.titulo}</span>
                            {servico.detalhes ? <AjudaDoServico texto={servico.detalhes} /> : null}
                          </span>
                          {servico.descricao ? (
                            <span className="mt-0.5 block text-[11px] leading-snug text-[var(--vd-muted-fg)]">
                              {servico.descricao}
                            </span>
                          ) : null}
                          {/* Sem preço para este visitante o serviço aparece
                              sem valor nenhum: nem "Grátis" (que seria
                              mentira) nem o preço. */}
                          {semPreco ? null : (
                            <span className="mt-1 block text-[13px] font-bold" style={{ color: "var(--vd-primary)" }}>
                              {servico.preco > 0 ? moedaDoVendor(servico.preco) : "Grátis"}
                              {servico.preco > 0 ? (
                                <span className="ml-1 text-[10px] font-medium text-[var(--vd-muted-fg)]">
                                  {servico.porPeca ? (porMedida ? "por peça" : "por unidade") : "no pedido"}
                                </span>
                              ) : null}
                            </span>
                          )}
                        </span>
                      </span>
                    </button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
      ) : null}

      {/* os campos que a loja pede */}
      {camposAtivos.length ? (
        <div className="space-y-3">
          {camposAtivos.map((campo) => {
            const chave = String(campo.id || campo.title);
            return (
              <div key={chave} className="space-y-1.5">
                <p className="text-xs font-medium text-[var(--vd-muted-fg)]">
                  {campo.title}
                  {campo.required ? <span style={{ color: "var(--vd-primary)" }}> *</span> : null}
                </p>
                {campo.field_type === "textarea" ? (
                  <textarea
                    rows={3}
                    value={campos[chave] || ""}
                    placeholder={campo.placeholder || ""}
                    onChange={(e) => setCampos((antes) => ({ ...antes, [chave]: e.target.value }))}
                    className="w-full border px-3 py-2 text-sm outline-none"
                    style={{
                      borderRadius: "var(--vd-radius-lg)",
                      borderColor: "var(--vd-border)",
                      backgroundColor: "var(--vd-muted-bg)",
                    }}
                  />
                ) : (
                  <input
                    type={campo.field_type === "number" ? "number" : "text"}
                    value={campos[chave] || ""}
                    placeholder={campo.placeholder || ""}
                    onChange={(e) => setCampos((antes) => ({ ...antes, [chave]: e.target.value }))}
                    className="h-10 w-full border px-3 text-sm outline-none"
                    style={{
                      borderRadius: "var(--vd-radius-xl)",
                      borderColor: "var(--vd-border)",
                      backgroundColor: "var(--vd-muted-bg)",
                    }}
                  />
                )}
              </div>
            );
          })}
        </div>
      ) : null}

      {combos?.length ? (
        <VendorCompreJunto combos={combos} slug={slug} corDaMarca={corDaMarca} aoEscolher={setCombosEscolhidos} />
      ) : null}

      {/* total, quantidade e o botão */}
      <div className="space-y-2 border-t pt-4" style={{ borderColor: "var(--vd-border)" }}>
        <p className="text-xs font-medium text-[var(--vd-muted-fg)]">{entregaNoTotal !== null ? "Total com entrega" : "Total"}</p>
        {/* O valor e o contador dividem a linha; os avisos ficam embaixo, na
            largura toda. Espremidos ao lado do contador, no celular cada frase
            quebrava em três linhas. */}
        <div className="flex items-center justify-between gap-4">
          <span className={semPreco && textoSemPreco.length > 14 ? "text-lg font-bold leading-snug" : "text-2xl font-bold tabular-nums"}>
            {/* O total aparece desde o primeiro segundo, com a quantidade
                que já está no campo. Esperar um clique na tabela deixava a
                página sem número nenhum. Sem preço, o texto da loja. */}
            {semPreco ? textoSemPreco : moedaDoVendor(totalNaTela)}
          </span>

          <div
            className="inline-flex shrink-0 items-center border"
            style={{ borderRadius: "var(--vd-radius-md)", borderColor: "var(--vd-border)" }}
          >
            <button
              type="button"
              aria-label="Diminuir"
              disabled={!podeDiminuir}
              onClick={() => {
                if (proximaAbaixo === null || !podeDiminuir) return;
                setQuantidade(proximaAbaixo);
                setQuantidadeEscolhida(true);
              }}
              className="inline-flex h-10 w-10 items-center justify-center text-base font-medium transition-colors hover:bg-[var(--vd-muted-bg)] disabled:cursor-not-allowed disabled:opacity-40"
            >
              <Minus className="h-4 w-4" />
            </button>
            <input
              className="vd-numero w-12 border-0 bg-transparent text-center font-semibold outline-none"
              value={quantidade}
              inputMode="numeric"
              onChange={(e) => {
                const valor = Number(String(e.target.value).replace(/\D/g, ""));
                setQuantidade(Number.isFinite(valor) && valor > 0 ? valor : quantidadeMinima);
              }}
            />
            <button
              type="button"
              aria-label="Aumentar"
              disabled={!podeAumentar}
              onClick={() => {
                if (proximaAcima === null || !podeAumentar) return;
                setQuantidade(proximaAcima);
                setQuantidadeEscolhida(true);
              }}
              className="inline-flex h-10 w-10 items-center justify-center text-base font-medium transition-colors hover:bg-[var(--vd-muted-bg)] disabled:cursor-not-allowed disabled:opacity-40"
            >
              <Plus className="h-4 w-4" />
            </button>
          </div>
        </div>

        {/* Os avisos, um abaixo do outro, na largura toda. */}
        <div className="space-y-1">
          {/* O valor mínimo do item: antes de adicionar, para não haver surpresa. */}
          {mostraMinimo && !semPreco && valorMinimo !== null ? (
            <p>
              <VendorValorMinimo minimo={valorMinimo} />
            </p>
          ) : null}
          {/* A conta do total quando a entrega já está nele: produto + a
              entrega escolhida, com o nome dela. */}
          {entregaNoTotal !== null && freteValido ? (
            <p className="text-xs text-[var(--vd-muted-fg)]">
              {`Produto ${moedaDoVendor(total)} · ${freteValido.option.service || (isPickupShippingOption(freteValido.option) ? "Retirada" : "Entrega")} ${
                entregaNoTotal > 0 ? moedaDoVendor(entregaNoTotal) : "grátis"
              }`}
            </p>
          ) : null}

          {/* O selo diz até onde o preço pode cair; a linha diz em que faixa a
              compra está agora. Os dois convivem, como nos outros temas. */}
          {melhorOferta && !semPreco ? (
            <p>
              <span
                className="inline-flex items-center rounded-full px-2.5 py-0.5 text-[11px] font-semibold"
                style={{ backgroundColor: "rgba(4, 120, 87, 0.10)", color: "#047857" }}
              >
                {numeroDoVendor(melhorOferta.discountPct, melhorOferta.discountPct >= 10 ? 0 : 1)}% OFF a partir de{" "}
                {numeroDoVendor(Number(melhorOferta.tier.min_qty) || 0)}{" "}
                {unidadeDaFaixa(Number(melhorOferta.tier.min_qty) || 0)}
              </span>
            </p>
          ) : null}

          {/* Sem desconto ainda, a frase antiga ("faixa de preço aplicada") só
              confundia: o cliente não sabe o que é faixa. Fala só quando há
              desconto de verdade. */}
          {faixaAplicada && !semPreco && descontoAplicado > 0 ? (
            <p className="text-[11.5px] font-semibold" style={{ color: "#047857" }}>
              {`Desconto aplicado: ${numeroDoVendor(descontoAplicado, descontoAplicado >= 10 ? 0 : 1)}% OFF (a partir de ${numeroDoVendor(Number(faixaAplicada.min_qty) || 0)} ${unidadeDaFaixa(Number(faixaAplicada.min_qty) || 0)}).`}
            </p>
          ) : null}

          {/* No produto por m², o que vai aqui é o preço do METRO — e ele
              aparece sempre: sem ele a página ficava sem preço nenhum enquanto
              a quantidade não fosse escolhida. */}
          {!semPreco ? (
            <p className="text-xs text-[var(--vd-muted-fg)]">
              {ehMetroQuadrado ? (
                <>
                  {moedaDoVendor(precoDaMedida)} / m²
                  {area > 0 && conta.minimoAplicado !== null ? (
                    <> · Valor mínimo</>
                  ) : area > 0 ? (
                    <> · {moedaDoVendor(precoUnitario)} por peça de {numeroDoVendor(area, 2)} m²</>
                  ) : null}
                </>
              ) : (
                <>{moedaDoVendor(precoUnitario)} / unid.</>
              )}
            </p>
          ) : null}

          {/* O aviso que a loja escreve embaixo do preço ("preços sem
              impostos", "valores para pedido mínimo"). Só quando há preço. */}
          {!semPreco && visao.avisoDePreco ? (
            <p className="text-[11px] text-[var(--vd-muted-fg)]">{visao.avisoDePreco}</p>
          ) : null}

          {ultimasUnidades ? (
            <p className="text-[11.5px] font-semibold" style={{ color: "#b45309" }}>
              Últimas unidades!
            </p>
          ) : null}
        </div>

        {(avisoDaQuantidade || (passaDoEstoque && !(ehEstoqueV2 && estoqueV2?.aviso))) && !esgotadoVirouOrcamento ? (
          <p
            className="inline-flex items-center border px-2.5 py-1 text-[11px] font-semibold"
            style={{
              borderRadius: "var(--vd-radius-sm)",
              borderColor: "#f59e0b",
              color: "#b45309",
              backgroundColor: "rgba(245, 158, 11, 0.08)",
            }}
          >
            {/* O número do estoque só sai quando a loja deixa mostrar o saldo
                exato; nos outros modos o aviso fica sem número. */}
            {passaDoEstoque
              ? estoqueVisivel === "exact"
                ? `Estoque disponível: ${numeroDoVendor(Number.isFinite(saldoQueVale) ? saldoQueVale : 0, vendidoPorMedida(produto.unit_type) ? 2 : 0)}${
                    ehMetroQuadrado ? " m²" : ""
                  }.`
                : "Quantidade acima do estoque."
              : avisoDaQuantidade}
          </p>
        ) : null}

        {ehEstoqueV2 && estoqueV2?.aviso && !esgotadoVirouOrcamento ? (
          /* Estoque v2: esgotado nesta combinação, ou a combinação não existe no estoque. */
          <p className="text-[12px] font-semibold" style={{ color: "#b91c1c" }}>
            {estoqueV2.aviso}
          </p>
        ) : null}

        {aviso ? (
          <p className="text-[12px] font-medium" style={{ color: "var(--vd-primary)" }}>
            {aviso}
          </p>
        ) : null}

        {/* No celular quem compra é a barra fixa: dois botões de "adicionar"
            na mesma tela fazem a pessoa procurar a diferença entre eles. Do
            `lg` para cima a barra some e este volta a ser o único.
            Só vitrine: sem botão nenhum. */}
        {soVitrine ? null : (
          <button
            type="button"
            onClick={adicionar}
            disabled={!podeComprar}
            className="hidden h-12 w-full items-center justify-center gap-2 text-[15px] font-medium shadow-sm transition-all duration-200 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-60 lg:inline-flex"
            style={{
              borderRadius: "var(--vd-radius-xl)",
              backgroundColor: "var(--vd-primary)",
              color: "var(--vd-primary-fg)",
            }}
          >
            {visao.precisaEntrar ? <LogIn className="h-4 w-4 shrink-0" /> : visao.modoDeCompra === "whatsapp" || esgotadoVirouOrcamento ? <MessageCircle className="h-4 w-4 shrink-0" /> : <ShoppingCart className="h-4 w-4 shrink-0" />}
            {adicionado ? rotuloDeAdicionado : rotuloDoBotao}
          </button>
        )}

        {/* Sob consulta no carrinho comum: o botão fica parado, e a pergunta
            vai para a conversa da loja. */}
        {sobConsultaSemCarrinho && visao.podeComprar && linkDeDuvida ? (
          <a
            href={linkDeDuvida}
            target="_blank"
            rel="noopener noreferrer"
            className="hidden items-center justify-center gap-1.5 text-[13px] font-semibold underline-offset-2 hover:underline lg:inline-flex lg:w-full"
            style={{ color: "var(--vd-primary)" }}
          >
            <MessageCircle className="h-4 w-4 shrink-0" />
            Tirar dúvidas no WhatsApp
          </a>
        ) : null}
      </div>

      {/* O frete é o mesmo motor da loja inteira: cotação, retirada e prazo.
          Só a moldura é do Vendor. Sem preço na tela não há cotação: o
          valor do frete entregaria um número onde a loja não quer nenhum. */}
      {!visao.mostrarPreco ? null : (
      <div className="border-t pt-4" style={{ borderColor: "var(--vd-border)" }}>
        {/* O título vem de dentro da calculadora; repetir aqui deixava a
            seção com o mesmo texto duas vezes. */}
        <div
          className="border p-4"
          style={{
            borderRadius: "var(--vd-radius-lg)",
            borderColor: "var(--vd-border)",
            backgroundColor: "color-mix(in srgb, var(--vd-muted-bg) 35%, transparent)",
          }}
        >
          <CatalogShippingCalculator
            slug={slug}
            primaryColor={corDaMarca}
            items={itensDoFrete}
            /* So a escolha que ainda vale fica marcada: mudou a quantidade ou
               a medida, a cotacao de transportadora caduca e sai do total. */
            selectedShipping={freteValido}
            onSelectShipping={escolherFrete}
            disabled={!medidasOk || produto.price_on_request === true}
          />
        </div>
      </div>
      )}

      {/* No celular o botão de comprar sobe com a página e some: quem rolou
          para ler a descrição perdia o caminho da compra. A barra fixa carrega
          o total e o botão, e só existe no celular. Só vitrine: sem barra.

          Ela é montada direto no corpo da página: dentro do card, herdava o
          espaçamento do bloco (`space-y`) e ficava 16px acima do rodapé, como
          se estivesse descolada da base. */}
      {casaDaBarra && !soVitrine
        ? createPortal(
      <div
        className="fixed inset-x-0 bottom-0 z-40 border-t px-4 py-3 lg:hidden"
        style={{
          borderColor: "var(--vd-border)",
          backgroundColor: "var(--vd-surface)",
          boxShadow: "0 -8px 24px rgba(15, 23, 42, 0.10)",
          paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))",
        }}
      >
        <div className="flex items-center gap-3">
          <div className="min-w-0 flex-1">
            <p className="text-[10.5px] uppercase tracking-wide text-[var(--vd-muted-fg)]">
              {entregaNoTotal !== null ? "Total com entrega" : "Total"}
            </p>
            <p
              className={`truncate font-bold ${semPreco && textoSemPreco.length > 14 ? "text-[14px]" : "text-[17px] tabular-nums"}`}
              style={{ color: "var(--vd-primary)" }}
            >
              {semPreco ? textoSemPreco : moedaDoVendor(totalNaTela)}
            </p>
          </div>
          {sobConsultaSemCarrinho && visao.podeComprar && linkDeDuvida ? (
            /* Sob consulta no carrinho comum: no celular o lugar do botão vira
               o caminho da conversa, que é o único que leva a algum lugar. */
            <a
              href={linkDeDuvida}
              target="_blank"
              rel="noopener noreferrer"
              className="inline-flex h-11 shrink-0 items-center justify-center gap-2 px-4 text-[13.5px] font-semibold"
              style={{
                borderRadius: "var(--vd-radius-xl)",
                backgroundColor: "var(--vd-primary)",
                color: "var(--vd-primary-fg)",
              }}
            >
              <MessageCircle className="h-4 w-4 shrink-0" />
              Tirar dúvidas no WhatsApp
            </a>
          ) : (
            <button
              type="button"
              onClick={adicionar}
              disabled={!podeComprar}
              className="inline-flex h-11 shrink-0 items-center justify-center gap-2 px-5 text-[14px] font-semibold transition-opacity disabled:cursor-not-allowed disabled:opacity-60"
              style={{
                borderRadius: "var(--vd-radius-xl)",
                backgroundColor: "var(--vd-primary)",
                color: "var(--vd-primary-fg)",
              }}
            >
              {visao.precisaEntrar ? <LogIn className="h-4 w-4 shrink-0" /> : visao.modoDeCompra === "whatsapp" || esgotadoVirouOrcamento ? <MessageCircle className="h-4 w-4 shrink-0" /> : <ShoppingCart className="h-4 w-4 shrink-0" />}
              {adicionado ? "Adicionado" : podeComprar ? rotuloCurto : rotuloDoBotao}
            </button>
          )}
        </div>
      </div>,
            casaDaBarra
          )
        : null}

      {/* Os selos que a loja escolheu para a página do produto. */}
      <div className="grid grid-cols-2 gap-2 border-t pt-4" style={{ borderColor: "var(--vd-border)" }}>
        {selosDaLoja(selos).map((selo, indice) => {
          const Desenho = DESENHOS_DA_CONFIANCA[selo.icon] || ShieldCheck;
          return (
            <div key={`selo-${indice}`} className="flex items-center gap-2">
              <span
                className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
                style={{
                  backgroundColor: "color-mix(in srgb, var(--vd-primary) 12%, transparent)",
                  color: "var(--vd-primary)",
                }}
              >
                <Desenho size={15} />
              </span>
              <span className="text-[12px] font-medium leading-snug">{selo.title}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}
