"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, type MutableRefObject, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { usePathname, useRouter } from "next/navigation";
import {
  AlignJustify,
  BadgeCheck,
  Clock,
  CreditCard,
  Headphones,
  Info,
  LogIn,
  MapPin,
  MessageCircle,
  Minus,
  Package,
  Plus,
  ShieldCheck,
  Tag,
  Truck,
  X,
} from "lucide-react";
import { useCart } from "@/context/CartContext";
import CatalogShippingCalculator from "@/components/CatalogShippingCalculator";
import type { SelectedComboData } from "@/components/ProductCombosWidget";
import { useVisaoDaLoja } from "@/components/VisaoDaLojaContext";
import { normalizeWhatsappForLink } from "@/lib/phone";
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 { moedaDaBoutique, numeroDaBoutique } from "./formato";
import { BoutiqueValorMinimo } from "./BoutiqueValorMinimo";
import { SeletorDeVariacoes } from "@/components/SeletorDeVariacoes";
import {
  juntarAdicionais,
  nomeDaMedida,
  precoDaLinha,
  unidadeDoAdicional,
  valorMinimoDoItem,
  vendidoPorMedida,
  type UnidadeDoAdicional,
} from "@/lib/adicionais";

/*
  O card de configuração da Boutique: 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 BoutiqueOpcao = {
  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 BoutiqueAtributo = {
  id?: string | null;
  name?: string | null;
  options?: Array<string | BoutiqueOpcao> | null;
  allow_multiple?: boolean | null;
  is_required?: boolean | null;
  position?: number | null;
  show_in_store?: boolean | null;
};

export type BoutiqueProdutoDaPagina = {
  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?: BoutiqueAtributo[] | 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: BoutiqueAtributo[] | 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 }));
}

/* Dígitos viram metros com duas casas: "250" vira "2,50". A conta é a mesma
   da vitrine, escrita aqui para a Boutique 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(--bq-muted-fg)] transition-colors hover:text-[var(--bq-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(--bq-surface)] p-3 text-left text-[12px] font-normal leading-relaxed text-[var(--bq-muted-fg)] shadow-xl"
          style={{ borderRadius: "var(--bq-radius-lg)", borderColor: "var(--bq-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;
}

/* O que a página de produto pode pedir à compra de fora dela: o "Compre
   junto" (que mora embaixo, em largura inteira) e a oferta flutuante usam o
   MESMO adicionar — um caminho só para o carrinho. */
export type BoutiqueControleDaCompra = {
  adicionar: () => void;
  /* Leva a pessoa de volta ao card de compra (falta escolher alguma coisa). */
  mostrarACompra: () => void;
};

/* O cupom da faixa de aviso (Catálogo › Marketing), que a caixa de cupom da
   página repete. Só os campos que a caixa usa. */
export type BoutiqueCupomDaPagina = {
  code?: string | null;
  type?: string | null;
  value?: number | null;
  active?: boolean | null;
  ends_at?: string | null;
};

export function BoutiqueCompra({
  produto,
  slug,
  gruposDeServico,
  servicosDoProduto,
  combosEscolhidos = [],
  aoPreferirImagem,
  selos,
  whatsapp,
  controleRef,
  fotoPrincipal,
  cupomDoAviso,
  antesDoBotao,
  ofertaFlutuante = true,
}: {
  produto: BoutiqueProdutoDaPagina;
  slug: string;
  /* A cor da marca (a Boutique compra em preto; fica no contrato da página). */
  corDaMarca: string;
  gruposDeServico?: GrupoDeServicos[];
  servicosDoProduto?: ServicoOpcional[];
  /* Os itens do "Compre junto" que a pessoa marcou (a seção mora embaixo,
     na página; quem soma e manda para o carrinho é este card). */
  combosEscolhidos?: SelectedComboData[];
  /* 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;
  controleRef?: MutableRefObject<BoutiqueControleDaCompra | null>;
  /* A foto da oferta flutuante do computador. */
  fotoPrincipal?: string | null;
  cupomDoAviso?: BoutiqueCupomDaPagina | null;
  /* O que a página põe logo acima do botão (gabaritos, prazo, corte). */
  antesDoBotao?: ReactNode;
  /* Com o vídeo na janelinha flutuante, a oferta flutuante não entra: as
     duas disputariam o mesmo canto da tela. */
  ofertaFlutuante?: boolean;
}) {
  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 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-bq-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
     Boutique 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: ${numeroDaBoutique(conferencia.minQty)}.`;
    }
    if (conferencia.isAboveMax && conferencia.maxQty !== null) {
      return `Quantidade máxima: ${numeroDaBoutique(conferencia.maxQty)}.`;
    }
    if (conferencia.isNotAllowedValue && conferencia.allowedQuantities.length > 0) {
      return `Quantidades permitidas: ${conferencia.allowedQuantities.map((v) => numeroDaBoutique(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) => numeroDaBoutique(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 ${moedaDaBoutique(precoLa)}.`;
    const pecasNecessarias = Math.ceil((inicio - 0.000001) / area);
    if (pecasNecessarias <= quantidade) return "";
    return `Leve ${numeroEnxuto(pecasNecessarias)} ${pecasNecessarias === 1 ? "peça" : "peças"} (${numeroDaBoutique(pecasNecessarias * area, 2)} ${medida}) e cada ${medida} cai para ${moedaDaBoutique(precoLa)}.`;
  }, [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";

  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})` : ""}, ${numeroDaBoutique(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);
  }

  /* ------------------------------------------------------------------ *
   *  O desenho (referência: .cache/tema-boutique/REFERENCIA.md, 7.1/7.2)
   * ------------------------------------------------------------------ */

  /* O preço grande do topo é o de UMA unidade (ou do m², ou do metro) na
     quantidade escolhida, já com as opções marcadas. Pix e parcelas não
     aparecem: a loja não cadastra condição de pagamento, e a página não
     promete o que o checkout não cobra. */
  const ehMetroLinear = produto.unit_type === "meter" || produto.unit_type === "linear_meter";
  const precoDoTopo = porMedida ? precoDaMedida : precoUnitario;
  const legendaDoTopo = ehMetroQuadrado
    ? "por m²"
    : ehMetroLinear
      ? "por metro"
      : faixas.length || quantidade > 1
        ? "por unidade"
        : "";
  const unidadeCurta = (qtd: number) => {
    if (ehMetroQuadrado) return "m²";
    if (ehMetroLinear) return qtd === 1 ? "metro" : "metros";
    return qtd === 1 ? "unidade" : "unidades";
  };

  /* A caixa de cupom repete o cupom da faixa de aviso. Sem cupom escolhido
     pela loja, a caixa não existe. */
  const cupom = cupomDoAviso && String(cupomDoAviso.code || "").trim() && cupomDoAviso.active !== false ? cupomDoAviso : null;
  const codigoDoCupom = String(cupom?.code || "").trim().toUpperCase();
  const ganhoDoCupom = !cupom
    ? ""
    : cupom.type === "free_shipping"
      ? "Frete grátis"
      : cupom.type === "percent"
        ? `${numeroDaBoutique(Number(cupom.value) || 0, Number.isInteger(Number(cupom.value)) ? 0 : 1)}% OFF`
        : `${moedaDaBoutique(Number(cupom.value) || 0)} OFF`;
  const [cupomCopiado, setCupomCopiado] = useState(false);
  const copiarCupom = async () => {
    if (!codigoDoCupom) return;
    try {
      await navigator.clipboard.writeText(codigoDoCupom);
      setCupomCopiado(true);
      window.setTimeout(() => setCupomCopiado(false), 1500);
    } catch {
      /* Sem permissão de área de transferência o código continua à vista. */
    }
  };

  /* Leva a pessoa de volta ao card de compra: a barra do celular e a oferta
     flutuante usam quando falta escolher alguma coisa. */
  const raizDaCompra = useRef<HTMLDivElement | null>(null);
  const mostrarACompra = useCallback(() => {
    const alvo = raizDaCompra.current;
    if (!alvo) return;
    const cabecalho = parseFloat(getComputedStyle(alvo).getPropertyValue("--bq-header-h")) || 0;
    window.scrollTo({ top: Math.max(0, alvo.getBoundingClientRect().top + window.scrollY - cabecalho - 16), behavior: "smooth" });
  }, []);

  /* Comprar de fora do card (barra do celular, oferta flutuante, "Compre
     junto"): pronto, vai direto; faltando escolha, o aviso aparece no card e
     a página volta até ele. */
  const comprarDeFora = () => {
    if (podeComprar) {
      adicionar();
      return;
    }
    setAviso(rotuloDoBotao);
    mostrarACompra();
  };
  const comprarDeForaRef = useRef(comprarDeFora);
  useEffect(() => {
    comprarDeForaRef.current = comprarDeFora;
  });
  useEffect(() => {
    if (!controleRef) return;
    controleRef.current = { adicionar: () => comprarDeForaRef.current(), mostrarACompra };
    return () => {
      controleRef.current = null;
    };
  }, [controleRef, mostrarACompra]);

  /* Bloqueio de verdade (loja parada, sem estoque, só vitrine): o botão de
     fora fica parado. Escolha que falta não é bloqueio — o toque leva até
     ela. */
  const bloqueioDuro =
    !visao.precisaEntrar &&
    !esgotadoVirouOrcamento &&
    (!visao.podeComprar || semEstoque || Boolean(escolhaForaDoEstoque) || sobConsultaSemCarrinho);

  /* A oferta flutuante do computador: entra pela direita depois de ~1000 px
     de rolagem, como na referência, e some de vez no X. */
  const [ofertaNaTela, setOfertaNaTela] = useState(false);
  const [ofertaFechada, setOfertaFechada] = useState(false);
  useEffect(() => {
    if (!ofertaFlutuante || soVitrine) return;
    /* Entra depois de ~1000 px E quando o card de compra já ficou para trás:
       por cima do próprio card ela só cobriria o frete. */
    const conferir = () => {
      const fimDoCard = raizDaCompra.current?.getBoundingClientRect().bottom ?? 0;
      setOfertaNaTela(window.scrollY > 1000 && fimDoCard < 120);
    };
    conferir();
    window.addEventListener("scroll", conferir, { passive: true });
    return () => window.removeEventListener("scroll", conferir);
  }, [ofertaFlutuante, soVitrine]);

  const iconeDoBotao = visao.precisaEntrar ? (
    <LogIn className="h-4 w-4 shrink-0" />
  ) : visao.modoDeCompra === "whatsapp" || esgotadoVirouOrcamento ? (
    <MessageCircle className="h-4 w-4 shrink-0" />
  ) : null;

  return (
    <div ref={raizDaCompra} data-bq-compra="" className="bq-compra">
      {/* ---------------- preço ---------------- */}
      <div className="flex items-start justify-between gap-4">
        <div className="min-w-0">
          {semPreco ? (
            <p className={textoSemPreco.length > 18 ? "text-[17px] font-bold leading-snug text-black" : "text-[22px] font-bold leading-[33px] text-black"}>
              {textoSemPreco}
            </p>
          ) : (
            <>
              <p className="flex flex-wrap items-center gap-x-2">
                <span className="text-[22px] font-bold leading-[33px] tracking-[-0.32px] tabular-nums text-black">
                  {moedaDaBoutique(precoDoTopo)}
                </span>
                {descontoAplicado > 0 ? (
                  <span className="bq-etiqueta">-{numeroDaBoutique(descontoAplicado, descontoAplicado >= 10 ? 0 : 1)}%</span>
                ) : null}
              </p>
              {legendaDoTopo ? <p className="text-[13px] leading-tight text-[#252525]">{legendaDoTopo}</p> : null}
            </>
          )}
        </div>

        {/* O lado direito: o preço cheio quando a faixa já abateu, ou até onde
            o preço cai levando mais (a melhor faixa). */}
        {!semPreco && descontoAplicado > 0 ? (
          <div className="shrink-0 text-right">
            <p className="text-[18px] leading-[27px] tabular-nums text-[#808b99] line-through">
              {moedaDaBoutique(Number(produto.price) || 0)}
            </p>
            <p className="text-[12px] leading-[13px] text-[#252525]">preço sem desconto</p>
          </div>
        ) : !semPreco && melhorOferta ? (
          <div className="shrink-0 text-right">
            <p className="flex items-center justify-end gap-1.5 text-[18px] leading-[27px] tabular-nums text-black">
              <Tag className="h-4 w-4" strokeWidth={1.6} />
              {moedaDaBoutique(Number(melhorOferta.tier.unit_price) || 0)}
            </p>
            <p className="text-[12px] leading-[14px] text-[#252525]">
              a partir de <strong>{numeroDaBoutique(Number(melhorOferta.tier.min_qty) || 0)}</strong>{" "}
              {unidadeCurta(Number(melhorOferta.tier.min_qty) || 0)}
            </p>
          </div>
        ) : null}
      </div>

      {/* O aviso que a loja escreve embaixo do preço ("preços sem impostos"). */}
      {!semPreco && visao.avisoDePreco ? (
        <p className="mt-2 text-[12px] text-[#666666]">{visao.avisoDePreco}</p>
      ) : null}

      <div className="mt-5 space-y-5">
        {/* medidas, quando o produto é vendido por metro quadrado */}
        {ehMetroQuadrado ? (
          <div>
            <p className="bq-rotulo">Medidas (em metros):</p>
            <div className="mt-2 grid grid-cols-2 gap-2">
              {(
                [
                  ["Largura", largura, setDigitosDaLargura],
                  ["Altura", altura, setDigitosDaAltura],
                ] as const
              ).map(([rotulo, valor, mudar]) => (
                <label key={rotulo} className="block">
                  <span className="mb-1 block text-[11px] uppercase tracking-[0.06em] text-[#666666]">{rotulo}</span>
                  <input
                    inputMode="numeric"
                    value={valor}
                    aria-label={`${rotulo} em metros`}
                    onKeyDown={(e) => {
                      if (e.key !== "Backspace") return;
                      if (e.currentTarget.selectionStart !== e.currentTarget.selectionEnd) return;
                      e.preventDefault();
                      mudar((d) => d.slice(0, -1));
                    }}
                    onChange={(e) => mudar(e.target.value.replace(/\D/g, ""))}
                    placeholder="0,00"
                    className="bq-campo"
                  />
                </label>
              ))}
            </div>
            {area > 0 ? (
              <p className="mt-1.5 text-[12px] text-[#666666]">
                Área: <strong className="tabular-nums text-black">{numeroDaBoutique(area, 2)} m²</strong>
                {quantidade > 1 ? (
                  <>
                    {" · "}
                    <strong className="tabular-nums text-black">{numeroDaBoutique(area * quantidade, 2)} m²</strong> no total
                  </>
                ) : null}
              </p>
            ) : null}
          </div>
        ) : null}

        {/* As variações usam o mesmo mecanismo do resto do sistema (bolinha,
            miniatura, chip, lista, menu, recolher em chip). A aparência da
            referência — miniatura 46×58, chip 32×32, anel preto de 2 px,
            esgotado com ✖ — é uma camada de CSS em volta (.bq-variacoes no
            theme.css): a lógica de seleção continua uma só. */}
        {produto.product_attributes?.length ? (
          <div className="bq-variacoes">
            <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="#252525"
              corDoTextoNaLoja="#ffffff"
              corDaLojaNoBranco="#252525"
              formatarDinheiro={moedaDaBoutique}
              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
              }
            />
          </div>
        ) : 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. */}
        {faixas.length && porMedida && !semPreco ? (
          <div>
            <p className="bq-rotulo">Desconto progressivo por {medida}:</p>
            <p className="mt-0.5 text-[12px] text-[#666666]">Quanto mais {medida} no pedido, mais barato sai cada {medida}.</p>
            <div className="mt-2 border" style={{ borderColor: "#e1e1e1" }}>
              {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-[13px] ${indice > 0 ? "border-t" : ""}`}
                    style={{ borderColor: "#e1e1e1", backgroundColor: marcada ? "#f6f6f6" : undefined }}
                  >
                    <span className={marcada ? "font-semibold text-black" : "text-[#4b4f58]"}>
                      {rotuloDaFaixa(faixa, indice === 0)}
                      {marcada ? (
                        <span className="ml-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-black">seu pedido</span>
                      ) : null}
                    </span>
                    <span className="font-semibold tabular-nums text-black">
                      {moedaDaBoutique((Number(faixa.unit_price) || 0) + adicionais.porMedida)}/{medida}
                    </span>
                  </div>
                );
              })}
            </div>
            {dicaDaProximaFaixa ? (
              <p className="mt-1.5 text-[12px] font-medium" style={{ color: "var(--bq-verde)" }}>
                {dicaDaProximaFaixa}
              </p>
            ) : null}
          </div>
        ) : null}

        {/* A tabela de quantidade: escolher uma linha muda a quantidade. */}
        {faixas.length && !porMedida && !semPreco ? (
          <div>
            <p className="bq-rotulo">Preço por quantidade:</p>
            <div className="mt-2 border" style={{ borderColor: "#e1e1e1" }}>
              <table className="w-full border-collapse text-[12px] sm:text-[13px]">
                <thead>
                  <tr className="text-left text-[10.5px] uppercase tracking-[0.06em] text-[#666666]">
                    <th className="px-3 py-2 font-medium">Quantidade</th>
                    <th className="px-3 py-2 font-medium">Unidade</th>
                    <th className="px-3 py-2 text-right font-medium">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 hover:bg-[#fafafa]"
                        style={{ borderColor: "#e1e1e1", backgroundColor: marcada ? "#f6f6f6" : undefined }}
                      >
                        <td className="whitespace-nowrap px-3 py-2.5">
                          <label className="flex cursor-pointer items-center gap-2">
                            <input
                              type="radio"
                              checked={marcada}
                              onChange={() => {
                                setQuantidade(qtd);
                                setQuantidadeEscolhida(true);
                              }}
                              className="bq-radio"
                            />
                            <span className={marcada ? "font-semibold text-black" : "text-[#252525]"}>
                              {numeroDaBoutique(qtd)} {unidadeDaFaixa(qtd)}
                            </span>
                          </label>
                        </td>
                        <td className="whitespace-nowrap px-3 py-2.5 tabular-nums text-[#666666]">{moedaDaBoutique(unitario)}</td>
                        <td className="whitespace-nowrap px-3 py-2.5 text-right font-semibold tabular-nums text-black">
                          {moedaDaBoutique(unitario * qtd + somaDasOpcoes.umaVez + somaDosServicosEscolhidos.umaVez)}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>
        ) : null}

        {/* os serviços da loja */}
        {vitrine.length ? (
          <div className="space-y-4">
            {vitrine.map((caixa) => (
              <div key={caixa.grupo.id || "servicos"}>
                <p className="bq-rotulo">
                  {caixa.grupo.nome}:
                  {caixa.grupo.obrigatorio ? (
                    <span className="ml-1.5 text-[11px] font-normal text-[#666666]">
                      {caixa.grupo.modo === "uma" ? "escolha uma opção" : "escolha uma ou mais opções"}
                    </span>
                  ) : null}
                </p>
                <div className="mt-2 grid gap-2 sm:grid-cols-2">
                  {caixa.servicos.map((servico) => {
                    const marcado = servicosMarcados.includes(servico.id);
                    return (
                      <button
                        key={servico.id}
                        type="button"
                        aria-pressed={marcado}
                        onClick={() => {
                          setAviso("");
                          setServicosMarcados((antes) => alternarEscolha(antes, servico, caixa));
                        }}
                        /* Sem `overflow-hidden`: o balão de ajuda sobe para fora do cartão. */
                        className="bq-servico relative border bg-white py-2 pl-2.5 pr-2 text-left"
                      >
                        <span className="flex items-start gap-2.5">
                          <span className="flex h-8 w-8 shrink-0 items-center justify-center bg-[#f3f3f3]" style={{ borderRadius: 4 }}>
                            {/* 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 text-black">{servico.titulo}</span>
                              {servico.detalhes ? <AjudaDoServico texto={servico.detalhes} /> : null}
                            </span>
                            {servico.descricao ? (
                              <span className="mt-0.5 block text-[11px] leading-snug text-[#666666]">{servico.descricao}</span>
                            ) : null}
                            {/* Sem preço para este visitante o serviço aparece
                                sem valor nenhum: nem "Grátis" nem o preço. */}
                            {semPreco ? null : (
                              <span className="mt-1 block text-[13px] font-bold text-black">
                                {servico.preco > 0 ? moedaDaBoutique(servico.preco) : "Grátis"}
                                {servico.preco > 0 ? (
                                  <span className="ml-1 text-[10px] font-medium text-[#666666]">
                                    {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 (o de número é texto com teclado
            numérico: nada de type="number", que some com a vírgula) */}
        {camposAtivos.length ? (
          <div className="space-y-3">
            {camposAtivos.map((campo) => {
              const chave = String(campo.id || campo.title);
              return (
                <label key={chave} className="block">
                  <span className="bq-rotulo block">
                    {campo.title}
                    {campo.required ? <span className="text-[#d7271d]"> *</span> : null}
                  </span>
                  {campo.field_type === "textarea" ? (
                    <textarea
                      rows={3}
                      value={campos[chave] || ""}
                      placeholder={campo.placeholder || ""}
                      onChange={(e) => setCampos((antes) => ({ ...antes, [chave]: e.target.value }))}
                      className="bq-campo mt-2 h-auto py-2"
                    />
                  ) : (
                    <input
                      type="text"
                      inputMode={campo.field_type === "number" ? "decimal" : undefined}
                      value={campos[chave] || ""}
                      placeholder={campo.placeholder || ""}
                      onChange={(e) =>
                        setCampos((antes) => ({
                          ...antes,
                          [chave]: campo.field_type === "number" ? e.target.value.replace(/[^\d,.-]/g, "") : e.target.value,
                        }))
                      }
                      className="bq-campo mt-2"
                    />
                  )}
                </label>
              );
            })}
          </div>
        ) : null}

        {/* A caixa de cupom da referência (.lfb-pdp-cupom), com o cupom que a
            loja escolheu para a faixa de aviso. Clicar no código copia. */}
        {cupom && !semPreco ? (
          <div className="bq-cupom">
            <div className="bq-cupom-grade">
              <div className="min-w-0">
                <p className="bq-cupom-olho">Oferta especial</p>
                <p className="bq-cupom-ganho">
                  {ganhoDoCupom} <em>em todo o site</em>
                </p>
              </div>
              <div className="bq-cupom-lado">
                <p className="bq-cupom-olho text-[#252525]">Aproveite seu desconto</p>
                <p className="mt-1 text-[15px] leading-snug text-[#171717]">
                  Use o cupom <strong>{codigoDoCupom}</strong>
                </p>
                <p className="mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-[#6e6a66]">
                  Ganhe <strong className="text-[#171717]">{ganhoDoCupom}</strong>
                  <button type="button" onClick={copiarCupom} className="bq-cupom-codigo" title="Copiar o cupom">
                    {cupomCopiado ? "Copiado!" : codigoDoCupom}
                  </button>
                </p>
                <p className="mt-2 text-[11px] text-[#6e6a66]">Clique no cupom para copiar e use no carrinho.</p>
              </div>
            </div>
          </div>
        ) : null}

        {antesDoBotao}

        {/* quantidade e total */}
        {soVitrine ? null : (
          <div>
            <div className="flex items-end justify-between gap-4">
              <div>
                <p className="bq-rotulo">Quantidade:</p>
                <div className="bq-stepper mt-2">
                  <button
                    type="button"
                    aria-label="Diminuir"
                    disabled={!podeDiminuir}
                    onClick={() => {
                      if (proximaAbaixo === null || !podeDiminuir) return;
                      setQuantidade(proximaAbaixo);
                      setQuantidadeEscolhida(true);
                    }}
                  >
                    <Minus className="h-3.5 w-3.5" />
                  </button>
                  <input
                    className="bq-numero"
                    value={quantidade}
                    inputMode="numeric"
                    aria-label="Quantidade"
                    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);
                    }}
                  >
                    <Plus className="h-3.5 w-3.5" />
                  </button>
                </div>
              </div>
              {semPreco ? null : (
                <div className="min-w-0 text-right">
                  <p className="text-[12px] text-[#666666]">{entregaNoTotal !== null ? "Total com entrega" : "Total"}</p>
                  <p className="text-[20px] font-bold leading-tight tabular-nums text-black" data-bq-total="">
                    {moedaDaBoutique(totalNaTela)}
                  </p>
                </div>
              )}
            </div>

            {/* Os avisos, um abaixo do outro, na largura toda. */}
            <div className="mt-2 space-y-1">
              {mostraMinimo && !semPreco && valorMinimo !== null ? (
                <p>
                  <BoutiqueValorMinimo minimo={valorMinimo} />
                </p>
              ) : null}
              {entregaNoTotal !== null && freteValido ? (
                <p className="text-[12px] text-[#666666]">
                  {`Produto ${moedaDaBoutique(total)} · ${freteValido.option.service || (isPickupShippingOption(freteValido.option) ? "Retirada" : "Entrega")} ${
                    entregaNoTotal > 0 ? moedaDaBoutique(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. */}
              {melhorOferta && !semPreco ? (
                <p>
                  <span className="inline-flex items-center px-2 py-0.5 text-[11px] font-semibold" style={{ backgroundColor: "rgba(0,164,65,0.10)", color: "#00863a", borderRadius: 3 }}>
                    {numeroDaBoutique(melhorOferta.discountPct, melhorOferta.discountPct >= 10 ? 0 : 1)}% OFF a partir de{" "}
                    {numeroDaBoutique(Number(melhorOferta.tier.min_qty) || 0)} {unidadeDaFaixa(Number(melhorOferta.tier.min_qty) || 0)}
                  </span>
                </p>
              ) : null}
              {faixaAplicada && !semPreco && descontoAplicado > 0 ? (
                <p className="text-[12px] font-semibold" style={{ color: "#00863a" }}>
                  {`Desconto aplicado: ${numeroDaBoutique(descontoAplicado, descontoAplicado >= 10 ? 0 : 1)}% OFF (a partir de ${numeroDaBoutique(Number(faixaAplicada.min_qty) || 0)} ${unidadeDaFaixa(Number(faixaAplicada.min_qty) || 0)}).`}
                </p>
              ) : null}
              {!semPreco ? (
                <p className="text-[12px] text-[#666666]">
                  {ehMetroQuadrado ? (
                    <>
                      {moedaDaBoutique(precoDaMedida)} / m²
                      {area > 0 && conta.minimoAplicado !== null ? (
                        <> · Valor mínimo</>
                      ) : area > 0 ? (
                        <> · {moedaDaBoutique(precoUnitario)} por peça de {numeroDaBoutique(area, 2)} m²</>
                      ) : null}
                    </>
                  ) : ehMetroLinear ? (
                    <>{moedaDaBoutique(precoUnitario)} / metro</>
                  ) : (
                    <>{moedaDaBoutique(precoUnitario)} / unid.</>
                  )}
                </p>
              ) : null}
              {ultimasUnidades ? (
                <p className="text-[12px] font-semibold" style={{ color: "#b45309" }}>
                  Últimas unidades!
                </p>
              ) : null}
            </div>

            {(avisoDaQuantidade || (passaDoEstoque && !(ehEstoqueV2 && estoqueV2?.aviso))) && !esgotadoVirouOrcamento ? (
              <p className="mt-2 inline-flex items-center border px-2.5 py-1 text-[11px] font-semibold" style={{ 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. */}
                {passaDoEstoque
                  ? estoqueVisivel === "exact"
                    ? `Estoque disponível: ${numeroDaBoutique(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="mt-2 text-[12px] font-semibold" style={{ color: "#b91c1c" }}>
                {estoqueV2.aviso}
              </p>
            ) : null}

            {aviso ? (
              <p className="mt-2 text-[12px] font-semibold text-[#d7271d]" role="status">
                {aviso}
              </p>
            ) : null}

            {/* ADICIONAR AO CARRINHO: preto, 50 px, caixa alta, hover #333. */}
            <button
              type="button"
              onClick={adicionar}
              disabled={!podeComprar}
              data-bq-comprar=""
              className="bq-botao-comprar mt-4"
            >
              {iconeDoBotao}
              {adicionado ? rotuloDeAdicionado : rotuloDoBotao}
            </button>

            {/* Sob consulta no carrinho comum: a pergunta vai para a conversa. */}
            {sobConsultaSemCarrinho && visao.podeComprar && linkDeDuvida ? (
              <a
                href={linkDeDuvida}
                target="_blank"
                rel="noopener noreferrer"
                className="mt-3 inline-flex w-full items-center justify-center gap-1.5 text-[13px] font-semibold text-black underline-offset-2 hover:underline"
              >
                <MessageCircle className="h-4 w-4 shrink-0" />
                Tirar dúvidas no WhatsApp
              </a>
            ) : null}
          </div>
        )}

        {/* Calcule o frete: o motor é o da loja inteira (cotação, retirada e
            prazo); só a moldura é da Boutique. Sem preço na tela não há
            cotação: o valor do frete entregaria um número onde a loja não quer. */}
        {!visao.mostrarPreco || soVitrine ? null : (
          <div className="bq-frete">
            <div className="flex shrink-0 items-center gap-3 pt-1.5">
              <Truck className="h-[22px] w-[22px] shrink-0 text-black" strokeWidth={1.4} />
              <div>
                <p className="text-[14px] font-bold leading-tight text-black">Calcule o frete</p>
                <a
                  href="https://buscacepinter.correios.com.br/app/endereco/index.php"
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-[11px] text-[#666666] hover:underline"
                >
                  Não sei o meu CEP
                </a>
              </div>
            </div>
            <div className="min-w-0 flex-1">
              <CatalogShippingCalculator
                slug={slug}
                primaryColor="#111111"
                compact
                items={itensDoFrete}
                /* Só a escolha que ainda vale fica marcada: mudou a quantidade
                   ou a medida, a cotação de transportadora caduca. */
                selectedShipping={freteValido}
                onSelectShipping={escolherFrete}
                disabled={!medidasOk || produto.price_on_request === true}
              />
            </div>
          </div>
        )}

        {/* "Descrição completa": desce até o acordeão da descrição. */}
        <button
          type="button"
          onClick={() => {
            const alvo = document.getElementById("bq-descricao");
            if (!alvo) return;
            const cabecalho = parseFloat(getComputedStyle(alvo).getPropertyValue("--bq-header-h")) || 0;
            window.scrollTo({ top: alvo.getBoundingClientRect().top + window.scrollY - cabecalho - 12, behavior: "smooth" });
          }}
          className="inline-flex items-center gap-3 text-[13px] text-black hover:underline"
        >
          <AlignJustify className="h-5 w-5" strokeWidth={1.4} />
          Descrição completa
        </button>

        {/* Os selos que a loja escolheu para a página do produto. */}
        <div className="grid grid-cols-2 gap-3 border-t pt-4" style={{ borderColor: "#e8eaed" }}>
          {selosDaLoja(selos).map((selo, indice) => {
            const Desenho = DESENHOS_DA_CONFIANCA[selo.icon] || ShieldCheck;
            return (
              <div key={`selo-${indice}`} className="flex items-center gap-2.5">
                <Desenho size={22} strokeWidth={1.4} className="shrink-0 text-black" />
                <span className="text-[12px] font-medium leading-snug text-[#252525]">{selo.title}</span>
              </div>
            );
          })}
        </div>
      </div>

      {/* A barra de compra fixa do celular (71 px, botão preto de raio 4), nas
          cores da barra de compra do lojista (catalog_buybar_*). Montada na
          raiz do template, que carrega as variáveis --bq-*. */}
      {casaDaBarra && !soVitrine
        ? createPortal(
            <div data-bq-barra-fixa="" className="bq-barra-de-compra lg:hidden">
              {sobConsultaSemCarrinho && visao.podeComprar && linkDeDuvida ? (
                <a href={linkDeDuvida} target="_blank" rel="noopener noreferrer" className="bq-barra-de-compra-botao">
                  <MessageCircle className="h-4 w-4 shrink-0" />
                  Tirar dúvidas no WhatsApp
                </a>
              ) : (
                <button type="button" onClick={comprarDeFora} disabled={bloqueioDuro} className="bq-barra-de-compra-botao">
                  {iconeDoBotao}
                  <span className="truncate">{adicionado ? rotuloDeAdicionado : rotuloDoBotao}</span>
                </button>
              )}
            </div>,
            casaDaBarra
          )
        : null}

      {/* A oferta flutuante do computador: entra pela direita depois de rolar
          (all 1s), com foto, nome, preço e COMPRAR AGORA. O X fecha de vez. */}
      {casaDaBarra && ofertaFlutuante && !soVitrine && !ofertaFechada
        ? createPortal(
            <aside
              data-bq-oferta=""
              aria-hidden={!ofertaNaTela}
              className="bq-oferta hidden lg:block"
              {...(ofertaNaTela ? { "data-visivel": "" } : {})}
            >
              <button type="button" onClick={() => setOfertaFechada(true)} aria-label="Fechar oferta" className="absolute right-2 top-2 p-1 text-black">
                <X className="h-4 w-4" strokeWidth={1.6} />
              </button>
              {fotoPrincipal ? (
                /* eslint-disable-next-line @next/next/no-img-element */
                <img src={fotoPrincipal} alt="" className="mx-auto h-[220px] w-full object-contain" />
              ) : null}
              <p className="mt-3 line-clamp-2 text-[13px] leading-snug text-[#333333]">{produto.name}</p>
              <p className="mt-2 text-[20px] font-bold tabular-nums text-black">
                {semPreco ? textoSemPreco : moedaDaBoutique(precoDoTopo)}
              </p>
              <button
                type="button"
                tabIndex={ofertaNaTela ? 0 : -1}
                onClick={comprarDeFora}
                disabled={bloqueioDuro}
                className="bq-botao-comprar mt-3 h-[44px] text-[14px] font-bold"
              >
                {visao.precisaEntrar ? visao.regras.textoPrecoLogin : "Comprar agora"}
              </button>
            </aside>,
            casaDaBarra
          )
        : null}
    </div>
  );
}
