import { randomUUID } from "node:crypto";

import { avisarLoja } from "@/lib/loja/avisar-loja";
import { updateProductCost } from "@/lib/production/update-product-cost";
import { apagarArquivosSemUso, excluirProdutosComoOPainel, vinculosDosProdutos } from "@/lib/produtos/servidor/excluir-produtos";

import type { ContextoDoAssistente } from "../contexto";
import { AVISO_DE_DADOS, contagem, dinheiro, exigir, falha, pode, recusar, texto, UNIDADE } from "./comum";
import { CAMPO_DE_ATRIBUTOS, CAMPO_DE_FOTOS } from "./produto-completo-campos";
import {
  bibliotecaDeServicos,
  cadastrosDeProducao,
  desfazerFotos,
  escopoEfetivo,
  fotoParaOProduto,
  gravarComoOPainel,
  guardarFotoDoProduto,
  lerFicha,
  usoDoProduto,
  type Ficha,
  type FotoPronta,
} from "./produto-completo-ficha";
import {
  adicionarFotos,
  aplicarPedidosNosAtributos,
  camposDepoisDe,
  cobrancaDaOpcao,
  custoDoProcesso,
  definirPrincipal,
  limparFotosDasOpcoes,
  linhaDeInsumo,
  NOME_DO_ESTILO,
  NOME_DO_TIPO_DE_CAMPO,
  removerFotos,
  reordenarFotos,
  servicoLigado,
  variacoesParaSalvar,
  vinculosDepoisDe,
  type AtributoDaFicha,
  type FotoDaFicha,
  type PedidoDeAtributo,
  type PedidoDeCampo,
} from "./produto-completo-regras";
import { emReais, quantidade, textoDasFaixas, textoDoPrazo, textoDoPreco } from "./regras-do-produto";
import type { Ferramenta, FerramentaDeEscrita, FerramentaDeLeitura } from "./tipos";
import { disponibilidadeNoBanco, empresaNoEstoqueV2, resumoDoEstoque } from "./estoque-do-canal";
import { escolhaDoProduto, lerCombinacao } from "./estoque-v2-regras";

/*
 * A ficha inteira do produto pelo assistente (pedido do Natan, 15/10/2026):
 * ver tudo, fotos, variações, serviços, compre junto, campos personalizados,
 * ficha técnica, saldo de estoque, duplicar e excluir. Tudo grava pelo mesmo
 * caminho da tela (o Salvar do produto, a exclusão da lista, o ajuste do
 * histórico de estoque, a aba Composição), com a sessão da pessoa.
 */

const ID_DO_PRODUTO = { tipo: "id", descricao: "Id do produto, devolvido por buscar_produtos.", obrigatorio: true } as const;

function avisarAMudanca(ctx: ContextoDoAssistente) {
  avisarLoja(ctx.donoDosDados, new Request(`${ctx.origemDoAdmin}/api/mcp`));
}

/** A foto pelo número na galeria (1 = primeira) ou pelo id. */
function acharFoto(fotos: FotoDaFicha[], referencia: string): FotoDaFicha {
  const limpa = referencia.trim();
  if (/^\d+$/.test(limpa)) {
    const foto = fotos[Number(limpa) - 1];
    if (!foto) recusar(`O produto não tem a foto número ${limpa} (tem ${fotos.length}).`);
    return foto;
  }
  const foto = fotos.find((item) => item.id === limpa.toLowerCase());
  if (!foto) recusar(`A foto ${limpa} não está na galeria deste produto. Use o número ou o id de ver_produto_completo.`);
  return foto;
}

const linhaDaFoto = (foto: FotoDaFicha, indice: number) => ({ numero: indice + 1, id: foto.id, endereco: foto.image_url, principal: foto.is_primary });

/* ============================================================ ver tudo */

function textoDoVideo(produto: Record<string, unknown>) {
  if (produto.video_file_id) return { tipo: "vídeo enviado", arquivo_id: produto.video_file_id, flutuante: produto.video_flutuante === true, como_toca: produto.video_opcoes ?? null };
  const link = produto.video_link as { url?: string; provedor?: string } | null;
  if (link?.url) return { tipo: `link (${link.provedor ?? "vídeo"})`, link: link.url, flutuante: produto.video_flutuante === true, como_toca: produto.video_opcoes ?? null };
  return null;
}

export const verProdutoCompleto: FerramentaDeLeitura = {
  nome: "ver_produto_completo",
  titulo: "Ver a ficha completa de um produto",
  descricao:
    "Mostra TUDO o que a tela do produto tem: dados gerais, preço (e como ele se forma), faixas e quantidades fixas, fotos da galeria (com número e id), vídeo, atributos e opções (com id, adicional, cor, foto, estoque), serviços opcionais, compre junto, campos personalizados, frete, prazo e gabaritos, dados fiscais, controle e saldo de estoque, ficha técnica e custo (para quem vê a precificação) e em quantos orçamentos e pedidos o produto aparece. Use antes de alterar um produto pronto.",
  campos: {
    produto_id: ID_DO_PRODUTO,
    com_cadastros_de_producao: {
      tipo: "booleano",
      descricao: "Junto, a lista de insumos, máquinas e mãos de obra da empresa (com id), para a ficha técnica e a receita das opções.",
    },
  },
  tipo: "leitura",
  permissoes: ["products.catalog.read"],
  async executar(ctx, args) {
    exigir(ctx, "products.catalog.read", "ver os produtos");
    const produtoId = String(args.produto_id);
    const ficha = await lerFicha(ctx, produtoId);
    const p = ficha.produto;
    const unidade = String(p.unit_type || "unit");
    const veCusto = pode(ctx, "pricing.read") || pode(ctx, "pricing.config.read");
    /* Estoque v2: o bloco de estoque sai do motor (de onde sai, o que sai ao
       vender 1, quanto dá para vender); os campos antigos são só espelho. */
    const estoqueNovo = (await empresaNoEstoqueV2(ctx))
      ? resumoDoEstoque(await disponibilidadeNoBanco(ctx, { produtoId, selected: null, quantidade: 1 }))
      : null;

    const [biblioteca, combos, uso, materiais] = await Promise.all([
      bibliotecaDeServicos(ctx),
      ctx.supabase.from("product_combos").select("id,combo_product_id,title,discount_type,discount_value,active,combo_product:products!combo_product_id(id,name)").eq("main_product_id", produtoId).order("created_at"),
      vinculosDosProdutos(ctx.supabase, [produtoId]),
      veCusto || pode(ctx, "production.read") ? cadastrosDeProducao(ctx) : Promise.resolve(null),
    ]);
    const nomeDoInsumo = new Map((materiais?.insumos ?? []).map((insumo) => [insumo.id, insumo.name]));
    const nomeDaMaquina = new Map((materiais?.maquinas ?? []).map((maquina) => [maquina.id, maquina.name]));
    const nomeDaFuncao = new Map((materiais?.funcoes ?? []).map((funcao) => [funcao.id, funcao.role_name]));
    const fotoDoId = new Map(ficha.fotos.map((foto, indice) => [foto.id, indice + 1]));
    const grupos = new Map(biblioteca.grupos.map((grupo) => [grupo.id, grupo.nome]));
    const escopo = escopoEfetivo(p);
    const fixas = p.allowed_sale_qty_enabled === true;

    const atributos = ficha.atributos.map((atributo) => ({
      nome: atributo.name,
      multipla_escolha: Boolean(atributo.allow_multiple),
      obrigatorio: Boolean(atributo.is_required),
      estilo: NOME_DO_ESTILO[atributo.display_style ?? ""] ?? "automático",
      mostrar_na_loja: atributo.show_in_store !== false,
      opcoes: atributo.options.map((opcao) => ({
        id: opcao.id ?? null,
        rotulo: opcao.label,
        preco_adicional: dinheiro(opcao.price),
        cobrar_por: cobrancaDaOpcao(opcao, unidade),
        cor: opcao.swatch ?? "automática",
        obrigatoria: Boolean(opcao.is_required),
        foto_numero: opcao.image_id ? fotoDoId.get(opcao.image_id) ?? null : null,
        ...(opcao.material_id ? { insumo: { id: opcao.material_id, nome: nomeDoInsumo.get(opcao.material_id) ?? null, quantidade_por_peca: opcao.material_qty ?? 0 } } : {}),
        ...(escopo === "option" && String(p.inventory_attribute_name ?? "").toLowerCase() === atributo.name.toLowerCase()
          ? { estoque: opcao.stock ?? 0, estoque_minimo: opcao.minimum_stock ?? 0, ponto_de_reposicao: opcao.reorder_point ?? 0 }
          : {}),
      })),
    }));

    const composicao = veCusto
      ? {
          custo_base: dinheiro(p.estimated_base_cost),
          custo_total: dinheiro(p.cost_total),
          insumos: ((p.product_materials ?? []) as Array<Record<string, unknown>>).map((linha) => ({
            linha_id: linha.id,
            insumo: nomeDoInsumo.get(String(linha.material_id)) ?? linha.material_id,
            quantidade: Number(linha.consumption_qty ?? 0),
            perda_pct: Number(linha.waste_pct ?? 0),
            ...(linha.pieces_per_unit ? { pecas_por_unidade: Number(linha.pieces_per_unit) } : {}),
            custo: dinheiro(linha.cost_total),
          })),
          processos: ((p.product_processes ?? []) as Array<Record<string, unknown>>).map((linha) => ({
            linha_id: linha.id,
            tipo: linha.kind === "MACHINE" ? "máquina" : "mão de obra",
            nome: (linha.machine_id ? nomeDaMaquina.get(String(linha.machine_id)) : nomeDaFuncao.get(String(linha.role_id))) ?? linha.process_name ?? null,
            minutos: Number(linha.minutes ?? 0),
            preparo_minutos: Number(linha.setup_minutes ?? 0),
            custo: dinheiro(linha.cost_total),
          })),
        }
      : null;

    const categoria = p.categories as { id?: string; name?: string } | null;
    return {
      dados: {
        _observacao: AVISO_DE_DADOS,
        produto: {
          id: p.id,
          nome: p.name,
          descricao: texto(p.description, 3000),
          categoria: categoria?.name ? { id: categoria.id, nome: categoria.name } : null,
          ativo: p.active !== false,
          na_loja: p.show_in_catalog !== false,
          exclusivo_do_catalogo: p.is_catalog_exclusive === true,
          vendido_por: UNIDADE[unidade] ?? unidade,
          preco: p.use_variation_price_only
            ? "só das opções (o preço base é ignorado)"
            : fixas
              ? "quantidades fixas (veja quantidades_fixas)"
              : textoDoPreco(Number(p.price ?? 0), unidade, p.price_on_request === true),
          sob_consulta: p.price_on_request === true,
          so_preco_das_opcoes: p.use_variation_price_only === true,
          valor_minimo_do_item:
            Number(p.valor_minimo_item) > 0
              ? {
                  valor: dinheiro(p.valor_minimo_item),
                  vale_nos_orcamentos: p.valor_minimo_orcamentos !== false,
                  vale_na_loja: p.valor_minimo_loja !== false,
                  vale_no_pdv: p.valor_minimo_pdv !== false,
                  como_funciona:
                    "Cada linha (todas as peças daquela linha juntas) é comparada sozinha com o mínimo; se a conta der menos, a linha custa o mínimo. Só vale em produto por m² ou por metro.",
                }
              : null,
          codigo: p.sku ?? null,
          codigo_de_barras: p.barcode ?? null,
          faixas_de_preco: !fixas && ficha.faixas.length ? textoDasFaixas(ficha.faixas) : [],
          quantidades_fixas: fixas
            ? ((p.allowed_sale_quantities ?? []) as number[]).map((qtd) => {
                const faixa = ficha.faixas.find((item) => Number(item.min_qty) === Number(qtd) && Number(item.max_qty) === Number(qtd));
                return { quantidade: Number(qtd), preco_total: dinheiro((faixa ? faixa.unit_price : Number(p.price ?? 0)) * Number(qtd)) };
              })
            : null,
          quantidade_minima: p.min_sale_qty_enabled ? Number(p.min_sale_qty) : null,
          fotos: ficha.fotos.map(linhaDaFoto),
          video: textoDoVideo(p),
          atributos,
          estoque: estoqueNovo
            ? {
                estoque_novo: true,
                ...estoqueNovo,
                como_mudar:
                  "De onde sai: configurar_estoque_do_produto. Composição: alterar_quando_sai_da_composicao. Saldo por combinação: ver_estoque_do_produto com a combinação; lançar: lancar_entrada_no_estoque, contar_estoque, registrar_producao.",
              }
            : {
            controlar: p.track_inventory === true,
            por: escopo === "option" ? `opção (${String(p.inventory_attribute_name ?? "")})` : escopo === "variant" ? "combinação" : "produto",
            vender_sem_estoque: p.allow_negative_stock === true,
            saldo_geral: Number(p.stock ?? 0),
            ...(escopo === "variant"
              ? { combinacoes: ficha.variacoes.map((variacao) => ({ id: variacao.id, combinacao: variacao.combination, saldo: variacao.stock, codigo: variacao.sku || null })) }
              : {}),
          },
          servicos_opcionais: biblioteca.servicos
            .filter((servico) => servico.ativo)
            .map((servico) => ({
              id: servico.id,
              titulo: servico.titulo,
              grupo: servico.grupoId ? grupos.get(servico.grupoId) ?? null : null,
              preco: dinheiro(servico.preco),
              cobrado: servico.porPeca ? "por peça" : "uma vez no pedido",
              oferecido_neste_produto: servicoLigado(servico, ficha.vinculos),
              padrao_da_biblioteca: servico.valeParaTodos ? "oferecido em todos" : "só onde for ligado",
            })),
          compre_junto: ((combos.data ?? []) as Array<Record<string, unknown>>).map((combo) => ({
            id: combo.id,
            produto: combo.combo_product ?? { id: combo.combo_product_id },
            titulo: combo.title,
            desconto: combo.discount_type === "percentage" ? `${Number(combo.discount_value)}%` : combo.discount_type === "fixed" ? emReais(Number(combo.discount_value)) : "sem desconto",
            ativo: combo.active !== false,
          })),
          campos_personalizados: ficha.campos.map((campo) => ({
            id: campo.id,
            titulo: campo.title,
            tipo: NOME_DO_TIPO_DE_CAMPO[campo.field_type] ?? campo.field_type,
            exemplo: campo.placeholder,
            obrigatorio: campo.required,
            so_na_combinacao: campo.variation_signature,
            ativo: campo.active,
          })),
          frete: {
            peso_kg: p.shipping_weight_kg ?? null,
            largura_cm: p.shipping_width_cm ?? null,
            altura_cm: p.shipping_height_cm ?? null,
            comprimento_cm: p.shipping_length_cm ?? null,
          },
          producao: {
            prazo: textoDoPrazo(p.production_time_value as number | null, p.production_time_unit as string | null),
            gabaritos: ((p.production_templates ?? []) as Array<{ format?: string; url?: string; name?: string }>).map((item) => ({ formato: item.format, nome: item.name, link: item.url })),
          },
          fiscal: { tipo: p.fiscal_tipo ?? null, ncm: p.ncm ?? null, cest: p.cest ?? null, origem: p.fiscal_origem ?? null, tributacao_id: p.fiscal_perfil_id ?? null },
          ...(composicao ? { ficha_tecnica_e_custo: composicao } : {}),
          aparece_em: { orcamentos: uso.orcamentos.get(produtoId) ?? 0, pedidos: uso.pedidos.get(produtoId) ?? 0 },
          no_painel: `${ctx.origemDoAdmin.replace(/\/$/, "")}/dashboard/products/${produtoId}`,
        },
        ...(args.com_cadastros_de_producao === true && materiais
          ? {
              cadastros_de_producao: {
                insumos: materiais.insumos.slice(0, 300).map((insumo) => ({ id: insumo.id, nome: insumo.name, unidade: insumo.unit, ...(veCusto ? { custo_por_unidade: dinheiro(insumo.cost_per_unit) } : {}) })),
                maquinas: materiais.maquinas.slice(0, 100).map((maquina) => ({ id: maquina.id, nome: maquina.name })),
                maos_de_obra: materiais.funcoes.slice(0, 100).map((funcao) => ({ id: funcao.id, nome: funcao.role_name })),
              },
            }
          : {}),
        para_mudar:
          "atualizar_produto (dados, preço, loja, frete, fiscal, estoque, vídeo, gabaritos), adicionar_fotos_ao_produto, organizar_fotos_do_produto, remover_fotos_do_produto, alterar_variacoes_do_produto, alterar_servicos_do_produto, alterar_compre_junto, alterar_campos_personalizados_do_produto, alterar_ficha_tecnica_do_produto, ajustar_estoque_do_produto, duplicar_produto, excluir_produto.",
      },
      resumo: `Consultou a ficha completa do produto ${p.name}`,
    };
  },
};

/* ================================================================ fotos */

type FotoPedida = { imagem: string; principal?: boolean; atributo?: string; opcao?: string };
type FotosNovas = { produtoId: string; nome: string; fotos: Array<FotoPronta & { id: string; principal: boolean }>; ficha: Ficha; vinculos: PedidoDeAtributo[] };

export const adicionarFotosAoProduto: FerramentaDeEscrita<FotosNovas> = {
  nome: "adicionar_fotos_ao_produto",
  titulo: "Adicionar fotos a um produto",
  descricao:
    'Põe fotos num produto que já existe (como "Enviar imagens" da galeria da tela): imagem anexada pela pessoa (chame pedir_imagem antes e use o código envio:...), link direto de uma imagem da internet (pode ser uma que você achou pesquisando), imagem criada com gerar_imagem_do_produto, ou "produto:<id>". As fotos entram no fim da galeria, preparadas como no painel (até 1600 px, WebP). Dá para escolher a principal e ligar cada foto a uma opção (ex.: Cor Azul).',
  campos: { produto_id: ID_DO_PRODUTO, fotos: { ...CAMPO_DE_FOTOS, obrigatorio: true, minItens: 1 } as typeof CAMPO_DE_FOTOS },
  tipo: "escrita",
  permissoes: ["products.catalog.write"],
  abertoAoMundo: true,
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "alterar produtos");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const pedidas = args.fotos as FotoPedida[];
    if (pedidas.filter((foto) => foto.principal).length > 1) recusar("Só uma foto pode ser a principal.");
    const fotos: FotosNovas["fotos"] = [];
    for (const pedida of pedidas) {
      const foto = await fotoParaOProduto(ctx, pedida.imagem);
      fotos.push({ ...foto, id: randomUUID(), principal: pedida.principal === true });
    }
    const vinculos: PedidoDeAtributo[] = [];
    pedidas.forEach((pedida, indice) => {
      if (!pedida.atributo && !pedida.opcao) return;
      if (!pedida.atributo || !pedida.opcao) recusar("Para ligar a foto a uma opção, mande atributo e opcao juntos (ex.: Cor e Azul).");
      const atributo = ficha.atributos.find((item) => item.name.trim().toLowerCase() === pedida.atributo!.trim().toLowerCase());
      if (!atributo?.options.some((opcao) => String(opcao.label).trim().toLowerCase() === pedida.opcao!.trim().toLowerCase())) {
        recusar(`O produto não tem a opção "${pedida.opcao}" em "${pedida.atributo}". Crie antes com alterar_variacoes_do_produto.`);
      }
      vinculos.push({ nome: pedida.atributo, opcoes: [{ rotulo: pedida.opcao, foto_id: fotos[indice].id }] });
    });
    return {
      previa: {
        o_que_vai_acontecer: `${contagem(fotos.length, "foto entra", "fotos entram")} na galeria do produto ${ficha.produto.name}.`,
        fotos: fotos.map((foto, indice) => `${ficha.fotos.length + indice + 1}. ${foto.origem} (${foto.largura} × ${foto.altura}${foto.encolhida ? ", reduzida para a loja" : ""})${foto.principal ? " — principal" : ""}`),
        galeria_hoje: contagem(ficha.fotos.length, "foto", "fotos"),
      },
      resumo: `Prévia: fotos no produto ${ficha.produto.name}`,
      avisos: fotos.some((foto) => Math.min(foto.largura, foto.altura) < 400) ? ["Alguma foto é pequena (menos de 400 px) e pode ficar borrada na loja."] : [],
      carga: { produtoId: ficha.produto.id, nome: ficha.produto.name, fotos, ficha, vinculos },
    };
  },
  async gravar(ctx, carga) {
    const subidas: string[] = [];
    try {
      const novas: Array<{ id: string; image_url: string; principal?: boolean }> = [];
      for (const foto of carga.fotos) {
        const guardada = await guardarFotoDoProduto(ctx, foto);
        subidas.push(guardada.caminho);
        novas.push({ id: foto.id, image_url: guardada.url, principal: foto.principal });
      }
      const galeria = adicionarFotos(carga.ficha.fotos, novas);
      const patch: Record<string, unknown> = { images: galeria, image_url: galeria.find((foto) => foto.is_primary)?.image_url ?? null };
      if (carga.vinculos.length) {
        const ligados = aplicarPedidosNosAtributos(carga.ficha.atributos, carga.vinculos, { unitType: String(carga.ficha.produto.unit_type), fotos: galeria, novoId: randomUUID });
        if (!ligados.ok) recusar(ligados.erro);
        patch.attributes = ligados.atributos;
        patch.variations = variacoesParaSalvar(ligados.atributos, carga.ficha.variacoes, escopoEfetivo(carga.ficha.produto));
      }
      await gravarComoOPainel(ctx, carga.produtoId, patch, "pôr as fotos no produto");
      return {
        dados: { feito: true, produto_id: carga.produtoId, fotos: galeria.map(linhaDaFoto) },
        resumo: `Pôs ${contagem(carga.fotos.length, "foto", "fotos")} no produto ${carga.nome}`,
      };
    } catch (erro) {
      await desfazerFotos(ctx, subidas);
      throw erro;
    }
  },
};

type Galeria = { produtoId: string; nome: string; patch: Record<string, unknown>; fotos: FotoDaFicha[]; saem: FotoDaFicha[] };

export const organizarFotosDoProduto: FerramentaDeEscrita<Galeria> = {
  nome: "organizar_fotos_do_produto",
  titulo: "Organizar as fotos de um produto",
  descricao: "Muda a ordem das fotos da galeria e/ou a foto principal (a capa), como arrastar e marcar a estrela na tela. Use os números ou ids de ver_produto_completo.",
  campos: {
    produto_id: ID_DO_PRODUTO,
    ordem: { tipo: "textos", descricao: 'A nova ordem, pelos números de hoje ou ids (ex.: ["3","1","2"]). As que faltarem vão para o fim.', maxItens: 200, max: 40 },
    principal: { tipo: "texto", descricao: "Número (de hoje) ou id da foto que vira a principal.", max: 40 },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "alterar produtos");
    if (args.ordem === undefined && args.principal === undefined) recusar("Diga a nova ordem (ordem) e/ou a foto principal (principal).");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    if (!ficha.fotos.length) recusar("Este produto ainda não tem fotos. Use adicionar_fotos_ao_produto.");
    let fotos = ficha.fotos;
    const principal = args.principal !== undefined ? acharFoto(ficha.fotos, String(args.principal)) : null;
    if (args.ordem !== undefined) fotos = reordenarFotos(fotos, (args.ordem as string[]).map((ref) => acharFoto(ficha.fotos, ref).id));
    if (principal) fotos = definirPrincipal(fotos, principal.id);
    return {
      previa: {
        o_que_vai_acontecer: `A galeria do produto ${ficha.produto.name} fica assim.`,
        nova_ordem: fotos.map((foto, indice) => `${indice + 1}. foto ${ficha.fotos.findIndex((antiga) => antiga.id === foto.id) + 1} de antes${foto.is_primary ? " — principal" : ""}`),
      },
      resumo: `Prévia: organizar as fotos do produto ${ficha.produto.name}`,
      avisos: [],
      carga: { produtoId: ficha.produto.id, nome: ficha.produto.name, patch: { images: fotos, image_url: fotos.find((foto) => foto.is_primary)?.image_url ?? null }, fotos, saem: [] },
    };
  },
  async gravar(ctx, carga) {
    await gravarComoOPainel(ctx, carga.produtoId, carga.patch, "organizar as fotos");
    return { dados: { feito: true, produto_id: carga.produtoId, fotos: carga.fotos.map(linhaDaFoto) }, resumo: `Organizou as fotos do produto ${carga.nome}` };
  },
};

export const removerFotosDoProduto: FerramentaDeEscrita<Galeria> = {
  nome: "remover_fotos_do_produto",
  titulo: "Remover fotos de um produto",
  descricao: "Tira fotos da galeria de um produto (pelos números ou ids de ver_produto_completo). O arquivo é apagado quando nenhum outro produto usa a mesma foto. Sempre com prévia.",
  campos: {
    produto_id: ID_DO_PRODUTO,
    fotos: { tipo: "textos", descricao: 'Números ou ids das fotos que saem (ex.: ["2"]).', obrigatorio: true, minItens: 1, maxItens: 200, max: 40 },
  },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: true,
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "alterar produtos");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const saem = (args.fotos as string[]).map((ref) => acharFoto(ficha.fotos, ref));
    const fotos = removerFotos(ficha.fotos, saem.map((foto) => foto.id));
    const patch: Record<string, unknown> = { images: fotos, image_url: fotos.find((foto) => foto.is_primary)?.image_url ?? null };
    /* Opção que mostrava uma foto que sai perde o vínculo, como a tela faz. */
    const limpos = limparFotosDasOpcoes(ficha.atributos, fotos);
    if (limpos.mudou) {
      patch.attributes = limpos.atributos;
      patch.variations = variacoesParaSalvar(limpos.atributos, ficha.variacoes, escopoEfetivo(ficha.produto));
    }
    return {
      previa: {
        o_que_vai_acontecer: `${contagem(saem.length, "foto sai", "fotos saem")} do produto ${ficha.produto.name}.`,
        saem: saem.map((foto) => `foto ${ficha.fotos.indexOf(foto) + 1}${foto.is_primary ? " (a principal)" : ""}`),
        ficam: contagem(fotos.length, "foto", "fotos"),
      },
      resumo: `Prévia: tirar fotos do produto ${ficha.produto.name}`,
      avisos: [
        ...(saem.some((foto) => foto.is_primary) && fotos.length ? ["A foto principal sai: a primeira que fica vira a principal."] : []),
        ...(limpos.mudou ? ["Alguma opção mostrava uma destas fotos e fica sem foto."] : []),
      ],
      carga: { produtoId: ficha.produto.id, nome: ficha.produto.name, patch, fotos, saem },
    };
  },
  async gravar(ctx, carga) {
    await gravarComoOPainel(ctx, carga.produtoId, carga.patch, "tirar as fotos");
    const apagados = await apagarArquivosSemUso(
      ctx.supabase,
      carga.saem.map((foto) => ({ url: null, path: null, image_url: foto.image_url })),
      []
    ).catch(() => 0);
    return {
      dados: { feito: true, produto_id: carga.produtoId, fotos: carga.fotos.map(linhaDaFoto), arquivos_apagados: apagados },
      resumo: `Tirou ${contagem(carga.saem.length, "foto", "fotos")} do produto ${carga.nome}`,
    };
  },
};

/* ============================================================ variações */

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

export const alterarVariacoesDoProduto: FerramentaDeEscrita<Variacoes> = {
  nome: "alterar_variacoes_do_produto",
  titulo: "Alterar atributos e opções de um produto",
  descricao:
    "Cria, muda e tira atributos (Cor, Tamanho, Papel...) e as opções de cada um, como a aba Variações da tela: adicional de preço, como é cobrado, cor da bolinha, foto da opção, escolha obrigatória ou múltipla, estilo na loja, insumo que a opção consome, estoque mínimo. Só o que você citar muda; os ids das opções não mudam (orçamentos e pedidos antigos continuam certos). Tirar atributo ou opção volta como prévia com código.",
  campos: { produto_id: ID_DO_PRODUTO, atributos: { ...CAMPO_DE_ATRIBUTOS, obrigatorio: true, minItens: 1 } as typeof CAMPO_DE_ATRIBUTOS },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: (args) =>
    Array.isArray(args.atributos) &&
    (args.atributos as Array<Record<string, unknown>>).some((atributo) => atributo.remover === true || (Array.isArray(atributo.opcoes) && (atributo.opcoes as Array<Record<string, unknown>>).some((opcao) => opcao.remover === true))),
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "alterar produtos");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const pedidos = args.atributos as PedidoDeAtributo[];
    const precisaDosInsumos = pedidos.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 resultado = aplicarPedidosNosAtributos(ficha.atributos, pedidos, { unitType: String(ficha.produto.unit_type || "unit"), fotos: ficha.fotos, novoId: randomUUID, insumos });
    if (!resultado.ok) recusar(resultado.erro);
    if (!resultado.mudancas.length) recusar("Nada muda: os atributos já estão assim.");

    const escopo = escopoEfetivo(ficha.produto);
    if (escopo === "variant") {
      const multiplos = resultado.atributos.filter((atributo) => atributo.allow_multiple).map((atributo) => atributo.name);
      if (multiplos.length) recusar(`Com estoque por combinação, cada atributo aceita uma escolha só. Desligue a múltipla escolha em: ${multiplos.join(", ")}.`);
    }
    const avisos: string[] = [];
    if (escopo === "option") {
      const controla = String(ficha.produto.inventory_attribute_name ?? "").toLowerCase();
      if (controla && !resultado.atributos.some((atributo) => atributo.name.toLowerCase() === controla)) {
        avisos.push(`O estoque é controlado pelo atributo "${String(ficha.produto.inventory_attribute_name)}", que deixa de existir: ajuste o estoque (atualizar_produto, campo estoque).`);
      }
    }
    if (resultado.saem.length) {
      const uso = await usoDoProduto(ctx, ficha.produto.id);
      for (const sai of resultado.saem) {
        const vezes = uso.usaAOpcao(sai.id, sai.opcao);
        if (vezes) avisos.push(`A opção ${sai.opcao} (${sai.atributo}) aparece em ${contagem(vezes, "item de orçamento ou pedido", "itens de orçamentos e pedidos")}: eles continuam como estão, mas ninguém mais escolhe essa opção.`);
      }
    }
    if (escopo === "variant") avisos.push("O estoque é por combinação: as combinações novas começam com saldo zero.");

    return {
      previa: { o_que_vai_acontecer: `Os atributos do produto ${ficha.produto.name} mudam.`, mudancas: resultado.mudancas },
      resumo: `Prévia: atributos do produto ${ficha.produto.name}`,
      avisos,
      carga: {
        produtoId: ficha.produto.id,
        nome: ficha.produto.name,
        mudancas: resultado.mudancas,
        /* Como o Salvar da tela: atributos, combinações e a galeria juntos. */
        patch: { attributes: resultado.atributos, variations: variacoesParaSalvar(resultado.atributos, ficha.variacoes, escopo), images: ficha.fotos },
      },
    };
  },
  async gravar(ctx, carga) {
    await gravarComoOPainel(ctx, carga.produtoId, carga.patch, "alterar os atributos");
    const atributos = carga.patch.attributes as AtributoDaFicha[];
    return {
      dados: {
        feito: true,
        produto_id: carga.produtoId,
        mudancas: carga.mudancas,
        atributos: atributos.map((atributo) => ({ nome: atributo.name, opcoes: atributo.options.map((opcao) => opcao.label) })),
      },
      resumo: `Alterou os atributos do produto ${carga.nome} (${carga.mudancas.slice(0, 3).join("; ")}${carga.mudancas.length > 3 ? "…" : ""})`,
    };
  },
};

/* ============================================================== serviços */

type Servicos = { produtoId: string; nome: string; links: Array<{ service_id: string; enabled: boolean }>; mudancas: string[] };

export const alterarServicosDoProduto: FerramentaDeEscrita<Servicos> = {
  nome: "alterar_servicos_do_produto",
  titulo: "Ligar ou desligar serviços opcionais de um produto",
  descricao:
    'Escolhe quais serviços opcionais da biblioteca da empresa (criação de arte, revisão de arquivo, montagem...) este produto oferece. O produto só liga e desliga: preço e texto do serviço são da biblioteca. "padrão" volta a seguir a biblioteca (oferecido em todos ou só onde for ligado). Os ids estão em ver_produto_completo.',
  campos: {
    produto_id: ID_DO_PRODUTO,
    servicos: {
      tipo: "lista",
      descricao: "Os serviços que mudam.",
      obrigatorio: true,
      minItens: 1,
      maxItens: 100,
      itens: {
        servico_id: { tipo: "id", descricao: "Id do serviço (ver_produto_completo, servicos_opcionais).", obrigatorio: true },
        escolha: { tipo: "opcao", descricao: "oferecer, não oferecer, ou padrão (segue a biblioteca).", opcoes: ["oferecer", "não oferecer", "padrão"], obrigatorio: true },
      },
    },
  },
  tipo: "escrita",
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "alterar produtos");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const biblioteca = await bibliotecaDeServicos(ctx);
    let vinculos = ficha.vinculos;
    const mudancas: string[] = [];
    for (const pedido of args.servicos as Array<{ servico_id: string; escolha: "oferecer" | "não oferecer" | "padrão" }>) {
      const servico = biblioteca.servicos.find((item) => item.id === pedido.servico_id);
      if (!servico) recusar(`O serviço ${pedido.servico_id} não está na biblioteca da empresa.`);
      const antes = servicoLigado(servico, vinculos);
      vinculos = vinculosDepoisDe(vinculos, servico, pedido.escolha);
      const depois = servicoLigado(servico, vinculos);
      mudancas.push(`${servico.titulo}: ${antes ? "oferecido" : "não oferecido"} → ${depois ? "oferecido" : "não oferecido"}${pedido.escolha === "padrão" ? " (segue a biblioteca)" : ""}`);
    }
    return {
      previa: { o_que_vai_acontecer: `Os serviços oferecidos no produto ${ficha.produto.name} mudam.`, mudancas },
      resumo: `Prévia: serviços do produto ${ficha.produto.name}`,
      avisos: [],
      carga: { produtoId: ficha.produto.id, nome: ficha.produto.name, links: vinculos.map((vinculo) => ({ service_id: vinculo.serviceId, enabled: vinculo.ligado })), mudancas },
    };
  },
  async gravar(ctx, carga) {
    /* A rota só aceita os serviços junto de algum campo da ficha (a tela manda
       a ficha inteira em todo Salvar): vai o nome, igual ao que está gravado. */
    await gravarComoOPainel(ctx, carga.produtoId, { name: carga.nome, optional_service_links: carga.links }, "mudar os serviços do produto");
    return { dados: { feito: true, produto_id: carga.produtoId, mudancas: carga.mudancas }, resumo: `Mudou os serviços opcionais do produto ${carga.nome}` };
  },
};

/* ========================================================= compre junto */

const DESCONTO = { porcentagem: "percentage", "valor fixo": "fixed", nenhum: "none" } as const;

type Combos = {
  produtoId: string;
  nome: string;
  novos: Array<Record<string, unknown>>;
  mudados: Array<{ id: string; campos: Record<string, unknown> }>;
  saem: string[];
  mudancas: string[];
};

export const alterarCompreJunto: FerramentaDeEscrita<Combos> = {
  nome: "alterar_compre_junto",
  titulo: "Alterar o compre junto de um produto",
  descricao:
    'O "Compre Junto" da página do produto na loja: outro produto sugerido, com um título e um desconto. Adicione, mude ou tire sugestões (os ids estão em ver_produto_completo). Tirar volta como prévia.',
  campos: {
    produto_id: ID_DO_PRODUTO,
    adicionar: {
      tipo: "lista",
      descricao: "Sugestões novas.",
      maxItens: 20,
      itens: {
        produto_sugerido_id: { tipo: "id", descricao: "Id do outro produto (ativo).", obrigatorio: true },
        titulo: { tipo: "texto", descricao: 'Título do bloco. Padrão: "Compre Junto e Economize!".', max: 120 },
        desconto: { tipo: "opcao", descricao: "Tipo do desconto. Padrão: porcentagem.", opcoes: Object.keys(DESCONTO) },
        valor_do_desconto: { tipo: "numero", descricao: "Valor do desconto (% ou reais). Padrão: 10.", min: 0, max: 1_000_000 },
      },
    },
    alterar: {
      tipo: "lista",
      descricao: "Sugestões que mudam.",
      maxItens: 20,
      itens: {
        combo_id: { tipo: "id", descricao: "Id da sugestão (compre_junto em ver_produto_completo).", obrigatorio: true },
        produto_sugerido_id: { tipo: "id", descricao: "Outro produto sugerido." },
        titulo: { tipo: "texto", descricao: "Novo título.", max: 120 },
        desconto: { tipo: "opcao", descricao: "Tipo do desconto.", opcoes: Object.keys(DESCONTO) },
        valor_do_desconto: { tipo: "numero", descricao: "Valor do desconto.", min: 0, max: 1_000_000 },
        ativo: { tipo: "booleano", descricao: "Ligada ou desligada." },
      },
    },
    remover: { tipo: "textos", descricao: "Ids das sugestões que saem.", maxItens: 20, max: 40 },
  },
  tipo: "escrita",
  sensivel: (args) => Array.isArray(args.remover) && (args.remover as unknown[]).length > 0,
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "alterar produtos");
    const produtoId = String(args.produto_id);
    const ficha = await lerFicha(ctx, produtoId);
    const { data: atuais } = await ctx.supabase.from("product_combos").select("id,combo_product_id,title").eq("main_product_id", produtoId);
    const existentes = (atuais ?? []) as Array<{ id: string; combo_product_id: string; title: string | null }>;

    const conferirSugerido = async (id: string) => {
      if (id === produtoId) recusar("O produto não pode sugerir ele mesmo.");
      const { data } = await ctx.supabase.from("products").select("id,name,active,product_kind").eq("id", id).in("user_id", ctx.tenantIds).maybeSingle();
      const sugerido = data as { id: string; name: string; active: boolean | null; product_kind: string | null } | null;
      if (!sugerido || sugerido.product_kind === "digital") recusar(`O produto ${id} não foi encontrado nesta empresa.`);
      if (sugerido.active === false) recusar(`O produto ${sugerido.name} está inativo: a tela só sugere produtos ativos.`);
      return sugerido;
    };

    const mudancas: string[] = [];
    const novos: Combos["novos"] = [];
    for (const pedido of (args.adicionar as Array<Record<string, unknown>> | undefined) ?? []) {
      const sugerido = await conferirSugerido(String(pedido.produto_sugerido_id));
      const tipo = DESCONTO[(pedido.desconto as keyof typeof DESCONTO | undefined) ?? "porcentagem"];
      const valor = tipo === "none" ? 0 : Number(pedido.valor_do_desconto ?? 10);
      if (tipo === "percentage" && valor > 100) recusar("O desconto em porcentagem vai até 100.");
      /* Os campos do "Adicionar combo" da aba Compre Junto. */
      novos.push({ main_product_id: produtoId, combo_product_id: sugerido.id, title: String(pedido.titulo ?? "Compre Junto e Economize!"), discount_type: tipo, discount_value: valor, active: true });
      mudancas.push(`nova sugestão: ${sugerido.name} (${tipo === "percentage" ? `${valor}%` : tipo === "fixed" ? emReais(valor) : "sem desconto"})`);
    }
    const mudados: Combos["mudados"] = [];
    for (const pedido of (args.alterar as Array<Record<string, unknown>> | undefined) ?? []) {
      const combo = existentes.find((item) => item.id === pedido.combo_id);
      if (!combo) recusar(`A sugestão ${String(pedido.combo_id)} não é deste produto.`);
      const campos: Record<string, unknown> = {};
      if (pedido.produto_sugerido_id !== undefined) campos.combo_product_id = (await conferirSugerido(String(pedido.produto_sugerido_id))).id;
      if (pedido.titulo !== undefined) campos.title = String(pedido.titulo);
      if (pedido.desconto !== undefined) campos.discount_type = DESCONTO[pedido.desconto as keyof typeof DESCONTO];
      if (pedido.valor_do_desconto !== undefined) campos.discount_value = Number(pedido.valor_do_desconto);
      if (pedido.ativo !== undefined) campos.active = pedido.ativo === true;
      if (!Object.keys(campos).length) recusar("Diga o que muda em cada sugestão.");
      mudados.push({ id: combo.id, campos });
      mudancas.push(`sugestão ${combo.title ?? combo.id}: ${Object.keys(campos).join(", ")}`);
    }
    const saem = ((args.remover as string[] | undefined) ?? []).map((id) => {
      const combo = existentes.find((item) => item.id === id.toLowerCase());
      if (!combo) recusar(`A sugestão ${id} não é deste produto.`);
      mudancas.push(`sai a sugestão ${combo.title ?? combo.id}`);
      return combo.id;
    });
    if (!mudancas.length) recusar("Diga o que adicionar, alterar ou remover.");
    return {
      previa: { o_que_vai_acontecer: `O compre junto do produto ${ficha.produto.name} muda.`, mudancas },
      resumo: `Prévia: compre junto do produto ${ficha.produto.name}`,
      avisos: [],
      carga: { produtoId, nome: ficha.produto.name, novos, mudados, saem, mudancas },
    };
  },
  async gravar(ctx, carga) {
    if (carga.novos.length) {
      const { error } = await ctx.supabase.from("product_combos").insert(carga.novos);
      if (error) falha("adicionar a sugestão", error);
    }
    for (const mudado of carga.mudados) {
      const { error } = await ctx.supabase.from("product_combos").update(mudado.campos).eq("id", mudado.id);
      if (error) falha("mudar a sugestão", error);
    }
    if (carga.saem.length) {
      const { error } = await ctx.supabase.from("product_combos").delete().in("id", carga.saem).eq("main_product_id", carga.produtoId);
      if (error) falha("tirar a sugestão", error);
    }
    avisarAMudanca(ctx);
    return { dados: { feito: true, produto_id: carga.produtoId, mudancas: carga.mudancas }, resumo: `Mudou o compre junto do produto ${carga.nome}` };
  },
};

/* =================================================== campos personalizados */

type Campos = { produtoId: string; nome: string; campos: unknown[]; mudancas: string[] };

export const alterarCamposPersonalizadosDoProduto: FerramentaDeEscrita<Campos> = {
  nome: "alterar_campos_personalizados_do_produto",
  titulo: "Alterar os campos personalizados de um produto",
  descricao:
    'Os campos que o cliente preenche ao comprar (aba Campos Personalizados): "Nome para gravar", "Data do evento"... Crie, mude ou tire pelo título. Um campo pode valer só numa combinação de opções (ex.: "Cor: Azul, Tamanho: P").',
  campos: {
    produto_id: ID_DO_PRODUTO,
    campos: {
      tipo: "lista",
      descricao: "Os campos que mudam ou entram (achados pelo título).",
      obrigatorio: true,
      minItens: 1,
      maxItens: 30,
      itens: {
        titulo: { tipo: "texto", descricao: "Título do campo (o atual, para mudar um que existe).", obrigatorio: true, max: 120 },
        novo_titulo: { tipo: "texto", descricao: "Novo título.", max: 120 },
        remover: { tipo: "booleano", descricao: "Tira o campo." },
        tipo: { tipo: "opcao", descricao: "Tipo da resposta.", opcoes: ["texto curto", "texto longo", "número"] },
        exemplo: { tipo: "texto", descricao: "O texto de exemplo dentro do campo.", max: 200 },
        obrigatorio: { tipo: "booleano", descricao: "O cliente precisa preencher." },
        ativo: { tipo: "booleano", descricao: "Falso esconde o campo sem apagar." },
        so_na_combinacao: { tipo: "texto", descricao: 'Só aparece nesta combinação, como "Cor: Azul, Tamanho: P". "todas" volta a valer sempre.', max: 300 },
      },
    },
  },
  tipo: "escrita",
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "alterar produtos");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const pedidos = (args.campos as PedidoDeCampo[]).map((pedido) =>
      pedido.so_na_combinacao !== undefined && /^todas$/i.test(pedido.so_na_combinacao.trim()) ? { ...pedido, so_na_combinacao: "" } : pedido
    );
    const resultado = camposDepoisDe(ficha.campos, pedidos, randomUUID);
    if (!resultado.ok) recusar(resultado.erro);
    return {
      previa: { o_que_vai_acontecer: `Os campos personalizados do produto ${ficha.produto.name} mudam.`, mudancas: resultado.mudancas },
      resumo: `Prévia: campos personalizados do produto ${ficha.produto.name}`,
      avisos: [],
      carga: { produtoId: ficha.produto.id, nome: ficha.produto.name, campos: resultado.campos, mudancas: resultado.mudancas },
    };
  },
  async gravar(ctx, carga) {
    await gravarComoOPainel(ctx, carga.produtoId, { custom_fields_config: carga.campos }, "mudar os campos personalizados");
    return { dados: { feito: true, produto_id: carga.produtoId, mudancas: carga.mudancas }, resumo: `Mudou os campos personalizados do produto ${carga.nome}` };
  },
};

/* ========================================================= ficha técnica */

type FichaTecnica = {
  produtoId: string;
  nome: string;
  insumos: Array<Record<string, unknown>>;
  processos: Array<Record<string, unknown>>;
  saemInsumos: string[];
  saemProcessos: string[];
  mudancas: string[];
};

export const alterarFichaTecnicaDoProduto: FerramentaDeEscrita<FichaTecnica> = {
  nome: "alterar_ficha_tecnica_do_produto",
  titulo: "Alterar a ficha técnica (composição) de um produto",
  descricao:
    "A aba Composição (Custos) do produto: insumos que ele consome e processos (máquina ou mão de obra, com minutos). O custo de cada linha sai do cadastro do insumo, da máquina ou da mão de obra, e o custo do produto é refeito como no painel. Ids em ver_produto_completo com com_cadastros_de_producao. Tirar linhas volta como prévia.",
  campos: {
    produto_id: ID_DO_PRODUTO,
    adicionar_insumos: {
      tipo: "lista",
      descricao: "Insumos que entram.",
      maxItens: 30,
      itens: {
        insumo_id: { tipo: "id", descricao: "Id do insumo.", obrigatorio: true },
        quantidade: { tipo: "numero", descricao: "Quanto do insumo vai em uma unidade do produto.", min: 0.0001, max: 1_000_000 },
        pecas_por_unidade: { tipo: "numero", descricao: "Ou o rendimento: quantas peças saem de uma unidade do insumo (vira 1 ÷ peças).", min: 0.0001, max: 1_000_000 },
      },
    },
    adicionar_processos: {
      tipo: "lista",
      descricao: "Processos que entram.",
      maxItens: 30,
      itens: {
        tipo: { tipo: "opcao", descricao: "máquina ou mão de obra.", opcoes: ["máquina", "mão de obra"], obrigatorio: true },
        id: { tipo: "id", descricao: "Id da máquina ou da mão de obra.", obrigatorio: true },
        minutos: { tipo: "numero", descricao: "Minutos por unidade.", obrigatorio: true, min: 0, max: 100_000 },
        preparo_minutos: { tipo: "numero", descricao: "Minutos de preparo (só máquina).", min: 0, max: 100_000 },
      },
    },
    remover_linhas: { tipo: "textos", descricao: "Ids das linhas que saem (linha_id em ver_produto_completo).", maxItens: 60, max: 40 },
  },
  tipo: "escrita",
  sensivel: (args) => Array.isArray(args.remover_linhas) && (args.remover_linhas as unknown[]).length > 0,
  permissoes: ["production.costs.write"],
  async preparar(ctx, args) {
    exigir(ctx, "production.costs.write", "mexer na ficha técnica dos produtos");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const cadastros = await cadastrosDeProducao(ctx);
    const mudancas: string[] = [];
    const insumos: FichaTecnica["insumos"] = [];
    for (const pedido of (args.adicionar_insumos as Array<{ insumo_id: string; quantidade?: number; pecas_por_unidade?: number }> | undefined) ?? []) {
      const insumo = cadastros.insumos.find((item) => item.id === pedido.insumo_id);
      if (!insumo) recusar(`O insumo ${pedido.insumo_id} não está no cadastro da empresa.`);
      const linha = linhaDeInsumo({ custoPorUnidade: Number(insumo.cost_per_unit) || 0, perdaPct: Number(insumo.waste_pct_default) || 0 }, pedido);
      if (!linha) recusar(`Informe a quantidade (ou pecas_por_unidade) do insumo ${insumo.name}.`);
      insumos.push({ product_id: ficha.produto.id, material_id: insumo.id, ...linha });
      mudancas.push(`insumo ${insumo.name}: ${quantidade(linha.consumption_qty)} ${insumo.unit ?? ""} (${emReais(linha.cost_total)})`.replace(/\s+\(/, " ("));
    }
    const processos: FichaTecnica["processos"] = [];
    for (const pedido of (args.adicionar_processos as Array<{ tipo: string; id: string; minutos: number; preparo_minutos?: number }> | undefined) ?? []) {
      const ehMaquina = pedido.tipo === "máquina";
      const preparo = Number(pedido.preparo_minutos ?? 0);
      if (ehMaquina) {
        const maquina = cadastros.maquinas.find((item) => item.id === pedido.id);
        if (!maquina) recusar(`A máquina ${pedido.id} não está no cadastro da empresa.`);
        const custo = custoDoProcesso("MACHINE", pedido.minutos, preparo, Number(maquina.hourly_cost) || 0);
        processos.push({ product_id: ficha.produto.id, kind: "MACHINE", minutes: pedido.minutos, setup_minutes: preparo, cost_total: custo, machine_id: maquina.id, process_name: maquina.name });
        mudancas.push(`máquina ${maquina.name}: ${quantidade(pedido.minutos)} min + ${quantidade(preparo)} de preparo (${emReais(custo)})`);
      } else {
        const funcao = cadastros.funcoes.find((item) => item.id === pedido.id);
        if (!funcao) recusar(`A mão de obra ${pedido.id} não está no cadastro da empresa.`);
        const custo = custoDoProcesso("ROLE", pedido.minutos, preparo, Number(funcao.hourly_rate) || 0);
        processos.push({ product_id: ficha.produto.id, kind: "ROLE", minutes: pedido.minutos, setup_minutes: preparo, cost_total: custo, role_id: funcao.id, process_name: funcao.role_name });
        mudancas.push(`mão de obra ${funcao.role_name}: ${quantidade(pedido.minutos)} min (${emReais(custo)})`);
      }
    }
    const linhasDeInsumo = ((ficha.produto.product_materials ?? []) as Array<{ id: string }>).map((linha) => linha.id);
    const linhasDeProcesso = ((ficha.produto.product_processes ?? []) as Array<{ id: string }>).map((linha) => linha.id);
    const saemInsumos: string[] = [];
    const saemProcessos: string[] = [];
    for (const id of (args.remover_linhas as string[] | undefined) ?? []) {
      const limpo = id.trim().toLowerCase();
      if (linhasDeInsumo.includes(limpo)) saemInsumos.push(limpo);
      else if (linhasDeProcesso.includes(limpo)) saemProcessos.push(limpo);
      else recusar(`A linha ${id} não é da ficha técnica deste produto.`);
    }
    if (saemInsumos.length + saemProcessos.length) mudancas.push(`saem ${contagem(saemInsumos.length + saemProcessos.length, "linha", "linhas")}`);
    if (!mudancas.length) recusar("Diga o que entra ou sai da ficha técnica.");
    return {
      previa: { o_que_vai_acontecer: `A ficha técnica do produto ${ficha.produto.name} muda, e o custo dele é refeito.`, mudancas },
      resumo: `Prévia: ficha técnica do produto ${ficha.produto.name}`,
      avisos: [],
      carga: { produtoId: ficha.produto.id, nome: ficha.produto.name, insumos, processos, saemInsumos, saemProcessos, mudancas },
    };
  },
  async gravar(ctx, carga) {
    /* Como as rotas da aba Composição: chave de serviço, depois da permissão,
       sempre com o produto desta empresa, e user_id = a empresa. */
    const empresa = ctx.empresa.companyId;
    if (carga.insumos.length) {
      const { error } = await ctx.admin.from("product_materials").insert(carga.insumos.map((linha) => ({ ...linha, user_id: empresa })));
      if (error) falha("pôr os insumos na ficha", error);
    }
    if (carga.processos.length) {
      const { error } = await ctx.admin.from("product_processes").insert(carga.processos.map((linha) => ({ ...linha, user_id: empresa })));
      if (error) falha("pôr os processos na ficha", error);
    }
    if (carga.saemInsumos.length) {
      const { error } = await ctx.admin.from("product_materials").delete().in("id", carga.saemInsumos).eq("product_id", carga.produtoId);
      if (error) falha("tirar os insumos da ficha", error);
    }
    if (carga.saemProcessos.length) {
      const { error } = await ctx.admin.from("product_processes").delete().in("id", carga.saemProcessos).eq("product_id", carga.produtoId);
      if (error) falha("tirar os processos da ficha", error);
    }
    await updateProductCost(carga.produtoId);
    const { data } = await ctx.admin.from("products").select("cost_total").eq("id", carga.produtoId).maybeSingle();
    return {
      dados: { feito: true, produto_id: carga.produtoId, mudancas: carga.mudancas, ...(pode(ctx, "pricing.read") ? { custo_total: dinheiro((data as { cost_total?: number } | null)?.cost_total) } : {}) },
      resumo: `Mudou a ficha técnica do produto ${carga.nome}`,
    };
  },
};

/* ================================================================ estoque */

type Ajuste = {
  produtoId: string;
  nome: string;
  tipo: "produto" | "opcao" | "combinacao" | "celula";
  alvo: string | null;
  quantidade: number;
  direcao: 1 | -1;
  motivo: string | null;
  descricao: string;
};

/**
 * Estoque v2: o produto pronto sai de uma célula do item pronto (a mesma que a
 * venda usaria para a combinação dita). Produto que sai pela composição
 * (encomenda) ou que não mexe no estoque não tem saldo próprio para ajustar.
 */
async function prepararAjusteNoEstoqueNovo(
  ctx: ContextoDoAssistente,
  ficha: Awaited<ReturnType<typeof lerFicha>>,
  args: Record<string, unknown>
) {
  const p = ficha.produto;
  const texto = [args.combinacao, args.opcao].filter(Boolean).map(String).join(", ");
  const escolha = escolhaDoProduto(lerCombinacao(texto), ficha.atributos as never);
  if (!escolha.ok) recusar(escolha.erro);
  const d = await disponibilidadeNoBanco(ctx, { produtoId: p.id, selected: escolha.selected, quantidade: 1 });
  if (!d) falha("ler o estoque do produto", null);
  if (d!.modo !== "pronto") {
    recusar(
      d!.modo === "encomenda"
        ? `"${p.name}" sai pela composição (encomenda): o estoque é dos insumos. Use consultar_estoque e lancar_entrada_no_estoque / contar_estoque nos itens.`
        : `"${p.name}" não mexe no estoque (de onde sai: nenhum). Para controlar, use configurar_estoque_do_produto.`
    );
  }
  const pronto = (d!.itens || []).find((item) => (item as { papel?: string }).papel === "pronto") as
    | { celula_id?: string | null; rotulo?: string | null; saldo?: number | null; sem_celula?: boolean | null; falta_escolher?: string | null; permite_negativo?: boolean | null }
    | undefined;
  if (!pronto) recusar(`"${p.name}" sai como pronto, mas não tem item de estoque ligado. Use configurar_estoque_do_produto.`);
  if (pronto!.sem_celula || !pronto!.celula_id) recusar(`Diga a combinação (campo combinacao): falta ${pronto!.falta_escolher || "a variação"}.`);
  const quantidadePedida = Number(args.quantidade);
  const direcao: 1 | -1 = args.movimento === "entrada" ? 1 : -1;
  const antes = Number(pronto!.saldo ?? 0);
  const depois = antes + direcao * quantidadePedida;
  const avisos: string[] = [];
  if (depois < 0 && pronto!.permite_negativo === false) avisos.push(`O saldo fica negativo (${quantidade(depois)}).`);
  const descricao = String(pronto!.rotulo || p.name);
  return {
    previa: {
      o_que_vai_acontecer: `${direcao > 0 ? "Entrada" : "Saída"} de ${quantidade(quantidadePedida)} em ${descricao} (estoque novo).`,
      saldo: `${quantidade(antes)} → ${quantidade(depois)}`,
      motivo: args.motivo ?? "Ajuste manual",
    },
    resumo: `Prévia: ajuste de estoque do produto ${p.name}`,
    avisos,
    carga: {
      produtoId: p.id,
      nome: p.name,
      tipo: "celula" as const,
      alvo: String(pronto!.celula_id),
      quantidade: quantidadePedida,
      direcao,
      motivo: (args.motivo as string | undefined) ?? null,
      descricao,
    },
  };
}

export const ajustarEstoqueDoProduto: FerramentaDeEscrita<Ajuste> = {
  nome: "ajustar_estoque_do_produto",
  titulo: "Ajustar o estoque de um produto",
  descricao:
    "Lança uma entrada ou saída no estoque do produto acabado, como o ajuste do Histórico de Estoque da tela: no saldo geral, numa opção (estoque por opção) ou numa combinação (estoque por combinação). Fica no extrato. Sempre com prévia.",
  campos: {
    produto_id: ID_DO_PRODUTO,
    movimento: { tipo: "opcao", descricao: "entrada (soma) ou saída (tira).", opcoes: ["entrada", "saída"], obrigatorio: true },
    quantidade: { tipo: "numero", descricao: "Quanto entra ou sai.", obrigatorio: true, min: 0.001, max: 100_000_000 },
    motivo: { tipo: "texto", descricao: 'O motivo, que vai para o extrato (ex.: "contagem do inventário").', max: 300 },
    opcao: { tipo: "texto", descricao: "Com estoque por opção: o nome da opção (ex.: Azul) ou o id dela.", max: 120 },
    combinacao: { tipo: "texto", descricao: 'Com estoque por combinação: "Cor: Azul, Tamanho: P", ou o id da combinação.', max: 300 },
  },
  tipo: "escrita",
  sensivel: true,
  permissoes: ["products.stock.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.stock.write", "ajustar o estoque");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const p = ficha.produto;
    if (await empresaNoEstoqueV2(ctx)) return prepararAjusteNoEstoqueNovo(ctx, ficha, args);
    const escopo = String(p.inventory_scope || "product");
    const quantidadePedida = Number(args.quantidade);
    const direcao = args.movimento === "entrada" ? 1 : -1;
    const avisos: string[] = [];
    if (p.track_inventory !== true) avisos.push("Este produto não está com o controle de estoque ligado: o saldo muda, mas as vendas não baixam dele até ligar (atualizar_produto, campo estoque).");

    let tipo: Ajuste["tipo"] = "produto";
    let alvo: string | null = null;
    let antes = Number(p.stock ?? 0);
    let descricao = "saldo geral";
    if (escopo === "option") {
      if (!args.opcao) recusar(`O estoque deste produto é por opção (${String(p.inventory_attribute_name)}): diga qual opção (campo opcao).`);
      const atributo = ficha.atributos.find((item) => item.name.toLowerCase() === String(p.inventory_attribute_name ?? "").toLowerCase());
      const ref = String(args.opcao).trim().toLowerCase();
      const opcao = atributo?.options.find((item) => item.id === ref || String(item.label).trim().toLowerCase() === ref);
      if (!opcao?.id) recusar(`A opção "${String(args.opcao)}" não existe em ${String(p.inventory_attribute_name)}.`);
      tipo = "opcao";
      alvo = opcao.id;
      antes = Number(opcao.stock ?? 0);
      descricao = `${atributo?.name}: ${opcao.label}`;
    } else if (escopo === "variant") {
      if (!args.combinacao) recusar('O estoque deste produto é por combinação: diga qual (campo combinacao, ex.: "Cor: Azul, Tamanho: P").');
      const texto = String(args.combinacao).trim();
      const pares = texto.split(/[,;|]/).map((parte) => parte.split(":").map((lado) => lado.trim().toLowerCase())).filter((par) => par.length === 2);
      const variacao = ficha.variacoes.find((item) => {
        if (item.id === texto.toLowerCase()) return true;
        const entradas = Object.entries(item.combination).map(([chave, valor]) => [chave.trim().toLowerCase(), String(valor).trim().toLowerCase()]);
        return entradas.length === pares.length && pares.every(([chave, valor]) => entradas.some(([c, v]) => c === chave && v === valor));
      });
      if (!variacao?.id) recusar(`A combinação "${texto}" não existe. As combinações estão em ver_produto_completo (estoque.combinacoes).`);
      tipo = "combinacao";
      alvo = variacao.id;
      antes = Number(variacao.stock ?? 0);
      descricao = Object.entries(variacao.combination).map(([chave, valor]) => `${chave}: ${valor}`).join(", ");
    } else if (args.opcao || args.combinacao) {
      recusar("O estoque deste produto é um saldo só (por produto): não mande opcao nem combinacao.");
    }
    const depois = antes + direcao * quantidadePedida;
    if (depois < 0 && p.allow_negative_stock !== true) avisos.push(`O saldo fica negativo (${quantidade(depois)}).`);
    return {
      previa: {
        o_que_vai_acontecer: `${direcao > 0 ? "Entrada" : "Saída"} de ${quantidade(quantidadePedida)} no estoque do produto ${p.name} (${descricao}).`,
        saldo: `${quantidade(antes)} → ${quantidade(depois)}`,
        motivo: args.motivo ?? "Ajuste manual",
      },
      resumo: `Prévia: ajuste de estoque do produto ${p.name}`,
      avisos,
      carga: { produtoId: p.id, nome: p.name, tipo, alvo, quantidade: quantidadePedida, direcao, motivo: (args.motivo as string | undefined) ?? null, descricao },
    };
  },
  async gravar(ctx, carga) {
    let saldo: number | null = null;
    if (carga.tipo === "celula") {
      /* Estoque v2: um ajuste na célula do item pronto (linha do tempo). */
      const { data, error } = await ctx.supabase.rpc("estoque_lancar", {
        p_celula: carga.alvo,
        p_tipo: "ajuste",
        p_quantidade: carga.direcao * carga.quantidade,
        p_custo_unitario: null,
        p_observacao: carga.motivo || "Ajuste manual",
        p_unidade: null,
      });
      if (error) {
        if (String((error as { hint?: string }).hint || "").startsWith("ESTOQUE_")) recusar(error.message);
        falha("lançar o ajuste de estoque", error);
      }
      saldo = Number(((data ?? {}) as { celula?: { saldo?: number } }).celula?.saldo ?? 0);
    } else if (carga.tipo === "produto") {
      /* O mesmo lançamento do "Estoque Geral" do histórico: o gatilho do banco acerta o saldo. */
      const { error } = await ctx.supabase.from("inventory_movements").insert({
        product_id: carga.produtoId,
        quantity: carga.quantidade,
        type: carga.direcao > 0 ? "adjustment_in" : "adjustment_out",
        reference_type: "manual",
        notes: carga.motivo || "Ajuste manual",
        user_id: ctx.user.id,
      });
      if (error) falha("lançar o ajuste de estoque", error);
      const { data } = await ctx.supabase.from("products").select("stock").eq("id", carga.produtoId).maybeSingle();
      saldo = Number((data as { stock?: number } | null)?.stock ?? 0);
    } else {
      const { data, error } = await ctx.supabase.rpc(carga.tipo === "opcao" ? "adjust_product_option_stock" : "adjust_product_variation_stock", {
        [carga.tipo === "opcao" ? "p_option_id" : "p_variation_id"]: carga.alvo,
        p_quantity: carga.quantidade,
        p_direction: carga.direcao,
        p_notes: carga.motivo || null,
      });
      if (error) {
        if (/Sem permissão|não encontrada|Quantidade/i.test(error.message)) recusar(`O banco recusou o ajuste: ${error.message}`);
        falha("lançar o ajuste de estoque", error);
      }
      saldo = Number(data ?? 0);
    }
    avisarAMudanca(ctx);
    return {
      dados: { feito: true, produto_id: carga.produtoId, onde: carga.descricao, saldo_agora: saldo },
      resumo: `${carga.direcao > 0 ? "Entrada" : "Saída"} de ${quantidade(carga.quantidade)} no estoque de ${carga.nome} (${carga.descricao})`,
    };
  },
};

/* ================================================================ duplicar */

type Duplicacao = { produtoId: string; nome: string };

export const duplicarProduto: FerramentaDeEscrita<Duplicacao> = {
  nome: "duplicar_produto",
  titulo: "Duplicar um produto",
  descricao:
    'Faz uma cópia do produto, como o "Duplicar" da lista de produtos: mesma ficha, atributos, faixas, fotos e ficha técnica, com o nome "<nome> - Cópia" e fora da loja virtual. Depois, ajuste a cópia com atualizar_produto.',
  campos: { produto_id: ID_DO_PRODUTO },
  tipo: "escrita",
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "cadastrar produtos");
    const { data } = await ctx.supabase.from("products").select("id,name,product_kind").eq("id", String(args.produto_id)).in("user_id", ctx.tenantIds).maybeSingle();
    const produto = data as { id: string; name: string; product_kind: string | null } | null;
    if (!produto) recusar("Produto não encontrado. Use buscar_produtos para achar o id certo.");
    if (produto.product_kind === "digital") recusar("Produto digital não se duplica por aqui.");
    return {
      previa: { o_que_vai_acontecer: `Uma cópia do produto ${produto.name} será criada, fora da loja virtual.`, nome_da_copia: `${produto.name} - Cópia` },
      resumo: `Prévia: duplicar o produto ${produto.name}`,
      avisos: [],
      carga: { produtoId: produto.id, nome: produto.name },
    };
  },
  async gravar(ctx, carga) {
    /* O mesmo "Duplicar" da lista de produtos: a função do banco
       duplicar_produto (migration 208) copia tudo numa transação só, com ids
       novos de opção (o saldo por opção é guardado pelo id dela) e estoque
       zerado; leva receitas e tempos da opção, serviços, faixas, fotos (com a
       foto de cada opção), processos e composição. */
    const { data: conferido } = await ctx.supabase
      .from("products")
      .select("id")
      .eq("id", carga.produtoId)
      .in("user_id", ctx.tenantIds)
      .maybeSingle();
    if (!conferido) recusar("Produto não encontrado. Use buscar_produtos para achar o id certo.");
    const { data, error } = await ctx.supabase.rpc("duplicar_produto", { p_produto: carga.produtoId });
    if (error || !data) falha("duplicar o produto", error);
    const copia = data as { produto_id: string; nome: string };
    avisarAMudanca(ctx);
    return {
      dados: {
        criado: true,
        produto_id: copia.produto_id,
        nome: copia.nome,
        na_loja: false,
      },
      resumo: `Duplicou o produto ${carga.nome}`,
    };
  },
};

/* ================================================================ excluir */

type Exclusao = { produtoId: string; nome: string };

export const excluirProduto: FerramentaDeEscrita<Exclusao> = {
  nome: "excluir_produto",
  titulo: "Excluir um produto",
  descricao:
    "Exclui um produto de vez, como o Excluir da lista de produtos: some do cadastro e da loja com fotos, atributos, faixas, estoque por opção, serviços e compre junto. Os orçamentos e pedidos que já têm o produto continuam como estão (o item perde o vínculo com o cadastro). Não dá para desfazer: sempre com prévia. Para só esconder, prefira atualizar_produto com ativo: false ou na_loja: false.",
  campos: { produto_id: ID_DO_PRODUTO },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: true,
  permissoes: ["products.catalog.write"],
  async preparar(ctx, args) {
    exigir(ctx, "products.catalog.write", "excluir produtos");
    const ficha = await lerFicha(ctx, String(args.produto_id));
    const p = ficha.produto;
    const [uso, combos] = await Promise.all([
      vinculosDosProdutos(ctx.supabase, [p.id]),
      ctx.supabase.from("product_combos").select("id", { count: "exact", head: true }).or(`main_product_id.eq.${p.id},combo_product_id.eq.${p.id}`),
    ]);
    const orcamentos = uso.orcamentos.get(p.id) ?? 0;
    const pedidos = uso.pedidos.get(p.id) ?? 0;
    const avisos: string[] = [];
    if (orcamentos || pedidos) {
      avisos.push(
        `O produto aparece em ${contagem(orcamentos, "item de orçamento", "itens de orçamento")} e ${contagem(pedidos, "item de pedido", "itens de pedido")}. Eles continuam, mas deixam de apontar para o cadastro. Se a ideia é só parar de vender, prefira desativar (atualizar_produto, ativo: false).`
      );
    }
    if (p.track_inventory === true && Number(p.stock ?? 0) !== 0) avisos.push(`O produto tem saldo em estoque (${quantidade(Number(p.stock))}), que some junto.`);
    return {
      previa: {
        o_que_vai_acontecer: `O produto ${p.name} será excluído de vez.`,
        produto: { id: p.id, nome: p.name, na_loja: p.show_in_catalog !== false && p.active !== false },
        sai_junto: {
          fotos: ficha.fotos.length,
          atributos: ficha.atributos.length,
          faixas_de_preco: ficha.faixas.length,
          sugestoes_de_compre_junto: combos.count ?? 0,
        },
        aparece_em: { orcamentos, pedidos },
      },
      resumo: `Prévia: excluir o produto ${p.name}`,
      avisos,
      carga: { produtoId: p.id, nome: p.name },
    };
  },
  async gravar(ctx, carga) {
    /* A exclusão da lista de produtos, já confirmada (forceDelete). */
    const feito = await excluirProdutosComoOPainel({ supabase: ctx.supabase, tenantIds: ctx.tenantIds, productIds: [carga.produtoId], forceDelete: true });
    if (!feito.ok) {
      if (feito.status === 404) recusar("Produto não encontrado. Nada foi excluído.");
      falha("excluir o produto", { message: feito.error });
    }
    const resultado = feito.resultado.results[0];
    if (!resultado?.success) falha("excluir o produto", { message: resultado?.error });
    avisarAMudanca(ctx);
    return { dados: { excluido: true, produto_id: carga.produtoId, nome: carga.nome }, resumo: `Excluiu o produto ${carga.nome}` };
  },
};

export const FERRAMENTAS_DE_PRODUTO_COMPLETO: Ferramenta[] = [
  verProdutoCompleto,
  adicionarFotosAoProduto,
  organizarFotosDoProduto,
  removerFotosDoProduto,
  alterarVariacoesDoProduto,
  alterarServicosDoProduto,
  alterarCompreJunto,
  alterarCamposPersonalizadosDoProduto,
  alterarFichaTecnicaDoProduto,
  ajustarEstoqueDoProduto,
  duplicarProduto,
  excluirProduto,
];
