"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useVisaoDaLoja } from "@/components/VisaoDaLojaContext";
import { estoqueLimitaAVenda, preencherModelo } from "@/lib/loja/regras-da-loja";
import { useEstoqueDaEscolha } from "@/lib/use-estoque-da-escolha";
import { SeletorDeVariacoes } from "@/components/SeletorDeVariacoes";
import ValorMinimoDoItem from "@/components/ValorMinimoDoItem";
import {
  juntarAdicionais,
  nomeDaMedida,
  precoDaLinha,
  somarAdicionais,
  valorMinimoDoItem,
  vendidoPorMedida,
} from "@/lib/adicionais";
import { ServicosOpcionais } from "@/components/ServicosOpcionais";
import { variacoesDaVitrine } from "@/lib/variacoes-da-loja";
import {
  alternarEscolha,
  escolhidosComGrupo,
  grupoPendente,
  servicoComoOpcao,
  somaDosServicos,
  vitrineDeServicos,
  type GrupoDeServicos,
  type GrupoNaVitrine,
  type ServicoOpcional,
} from "@/lib/servicos-opcionais";
import { useCart } from "@/context/CartContext";
import { ShoppingBag, MessageCircle, Truck, ShieldCheck, CreditCard, Package, Clock, BadgeCheck, MapPin, Headphones, Hash } from "lucide-react";
import { normalizeWhatsappForLink } from "@/lib/phone";
import { ensureColorContrast, getReadableTextColor, withAlpha } from "@/lib/color-contrast";
import { toast } from "sonner";
import {
  calculateEffectiveQuantity,
  calculateTierDiscountPct,
  checkSaleQuantityLimits,
  getBestTierDiscount,
  resolveTierPrice,
  type ProductPriceTier,
} from "@/lib/price-tiers";
import { buildShippingPackageFromItems, formatShippingPrice, isPickupShippingOption, type ShippingSelection } from "@/lib/catalog-shipping";
import CatalogShippingCalculator from "@/components/CatalogShippingCalculator";
import ProductCombosWidget from "@/components/ProductCombosWidget";
import type { ProductCombo, SelectedComboData } from "@/components/ProductCombosWidget";

/** Cores da barra fixa escolhidas pelo lojista. Vazio = padrao do sistema. */
export type BuyBarColors = {
  bg?: string | null;
  button?: string | null;
  text?: string | null;
};

interface ProductPurchaseCardProps {
  product: {
    id: string;
    name: string;
    price: number;
    image_url?: string | null;
    stock: number;
    user_id: string;
    unit_type?: string | null;
    price_on_request?: boolean | null;
    use_variation_price_only?: boolean | null;
    product_price_tiers?: ProductPriceTier[] | 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;
    track_inventory?: boolean | null;
    allow_negative_stock?: boolean | null;
    /* Estoque v2 (marcado pelo servidor): quem diz o saldo e o banco, por combinacao. */
    estoque_v2?: boolean | null;
    estoque_v2_limita?: boolean | null;
    estoque_v2_esgotado?: boolean | null;
    inventory_scope?: "product" | "option" | "variant" | null;
    inventory_attribute_name?: string | null;
    product_images?: { id: string; image_url: string; sort_order?: number | null; is_primary?: boolean | null }[] | null;
    product_attributes?: {
      id: string;
      name: string;
      options: (string | {
        id?: string;
        label: string;
        price: number;
        price_applies_per_quantity?: boolean;
        price_unit?: string | null;
        stock?: number;
        is_required?: boolean;
        position?: number;
        image_id?: string | null;
        minimum_stock?: number;
        reorder_point?: number;
        swatch?: string | null;
      })[];
      allow_multiple?: boolean | null;
      is_required?: boolean | null;
      position?: number | null;
      /* Como a loja desenha (null = automatico) e se ela mostra. */
      display_style?: string | null;
      show_in_store?: boolean | null;
    }[] | null;
    product_variations?: { id: string; combination: Record<string, string>; price: number | null; stock: number; sku?: string }[] | null;
    custom_fields_config?: Array<{
      id?: string | null;
      title?: string | null;
      field_type?: "text" | "textarea" | "number" | null;
      placeholder?: string | null;
      required?: boolean | null;
      variation_signature?: string | null;
      position?: number | null;
      active?: boolean | null;
    }> | 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?: string | null;
  };
  primaryColor: string;
  storeSlug: string;
  stickyFooter?: boolean;
  buyBarColors?: BuyBarColors | null;
  whatsappNumber?: string | null;
  onPreferredImageChange?: (imageId: string | null) => void;
  trustItems?: Array<{ icon?: string | null; title?: string | null }> | null;
  combosData?: ProductCombo[];
  /* Os serviços vêm resolvidos do servidor: a biblioteca da empresa já
     filtrada pelo que este produto oferece. */
  gruposDeServico?: GrupoDeServicos[];
  servicosDoProduto?: ServicoOpcional[];
}

function formatDiscountPct(value: number) {
  const safeValue = Number(value) || 0;
  return new Intl.NumberFormat("pt-BR", {
    maximumFractionDigits: safeValue >= 10 ? 0 : 1,
    minimumFractionDigits: 0,
  }).format(safeValue);
}

function formatTierThreshold(minQty: number, unitType: string) {
  const qty = Number(minQty) || 0;
  const qtyLabel = Number.isInteger(qty)
    ? String(qty)
    : new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 2 }).format(qty);

  if (unitType === "square_meter") return `${qtyLabel} m²`;
  if (unitType === "meter" || unitType === "linear_meter") return `${qtyLabel} m`;
  return `${qtyLabel} unidades`;
}

function resolveAllowedQuantities(product: ProductPurchaseCardProps["product"]) {
  if (!product.allowed_sale_qty_enabled || !Array.isArray(product.allowed_sale_quantities)) return [];
  const values = product.allowed_sale_quantities
    .map((value) => Number(value))
    .filter((value) => Number.isFinite(value) && value > 0)
    .map((value) => Number(value.toFixed(3)));
  if (!values.length) return [];
  return Array.from(new Set(values)).sort((a, b) => a - b);
}

function resolveAdjacentAllowedQuantity(allowedValues: number[], currentValue: number, direction: -1 | 1) {
  if (!allowedValues.length) return null;

  if (direction < 0) {
    for (let index = allowedValues.length - 1; index >= 0; index -= 1) {
      if (allowedValues[index] < currentValue) return allowedValues[index];
    }
    return null;
  }

  for (let index = 0; index < allowedValues.length; index += 1) {
    if (allowedValues[index] > currentValue) return allowedValues[index];
  }
  return null;
}

function formatQuantityValue(value: number) {
  if (!Number.isFinite(value) || value <= 0) return "0";
  const normalized = Number(value.toFixed(3));
  return Number.isInteger(normalized)
    ? String(normalized)
    : new Intl.NumberFormat("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 3 }).format(normalized);
}

function getMinimumVariationUnitPrice(
  attributes?: ProductPurchaseCardProps["product"]["product_attributes"] | null
) {
  const list = Array.isArray(attributes) ? attributes : [];
  if (!list.length) return 0;
  const hasRequiredAttribute = list.some((attribute) => Boolean(attribute?.is_required));
  const relevantAttributes = hasRequiredAttribute
    ? list.filter((attribute) => Boolean(attribute?.is_required))
    : list;
  let total = 0;
  for (const attribute of relevantAttributes) {
    const options = Array.isArray(attribute?.options) ? attribute.options : [];
    const optionPrices = options
      .map((option) => (typeof option === "string" ? 0 : Number(option?.price) || 0))
      .filter((value) => Number.isFinite(value) && value >= 0);
    if (!optionPrices.length) continue;
    total += Math.min(...optionPrices);
  }
  return Math.max(0, total);
}

type NormalizedCustomField = {
  id: string;
  title: string;
  field_type: "text" | "textarea" | "number";
  placeholder: string;
  required: boolean;
  variation_signature: string | null;
  position: number;
};

function normalizeVariationSignature(combination: Record<string, string> | null | undefined) {
  if (!combination || typeof combination !== "object") return "";
  return Object.entries(combination)
    .map(([key, value]) => [String(key || "").trim().toLowerCase(), String(value || "").trim().toLowerCase()] as const)
    .filter(([key, value]) => Boolean(key) && Boolean(value))
    .sort((a, b) => a[0].localeCompare(b[0]))
    .map(([key, value]) => `${key}:${value}`)
    .join("|");
}

function normalizeText(value: string) {
  return String(value || "").trim().toLowerCase();
}

function matchesFieldVariationSignature(
  fieldSignature: string,
  params: {
    selectedVariationSignature: string;
    selectedOptions: Record<string, string[]>;
  }
) {
  const signature = normalizeText(fieldSignature);
  if (!signature) return true;

  if (signature.startsWith("option::")) {
    const [, rawAttr, rawValue] = signature.split("::");
    const attr = normalizeText(rawAttr || "");
    const value = normalizeText(rawValue || "");
    if (!attr || !value) return false;
    const selected = params.selectedOptions[Object.keys(params.selectedOptions).find((key) => normalizeText(key) === attr) || ""];
    if (!Array.isArray(selected)) return false;
    return selected.some((item) => normalizeText(item) === value);
  }

  return signature === normalizeText(params.selectedVariationSignature);
}

function buildOptionSignature(attributeName: string, optionLabel: string) {
  return `option::${normalizeText(attributeName)}::${normalizeText(optionLabel)}`;
}

function colorWithAlpha(color: string, alpha: number) {
  const safeAlpha = Math.max(0, Math.min(1, alpha));
  const value = String(color || "").trim();
  const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
  if (hexMatch) {
    let hex = hexMatch[1];
    if (hex.length === 3) {
      hex = hex
        .split("")
        .map((c) => c + c)
        .join("");
    }
    const r = Number.parseInt(hex.slice(0, 2), 16);
    const g = Number.parseInt(hex.slice(2, 4), 16);
    const b = Number.parseInt(hex.slice(4, 6), 16);
    return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
  }
  const rgbMatch = value.match(/^rgb\(\s*(\d{1,3})\s*[,\s]\s*(\d{1,3})\s*[,\s]\s*(\d{1,3})\s*\)$/i);
  if (rgbMatch) {
    const [, r, g, b] = rgbMatch;
    return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
  }
  return `rgba(15, 23, 42, ${safeAlpha})`;
}

function normalizeCustomFieldsConfig(input: ProductPurchaseCardProps["product"]["custom_fields_config"]) {
  if (!Array.isArray(input)) return [] as NormalizedCustomField[];
  return input
    .map((field, index) => {
      const title = String(field?.title || "").trim();
      if (!title || field?.active === false) return null;
      const id = String(field?.id || `cf_${index + 1}`).trim() || `cf_${index + 1}`;
      const fieldType = field?.field_type === "textarea" || field?.field_type === "number" ? field.field_type : "text";
      const variationSignature = String(field?.variation_signature || "").trim().toLowerCase();
      return {
        id,
        title,
        field_type: fieldType,
        placeholder: String(field?.placeholder || "").trim(),
        required: Boolean(field?.required),
        variation_signature: variationSignature || null,
        position: Number.isFinite(Number(field?.position)) ? Number(field?.position) : index,
      } satisfies NormalizedCustomField;
    })
    .filter((field): field is NormalizedCustomField => Boolean(field))
    .sort((a, b) => a.position - b.position);
}

export default function ProductPurchaseCard({ 
  product, 
  primaryColor,
  storeSlug,
  stickyFooter = false,
  buyBarColors = null,
  whatsappNumber,
  onPreferredImageChange,
  trustItems,
  combosData,
  gruposDeServico,
  servicosDoProduto
}: ProductPurchaseCardProps) {
  const { addToCart, setShippingSelection } = useCart();
  const allowedQuantities = useMemo(() => resolveAllowedQuantities(product), [product]);
  const minimumQuantity = useMemo(() => {
    if (allowedQuantities.length > 0) return Number(allowedQuantities[0].toFixed(3));
    const minEnabled = Boolean(product.min_sale_qty_enabled);
    const minQty = minEnabled ? Number(product.min_sale_qty) : 0;
    if (!minEnabled || !Number.isFinite(minQty) || minQty <= 0) return 1;
    return Number(minQty.toFixed(3));
  }, [allowedQuantities, product.min_sale_qty, product.min_sale_qty_enabled]);
  const [addError, setAddError] = useState("");
  const [quantity, setQuantity] = useState(minimumQuantity);
  const [quantityInput, setQuantityInput] = useState(formatQuantityValue(minimumQuantity));
  const [widthDigits, setWidthDigits] = useState("");
  const [heightDigits, setHeightDigits] = useState("");
  
  // Variations State
  const [selectedOptions, setSelectedOptions] = useState<Record<string, string[]>>({});
  const [customFieldValues, setCustomFieldValues] = useState<Record<string, string>>({});
  const hasVariations = product.product_attributes && product.product_attributes.length > 0;
  const customFieldsConfig = useMemo(
    () => normalizeCustomFieldsConfig(product.custom_fields_config),
    [product.custom_fields_config]
  );

  // Calculate total price from selected options (Independent Options Model)
  const orderedAttributes = useMemo(() => {
    return (product.product_attributes || []).slice().sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
  }, [product.product_attributes]);

  /*
    O que a vitrine desenha como escolha. Fica de fora o que o lojista
    escondeu e o que tem uma opcao so sem preco — isso vira linha de texto,
    porque nao e escolha nenhuma.
  */
  const variacoesVisiveis = useMemo(
    () => variacoesDaVitrine(product.product_attributes),
    [product.product_attributes]
  );
  const nomesQueViramEscolha = useMemo(
    () => new Set(variacoesVisiveis.escolhas.map((atributo) => atributo.nomeOriginal)),
    [variacoesVisiveis]
  );

  const requiredOptionsByAttr = useMemo(() => {
    const map: Record<string, string[]> = {};
    orderedAttributes.forEach((attr) => {
      const options = (attr.options || []).map((opt) =>
        typeof opt === "string" ? { label: opt, is_required: false } : opt
      );
      const required = options.filter((opt) => Boolean(opt.is_required)).map((opt) => opt.label);
      /*
        Atributo obrigatorio que a vitrine nao desenha (escondido pelo
        lojista, ou de uma opcao so): marcado por conta. Sem isto o botao
        Comprar travaria esperando um clique num botao que nao existe mais —
        e a escolha continua indo para o pedido, igual a antes.
      */
      if (attr.is_required && !nomesQueViramEscolha.has(attr.name) && options.length === 1 && options[0]?.label) {
        if (!required.includes(options[0].label)) required.push(options[0].label);
      }
      if (required.length) map[attr.name] = required;
    });
    return map;
  }, [orderedAttributes, nomesQueViramEscolha]);

  const effectiveSelectedOptions = useMemo(() => {
    const next: Record<string, string[]> = {};
    orderedAttributes.forEach((attr) => {
      const required = requiredOptionsByAttr[attr.name] || [];
      const userSelected = selectedOptions[attr.name] || [];
      if (attr.allow_multiple) {
        const merged = Array.from(new Set([...required, ...userSelected]));
        if (merged.length) next[attr.name] = merged;
      } else {
        const chosen = userSelected[0] || required[0];
        if (chosen) next[attr.name] = [chosen];
      }
    });
    return next;
  }, [orderedAttributes, requiredOptionsByAttr, selectedOptions]);

  const selectedOptionImageId = useMemo(() => {
    const availableImageIds = new Set((product.product_images || []).map((image) => image.id));
    for (const attribute of orderedAttributes) {
      const selected = effectiveSelectedOptions[attribute.name] || [];
      if (!selected.length || !Array.isArray(attribute.options)) continue;
      for (const selectedLabel of selected) {
        const option = attribute.options.find(
          (item) => (typeof item === "string" ? item : item.label) === selectedLabel
        );
        if (!option || typeof option === "string") continue;
        if (option.image_id && availableImageIds.has(option.image_id)) {
          return option.image_id;
        }
      }
    }
    return null;
  }, [effectiveSelectedOptions, orderedAttributes, product.product_images]);

  /*
    Os serviços marcados. Ficam à parte das variações no estado, mas viajam
    junto delas no pedido: são opções com preço, como qualquer outra.
  */
  /* Os serviços da loja, agrupados como o lojista mandou. A regra de quantos
     podem ser marcados é do grupo, e mora na lib com teste. */
  const vitrineDeServicosDoProduto = useMemo(
    () => vitrineDeServicos(gruposDeServico ?? [], servicosDoProduto ?? []),
    [gruposDeServico, servicosDoProduto]
  );
  const [servicosMarcados, setServicosMarcados] = useState<string[]>([]);
  const servicosEscolhidos = useMemo(
    () => escolhidosComGrupo(vitrineDeServicosDoProduto, servicosMarcados),
    [vitrineDeServicosDoProduto, servicosMarcados]
  );
  const servicoPendente = useMemo(
    () => grupoPendente(vitrineDeServicosDoProduto, servicosMarcados),
    [vitrineDeServicosDoProduto, servicosMarcados]
  );

  const escolherServico = useCallback((servico: ServicoOpcional, caixa: GrupoNaVitrine) => {
    setAddError("");
    setServicosMarcados((antes) => alternarEscolha(antes, servico, caixa));
  }, []);

  const [selectedCombos, setSelectedCombos] = useState<SelectedComboData[]>([]);
  const [selectedProductShipping, setSelectedProductShipping] = useState<ShippingSelection | null>(null);

  useEffect(() => {
    if (!onPreferredImageChange) return;
    onPreferredImageChange(selectedOptionImageId);
  }, [onPreferredImageChange, selectedOptionImageId]);

  /* O que os serviços somam: por peça ou uma vez só na venda. */
  const precoDosServicos = useMemo(() => {
    const soma = somaDosServicos(vitrineDeServicosDoProduto, servicosMarcados);
    return { perQuantity: soma.porQuantidade, oneTime: soma.umaVez };
  }, [vitrineDeServicosDoProduto, servicosMarcados]);

  /* Cada opção marcada diz em que se multiplica: por m², por peça ou uma
     vez no pedido. Serviço "por unidade" num produto por m² conta por peça. */
  const optionPriceBreakdown = useMemo(() => {
    const escolhidas: Array<{ price?: number | null; price_unit?: string | null; price_applies_per_quantity?: boolean | null }> = [];
    for (const attr of orderedAttributes) {
      const selected = effectiveSelectedOptions[attr.name] || [];
      if (!selected.length || !Array.isArray(attr.options)) continue;
      for (const label of selected) {
        const option = attr.options.find((o) => (typeof o === "string" ? o : o.label) === label);
        if (!option || typeof option === "string") continue;
        escolhidas.push(option);
      }
    }
    return somarAdicionais(escolhidas, product.unit_type || "unit");
  }, [effectiveSelectedOptions, orderedAttributes, product.unit_type]);
  const adicionais = useMemo(
    () =>
      juntarAdicionais(optionPriceBreakdown, {
        porMedida: 0,
        porPeca: precoDosServicos.perQuantity,
        umaVez: precoDosServicos.oneTime,
      }),
    [optionPriceBreakdown, precoDosServicos]
  );
  const minimumVariationUnitPrice = useMemo(
    () => getMinimumVariationUnitPrice(product.product_attributes),
    [product.product_attributes]
  );
  const trackInventory = product.track_inventory !== false;
  /* 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 e sem aviso de estoque. */
  const inventoryScope = product.inventory_scope || "product";
  const inventoryAttributeName = (product.inventory_attribute_name || "").trim().toLowerCase();
  const selectedOptionEntries = useMemo(() => {
    return orderedAttributes.flatMap((attribute) => {
      const selected = effectiveSelectedOptions[attribute.name] || [];
      if (!selected.length) return [];
      return selected
        .map((selectedLabel) => {
          const option = (attribute.options || []).find((item) => {
            const label = typeof item === "string" ? item : item.label;
            return label === selectedLabel;
          });
          if (!option || typeof option === "string") return null;
          const optionId = typeof option.id === "string" ? option.id : null;
          return {
            option_id: optionId,
            attribute_name: attribute.name,
            label: option.label,
            price: Number(option.price) || 0,
            price_applies_per_quantity: option.price_applies_per_quantity !== false,
            price_unit: option.price_unit ?? null,
            stock: Number.isFinite(Number(option.stock)) ? Number(option.stock) : null,
            affects_stock: inventoryScope === "option" && attribute.name.trim().toLowerCase() === inventoryAttributeName,
          };
        })
        .filter(Boolean) as Array<{
        option_id: string | null;
        attribute_name: string;
        label: string;
        price: number;
        price_applies_per_quantity: boolean;
        price_unit: string | null;
        stock: number | null;
        affects_stock: boolean;
      }>;
    });
  }, [effectiveSelectedOptions, inventoryAttributeName, inventoryScope, orderedAttributes]);

  /* Estoque v2: o banco diz quanto da para vender DESTA combinacao (a mesma
     conta do PDV e do orcamento: item pronto da celula escolhida, ou os
     insumos controlados da composicao). As escolhas vao no mesmo formato do
     carrinho, servicos inclusive (servico com insumo tambem sai do estoque).
     Fora do v2 nada muda: o saldo vem do produto, como sempre. */
  const ehEstoqueV2 = product.estoque_v2 === true;
  const escolhaParaOEstoque = useMemo(
    () =>
      ehEstoqueV2
        ? {
            inventory_scope: inventoryScope,
            options: [
              ...selectedOptionEntries.map((entry) => ({
                option_id: entry.option_id,
                attribute_name: entry.attribute_name,
                label: entry.label,
              })),
              ...servicosEscolhidos.map(({ servico, grupo }) => servicoComoOpcao(servico, grupo.nome)),
            ],
          }
        : null,
    [ehEstoqueV2, inventoryScope, selectedOptionEntries, servicosEscolhidos]
  );
  const escolhaCompletaParaOEstoque = hasVariations
    ? orderedAttributes.every(
        (attr) => !attr.is_required || !nomesQueViramEscolha.has(attr.name) || (effectiveSelectedOptions[attr.name] || []).length > 0
      )
    : true;
  const estoqueV2 = useEstoqueDaEscolha({
    ativo: ehEstoqueV2,
    slug: storeSlug,
    productId: product.id,
    selected: escolhaParaOEstoque,
    largura: product.unit_type === "square_meter" ? Number(widthDigits.replace(/\D/g, "")) / 100 || null : null,
    altura: product.unit_type === "square_meter" ? Number(heightDigits.replace(/\D/g, "")) / 100 || null : null,
    completa: escolhaCompletaParaOEstoque,
  });
  /* O saldo so limita quando o produto controla estoque e NAO permite estoque
     negativo (no v2: quando o banco diz que limita). Com a permissao ligada,
     a pagina vende como se houvesse saldo: sem "Indisponivel", sem teto de
     quantidade e sem aviso de estoque. No v2, enquanto a resposta nao chega,
     nada trava (o servidor confere de novo no carrinho e ao fechar) — menos
     o produto que o servidor ja marcou esgotado: esse nasce "Indisponivel". */
  const esgotadoAntesDaResposta = ehEstoqueV2 && estoqueV2 == null && product.estoque_v2_esgotado === true;
  const estoqueLimita = ehEstoqueV2
    ? estoqueV2 ? estoqueV2.limita === true : esgotadoAntesDaResposta
    : estoqueLimitaAVenda(product);
  /* Escolha que o estoque v2 nao acha (valor fora da grade): nao vende. */
  const escolhaForaDoEstoque = ehEstoqueV2 && estoqueV2?.motivo === "falta_escolher" && escolhaCompletaParaOEstoque;

  const trackedOptionStocks = selectedOptionEntries
    .filter((entry) => entry.affects_stock && typeof entry.stock === "number")
    .map((entry) => Number(entry.stock));
  const matchedVariant = useMemo(() => {
    if (inventoryScope !== "variant") return null;
    const selectedFlat: Record<string, string> = {};
    for (const [attribute, labels] of Object.entries(effectiveSelectedOptions)) {
      const label = labels[0];
      if (!label) continue;
      selectedFlat[attribute.trim().toLowerCase()] = label.trim().toLowerCase();
    }
    const selectedKeys = Object.keys(selectedFlat).sort();
    if (!selectedKeys.length) return null;
    const variations = Array.isArray(product.product_variations) ? product.product_variations : [];
    for (const variation of variations) {
      const comboRaw = variation.combination || {};
      const combo: Record<string, string> = {};
      for (const [attribute, label] of Object.entries(comboRaw)) {
        combo[attribute.trim().toLowerCase()] = (label || "").trim().toLowerCase();
      }
      const comboKeys = Object.keys(combo).sort();
      if (comboKeys.length !== selectedKeys.length) continue;
      const same = selectedKeys.every((key) => combo[key] === selectedFlat[key]);
      if (same) return variation;
    }
    return null;
  }, [effectiveSelectedOptions, inventoryScope, product.product_variations]);
  const selectedVariationSignature = useMemo(() => {
    if (matchedVariant?.combination) {
      return normalizeVariationSignature(matchedVariant.combination);
    }
    const selectedFlat: Record<string, string> = {};
    for (const [attribute, labels] of Object.entries(effectiveSelectedOptions)) {
      const label = labels[0];
      if (!label) continue;
      selectedFlat[attribute] = label;
    }
    return normalizeVariationSignature(selectedFlat);
  }, [effectiveSelectedOptions, matchedVariant]);
  const alwaysVisibleCustomFields = useMemo(
    () => customFieldsConfig.filter((field) => !field.variation_signature),
    [customFieldsConfig]
  );
  const linkedVisibleCustomFields = useMemo(() => {
    if (!customFieldsConfig.length) return [] as NormalizedCustomField[];
    return customFieldsConfig.filter((field) => {
      if (!field.variation_signature) return false;
      return matchesFieldVariationSignature(field.variation_signature, {
        selectedVariationSignature,
        selectedOptions: effectiveSelectedOptions,
      });
    });
  }, [customFieldsConfig, effectiveSelectedOptions, selectedVariationSignature]);
  const visibleCustomFields = useMemo(
    () => [...alwaysVisibleCustomFields, ...linkedVisibleCustomFields].sort((a, b) => a.position - b.position),
    [alwaysVisibleCustomFields, linkedVisibleCustomFields]
  );
  const missingRequiredCustomFields = useMemo(() => {
    return visibleCustomFields.filter((field) => {
      if (!field.required) return false;
      const value = String(customFieldValues[field.id] || "").trim();
      return !value;
    });
  }, [customFieldValues, visibleCustomFields]);
  const hasValidCustomFields = missingRequiredCustomFields.length === 0;
  const hasTrackedSelection =
    ehEstoqueV2
      ? !escolhaForaDoEstoque
      : !trackInventory
      ? true
      : inventoryScope === "option"
        ? selectedOptionEntries.some((entry) => entry.affects_stock)
        : inventoryScope === "variant"
          ? matchedVariant !== null
          : true;

  const currentStock =
    !estoqueLimita
      ? Number.POSITIVE_INFINITY
      : esgotadoAntesDaResposta
        ? 0
        : ehEstoqueV2
        ? /* Ainda falta escolher tamanho/cor: o banco responde 0 porque nao
             sabe a celula, mas isso nao e "esgotado" (era "Maximo disponivel:
             0" e "Indisponivel" antes de o cliente escolher). O botao ja pede a
             escolha; com tudo escolhido e fora da grade, vale o 0. */
          estoqueV2?.motivo === "falta_escolher" && !escolhaCompletaParaOEstoque
          ? Number.POSITIVE_INFINITY
          : estoqueV2?.disponivel ?? Number.POSITIVE_INFINITY
      : inventoryScope === "option" && trackedOptionStocks.length > 0
        ? Math.min(...trackedOptionStocks)
        : inventoryScope === "variant" && matchedVariant
          ? Number(matchedVariant.stock) || 0
          : product.stock;
  const normalizedStock = Number.isFinite(Number(currentStock))
    ? Math.max(0, Number(currentStock))
    : Number.POSITIVE_INFINITY;
  
  const isVariationSelected = hasVariations
    ? orderedAttributes.every((attr) => {
        if (!attr.is_required) return true;
        /* Escondido ou informativo ja foi resolvido acima; exigir clique
           nele seria pedir o impossivel. */
        if (!nomesQueViramEscolha.has(attr.name)) return true;
        return (effectiveSelectedOptions[attr.name] || []).length > 0;
      })
    : true;

  const handleOptionSelect = (attrName: string, option: string, allowMultiple?: boolean, isRequiredOption?: boolean) => {
    setAddError("");
    setSelectedOptions((prev) => {
      const required = requiredOptionsByAttr[attrName] || [];
      const current = allowMultiple
        ? Array.from(new Set([...required, ...(prev[attrName] || [])]))
        : (prev[attrName]?.[0] ? [prev[attrName][0]] : (required[0] ? [required[0]] : []));
      if (allowMultiple) {
        const exists = current.includes(option);
        if (exists) {
          if (isRequiredOption) return prev;
          const next = (prev[attrName] || []).filter((v) => v !== option);
          return { ...prev, [attrName]: next };
        }
        return { ...prev, [attrName]: [...(prev[attrName] || []), option] };
      }
      const isCurrentlySelected = current[0] === option;
      if (isCurrentlySelected) {
        if (isRequiredOption) return prev;
        const next = { ...prev };
        delete next[attrName];
        return next;
      }
      return { ...prev, [attrName]: [option] };
    });
  };

  const unitType = product.unit_type || "unit";
  const isSquareMeter = unitType === "square_meter";
  const usesAllowedQuantitySteps = !isSquareMeter && allowedQuantities.length > 0;
  const isIntervalMode = Boolean(product.allowed_sale_qty_enabled) && allowedQuantities.length > 0;
  const selectedQuantityRaw =
    usesAllowedQuantitySteps && !allowedQuantities.some((value) => Math.abs(value - quantity) <= 0.000001)
      ? allowedQuantities[0]
      : quantity;
  const selectedQuantity = Math.max(minimumQuantity, selectedQuantityRaw);
  const quantityInputDigits = quantityInput.replace(/\D/g, "");
  const manualInputQuantity =
    !usesAllowedQuantitySteps && quantityInputDigits
      ? Math.max(minimumQuantity, Number.parseInt(quantityInputDigits, 10))
      : null;
  const quantityForValidation =
    usesAllowedQuantitySteps || manualInputQuantity === null ? selectedQuantity : manualInputQuantity;

  const formatDigitsToMeters = (digits: string) => {
    const cleaned = digits.replace(/\D/g, "");
    if (!cleaned) return "";

    const padded = cleaned.padStart(3, "0");
    const intPartRaw = padded.slice(0, -2);
    const intPart = intPartRaw.replace(/^0+(?=\d)/, "") || "0";
    const fracPart = padded.slice(-2);
    return `${intPart},${fracPart}`;
  };

  const widthM = formatDigitsToMeters(widthDigits);
  const heightM = formatDigitsToMeters(heightDigits);
  const parsedWidth = Number(widthM.replace(",", "."));
  const parsedHeight = Number(heightM.replace(",", "."));
  const isValidDims = isSquareMeter ? parsedWidth > 0 && parsedHeight > 0 : true;

  const getQuantityCheck = (nextQuantity: number) => {
    const effectiveQuantity = calculateEffectiveQuantity({
      quantity: nextQuantity,
      unit_type: unitType,
      width_m: isSquareMeter ? parsedWidth : undefined,
      height_m: isSquareMeter ? parsedHeight : undefined,
    });

    return checkSaleQuantityLimits({
      min_sale_qty_enabled: product.min_sale_qty_enabled,
      min_sale_qty: product.min_sale_qty,
      max_sale_qty_enabled: product.max_sale_qty_enabled,
      max_sale_qty: product.max_sale_qty,
      allowed_sale_qty_enabled: product.allowed_sale_qty_enabled,
      allowed_sale_quantities: product.allowed_sale_quantities,
      effectiveQuantity,
    });
  };
  const hasStockForQuantity = (nextQuantity: number) => {
    if (!estoqueLimita) return true;
    if (!Number.isFinite(normalizedStock)) return true;
    /* No v2 o banco ja respondeu em pecas para a medida escolhida. */
    if (ehEstoqueV2) return nextQuantity <= normalizedStock + 0.000001;
    const nextEffectiveQuantity = calculateEffectiveQuantity({
      quantity: nextQuantity,
      unit_type: unitType,
      width_m: isSquareMeter ? parsedWidth : undefined,
      height_m: isSquareMeter ? parsedHeight : undefined,
    });
    return nextEffectiveQuantity <= normalizedStock + 0.000001;
  };

  const quantityCheck = getQuantityCheck(quantityForValidation);

  const nextQuantityDown = usesAllowedQuantitySteps
    ? resolveAdjacentAllowedQuantity(allowedQuantities, selectedQuantity, -1)
    : Math.max(minimumQuantity, selectedQuantity - 1);
  const nextQuantityUp = usesAllowedQuantitySteps
    ? resolveAdjacentAllowedQuantity(allowedQuantities, selectedQuantity, 1)
    : selectedQuantity + 1;
  const canDecreaseQuantity =
    nextQuantityDown !== null &&
    nextQuantityDown > 0 &&
    (usesAllowedQuantitySteps ? getQuantityCheck(nextQuantityDown).isValid : selectedQuantity > minimumQuantity);
  const canIncreaseQuantity =
    nextQuantityUp !== null &&
    getQuantityCheck(nextQuantityUp).isValid &&
    hasStockForQuantity(nextQuantityUp);

  const handleQuantityChange = (delta: number) => {
    setAddError("");
    if (usesAllowedQuantitySteps) {
      const direction: -1 | 1 = delta < 0 ? -1 : 1;
      const nextAllowed = resolveAdjacentAllowedQuantity(allowedQuantities, selectedQuantity, direction);
      if (nextAllowed === null) return;
      if (!getQuantityCheck(nextAllowed).isValid) return;
      if (!hasStockForQuantity(nextAllowed)) return;
      setQuantity(nextAllowed);
      setQuantityInput(formatQuantityValue(nextAllowed));
      return;
    }

    const next = Math.max(minimumQuantity, selectedQuantity + delta);
    if (!getQuantityCheck(next).isValid) return;
    if (!hasStockForQuantity(next)) return;
    setQuantity(next);
    setQuantityInput(String(next));
  };

  const handleQuantityInputChange = (rawValue: string) => {
    if (usesAllowedQuantitySteps) return;
    const digitsOnly = rawValue.replace(/\D/g, "");
    setAddError("");
    setQuantityInput(digitsOnly);
    if (!digitsOnly) return;
    const next = Number.parseInt(digitsOnly, 10);
    if (Number.isFinite(next) && getQuantityCheck(next).isValid && hasStockForQuantity(next)) {
      setQuantity(Math.max(minimumQuantity, next));
    }
  };

  const normalizeQuantityInput = () => {
    if (usesAllowedQuantitySteps) {
      setQuantityInput(formatQuantityValue(selectedQuantity));
      return;
    }

    const digitsOnly = quantityInput.replace(/\D/g, "");
    if (!digitsOnly) {
      setQuantityInput(String(selectedQuantity));
      return;
    }
    const normalized = Math.max(minimumQuantity, Number.parseInt(digitsOnly, 10));
    if (!getQuantityCheck(normalized).isValid || !hasStockForQuantity(normalized)) {
      setQuantityInput(String(normalized));
      return;
    }
    setQuantity(normalized);
    setQuantityInput(String(normalized));
  };

  const areaEach = isSquareMeter && isValidDims ? parsedWidth * parsedHeight : 0;
  const totalArea = isSquareMeter && isValidDims ? areaEach * quantityForValidation : 0;
  const effectiveQty = calculateEffectiveQuantity({
    quantity: quantityForValidation,
    unit_type: unitType,
    width_m: isSquareMeter ? parsedWidth : undefined,
    height_m: isSquareMeter ? parsedHeight : undefined,
  });
  const exceedsStock =
    estoqueLimita &&
    Number.isFinite(normalizedStock) &&
    (ehEstoqueV2 ? quantityForValidation : effectiveQty) > normalizedStock + 0.000001;
  /* A conta da linha inteira, a mesma do carrinho e do checkout. O valor
     mínimo do item entra por último; no produto por m² só depois das medidas
     (sem elas, a quantidade conta como m² e o mínimo viraria "preço do m²"). */
  const valorMinimo = valorMinimoDoItem(product, "loja");
  const contaSemMinimo = precoDaLinha({
    precoBase: Number(product.price) || 0,
    faixas: product.product_price_tiers || [],
    pecas: quantityForValidation,
    quantidadeEfetiva: effectiveQty,
    adicionais,
    soVariacao: Boolean(product.use_variation_price_only),
  });
  const conta =
    valorMinimo !== null && (!isSquareMeter || isValidDims)
      ? precoDaLinha({
          precoBase: Number(product.price) || 0,
          faixas: product.product_price_tiers || [],
          pecas: quantityForValidation,
          quantidadeEfetiva: effectiveQty,
          adicionais,
          soVariacao: Boolean(product.use_variation_price_only),
          valorMinimo,
        })
      : contaSemMinimo;
  const minimoAplicado = conta.minimoAplicado;
  const optionsPrice = adicionais.porMedida + adicionais.porPeca + adicionais.umaVez;

  const intervalTierUnitPrices = useMemo(() => {
    if (!isIntervalMode) return [] as number[];
    return (product.product_price_tiers || [])
      .filter((tier) => tier.active !== false)
      .map((tier) => {
        const minQty = Number(tier.min_qty);
        const maxQty = tier.max_qty === null || tier.max_qty === undefined ? null : Number(tier.max_qty);
        const unitPrice = Number(tier.unit_price);
        if (!Number.isFinite(minQty) || minQty <= 0 || !Number.isFinite(unitPrice) || unitPrice <= 0) return null;
        if (maxQty === null || !Number.isFinite(maxQty) || Math.abs(maxQty - minQty) > 0.000001) return null;
        const isAllowed = allowedQuantities.some((value) => Math.abs(value - minQty) <= 0.000001);
        if (!isAllowed) return null;
        return unitPrice;
      })
      .filter((value): value is number => value !== null);
  }, [allowedQuantities, isIntervalMode, product.product_price_tiers]);

  const pricingBase = Number(product.price) || 0;
  const tierResolution = resolveTierPrice(pricingBase, effectiveQty, product.product_price_tiers || []);
  /* Por medida, com peça e pedido rateados: é o que o carrinho guarda. Para
     mostrar "R$ X / m²" vale sempre o preço sem o mínimo: com ele, o m² de uma
     peça pequena sairia R$ 280,00 e confundiria. */
  const resolvedUnitPrice = conta.precoPorMedida;
  const precoDaMedidaNaTela = contaSemMinimo.precoPorMedida;
  const appliedTier = tierResolution.tier;
  const appliedDiscountPct = product.use_variation_price_only
    ? 0
    : calculateTierDiscountPct(pricingBase, tierResolution.unitPrice);
  const showcaseTierDiscount = getBestTierDiscount(pricingBase, product.product_price_tiers || []);

  const baseTotalPrice = minimoAplicado !== null
    ? minimoAplicado
    : isSquareMeter
      ? (isValidDims ? resolvedUnitPrice * effectiveQty : 0)
      : resolvedUnitPrice * effectiveQty;
  const totalPrice = product.price_on_request
    ? null
    : baseTotalPrice;
  const combosTotalPrice = useMemo(() => {
    if (!selectedCombos || selectedCombos.length === 0) return 0;
    return selectedCombos.reduce((sum, selectedCombo) => {
      const unitPrice = Number(selectedCombo?.discountedPrice) || 0;
      const quantity = Number(selectedCombo?.quantity) || 0;
      return sum + unitPrice * quantity;
    }, 0);
  }, [selectedCombos]);
  const totalWithCombos = totalPrice === null ? null : totalPrice + combosTotalPrice;
  const hasSelectedCombos = selectedCombos.length > 0;

  const lowestIntervalUnitPrice = intervalTierUnitPrices.length > 0 ? Math.min(...intervalTierUnitPrices) : null;
  const formatMoney = (value: number) =>
    new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(value);
  /* O que este visitante pode ver e fazer (regras da loja). */
  const visao = useVisaoDaLoja();
  const router = useRouter();
  const textoSemPreco = visao.textoNoLugarDoPreco;
  /* A mensagem de duvida vem do modelo que a loja escreveu em WhatsApp >
     Mensagens (antes a loja editava o texto e a vitrine ignorava). */
  const mensagemDeDuvida = preencherModelo(
    visao.regras.mensagensDoWhatsapp.produto || "Olá! Tenho interesse no produto {{nome_produto}}",
    { nome_produto: product.name }
  );
  const priceLabel = product.price_on_request
    ? textoSemPreco
    : product.use_variation_price_only && optionsPrice <= 0 && minimumVariationUnitPrice > 0
      ? formatMoney(minimumVariationUnitPrice)
    : isIntervalMode && lowestIntervalUnitPrice !== null
      ? formatMoney(lowestIntervalUnitPrice)
      : isSquareMeter
        ? `${formatMoney(precoDaMedidaNaTela)} / m²`
        : minimoAplicado !== null
          ? formatMoney(precoDaMedidaNaTela)
          : formatMoney(resolvedUnitPrice);
  /* A escada do produto medido, só de leitura, e o empurrão para a próxima
     faixa: quantas peças faltam para o m² cair. */
  const faixasPorArea = vendidoPorMedida(unitType) && !product.price_on_request
    ? (product.product_price_tiers || [])
        .filter((tier) => tier.active !== false)
        .slice()
        .sort((a, b) => (Number(a.min_qty) || 0) - (Number(b.min_qty) || 0))
    : [];
  const numeroEnxuto = (n: number) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 2 }).format(n);
  const escadaDeFaixas = faixasPorArea.map((faixa, indice) => {
    const medida = nomeDaMedida(unitType);
    const de = Number(faixa.min_qty) || 0;
    const ate = faixa.max_qty === null || faixa.max_qty === undefined ? null : Number(faixa.max_qty);
    const rotulo =
      ate === null
        ? `${numeroEnxuto(de)} ${medida} ou mais`
        : indice === 0 && de <= 1
          ? `Até ${numeroEnxuto(ate)} ${medida}`
          : `De ${numeroEnxuto(de)} a ${numeroEnxuto(ate)} ${medida}`;
    return {
      chave: `${faixa.min_qty}-${faixa.unit_price}`,
      rotulo,
      preco: (Number(faixa.unit_price) || 0) + adicionais.porMedida,
      atual: Boolean(appliedTier) && Number(appliedTier?.min_qty) === de,
    };
  });
  const dicaDaProximaFaixa = (() => {
    if (!faixasPorArea.length || product.use_variation_price_only) return "";
    const medida = nomeDaMedida(unitType);
    const proxima = faixasPorArea.find((faixa) => (Number(faixa.min_qty) || 0) > effectiveQty + 0.000001);
    if (!proxima) return "";
    const precoLa = (Number(proxima.unit_price) || 0) + adicionais.porMedida;
    if (precoLa >= tierResolution.unitPrice + adicionais.porMedida) return "";
    const inicio = Number(proxima.min_qty) || 0;
    if (!(areaEach > 0)) return `A partir de ${numeroEnxuto(inicio)} ${medida}, cada ${medida} sai por ${formatMoney(precoLa)}.`;
    const pecas = Math.ceil((inicio - 0.000001) / areaEach);
    if (pecas <= quantityForValidation) return "";
    return `Leve ${pecas} ${pecas === 1 ? "peça" : "peças"} (${numeroEnxuto(pecas * areaEach)} ${medida}) e cada ${medida} cai para ${formatMoney(precoLa)}.`;
  })();
  const hideSquareMeterUnitPrice = isSquareMeter && !product.price_on_request;
  const priceTitle = product.use_variation_price_only && optionsPrice <= 0 && minimumVariationUnitPrice > 0
    ? "A partir de"
    : isIntervalMode
    ? "A partir de"
    : hideSquareMeterUnitPrice
      ? (isValidDims ? "Preço sob medida" : "Preço por m²")
      : isSquareMeter
        ? "Preço por m²"
        : "Preço unitário";
  /* Enquanto nao ha medidas, mostrar o preco do metro quadrado. Antes aparecia
     so "Informe as medidas": o cliente nao tinha como estimar nem comparar, e o
     valor do m2 so aparecia depois, dentro do carrinho. */
  const priceValue = hideSquareMeterUnitPrice
    ? (isValidDims ? formatMoney(totalPrice || 0) : `${formatMoney(precoDaMedidaNaTela)} / m²`)
    : priceLabel;
  /* A etiqueta do valor mínimo aparece sempre que o produto tem mínimo na
     loja, antes de adicionar, para não haver surpresa. O cliente nunca vê
     quanto o tamanho "daria": só o preço final. */
  const mostraMinimo = valorMinimo !== null && !product.price_on_request;

  const quantityRuleMessage = useMemo(() => {
    if (quantityCheck.isBelowMin && quantityCheck.minQty !== null) {
      return `Quantidade minima: ${formatQuantityValue(quantityCheck.minQty)}.`;
    }
    if (quantityCheck.isAboveMax && quantityCheck.maxQty !== null) {
      return `Quantidade maxima: ${formatQuantityValue(quantityCheck.maxQty)}.`;
    }
    if (quantityCheck.isNotAllowedValue && quantityCheck.allowedQuantities.length > 0) {
      return `Use quantidade permitida: ${quantityCheck.allowedQuantities
        .map((value) => formatQuantityValue(value))
        .join(", ")}.`;
    }
    return "";
  }, [
    quantityCheck.allowedQuantities,
    quantityCheck.isAboveMax,
    quantityCheck.isBelowMin,
    quantityCheck.isNotAllowedValue,
    quantityCheck.maxQty,
    quantityCheck.minQty,
  ]);
  /* O saldo aparece conforme a regra da loja: o numero exato (como sempre),
     so o aviso de "ultimas unidades", ou nada. O bloqueio por falta de
     estoque continua igual nos tres casos. */
  const estoqueVisivel = visao.regras.estoqueVisivel;
  const limiteEstoqueBaixo = visao.regras.limiteEstoqueBaixo;
  const stockMaxHelperMessage = useMemo(() => {
    if (!estoqueLimita) return "";
    if (inventoryScope !== "product" && !hasTrackedSelection) {
      return "";
    }
    if (!Number.isFinite(normalizedStock)) return "";
    if (estoqueVisivel === "never") return "";
    if (estoqueVisivel === "low_only") {
      return normalizedStock > 0 && normalizedStock <= limiteEstoqueBaixo ? "Últimas unidades!" : "";
    }
    return `Máximo disponível: ${formatQuantityValue(normalizedStock)}.`;
  }, [estoqueVisivel, estoqueLimita, hasTrackedSelection, inventoryScope, limiteEstoqueBaixo, normalizedStock]);

  const productForCart = (() => {
    if (ehEstoqueV2) {
      /* Estoque v2: o carrinho leva o maximo DESTA combinacao (em pecas) e
         confere de novo no servidor a cada mudanca (soma por celula). Por
         area, o teto local em m2 nao vale: so o servidor confere. */
      return {
        ...product,
        stock: Number.isFinite(normalizedStock) ? normalizedStock : null,
        track_inventory: estoqueLimita && !isSquareMeter,
        allow_negative_stock: !estoqueLimita,
        estoque_v2: true,
      };
    }
    return {
      ...product,
      stock: normalizedStock,
      track_inventory: trackInventory,
      allow_negative_stock: product.allow_negative_stock === true,
    };
  })();
  const currentShippingItem = useMemo(
    () => [
      {
        product_id: product.id,
        /* O nome vai junto para o aviso de frete poder dizer QUAL produto esta
           sem medidas, em vez de "um dos itens". */
        name: product.name,
        quantity: quantityForValidation,
        unit_type: unitType,
        width_m: isSquareMeter ? parsedWidth : undefined,
        height_m: isSquareMeter ? parsedHeight : undefined,
        shipping_weight_kg: product.shipping_weight_kg ?? null,
        shipping_width_cm: product.shipping_width_cm ?? null,
        shipping_height_cm: product.shipping_height_cm ?? null,
        shipping_length_cm: product.shipping_length_cm ?? null,
        production_time_value: product.production_time_value ?? null,
        production_time_unit: product.production_time_unit ?? null,
        price_on_request: product.price_on_request,
      },
    ],
    [
      isSquareMeter,
      parsedHeight,
      parsedWidth,
      product.id,
      product.name,
      product.price_on_request,
      product.production_time_unit,
      product.production_time_value,
      product.shipping_height_cm,
      product.shipping_length_cm,
      product.shipping_weight_kg,
      product.shipping_width_cm,
      quantityForValidation,
      unitType,
    ]
  );
  /*
    O frete pertence ao carrinho, nao a pagina do produto. Sem espelhar a
    escolha aqui, quem trocava a retirada por SEDEX na pagina voltava ao
    carrinho e continuava vendo a retirada: duas respostas diferentes para a
    mesma pergunta.

    Retirada vale sempre. Cotacao de transportadora so vale se o carrinho for
    exatamente este pacote, e o CartContext descarta quando a assinatura nao
    bate, o que deixa o carrinho pedindo uma nova cotacao em vez de exibir um
    frete que nao e mais o escolhido.
  */
  /*
    Cor vazia significa "deixa como sempre foi", entao loja que nunca abriu essa
    tela continua exatamente igual. Quando o lojista escolhe um fundo mas nao
    escolhe a cor do texto, o texto e calculado sobre esse fundo — senao um
    fundo escuro engoliria o texto padrao, que e escuro.
  */
  const buyBar = useMemo(() => {
    const fundo = String(buyBarColors?.bg || "").trim();
    const botao = String(buyBarColors?.button || "").trim() || primaryColor;
    const textoEscolhido = String(buyBarColors?.text || "").trim();
    return {
      bg: fundo || null,
      button: botao,
      buttonText: getReadableTextColor(botao),
      text: textoEscolhido || (fundo ? getReadableTextColor(fundo) : null),
    };
  }, [buyBarColors?.bg, buyBarColors?.button, buyBarColors?.text, primaryColor]);

  /* Cor de referencia dos controles da barra: segue o texto escolhido e, no
     padrao, o cinza escuro de sempre. */
  const quantityControlColor = buyBar.text || "#3F3F46";

  const handleProductShippingSelection = useCallback(
    (selection: ShippingSelection | null) => {
      setSelectedProductShipping(selection);
      setShippingSelection(selection);
    },
    [setShippingSelection]
  );

  const effectiveSelectedProductShipping = useMemo(() => {
    if (!selectedProductShipping) return null;
    /* Retirada nao tem assinatura de pacote para conferir: o preco e do balcao,
       nao de uma cotacao sobre as medidas deste produto. */
    if (isPickupShippingOption(selectedProductShipping.option)) return selectedProductShipping;
    const packageResult = buildShippingPackageFromItems(currentShippingItem);
    if (!packageResult.ok || selectedProductShipping.package_signature !== packageResult.packageSignature) {
      return null;
    }
    return selectedProductShipping;
  }, [currentShippingItem, selectedProductShipping]);

  const whatsappDigits = normalizeWhatsappForLink(whatsappNumber);
  const trustIconMap = useMemo(() => ({
    truck: Truck,
    shieldCheck: ShieldCheck,
    creditCard: CreditCard,
    package: Package,
    clock: Clock,
    badgeCheck: BadgeCheck,
    mapPin: MapPin,
    headphones: Headphones,
  }), []);
  const normalizedTrustItems = useMemo(() => {
    const source = Array.isArray(trustItems) ? trustItems : [];
    const items = source
      .map((item) => ({
        icon: String(item?.icon || "").trim(),
        title: String(item?.title || "").trim(),
      }))
      .filter((item) => item.icon || item.title)
      .slice(0, 2);
    if (items.length >= 2) return items;
    const defaults = [
      { icon: "truck", title: "Entrega Rápida e Segura" },
      { icon: "shieldCheck", title: "Garantia de Qualidade" },
    ];
    while (items.length < 2) items.push(defaults[items.length]);
    return items;
  }, [trustItems]);
  const primaryTextColor = useMemo(() => getReadableTextColor(primaryColor), [primaryColor]);
  const primaryOnWhite = useMemo(
    () => ensureColorContrast(primaryColor, "#FFFFFF", { minRatio: 4.5 }),
    [primaryColor]
  );
  const quantityInputValue = usesAllowedQuantitySteps ? formatQuantityValue(selectedQuantity) : quantityInput;
  const activePriceTiers = (product.product_price_tiers || []).filter((tier) => {
    if (tier.active === false) return false;
    const minQty = Number(tier.min_qty);
    const unitPrice = Number(tier.unit_price);
    return Number.isFinite(minQty) && minQty > 0 && Number.isFinite(unitPrice) && unitPrice > 0;
  });
  const quantityPricingRows = (() => {
    if (isSquareMeter || product.price_on_request || activePriceTiers.length === 0) return [] as Array<{
      quantity: number;
      unitPrice: number;
      totalPrice: number;
      noMinimo: boolean;
      isSelected: boolean;
      disabled: boolean;
    }>;
    const candidateQuantities = isIntervalMode && allowedQuantities.length > 0
      ? allowedQuantities
      : Array.from(
          new Set(
            [minimumQuantity, ...activePriceTiers.map((tier) => Number(tier.min_qty))]
              .filter((value) => Number.isFinite(value) && value > 0)
              .map((value) => Number(value.toFixed(3)))
          )
        ).sort((a, b) => a - b);
    return candidateQuantities.map((qty) => {
      const effectiveQuantity = calculateEffectiveQuantity({
        quantity: qty,
        unit_type: unitType,
      });
      const contaDaLinha = precoDaLinha({
        precoBase: pricingBase,
        faixas: product.product_price_tiers || [],
        pecas: qty,
        quantidadeEfetiva: effectiveQuantity,
        adicionais,
        soVariacao: Boolean(product.use_variation_price_only),
        valorMinimo,
      });
      const unitPrice = contaDaLinha.precoPorMedida;
      const totalPrice = contaDaLinha.total;
      const isValid = getQuantityCheck(qty).isValid && hasStockForQuantity(qty);
      return {
        quantity: qty,
        unitPrice,
        totalPrice,
        noMinimo: contaDaLinha.minimoAplicado !== null,
        isSelected: Math.abs(selectedQuantity - qty) <= 0.000001,
        disabled: !isValid,
      };
    });
  })();

  const handleQuantityFromPricingTable = (nextQuantity: number) => {
    if (!Number.isFinite(nextQuantity) || nextQuantity <= 0) return;
    if (!getQuantityCheck(nextQuantity).isValid || !hasStockForQuantity(nextQuantity)) return;
    setAddError("");
    setQuantity(nextQuantity);
    setQuantityInput(formatQuantityValue(nextQuantity));
  };

  const customFieldInputClass =
    "w-full rounded-xl border bg-white px-3 py-2.5 text-sm text-slate-800 placeholder:text-[var(--cf-placeholder)] transition-colors focus:outline-none";
  const customFieldTextColor = ensureColorContrast(primaryColor, "#FFFFFF", { minRatio: 4.5 });
  const customFieldPlaceholderColor = colorWithAlpha(customFieldTextColor, 0.62);
  const customFieldBorder = colorWithAlpha(primaryColor, 0.55);
  const customFieldFocusRing = colorWithAlpha(primaryColor, 0.28);
  const linkedFieldBorder = colorWithAlpha(primaryColor, 0.55);
  const linkedFieldBg = colorWithAlpha(primaryColor, 0.1);
  const numberBadgeBorder = colorWithAlpha(primaryColor, 0.6);
  const numberBadgeBg = colorWithAlpha(primaryColor, 0.16);
  const qtyTableBorder = colorWithAlpha(primaryColor, 0.34);
  const qtyTableHeaderBg = colorWithAlpha(primaryColor, 0.11);
  const qtyTableHeaderText = ensureColorContrast(primaryColor, "#FFFFFF", { minRatio: 4.5 });
  const qtyTableRowText = ensureColorContrast(primaryColor, "#FFFFFF", { minRatio: 7 });
  const qtyTableRowHoverBg = colorWithAlpha(primaryColor, 0.06);
  const qtyTableRowSelectedBg = colorWithAlpha(primaryColor, 0.13);
  const qtyTableRowDisabledText = colorWithAlpha(qtyTableRowText, 0.55);
  /* A entrega escolhida soma no total da barra, com o nome dela embaixo: o
     carrinho ja recebe a escolha, e o numero da pagina precisa bater com o
     dele. Antes a barra seguia mostrando so o produto. */
  const entregaNaBarra =
    !product.price_on_request && effectiveSelectedProductShipping
      ? Math.max(0, Number(effectiveSelectedProductShipping.option.price) || 0)
      : null;
  const displayTotalValue = (totalWithCombos ?? totalPrice ?? 0) + (entregaNaBarra ?? 0);
  const displayTotalLabel = product.price_on_request ? textoSemPreco : formatMoney(displayTotalValue);
  /* Produto esgotado que, pela regra da loja, vira pedido de orcamento: o
     botao fica ativo e abre o WhatsApp em vez de travar em "Indisponível". */
  const esgotadoVirouOrcamento =
    visao.regras.esgotado === "quote" && estoqueLimita && normalizedStock <= 0 && visao.podeComprar && !visao.precisaEntrar;
  /* Preco so para quem entrou: o botao fica ativo para levar ao login. */
  const canAddToCart = visao.precisaEntrar || esgotadoVirouOrcamento ? true : visao.podeComprar && !(
    (estoqueLimita && normalizedStock <= 0) ||
    exceedsStock ||
    (isSquareMeter && !isValidDims) ||
    !isVariationSelected ||
    Boolean(servicoPendente) ||
    !quantityCheck.isValid ||
    !hasTrackedSelection ||
    !hasValidCustomFields
  );
  const callToActionLabel =
    visao.precisaEntrar
      ? visao.regras.textoPrecoLogin
      : esgotadoVirouOrcamento
        ? "Pedir orçamento"
      : !visao.podeComprar
        ? (visao.rotuloDoBotao || visao.motivoDoBloqueio || "Indisponível")
        : !estoqueLimita || normalizedStock > 0
      ? (servicoPendente
          ? `Escolha: ${servicoPendente.grupo.nome}`
          : !isVariationSelected
          ? "Selecione as opções"
          : (!hasTrackedSelection
              ? (escolhaForaDoEstoque ? "Indisponível" : "Selecione uma opção")
              : (!hasValidCustomFields
                  ? "Preencha os campos"
                  : (isSquareMeter && !isValidDims
                      ? "Informe as medidas"
                      : (exceedsStock ? "Quantidade acima do estoque" : (visao.rotuloDoBotao || "Adicionar ao carrinho"))))))
      : "Indisponível";

  const handleAddToCart = () => {
    /* As regras da loja vem antes de tudo: entrar para ver preco, loja em
       pausa, pedido pelo WhatsApp sem carrinho. */
    if (visao.precisaEntrar) {
      router.push(`/${storeSlug}/cliente/login?next=${encodeURIComponent(window.location.pathname)}`);
      return;
    }
    if (!visao.podeComprar) return;
    if (esgotadoVirouOrcamento) {
      if (!whatsappDigits) {
        setAddError("A loja ainda não configurou o WhatsApp.");
        return;
      }
      const texto = `${mensagemDeDuvida} - está sem estoque na loja. Vocês conseguem fazer sob encomenda? Quero um orçamento.`;
      window.open(`https://wa.me/${whatsappDigits}?text=${encodeURIComponent(texto)}`, "_blank", "noopener");
      return;
    }
    if (visao.modoDeCompra === "whatsapp") {
      if (!whatsappDigits) {
        setAddError("A loja ainda não configurou o WhatsApp.");
        return;
      }
      const escolhas = Object.entries(effectiveSelectedOptions)
        .map(([key, values]) => `${key}: ${values.join(", ")}`)
        .join(" / ");
      const texto = `${mensagemDeDuvida}${escolhas ? ` (${escolhas})` : ""} - ${formatQuantityValue(quantityForValidation)} ${quantityForValidation === 1 ? "unidade" : "unidades"}.`;
      window.open(`https://wa.me/${whatsappDigits}?text=${encodeURIComponent(texto)}`, "_blank", "noopener");
      return;
    }
    const customFieldsPayload = visibleCustomFields
      .map((field) => ({
        id: field.id,
        title: field.title,
        field_type: field.field_type,
        required: field.required,
        variation_signature: field.variation_signature,
        value: String(customFieldValues[field.id] || "").trim(),
      }))
      .filter((field) => field.value.length > 0 || field.required);
    if (!hasValidCustomFields) {
      setAddError(
        `Preencha os campos obrigatorios: ${missingRequiredCustomFields.map((field) => field.title).join(", ")}.`
      );
      return;
    }
    const selectedOptionsDescription = Object.entries(effectiveSelectedOptions)
      .map(([key, values]) => `${key}: ${values.join(", ")}`)
      .join(" / ");
    const customFieldsDescription = customFieldsPayload
      .map((field) => `${field.title}: ${field.value}`)
      .join(" / ");
    /* O nome do grupo entra na descrição: no cupom sai "Prazo: Expressa",
       que é o que o cliente escolheu, e não um "Serviços" genérico. */
    const servicosDescription = servicosEscolhidos
      .map(({ servico, grupo }) => `${grupo.nome}: ${servico.titulo}`)
      .join(" / ");
    const finalVariationDescription = [selectedOptionsDescription, servicosDescription, customFieldsDescription]
      .filter(Boolean)
      .join(" / ");
    const result = addToCart(
      productForCart,
      quantityForValidation,
      isSquareMeter ? { width_m: parsedWidth, height_m: parsedHeight } : undefined,
      hasVariations || customFieldsPayload.length > 0 || servicosEscolhidos.length > 0
        ? {
            id: "custom-options",
            description: finalVariationDescription,
            selected_options: {
              inventory_scope: inventoryScope,
              inventory_attribute_name: inventoryScope === "option" ? inventoryAttributeName || null : null,
              options: [
                ...selectedOptionEntries.map((entry) => ({
                  option_id: entry.option_id,
                  attribute_name: entry.attribute_name,
                  label: entry.label,
                  price: entry.price,
                  price_applies_per_quantity: entry.price_applies_per_quantity,
                  price_unit: entry.price_unit,
                  affects_stock: entry.affects_stock,
                })),
                /* Os serviços viajam como opções: mesmo formato, mesmo
                   caminho, e o custo deles é achado pelo `option_id`. O
                   atributo é o nome do grupo que o lojista escolheu. */
                ...servicosEscolhidos.map(({ servico, grupo }) => servicoComoOpcao(servico, grupo.nome)),
              ],
              custom_fields: customFieldsPayload,
            },
          }
        : undefined
    );
    if (!result.ok) {
      setAddError(result.message || "Nao foi possivel adicionar esse produto.");
      return;
    }

    if (selectedCombos && selectedCombos.length > 0) {
      let hasComboError = false;

      selectedCombos.forEach((selectedCombo) => {
        const comboItem = {
          id: selectedCombo.combo.combo_product.id,
          name: selectedCombo.combo.combo_product.name,
          price: selectedCombo.combo.combo_product.price,
          image_url: selectedCombo.combo.combo_product.image_url,
          user_id: product.user_id,
          track_inventory: false,
          unit_type: selectedCombo.configuration?.unit_type || "unit",
          shipping_weight_kg: selectedCombo.combo.combo_product.shipping_weight_kg ?? null,
          shipping_width_cm: selectedCombo.combo.combo_product.shipping_width_cm ?? null,
          shipping_height_cm: selectedCombo.combo.combo_product.shipping_height_cm ?? null,
          shipping_length_cm: selectedCombo.combo.combo_product.shipping_length_cm ?? null,
        };

        const comboRes = addToCart(
          comboItem,
          selectedCombo.quantity,
          {
            price_override: selectedCombo.discountedPrice,
            is_combo_item: true,
            combo_parent_id: result.cart_item_id,
            width_m: selectedCombo.configuration?.width_m,
            height_m: selectedCombo.configuration?.height_m,
          },
          selectedCombo.configuration?.variation
        );

        if (!comboRes.ok) {
          hasComboError = true;
        }
      });

      if (hasComboError) {
        toast.error("Alguns itens do combo não puderam ser adicionados.");
      } else {
        toast.success("Produto e combo(s) adicionados!");
      }
    } else {
      toast.success("Adicionado ao carrinho!");
    }

    /*
      Depois dos combos entrarem, nunca antes. A validade do frete depende de
      uma assinatura calculada sobre todos os itens do carrinho. Gravando a
      selecao antes, o carrinho mudava em seguida, a assinatura deixava de
      bater e o frete era descartado em silencio: todo produto com Compre
      Junto perdia o frete sem excecao.
    */
    if (effectiveSelectedProductShipping) {
      setShippingSelection(effectiveSelectedProductShipping);
    }

    setAddError("");
    setTimeout(() => {
      const btn = document.querySelector('[data-ks-cart-trigger]') as HTMLButtonElement | null;
      if (btn) btn.click();
    }, 50);
  };

  const handleCustomFieldValueChange = (fieldId: string, rawValue: string, isNumberField: boolean) => {
    setAddError("");
    if (!isNumberField) {
      setCustomFieldValues((prev) => ({ ...prev, [fieldId]: rawValue }));
      return;
    }
    const normalized = rawValue
      .replace(/[^\d,.-]/g, "")
      .replace(/(,.*),/g, "$1")
      .replace(/(\..*)\./g, "$1");
    setCustomFieldValues((prev) => ({ ...prev, [fieldId]: normalized }));
  };

  const renderCustomField = (field: NormalizedCustomField, linkedToVariation: boolean) => (
    <div
      key={field.id}
      className={
        linkedToVariation
          ? "space-y-1 rounded-xl border p-3"
          : "space-y-1"
      }
      style={linkedToVariation ? { borderColor: linkedFieldBorder, backgroundColor: linkedFieldBg } : undefined}
    >
      <label
        className="text-sm font-medium"
        style={{ color: customFieldTextColor }}
      >
        {field.title}
        {field.required ? <span className="text-rose-600"> *</span> : null}
      </label>
      {field.field_type === "textarea" ? (
        <textarea
          value={customFieldValues[field.id] || ""}
          onChange={(e) => handleCustomFieldValueChange(field.id, e.target.value, false)}
          placeholder={field.placeholder || ""}
          rows={3}
          className={customFieldInputClass}
          style={{
            borderColor: customFieldBorder,
            boxShadow: `0 0 0 0 ${customFieldFocusRing}`,
            color: customFieldTextColor,
            caretColor: primaryColor,
            ["--cf-placeholder" as string]: customFieldPlaceholderColor,
          }}
          onFocus={(e) => {
            e.currentTarget.style.borderColor = primaryColor;
            e.currentTarget.style.boxShadow = `0 0 0 3px ${customFieldFocusRing}`;
          }}
          onBlur={(e) => {
            e.currentTarget.style.borderColor = customFieldBorder;
            e.currentTarget.style.boxShadow = "0 0 0 0 transparent";
          }}
        />
      ) : field.field_type === "number" ? (
        <div className="relative">
          <span
            className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 inline-flex h-6 min-w-6 items-center justify-center rounded-md border px-1 text-[11px] font-semibold"
            style={{ borderColor: numberBadgeBorder, backgroundColor: numberBadgeBg, color: customFieldTextColor }}
          >
            <Hash size={12} />
          </span>
          <input
            type="text"
            inputMode="decimal"
            value={customFieldValues[field.id] || ""}
            onChange={(e) => handleCustomFieldValueChange(field.id, e.target.value, true)}
            placeholder={field.placeholder || "Digite um numero"}
            className={`${customFieldInputClass} pl-11`}
            style={{
              borderColor: customFieldBorder,
              boxShadow: `0 0 0 0 ${customFieldFocusRing}`,
              color: customFieldTextColor,
              caretColor: primaryColor,
              ["--cf-placeholder" as string]: customFieldPlaceholderColor,
            }}
            onFocus={(e) => {
              e.currentTarget.style.borderColor = primaryColor;
              e.currentTarget.style.boxShadow = `0 0 0 3px ${customFieldFocusRing}`;
            }}
            onBlur={(e) => {
              e.currentTarget.style.borderColor = customFieldBorder;
              e.currentTarget.style.boxShadow = "0 0 0 0 transparent";
            }}
          />
        </div>
      ) : (
        <input
          type="text"
          value={customFieldValues[field.id] || ""}
          onChange={(e) => handleCustomFieldValueChange(field.id, e.target.value, false)}
          placeholder={field.placeholder || ""}
          className={customFieldInputClass}
          style={{
            borderColor: customFieldBorder,
            boxShadow: `0 0 0 0 ${customFieldFocusRing}`,
            color: customFieldTextColor,
            caretColor: primaryColor,
            ["--cf-placeholder" as string]: customFieldPlaceholderColor,
          }}
          onFocus={(e) => {
            e.currentTarget.style.borderColor = primaryColor;
            e.currentTarget.style.boxShadow = `0 0 0 3px ${customFieldFocusRing}`;
          }}
          onBlur={(e) => {
            e.currentTarget.style.borderColor = customFieldBorder;
            e.currentTarget.style.boxShadow = "0 0 0 0 transparent";
          }}
        />
      )}
    </div>
  );

  return (
    <div className="catalog-product-purchase-card flex flex-col gap-6 border-t border-zinc-200 pt-1">
      <div className="flex items-end justify-between gap-4 border-b border-zinc-200 pb-5">
         <span className="text-sm font-medium text-zinc-500">
           {priceTitle}
         </span>
         <span className={`font-semibold tracking-[-0.04em] ${hideSquareMeterUnitPrice ? "text-[1.7rem] text-violet-700" : "text-[2rem] text-zinc-950"}`}>{priceValue}</span>
      </div>
      {visao.avisoDePreco && visao.mostrarPreco ? (
        <p className="-mt-3 text-xs text-zinc-500">{visao.avisoDePreco}</p>
      ) : null}
      {mostraMinimo && visao.mostrarPreco && valorMinimo !== null ? (
        <div className="-mt-3">
          <ValorMinimoDoItem minimo={valorMinimo} cor={primaryColor} />
        </div>
      ) : null}
      {showcaseTierDiscount && !product.price_on_request && !isIntervalMode && (
        <div className="-mt-3">
          <span className="inline-flex items-center rounded-full bg-emerald-50 px-3 py-1 text-[11px] font-semibold text-emerald-700">
            {formatDiscountPct(showcaseTierDiscount.discountPct)}% OFF a partir de{" "}
            {formatTierThreshold(showcaseTierDiscount.tier.min_qty, unitType)}
          </span>
        </div>
      )}
      {/* Sem desconto ainda, "faixa de preço aplicada" só confundia: o cliente
          não sabe o que é faixa. Fala só quando há desconto de verdade. */}
      {appliedTier && !product.price_on_request && !isIntervalMode && appliedDiscountPct > 0 && (
        <div className="text-xs -mt-3 text-emerald-600">
          {`Desconto aplicado: ${formatDiscountPct(appliedDiscountPct)}% OFF (a partir de ${formatTierThreshold(appliedTier.min_qty, unitType)}).`}
        </div>
      )}

      {hasVariations && (
        <SeletorDeVariacoes
          unidadeDeVenda={unitType}
          atributos={product.product_attributes}
          selecionadas={effectiveSelectedOptions}
          aoEscolher={handleOptionSelect}
          corDaLoja={primaryColor}
          corDoTextoNaLoja={primaryTextColor}
          corDaLojaNoBranco={primaryOnWhite}
          formatarDinheiro={formatMoney}
          imagens={product.product_images}
          atributoDeEstoque={estoqueLimita && inventoryScope === "option" ? product.inventory_attribute_name : null}
          camposDaOpcao={(nomeDoAtributo) => {
            /* Campo que so aparece quando a opcao dele foi escolhida — o
               "nome para gravar" que segue o "Personalizado", por exemplo. */
            const campos = linkedVisibleCustomFields.filter((field) => {
              const signature = normalizeText(field.variation_signature || "");
              if (!signature.startsWith("option::")) return false;
              const selected = effectiveSelectedOptions[nomeDoAtributo] || [];
              return selected.some(
                (selectedOptionLabel) =>
                  signature === normalizeText(buildOptionSignature(nomeDoAtributo, selectedOptionLabel))
              );
            });
            if (!campos.length) return null;
            return (
              <div className="space-y-2 pt-1">
                {campos.map((field) => (
                  <div key={field.id}>{renderCustomField(field, true)}</div>
                ))}
              </div>
            );
          }}
        />
      )}

      {linkedVisibleCustomFields
        .filter((field) => !normalizeText(field.variation_signature || "").startsWith("option::"))
        .map((field) => renderCustomField(field, true))}

      {alwaysVisibleCustomFields.length > 0 && (
        <div className="space-y-3">
          {alwaysVisibleCustomFields.map((field) => (
            renderCustomField(field, false)
          ))}
        </div>
      )}

      {isSquareMeter && (
        <div className="grid grid-cols-2 gap-4">
          <div className="space-y-1">
            <span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-zinc-500">Largura (m)</span>
            <input
              inputMode="numeric"
              value={widthM}
              onKeyDown={(e) => {
                if (e.key !== "Backspace") return;
                const hasSelection = e.currentTarget.selectionStart !== e.currentTarget.selectionEnd;
                if (hasSelection) return;
                e.preventDefault();
                setWidthDigits((d) => d.slice(0, -1));
              }}
              onChange={(e) => {
                setAddError("");
                setWidthDigits(e.target.value.replace(/\D/g, ""));
              }}
              className="w-full rounded-xl border border-zinc-200 px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-[var(--ks-color-primary)]/30 focus:border-[var(--ks-color-primary)]"
              placeholder="Ex: 1,20"
            />
          </div>
          <div className="space-y-1">
            <span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-zinc-500">Altura (m)</span>
            <input
              inputMode="numeric"
              value={heightM}
              onKeyDown={(e) => {
                if (e.key !== "Backspace") return;
                const hasSelection = e.currentTarget.selectionStart !== e.currentTarget.selectionEnd;
                if (hasSelection) return;
                e.preventDefault();
                setHeightDigits((d) => d.slice(0, -1));
              }}
              onChange={(e) => {
                setAddError("");
                setHeightDigits(e.target.value.replace(/\D/g, ""));
              }}
              className="w-full rounded-xl border border-zinc-200 px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-[var(--ks-color-primary)]/30 focus:border-[var(--ks-color-primary)]"
              placeholder="Ex: 0,80"
            />
          </div>
        </div>
      )}

      {/* Produto por m² (ou por metro): a faixa vem da área total do pedido,
          então a tabela vira uma escada só de leitura; o que se escolhe é
          quantas peças, no contador. */}
      {escadaDeFaixas.length > 0 && (
        <div className="space-y-2">
          <span className="text-sm font-semibold" style={{ color: qtyTableHeaderText }}>
            Desconto progressivo por {nomeDaMedida(unitType)}
          </span>
          <p className="text-xs" style={{ color: qtyTableRowText }}>
            Quanto mais {nomeDaMedida(unitType)} no pedido, mais barato sai cada {nomeDaMedida(unitType)}.
          </p>
          <div className="rounded-2xl border bg-white" style={{ borderColor: qtyTableBorder }}>
            {escadaDeFaixas.map((faixa, indice) => (
              <div
                key={faixa.chave}
                className={`flex items-center justify-between gap-3 px-3 py-2.5 text-sm ${indice > 0 ? "border-t" : ""}`}
                style={{
                  borderColor: qtyTableBorder,
                  color: qtyTableRowText,
                  backgroundColor: faixa.atual ? qtyTableRowSelectedBg : "transparent",
                }}
              >
                <span className={faixa.atual ? "font-semibold" : ""}>
                  {faixa.rotulo}
                  {faixa.atual ? (
                    <span className="ml-1.5 text-[10px] font-semibold uppercase" style={{ color: primaryColor }}>
                      seu pedido
                    </span>
                  ) : null}
                </span>
                <span className="font-semibold">
                  {formatMoney(faixa.preco)}/{nomeDaMedida(unitType)}
                </span>
              </div>
            ))}
          </div>
          {dicaDaProximaFaixa ? (
            <p className="text-xs font-medium" style={{ color: "#047857" }}>{dicaDaProximaFaixa}</p>
          ) : null}
        </div>
      )}

      {quantityPricingRows.length > 0 && !vendidoPorMedida(unitType) && (
        <div className="space-y-2">
          <span className="text-sm font-semibold" style={{ color: qtyTableHeaderText }}>Escolha a quantidade</span>
          <div
            className="max-h-72 overflow-auto rounded-2xl border bg-white md:max-h-none"
            style={{ borderColor: qtyTableBorder }}
          >
            <div
              className="sticky top-0 z-10 grid grid-cols-[1.2fr_1fr_1fr] border-b backdrop-blur"
              style={{ borderColor: qtyTableBorder, backgroundColor: qtyTableHeaderBg }}
            >
              <span className="px-3 py-2 text-xs font-bold uppercase tracking-wide" style={{ color: qtyTableHeaderText }}>Quantidade</span>
              <span className="px-3 py-2 text-xs font-bold uppercase tracking-wide" style={{ color: qtyTableHeaderText }}>Valor por unidade</span>
              <span className="px-3 py-2 text-xs font-bold uppercase tracking-wide" style={{ color: qtyTableHeaderText }}>Valor total</span>
            </div>
            {quantityPricingRows.map((row) => (
              <label
                key={`qty-tier-${row.quantity}`}
                className={`grid grid-cols-[1.2fr_1fr_1fr] items-center border-b last:border-b-0 ${
                  row.disabled ? "cursor-not-allowed" : "cursor-pointer"
                }`}
                style={{
                  borderColor: qtyTableBorder,
                  color: row.disabled ? qtyTableRowDisabledText : qtyTableRowText,
                  backgroundColor: row.isSelected ? qtyTableRowSelectedBg : "transparent",
                }}
                onMouseEnter={(e) => {
                  if (!row.disabled && !row.isSelected) e.currentTarget.style.backgroundColor = qtyTableRowHoverBg;
                }}
                onMouseLeave={(e) => {
                  if (!row.disabled) e.currentTarget.style.backgroundColor = row.isSelected ? qtyTableRowSelectedBg : "transparent";
                }}
              >
                <span className="flex items-center gap-2 px-3 py-3">
                  <input
                    type="radio"
                    name={`qty-tier-${product.id}`}
                    checked={row.isSelected}
                    onChange={() => handleQuantityFromPricingTable(row.quantity)}
                    disabled={row.disabled}
                    className="h-4 w-4"
                    style={{ accentColor: primaryColor }}
                  />
                  <span className="text-sm font-semibold">{formatTierThreshold(row.quantity, unitType)}</span>
                </span>
                <span className="px-3 py-3 text-sm font-medium">{row.noMinimo ? "Valor mínimo" : formatMoney(row.unitPrice)}</span>
                <span className="px-3 py-3 text-sm font-semibold">{formatMoney(row.totalPrice)}</span>
              </label>
            ))}
          </div>
        </div>
      )}

      {/* Os serviços da loja ficam por último, logo acima do total: depois de
          o cliente já ter decidido o que quer. */}
      <ServicosOpcionais
        unidadeDeVenda={unitType}
        vitrine={vitrineDeServicosDoProduto}
        escolhidos={servicosMarcados}
        aoEscolher={escolherServico}
        corDaLoja={primaryColor}
        formatarDinheiro={formatMoney}
        faltando={servicoPendente?.grupo.nome ?? null}
      />

      {/* Quantity & Total */}
      <div className="grid gap-5 border-t border-zinc-200 pt-5 sm:grid-cols-[auto_1fr] sm:items-end">
        <div className="flex flex-col items-start gap-2">
          <div className="flex items-center rounded-xl border border-zinc-200 bg-white">
            <button 
              onClick={() => handleQuantityChange(-1)}
              disabled={!canDecreaseQuantity}
              className="flex h-11 w-11 items-center justify-center text-zinc-500 transition-colors hover:bg-zinc-50 disabled:opacity-50"
            >
              -
            </button>
            <input
              type="text"
              inputMode="numeric"
              pattern="[0-9]*"
              value={quantityInputValue}
              onChange={(e) => handleQuantityInputChange(e.target.value)}
              onFocus={(e) => e.currentTarget.select()}
              onBlur={normalizeQuantityInput}
              readOnly={usesAllowedQuantitySteps}
              className={`h-11 w-16 text-center font-medium text-zinc-900 outline-none ${
                usesAllowedQuantitySteps ? "cursor-not-allowed bg-zinc-50 text-zinc-500" : ""
              }`}
              aria-label="Quantidade"
            />
            <button 
              onClick={() => handleQuantityChange(1)}
              disabled={!canIncreaseQuantity}
              className="flex h-11 w-11 items-center justify-center text-zinc-500 transition-colors hover:bg-zinc-50"
            >
              +
            </button>
          </div>
          {ehEstoqueV2 && estoqueV2?.aviso ? (
            /* Estoque v2: esgotado nesta combinacao, ou a combinacao nao existe no estoque. */
            <span className="inline-flex items-center rounded-full border border-red-300 px-2.5 py-1 text-[11px] font-semibold text-red-600">
              {estoqueV2.aviso}
            </span>
          ) : stockMaxHelperMessage && (
            <span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-semibold ${
              exceedsStock ? "border-red-300 text-red-600" : "border-gray-300 text-gray-600"
            }`}>
              {stockMaxHelperMessage}
            </span>
          )}
        </div>
        
        <div className="flex flex-col items-start sm:items-end">
          <span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-zinc-500">{isSquareMeter ? "Total estimado (m²)" : "Total estimado"}</span>
          <span className="text-[1.65rem] font-semibold tracking-[-0.04em]" style={{ color: primaryOnWhite }}>
            {product.price_on_request
              ? textoSemPreco
              : formatMoney(totalPrice || 0)}
          </span>
          {isSquareMeter && (
            <span className="text-[11px] text-gray-500">
              {isValidDims ? `${totalArea.toFixed(2)} m²` : "Informe as medidas"}
            </span>
          )}
          {mostraMinimo && minimoAplicado !== null && (
            <span
              className="mt-1 inline-flex items-center rounded-full px-2.5 py-0.5 text-[11px] font-bold"
              style={{ backgroundColor: primaryColor, color: primaryTextColor }}
            >
              Valor mínimo do item
            </span>
          )}
          {hasSelectedCombos && (
            <>
              <span className="text-[11px] text-zinc-500">
                Combo selecionado: +{formatMoney(combosTotalPrice)}
              </span>
              <span className="text-sm font-bold text-emerald-600">
                Total com combo: {totalWithCombos === null ? textoSemPreco : formatMoney(totalWithCombos)}
              </span>
            </>
          )}
          {quantityRuleMessage && (
            <span className="mt-1 inline-flex items-center rounded-full border border-amber-300 px-2.5 py-1 text-[11px] font-semibold text-amber-700">
              {quantityRuleMessage}
            </span>
          )}
        </div>
      </div>

      {combosData && combosData.length > 0 && (
        <div className="pt-2">
          <ProductCombosWidget
            combos={combosData}
            mainProduct={product}
            primaryColor={primaryColor}
            slug={storeSlug}
            onComboSelect={(combos) => {
              setSelectedCombos(combos || []);
            }}
          />
        </div>
      )}

      {!stickyFooter && visao.modoDeCompra !== "showcase" && (
        <div className="flex flex-col gap-3 border-t border-zinc-200 pt-5">
          <button
            onClick={handleAddToCart}
            disabled={!canAddToCart}
            className="flex w-full items-center justify-center gap-3 rounded-xl px-6 py-4 text-base font-semibold transition-all hover:opacity-90 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50"
            style={{ backgroundColor: primaryColor, color: primaryTextColor }}
          >
            <ShoppingBag size={24} />
            {callToActionLabel}
          </button>
          {addError && (
            <p className="text-xs font-medium text-red-600">{addError}</p>
          )}

          {whatsappDigits && (
            <a
              href={`https://wa.me/${whatsappDigits}?text=${encodeURIComponent(mensagemDeDuvida)}`}
              target="_blank"
              rel="noopener noreferrer"
              className="flex w-full items-center justify-center gap-2 rounded-xl border border-zinc-200 px-6 py-3.5 font-semibold text-zinc-700 transition-all hover:border-zinc-300 hover:bg-zinc-50"
            >
              <MessageCircle size={20} className="text-[#25D366]" />
              Tirar dúvidas no WhatsApp
            </a>
          )}
        </div>
      )}

      <div className="grid grid-cols-2 gap-3 border-t border-zinc-200 pt-5">
        {normalizedTrustItems.map((item, idx) => {
          const Icon = trustIconMap[item.icon as keyof typeof trustIconMap] || ShieldCheck;
          return (
            <div
              key={`trust-item-${idx}`}
              className="flex items-center gap-3 rounded-2xl px-3 py-3"
              style={{ backgroundColor: withAlpha(primaryColor, 0.08) }}
            >
              <div
                className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl"
                style={{ backgroundColor: withAlpha(primaryColor, 0.16), color: primaryOnWhite }}
              >
                <Icon className="shrink-0" size={18} />
              </div>
              <div className="flex flex-col">
                <span className="text-xs font-semibold" style={{ color: primaryOnWhite }}>
                  {item.title || "Selo de confiança"}
                </span>
              </div>
            </div>
          );
        })}
      </div>

      <CatalogShippingCalculator
        slug={storeSlug}
        primaryColor={primaryColor}
        items={currentShippingItem}
        selectedShipping={effectiveSelectedProductShipping}
        onSelectShipping={handleProductShippingSelection}
        disabled={(isSquareMeter && !isValidDims) || product.price_on_request === true}
      />

      {stickyFooter && visao.modoDeCompra !== "showcase" && (
        <>
          {/* Nao ha espacador aqui: a folga para a barra fixa fica no rodape da
              pagina (`pb-[11rem] md:pb-28` no container do produto). Enquanto
              ela morava neste ponto, abria um vao no meio da pagina, entre o
              frete e o bloco de prazo de producao. */}
          {/*
            No mobile a barra sobe a altura do menu inferior (h-16 + area
            segura). Ambos eram `bottom-0` e o menu, com z maior, cobria o botao
            de comprar: o produto simplesmente nao tinha como ser comprado pelo
            celular. Do `md` para cima o menu some e a barra volta para o rodape.
          */}
          <div
            className="fixed inset-x-0 bottom-[calc(4rem+env(safe-area-inset-bottom))] z-40 border-t border-zinc-200 bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/85 md:bottom-0"
            style={buyBar.bg ? { backgroundColor: buyBar.bg } : undefined}
          >
            {/* `justify-center` sem `flex-1` no bloco do texto: nome, total,
                quantidade e botao viram um grupo unico no meio da barra, em vez
                do texto colado a esquerda e o botao jogado na outra ponta. */}
            <div className="mx-auto flex w-full max-w-[1440px] items-center justify-center gap-4 px-4 py-3 md:px-6">
              <div className="min-w-0 max-w-[46vw] md:max-w-none">
                <div
                  className="truncate text-sm font-medium text-zinc-700"
                  style={buyBar.text ? { color: buyBar.text } : undefined}
                >
                  {product.name}
                </div>
                <div className="mt-1 flex items-center gap-3">
                  <span
                    className="text-[11px] uppercase tracking-[0.08em] text-zinc-500"
                    style={buyBar.text ? { color: withAlpha(buyBar.text, 0.72) } : undefined}
                  >
                    {entregaNaBarra !== null ? "Total com entrega" : "Total"}
                  </span>
                  <span
                    className="text-2xl font-semibold tracking-[-0.04em]"
                    style={{ color: buyBar.text || primaryOnWhite }}
                  >
                    {displayTotalLabel}
                  </span>
                </div>
                {hasSelectedCombos && !product.price_on_request && (
                  <div className="mt-1 text-xs text-emerald-600">Inclui combo selecionado: +{formatMoney(combosTotalPrice)}</div>
                )}
                {entregaNaBarra !== null && effectiveSelectedProductShipping && (
                  <div
                    className="mt-1 truncate text-xs text-zinc-500"
                    style={buyBar.text ? { color: withAlpha(buyBar.text, 0.72) } : undefined}
                  >
                    {isPickupShippingOption(effectiveSelectedProductShipping.option) ? "Inclui retirada" : "Inclui entrega"}
                    {effectiveSelectedProductShipping.option.service ? ` (${effectiveSelectedProductShipping.option.service})` : ""}:{" "}
                    {entregaNaBarra > 0 ? `+${formatShippingPrice(entregaNaBarra)}` : formatShippingPrice(entregaNaBarra)}
                  </div>
                )}
              </div>
              <div className="flex shrink-0 items-center gap-3">
                {/* O seletor so tinha borda cinza clara. Numa barra colorida ele
                    praticamente sumia, entao agora ganha fundo e borda tirados
                    da propria cor do texto da barra: funciona em barra clara,
                    escura ou colorida sem precisar de regra por tema. */}
                <div
                  className="hidden items-center gap-2 rounded-full border px-3 py-2 sm:flex"
                  style={{
                    backgroundColor: withAlpha(quantityControlColor, 0.1),
                    borderColor: withAlpha(quantityControlColor, 0.26),
                  }}
                >
                  <button
                    type="button"
                    onClick={() => handleQuantityChange(-1)}
                    disabled={selectedQuantity <= minimumQuantity}
                    className="flex h-9 w-9 items-center justify-center rounded-full text-lg font-semibold transition hover:brightness-95 disabled:cursor-not-allowed disabled:opacity-40"
                    style={{ color: quantityControlColor }}
                  >
                    -
                  </button>
                  <div
                    className="min-w-[2ch] text-center text-base font-semibold"
                    style={{ color: quantityControlColor }}
                  >
                    {formatQuantityValue(selectedQuantity)}
                  </div>
                  <button
                    type="button"
                    onClick={() => handleQuantityChange(1)}
                    disabled={!canIncreaseQuantity}
                    className="flex h-9 w-9 items-center justify-center rounded-full text-lg font-semibold transition hover:brightness-95 disabled:cursor-not-allowed disabled:opacity-40"
                    style={{ color: quantityControlColor }}
                  >
                    +
                  </button>
                </div>
                <button
                  onClick={handleAddToCart}
                  disabled={!canAddToCart}
                  className="inline-flex h-12 items-center justify-center gap-3 rounded-full px-7 text-base font-semibold transition hover:opacity-90 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50"
                  style={{ backgroundColor: buyBar.button, color: buyBar.buttonText }}
                >
                  <ShoppingBag size={22} />
                  {/* Quando da para comprar, o texto e curto: "Comprar". Quando nao
                      da, ele diz o que falta ("Selecione as opções", "Informe as
                      medidas"). Antes escrevia "Comprar" fixo em todo caso, e o
                      botao ficava apagado sem nenhuma explicacao — a pessoa
                      clicava achando que a loja tinha travado. */}
                  {canAddToCart && !visao.precisaEntrar && !esgotadoVirouOrcamento ? (visao.rotuloDoBotao || "Comprar") : callToActionLabel}
                </button>
              </div>
            </div>
            {addError && (
              <div className="border-t border-rose-100 bg-rose-50 px-4 py-2 text-center text-xs font-medium text-rose-700">
                {addError}
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}
