import { randomUUID } from "node:crypto";

import sharp from "sharp";

import { conferirValores, formatarValor, semAcento, chaveDoRotulo, valoresDaLinha, type DefinicaoDeCampo, type ValoresDosCampos } from "@/lib/campos-proprios";
import { lerDefinicoes } from "@/lib/campos-proprios-do-banco";
import { FORMAS_DE_PAGAMENTO, erroDasCondicoes, normalizarCondicoes, padraoDasCondicoes, resumirCondicoes, type CondicoesComerciais } from "@/lib/condicoes-comerciais";
import { condicoesDoPadrao, lerModelos, type ModeloDeCondicoes } from "@/lib/modelos-de-condicoes";
import { COLUNAS_DO_VALOR_MINIMO, textoDoMinimoAplicado } from "@/lib/adicionais";

import type { Campos } from "../esquema";
import { DESCRICAO_DO_CAMPO_DE_IMAGEM, receberImagem } from "../imagens";
import type { ContextoDoAssistente } from "../contexto";
import { UNIDADE, dinheiro, falha, pode, recusar } from "./comum";
import {
  avisoDeEstoque,
  casarEscolhas,
  centavosDoFormulario,
  contaDaLinha,
  detalheDaConta,
  escolhasDoItem,
  listaDeEscolhas,
  minimoDaConta,
  montarSelectedOptions,
  obrigatoriosFaltando,
  precoDoPainel,
  quantidadeEfetiva,
  regraDeQuantidade,
  resumoDasEscolhas,
  type EscolhaPedida,
  type Escolhas,
  type ProdutoDoOrcamento,
  type SelectedOptions,
} from "./conta-do-orcamento";
import type { Argumentos } from "./tipos";

/*
 * O que criar_orcamento, alterar_orcamento e calcular_preco dividem: os campos
 * de um item, a leitura do produto com as opções (a mesma das telas), a
 * montagem do item como a tela monta, as condições comerciais, os campos
 * próprios e a arte do item.
 */

/* ------------------------------------------------------------------ campos */

export const CAMPOS_DAS_OPCOES: Campos = {
  atributo: { tipo: "texto", descricao: "Nome do atributo, como em ver_opcoes_do_produto (ex.: Papel, Cor). Pode faltar se a opção só existe num atributo.", max: 120 },
  opcao: { tipo: "texto", descricao: "Nome da opção escolhida (ex.: 180 g, Colorida). Acento e maiúscula não importam.", max: 160 },
  opcao_id: { tipo: "id", descricao: "Ou o id da opção (de ver_opcoes_do_produto)." },
};

const CAMPO_OPCOES = {
  tipo: "lista",
  descricao:
    "As opções escolhidas de cada atributo do produto (o preço costuma vir delas). Uma entrada por opção; atributo que aceita várias recebe várias entradas. Veja os atributos em ver_opcoes_do_produto e pergunte à pessoa antes.",
  itens: CAMPOS_DAS_OPCOES,
  maxItens: 60,
} as const;

export const CAMPOS_DA_ARTE: Campos = {
  titulo: { tipo: "texto", descricao: "Título da arte (ex.: Arte frente).", max: 160 },
  observacao: { tipo: "texto", descricao: "Observações da arte para o cliente.", max: 1000 },
  imagens: { tipo: "textos", descricao: `Imagens da arte (até 6). Cada uma: ${DESCRICAO_DO_CAMPO_DE_IMAGEM}`, maxItens: 6, max: 2000 },
};

const OPCOES_DE_VENDA = ["unidade", "metro quadrado", "metro linear", "metro", "caixa", "resma"] as const;
export const UNIDADE_DO_TEXTO: Record<string, string> = Object.fromEntries(Object.entries(UNIDADE).map(([codigo, nome]) => [nome, codigo]));

/** Os campos de um item, os mesmos da tela (produto, opções, medidas, preço, custo, descrição e arte). */
export const CAMPOS_DO_ITEM: Campos = {
  produto_id: { tipo: "id", descricao: "Id de um produto do cadastro (de buscar_produtos). Com ele, nome, preço e custo vêm do cadastro." },
  titulo: { tipo: "texto", descricao: "Nome do item. Obrigatório quando não há produto_id; com produto, troca o nome que aparece.", max: 200 },
  descricao: { tipo: "texto", descricao: "Detalhes do item que o cliente lê: arte, acabamento, sangria.", max: 1000 },
  quantidade: { tipo: "numero", descricao: "Quantidade de peças (no mínimo 1, como na tela).", obrigatorio: true, min: 0.001, max: 1_000_000 },
  opcoes: CAMPO_OPCOES,
  largura_m: { tipo: "numero", descricao: "Largura em metros, para produto vendido por metro quadrado.", min: 0.001, max: 1000 },
  altura_m: { tipo: "numero", descricao: "Altura em metros, para produto vendido por metro quadrado.", min: 0.001, max: 1000 },
  comprimento_m: { tipo: "numero", descricao: "Comprimento em metros, para produto vendido por metro.", min: 0.001, max: 100_000 },
  preco_unitario: {
    tipo: "numero",
    descricao: "Preço manual de cada unidade (ou de cada m²/metro), em reais. Sem isto, o preço é calculado como no painel: cadastro, faixa da quantidade e opções.",
    min: 0,
    max: 10_000_000,
  },
  custo_unitario: { tipo: "numero", descricao: "Custo de cada unidade, em reais (só para quem vê custos). Sem isto, vem do cadastro do produto.", min: 0, max: 10_000_000 },
  vendido_por: { tipo: "opcao", descricao: "Só para item avulso (sem produto_id): como é vendido. O padrão é unidade.", opcoes: OPCOES_DE_VENDA },
  arte: { tipo: "objeto", descricao: "A arte do item (título, observação e imagens), como o botão Adicionar arte da tela.", campos: CAMPOS_DA_ARTE },
};

/* ----------------------------------------------------------------- produto */

/* As mesmas colunas que as duas telas leem (quotes/new e quotes/[id]). */
const COLUNAS_DO_PRODUTO =
  "id,name,price,cost_total,unit_type,active,product_kind,price_on_request,use_variation_price_only,min_sale_qty_enabled,min_sale_qty,max_sale_qty_enabled,max_sale_qty,allowed_sale_qty_enabled,allowed_sale_quantities,inventory_scope,inventory_attribute_name,allow_negative_stock,track_inventory,stock,product_attributes(name,allow_multiple,is_required,position,options),product_variations(combination,stock),product_price_tiers(min_qty,max_qty,unit_price,sort_order,active)," +
  COLUNAS_DO_VALOR_MINIMO;

export type ProdutoCarregado = ProdutoDoOrcamento & { active: boolean | null; product_kind: string | null };

export async function carregarProdutos(ctx: ContextoDoAssistente, ids: string[]): Promise<Map<string, ProdutoCarregado>> {
  const unicos = Array.from(new Set(ids.filter(Boolean)));
  const produtos = new Map<string, ProdutoCarregado>();
  if (!unicos.length) return produtos;
  const { data, error } = await ctx.supabase.from("products").select(COLUNAS_DO_PRODUTO).in("id", unicos).in("user_id", ctx.tenantIds);
  if (error) falha("ler os produtos", error);
  for (const produto of (data ?? []) as unknown as ProdutoCarregado[]) produtos.set(produto.id, produto);
  return produtos;
}

/* ------------------------------------------------------------------- item */

/** Uma linha de `quote_items`, como as telas gravam. */
export type LinhaDoOrcamento = {
  id: string;
  product_id: string | null;
  unit_type: string;
  width_m: number | null;
  height_m: number | null;
  length_m: number | null;
  title: string;
  description: string | null;
  quantity: number;
  unit_price: number;
  unit_cost: number;
  selected_options: Record<string, unknown> | null;
  /** O valor mínimo do item pegou nesta linha (migration 194): ela vale o mínimo. */
  valor_minimo_aplicado?: number | null;
  valor_sem_minimo?: number | null;
};

/* A tela "Novo orçamento" arredonda o preço em centavos (o campo de dinheiro);
   a "Editar orçamento" guarda o preço que a conta deu. */
export type ModoDaConta = "criacao" | "edicao";

export type ArtePedida = { titulo?: string; observacao?: string; imagens?: string[] };

export type ItemMontado = {
  linha: LinhaDoOrcamento;
  previa: Record<string, unknown>;
  avisos: string[];
  arte: ArtePedida | null;
  doCadastro: boolean;
};

function numeroOuNulo(valor: unknown) {
  const numero = Number(valor);
  return Number.isFinite(numero) && numero > 0 ? numero : null;
}

function medidasDaLinha(linha: Pick<LinhaDoOrcamento, "unit_type" | "width_m" | "height_m" | "length_m">) {
  if (linha.unit_type === "square_meter" && linha.width_m && linha.height_m) return `${linha.width_m} m x ${linha.height_m} m`;
  if ((linha.unit_type === "linear_meter" || linha.unit_type === "meter") && linha.length_m) return `${linha.length_m} m`;
  return null;
}

export { medidasDaLinha };

/**
 * Monta um item como a tela monta.
 *
 * - `pedido`: os campos do item que o assistente mandou.
 * - `atual`: no alterar, a linha gravada (o que não veio fica como estava).
 * - Preço: com `preco_unitario`, é o preço digitado (a tela trava o cadeado).
 *   Sem ele, e com produto do cadastro, a tela recalcula quando muda produto,
 *   quantidade, medida ou opção (`updateItem`); com `manter_preco`, não.
 */
export function montarItem(params: {
  ctx: ContextoDoAssistente;
  posicao: number;
  pedido: Argumentos;
  produtos: Map<string, ProdutoCarregado>;
  modo: ModoDaConta;
  atual?: LinhaDoOrcamento | null;
  /* Estoque v2: o aviso de estoque vem do banco, pelo orçamento inteiro
     (avisosDoEstoqueV2, somando por célula); o daqui não vale. */
  estoqueV2?: boolean;
}): ItemMontado {
  const { ctx, posicao, pedido, produtos, modo, atual } = params;
  const avisos: string[] = [];
  const quemE = (titulo: string) => `Item ${posicao}${titulo ? ` (${titulo})` : ""}`;

  const produtoPedido = (pedido.produto_id as string | undefined) ?? null;
  const trocouDeProduto = Boolean(produtoPedido) && produtoPedido !== (atual?.product_id ?? null);
  const produtoId = produtoPedido ?? atual?.product_id ?? null;
  const produto = produtoId ? produtos.get(produtoId) : undefined;

  if (produtoPedido && !produto) recusar(`${quemE("")}: produto não encontrado. Use buscar_produtos para pegar o id certo, ou mande o item sem produto_id, com titulo e preco_unitario.`);
  /* A tela só lista produto ativo (e o "Novo orçamento" tira os digitais). */
  if (trocouDeProduto && produto) {
    if (produto.active === false) recusar(`${quemE(produto.name)}: este produto está inativo no cadastro, e a tela do orçamento não oferece produto inativo. Ative o produto antes ou use um item avulso.`);
    if (modo === "criacao" && produto.product_kind === "digital") recusar(`${quemE(produto.name)}: produto digital não entra em orçamento novo pela tela. Use um item avulso.`);
  }
  /* Produto que saiu da lista da tela (inativo, apagado): a tela não recalcula o item. */
  const produtoNaTela = produto && produto.active !== false ? produto : undefined;

  /* Ao trocar de produto a tela zera opções e medidas e põe nome, unidade e custo do cadastro. */
  const base: LinhaDoOrcamento =
    atual && !trocouDeProduto
      ? { ...atual }
      : {
          id: atual?.id ?? randomUUID(),
          product_id: produto?.id ?? null,
          unit_type: produto ? produto.unit_type || "unit" : "unit",
          width_m: null,
          height_m: null,
          length_m: null,
          title: produto?.name ?? atual?.title ?? "",
          description: atual?.description ?? null,
          quantity: atual?.quantity ?? 1,
          unit_price: 0,
          unit_cost: produto ? (modo === "criacao" ? centavosDoFormulario(Number(produto.cost_total) || 0) : Number(produto.cost_total) || 0) : 0,
          selected_options: null,
          valor_minimo_aplicado: null,
          valor_sem_minimo: null,
        };
  const linha: LinhaDoOrcamento = { ...base };

  if (pedido.titulo !== undefined) linha.title = String(pedido.titulo).trim();
  if (pedido.descricao !== undefined) linha.description = String(pedido.descricao).trim() || null;
  if (!linha.title) recusar(`${quemE("")}: informe o titulo, ou um produto_id do cadastro.`);

  if (!produto) {
    if (pedido.opcoes !== undefined && (pedido.opcoes as unknown[]).length) recusar(`${quemE(linha.title)}: item avulso não tem opções. Use um produto_id do cadastro para escolher opções.`);
    if (pedido.vendido_por !== undefined) linha.unit_type = UNIDADE_DO_TEXTO[String(pedido.vendido_por)] ?? "unit";
  } else if (pedido.vendido_por !== undefined && UNIDADE_DO_TEXTO[String(pedido.vendido_por)] !== (produto.unit_type || "unit")) {
    avisos.push(`${quemE(linha.title)}: vendido_por vale só para item avulso; o produto é vendido por ${UNIDADE[produto.unit_type || "unit"] ?? "unidade"}.`);
  }

  /* Quantidade e medidas: a tela grava no mínimo 1 peça. */
  let mexeuNaConta = trocouDeProduto;
  if (pedido.quantidade !== undefined) {
    const quantidade = Number(pedido.quantidade);
    if (quantidade < 1) {
      recusar(
        `${quemE(linha.title)}: a quantidade mínima de um item é 1, como na tela.${
          linha.unit_type === "square_meter" ? " Para um pedaço menor que 1 m², mande quantidade 1 com largura_m e altura_m." : ""
        }`
      );
    }
    if (quantidade !== linha.quantity) mexeuNaConta = true;
    linha.quantity = quantidade;
  }
  const medidasPedidas: Array<[keyof LinhaDoOrcamento, string, boolean]> = [
    ["width_m", "largura_m", linha.unit_type === "square_meter"],
    ["height_m", "altura_m", linha.unit_type === "square_meter"],
    ["length_m", "comprimento_m", linha.unit_type === "linear_meter" || linha.unit_type === "meter"],
  ];
  for (const [coluna, campo, vale] of medidasPedidas) {
    if (pedido[campo] === undefined) continue;
    if (!vale) {
      avisos.push(`${quemE(linha.title)}: ${campo} não vale para produto vendido por ${UNIDADE[linha.unit_type] ?? "unidade"} e foi ignorado.`);
      continue;
    }
    const valor = numeroOuNulo(pedido[campo]);
    if (valor !== (linha[coluna] ?? null)) mexeuNaConta = true;
    (linha as Record<string, unknown>)[coluna] = valor;
  }
  if (linha.unit_type === "square_meter" && !(linha.width_m && linha.height_m)) {
    avisos.push(`${quemE(linha.title)} é vendido por metro quadrado e está sem largura e altura: a quantidade conta como metros quadrados.`);
  }
  if ((linha.unit_type === "linear_meter" || linha.unit_type === "meter") && !linha.length_m) {
    avisos.push(`${quemE(linha.title)} é vendido por metro e está sem comprimento: a quantidade conta como metros.`);
  }

  /* Opções. */
  let escolhas: Escolhas = escolhasDoItem(linha.selected_options);
  if (produto && pedido.opcoes !== undefined) {
    const casado = casarEscolhas(produto, pedido.opcoes as EscolhaPedida[], pedido.limpar_opcoes === true ? {} : escolhas);
    if (!casado.ok) recusar(`${quemE(linha.title)}: ${casado.erro}`);
    escolhas = casado.escolhas;
    linha.selected_options = montarSelectedOptions(produto, escolhas) as Record<string, unknown> | null;
    mexeuNaConta = true;
  } else if (produto && pedido.limpar_opcoes === true) {
    escolhas = {};
    linha.selected_options = null;
    mexeuNaConta = true;
  }

  const efetiva = quantidadeEfetiva(linha.unit_type, { quantidade: linha.quantity, largura: linha.width_m, altura: linha.height_m, comprimento: linha.length_m });

  if (produtoNaTela) {
    /* O que barra o salvar na tela. */
    const faltando = obrigatoriosFaltando(produtoNaTela, escolhas);
    if (faltando.length) {
      recusar(
        `${quemE(linha.title)}: falta escolher ${faltando.map((a) => a.name).join(", ")} — é obrigatório no cadastro e o preço depende disso. Pergunte à pessoa. Escolhas possíveis: ${listaDeEscolhas(produtoNaTela, faltando)}.`
      );
    }
    const regra = regraDeQuantidade(produtoNaTela, efetiva);
    if (regra) recusar(`${quemE(linha.title)}: ${regra}`);
    const estoque = params.estoqueV2 ? null : avisoDeEstoque(produtoNaTela, escolhas, efetiva);
    if (estoque) avisos.push(`${quemE(linha.title)}: ${estoque}`);
  } else if (produto) {
    avisos.push(`${quemE(linha.title)}: o produto está inativo; o preço não é recalculado pelo cadastro (a tela também não recalcula).`);
  } else {
    avisos.push(`${quemE(linha.title)} é avulso, sem produto do cadastro: não tem custo calculado.`);
  }

  /* Preço. */
  let origemDoPreco: string;
  const precoAnterior = linha.unit_price;
  if (pedido.preco_unitario !== undefined) {
    linha.unit_price = dinheiro(pedido.preco_unitario);
    origemDoPreco = "manual (informado)";
    /* Preço digitado não leva o valor mínimo do item, como o cadeado da tela. */
    linha.valor_minimo_aplicado = null;
    linha.valor_sem_minimo = null;
  } else if (produtoNaTela && (mexeuNaConta || pedido.preco_automatico === true) && pedido.manter_preco !== true) {
    const medidasDaConta = { quantidade: linha.quantity, largura: linha.width_m, altura: linha.height_m, comprimento: linha.length_m };
    const calculado = precoDoPainel(produtoNaTela, medidasDaConta, escolhas);
    const minimo = produtoNaTela.price_on_request
      ? { valor_minimo_aplicado: null, valor_sem_minimo: null }
      : minimoDaConta(contaDaLinha(produtoNaTela, medidasDaConta, escolhas));
    linha.valor_minimo_aplicado = minimo.valor_minimo_aplicado;
    linha.valor_sem_minimo = minimo.valor_sem_minimo;
    /* Com o mínimo, o preço por medida vai inteiro (70 ÷ 0,24 m²), como a tela
       grava; a linha fica no valor exato do mínimo. */
    linha.unit_price =
      minimo.valor_minimo_aplicado !== null
        ? Math.round(Math.max(0, calculado) * 1e6) / 1e6
        : modo === "criacao"
          ? centavosDoFormulario(calculado)
          : Math.max(0, calculado);
    origemDoPreco = produtoNaTela.price_on_request
      ? "produto sob consulta (zero)"
      : minimo.valor_minimo_aplicado !== null
        ? "valor mínimo do item (a conta pelo cadastro deu menos)"
        : "calculado pelo cadastro, como no painel";
    if (atual && !trocouDeProduto && Math.abs(precoAnterior - linha.unit_price) > 0.0049) {
      avisos.push(`${quemE(linha.title)}: o preço foi recalculado pelo cadastro (de ${dinheiro(precoAnterior)} para ${dinheiro(linha.unit_price)}), como a tela faz ao mudar quantidade, medida ou opção. Para manter o preço anterior, mande manter_preco.`);
    }
  } else {
    origemDoPreco = atual ? "o mesmo de antes" : "não informado";
    /* manter_preco com tamanho ou quantidade mudados: o preço fica travado e
       sai do valor mínimo do item. */
    if (mexeuNaConta) {
      linha.valor_minimo_aplicado = null;
      linha.valor_sem_minimo = null;
    }
  }
  if (!(linha.unit_price > 0)) {
    avisos.push(
      produto?.price_on_request
        ? `${quemE(linha.title)} é sob consulta: informe preco_unitario.`
        : `${quemE(linha.title)} está com preço zero. Se o preço vem das opções, confira se elas foram escolhidas; se não for brinde, informe preco_unitario.`
    );
  }

  if (pedido.custo_unitario !== undefined) {
    if (!pode(ctx, "pricing.read")) recusar("Esta pessoa não vê custos no painel, então não pode mudar o custo do item.");
    linha.unit_cost = dinheiro(pedido.custo_unitario);
  }

  const total = linha.valor_minimo_aplicado != null ? linha.valor_minimo_aplicado : efetiva * linha.unit_price;
  const previa: Record<string, unknown> = {
    titulo: linha.title,
    quantidade: linha.quantity,
    vendido_por: UNIDADE[linha.unit_type] ?? "unidade",
    medidas: medidasDaLinha(linha),
    ...(Object.keys(escolhas).length ? { opcoes: resumoDasEscolhas(escolhas) } : {}),
    preco_unitario: dinheiro(linha.unit_price),
    preco_veio_de: origemDoPreco,
    total: dinheiro(total),
    ...(linha.valor_minimo_aplicado != null
      ? { valor_minimo: textoDoMinimoAplicado(linha.valor_minimo_aplicado, linha.valor_sem_minimo ?? null) }
      : {}),
    do_cadastro: Boolean(produto),
    ...(linha.description ? { descricao: linha.description } : {}),
  };

  const artePedida = pedido.arte as ArtePedida | undefined;
  return { linha, previa, avisos, arte: artePedida && Object.keys(artePedida).length ? artePedida : null, doCadastro: Boolean(produto) };
}

/** O detalhe da conta de um item com produto (para calcular_preco e a prévia). */
export function detalheDoItem(produto: ProdutoDoOrcamento, linha: LinhaDoOrcamento) {
  return detalheDaConta(
    produto,
    { quantidade: linha.quantity, largura: linha.width_m, altura: linha.height_m, comprimento: linha.length_m },
    escolhasDoItem(linha.selected_options)
  );
}

export function selecionadas(selectedOptions: unknown): SelectedOptions | null {
  const lido = selectedOptions as SelectedOptions | null;
  return lido && Array.isArray(lido.options) ? lido : null;
}

/** As opções de um item gravado, para ler: [{atributo, opcao, preco}]. */
export function opcoesParaLer(selectedOptions: unknown) {
  const lido = selecionadas(selectedOptions);
  if (!lido || !lido.options.length) return null;
  return lido.options
    .filter((opcao) => opcao && typeof opcao === "object")
    .map((opcao) => ({ atributo: opcao.attribute_name, opcao: opcao.label, ...(Number(opcao.price) ? { preco: Number(opcao.price) } : {}) }));
}

/* ---------------------------------------------------- condições comerciais */

const FORMA_PELO_TEXTO: Record<string, string> = Object.fromEntries(
  FORMAS_DE_PAGAMENTO.flatMap((forma) => [
    [forma.codigo, forma.codigo],
    [semAcento(forma.nome).toLowerCase(), forma.codigo],
    [semAcento(forma.curto).toLowerCase(), forma.codigo],
  ])
);
FORMA_PELO_TEXTO["cartao"] = "credit_card";
FORMA_PELO_TEXTO["credito"] = "credit_card";
FORMA_PELO_TEXTO["debito"] = "debit_card";
FORMA_PELO_TEXTO["transferencia"] = "transfer";

const MOMENTOS = ["aprovacao", "entrega", "retirada", "faturado"] as const;

export const CAMPOS_DAS_CONDICOES: Campos = {
  modelo: { tipo: "texto", descricao: "Nome de um modelo de condições da empresa (ver_modelos em ver_opcoes_do_orcamento) para partir dele. Os outros campos ajustam por cima.", max: 80 },
  formas: { tipo: "textos", descricao: "Formas de pagamento aceitas: pix, dinheiro, cartão de crédito, cartão de débito, boleto, transferência, link de pagamento.", maxItens: 7, max: 40 },
  esquema: { tipo: "opcao", descricao: "a_vista, entrada_e_saldo ou parcelado.", opcoes: ["a_vista", "entrada_e_saldo", "parcelado"] },
  quando: { tipo: "opcao", descricao: "À vista: quando paga (aprovacao, entrega, retirada ou faturado = N dias depois da entrega).", opcoes: MOMENTOS },
  dias: { tipo: "inteiro", descricao: "À vista faturado: em quantos dias depois da entrega.", min: 1, max: 365 },
  entrada_tipo: { tipo: "opcao", descricao: "Entrada e saldo: a entrada em percentual ou em valor.", opcoes: ["percentual", "valor"] },
  entrada_valor: { tipo: "numero", descricao: "Entrada e saldo: o percentual (1 a 99) ou o valor em reais da entrada.", min: 0, max: 10_000_000 },
  saldo_quando: { tipo: "opcao", descricao: "Entrada e saldo: quando paga o saldo.", opcoes: [...MOMENTOS, "parcelado"] },
  saldo_dias: { tipo: "inteiro", descricao: "Saldo faturado: dias depois da entrega.", min: 1, max: 365 },
  saldo_parcelas: { tipo: "inteiro", descricao: "Saldo parcelado: em quantas parcelas.", min: 2, max: 24 },
  saldo_intervalo_dias: { tipo: "inteiro", descricao: "Saldo parcelado: dias entre as parcelas.", min: 1, max: 120 },
  parcelas: { tipo: "inteiro", descricao: "Parcelado: número de parcelas.", min: 2, max: 24 },
  intervalo_dias: { tipo: "inteiro", descricao: "Parcelado: dias entre as parcelas.", min: 1, max: 120 },
  primeira: { tipo: "opcao", descricao: "Parcelado: quando vence a primeira (aprovacao, entrega ou dias).", opcoes: ["aprovacao", "entrega", "dias"] },
  primeira_dias: { tipo: "inteiro", descricao: "Parcelado com primeira = dias: em quantos dias.", min: 1, max: 365 },
  cartao_parcelas: { tipo: "inteiro", descricao: "Só informativo: em até quantas vezes no cartão.", min: 1, max: 12 },
  cartao_sem_juros: { tipo: "booleano", descricao: "Só informativo: parcelas do cartão sem juros." },
  observacoes: { tipo: "texto", descricao: "Observação das condições (aparece na proposta).", max: 1000 },
};

export async function modelosDaEmpresa(ctx: ContextoDoAssistente): Promise<ModeloDeCondicoes[]> {
  const { data, error } = await ctx.supabase
    .from("profiles")
    .select("quote_default_payment_terms,quote_payment_terms_templates")
    .eq("id", ctx.donoDosDados)
    .maybeSingle();
  if (error) falha("ler os modelos de condições", error);
  const perfil = data as { quote_default_payment_terms?: unknown; quote_payment_terms_templates?: unknown } | null;
  return lerModelos(perfil?.quote_payment_terms_templates, perfil?.quote_default_payment_terms);
}

/** As condições pedidas, na forma que o banco guarda (`normalizarCondicoes`). */
export function condicoesPedidas(pedido: Argumentos, modelos: ModeloDeCondicoes[], atuais: CondicoesComerciais | null): CondicoesComerciais {
  let partida: CondicoesComerciais | null = atuais;
  if (pedido.modelo) {
    const procurado = semAcento(String(pedido.modelo)).toLowerCase().trim();
    const modelo = modelos.find((m) => semAcento(m.nome).toLowerCase().trim() === procurado) ?? (/^padr[aã]o$/i.test(String(pedido.modelo).trim()) ? modelos.find((m) => m.padrao) : undefined);
    if (!modelo) {
      recusar(modelos.length ? `Não existe o modelo de condições "${pedido.modelo}". Modelos da empresa: ${modelos.map((m) => m.nome).join(", ")}.` : "A empresa não tem modelos de condições salvos. Mande as condições pelos campos (formas, esquema...).");
    }
    partida = modelo.condicoes;
  }
  const bruto: Record<string, unknown> = JSON.parse(JSON.stringify(partida ?? condicoesDoPadrao(modelos) ?? padraoDasCondicoes()));
  const parte = (chave: string) => ((bruto[chave] ??= {}) as Record<string, unknown>);
  if (pedido.formas !== undefined) {
    const formas = (pedido.formas as string[]).map((texto) => {
      const codigo = FORMA_PELO_TEXTO[semAcento(texto).toLowerCase().trim()];
      if (!codigo) recusar(`Forma de pagamento desconhecida: "${texto}". Use: ${FORMAS_DE_PAGAMENTO.map((f) => f.nome).join(", ")}.`);
      return codigo;
    });
    bruto.formas = formas;
  }
  if (pedido.esquema !== undefined) bruto.esquema = pedido.esquema;
  if (pedido.quando !== undefined) parte("a_vista").quando = pedido.quando;
  if (pedido.dias !== undefined) parte("a_vista").dias = pedido.dias;
  if (pedido.entrada_tipo !== undefined) parte("entrada").tipo = pedido.entrada_tipo;
  if (pedido.entrada_valor !== undefined) parte("entrada").valor = pedido.entrada_valor;
  if (pedido.saldo_quando !== undefined) parte("saldo").quando = pedido.saldo_quando;
  if (pedido.saldo_dias !== undefined) parte("saldo").dias = pedido.saldo_dias;
  if (pedido.saldo_parcelas !== undefined) parte("saldo").parcelas = pedido.saldo_parcelas;
  if (pedido.saldo_intervalo_dias !== undefined) parte("saldo").intervalo_dias = pedido.saldo_intervalo_dias;
  if (pedido.parcelas !== undefined) parte("parcelado").parcelas = pedido.parcelas;
  if (pedido.intervalo_dias !== undefined) parte("parcelado").intervalo_dias = pedido.intervalo_dias;
  if (pedido.primeira !== undefined) parte("parcelado").primeira = pedido.primeira;
  if (pedido.primeira_dias !== undefined) parte("parcelado").dias = pedido.primeira_dias;
  if (pedido.cartao_parcelas !== undefined) parte("cartao").parcelas = pedido.cartao_parcelas;
  if (pedido.cartao_sem_juros !== undefined) parte("cartao").sem_juros = pedido.cartao_sem_juros;
  if (pedido.observacoes !== undefined) bruto.observacoes = pedido.observacoes;
  return normalizarCondicoes(bruto) as CondicoesComerciais;
}

/** O que barra o salvar (erroDasCondicoes) e o texto que a proposta mostra. */
export function conferirCondicoes(condicoes: CondicoesComerciais | null, total: number) {
  if (!condicoes) return null;
  const erro = erroDasCondicoes(condicoes, total);
  if (erro) recusar(`Condições comerciais: ${erro}`);
  return resumirCondicoes(condicoes, total);
}

/* --------------------------------------------------------- campos próprios */

export const CAMPOS_DOS_CAMPOS_PROPRIOS: Campos = {
  campo: { tipo: "texto", descricao: "Chave ou nome do campo próprio (de listar_campos_proprios).", obrigatorio: true, max: 80 },
  valor: { tipo: "texto", descricao: "O valor, como texto. Vazio apaga.", max: 4000 },
};

const comparavelDoCampo = (texto: string) => semAcento(texto).toLowerCase().replace(/\s+/g, " ").trim();

/**
 * Os campos próprios do orçamento ou do pedido, como a tela salva: só os
 * ativos, e os obrigatórios precisam estar preenchidos para gravar.
 * Devolve null quando a empresa não tem campo ativo (a tela nem mexe).
 */
export async function camposPropriosParaGravar(
  ctx: ContextoDoAssistente,
  entidade: "orcamento" | "pedido",
  pedidos: Array<{ campo: string; valor?: string }> | undefined,
  atuais: unknown
): Promise<{ valores: ValoresDosCampos; mudou: Record<string, string> } | null> {
  const { definicoes, erro } = await lerDefinicoes(ctx.supabase, entidade);
  if (erro) falha("ler os campos próprios", { message: erro });
  const ativos = definicoes.filter((def) => def.ativo);
  if (!ativos.length) {
    if (pedidos?.length) recusar(`${entidade === "pedido" ? "O pedido" : "O orçamento"} não tem campos próprios ligados. Crie com criar_campo_proprio.`);
    return null;
  }
  const novos: Record<string, unknown> = {};
  const rotulos: Record<string, DefinicaoDeCampo> = {};
  for (const item of pedidos ?? []) {
    const procurado = String(item.campo ?? "").trim();
    const def =
      ativos.find((d) => d.chave === procurado) ??
      ativos.find((d) => comparavelDoCampo(d.rotulo) === comparavelDoCampo(procurado)) ??
      ativos.find((d) => d.chave === chaveDoRotulo(procurado));
    if (!def) recusar(`Não existe o campo próprio "${procurado}". Campos ligados: ${ativos.map((d) => `${d.rotulo} (${d.chave})`).join(", ")}.`);
    novos[def.chave] = item.valor ?? null;
    rotulos[def.chave] = def;
  }
  const valoresAtuais = valoresDaLinha(atuais);
  const conferido = conferirValores(definicoes, novos, { atuais: valoresAtuais, exigirObrigatorios: true });
  if (!conferido.ok) {
    const nomes = Object.entries(conferido.erros).map(([chave, mensagem]) => `${rotulos[chave]?.rotulo ?? definicoes.find((d) => d.chave === chave)?.rotulo ?? chave}: ${mensagem}`);
    recusar(`Campos próprios: ${nomes.join(" ")} A tela não salva sem eles; pergunte à pessoa e mande em "campos".`);
  }
  const mudou: Record<string, string> = {};
  for (const [chave, def] of Object.entries(rotulos)) {
    if ((valoresAtuais[chave] ?? null) !== (conferido.valores[chave] ?? null)) mudou[def.rotulo] = formatarValor(def, conferido.valores[chave]) || "(vazio)";
  }
  return { valores: conferido.valores, mudou };
}

/* ----------------------------------------------------------- arte do item */

export type ArtePronta = { titulo: string | null; observacao: string | null; imagens: Buffer[]; origens: string[] };

/** Lê e prepara as imagens da arte (webp de até 2000 px, qualidade 82: o que a tela faz no navegador). */
export async function prepararArte(ctx: ContextoDoAssistente, arte: ArtePedida): Promise<ArtePronta> {
  const imagens: Buffer[] = [];
  const origens: string[] = [];
  for (const valor of arte.imagens ?? []) {
    const recebida = await receberImagem(ctx, valor, { minimo: 16 });
    const webp = await sharp(recebida.conteudo, { limitInputPixels: 60_000_000, animated: false })
      .rotate()
      .resize({ width: 2000, height: 2000, fit: "inside", withoutEnlargement: true })
      .webp({ quality: 82 })
      .toBuffer();
    imagens.push(webp);
    origens.push(recebida.origem);
  }
  return { titulo: arte.titulo?.trim() || null, observacao: arte.observacao?.trim() || null, imagens, origens };
}

/** Grava a arte de um item: imagens no depósito `quote-arts` e a linha em `quote_item_arts`. */
export async function gravarArte(
  ctx: ContextoDoAssistente,
  params: { orcamentoId: string; donoDoOrcamento: string; itemId: string; arte: ArtePronta; existente?: { id: string; image_urls: string[] } | null }
) {
  const enderecos: string[] = [];
  for (const imagem of params.arte.imagens) {
    const caminho = `${params.donoDoOrcamento}/quotes/${params.orcamentoId}/items/${params.itemId}/${randomUUID()}.webp`;
    const { error } = await ctx.supabase.storage.from("quote-arts").upload(caminho, imagem, { contentType: "image/webp" });
    if (error) falha("enviar a imagem da arte", error);
    const publico = ctx.supabase.storage.from("quote-arts").getPublicUrl(caminho).data.publicUrl;
    if (publico) enderecos.push(publico);
  }
  if (params.existente) {
    const mudancas: Record<string, unknown> = { image_urls: [...params.existente.image_urls, ...enderecos] };
    if (params.arte.titulo !== null) mudancas.title = params.arte.titulo;
    if (params.arte.observacao !== null) mudancas.notes = params.arte.observacao;
    const { error } = await ctx.supabase.from("quote_item_arts").update(mudancas).eq("id", params.existente.id);
    if (error) falha("gravar a arte do item", error);
    return;
  }
  if (!params.arte.titulo && !params.arte.observacao && !enderecos.length) return;
  const { error } = await ctx.supabase.from("quote_item_arts").insert({
    quote_id: params.orcamentoId,
    quote_item_id: params.itemId,
    title: params.arte.titulo,
    notes: params.arte.observacao,
    image_urls: enderecos,
  });
  if (error) falha("gravar a arte do item", error);
}

/** Se a empresa ligou o cadastro de arte no orçamento (a tela "Novo orçamento" só mostra assim). */
export async function arteLigada(ctx: ContextoDoAssistente) {
  const { data } = await ctx.supabase.from("profiles").select("quote_art_registration_enabled").eq("id", ctx.donoDosDados).maybeSingle();
  return Boolean((data as { quote_art_registration_enabled?: boolean | null } | null)?.quote_art_registration_enabled);
}

export { precoDoPainel, quantidadeEfetiva };
