import { randomUUID } from "node:crypto";

import { custoDoProduto } from "@/lib/precificacao/custo-do-produto";
import { resolveSaleQuantityLimits, type ProductPriceTier } from "@/lib/price-tiers";
import { sanitizeProductDescription } from "@/lib/product-description-rich-text";
import { buildAllowedQuantityPricing } from "@/lib/quantity-intervals";
import { vendidoPorMedida } from "@/lib/adicionais";

import { contatosNovos, recusaDeContatos } from "../contatos";
import type { ContextoDoAssistente } from "../contexto";
import type { Campos } from "../esquema";
import { CAMPOS_DA_FICHA, pedacoDaFicha } from "./cadastro-de-produto-ficha";
import { exigir, pode, recusar, um } from "./comum";
import { CAMPO_DE_ATRIBUTOS, CAMPO_DE_FOTOS } from "./produto-completo-campos";
import {
  cadastrarComoOPainel,
  cadastrosDeProducao,
  desfazerFotos,
  escopoEfetivo,
  fotoParaOProduto,
  gravarComoOPainel,
  guardarFotoDoProduto,
  lerFicha,
  type FotoPronta,
} from "./produto-completo-ficha";
import { adicionarFotos, aplicarPedidosNosAtributos, variacoesParaSalvar, type AtributoDaFicha, type PedidoDeAtributo } from "./produto-completo-regras";
import {
  avisosDoPreco,
  descricaoParaLer,
  emReais,
  lerFaixas,
  NOME_DA_UNIDADE,
  OPCOES_DE_VENDA,
  OPCOES_DO_PRAZO,
  quantidade,
  resumoDasMudancas,
  soOQueMuda,
  textoDaMargem,
  textoDasFaixas,
  textoDoPrazo,
  textoDoPreco,
  UNIDADES_DO_PRAZO,
  VALOR_MAXIMO,
  VENDIDO_POR,
  type FaixaDoAssistente,
  type Mudanca,
} from "./regras-do-produto";
import type { FerramentaDeEscrita } from "./tipos";

/**
 * Cadastrar e alterar produto pelo assistente.
 *
 * Desde 15/10/2026 grava pelo MESMO caminho da tela do produto
 * (lib/produtos/servidor/gravar-produto.ts, a função da rota /api/products),
 * com a sessão da própria pessoa: as conferências e os formatos no banco são
 * os do painel. O cadastro aceita a ficha quase inteira de uma vez (fotos,
 * atributos, frete, fiscal, estoque, vídeo); o resto da ficha tem ferramenta
 * própria (fotos, variações, serviços, compre junto, campos personalizados,
 * ficha técnica, estoque).
 *
 * Duas escolhas de propósito, diferentes do painel:
 * - produto novo nasce FORA da loja virtual. Publicar é pedido à parte (e
 *   pede a permissão de publicar). Ninguém quer descobrir pela loja que o
 *   assistente cadastrou um rascunho;
 * - mudar o preço de um produto sob consulta tira ele de sob consulta, e a
 *   prévia avisa. No painel isso dependia de mandar os dois campos juntos.
 */

const DEPOIS_DO_CADASTRO =
  "Para o resto da ficha: adicionar_fotos_ao_produto, alterar_variacoes_do_produto, alterar_servicos_do_produto, alterar_compre_junto, alterar_campos_personalizados_do_produto, alterar_ficha_tecnica_do_produto e ajustar_estoque_do_produto. ver_produto_completo mostra tudo.";

const CAMPOS_COMUNS: Campos = {
  preco: { tipo: "numero", descricao: "Preço de venda em reais, por unidade de venda.", min: 0, max: VALOR_MAXIMO },
  sob_consulta: { tipo: "booleano", descricao: "Verdadeiro para o produto sem preço fixo (o cliente pede orçamento). Não use junto com preco." },
  so_preco_das_opcoes: {
    tipo: "booleano",
    descricao: "Verdadeiro quando o preço vem só das opções escolhidas (os adicionais dos atributos); o preço base fica zero. É o \"Usar preço apenas das variações\" da tela.",
  },
  valor_minimo_item: {
    tipo: "numero",
    descricao:
      "Só para produto vendido por m² ou por metro (lona, adesivo, chapa): o valor mínimo de cada item, em reais. Cada linha do orçamento, do carrinho ou do PDV (todas as peças daquela linha juntas) é comparada sozinha com ele; se a conta der menos, a linha custa o mínimo. Zero tira o mínimo.",
    min: 0,
    max: VALOR_MAXIMO,
  },
  valor_minimo_nos_orcamentos: { tipo: "booleano", descricao: "Se o valor mínimo do item vale nos orçamentos. O padrão é sim." },
  valor_minimo_na_loja: { tipo: "booleano", descricao: "Se o valor mínimo do item vale na loja virtual. O padrão é sim." },
  valor_minimo_no_pdv: { tipo: "booleano", descricao: "Se o valor mínimo do item vale no PDV. O padrão é sim." },
  categoria_id: { tipo: "id", descricao: "Id da categoria, devolvido por listar_categorias." },
  descricao: { tipo: "texto", descricao: "Descrição do produto, como o cliente vai ler. Pode ter parágrafos.", max: 4000 },
  custo: {
    tipo: "numero",
    descricao: "Custo do produto em reais, por unidade de venda, fora a ficha técnica. Só para quem pode mexer na precificação.",
    min: 0,
    max: VALOR_MAXIMO,
  },
  codigo: { tipo: "texto", descricao: "Código interno (SKU).", max: 60 },
  codigo_de_barras: { tipo: "texto", descricao: "Código de barras (EAN).", max: 60 },
  na_loja: { tipo: "booleano", descricao: "Se o produto aparece na loja virtual. Publicar e tirar da loja pede a permissão de publicar." },
  prazo_de_producao: { tipo: "numero", descricao: "Prazo de produção. Use junto com unidade_do_prazo.", min: 0.5, max: 365 },
  unidade_do_prazo: { tipo: "opcao", descricao: "Unidade do prazo de produção. O padrão é dias úteis.", opcoes: OPCOES_DO_PRAZO },
  quantidade_minima: { tipo: "numero", descricao: "Quantidade mínima por pedido. Zero tira o mínimo.", min: 0, max: 1_000_000 },
  faixas_de_preco: {
    tipo: "lista",
    descricao:
      "Preço por quantidade. Cada faixa diz a partir de quantas unidades vale e o preço de cada unidade. Substitui as faixas que existem; lista vazia tira todas.",
    maxItens: 20,
    itens: {
      a_partir_de: { tipo: "numero", descricao: "Quantidade em que a faixa começa.", obrigatorio: true, min: 0.001, max: 10_000_000 },
      ate: { tipo: "numero", descricao: "Quantidade em que a faixa termina. Sem isto, vale daí para cima.", min: 0.001, max: 10_000_000 },
      preco_unitario: { tipo: "numero", descricao: "Preço de cada unidade nesta faixa, em reais.", obrigatorio: true, min: 0.01, max: VALOR_MAXIMO },
    },
  },
  quantidades_fixas: {
    tipo: "lista",
    descricao:
      'Vende só em quantidades fixas, cada uma com o preço TOTAL dela (o "Intervalo de Quantidades" da tela: 100 por R$ 50, 250 por R$ 100...). Liga isso e desliga quantidade mínima, sob consulta e preço só das opções. Substitui a lista; lista vazia desliga.',
    maxItens: 30,
    itens: {
      quantidade: { tipo: "numero", descricao: "A quantidade vendida.", obrigatorio: true, min: 0.001, max: 10_000_000 },
      preco_total: { tipo: "numero", descricao: "O preço total desta quantidade, em reais.", obrigatorio: true, min: 0.01, max: VALOR_MAXIMO },
    },
  },
  ...CAMPOS_DA_FICHA,
};

type Categoria = { id: string; name: string | null };

async function categoriaDaEmpresa(ctx: ContextoDoAssistente, id: unknown): Promise<Categoria | null> {
  if (!id) return null;
  const { data, error } = await ctx.supabase.from("categories").select("id,name").eq("id", String(id)).in("user_id", ctx.tenantIds).maybeSingle();
  if (error) recusar("Não deu para conferir a categoria agora. Tente de novo em instantes.");
  if (!data) recusar("Categoria não encontrada nesta empresa. Use listar_categorias para pegar o id certo.");
  return data as Categoria;
}

/**
 * Os produtos com o mesmo nome, sem diferença de maiúsculas.
 *
 * No filtro do banco, `%`, `_` e `*` são curingas: um nome com eles viraria
 * outra busca. Cada um deles vira `_` (um caractere qualquer), o que ainda
 * acha o nome certo, e a conferência exata é feita aqui.
 */
async function produtosComOMesmoNome(ctx: ContextoDoAssistente, nome: string) {
  const { data, error } = await ctx.supabase
    .from("products")
    .select("id,name")
    .in("user_id", ctx.tenantIds)
    .neq("product_kind", "digital")
    .ilike("name", nome.replace(/[%_*\\]/g, "_"))
    .limit(10);
  if (error) return [];
  const alvo = nome.toLocaleLowerCase("pt-BR");
  return ((data ?? []) as Array<{ id: string; name: string | null }>)
    .filter((produto) => String(produto.name ?? "").replace(/\s+/g, " ").trim().toLocaleLowerCase("pt-BR") === alvo)
    .slice(0, 3);
}

function prazoPedido(args: Record<string, unknown>, unidadeAtual?: string | null): { valor: number; unidade: string } | null {
  if (args.prazo_de_producao === undefined) {
    if (args.unidade_do_prazo !== undefined) recusar("Informe também prazo_de_producao, junto com a unidade.");
    return null;
  }
  const unidade = args.unidade_do_prazo
    ? UNIDADES_DO_PRAZO[args.unidade_do_prazo as keyof typeof UNIDADES_DO_PRAZO]
    : unidadeAtual || UNIDADES_DO_PRAZO["dias úteis"];
  return { valor: Number(args.prazo_de_producao), unidade };
}

function exigirOsDoPreco(ctx: ContextoDoAssistente, args: Record<string, unknown>) {
  if (args.custo !== undefined) exigir(ctx, "pricing.config.write", "mexer no custo dos produtos");
  if (args.na_loja !== undefined) exigir(ctx, "catalog.products.write", "publicar ou tirar produtos da loja virtual");
}

/** As quantidades fixas como a tela salva: a lista permitida e uma faixa exata por quantidade (preço total ÷ quantidade). */
function lerQuantidadesFixas(pedidas: Array<{ quantidade: number; preco_total: number }>) {
  const vistas = new Set<number>();
  const intervalos = pedidas.map((item) => {
    const qtd = Number(Number(item.quantidade).toFixed(3));
    if (vistas.has(qtd)) recusar(`A quantidade ${quantidade(qtd)} aparece duas vezes nas quantidades fixas.`);
    vistas.add(qtd);
    return { quantity: qtd, totalPrice: Number(Number(item.preco_total).toFixed(2)) };
  });
  intervalos.sort((a, b) => a.quantity - b.quantity);
  return buildAllowedQuantityPricing(intervalos);
}

const textoDasFixas = (pedidas: Array<{ quantidade: number; preco_total: number }>) =>
  pedidas
    .slice()
    .sort((a, b) => a.quantidade - b.quantidade)
    .map((item) => `${quantidade(item.quantidade)} por ${emReais(item.preco_total)}`)
    .join("; ");

/** Opção a vincular a uma foto (campo atributo/opcao de cada foto): vira um pedido de atributo com foto_id. */
function pedidosDeVinculoDasFotos(fotos: Array<{ id: string; atributo?: string; opcao?: string }>): PedidoDeAtributo[] {
  const pedidos: PedidoDeAtributo[] = [];
  for (const foto of fotos) {
    if (!foto.atributo && !foto.opcao) continue;
    if (!foto.atributo || !foto.opcao) recusar("Para ligar a foto a uma opção, mande atributo e opcao juntos (ex.: Cor e Azul).");
    pedidos.push({ nome: foto.atributo, opcoes: [{ rotulo: foto.opcao, foto_id: foto.id }] });
  }
  return pedidos;
}

/** Confere que o vínculo foto → opção aponta para opção que existe (vincular não cria opção). */
function conferirOpcoesDosVinculos(atributos: AtributoDaFicha[], pedidos: PedidoDeAtributo[]) {
  for (const pedido of pedidos) {
    const atributo = atributos.find((item) => item.name.trim().toLocaleLowerCase("pt-BR") === pedido.nome.trim().toLocaleLowerCase("pt-BR"));
    const rotulo = pedido.opcoes?.[0]?.rotulo ?? "";
    if (!atributo || !atributo.options.some((opcao) => String(opcao.label).trim().toLocaleLowerCase("pt-BR") === rotulo.trim().toLocaleLowerCase("pt-BR"))) {
      recusar(`A foto pede a opção "${rotulo}" do atributo "${pedido.nome}", que não existe no produto. Crie a opção antes (campo atributos).`);
    }
  }
}

type FotoPedida = { imagem: string; principal?: boolean; atributo?: string; opcao?: string };

async function prepararFotos(ctx: ContextoDoAssistente, pedidas: FotoPedida[]) {
  const prontas: Array<FotoPronta & { id: string; principal: boolean; atributo?: string; opcao?: string }> = [];
  for (const pedida of pedidas) {
    const foto = await fotoParaOProduto(ctx, pedida.imagem);
    prontas.push({ ...foto, id: randomUUID(), principal: pedida.principal === true, atributo: pedida.atributo, opcao: pedida.opcao });
  }
  if (prontas.filter((foto) => foto.principal).length > 1) recusar("Só uma foto pode ser a principal.");
  return prontas;
}

/* ------------------------------------------------------------------ criar */

type NovoProduto = {
  corpo: Record<string, unknown>;
  fotos: Array<FotoPronta & { id: string; principal: boolean }>;
  atributos: AtributoDaFicha[];
  nome: string;
};

export const criarProduto: FerramentaDeEscrita<NovoProduto> = {
  nome: "criar_produto",
  titulo: "Cadastrar um produto",
  descricao:
    "Cadastra um produto novo com a ficha quase inteira de uma vez, como a tela do produto: nome, preço (ou sob consulta, ou preço só das opções, ou quantidades fixas), unidade de venda, categoria, descrição, custo, códigos, prazo, quantidade mínima, faixas de preço, FOTOS (imagem enviada pela pessoa, link de imagem da internet ou foto de outro produto), atributos com opções e adicionais, frete, dados fiscais, controle de estoque, vídeo e gabaritos. O produto nasce fora da loja virtual, a não ser que na_loja seja verdadeiro. Antes, use buscar_produtos para não cadastrar repetido. Para foto anexada na conversa, chame pedir_imagem antes e use o código envio:...",
  campos: {
    nome: { tipo: "texto", descricao: "Nome do produto, como aparece no orçamento e na loja.", obrigatorio: true, min: 2, max: 150 },
    vendido_por: { tipo: "opcao", descricao: "Como o produto é vendido. O padrão é unidade.", opcoes: OPCOES_DE_VENDA },
    ...CAMPOS_COMUNS,
    fotos: CAMPO_DE_FOTOS,
    atributos: CAMPO_DE_ATRIBUTOS,
  },
  tipo: "escrita",
  permissoes: ["products.catalog.write"],
  abertoAoMundo: true,
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "cadastrar produtos");
    exigirOsDoPreco(ctx, args);

    const nome = String(args.nome).replace(/\s+/g, " ").trim();
    const sobConsulta = args.sob_consulta === true;
    const soOpcoes = args.so_preco_das_opcoes === true;
    const fixasPedidas = (args.quantidades_fixas as Array<{ quantidade: number; preco_total: number }> | undefined) ?? [];
    const comFixas = fixasPedidas.length > 0;
    if ([sobConsulta, soOpcoes, comFixas].filter(Boolean).length > 1) recusar("Escolha um jeito de dar preço: sob consulta, preço só das opções ou quantidades fixas.");
    if ((sobConsulta || soOpcoes) && args.preco !== undefined) recusar("Escolha um dos dois: o preço, ou sob consulta / preço só das opções.");
    if (!sobConsulta && !soOpcoes && !comFixas && !(Number(args.preco) > 0)) recusar("Informe o preço de venda (maior que zero), ou marque sob_consulta, so_preco_das_opcoes ou quantidades_fixas.");
    const preco = sobConsulta || soOpcoes ? 0 : Number(args.preco ?? 0);
    const unidade = VENDIDO_POR[(args.vendido_por as keyof typeof VENDIDO_POR | undefined) ?? "unidade"];

    const categoria = await categoriaDaEmpresa(ctx, args.categoria_id);
    const prazo = prazoPedido(args);
    const minimo = Number(args.quantidade_minima ?? 0);
    if (minimo > 0 && comFixas) recusar("Quantidade mínima e quantidades fixas não valem juntas (como na tela).");
    const limites = { min_sale_qty_enabled: minimo > 0, min_sale_qty: minimo > 0 ? minimo : null };

    let faixas: ProductPriceTier[] = [];
    const faixasPedidas = (args.faixas_de_preco as FaixaDoAssistente[] | undefined) ?? [];
    if (faixasPedidas.length) {
      if (sobConsulta) recusar("Produto sob consulta não tem faixas de preço.");
      if (comFixas) recusar("Com quantidades fixas, o preço de cada quantidade já vem na lista: não mande faixas_de_preco junto.");
      const lidas = lerFaixas(faixasPedidas, limites);
      if (!lidas.ok) recusar(lidas.erro);
      faixas = lidas.faixas;
    }
    const fixas = comFixas ? lerQuantidadesFixas(fixasPedidas) : null;

    const descricao = args.descricao ? sanitizeProductDescription(String(args.descricao)) : "";
    const contatos = contatosNovos(null, `${nome}\n${descricao}`);
    if (contatos.length) recusar(recusaDeContatos(contatos, "na tela do produto"));
    const custo = Number(args.custo ?? 0);
    const naLoja = args.na_loja === true;

    /* Fotos: baixadas e preparadas já na prévia (se o link não abrir, a pessoa sabe antes). */
    const fotosPedidas = (args.fotos as FotoPedida[] | undefined) ?? [];
    const fotos = await prepararFotos(ctx, fotosPedidas);
    const galeriaPrevista = adicionarFotos(
      [],
      fotos.map((foto) => ({ id: foto.id, image_url: `pendente:${foto.id}`, principal: foto.principal }))
    );

    /* Atributos: as mesmas regras do "Adicionar atributo/opção" da tela. */
    const pedidosDeAtributo = (args.atributos as PedidoDeAtributo[] | undefined) ?? [];
    let atributos: AtributoDaFicha[] = [];
    let mudancasDosAtributos: string[] = [];
    if (pedidosDeAtributo.length || fotos.some((foto) => foto.atributo || foto.opcao)) {
      const precisaDosInsumos = pedidosDeAtributo.some((pedido) => pedido.opcoes?.some((opcao) => opcao.insumo_id && opcao.insumo_id.toLowerCase() !== "nenhum"));
      const insumos = precisaDosInsumos ? new Set((await cadastrosDeProducao(ctx)).insumos.map((insumo) => insumo.id)) : undefined;
      const vinculos = pedidosDeVinculoDasFotos(fotos);
      const resultado = aplicarPedidosNosAtributos([], pedidosDeAtributo, { unitType: unidade, fotos: galeriaPrevista, novoId: randomUUID, insumos });
      if (!resultado.ok) recusar(resultado.erro);
      conferirOpcoesDosVinculos(resultado.atributos, vinculos);
      const comFotos = aplicarPedidosNosAtributos(resultado.atributos, vinculos, { unitType: unidade, fotos: galeriaPrevista, novoId: randomUUID });
      if (!comFotos.ok) recusar(comFotos.erro);
      atributos = comFotos.atributos;
      mudancasDosAtributos = resultado.mudancas;
    }
    if (soOpcoes && !atributos.some((atributo) => atributo.options.some((opcao) => Number(opcao.price) > 0))) {
      recusar("Com so_preco_das_opcoes, o preço vem dos adicionais das opções: mande os atributos com preco_adicional nas opções.");
    }

    const ficha = await pedacoDaFicha(ctx, args, null, atributos);
    const minimoDoItem = valorMinimoPedido(args, unidade, null);

    /* Nome repetido não impede (há produto com o mesmo nome em tamanhos
       diferentes), mas a pessoa precisa saber. */
    const avisos: string[] = [...ficha.avisos];
    for (const parecido of await produtosComOMesmoNome(ctx, nome)) {
      avisos.push(`Já existe um produto com este nome: ${parecido.name} (id ${parecido.id}). Confira se não é o mesmo.`);
    }

    /* O rascunho da tela de produto novo (products/new), já com o que a
       pessoa pediu; fotos e atributos entram depois, pelo Salvar da tela. */
    const corpo: Record<string, unknown> = {
      name: nome,
      description: descricao,
      price: preco,
      price_on_request: sobConsulta,
      use_variation_price_only: soOpcoes,
      estimated_base_cost: custo,
      stock: 0,
      unit_type: unidade,
      active: true,
      category_id: categoria?.id ?? null,
      image_url: null,
      show_in_catalog: naLoja,
      catalog_color: "#ffffff",
      is_catalog_exclusive: false,
      min_sale_qty_enabled: limites.min_sale_qty_enabled,
      min_sale_qty: limites.min_sale_qty,
      max_sale_qty_enabled: false,
      max_sale_qty: null,
      allowed_sale_qty_enabled: Boolean(fixas),
      allowed_sale_quantities: fixas ? fixas.allowedQuantities : null,
      sku: args.codigo ? String(args.codigo) : null,
      barcode: args.codigo_de_barras ? String(args.codigo_de_barras) : null,
      track_inventory: false,
      inventory_scope: "product",
      inventory_attribute_name: null,
      allow_negative_stock: false,
      production_time_value: prazo?.valor ?? null,
      production_time_unit: prazo?.unidade ?? null,
      materials: [],
      processes: [],
      attributes: [],
      variations: [],
      custom_fields_config: [],
      price_tiers: fixas ? fixas.tiers : faixas,
      images: [],
      ...ficha.patch,
      ...minimoDoItem.patch,
    };

    const veOCusto = pode(ctx, "pricing.config.read");
    const custoTotal = custoDoProduto({ estimatedBaseCost: custo, somaDaFicha: 0, unitType: unidade });
    return {
      previa: {
        o_que_vai_acontecer: "Um produto novo será cadastrado.",
        produto: {
          nome,
          preco: soOpcoes ? "só das opções (soma dos adicionais escolhidos)" : fixas ? `quantidades fixas: ${textoDasFixas(fixasPedidas)}` : textoDoPreco(preco, unidade, sobConsulta),
          categoria: categoria?.name ?? null,
          descricao: descricaoParaLer(descricao, 600),
          ...(veOCusto && custo > 0 ? { custo: emReais(custoTotal), margem: textoDaMargem(preco, custoTotal) } : {}),
          codigo: corpo.sku,
          codigo_de_barras: corpo.barcode,
          prazo_de_producao: prazo ? textoDoPrazo(prazo.valor, prazo.unidade) : null,
          quantidade_minima: minimo > 0 ? quantidade(minimo) : null,
          faixas_de_preco: faixas.length ? textoDasFaixas(faixas) : null,
          ...(minimoDoItem.mudancas.length
            ? { valor_minimo_do_item: minimoDoItem.mudancas.map((mudanca) => `${mudanca.campo}: ${mudanca.depois ?? "vazio"}`) }
            : {}),
          fotos: fotos.length ? fotos.map((foto, indice) => `${indice + 1}. ${foto.origem} (${foto.largura} × ${foto.altura})${foto.principal ? " — principal" : ""}`) : null,
          atributos: mudancasDosAtributos.length ? mudancasDosAtributos : null,
          ...(ficha.mudancas.length ? { outros: ficha.mudancas.map((mudanca) => `${mudanca.campo}: ${mudanca.depois ?? "vazio"}`) } : {}),
          loja_virtual: naLoja ? "Aparece na loja virtual assim que for cadastrado." : "Fica fora da loja virtual. Dá para publicar depois, pelo assistente ou pelo painel.",
        },
      },
      resumo: `Prévia do cadastro do produto ${nome}`,
      avisos,
      carga: { corpo, fotos, atributos, nome },
    };
  },
  async gravar(ctx, carga) {
    const produtoId = await cadastrarComoOPainel(ctx, carga.corpo);

    let atencao: string | null = null;
    if (carga.fotos.length || carga.atributos.length) {
      const subidas: string[] = [];
      try {
        const enderecos = new Map<string, string>();
        for (const foto of carga.fotos) {
          const guardada = await guardarFotoDoProduto(ctx, foto);
          subidas.push(guardada.caminho);
          enderecos.set(foto.id, guardada.url);
        }
        const galeria = adicionarFotos(
          [],
          carga.fotos.map((foto) => ({ id: foto.id, image_url: enderecos.get(foto.id) as string, principal: foto.principal }))
        );
        const patch: Record<string, unknown> = { images: galeria, image_url: galeria.find((foto) => foto.is_primary)?.image_url ?? null };
        if (carga.atributos.length) {
          const escopo = carga.corpo.track_inventory ? String(carga.corpo.inventory_scope || "product") : "product";
          patch.attributes = carga.atributos;
          patch.variations = variacoesParaSalvar(carga.atributos, [], escopo);
        }
        await gravarComoOPainel(ctx, produtoId, patch, "gravar as fotos e os atributos do produto");
      } catch (erro) {
        await desfazerFotos(ctx, subidas);
        console.error("[mcp] produto cadastrado sem fotos/atributos:", produtoId, erro instanceof Error ? erro.message : erro);
        atencao = `O produto foi cadastrado, mas as fotos e os atributos não entraram (${erro instanceof Error ? erro.message : "erro"}). Tente de novo com adicionar_fotos_ao_produto e alterar_variacoes_do_produto.`;
      }
    }

    return {
      dados: {
        criado: true,
        produto_id: produtoId,
        nome: carga.nome,
        na_loja: carga.corpo.show_in_catalog === true,
        fotos: atencao ? 0 : carga.fotos.length,
        atributos: atencao ? 0 : carga.atributos.length,
        no_painel: `${ctx.origemDoAdmin.replace(/\/$/, "")}/dashboard/products/${produtoId}`,
        ...(atencao ? { atencao } : {}),
        proximo_passo: DEPOIS_DO_CADASTRO,
      },
      resumo: atencao ? `Cadastrou o produto ${carga.nome} (sem fotos e atributos)` : `Cadastrou o produto ${carga.nome}`,
    };
  },
};

/* ------------------------------------------------- valor mínimo do item */

const CHAVES_DO_MINIMO = [
  ["valor_minimo_nos_orcamentos", "valor_minimo_orcamentos", "nos orçamentos"],
  ["valor_minimo_na_loja", "valor_minimo_loja", "na loja"],
  ["valor_minimo_no_pdv", "valor_minimo_pdv", "no PDV"],
] as const;

/**
 * O valor mínimo do item que a pessoa pediu (migration 194), como a tela grava:
 * o valor (zero tira) e as três chaves de onde vale. `atual` vazio é cadastro.
 */
function valorMinimoPedido(args: Record<string, unknown>, unidade: string, atual: Record<string, unknown> | null) {
  const patch: Record<string, unknown> = {};
  const mudancas: Mudanca[] = [];
  const tocou = args.valor_minimo_item !== undefined || CHAVES_DO_MINIMO.some(([campo]) => args[campo] !== undefined);
  if (!tocou) return { patch, mudancas };
  const valorPedido = Number(args.valor_minimo_item);
  if (!vendidoPorMedida(unidade) && (args.valor_minimo_item === undefined ? Number(atual?.valor_minimo_item) > 0 : valorPedido > 0)) {
    recusar("O valor mínimo do item só vale para produto vendido por m² ou por metro.");
  }
  if (args.valor_minimo_item !== undefined) {
    const novo = valorPedido > 0 ? Math.round(valorPedido * 100) / 100 : null;
    const antes = Number(atual?.valor_minimo_item) > 0 ? Number(atual?.valor_minimo_item) : null;
    if (novo !== antes) {
      patch.valor_minimo_item = novo;
      mudancas.push({ campo: "valor mínimo do item", antes: antes === null ? "sem mínimo" : emReais(antes), depois: novo === null ? "sem mínimo" : emReais(novo) });
    }
  }
  for (const [campo, coluna, onde] of CHAVES_DO_MINIMO) {
    if (args[campo] === undefined) continue;
    const novo = args[campo] !== false;
    const antes = atual ? atual[coluna] !== false : true;
    if (novo === antes && atual) continue;
    patch[coluna] = novo;
    mudancas.push({ campo: `valor mínimo vale ${onde}`, antes: atual ? (antes ? "sim" : "não") : null, depois: novo ? "sim" : "não" });
  }
  return { patch, mudancas };
}

/* -------------------------------------------------------------- atualizar */

type ProdutoAtual = Record<string, unknown> & {
  id: string;
  user_id: string;
  name: string;
  description: string | null;
  price: number | null;
  price_on_request: boolean | null;
  use_variation_price_only: boolean | null;
  unit_type: string | null;
  category_id: string | null;
  categories?: { name?: string | null } | Array<{ name?: string | null }> | null;
  estimated_base_cost: number | null;
  cost_total: number | null;
  sku: string | null;
  barcode: string | null;
  show_in_catalog: boolean | null;
  active: boolean | null;
  product_kind?: string | null;
  pricing_reference_width_m: number | null;
  pricing_reference_height_m: number | 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;
  production_time_value: number | null;
  production_time_unit: string | null;
  product_processes?: Array<{ cost_total: number | null }> | null;
  product_materials?: Array<{ cost_total: number | null }> | null;
  product_price_tiers?: ProductPriceTier[] | null;
};

const COLUNAS_DO_PRODUTO =
  "*, categories(name), product_processes(cost_total), product_materials(cost_total), product_price_tiers(min_qty,max_qty,unit_price,sort_order,active)";

type Alteracao = {
  produtoId: string;
  nome: string;
  patch: Record<string, unknown>;
  mudancas: Mudanca[];
};

const sim = (valor: boolean) => (valor ? "sim" : "não");

/* Os campos que só dizem respeito ao controle de estoque: quem só mexe no estoque também pode. */
const SO_DO_ESTOQUE = new Set(["produto_id", "estoque"]);
const SO_DA_LOJA = new Set(["produto_id", "na_loja"]);

export const atualizarProduto: FerramentaDeEscrita<Alteracao> = {
  nome: "atualizar_produto",
  titulo: "Alterar um produto",
  descricao:
    "Altera um produto que já existe, pelo mesmo caminho do Salvar da tela do produto: nome, preço (ou sob consulta, preço só das opções, quantidades fixas), unidade de venda, categoria, descrição, custo, códigos, ativo, publicar ou tirar da loja virtual, prazo, quantidade mínima, faixas de preço, frete, dados fiscais, controle de estoque, vídeo e gabaritos. Mande só o que muda. Fotos, atributos, serviços, compre junto, campos personalizados, ficha técnica e saldo de estoque têm ferramenta própria (veja ver_produto_completo). Mudanças de preço e de loja voltam primeiro como prévia com código.",
  campos: {
    produto_id: { tipo: "id", descricao: "Id do produto, devolvido por buscar_produtos.", obrigatorio: true },
    nome: { tipo: "texto", descricao: "Nome novo do produto.", min: 2, max: 150 },
    vendido_por: { tipo: "opcao", descricao: "Unidade de venda (unidade, metro quadrado...). Mudar muda como o preço e os adicionais são contados.", opcoes: OPCOES_DE_VENDA },
    ...CAMPOS_COMUNS,
  },
  tipo: "escrita",
  sobrescreve: true,
  /* Quem só publica e tira da loja, ou só mexe no estoque, também usa: a
     permissão exata de cada campo é conferida lá dentro. */
  permissoes: ["products.catalog.write", "catalog.products.write", "products.stock.write"],
  async preparar(ctx, args) {
    const campos = Object.keys(args);
    const soALoja = campos.every((campo) => SO_DA_LOJA.has(campo));
    const soOEstoque = campos.every((campo) => SO_DO_ESTOQUE.has(campo));
    if (soOEstoque) exigir(ctx, "products.stock.write", "mexer no controle de estoque");
    else if (!soALoja) exigir(ctx, "products.catalog.write", "alterar produtos");
    exigirOsDoPreco(ctx, args);
    if (campos.length <= 1) recusar("Diga o que muda no produto: mande ao menos um campo além de produto_id.");

    const { data, error } = await ctx.supabase.from("products").select(COLUNAS_DO_PRODUTO).eq("id", String(args.produto_id)).in("user_id", ctx.tenantIds).maybeSingle();
    if (error) recusar("Não deu para ler o produto agora. Tente de novo em instantes.");
    if (!data) recusar("Produto não encontrado. Use buscar_produtos para achar o id certo.");
    const atual = data as unknown as ProdutoAtual;
    if (atual.product_kind === "digital") recusar("Este é um produto digital: use alterar_produto_digital.");

    const patch: Record<string, unknown> = {};
    const mudancas: Mudanca[] = [];
    const avisos: string[] = [];
    let unidade = atual.unit_type || "unit";
    const eraSobConsulta = atual.price_on_request === true;
    const eraSoOpcoes = atual.use_variation_price_only === true;
    const eraFixas = atual.allowed_sale_qty_enabled === true;
    const faixasAtuais = (atual.product_price_tiers ?? []).filter((faixa) => faixa.active !== false);

    /* Nome */
    if (args.nome !== undefined) {
      const nome = String(args.nome).replace(/\s+/g, " ").trim();
      patch.name = nome;
      mudancas.push({ campo: "nome", antes: atual.name, depois: nome });
    }

    /* Unidade de venda */
    if (args.vendido_por !== undefined) {
      const nova = VENDIDO_POR[args.vendido_por as keyof typeof VENDIDO_POR];
      if (nova !== unidade) {
        patch.unit_type = nova;
        mudancas.push({ campo: "vendido por", antes: NOME_DA_UNIDADE[unidade] ?? unidade, depois: String(args.vendido_por) });
        avisos.push("A unidade de venda muda a conta do preço e dos adicionais (por medida ou por peça) nos orçamentos e na loja.");
        unidade = nova;
      }
    }

    /* Preço: sob consulta, só das opções e quantidades fixas são três jeitos
       que não andam juntos (na tela, ligar um desliga ou trava os outros). */
    const fixasPedidas = args.quantidades_fixas as Array<{ quantidade: number; preco_total: number }> | undefined;
    const ligaFixas = Boolean(fixasPedidas && fixasPedidas.length);
    const desligaFixas = Boolean(fixasPedidas && !fixasPedidas.length);
    if ([args.sob_consulta === true, args.so_preco_das_opcoes === true, ligaFixas].filter(Boolean).length > 1) {
      recusar("Escolha um jeito de dar preço: sob consulta, preço só das opções ou quantidades fixas.");
    }
    if ((args.sob_consulta === true || args.so_preco_das_opcoes === true) && args.preco !== undefined) recusar("Escolha um dos dois: o preço, ou sob consulta / preço só das opções.");

    let fixas = eraFixas;
    if (ligaFixas) fixas = true;
    if (desligaFixas) fixas = false;
    let sobConsulta = eraSobConsulta;
    let soOpcoes = eraSoOpcoes;
    if (args.sob_consulta === true) {
      if (fixas) recusar("Este produto vende em quantidades fixas: desligue-as junto (quantidades_fixas: []) para deixar sob consulta.");
      sobConsulta = true;
      soOpcoes = false;
    }
    if (args.so_preco_das_opcoes === true) {
      if (fixas) recusar("Este produto vende em quantidades fixas: desligue-as junto (quantidades_fixas: []) para usar o preço só das opções.");
      soOpcoes = true;
      sobConsulta = false;
    }
    if (args.so_preco_das_opcoes === false) soOpcoes = false;
    if (ligaFixas) {
      sobConsulta = false;
      soOpcoes = false;
    }
    let preco = Number(atual.price ?? 0);
    if (sobConsulta || soOpcoes) preco = 0;
    if (args.preco !== undefined) {
      if (eraSoOpcoes && args.so_preco_das_opcoes !== false) {
        recusar("Este produto usa o preço só das opções. Para dar um preço base, mande junto so_preco_das_opcoes: false.");
      }
      if (!(Number(args.preco) > 0)) recusar("O preço precisa ser maior que zero. Para tirar o preço, marque sob_consulta.");
      preco = Number(args.preco);
      if (args.sob_consulta === undefined) sobConsulta = false;
      if (eraSobConsulta) avisos.push("O produto deixa de ser sob consulta e passa a mostrar este preço.");
    } else if (args.sob_consulta === false && eraSobConsulta && !soOpcoes && !fixas) {
      recusar("Para tirar o produto de sob consulta, informe também o preço.");
    }
    if (args.sob_consulta === false) sobConsulta = false;
    if (!sobConsulta && !soOpcoes && !fixas && !(preco > 0)) recusar("Informe o preço de venda (maior que zero): sem sob consulta, preço só das opções ou quantidades fixas, o produto precisa de preço.");

    const textoDoJeito = (estado: { sob: boolean; opcoes: boolean; fixas: boolean; preco: number }) =>
      estado.opcoes ? "só das opções" : estado.fixas ? "quantidades fixas" : textoDoPreco(estado.preco, unidade, estado.sob);
    const antesDoPreco = textoDoJeito({ sob: eraSobConsulta, opcoes: eraSoOpcoes, fixas: eraFixas, preco: Number(atual.price ?? 0) });
    const depoisDoPreco = textoDoJeito({ sob: sobConsulta, opcoes: soOpcoes, fixas, preco });
    if (sobConsulta !== eraSobConsulta || soOpcoes !== eraSoOpcoes || preco !== Number(atual.price ?? 0)) {
      patch.price = preco;
      patch.price_on_request = sobConsulta;
      patch.use_variation_price_only = soOpcoes;
      mudancas.push({ campo: "preço", antes: antesDoPreco, depois: depoisDoPreco === antesDoPreco ? `${depoisDoPreco} (${emReais(preco)})` : depoisDoPreco });
    }

    /* Valor mínimo do item (só produto por m² ou metro). */
    const minimoDoItem = valorMinimoPedido(args, unidade, atual);
    Object.assign(patch, minimoDoItem.patch);
    mudancas.push(...minimoDoItem.mudancas);

    /* Categoria: comparada pelo id. Duas categorias com o mesmo nome são
       categorias diferentes, e a prévia diz isso. */
    if (args.categoria_id !== undefined && String(args.categoria_id) !== atual.category_id) {
      const categoria = await categoriaDaEmpresa(ctx, args.categoria_id);
      const antes = um(atual.categories)?.name ?? null;
      const depois = categoria?.name ?? null;
      patch.category_id = categoria?.id ?? null;
      mudancas.push({ campo: "categoria", antes, depois: depois !== null && depois === antes ? `${depois} (outra categoria com o mesmo nome, id ${categoria?.id})` : depois });
    }

    /* Descrição: comparada pelo que de fato seria gravado, e não pelo trecho
       que aparece na prévia. */
    if (args.descricao !== undefined) {
      const descricao = sanitizeProductDescription(String(args.descricao));
      if (descricao !== (atual.description ?? "")) {
        patch.description = descricao;
        const antes = descricaoParaLer(atual.description, 600);
        const depois = descricaoParaLer(descricao, 600);
        mudancas.push({
          campo: "descrição",
          antes,
          depois: depois === antes ? `${depois ?? "vazia"} (o texto que aparece é o mesmo; muda o que vem depois deste trecho, ou a formatação)` : depois,
        });
      }
    }

    /* Custo: a rota refaz a conta com a ficha técnica e a unidade do produto. */
    if (args.custo !== undefined) {
      const custo = Number(args.custo);
      const somaDaFicha = [...(atual.product_processes ?? []), ...(atual.product_materials ?? [])].reduce((soma, linha) => soma + (Number(linha.cost_total) || 0), 0);
      const total = custoDoProduto({
        estimatedBaseCost: custo,
        somaDaFicha,
        unitType: unidade,
        referenceWidthM: atual.pricing_reference_width_m,
        referenceHeightM: atual.pricing_reference_height_m,
      });
      patch.estimated_base_cost = custo;
      mudancas.push({ campo: "custo", antes: emReais(Number(atual.cost_total ?? 0)), depois: emReais(total) });
      if (somaDaFicha > 0) avisos.push(`O custo total soma este valor à ficha técnica do produto: fica ${emReais(total)} por ${NOME_DA_UNIDADE[unidade] ?? "unidade"}.`);
    }

    /* Códigos */
    if (args.codigo !== undefined) {
      patch.sku = String(args.codigo);
      mudancas.push({ campo: "código", antes: atual.sku, depois: String(args.codigo) });
    }
    if (args.codigo_de_barras !== undefined) {
      patch.barcode = String(args.codigo_de_barras);
      mudancas.push({ campo: "código de barras", antes: atual.barcode, depois: String(args.codigo_de_barras) });
    }

    /* Loja */
    if (args.na_loja !== undefined) {
      const naLoja = args.na_loja === true;
      patch.show_in_catalog = naLoja;
      mudancas.push({ campo: "na loja virtual", antes: sim(atual.show_in_catalog !== false), depois: sim(naLoja) });
      if (naLoja && atual.active === false && args.ativo !== true) avisos.push("Este produto está inativo: mesmo publicado, ele não aparece na loja até ser ativado (campo ativo).");
    }

    /* Prazo */
    const prazo = prazoPedido(args, atual.production_time_unit);
    if (prazo) {
      patch.production_time_value = prazo.valor;
      patch.production_time_unit = prazo.unidade;
      mudancas.push({ campo: "prazo de produção", antes: textoDoPrazo(atual.production_time_value, atual.production_time_unit), depois: textoDoPrazo(prazo.valor, prazo.unidade) });
    }

    /* Quantidade mínima e quantidades fixas */
    const limites = {
      min_sale_qty_enabled: atual.min_sale_qty_enabled,
      min_sale_qty: atual.min_sale_qty,
      max_sale_qty_enabled: atual.max_sale_qty_enabled,
      max_sale_qty: atual.max_sale_qty,
      allowed_sale_qty_enabled: fixas,
      allowed_sale_quantities: atual.allowed_sale_quantities,
    };
    if (ligaFixas && atual.min_sale_qty_enabled) {
      /* Como a chave da tela: ligar as quantidades fixas desliga o mínimo. */
      limites.min_sale_qty_enabled = false;
      limites.min_sale_qty = null;
      patch.min_sale_qty_enabled = false;
      patch.min_sale_qty = null;
      mudancas.push({ campo: "quantidade mínima", antes: quantidade(Number(atual.min_sale_qty)), depois: "sem mínimo (as quantidades fixas mandam)" });
    }
    if (args.quantidade_minima !== undefined) {
      const minimo = Number(args.quantidade_minima);
      if (minimo > 0 && fixas) recusar("Este produto vende só em quantidades fixas, e a quantidade mínima não vale junto com elas. Desligue as quantidades fixas junto (quantidades_fixas: []).");
      const maximo = atual.max_sale_qty_enabled ? Number(atual.max_sale_qty) : null;
      if (minimo > 0 && maximo !== null && minimo > maximo) recusar(`A quantidade mínima não pode passar da máxima, que é ${quantidade(maximo)}.`);
      limites.min_sale_qty_enabled = minimo > 0;
      limites.min_sale_qty = minimo > 0 ? minimo : null;
      patch.min_sale_qty_enabled = limites.min_sale_qty_enabled;
      patch.min_sale_qty = limites.min_sale_qty;
      const antes = resolveSaleQuantityLimits(atual).minQty;
      mudancas.push({ campo: "quantidade mínima", antes: antes === null ? "sem mínimo" : quantidade(antes), depois: minimo > 0 ? quantidade(minimo) : "sem mínimo" });
    }

    let faixas: ProductPriceTier[] | null = null;
    if (fixasPedidas !== undefined) {
      if (args.faixas_de_preco !== undefined && ligaFixas) recusar("Com quantidades fixas, o preço de cada quantidade já vem na lista: não mande faixas_de_preco junto.");
      const antesDasFixas = eraFixas ? (atual.allowed_sale_quantities ?? []).map((valor) => quantidade(Number(valor))).join(", ") : "desligado";
      if (ligaFixas) {
        const lidas = lerQuantidadesFixas(fixasPedidas!);
        patch.allowed_sale_qty_enabled = true;
        patch.allowed_sale_quantities = lidas.allowedQuantities;
        faixas = lidas.tiers;
        mudancas.push({ campo: "quantidades fixas", antes: antesDasFixas, depois: textoDasFixas(fixasPedidas!) });
      } else if (eraFixas) {
        patch.allowed_sale_qty_enabled = false;
        patch.allowed_sale_quantities = null;
        /* As faixas exatas das quantidades fixas não viram faixas comuns: saem (ou entram as pedidas). */
        if (args.faixas_de_preco === undefined) faixas = [];
        mudancas.push({ campo: "quantidades fixas", antes: antesDasFixas, depois: "desligado" });
      }
    }
    if (args.faixas_de_preco !== undefined) {
      const pedidas = args.faixas_de_preco as FaixaDoAssistente[];
      if (fixas) recusar("Este produto vende só em quantidades fixas, cada uma com o próprio preço: mude a lista em quantidades_fixas.");
      if (pedidas.length && sobConsulta) recusar("Produto sob consulta não tem faixas de preço.");
      const lidas = lerFaixas(pedidas, limites);
      if (!lidas.ok) recusar(lidas.erro);
      faixas = lidas.faixas;
    } else if (faixas === null && sobConsulta && !eraSobConsulta && faixasAtuais.length) {
      /* Sob consulta não tem faixas: as que existem saem junto, como no painel. */
      faixas = [];
    } else if (faixas === null && args.quantidade_minima !== undefined && faixasAtuais.length) {
      /* A quantidade mínima nova tem de caber nas faixas que ficam. */
      const lidas = lerFaixas(
        faixasAtuais.map((faixa) => ({ a_partir_de: faixa.min_qty, ate: faixa.max_qty, preco_unitario: faixa.unit_price })),
        limites
      );
      if (!lidas.ok) recusar(`${lidas.erro} Ajuste as faixas junto (campo faixas_de_preco).`);
    }
    if (faixas) {
      patch.price_tiers = faixas.map((faixa, indice) => ({ ...faixa, sort_order: indice, active: true }));
      if (args.faixas_de_preco !== undefined || (!ligaFixas && faixasAtuais.length)) {
        mudancas.push({
          campo: "faixas de preço",
          antes: faixasAtuais.length && !eraFixas ? textoDasFaixas(faixasAtuais).join("; ") : "nenhuma",
          depois: faixas.length && !ligaFixas ? textoDasFaixas(faixas).join("; ") : "nenhuma",
        });
      }
    }

    if (patch.name !== undefined || patch.description !== undefined) {
      const contatos = contatosNovos(
        `${atual.name}\n${atual.description ?? ""}`,
        `${String(patch.name ?? atual.name)}\n${String(patch.description ?? atual.description ?? "")}`
      );
      if (contatos.length) recusar(recusaDeContatos(contatos, "na tela do produto"));
    }

    /* O resto da ficha (ativo, frete, fiscal, estoque, vídeo, gabaritos). */
    const ficha = await lerFichaSePrecisar(ctx, args, atual.id);
    const pedaco = await pedacoDaFicha(ctx, args, atual, ficha?.atributos ?? []);
    Object.assign(patch, pedaco.patch);
    mudancas.push(...pedaco.mudancas);
    avisos.push(...pedaco.avisos);
    if (pedaco.estoquePorPartes && ficha) {
      /* Estoque por opção ou combinação: a tela salva os atributos junto, e é
         isso que cria os saldos por opção e as combinações. */
      exigir(ctx, "products.catalog.write", "mudar o estoque para por opção ou por combinação");
      const escopo = escopoEfetivo({ ...atual, ...pedaco.patch });
      patch.attributes = ficha.atributos;
      patch.variations = variacoesParaSalvar(ficha.atributos, ficha.variacoes, escopo);
      patch.images = ficha.fotos;
    }

    const valendo = soOQueMuda(mudancas);
    if (!valendo.length) recusar("Nada muda: o produto já está com esses valores.");

    avisos.push(
      ...avisosDoPreco({
        preco,
        sobConsulta,
        faixas: faixas ?? faixasAtuais,
        quantidadesFixas: fixas,
        mudouOPreco: patch.price !== undefined,
      })
    );

    return {
      previa: {
        o_que_vai_acontecer: `O produto ${atual.name} será alterado.`,
        produto: atual.name,
        mudancas: valendo.map((mudanca) => ({ campo: mudanca.campo, antes: mudanca.antes ?? "vazio", depois: mudanca.depois ?? "vazio" })),
      },
      resumo: `Prévia da alteração do produto ${atual.name}`,
      avisos,
      /* Preço, publicação e ativo são o que o cliente vê e paga: passam pela
         aprovação da pessoa. O resto (descrição, códigos, prazo) grava direto. */
      sensivel: valendo.some((mudanca) => ["preço", "faixas de preço", "quantidades fixas", "na loja virtual", "ativo", "vendido por"].includes(mudanca.campo)),
      carga: { produtoId: atual.id, nome: String(patch.name ?? atual.name), patch, mudancas: valendo },
    };
  },
  async gravar(ctx, carga) {
    await gravarComoOPainel(ctx, carga.produtoId, carga.patch);
    return {
      dados: {
        alterado: true,
        produto_id: carga.produtoId,
        nome: carga.nome,
        mudancas: carga.mudancas.map((mudanca) => `${mudanca.campo}: ${mudanca.depois ?? "vazio"}`),
      },
      resumo: `Alterou o produto ${carga.nome} (${resumoDasMudancas(carga.mudancas)})`,
    };
  },
};

/** A ficha inteira só quando o pedido mexe no estoque (precisa dos atributos). */
async function lerFichaSePrecisar(ctx: ContextoDoAssistente, args: Record<string, unknown>, produtoId: string) {
  if (args.estoque === undefined) return null;
  return lerFicha(ctx, produtoId);
}
