/*
 * A conta de um item de orçamento, igual à das telas do painel.
 *
 * As telas "Novo orçamento" e "Editar orçamento" (app/dashboard/quotes/new e
 * quotes/[id]) fazem a conta dentro do componente, e uma página do Next não
 * exporta função solta. Por isso as funções delas estão aqui, uma a uma, com o
 * nome da original no comentário: mesma ordem dos atributos, mesmo formato do
 * `selected_options`, mesma conta (lib/adicionais.ts, a "conta única" da loja,
 * do carrinho, do PDV e do orçamento). Mudou lá, muda aqui; o teste
 * (orcamento-completo.test.ts) compara com a conta única em casos reais.
 *
 * O `selected_options` é contrato de 21 funções do banco (baixa por opção,
 * receita do item vendido, ficha de produção, portal do cliente, aprovação):
 * o que sai daqui é byte a byte o que a tela grava.
 *
 * Sem banco e sem React: só regra, para poder ser testada sozinha.
 */
import {
  nomeDaMedida,
  precoDaLinha,
  rotuloDaUnidade,
  somarAdicionais,
  unidadeDoAdicional,
  valorMinimoDoItem,
  vendidoPorMedida,
  SEM_ADICIONAIS,
  type OpcaoComPreco,
  type ResultadoDaLinha,
  type SomaDosAdicionais,
} from "../../adicionais.ts";
import { calculateEffectiveQuantity, checkSaleQuantityLimits, type ProductPriceTier } from "../../price-tiers.ts";

export type OpcaoDoCadastro =
  | string
  | {
      id?: string | null;
      label: string;
      price?: number | string | null;
      price_applies_per_quantity?: boolean | null;
      price_unit?: string | null;
      stock?: number | null;
      is_required?: boolean | null;
      position?: number | null;
    };

export type AtributoDoCadastro = {
  name: string;
  allow_multiple?: boolean | null;
  is_required?: boolean | null;
  position?: number | null;
  options: OpcaoDoCadastro[] | null;
};

/* As colunas que as duas telas do orçamento leem do produto. */
export type ProdutoDoOrcamento = {
  id: string;
  name: string;
  price: number | null;
  cost_total: number | null;
  unit_type: string | null;
  price_on_request?: boolean | null;
  use_variation_price_only?: boolean | null;
  min_sale_qty_enabled?: boolean | null;
  min_sale_qty?: number | null;
  max_sale_qty_enabled?: boolean | null;
  max_sale_qty?: number | null;
  allowed_sale_qty_enabled?: boolean | null;
  allowed_sale_quantities?: number[] | null;
  inventory_scope?: "product" | "option" | "variant" | null;
  inventory_attribute_name?: string | null;
  allow_negative_stock?: boolean | null;
  track_inventory?: boolean | null;
  stock?: number | null;
  product_attributes?: AtributoDoCadastro[] | null;
  product_variations?: Array<{ combination?: Record<string, string> | null; stock?: number | null }> | null;
  product_price_tiers?: ProductPriceTier[] | null;
  /** Valor mínimo do item (migration 194): só em produto por m²/metro. */
  valor_minimo_item?: number | string | null;
  valor_minimo_orcamentos?: boolean | null;
  valor_minimo_loja?: boolean | null;
  valor_minimo_pdv?: boolean | null;
};

export type OpcaoNormalizada = {
  id: string | null;
  label: string;
  price: number;
  price_applies_per_quantity: boolean;
  price_unit: string | null;
  stock: number | null;
  is_required: boolean;
  position: number;
};

export type OpcaoEscolhida = {
  option_id: string | null;
  attribute_name: string;
  label: string;
  price: number;
  price_applies_per_quantity: boolean;
  price_unit?: string | null;
  affects_stock: boolean;
};

export type SelectedOptions = {
  inventory_scope: string;
  inventory_attribute_name: string | null;
  options: OpcaoEscolhida[];
};

/** Atributo -> rótulos escolhidos. É o "mapa" com que as telas trabalham. */
export type Escolhas = Record<string, string[]>;

export type Medidas = { quantidade: number; largura?: number | null; altura?: number | null; comprimento?: number | null };

/* ------------------------------------------------------------ as da tela */

/** normalizeAttributeOption */
export function normalizarOpcao(opcao: OpcaoDoCadastro, indice: number): OpcaoNormalizada {
  if (typeof opcao === "string") {
    return { id: null, label: opcao, price: 0, price_applies_per_quantity: true, price_unit: null, stock: null, is_required: false, position: indice };
  }
  return {
    id: opcao.id ?? null,
    label: opcao.label,
    price: Number(opcao.price) || 0,
    price_applies_per_quantity: opcao.price_applies_per_quantity !== false,
    price_unit: typeof opcao.price_unit === "string" ? opcao.price_unit : null,
    stock: Number.isFinite(Number(opcao.stock)) ? Number(opcao.stock) : null,
    is_required: Boolean(opcao.is_required),
    position: typeof opcao.position === "number" ? opcao.position : indice,
  };
}

/** getSortedAttributes */
export function atributosOrdenados(produto: Pick<ProdutoDoOrcamento, "product_attributes"> | null): AtributoDoCadastro[] {
  return (produto?.product_attributes || [])
    .filter((atributo) => Boolean(atributo?.name))
    .slice()
    .sort((a, b) => Number(a.position ?? 0) - Number(b.position ?? 0));
}

/** As opções de um atributo, na ordem em que a tela mostra. */
export function opcoesDoAtributo(atributo: AtributoDoCadastro): OpcaoNormalizada[] {
  return (atributo.options || []).map((opcao, indice) => normalizarOpcao(opcao, indice)).sort((a, b) => Number(a.position ?? 0) - Number(b.position ?? 0));
}

/** getItemVariationMap: o que já está escolhido num item gravado. */
export function escolhasDoItem(selectedOptions: unknown): Escolhas {
  const lido = selectedOptions as { options?: unknown } | null | undefined;
  const opcoes = Array.isArray(lido?.options) ? lido.options : [];
  const mapa: Escolhas = {};
  for (const bruto of opcoes) {
    if (!bruto || typeof bruto !== "object" || Array.isArray(bruto)) continue;
    const linha = bruto as Record<string, unknown>;
    const atributo = typeof linha.attribute_name === "string" ? linha.attribute_name : "";
    const rotulo = typeof linha.label === "string" ? linha.label : "";
    if (!atributo || !rotulo) continue;
    const lista = mapa[atributo] || [];
    if (!lista.includes(rotulo)) lista.push(rotulo);
    mapa[atributo] = lista;
  }
  return mapa;
}

/** buildSelectedOptionsPayload: o JSON que vai para `quote_items.selected_options`. */
export function montarSelectedOptions(produto: ProdutoDoOrcamento, escolhas: Escolhas): SelectedOptions | null {
  const atributos = atributosOrdenados(produto);
  const escopo = produto.inventory_scope || "product";
  const atributoDoEstoque = (produto.inventory_attribute_name || "").trim().toLowerCase();
  const opcoes: OpcaoEscolhida[] = [];

  for (const atributo of atributos) {
    const normalizadas = opcoesDoAtributo(atributo);
    for (const rotulo of escolhas[atributo.name] || []) {
      const opcao = normalizadas.find((candidata) => candidata.label === rotulo);
      if (!opcao) continue;
      opcoes.push({
        option_id: opcao.id || null,
        attribute_name: atributo.name,
        label: opcao.label,
        price: Number(opcao.price) || 0,
        price_applies_per_quantity: opcao.price_applies_per_quantity !== false,
        price_unit: opcao.price_unit ?? null,
        affects_stock: escopo === "option" && atributo.name.trim().toLowerCase() === atributoDoEstoque,
      });
    }
  }

  if (!opcoes.length) return null;
  return {
    inventory_scope: escopo,
    inventory_attribute_name: escopo === "option" ? produto.inventory_attribute_name || null : null,
    options: opcoes,
  };
}

/** As opções escolhidas que existem no cadastro, na ordem da tela. */
function opcoesEscolhidas(produto: ProdutoDoOrcamento, escolhas: Escolhas): Array<{ atributo: string; opcao: OpcaoNormalizada }> {
  const lista: Array<{ atributo: string; opcao: OpcaoNormalizada }> = [];
  for (const atributo of atributosOrdenados(produto)) {
    const rotulos = escolhas[atributo.name] || [];
    if (!rotulos.length) continue;
    const normalizadas = opcoesDoAtributo(atributo);
    for (const rotulo of rotulos) {
      const opcao = normalizadas.find((candidata) => candidata.label === rotulo);
      if (opcao) lista.push({ atributo: atributo.name, opcao });
    }
  }
  return lista;
}

/** getSelectedOptionsPriceBreakdown */
export function adicionaisDasEscolhas(produto: ProdutoDoOrcamento | null | undefined, escolhas: Escolhas): SomaDosAdicionais {
  if (!produto) return SEM_ADICIONAIS;
  return somarAdicionais(
    opcoesEscolhidas(produto, escolhas).map(({ opcao }) => opcao as OpcaoComPreco),
    produto.unit_type
  );
}

export function quantidadeEfetiva(unidade: string | null | undefined, medidas: Medidas) {
  return calculateEffectiveQuantity({
    quantity: medidas.quantidade || 0,
    unit_type: unidade || "unit",
    width_m: medidas.largura || 0,
    height_m: medidas.altura || 0,
    length_m: medidas.comprimento || 0,
  });
}

/** A conta inteira da linha (lib/adicionais.ts), como `resolveProductUnitPrice` a chama. */
export function contaDaLinha(produto: ProdutoDoOrcamento, medidas: Medidas, escolhas: Escolhas): ResultadoDaLinha {
  return precoDaLinha({
    precoBase: Number(produto.price) || 0,
    faixas: produto.product_price_tiers || [],
    pecas: Number(medidas.quantidade) || 0,
    quantidadeEfetiva: quantidadeEfetiva(produto.unit_type, medidas),
    adicionais: adicionaisDasEscolhas(produto, escolhas),
    soVariacao: Boolean(produto.use_variation_price_only),
    sobConsulta: Boolean(produto.price_on_request),
    valorMinimo: valorMinimoDoItem(produto, "orcamentos"),
  });
}

/**
 * O que vai junto da linha gravada quando o valor mínimo do item pegou: o
 * total da linha é o mínimo, e as duas colunas guardam o aviso.
 */
export function minimoDaConta(conta: ResultadoDaLinha): { valor_minimo_aplicado: number | null; valor_sem_minimo: number | null } {
  if (conta.minimoAplicado == null) return { valor_minimo_aplicado: null, valor_sem_minimo: null };
  return { valor_minimo_aplicado: conta.minimoAplicado, valor_sem_minimo: Math.round(conta.totalSemMinimo * 100) / 100 };
}

/** resolveProductUnitPrice: o preço unitário (por medida, com peça e pedido rateados). */
export function precoDoPainel(produto: ProdutoDoOrcamento, medidas: Medidas, escolhas: Escolhas): number {
  if (produto.price_on_request) return 0;
  return contaDaLinha(produto, medidas, escolhas).precoPorMedida;
}

/**
 * O preço como a tela "Novo orçamento" guarda: ela escreve o valor no campo
 * com duas casas (formatUnitPriceValue) e lê de volta (parseMoneyInput). O
 * Intl arredonda igual nas duas línguas; aqui sai sem o separador de milhar.
 */
export function centavosDoFormulario(valor: number): number {
  const escrito = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, useGrouping: false }).format(valor || 0);
  return Number(escrito);
}

/** O total da linha como a tela "Editar orçamento" grava: em centavos. Com o
    valor mínimo do item, a linha vale exatamente o mínimo. */
export function totalDaLinhaEmCentavos(item: {
  quantity: number;
  unit_type: string;
  width_m: number | null;
  height_m: number | null;
  length_m: number | null;
  unit_price: number;
  valor_minimo_aplicado?: number | string | null;
}) {
  const minimo = Number(item.valor_minimo_aplicado);
  if (item.valor_minimo_aplicado != null && Number.isFinite(minimo) && minimo > 0) return Math.round(minimo * 100) / 100;
  return (
    Math.round(
      calculateEffectiveQuantity({
        quantity: item.quantity,
        unit_type: item.unit_type,
        width_m: item.width_m || 0,
        height_m: item.height_m || 0,
        length_m: item.length_m || 0,
      }) *
        item.unit_price *
        100
    ) / 100
  );
}

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

/** getItemQuantityRuleMessage: o que barra o salvar por quantidade. */
export function regraDeQuantidade(produto: ProdutoDoOrcamento, efetiva: number): string | null {
  const conferido = checkSaleQuantityLimits({
    min_sale_qty_enabled: produto.min_sale_qty_enabled,
    min_sale_qty: produto.min_sale_qty,
    max_sale_qty_enabled: produto.max_sale_qty_enabled,
    max_sale_qty: produto.max_sale_qty,
    allowed_sale_qty_enabled: produto.allowed_sale_qty_enabled,
    allowed_sale_quantities: produto.allowed_sale_quantities,
    effectiveQuantity: efetiva,
  });
  if (conferido.isBelowMin && conferido.minQty !== null) return `Quantidade mínima: ${quantidadeEscrita(conferido.minQty)}.`;
  if (conferido.isAboveMax && conferido.maxQty !== null) return `Quantidade máxima: ${quantidadeEscrita(conferido.maxQty)}.`;
  if (conferido.isNotAllowedValue && conferido.allowedQuantities.length > 0) {
    return `Use uma quantidade permitida: ${conferido.allowedQuantities.map(quantidadeEscrita).join(", ")}.`;
  }
  return null;
}

/** Os atributos obrigatórios sem escolha: a tela não salva assim. */
export function obrigatoriosFaltando(produto: ProdutoDoOrcamento, escolhas: Escolhas): AtributoDoCadastro[] {
  return atributosOrdenados(produto).filter((atributo) => atributo.is_required && !(escolhas[atributo.name] || []).length);
}

/** findMatchedVariation */
function combinacaoDoCadastro(produto: ProdutoDoOrcamento, escolhas: Escolhas) {
  const escolhido: Record<string, string> = {};
  for (const [atributo, rotulos] of Object.entries(escolhas)) {
    const rotulo = rotulos[0];
    if (!rotulo) continue;
    escolhido[atributo.trim().toLowerCase()] = rotulo.trim().toLowerCase();
  }
  const chaves = Object.keys(escolhido).sort();
  if (!chaves.length) return null;
  for (const variacao of (produto.product_variations || []).filter((v) => v && v.combination)) {
    const combinacao: Record<string, string> = {};
    for (const [atributo, rotulo] of Object.entries(variacao.combination || {})) combinacao[atributo.trim().toLowerCase()] = (rotulo || "").trim().toLowerCase();
    const chavesDaCombinacao = Object.keys(combinacao).sort();
    if (chavesDaCombinacao.length !== chaves.length) continue;
    if (chaves.every((chave) => combinacao[chave] === escolhido[chave])) return variacao;
  }
  return null;
}

/** getItemVariationStockMessage: o aviso de estoque que a tela mostra (não barra o salvar). */
export function avisoDeEstoque(produto: ProdutoDoOrcamento, escolhas: Escolhas, efetiva: number): string | null {
  if (produto.track_inventory === false) return null;
  const escopo = produto.inventory_scope || "product";
  if (escopo === "product") {
    const disponivel = Number(produto.stock) || 0;
    if (disponivel <= 0) return "Sem estoque para o produto selecionado.";
    if (efetiva > disponivel + 0.000001) return `Estoque insuficiente para o produto selecionado. Disponível: ${quantidadeEscrita(disponivel)}.`;
    return null;
  }
  if (escopo === "variant") {
    if (Object.values(escolhas).some((rotulos) => rotulos.length > 1)) return "Estoque por combinação exige apenas uma opção por atributo.";
    const esperados = atributosOrdenados(produto).filter((atributo) => (atributo.options || []).length > 0);
    const faltando = esperados.filter((atributo) => !(escolhas[atributo.name] || [])[0]);
    if (faltando.length > 0) return `Selecione uma combinação completa (${faltando.map((atributo) => atributo.name).join(", ")}).`;
    const variacao = combinacaoDoCadastro(produto, escolhas);
    if (!variacao) return "A combinação selecionada não existe no cadastro de variações.";
    const disponivel = Number(variacao.stock) || 0;
    if (disponivel <= 0) return "Sem estoque para a combinação selecionada.";
    if (efetiva > disponivel + 0.000001) return `Estoque insuficiente para a combinação selecionada. Disponível: ${quantidadeEscrita(disponivel)}.`;
    return null;
  }
  const nomeDoAtributo = (produto.inventory_attribute_name || "").trim();
  if (!nomeDoAtributo) return "Produto configurado com estoque por opção sem atributo de estoque definido.";
  const atributo = atributosOrdenados(produto).find((candidato) => candidato.name.trim().toLowerCase() === nomeDoAtributo.toLowerCase()) || null;
  if (!atributo) return `Atributo de estoque (${nomeDoAtributo}) não encontrado no produto.`;
  const entrada = Object.entries(escolhas).find(([nome]) => nome.trim().toLowerCase() === atributo.name.trim().toLowerCase()) || null;
  const rotulos = entrada?.[1] || [];
  if (rotulos.length === 0) return `Selecione uma opção de ${atributo.name} para controlar o estoque deste item.`;
  const normalizadas = opcoesDoAtributo(atributo);
  const opcoes = rotulos.map((rotulo) => normalizadas.find((opcao) => opcao.label === rotulo) || null).filter((opcao): opcao is OpcaoNormalizada => opcao !== null);
  if (!opcoes.length) return `As opções selecionadas de ${atributo.name} não existem mais no cadastro do produto.`;
  const saldos = opcoes.map((opcao) => (typeof opcao.stock === "number" ? opcao.stock : null)).filter((valor): valor is number => valor !== null);
  if (!saldos.length) return null;
  const disponivel = Math.min(...saldos);
  if (disponivel <= 0) return `Sem estoque para ${atributo.name}: ${rotulos.join(", ")}.`;
  if (efetiva > disponivel + 0.000001) return `Estoque insuficiente para ${atributo.name}: ${rotulos.join(", ")}. Disponível: ${quantidadeEscrita(disponivel)}.`;
  return null;
}

/** getSelectedOptionsSummary: "Papel: 180 g / Cor: Colorida". */
export function resumoDasEscolhas(escolhas: Escolhas): string {
  return Object.entries(escolhas)
    .filter(([, rotulos]) => rotulos.length > 0)
    .map(([atributo, rotulos]) => `${atributo}: ${rotulos.join(", ")}`)
    .join(" / ");
}

/* ------------------------------------------- casar o que o assistente mandou */

/** "Papel:" e "papel", "Couchê 90 g" e "couche 90g" são o mesmo nome. */
export function comparavel(texto: string): string {
  return String(texto ?? "")
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .toLowerCase()
    .replace(/[:]+\s*$/, "")
    .replace(/\s+/g, " ")
    .trim();
}

const compacto = (texto: string) => comparavel(texto).replace(/[\s.\-_/]+/g, "");

/** Acha um nome numa lista: igual primeiro; depois um só que contenha o pedido. */
function acharPorNome<T>(lista: T[], nome: (item: T) => string, procurado: string): T | null | "varios" {
  const alvo = comparavel(procurado);
  const iguais = lista.filter((item) => comparavel(nome(item)) === alvo);
  if (iguais.length === 1) return iguais[0];
  if (iguais.length > 1) return "varios";
  const alvoCompacto = compacto(procurado);
  if (!alvoCompacto) return null;
  const mesmoCompacto = lista.filter((item) => compacto(nome(item)) === alvoCompacto);
  if (mesmoCompacto.length === 1) return mesmoCompacto[0];
  const contem = lista.filter((item) => compacto(nome(item)).includes(alvoCompacto));
  if (contem.length === 1) return contem[0];
  return contem.length > 1 ? "varios" : null;
}

export type EscolhaPedida = { atributo?: string; opcao?: string; opcao_id?: string };

/** "Papel (obrigatório, uma opção): 90 g, 180 g (+ R$ 0,20 por unidade)". */
export function listaDeEscolhas(produto: ProdutoDoOrcamento, atributos: AtributoDoCadastro[] = atributosOrdenados(produto)): string {
  return atributos
    .map((atributo) => {
      const regra = [atributo.is_required ? "obrigatório" : "opcional", atributo.allow_multiple ? "pode marcar várias" : "uma opção"].join(", ");
      const opcoes = opcoesDoAtributo(atributo)
        .map((opcao) => (opcao.price ? `${opcao.label} (${efeitoNoPreco(opcao, produto.unit_type)})` : opcao.label))
        .join("; ");
      return `${atributo.name} (${regra}): ${opcoes || "sem opções cadastradas"}`;
    })
    .join(" | ");
}

/**
 * Casa as escolhas pedidas (por nome do atributo e da opção, sem ligar para
 * acento, maiúscula ou espaço, ou pelo id da opção) com o cadastro, e devolve
 * o mapa que a tela usaria. `base` são as escolhas que o item já tinha: o
 * atributo mencionado troca; os outros ficam.
 */
export function casarEscolhas(
  produto: ProdutoDoOrcamento,
  pedidas: EscolhaPedida[],
  base: Escolhas = {}
): { ok: true; escolhas: Escolhas } | { ok: false; erro: string } {
  const atributos = atributosOrdenados(produto);
  if (pedidas.length && !atributos.length) {
    return { ok: false, erro: `O produto "${produto.name}" não tem opções (atributos) cadastradas: mande o item sem "opcoes".` };
  }
  /* Começa do que o item já tinha, só com o que ainda existe no cadastro. */
  const escolhas: Escolhas = {};
  for (const atributo of atributos) {
    const validas = new Set(opcoesDoAtributo(atributo).map((opcao) => opcao.label));
    const antigas = (base[atributo.name] || []).filter((rotulo) => validas.has(rotulo));
    if (antigas.length) escolhas[atributo.name] = antigas;
  }
  const trocados = new Set<string>();

  for (const pedida of pedidas) {
    let atributo: AtributoDoCadastro | null = null;
    let opcao: OpcaoNormalizada | null = null;

    if (pedida.opcao_id) {
      const id = String(pedida.opcao_id).trim().toLowerCase();
      for (const candidato of atributos) {
        const achada = opcoesDoAtributo(candidato).find((o) => (o.id || "").toLowerCase() === id);
        if (achada) {
          atributo = candidato;
          opcao = achada;
          break;
        }
      }
      if (!opcao) return { ok: false, erro: `A opção de id ${pedida.opcao_id} não existe em "${produto.name}". Escolhas possíveis: ${listaDeEscolhas(produto)}.` };
    } else {
      const textoDaOpcao = String(pedida.opcao ?? "").trim();
      if (!textoDaOpcao) return { ok: false, erro: `Em "${produto.name}", cada escolha precisa de "opcao" (o nome da opção) ou "opcao_id".` };
      if (pedida.atributo) {
        const achado = acharPorNome(atributos, (a) => a.name, pedida.atributo);
        if (achado === "varios" || !achado) {
          return { ok: false, erro: `"${produto.name}" não tem o atributo "${pedida.atributo}". Escolhas possíveis: ${listaDeEscolhas(produto)}.` };
        }
        atributo = achado;
        const achada = acharPorNome(opcoesDoAtributo(atributo), (o) => o.label, textoDaOpcao);
        if (achada === "varios" || !achada) {
          return {
            ok: false,
            erro: `Em "${produto.name}", o atributo ${atributo.name} não tem a opção "${textoDaOpcao}"${achada === "varios" ? " (mais de uma parecida)" : ""}. Escolhas possíveis: ${listaDeEscolhas(produto, [atributo])}.`,
          };
        }
        opcao = achada;
      } else {
        /* Sem o atributo: a opção precisa existir em um atributo só. */
        const candidatas = atributos.flatMap((a) => opcoesDoAtributo(a).map((o) => ({ atributo: a, opcao: o })));
        const achada = acharPorNome(candidatas, (c) => c.opcao.label, textoDaOpcao);
        if (achada === "varios" || !achada) {
          return {
            ok: false,
            erro: `Em "${produto.name}", ${achada === "varios" ? `a opção "${textoDaOpcao}" existe em mais de um atributo: diga também o "atributo"` : `não existe a opção "${textoDaOpcao}"`}. Escolhas possíveis: ${listaDeEscolhas(produto)}.`,
          };
        }
        atributo = achada.atributo;
        opcao = achada.opcao;
      }
    }

    const nome = (atributo as AtributoDoCadastro).name;
    const rotulo = (opcao as OpcaoNormalizada).label;
    /* A primeira escolha de um atributo nesta chamada troca o que havia. */
    const atuais = trocados.has(nome) ? escolhas[nome] || [] : [];
    trocados.add(nome);
    if ((atributo as AtributoDoCadastro).allow_multiple) {
      escolhas[nome] = atuais.includes(rotulo) ? atuais : [...atuais, rotulo];
    } else {
      if (atuais.length && !atuais.includes(rotulo)) {
        return { ok: false, erro: `Em "${produto.name}", ${nome} aceita uma opção só, e vieram duas (${atuais[0]} e ${rotulo}).` };
      }
      escolhas[nome] = [rotulo];
    }
  }

  return { ok: true, escolhas };
}

/* ------------------------------------------------ explicar para o assistente */

function emReais(valor: number) {
  return valor.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}

/** "+ R$ 12,00/m²", "+ R$ 5,00 no pedido", "- R$ 6,00/m²". */
export function efeitoNoPreco(opcao: OpcaoComPreco, unitType: string | null | undefined): string {
  const preco = Number(opcao.price) || 0;
  if (!preco) return "não muda o preço";
  const unidade = unidadeDoAdicional(opcao, unitType);
  const rotulo = rotuloDaUnidade(unidade, unitType);
  const separador = rotulo.startsWith("/") ? "" : " ";
  return `${preco > 0 ? "+" : "-"} ${emReais(Math.abs(preco))}${separador}${rotulo}`;
}

/** Como o preço do produto se forma, em uma frase para o assistente. */
export function comoOPrecoSeForma(produto: ProdutoDoOrcamento): string {
  if (produto.price_on_request) return "Produto sob consulta: o cadastro não tem preço, então o preço do item precisa ser informado (preco_unitario).";
  const medida = nomeDaMedida(produto.unit_type);
  const medido = vendidoPorMedida(produto.unit_type);
  const temOpcoesComPreco = atributosOrdenados(produto).some((atributo) => opcoesDoAtributo(atributo).some((opcao) => opcao.price));
  const base = Number(produto.price) || 0;
  const partes: string[] = [];
  if (produto.use_variation_price_only) partes.push("O preço vem só das opções escolhidas (o preço base do cadastro não entra).");
  else if (base <= 0 && temOpcoesComPreco) partes.push("O produto está com preço base zero: o preço vem das opções escolhidas.");
  else partes.push(`Preço base: ${emReais(base)} por ${medida}${(produto.product_price_tiers || []).some((f) => f.active !== false) ? ", que muda pela faixa de quantidade" : ""}.`);
  if (temOpcoesComPreco) partes.push("Cada opção soma (ou desconta) o valor dela: por medida, por peça ou uma vez no pedido, como está escrito ao lado.");
  if (medido) partes.push(`A faixa é escolhida pela quantidade total em ${medida} (peças × ${medida === "m²" ? "largura × altura" : "comprimento"}), não pelo número de peças.`);
  partes.push("O preço unitário guardado no orçamento é por " + (medido ? medida : "unidade") + ", com o que é por peça e por pedido já rateado.");
  const minimo = valorMinimoDoItem(produto, "orcamentos");
  if (minimo !== null) {
    partes.push(
      `Valor mínimo do item: ${emReais(minimo)}. Cada linha (todas as peças daquela linha juntas) é comparada sozinha com ele: se a conta der menos, a linha custa ${emReais(minimo)}. Juntar itens diferentes não soma para chegar no mínimo, e preço digitado à mão (preco_unitario) não leva o mínimo.`
    );
  }
  if (atributosOrdenados(produto).length) partes.push("Pergunte à pessoa qual opção de cada atributo (os obrigatórios precisam de resposta) antes de calcular ou montar o orçamento.");
  return partes.join(" ");
}

/* ----------------------------------------------------- totais do orçamento */

export type LinhaParaTotal = {
  quantity: number;
  unit_type: string;
  width_m: number | null;
  height_m: number | null;
  length_m: number | null;
  unit_price: number;
  unit_cost: number;
  /** A linha pegou o valor mínimo do item: vale ele, não quantidade × preço. */
  valor_minimo_aplicado?: number | string | null;
};

/** `totals` das duas telas: subtotal, desconto (em reais ou %), frete, total e custo. */
export function totaisDoOrcamento(linhas: LinhaParaTotal[], opcoes: { desconto?: number; descontoPercentual?: number | null; frete?: number }) {
  const efetiva = (linha: LinhaParaTotal) =>
    calculateEffectiveQuantity({ quantity: linha.quantity, unit_type: linha.unit_type, width_m: linha.width_m || 0, height_m: linha.height_m || 0, length_m: linha.length_m || 0 });
  const valorDaLinha = (linha: LinhaParaTotal) => {
    const minimo = Number(linha.valor_minimo_aplicado);
    if (linha.valor_minimo_aplicado != null && Number.isFinite(minimo) && minimo > 0) return minimo;
    return efetiva(linha) * (linha.unit_price || 0);
  };
  const subtotal = linhas.reduce((soma, linha) => soma + valorDaLinha(linha), 0);
  const percentual = opcoes.descontoPercentual;
  const desconto = percentual !== null && percentual !== undefined ? (subtotal * Math.max(0, percentual)) / 100 : Math.max(0, opcoes.desconto || 0);
  const frete = opcoes.frete || 0;
  const total = Math.max(0, subtotal - desconto + frete);
  const custo = linhas.reduce((soma, linha) => soma + efetiva(linha) * (linha.unit_cost || 0), 0);
  return { subtotal, desconto, frete, total, custo };
}

/* --------------------------------------------- o detalhe da conta, para ler */

export function detalheDaConta(produto: ProdutoDoOrcamento, medidas: Medidas, escolhas: Escolhas) {
  const efetiva = quantidadeEfetiva(produto.unit_type, medidas);
  const conta = contaDaLinha(produto, medidas, escolhas);
  const medida = nomeDaMedida(produto.unit_type);
  const opcoes = opcoesEscolhidas(produto, escolhas).map(({ atributo, opcao }) => {
    const unidade = unidadeDoAdicional(opcao, produto.unit_type);
    const vezes = unidade === "order" ? 1 : unidade === "piece" ? Number(medidas.quantidade) || 1 : efetiva;
    return {
      atributo,
      opcao: opcao.label,
      efeito: efeitoNoPreco(opcao, produto.unit_type),
      soma_na_linha: Math.round(opcao.price * vezes * 100) / 100,
    };
  });
  return {
    quantidade_efetiva: Number(efetiva.toFixed(6)),
    medida_do_preco: medida,
    preco_base_do_cadastro: Number(produto.price) || 0,
    faixa_aplicada: conta.faixa
      ? { de: Number(conta.faixa.min_qty), ate: conta.faixa.max_qty === null ? null : Number(conta.faixa.max_qty), preco: Number(conta.faixa.unit_price) }
      : null,
    preco_base_usado: produto.use_variation_price_only ? 0 : conta.precoDaFaixa,
    so_pelas_opcoes: Boolean(produto.use_variation_price_only),
    opcoes,
    preco_por_medida_sem_rateio: Math.round(conta.precoPorMedidaSemRateio * 100) / 100,
    preco_por_peca: Math.round(conta.precoPorPeca * 100) / 100,
    total_exato: Math.round(conta.total * 100) / 100,
    valor_minimo_do_item: valorMinimoDoItem(produto, "orcamentos"),
    valor_minimo_aplicado: conta.minimoAplicado,
    total_sem_minimo: Math.round(conta.totalSemMinimo * 100) / 100,
  };
}
