import { createHash } from "node:crypto";

import sharp from "sharp";

import { isApiV1Error } from "@/lib/api-v1/errors";
import { createCategory, deleteCategory, updateCategory } from "@/lib/api-v1/services/categories-service";
import { resolveCompanyPublicProfileId } from "@/lib/company-context";
import { avisarLojaEEsperar } from "@/lib/loja/avisar-loja";

import { contatosNovos, recusaDeContatos } from "../contatos";
import type { ContextoDoAssistente } from "../contexto";
import type { Campos } from "../esquema";
import { DESCRICAO_DO_CAMPO_DE_IMAGEM, guardarImagemDaLoja, receberImagem } from "../imagens";
import { AVISO_DE_DADOS, baseDaLoja, contagem, exigir, falha, pode, recusar, todasAsLinhas } from "./comum";
import {
  acharCategoria,
  ALINHAMENTOS,
  ARREDONDAMENTOS,
  ARRUMACOES,
  FORMATOS_DE_BANNER,
  LIMITE_DO_CARROSSEL,
  NO_CELULAR,
  PROPORCOES_DA_IMAGEM,
  QUANTAS_NA_ARRUMACAO,
  type MedidaDeImagem,
  AVISO_DO_VENDOR,
  caminhoDaCategoria,
  conferirOrdemPedida,
  corHex,
  descendentes,
  encaixar,
  IMAGEM_DO_ATALHO,
  IMAGEM_DO_BANNER,
  LARGURAS,
  larguraEfetiva,
  lerSecao,
  LIMITE_DE_ATALHOS,
  LIMITE_DE_PRODUTOS_POR_SECAO,
  LIMITE_DE_PRODUTOS_POR_VEZ,
  LIMITE_DE_SECOES,
  linkDoBanner,
  LUGARES,
  modoPeloNome,
  montarRegraDePreco,
  NOME_DO_TEMPLATE,
  NOME_DO_TIPO,
  nomeDe,
  novaListaDaSecao,
  novaOrdemDosProdutos,
  ordemDaPagina,
  ordenarComoALoja,
  placementNaOrdem,
  posicoesDasSecoes,
  posicoesDosProdutos,
  posicoesParaAFrente,
  PRODUTOS_INVENTADOS,
  REGRA_PADRAO,
  regraDePrecoEmPalavras,
  slugDaCategoria,
  TAMANHOS,
  templateMostraSecoes,
  tipoGravado,
  TIPOS_DE_SECAO,
  TITULO_PADRAO,
  type CategoriaBruta,
  type Lugar,
  type Modo,
  type SecaoBruta,
} from "./loja-estrutura-regras";
import {
  CAMPOS_DE_IMAGEM,
  CAMPO_DO_BLOCO,
  aplicarPedidoDoBloco,
  blocoEmPalavras,
  ehWidget,
  faltaNoBloco,
  medidaDaImagem,
  widgetNovo,
  MODOS_DOS_WIDGETS,
} from "./loja-estrutura-blocos";
import { CAMPOS_DA_EXIBICAO, aplicarPedidoDeExibicao, exibicaoEmPalavras, fundoTemImagem } from "./loja-estrutura-exibicao";
import type { Ferramenta, FerramentaDeEscrita, FerramentaDeLeitura } from "./tipos";

/*
 * A estrutura da loja virtual pelo assistente: as seções da página inicial
 * (Catálogo > Produtos > Página inicial), as categorias (Produtos > Categorias) e a ordem
 * e a visibilidade dos produtos na loja (Loja > Produtos).
 *
 * Tudo grava com a sessão da pessoa (as travas do banco valem), como as
 * telas. As seções moram no perfil do dono da empresa; as categorias passam
 * pelo mesmo serviço da rota /api/categories (lib/api-v1/services/
 * categories-service.ts), com a conferência de ciclo e o aviso à loja dele.
 * Depois de gravar, a loja é avisada e o assistente espera a resposta.
 */

const ESCREVER_NA_LOJA = "catalog.products.write" as const;
const LER_A_LOJA = "catalog.products.read" as const;

/* ================================================================ apoio */

function perfilDaLoja(ctx: ContextoDoAssistente) {
  return resolveCompanyPublicProfileId(ctx.empresa, ctx.user.id);
}

async function avisar(ctx: ContextoDoAssistente) {
  const ouviu = await avisarLojaEEsperar(perfilDaLoja(ctx), new Request(`${ctx.origemDoAdmin}/api/mcp`));
  return ouviu ? {} : { observacao: "A loja pode levar até dois minutos para mostrar a mudança." };
}

type Secao = SecaoBruta & { id: string; user_id: string };

/* Só as seções da página inicial: as das páginas extras (migration 197,
   pagina_id) são das ferramentas de loja-paginas.ts. `todas` traz também as
   das páginas (para saber quem usa uma categoria). */
async function lerSecoes(ctx: ContextoDoAssistente, todas = false): Promise<Secao[]> {
  let consulta = ctx.supabase.from("catalog_sections").select("*").eq("user_id", perfilDaLoja(ctx));
  if (!todas) consulta = consulta.is("pagina_id", null);
  const { data, error } = await consulta.order("position", { ascending: true }).limit(200);
  if (error) falha("ler as seções da loja", error);
  return (data ?? []) as Secao[];
}

async function lerProdutosDasSecoes(ctx: ContextoDoAssistente, ids: string[]) {
  if (!ids.length) return [] as Array<{ section_id: string; product_id: string; position: number | null }>;
  const { data, error } = await ctx.supabase
    .from("catalog_section_products")
    .select("section_id,product_id,position")
    .in("section_id", ids)
    .order("position", { ascending: true })
    .limit(5000);
  if (error) falha("ler os produtos das seções", error);
  return (data ?? []) as Array<{ section_id: string; product_id: string; position: number | null }>;
}

type Categoria = CategoriaBruta & { user_id: string; pricing_rule: unknown };

async function lerCategorias(ctx: ContextoDoAssistente): Promise<Categoria[]> {
  const { data, error } = await ctx.supabase
    .from("categories")
    .select("id,name,slug,parent_id,is_visible,user_id,pricing_rule")
    .in("user_id", ctx.tenantIds)
    .order("name", { ascending: true })
    .limit(2000);
  if (error) falha("ler as categorias", error);
  return (data ?? []) as Categoria[];
}

type Produto = {
  id: string;
  name: string | null;
  category_id: string | null;
  show_in_catalog: boolean | null;
  active: boolean | null;
  catalog_position: number | null;
  created_at: string | null;
  is_catalog_exclusive: boolean | null;
};

/** Os produtos que a tela Loja > Produtos lista (os digitais ficam na área própria). */
async function lerProdutos(ctx: ContextoDoAssistente): Promise<{ produtos: Produto[]; cortado: boolean }> {
  const { linhas, cortado } = await todasAsLinhas<Produto>(
    (de, ate) =>
      ctx.supabase
        .from("products")
        .select("id,name,category_id,show_in_catalog,active,catalog_position,created_at,is_catalog_exclusive")
        .in("user_id", ctx.tenantIds)
        .neq("product_kind", "digital")
        .order("catalog_position", { ascending: true, nullsFirst: false })
        .order("created_at", { ascending: false })
        .order("id", { ascending: true })
        .range(de, ate),
    "ler os produtos",
    10_000
  );
  return { produtos: ordenarComoALoja(linhas), cortado };
}

/** O que o visitante vê: ativo e marcado para aparecer na loja. */
function naLoja(p: Produto) {
  return p.active !== false && p.show_in_catalog === true;
}

async function produtosPorIds(ctx: ContextoDoAssistente, ids: string[]): Promise<Map<string, Produto>> {
  const mapa = new Map<string, Produto>();
  for (let i = 0; i < ids.length; i += 200) {
    const { data, error } = await ctx.supabase
      .from("products")
      .select("id,name,category_id,show_in_catalog,active,catalog_position,created_at,is_catalog_exclusive")
      .in("user_id", ctx.tenantIds)
      .neq("product_kind", "digital")
      .in("id", ids.slice(i, i + 200));
    if (error) falha("ler os produtos", error);
    for (const p of (data ?? []) as Produto[]) mapa.set(p.id, p);
  }
  return mapa;
}

const SO_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function idsDeProdutos(valor: unknown): string[] {
  const lista = Array.isArray(valor) ? valor.map((v) => String(v).trim().toLowerCase()).filter(Boolean) : [];
  const ruim = lista.find((id) => !SO_UUID.test(id));
  if (ruim) recusar(`"${ruim}" não é um id de produto. Use os ids devolvidos por buscar_produtos.`);
  return [...new Set(lista)];
}

/** Confere que todos os produtos são desta empresa (e não digitais). */
async function conferirProdutos(ctx: ContextoDoAssistente, ids: string[]) {
  const achados = await produtosPorIds(ctx, ids);
  const faltando = ids.filter((id) => !achados.has(id));
  if (faltando.length) {
    recusar(`${contagem(faltando.length, "produto não é", "produtos não são")} desta loja (ou ${faltando.length === 1 ? "é digital" : "são digitais"}): ${faltando.slice(0, 5).join(", ")}${faltando.length > 5 ? "…" : ""}. Use buscar_produtos para achar os ids.`);
  }
  return achados;
}

async function temaDaLoja(ctx: ContextoDoAssistente): Promise<{ tema: string; slug: string | null; homeEditada: boolean }> {
  const [{ data }, publicacoes] = await Promise.all([
    ctx.supabase.from("profiles").select("catalog_theme,slug").eq("id", perfilDaLoja(ctx)).maybeSingle(),
    /* A página publicada pelo editor (migration 196): é o que faz o Vendor
       mostrar as seções. */
    ctx.supabase
      .from("catalog_home_publicacoes")
      .select("id")
      .eq("user_id", perfilDaLoja(ctx))
      /* Da home e que mexeu nas seções (publicar só o menu não conta). */
      .is("pagina_id", null)
      .eq("secoes_mexidas", true)
      .limit(1),
  ]);
  const linha = (data ?? {}) as { catalog_theme?: string | null; slug?: string | null };
  const tema = linha.catalog_theme && NOME_DO_TEMPLATE[linha.catalog_theme] ? linha.catalog_theme : "default";
  return { tema, slug: linha.slug ?? null, homeEditada: !publicacoes.error && (publicacoes.data ?? []).length > 0 };
}

/** Os endereços em que a loja atende: o do Ktalogu e o domínio próprio. */
async function enderecosDaLoja(ctx: ContextoDoAssistente): Promise<string[]> {
  const hosts: string[] = [];
  try {
    hosts.push(new URL(baseDaLoja(ctx)).hostname);
  } catch {
    /* sem endereço configurado: fica só o domínio próprio */
  }
  const { data } = await ctx.admin.from("profile_custom_domains").select("domain").eq("profile_id", perfilDaLoja(ctx)).limit(5);
  for (const linha of (data ?? []) as Array<{ domain?: string | null }>) if (linha.domain) hosts.push(linha.domain);
  return hosts;
}

function semContatoNovo(antes: string | null | undefined, depois: string | null | undefined, onde: string) {
  const novos = contatosNovos(antes, depois);
  if (novos.length) recusar(recusaDeContatos(novos, onde));
}

/* ---------------------------------------------------- imagens das seções */

type ImagemDaSecao = { conteudo: Buffer; tipo: string; extensao: string; origem: string; conferencia: string; pasta: string };

/**
 * A mesma imagem que a tela sobe (components/image-upload.tsx, formato
 * "svg-webp"): recorte do centro no tamanho exato, WebP, embrulhado num
 * arquivo .svg. A loja desenha o .svg como desenhava antes: banner e
 * atalhos feitos pelo assistente ficam iguais aos feitos pela tela.
 */
async function prepararImagemDaSecao(
  ctx: ContextoDoAssistente,
  valor: unknown,
  medida: typeof IMAGEM_DO_BANNER | typeof IMAGEM_DO_ATALHO | MedidaDeImagem,
  avisos: string[]
): Promise<ImagemDaSecao> {
  const recebida = await receberImagem(ctx, valor, { minimo: medida === IMAGEM_DO_ATALHO ? 80 : 100 });
  const { largura, altura, qualidade } = medida;
  const proporcao = recebida.largura / recebida.altura;
  const alvo = largura / altura;
  if (Math.abs(proporcao - alvo) / alvo > 0.25) {
    avisos.push(
      medida === IMAGEM_DO_BANNER
        ? `A imagem (${recebida.largura} × ${recebida.altura}) não é comprida como um banner (${largura} × ${altura}, 8 por 1): o meio dela será usado e as bordas cortadas.`
        : medida === IMAGEM_DO_ATALHO
          ? `A imagem do atalho (${recebida.largura} × ${recebida.altura}) não é quadrada: o centro será usado.`
          : `A imagem (${recebida.largura} × ${recebida.altura}) tem outra proporção que ${largura} × ${altura}: o centro será usado e as bordas cortadas.`
    );
  }
  if (recebida.largura < largura / 2) avisos.push(`A imagem é pequena (${recebida.largura} px de largura) e pode ficar borrada na loja.`);
  const webp = await sharp(recebida.conteudo, { limitInputPixels: 60_000_000, failOn: "error", animated: false })
    .rotate()
    .resize(largura, altura, { fit: "cover", position: "centre" })
    .webp({ quality: qualidade })
    .toBuffer();
  /* As seções do editor da página inicial sobem WebP de verdade (como a tela
     do editor), que o serviço de imagem consegue encolher para o celular. */
  if (medida.pasta === "catalog/sections/pagina-inicial") {
    return {
      conteudo: webp,
      tipo: "image/webp",
      extensao: "webp",
      origem: recebida.origem,
      conferencia: createHash("sha256").update(webp).digest("hex").slice(0, 12),
      pasta: medida.pasta,
    };
  }
  const svg = [
    `<svg xmlns="http://www.w3.org/2000/svg" width="${largura}" height="${altura}" viewBox="0 0 ${largura} ${altura}">`,
    `<image href="data:image/webp;base64,${webp.toString("base64")}" width="${largura}" height="${altura}" preserveAspectRatio="xMidYMid slice" />`,
    "</svg>",
  ].join("");
  const conteudo = Buffer.from(svg, "utf8");
  return {
    conteudo,
    tipo: "image/svg+xml",
    extensao: "svg",
    origem: recebida.origem,
    conferencia: createHash("sha256").update(conteudo).digest("hex").slice(0, 12),
    pasta: medida.pasta,
  };
}

async function guardar(ctx: ContextoDoAssistente, imagem: ImagemDaSecao) {
  return guardarImagemDaLoja(ctx, { conteudo: imagem.conteudo, tipo: imagem.tipo, extensao: imagem.extensao }, imagem.pasta);
}

/* ---------------------------------------------------- descrição da seção */

function aparenciaDe(s: SecaoBruta) {
  return {
    cor_de_fundo: s.background_color ?? "#ffffff",
    cor_do_texto: s.text_color ?? "#000000",
    largura: nomeDe(LARGURAS, s.layout_width ?? "full") ?? "total",
    alinhamento_do_titulo: nomeDe(ALINHAMENTOS, s.title_align ?? "center") ?? "centro",
    tamanho_do_titulo: nomeDe(TAMANHOS, s.title_size ?? "lg") ?? "grande",
  };
}

type Contexto = {
  categorias: Map<string, Categoria>;
  produtos: Map<string, Produto>;
  visiveis: Produto[];
  itensDasSecoes: Map<string, string[]>;
};

function categoriaEmPalavras(id: unknown, slug: unknown, c: Contexto) {
  const categoria: Categoria | undefined = (id ? c.categorias.get(String(id)) : undefined) ?? [...c.categorias.values()].find((x) => Boolean(slug) && x.slug === slug);
  if (!categoria) return id || slug ? { id: id ?? null, aviso: "categoria apagada ou de fora: a seção não mostra nada" } : null;
  return {
    id: categoria.id,
    nome: categoria.name,
    caminho: caminhoDaCategoria(categoria.id, c.categorias),
    ...(categoria.is_visible === false ? { aviso: "categoria escondida da loja: a seção não mostra nada" } : {}),
  };
}

function detalhesDaSecao(secao: Secao, modo: Modo, config: Record<string, unknown>, c: Contexto) {
  switch (modo) {
    case "manual_products": {
      const ids = c.itensDasSecoes.get(secao.id) ?? [];
      return {
        produtos: ids.length,
        lista: ids.slice(0, 20).map((id) => {
          const p = c.produtos.get(id);
          return { id, nome: p?.name ?? "(produto de fora ou apagado)", ...(p && p.active === false ? { aviso: "produto inativo" } : {}) };
        }),
        ...(ids.length > 20 ? { mais: ids.length - 20 } : {}),
      };
    }
    case "all_products":
      return { produtos_na_loja: c.visiveis.length, observacao: "É a grade de todos os produtos da loja, na ordem da loja." };
    case "category_products":
    case "subcategory_products": {
      const alvo = categoriaEmPalavras(config.target_category_id, config.target_category_slug, c);
      const alvoId = alvo && "nome" in alvo ? String(alvo.id) : null;
      const dentro = alvoId ? (modo === "category_products" ? descendentes(alvoId, [...c.categorias.values()]) : new Set([alvoId])) : new Set<string>();
      return {
        categoria: alvo,
        produtos_na_loja: c.visiveis.filter((p) => p.category_id && dentro.has(p.category_id)).length,
        mostrar_ver_mais: config.show_view_more !== false,
      };
    }
    case "image_banner":
      return {
        imagem: config.image_url || null,
        link: config.link_url || null,
        abrir_em_nova_aba: config.open_in_new_tab === true,
      };
    case "category_navigator": {
      const itens = Array.isArray(config.items) ? (config.items as Array<Record<string, unknown>>) : [];
      return {
        atalhos: itens.slice(0, LIMITE_DE_ATALHOS).map((item) => ({
          categoria: categoriaEmPalavras(item.target_category_id, item.target_category_slug, c),
          rotulo: item.label || null,
          imagem: item.image_url || null,
        })),
      };
    }
    case "image_grid":
    case "banner_carousel":
      return {
        ...(modo === "image_grid"
          ? { arrumacao: config.layout ?? "3", proporcao: config.proporcao ?? "quadrado", no_celular: config.celular ?? "deslizar" }
          : { formato: config.formato ?? "largo", passar_sozinho: config.automatico !== false }),
        imagens: imagensEmPalavras(config.itens, c),
      };
    case "featured_products":
      return { quantidade: Number(config.quantidade) || 4, observacao: "Os primeiros produtos na ordem da loja (Loja > Produtos)." };
    case "category_circles":
      return { observacao: "As categorias principais da loja, cada uma com a foto do primeiro produto. Categoria nova entra sozinha." };
    default:
      /* Os blocos novos (oferta, cupom, vídeo, perguntas...). */
      return ehWidget(modo) ? { bloco: blocoEmPalavras(modo, config, (id) => c.produtos.get(id)?.name ?? "(produto de fora ou apagado)") } : {};
  }
}

/* As imagens de "imagens lado a lado" e do carrossel, com o link de cada. */
function imagensEmPalavras(itens: unknown, c: Contexto) {
  return (Array.isArray(itens) ? (itens as Array<Record<string, unknown>>) : []).map((item, i) => {
    const link = (item?.link ?? {}) as Record<string, unknown>;
    const destino =
      link.tipo === "produto"
        ? { produto: c.produtos.get(String(link.produto_id))?.name ?? String(link.produto_id ?? "") }
        : link.tipo === "categoria"
          ? { categoria: categoriaEmPalavras(link.categoria_id, link.categoria_slug, c) }
          : link.tipo === "url"
            ? { link: link.url ?? "" }
            : { link: "sem link" };
    return { ordem: i + 1, imagem: item?.imagem || "(sem imagem: o lugar não aparece na loja)", ...(item?.legenda ? { legenda: item.legenda } : {}), ...destino };
  });
}

/* =================================================== ver_estrutura_da_loja */

export const verEstruturaDaLoja: FerramentaDeLeitura = {
  nome: "ver_estrutura_da_loja",
  titulo: "Ver a estrutura da loja",
  descricao:
    "Mostra a página inicial da loja como o visitante vê, de cima para baixo: as seções (id, tipo, título, ligada ou não, onde fica em relação à grade de produtos, aparência e o conteúdo de cada uma: produtos, categoria, banner, atalhos), o modelo da loja e se ele mostra seções, a árvore de categorias (id, nome, endereço, visível, quantos produtos) e os 30 primeiros produtos na ordem da loja. Use antes de criar, mudar ou reorganizar seções.",
  campos: {},
  tipo: "leitura",
  permissoes: [LER_A_LOJA],
  async executar(ctx) {
    exigir(ctx, LER_A_LOJA, "ver a loja");
    const [secoes, { tema, slug, homeEditada }, { produtos, cortado }, categoriasLidas] = await Promise.all([
      lerSecoes(ctx),
      temaDaLoja(ctx),
      lerProdutos(ctx),
      pode(ctx, "categories.read") ? lerCategorias(ctx) : Promise.resolve(null),
    ]);
    const itens = await lerProdutosDasSecoes(ctx, secoes.map((s) => s.id));
    const itensDasSecoes = new Map<string, string[]>();
    for (const item of itens) {
      const lista = itensDasSecoes.get(item.section_id) ?? [];
      lista.push(item.product_id);
      itensDasSecoes.set(item.section_id, lista);
    }
    const produtosPorId = new Map(produtos.map((p) => [p.id, p]));
    const foraDaLista = [...new Set(itens.map((i) => i.product_id))].filter((id) => !produtosPorId.has(id));
    if (foraDaLista.length) for (const [id, p] of await produtosPorIds(ctx, foraDaLista.slice(0, 400))) produtosPorId.set(id, p);
    const visiveis = produtos.filter(naLoja);
    const categorias = categoriasLidas ?? [];
    const contexto: Contexto = { categorias: new Map(categorias.map((c) => [c.id, c])), produtos: produtosPorId, visiveis, itensDasSecoes };

    const lidas = secoes.map((s) => ({ secao: s, ...lerSecao(s) }));
    const grade = lidas.find((l) => l.modo === "all_products" && l.secao.active !== false) ?? null;
    const ordem = ordemDaPagina(lidas.map((l) => ({ id: l.secao.id, modo: l.modo, ativa: l.secao.active !== false })));
    const indiceDaGrade = ordem.indexOf(grade ? grade.secao.id : PRODUTOS_INVENTADOS);

    const pagina = ordem.map((id, i) => {
      if (id === PRODUTOS_INVENTADOS) {
        return {
          ordem: i + 1,
          id: "produtos",
          tipo: "todos os produtos",
          titulo: "(grade automática dos produtos)",
          observacao: 'A empresa não tem a seção "Todos os produtos"; a loja mostra a grade sozinha, sempre no fim. Para pôr algo abaixo dela, reorganize com "produtos" na ordem: a seção é criada.',
          produtos_na_loja: visiveis.length,
        };
      }
      const l = lidas.find((x) => x.secao.id === id)!;
      return {
        ordem: i + 1,
        id,
        tipo: NOME_DO_TIPO[l.modo],
        titulo: l.titulo,
        ativa: true,
        lugar: l.modo === "all_products" ? "é a grade dos produtos" : i < indiceDaGrade ? "acima dos produtos" : "abaixo dos produtos",
        aparencia: aparenciaDe(l.secao),
        ...detalhesDaSecao(l.secao, l.modo, l.config, contexto),
        ...exibicaoEmPalavras(l.config),
      };
    });
    const desligadas = lidas
      .filter((l) => l.secao.active === false)
      .map((l) => ({ id: l.secao.id, tipo: NOME_DO_TIPO[l.modo], titulo: l.titulo, ativa: false, aparencia: aparenciaDe(l.secao), ...detalhesDaSecao(l.secao, l.modo, l.config, contexto) }));

    const contagemPorCategoria = new Map<string, number>();
    for (const p of produtos) if (p.category_id) contagemPorCategoria.set(p.category_id, (contagemPorCategoria.get(p.category_id) ?? 0) + 1);
    const arvore = categorias
      .map((c) => ({
        id: c.id,
        nome: c.name,
        caminho: caminhoDaCategoria(c.id, contexto.categorias),
        endereco: c.slug,
        visivel: c.is_visible !== false,
        dentro_de: c.parent_id ? (contexto.categorias.get(c.parent_id)?.name ?? null) : null,
        produtos: contagemPorCategoria.get(c.id) ?? 0,
      }))
      .sort((a, b) => a.caminho.localeCompare(b.caminho, "pt-BR"));

    const mostra = templateMostraSecoes(tema, homeEditada);
    return {
      dados: {
        _observacao: AVISO_DE_DADOS,
        loja: slug ? `${baseDaLoja(ctx)}/${slug}` : null,
        modelo_da_loja: NOME_DO_TEMPLATE[tema],
        modelo_mostra_secoes: mostra,
        ...(mostra ? {} : { aviso: AVISO_DO_VENDOR }),
        pagina_inicial: pagina,
        secoes_desligadas: desligadas,
        limite_de_secoes: LIMITE_DE_SECOES,
        categorias: categoriasLidas ? arvore : "Esta pessoa não tem permissão para ver as categorias.",
        produtos: {
          total: produtos.length,
          na_loja: visiveis.length,
          escondidos: produtos.length - visiveis.length,
          primeiros_na_ordem_da_loja: visiveis.slice(0, 30).map((p, i) => ({ posicao: i + 1, id: p.id, nome: p.name })),
          ...(cortado ? { observacao: "A loja tem mais de 10 mil produtos; as contas consideram os 10 mil primeiros." } : {}),
        },
      },
      resumo: `Consultou a estrutura da loja (${contagem(secoes.length, "seção", "seções")}, ${contagem(categorias.length, "categoria", "categorias")})`,
    };
  },
};

/* ===================================================== campos das seções */

const CAMPOS_DA_APARENCIA: Campos = {
  titulo: { tipo: "texto", descricao: "O título que aparece em cima da seção.", max: 120 },
  cor_de_fundo: { tipo: "texto", descricao: "Cor de fundo em hexadecimal, como #ffffff.", max: 9 },
  cor_do_texto: { tipo: "texto", descricao: "Cor do título em hexadecimal, como #000000.", max: 9 },
  largura: { tipo: "opcao", descricao: "total (de ponta a ponta) ou caixa (com margens). Banner e atalhos são sempre caixa.", opcoes: Object.keys(LARGURAS) },
  alinhamento: { tipo: "opcao", descricao: "Alinhamento do título.", opcoes: Object.keys(ALINHAMENTOS) },
  tamanho_do_titulo: { tipo: "opcao", descricao: "Tamanho do título.", opcoes: Object.keys(TAMANHOS) },
};

const CAMPOS_DO_LUGAR: Campos = {
  lugar: { tipo: "opcao", descricao: "Onde fica na página: no topo, acima dos produtos (a grade de todos os produtos), abaixo dos produtos ou no fim.", opcoes: LUGARES },
  posicao: { tipo: "inteiro", descricao: "Ou o número do bloco na página (1 = primeiro), contando a grade dos produtos como em ver_estrutura_da_loja. Vale mais que lugar.", min: 1, max: 60 },
};

const CAMPOS_DO_CONTEUDO: Campos = {
  mostrar_ver_mais: { tipo: "booleano", descricao: 'Seções de categoria: mostra o botão "ver mais". Padrão: sim.' },
  categoria: { tipo: "texto", descricao: "Seções de categoria/subcategoria: a categoria (id, nome, endereço ou caminho como \"Roupas / Camisetas\"). Subcategoria precisa estar dentro de outra.", max: 200 },
  imagem: { tipo: "texto", descricao: `Banner: ${DESCRICAO_DO_CAMPO_DE_IMAGEM} Fica em 1920 × 240 (comprida, 8 por 1), cortada pelo centro.`, max: 2000 },
  link: { tipo: "texto", descricao: 'Banner: para onde leva o clique. Só caminho da própria loja ("/minha-loja/produto/caneca", "#produtos") ou endereço completo da loja.', max: 500 },
  abrir_em_nova_aba: { tipo: "booleano", descricao: "Banner: o link abre em outra aba." },
  formato: {
    tipo: "opcao",
    descricao: "Banner e carrossel de banners: o formato da imagem. faixa (8 por 1, fina), largo (16 por 5), panoramico (32 por 9), cinema (21 por 9). Banner com formato pode ir de ponta a ponta (largura total).",
    opcoes: Object.keys(FORMATOS_DE_BANNER),
  },
  titulo_no_banner: { tipo: "texto", descricao: "Banner com formato (não faixa): título escrito por cima da imagem. Vazio tira.", max: 90 },
  subtitulo_no_banner: { tipo: "texto", descricao: "Banner com formato: frase de apoio por cima da imagem.", max: 160 },
  botao_no_banner: { tipo: "texto", descricao: "Banner com formato: texto do botão por cima da imagem (ex.: Ver agora).", max: 30 },
  arrumacao: {
    tipo: "opcao",
    descricao: 'Imagens lado a lado: "2", "3" ou "4" colunas, "1+2" (uma grande à esquerda e duas pequenas) ou "2+1" (duas pequenas e uma grande à direita).',
    opcoes: [...ARRUMACOES],
  },
  proporcao: { tipo: "opcao", descricao: "Imagens lado a lado: o formato de cada imagem.", opcoes: Object.keys(PROPORCOES_DA_IMAGEM) },
  no_celular: { tipo: "opcao", descricao: "Imagens lado a lado: como ficam no celular.", opcoes: Object.keys(NO_CELULAR) },
  passar_sozinho: { tipo: "booleano", descricao: "Carrossel de banners: passa sozinho (padrão sim)." },
  quantidade: { tipo: "inteiro", descricao: "Primeiros produtos: quantos mostrar (padrão 4).", min: 1, max: 48 },
  imagens: {
    tipo: "lista",
    descricao: `Imagens lado a lado (a quantidade da arrumação) e carrossel de banners (até ${LIMITE_DO_CARROSSEL}): cada uma com a imagem e para onde leva. Na alteração, a lista mandada substitui a de hoje.`,
    maxItens: LIMITE_DO_CARROSSEL,
    itens: {
      imagem: { tipo: "texto", descricao: `${DESCRICAO_DO_CAMPO_DE_IMAGEM} Vazio mantém a imagem de hoje na mesma posição.`, max: 2000 },
      legenda: { tipo: "texto", descricao: "Imagens lado a lado: texto curto por cima da imagem (opcional).", max: 80 },
      produto: { tipo: "texto", descricao: "O clique leva a este produto (id de buscar_produtos).", max: 40 },
      categoria: { tipo: "texto", descricao: "O clique leva a esta categoria (id, nome, endereço ou caminho).", max: 200 },
      link: { tipo: "texto", descricao: 'Ou o clique leva a um caminho da própria loja ("/minha-loja/c/canecas") ou endereço da loja.', max: 500 },
    },
  },
  atalhos: {
    tipo: "lista",
    descricao: `Atalhos de categorias: de 1 a ${LIMITE_DE_ATALHOS} bolinhas com foto que levam à categoria. Na alteração, a lista mandada substitui a de hoje.`,
    maxItens: LIMITE_DE_ATALHOS,
    itens: {
      categoria: { tipo: "texto", descricao: "A categoria (id, nome, endereço ou caminho).", obrigatorio: true, max: 200 },
      rotulo: { tipo: "texto", descricao: "O nome embaixo da foto. Vazio usa o nome da categoria.", max: 60 },
      imagem: { tipo: "texto", descricao: `A foto (240 × 240, cortada pelo centro). ${DESCRICAO_DO_CAMPO_DE_IMAGEM} Vazio mantém a foto de hoje do atalho da mesma categoria, se houver.`, max: 2000 },
    },
  },
};

type DadosDaSecao = {
  title: string;
  background_color: string;
  text_color: string;
  layout_width: "full" | "contained";
  title_align: "left" | "center" | "right";
  title_size: "sm" | "md" | "lg" | "xl";
  config: Record<string, unknown>;
};

type ImagemPendente = { imagem: ImagemDaSecao; onde: "banner" | number | { item: number } | { campo: string } | "fundo" };

/* O pedido trouxe imagem num bloco novo (texto com imagem, pontos, antes e depois, capa do vídeo)? */
function blocoTemImagem(args: Record<string, unknown>) {
  const bloco = args.bloco && typeof args.bloco === "object" ? (args.bloco as Record<string, unknown>) : null;
  return Boolean(bloco && Object.keys(CAMPOS_DE_IMAGEM).some((campo) => bloco[campo] !== undefined));
}

/* Os blocos novos: junta o campo `bloco` na configuração, confere ids no
   banco e prepara as imagens (loja-estrutura-blocos.ts). */
async function montarBloco(
  ctx: ContextoDoAssistente,
  args: Record<string, unknown>,
  modo: Modo,
  dados: DadosDaSecao,
  imagens: ImagemPendente[],
  mudancas: string[],
  avisos: string[]
) {
  if (!ehWidget(modo)) return;
  const lido = aplicarPedidoDoBloco(modo, dados.config, args.bloco as Record<string, unknown> | undefined);
  if (!lido.ok) recusar(lido.erro);
  const pedido = lido.pedido;
  dados.config = pedido.config;
  mudancas.push(...pedido.mudancas);
  if (pedido.produtos.length) await conferirProdutos(ctx, [...new Set(pedido.produtos)]);
  if (pedido.cupom !== null) {
    const codigo = pedido.cupom;
    const consulta = ctx.supabase.from("catalog_coupons").select("id,code,active,ends_at").eq("user_id", perfilDaLoja(ctx)).limit(1);
    const { data } = await (/^[0-9a-f-]{36}$/i.test(codigo) ? consulta.eq("id", codigo) : consulta.ilike("code", codigo.replace(/[%_]/g, "")));
    const cupom = (data ?? [])[0] as { id: string; code: string; active: boolean; ends_at: string | null } | undefined;
    if (!cupom) recusar(`Cupom "${codigo}" não encontrado nesta loja. Veja os cupons com listar_cupons.`);
    if (!cupom.active || (cupom.ends_at && new Date(cupom.ends_at).getTime() <= Date.now())) {
      avisos.push(`O cupom ${cupom.code} está desligado ou vencido: o bloco só aparece na loja enquanto ele vale.`);
    }
    dados.config.cupom_id = cupom.id;
  }
  if (pedido.combos && pedido.combos.length) {
    const { data } = await ctx.supabase.from("product_combos").select("id").eq("user_id", perfilDaLoja(ctx)).in("id", pedido.combos);
    const achados = new Set(((data ?? []) as Array<{ id: string }>).map((c) => c.id));
    const faltando = pedido.combos.filter((id) => !achados.has(id));
    if (faltando.length) recusar(`Combo não encontrado nesta loja: ${faltando.slice(0, 3).join(", ")}.`);
  }
  if (pedido.linkDoBotao !== null) {
    if (!pedido.linkDoBotao) {
      dados.config.botao_link = { tipo: "nenhum" };
    } else {
      const link = linkDoBanner(pedido.linkDoBotao, await enderecosDaLoja(ctx), null);
      if (!link.ok) recusar(link.erro);
      dados.config.botao_link = { tipo: "url", url: link.link };
    }
  }
  for (const pendente of pedido.imagens) {
    const medida = medidaDaImagem(modo, pendente.campo, dados.config);
    const preparada = await prepararImagemDaSecao(ctx, pendente.valor, { ...medida, qualidade: 84, pasta: "catalog/sections/pagina-inicial" }, avisos);
    imagens.push({ imagem: preparada, onde: { campo: CAMPOS_DE_IMAGEM[pendente.campo] } });
  }
  const falta = faltaNoBloco(modo, dados.config, pedido.imagens.map((i) => i.campo));
  if (falta) recusar(`Falta no bloco (${NOME_DO_TIPO[modo]}): ${falta}`);
}

/* Lê aparência e conteúdo do pedido sobre a base (a seção de hoje, ou os
   padrões da tela ao criar). Imagens ficam para `gravar` subir. */
async function montarSecao(
  ctx: ContextoDoAssistente,
  args: Record<string, unknown>,
  modo: Modo,
  base: DadosDaSecao,
  avisos: string[],
  categorias: () => Promise<Categoria[]>
): Promise<{ dados: DadosDaSecao; imagens: ImagemPendente[]; mudancas: string[] }> {
  const dados: DadosDaSecao = { ...base, config: { ...base.config, mode: modo } };
  const imagens: ImagemPendente[] = [];
  const mudancas: string[] = [];

  if (args.titulo !== undefined) {
    const titulo = String(args.titulo).trim();
    semContatoNovo(base.title, titulo, "em Catálogo > Produtos > Página inicial");
    if (titulo !== base.title) mudancas.push("título");
    dados.title = titulo;
  }
  for (const [campo, coluna] of [
    ["cor_de_fundo", "background_color"],
    ["cor_do_texto", "text_color"],
  ] as const) {
    if (args[campo] === undefined) continue;
    const cor = corHex(args[campo]);
    if (!cor) recusar(`"${campo}" não é uma cor: use hexadecimal, como #1a73e8.`);
    if (cor !== base[coluna]) mudancas.push(campo.replace(/_/g, " "));
    dados[coluna] = cor;
  }
  if (args.largura !== undefined) {
    dados.layout_width = LARGURAS[args.largura as keyof typeof LARGURAS];
    mudancas.push("largura");
  }
  if (modo === "image_banner" && args.formato !== undefined) dados.config.formato = args.formato;
  dados.layout_width = larguraEfetiva(modo, dados.layout_width, dados.config);
  if (args.alinhamento !== undefined) {
    dados.title_align = ALINHAMENTOS[args.alinhamento as keyof typeof ALINHAMENTOS];
    mudancas.push("alinhamento");
  }
  if (args.tamanho_do_titulo !== undefined) {
    dados.title_size = TAMANHOS[args.tamanho_do_titulo as keyof typeof TAMANHOS];
    mudancas.push("tamanho do título");
  }

  const soDe = (campo: string, modos: Modo[]) => {
    if (args[campo] !== undefined && !modos.includes(modo)) {
      recusar(`"${campo}" não vale para uma seção de ${NOME_DO_TIPO[modo]}.`);
    }
  };
  soDe("mostrar_ver_mais", ["category_products", "subcategory_products", "manual_products"]);
  soDe("categoria", ["category_products", "subcategory_products"]);
  soDe("imagem", ["image_banner"]);
  soDe("link", ["image_banner"]);
  soDe("abrir_em_nova_aba", ["image_banner"]);
  soDe("atalhos", ["category_navigator"]);
  soDe("formato", ["image_banner", "banner_carousel"]);
  soDe("titulo_no_banner", ["image_banner"]);
  soDe("subtitulo_no_banner", ["image_banner"]);
  soDe("botao_no_banner", ["image_banner"]);
  soDe("arrumacao", ["image_grid"]);
  soDe("proporcao", ["image_grid"]);
  soDe("no_celular", ["image_grid"]);
  soDe("passar_sozinho", ["banner_carousel"]);
  soDe("quantidade", ["featured_products"]);
  soDe("imagens", ["image_grid", "banner_carousel"]);
  soDe("bloco", [...MODOS_DOS_WIDGETS]);
  if (Array.isArray(args.apagar) && (args.apagar as string[]).includes("link")) soDe("link", ["image_banner"]);

  if (args.mostrar_ver_mais !== undefined) {
    dados.config.show_view_more = args.mostrar_ver_mais === true;
    mudancas.push("ver mais");
  }

  if (modo === "category_products" || modo === "subcategory_products") {
    if (args.categoria !== undefined) {
      const lista = await categorias();
      const achada = acharCategoria(args.categoria, lista);
      if (!achada.ok) recusar(achada.erro);
      const categoria = achada.categoria;
      if (categoria.is_visible === false) recusar(`A categoria ${categoria.name} está escondida da loja: a seção não mostraria nada. Deixe ela visível (alterar_categoria) ou escolha outra.`);
      if (modo === "subcategory_products" && !categoria.parent_id) {
        recusar(`${categoria.name} não está dentro de outra categoria, então não é subcategoria. Use o tipo "produtos de uma categoria".`);
      }
      dados.config.target_category_id = categoria.id;
      dados.config.target_category_slug = categoria.slug ?? "";
      mudancas.push("categoria");
    } else if (!dados.config.target_category_id) {
      recusar(`Diga a categoria da seção (campo categoria).`);
    }
  }

  if (modo === "image_banner") {
    if (args.formato !== undefined) mudancas.push("formato");
    const formato = typeof dados.config.formato === "string" ? FORMATOS_DE_BANNER[dados.config.formato] : undefined;
    for (const [campo, chave] of [
      ["titulo_no_banner", "titulo_na_imagem"],
      ["subtitulo_no_banner", "subtitulo"],
      ["botao_no_banner", "botao"],
    ] as const) {
      if (args[campo] === undefined) continue;
      if (!formato || dados.config.formato === "faixa") recusar(`"${campo}" pede um banner com formato largo, panoramico ou cinema (a faixa fina não tem altura para texto).`);
      const texto = String(args[campo]).trim();
      semContatoNovo(String(base.config[chave] ?? ""), texto, "em Catálogo > Produtos > Página inicial");
      dados.config[chave] = texto;
      mudancas.push(campo.replace(/_/g, " "));
    }
    if (args.imagem !== undefined) {
      imagens.push({ imagem: await prepararImagemDaSecao(ctx, args.imagem, formato ?? IMAGEM_DO_BANNER, avisos), onde: "banner" });
      mudancas.push("imagem");
    } else if (!dados.config.image_url) {
      recusar('Falta a imagem do banner (campo imagem). Se a pessoa anexou o arquivo na conversa, chame pedir_imagem e mande o link para ela enviar; depois use o código "envio:...".');
    }
    const apagarLink = Array.isArray(args.apagar) && (args.apagar as string[]).includes("link");
    if (apagarLink) {
      dados.config.link_url = "";
      mudancas.push("link");
    } else if (args.link !== undefined) {
      const lido = linkDoBanner(args.link, await enderecosDaLoja(ctx), String(base.config.link_url ?? "") || null);
      if (!lido.ok) recusar(lido.erro);
      dados.config.link_url = lido.link;
      mudancas.push("link");
    }
    if (args.abrir_em_nova_aba !== undefined) {
      dados.config.open_in_new_tab = args.abrir_em_nova_aba === true;
      mudancas.push("abrir em nova aba");
    }
    dados.config.link_url ??= "";
    dados.config.open_in_new_tab ??= false;
  }

  if (modo === "category_navigator") {
    if (args.atalhos !== undefined) {
      const pedidos = args.atalhos as Array<Record<string, unknown>>;
      if (!pedidos.length) recusar("Mande ao menos um atalho.");
      const lista = await categorias();
      const deHoje = Array.isArray(base.config.items) ? (base.config.items as Array<Record<string, unknown>>) : [];
      const itens: Array<Record<string, string>> = [];
      for (let i = 0; i < pedidos.length; i += 1) {
        const pedido = pedidos[i];
        const achada = acharCategoria(pedido.categoria, lista);
        if (!achada.ok) recusar(`atalhos[${i + 1}]: ${achada.erro}`);
        const categoria = achada.categoria;
        if (categoria.is_visible === false) recusar(`atalhos[${i + 1}]: a categoria ${categoria.name} está escondida da loja; o atalho levaria a uma página vazia.`);
        const rotulo = typeof pedido.rotulo === "string" ? pedido.rotulo.trim() : "";
        const anterior = deHoje.find((x) => x.target_category_id === categoria.id);
        semContatoNovo(String(anterior?.label ?? ""), rotulo, "em Catálogo > Produtos > Página inicial");
        if (pedido.imagem !== undefined) {
          imagens.push({ imagem: await prepararImagemDaSecao(ctx, pedido.imagem, IMAGEM_DO_ATALHO, avisos), onde: i });
        }
        itens.push({
          target_category_id: categoria.id,
          target_category_slug: categoria.slug ?? "",
          label: rotulo,
          image_url: pedido.imagem !== undefined ? "" : String(anterior?.image_url ?? ""),
        });
      }
      const repetida = itens.find((item, i) => itens.findIndex((x) => x.target_category_id === item.target_category_id) !== i);
      if (repetida) avisos.push("Uma categoria aparece em dois atalhos.");
      const semFoto = itens.filter((item, i) => !item.image_url && !imagens.some((img) => img.onde === i)).length;
      if (semFoto) avisos.push(`${contagem(semFoto, "atalho fica", "atalhos ficam")} sem foto (a loja mostra só o nome).`);
      dados.config.items = itens;
      mudancas.push("atalhos");
    } else if (!Array.isArray(dados.config.items) || !(dados.config.items as unknown[]).length) {
      recusar("Mande os atalhos (campo atalhos): cada um com a categoria e, se quiser, rótulo e imagem.");
    }
  }
  if (modo === "featured_products" && args.quantidade !== undefined) {
    dados.config.quantidade = Math.trunc(Number(args.quantidade));
    mudancas.push("quantidade");
  }

  if (modo === "banner_carousel") {
    if (args.formato !== undefined) {
      dados.config.formato = args.formato;
      mudancas.push("formato");
    }
    if (args.passar_sozinho !== undefined) {
      dados.config.automatico = args.passar_sozinho === true;
      mudancas.push("passar sozinho");
    }
  }
  if (modo === "image_grid") {
    if (args.arrumacao !== undefined) {
      dados.config.layout = args.arrumacao;
      mudancas.push("arrumação");
    }
    if (args.proporcao !== undefined) {
      dados.config.proporcao = args.proporcao;
      mudancas.push("formato das imagens");
    }
    if (args.no_celular !== undefined) {
      dados.config.celular = NO_CELULAR[args.no_celular as keyof typeof NO_CELULAR];
      mudancas.push("no celular");
    }
  }

  if (modo === "image_grid" || modo === "banner_carousel") {
    const medida =
      modo === "image_grid"
        ? PROPORCOES_DA_IMAGEM[String(dados.config.proporcao ?? "quadrado")] ?? PROPORCOES_DA_IMAGEM.quadrado
        : FORMATOS_DE_BANNER[String(dados.config.formato ?? "largo")] ?? FORMATOS_DE_BANNER.largo;
    const cabem = modo === "image_grid" ? QUANTAS_NA_ARRUMACAO[String(dados.config.layout ?? "3")] ?? 3 : LIMITE_DO_CARROSSEL;
    const deHoje = Array.isArray(base.config.itens) ? (base.config.itens as Array<Record<string, unknown>>) : [];
    if (args.imagens !== undefined) {
      const pedidos = args.imagens as Array<Record<string, unknown>>;
      if (!pedidos.length) recusar("Mande ao menos uma imagem.");
      if (pedidos.length > cabem) {
        recusar(modo === "image_grid" ? `A arrumação "${dados.config.layout ?? "3"}" tem ${cabem} imagens; vieram ${pedidos.length}.` : `O carrossel aceita até ${cabem} banners.`);
      }
      const lista = await categorias();
      const enderecos = await enderecosDaLoja(ctx);
      const itens: Array<Record<string, unknown>> = [];
      for (let i = 0; i < pedidos.length; i += 1) {
        const pedido = pedidos[i];
        const anterior = deHoje[i] ?? {};
        const destinos = ["produto", "categoria", "link"].filter((campo) => pedido[campo] !== undefined && String(pedido[campo]).trim());
        if (destinos.length > 1) recusar(`imagens[${i + 1}]: escolha um destino só (produto, categoria ou link).`);
        let link: Record<string, unknown> = { tipo: "nenhum" };
        if (pedido.produto !== undefined && String(pedido.produto).trim()) {
          const [id] = idsDeProdutos([pedido.produto]);
          await conferirProdutos(ctx, [id]);
          link = { tipo: "produto", produto_id: id };
        } else if (pedido.categoria !== undefined && String(pedido.categoria).trim()) {
          const achada = acharCategoria(pedido.categoria, lista);
          if (!achada.ok) recusar(`imagens[${i + 1}]: ${achada.erro}`);
          if (achada.categoria.is_visible === false) recusar(`imagens[${i + 1}]: a categoria ${achada.categoria.name} está escondida da loja.`);
          link = { tipo: "categoria", categoria_id: achada.categoria.id, categoria_slug: achada.categoria.slug ?? "" };
        } else if (pedido.link !== undefined && String(pedido.link).trim()) {
          const anteriorUrl = (anterior.link as Record<string, unknown> | undefined)?.url;
          const lido = linkDoBanner(pedido.link, enderecos, typeof anteriorUrl === "string" ? anteriorUrl : null);
          if (!lido.ok) recusar(`imagens[${i + 1}]: ${lido.erro}`);
          link = { tipo: "url", url: lido.link };
        }
        const legenda = typeof pedido.legenda === "string" ? pedido.legenda.trim() : String(anterior.legenda ?? "");
        semContatoNovo(String(anterior.legenda ?? ""), legenda, "em Catálogo > Produtos > Página inicial");
        if (pedido.imagem !== undefined && String(pedido.imagem).trim()) {
          imagens.push({ imagem: await prepararImagemDaSecao(ctx, pedido.imagem, medida, avisos), onde: { item: i } });
        } else if (!anterior.imagem) {
          recusar(`imagens[${i + 1}]: falta a imagem. Se a pessoa anexou o arquivo, chame pedir_imagem e mande o link para ela enviar.`);
        }
        itens.push({ imagem: pedido.imagem !== undefined && String(pedido.imagem).trim() ? "" : String(anterior.imagem ?? ""), legenda, link });
      }
      dados.config.itens = itens;
      mudancas.push("imagens");
    } else if (!deHoje.length) {
      recusar("Mande as imagens (campo imagens): cada uma com a imagem e, se quiser, produto, categoria ou link.");
    } else if (modo === "image_grid" && deHoje.length < cabem) {
      avisos.push(`A arrumação tem ${cabem} lugares e a seção tem ${deHoje.length} imagens: os lugares sem imagem não aparecem na loja.`);
    }
  }

  await montarBloco(ctx, args, modo, dados, imagens, mudancas, avisos);

  /* Agendar, aparelho e fundo: valem para qualquer tipo (loja-estrutura-exibicao.ts). */
  const exibicao = aplicarPedidoDeExibicao(dados.config, args);
  if (!exibicao.ok) recusar(exibicao.erro);
  dados.config = exibicao.config;
  mudancas.push(...exibicao.mudancas);
  if (exibicao.imagemDoFundo !== null) {
    const preparada = await prepararImagemDaSecao(ctx, exibicao.imagemDoFundo, { largura: 1920, altura: 1080, qualidade: 82, pasta: "catalog/sections/pagina-inicial" }, avisos);
    imagens.push({ imagem: preparada, onde: "fundo" });
  }

  return { dados, imagens, mudancas };
}

/* Sobe as imagens prontas e põe os endereços na configuração. */
async function subirImagens(ctx: ContextoDoAssistente, dados: DadosDaSecao, imagens: ImagemPendente[]) {
  const config = { ...dados.config };
  const itens = Array.isArray(config.items) ? (config.items as Array<Record<string, string>>).map((x) => ({ ...x })) : [];
  /* Só objetos viram cópia: a faixa que desliza guarda frases (texto) em itens. */
  const imagensDaSecao = Array.isArray(config.itens) ? (config.itens as Array<Record<string, unknown>>).map((x) => (x && typeof x === "object" ? { ...x } : x)) : [];
  for (const pendente of imagens) {
    const endereco = await guardar(ctx, pendente.imagem);
    if (pendente.onde === "banner") config.image_url = endereco;
    else if (pendente.onde === "fundo") {
      const fundo = config.fundo_da_secao && typeof config.fundo_da_secao === "object" ? (config.fundo_da_secao as Record<string, unknown>) : {};
      config.fundo_da_secao = { ...fundo, tipo: "imagem", imagem: endereco };
    }
    else if (typeof pendente.onde === "object" && "campo" in pendente.onde) config[pendente.onde.campo] = endereco;
    else if (typeof pendente.onde === "object") {
      if (imagensDaSecao[pendente.onde.item]) imagensDaSecao[pendente.onde.item].imagem = endereco;
    } else if (itens[pendente.onde]) itens[pendente.onde].image_url = endereco;
  }
  if (itens.length) config.items = itens;
  if (imagensDaSecao.length) config.itens = imagensDaSecao;
  return { ...dados, config };
}

function previaDaSecao(modo: Modo, d: DadosDaSecao, imagens: ImagemPendente[], categorias: Map<string, Categoria>) {
  const contexto: Contexto = { categorias, produtos: new Map(), visiveis: [], itensDasSecoes: new Map() };
  const conteudo = (() => {
    switch (modo) {
      case "category_products":
      case "subcategory_products":
        return { categoria: categoriaEmPalavras(d.config.target_category_id, d.config.target_category_slug, contexto), mostrar_ver_mais: d.config.show_view_more !== false };
      case "image_banner": {
        const nova = imagens.find((i) => i.onde === "banner");
        return {
          imagem: nova ? `nova: ${nova.imagem.origem}` : d.config.image_url,
          ...(nova ? { conferencia_da_imagem: nova.imagem.conferencia } : {}),
          ...(d.config.formato ? { formato: d.config.formato } : {}),
          ...(d.config.titulo_na_imagem ? { texto_por_cima: d.config.titulo_na_imagem } : {}),
          link: d.config.link_url || "sem link",
          abrir_em_nova_aba: d.config.open_in_new_tab === true,
        };
      }
      case "image_grid":
      case "banner_carousel":
        return {
          ...(modo === "image_grid"
            ? { arrumacao: d.config.layout ?? "3", proporcao: d.config.proporcao ?? "quadrado" }
            : { formato: d.config.formato ?? "largo", passar_sozinho: d.config.automatico !== false }),
          imagens: imagensEmPalavras(d.config.itens, contexto).map((item, i) => {
            const nova = imagens.find((x) => typeof x.onde === "object" && "item" in x.onde && x.onde.item === i);
            return nova ? { ...item, imagem: `nova: ${nova.imagem.origem}`, conferencia_da_imagem: nova.imagem.conferencia } : item;
          }),
        };
      case "featured_products":
        return { quantidade: Number(d.config.quantidade) || 4 };
      case "category_navigator":
        return {
          atalhos: ((d.config.items as Array<Record<string, string>>) ?? []).map((item, i) => {
            const nova = imagens.find((x) => x.onde === i);
            return {
              categoria: categoriaEmPalavras(item.target_category_id, item.target_category_slug, contexto),
              rotulo: item.label || "(nome da categoria)",
              imagem: nova ? `nova: ${nova.imagem.origem}` : item.image_url || "sem foto",
              ...(nova ? { conferencia_da_imagem: nova.imagem.conferencia } : {}),
            };
          }),
        };
      default:
        if (ehWidget(modo)) {
          return {
            bloco: blocoEmPalavras(modo, d.config, (id) => id),
            ...(imagens.length ? { imagens_novas: imagens.map((i) => ({ origem: i.imagem.origem, conferencia: i.imagem.conferencia })) } : {}),
          };
        }
        return {};
    }
  })();
  return { tipo: NOME_DO_TIPO[modo], titulo: d.title, aparencia: aparenciaDe({ id: "", ...d }), ...conteudo, ...exibicaoEmPalavras(d.config) };
}

async function cacheDeCategorias(ctx: ContextoDoAssistente) {
  let lidas: Categoria[] | null = null;
  return async () => {
    lidas ??= await lerCategorias(ctx);
    return lidas;
  };
}

const PADRAO_DA_GRADE = {
  title: "Todos os Produtos",
  background_color: "#ffffff",
  text_color: "#000000",
  layout_width: "full",
  title_align: "left",
  title_size: "lg",
  section_type: "all_products",
  config: { mode: "all_products", show_view_more: false },
  active: true,
} as const;

/** Cria a seção "Todos os produtos" quando a ordem pede algo abaixo da grade. */
async function criarGrade(ctx: ContextoDoAssistente, posicao: number): Promise<string> {
  const { data, error } = await ctx.supabase
    .from("catalog_sections")
    .insert({ ...PADRAO_DA_GRADE, placement: "above", user_id: perfilDaLoja(ctx), position: posicao })
    .select("id")
    .single();
  if (error?.code === "23505") recusar("A seção Todos os produtos acabou de ser criada por outra pessoa. Veja a loja de novo (ver_estrutura_da_loja) e repita.");
  if (error || !data) falha("criar a seção Todos os produtos", error);
  return (data as { id: string }).id;
}

/**
 * Grava a ordem da página: posições 0, 1, 2... e acima/abaixo de cada seção
 * ligada. `trocas` traduz os marcadores (a seção nova, a grade criada agora).
 */
async function gravarOrdem(ctx: ContextoDoAssistente, ordem: string[], trocas: Record<string, string>) {
  const final = ordem.map((id) => trocas[id] ?? id).filter((id) => id !== PRODUTOS_INVENTADOS);
  const todas = await lerSecoes(ctx);
  const grade = todas.find((s) => s.active !== false && lerSecao(s).modo === "all_products");
  const placementAntes = new Map(todas.map((s) => [s.id, s.placement ?? "above"]));
  const posicoes = posicoesDasSecoes(final, todas);
  const comGrade = grade ? final : [...final, PRODUTOS_INVENTADOS];
  const mudancas = posicoes
    .map((p) => {
      const placement = final.includes(p.id) ? placementNaOrdem(comGrade, p.id, grade?.id ?? null) : null;
      const linha: Record<string, unknown> = {};
      if (p.mudou) linha.position = p.posicao;
      if (placement && placement !== placementAntes.get(p.id)) linha.placement = placement;
      return { id: p.id, linha };
    })
    .filter((m) => Object.keys(m.linha).length);
  const resultados = await Promise.all(
    mudancas.map((m) => ctx.supabase.from("catalog_sections").update(m.linha).eq("id", m.id).eq("user_id", perfilDaLoja(ctx)))
  );
  const erro = resultados.find((r) => r.error)?.error;
  if (erro) falha("gravar a ordem das seções", erro);
}

function lugarPedido(args: Record<string, unknown>): { lugar?: Lugar; posicao?: number } | null {
  if (args.lugar === undefined && args.posicao === undefined) return null;
  return { lugar: args.lugar as Lugar | undefined, posicao: args.posicao as number | undefined };
}

function descricaoDaOrdem(ordem: string[], nomes: Map<string, string>) {
  return ordem.map((id, i) => `${i + 1}. ${id === PRODUTOS_INVENTADOS ? "Todos os produtos (grade)" : (nomes.get(id) ?? id)}`);
}

/* ============================================================ criar_secao */

type CargaDaCriacao = {
  modo: Modo;
  dados: DadosDaSecao;
  imagens: ImagemPendente[];
  produtos: string[];
  ordem: string[];
  criarGrade: boolean;
  ativa: boolean;
  tema: string;
  homeEditada?: boolean;
};

const NOVA = "__nova__";

export const criarSecao: FerramentaDeEscrita<CargaDaCriacao> = {
  nome: "criar_secao",
  titulo: "Criar uma seção na página inicial da loja",
  descricao:
    `Cria um bloco na página inicial da loja. Tipos: ${TIPOS_DE_SECAO.join(", ")}. "produtos escolhidos" recebe produto_ids (até ${LIMITE_DE_PRODUTOS_POR_SECAO}); "produtos de uma categoria" mostra a categoria e as de dentro dela; "produtos de uma subcategoria" só aquela; "banner de imagem" precisa de imagem (e link opcional da própria loja; com formato largo, panoramico ou cinema aceita texto por cima); "atalhos de categorias" recebe atalhos; "imagens lado a lado" recebe arrumacao e imagens (cada uma com produto, categoria ou link); "carrossel de banners" recebe imagens (até ${LIMITE_DO_CARROSSEL}); "primeiros produtos" mostra os primeiros da loja (quantidade); "categorias em círculos" mostra as categorias principais sozinha. "todos os produtos" é a grade da loja e só existe uma. Os blocos novos (oferta relâmpago, cupom em destaque, peça até tal hora, próxima live, lista automática, kits e combos, vistos recentemente, vídeo, texto com imagem, perguntas frequentes, onde estamos, faixa que desliza, imagem com pontos, antes e depois) recebem os ajustes no campo bloco. A pessoa também pode montar a página vendo a loja de verdade no editor do painel (Catálogo > Produtos > Página inicial). Diga onde fica com lugar ou posicao. Qualquer tipo aceita agendamento (entra e sai sozinha), dispositivo (só celular ou só computador) e fundo (cor, gradiente ou imagem, e espaço). Até ${LIMITE_DE_SECOES} seções. Veja antes com ver_estrutura_da_loja.`,
  campos: {
    tipo: { tipo: "opcao", descricao: "O tipo do bloco.", opcoes: TIPOS_DE_SECAO, obrigatorio: true },
    ...CAMPOS_DA_APARENCIA,
    ...CAMPOS_DO_LUGAR,
    produto_ids: { tipo: "textos", descricao: `Produtos escolhidos: os ids (de buscar_produtos), na ordem em que aparecem. Até ${LIMITE_DE_PRODUTOS_POR_SECAO}.`, maxItens: LIMITE_DE_PRODUTOS_POR_SECAO, max: 40 },
    ...CAMPOS_DO_CONTEUDO,
    ...CAMPO_DO_BLOCO,
    ...CAMPOS_DA_EXIBICAO,
    ativa: { tipo: "booleano", descricao: "Já aparece na loja. Padrão: sim." },
  },
  tipo: "escrita",
  abertoAoMundo: true,
  /* Imagem de fora vira parte da cara pública da loja: como a logo, passa
     pela aprovação da pessoa. */
  sensivel: (args) =>
    args.imagem !== undefined ||
    (Array.isArray(args.atalhos) && (args.atalhos as Array<Record<string, unknown>>).some((a) => a.imagem !== undefined)) ||
    (Array.isArray(args.imagens) && (args.imagens as Array<Record<string, unknown>>).some((a) => a.imagem !== undefined)) ||
    blocoTemImagem(args) ||
    fundoTemImagem(args),
  permissoes: [ESCREVER_NA_LOJA],
  async preparar(ctx, args) {
    exigir(ctx, ESCREVER_NA_LOJA, "mexer nas seções da loja");
    const modo = modoPeloNome(args.tipo);
    if (!modo) recusar(`Tipo desconhecido. Use um destes: ${TIPOS_DE_SECAO.join(", ")}.`);
    if (args.produto_ids !== undefined && modo !== "manual_products") recusar('produto_ids só vale para o tipo "produtos escolhidos".');

    const [secoes, { tema, homeEditada }] = await Promise.all([lerSecoes(ctx), temaDaLoja(ctx)]);
    if (secoes.length >= LIMITE_DE_SECOES) recusar(`A loja já tem ${secoes.length} seções (o limite é ${LIMITE_DE_SECOES}). Remova ou reaproveite uma antes.`);
    const lidas = secoes.map((s) => ({ secao: s, ...lerSecao(s) }));
    const gradeAtiva = lidas.find((l) => l.modo === "all_products" && l.secao.active !== false) ?? null;
    if (modo === "all_products") {
      const qualquer = gradeAtiva ?? lidas.find((l) => l.modo === "all_products");
      if (qualquer) {
        recusar(
          qualquer.secao.active === false
            ? `A seção Todos os produtos já existe, desligada (id ${qualquer.secao.id}). Para mostrá-la, use alterar_secao com ativa=true.`
            : `A seção Todos os produtos já existe (id ${qualquer.secao.id}); só pode haver uma. Para mudar o lugar dela, use reorganizar_secoes ou alterar_secao.`
        );
      }
    }

    const avisos: string[] = [];
    const categorias = await cacheDeCategorias(ctx);
    const base: DadosDaSecao = {
      title: TITULO_PADRAO[modo],
      background_color: "#ffffff",
      text_color: "#000000",
      layout_width: "full",
      title_align: "left",
      title_size: "lg",
      config:
        modo === "image_banner"
          ? { mode: modo, image_url: "", link_url: "", open_in_new_tab: false }
          : modo === "category_navigator"
            ? { mode: modo, items: [] }
            : modo === "all_products"
              ? { mode: modo, show_view_more: false }
              : modo === "image_grid"
                ? { mode: modo, layout: "3", proporcao: "quadrado", espaco: "medio", cantos: "suaves", celular: "deslizar", margem: "normal", itens: [] }
                : modo === "banner_carousel"
                  ? { mode: modo, formato: "largo", automatico: true, intervalo: 6, margem: "normal", itens: [] }
                  : modo === "featured_products"
                    ? { mode: modo, quantidade: 4 }
                    : modo === "category_circles"
                      ? { mode: modo }
                      : ehWidget(modo)
                        ? widgetNovo(modo).config
                        : { mode: modo, show_view_more: true },
    };
    const { dados, imagens } = await montarSecao(ctx, { ...args, titulo: args.titulo ?? TITULO_PADRAO[modo] }, modo, base, avisos, categorias);

    let produtos: string[] = [];
    if (modo === "manual_products") {
      produtos = idsDeProdutos(args.produto_ids);
      if (produtos.length) {
        const achados = await conferirProdutos(ctx, produtos);
        const inativos = produtos.filter((id) => achados.get(id)?.active === false);
        if (inativos.length) avisos.push(`${contagem(inativos.length, "produto está inativo", "produtos estão inativos")} e não ${inativos.length === 1 ? "aparece" : "aparecem"} na loja.`);
      } else {
        avisos.push("A seção nasce sem produtos: use definir_produtos_da_secao para colocar.");
      }
    }

    const ativa = args.ativa !== false;
    let ordem = ordemDaPagina(lidas.map((l) => ({ id: l.secao.id, modo: l.modo, ativa: l.secao.active !== false })));
    let precisaDaGrade = false;
    if (ativa) {
      const pedido = lugarPedido(args) ?? { lugar: "acima dos produtos" as Lugar };
      const encaixe = encaixar(ordem, NOVA, modo === "all_products" ? { posicao: pedido.posicao ?? (pedido.lugar === "no topo" ? 1 : 999) } : pedido, gradeAtiva?.secao.id ?? null);
      ordem = modo === "all_products" ? encaixe.ordem.filter((id) => id !== PRODUTOS_INVENTADOS) : encaixe.ordem;
      precisaDaGrade = modo !== "all_products" && encaixe.precisaDaGradeDeVerdade;
    } else if (lugarPedido(args)) {
      avisos.push("A seção nasce desligada; o lugar é escolhido quando ela for ligada (alterar_secao com ativa=true e lugar).");
    }
    if (precisaDaGrade) avisos.push('Para ficar abaixo dos produtos, a seção "Todos os produtos" também será criada (hoje a loja monta a grade sozinha, sempre no fim).');
    if (!templateMostraSecoes(tema, homeEditada)) avisos.push(AVISO_DO_VENDOR);

    const nomes = new Map(lidas.map((l) => [l.secao.id, l.titulo]));
    nomes.set(NOVA, `${dados.title} (nova)`);
    return {
      previa: {
        o_que_vai_acontecer: `Uma seção nova (${NOME_DO_TIPO[modo]}) será criada na página inicial da loja${ativa ? "" : ", desligada"}.`,
        secao: {
          ...previaDaSecao(modo, dados, imagens, new Map((await categorias()).map((c) => [c.id, c]))),
          ...(modo === "manual_products" ? { produtos: produtos.length } : {}),
        },
        ...(ativa ? { pagina_inicial_depois: descricaoDaOrdem(ordem, nomes) } : {}),
      },
      resumo: `Prévia da seção nova "${dados.title}"`,
      avisos,
      carga: { modo, dados, imagens, produtos, ordem, criarGrade: precisaDaGrade, ativa, tema, homeEditada },
    };
  },
  async gravar(ctx, carga) {
    const dados = await subirImagens(ctx, carga.dados, carga.imagens);
    const dono = perfilDaLoja(ctx);
    const { data, error } = await ctx.supabase
      .from("catalog_sections")
      .insert({
        user_id: dono,
        title: dados.title,
        background_color: dados.background_color,
        text_color: dados.text_color,
        placement: "above",
        layout_width: dados.layout_width,
        title_align: dados.title_align,
        title_size: dados.title_size,
        section_type: tipoGravado(carga.modo),
        config: dados.config,
        position: 9999,
        active: carga.ativa,
      })
      .select("id")
      .single();
    if (error?.code === "23505") recusar("A seção Todos os produtos já existe; só pode haver uma.");
    if (error || !data) falha("criar a seção", error);
    const id = (data as { id: string }).id;

    try {
      if (carga.produtos.length) {
        const { error: erroDosProdutos } = await ctx.supabase
          .from("catalog_section_products")
          .insert(carga.produtos.map((product_id, position) => ({ section_id: id, product_id, position })));
        if (erroDosProdutos) falha("pôr os produtos na seção", erroDosProdutos);
      }
      const trocas: Record<string, string> = { [NOVA]: id };
      if (carga.criarGrade) trocas[PRODUTOS_INVENTADOS] = await criarGrade(ctx, 9999);
      if (carga.ativa) await gravarOrdem(ctx, carga.ordem, trocas);
    } catch (erro) {
      /* Não deixa meia seção na loja. */
      await ctx.supabase.from("catalog_sections").delete().eq("id", id).eq("user_id", dono);
      throw erro;
    }
    const aviso = await avisar(ctx);
    return {
      dados: {
        criada: true,
        secao_id: id,
        tipo: NOME_DO_TIPO[carga.modo],
        titulo: dados.title,
        ...(carga.modo === "manual_products" ? { produtos: carga.produtos.length } : {}),
        ...(carga.criarGrade ? { observacao_da_grade: 'A seção "Todos os produtos" também foi criada.' } : {}),
        ...(templateMostraSecoes(carga.tema, carga.homeEditada) ? {} : { aviso: AVISO_DO_VENDOR }),
        para_desfazer: `remover_secao com secao_id ${id}`,
        ...aviso,
      },
      resumo: `Criou a seção "${dados.title}" (${NOME_DO_TIPO[carga.modo]}) na loja`,
    };
  },
};

/* ========================================================== alterar_secao */

/** A seção de hoje no formato do pedido (título limpo, configuração juntada). */
function dadosDe(secao: SecaoBruta, lida: { titulo: string; config: Record<string, unknown> }): DadosDaSecao {
  return {
    title: lida.titulo,
    background_color: secao.background_color ?? "#ffffff",
    text_color: secao.text_color ?? "#000000",
    layout_width: secao.layout_width === "contained" ? "contained" : "full",
    title_align: (["left", "center", "right"].includes(String(secao.title_align)) ? secao.title_align : "center") as DadosDaSecao["title_align"],
    title_size: (["sm", "md", "lg", "xl"].includes(String(secao.title_size)) ? secao.title_size : "lg") as DadosDaSecao["title_size"],
    config: lida.config,
  };
}

async function acharSecao(ctx: ContextoDoAssistente, id: unknown) {
  const secoes = await lerSecoes(ctx);
  const secao = secoes.find((s) => s.id === String(id ?? "").toLowerCase());
  if (!secao) recusar("Seção não encontrada nesta loja. Use ver_estrutura_da_loja para ver os ids.");
  return { secao, secoes, lida: lerSecao(secao) };
}

type CargaDaAlteracao = {
  id: string;
  modo: Modo;
  dados: DadosDaSecao;
  imagens: ImagemPendente[];
  ativa: boolean;
  ordem: string[] | null;
  criarGrade: boolean;
  antes: Record<string, unknown>;
  tema: string;
  homeEditada?: boolean;
};

export const alterarSecao: FerramentaDeEscrita<CargaDaAlteracao> = {
  nome: "alterar_secao",
  titulo: "Alterar uma seção da página inicial",
  descricao:
    "Muda uma seção da página inicial: título, cores, largura, alinhamento, tamanho do título, categoria, imagem, formato, texto e link do banner, imagens (imagens lado a lado e carrossel; a lista mandada substitui a de hoje), arrumação, atalhos (a lista mandada substitui a de hoje), quantidade, ver mais, liga/desliga (ativa) e o lugar na página (lugar ou posicao). Mande só o que muda. Os ajustes dos blocos novos (oferta, cupom, vídeo, perguntas...) vão no campo bloco. Para qualquer tipo: agendamento (entra e sai sozinha, hora de Brasília), dispositivo (só no celular ou só no computador) e fundo (cor, gradiente ou imagem, e espaço acima e abaixo). O tipo não muda: para outro tipo, crie uma seção nova. Para tirar o link do banner, apagar=[\"link\"]. Os produtos de uma seção de produtos escolhidos mudam em definir_produtos_da_secao.",
  campos: {
    secao_id: { tipo: "id", descricao: "Id da seção (de ver_estrutura_da_loja).", obrigatorio: true },
    ...CAMPOS_DA_APARENCIA,
    ...CAMPOS_DO_LUGAR,
    ...CAMPOS_DO_CONTEUDO,
    ...CAMPO_DO_BLOCO,
    ...CAMPOS_DA_EXIBICAO,
    ativa: { tipo: "booleano", descricao: "true mostra a seção na loja; false esconde (fica guardada)." },
    apagar: { tipo: "textos", descricao: 'O que tirar: "link" (do banner).', maxItens: 1, max: 10 },
  },
  tipo: "escrita",
  sobrescreve: true,
  abertoAoMundo: true,
  sensivel: (args) =>
    args.imagem !== undefined ||
    (Array.isArray(args.atalhos) && (args.atalhos as Array<Record<string, unknown>>).some((a) => a.imagem !== undefined)) ||
    (Array.isArray(args.imagens) && (args.imagens as Array<Record<string, unknown>>).some((a) => a.imagem !== undefined)) ||
    blocoTemImagem(args) ||
    fundoTemImagem(args),
  permissoes: [ESCREVER_NA_LOJA],
  async preparar(ctx, args) {
    exigir(ctx, ESCREVER_NA_LOJA, "mexer nas seções da loja");
    const apagar = (args.apagar as string[] | undefined) ?? [];
    for (const nome of apagar) if (nome !== "link") recusar(`Não dá para apagar "${nome}". Só "link" (do banner).`);
    const { secao, secoes, lida } = await acharSecao(ctx, args.secao_id);
    const { tema, homeEditada } = await temaDaLoja(ctx);
    const avisos: string[] = [];
    const categorias = await cacheDeCategorias(ctx);
    const base = dadosDe(secao, lida);
    const { dados, imagens, mudancas } = await montarSecao(ctx, args, lida.modo, base, avisos, categorias);

    const lidas = secoes.map((s) => ({ secao: s, ...lerSecao(s) }));
    const estavaAtiva = secao.active !== false;
    const ativa = args.ativa !== undefined ? args.ativa === true : estavaAtiva;
    if (ativa !== estavaAtiva) mudancas.push(ativa ? "ligada" : "desligada");
    if (lida.modo === "all_products" && ativa && !estavaAtiva) {
      const outra = lidas.find((l) => l.modo === "all_products" && l.secao.active !== false && l.secao.id !== secao.id);
      if (outra) recusar(`Já existe outra seção Todos os produtos ligada (id ${outra.secao.id}); só pode haver uma.`);
    }
    if (lida.modo === "all_products" && !ativa && estavaAtiva) avisos.push("Sem esta seção, a loja monta a grade dos produtos sozinha, sempre no fim da página.");

    const pedido = lugarPedido(args);
    if (pedido && !ativa) recusar("Uma seção desligada não tem lugar na página. Ligue (ativa=true) junto com o lugar.");
    let ordem: string[] | null = null;
    let criarGradeAgora = false;
    if (pedido || ativa !== estavaAtiva) {
      const ordemDeHoje = ordemDaPagina(
        lidas.map((l) => ({ id: l.secao.id, modo: l.modo, ativa: l.secao.id === secao.id ? ativa : l.secao.active !== false }))
      );
      const grade = lidas.find((l) => l.modo === "all_products" && (l.secao.id === secao.id ? ativa : l.secao.active !== false));
      if (pedido) {
        const encaixe = encaixar(ordemDeHoje, secao.id, pedido, grade?.secao.id ?? null);
        ordem = encaixe.ordem;
        criarGradeAgora = encaixe.precisaDaGradeDeVerdade;
        if (lida.modo === "all_products") {
          ordem = ordem.filter((id) => id !== PRODUTOS_INVENTADOS);
          criarGradeAgora = false;
        }
        mudancas.push("lugar na página");
      } else {
        ordem = ordemDeHoje;
      }
    }
    if (criarGradeAgora) avisos.push('Para ficar abaixo dos produtos, a seção "Todos os produtos" também será criada.');
    if (!mudancas.length) recusar("Nada para mudar: mande ao menos um campo com valor novo.");
    if (!templateMostraSecoes(tema, homeEditada)) avisos.push(AVISO_DO_VENDOR);

    const mapa = new Map((await categorias()).map((c) => [c.id, c]));
    const nomes = new Map(lidas.map((l) => [l.secao.id, l.titulo]));
    nomes.set(secao.id, dados.title);
    const antes = previaDaSecao(lida.modo, base, [], mapa);
    return {
      previa: {
        o_que_vai_acontecer: `A seção "${lida.titulo}" será alterada: ${mudancas.join(", ")}.`,
        antes: { ...antes, ativa: estavaAtiva },
        depois: { ...previaDaSecao(lida.modo, dados, imagens, mapa), ativa },
        ...(ordem ? { pagina_inicial_depois: descricaoDaOrdem(ordem, nomes) } : {}),
      },
      resumo: `Prévia da alteração da seção "${lida.titulo}"`,
      avisos,
      carga: { id: secao.id, modo: lida.modo, dados, imagens, ativa, ordem, criarGrade: criarGradeAgora, antes, tema, homeEditada },
    };
  },
  async gravar(ctx, carga) {
    const dados = await subirImagens(ctx, carga.dados, carga.imagens);
    const { data, error } = await ctx.supabase
      .from("catalog_sections")
      .update({
        title: dados.title,
        background_color: dados.background_color,
        text_color: dados.text_color,
        layout_width: dados.layout_width,
        title_align: dados.title_align,
        title_size: dados.title_size,
        section_type: tipoGravado(carga.modo),
        config: dados.config,
        active: carga.ativa,
        updated_at: new Date().toISOString(),
      })
      .eq("id", carga.id)
      .eq("user_id", perfilDaLoja(ctx))
      .select("id");
    if (error?.code === "23505") recusar("Já existe outra seção Todos os produtos ligada; só pode haver uma.");
    if (error) falha("alterar a seção", error);
    if (!(data ?? []).length) recusar("A seção não existe mais. Veja a loja de novo com ver_estrutura_da_loja.");
    if (carga.ordem) {
      const trocas: Record<string, string> = {};
      if (carga.criarGrade) trocas[PRODUTOS_INVENTADOS] = await criarGrade(ctx, 9999);
      await gravarOrdem(ctx, carga.ordem, trocas);
    }
    const aviso = await avisar(ctx);
    return {
      dados: {
        alterada: true,
        secao_id: carga.id,
        titulo: dados.title,
        ativa: carga.ativa,
        como_era: carga.antes,
        ...(templateMostraSecoes(carga.tema, carga.homeEditada) ? {} : { aviso: AVISO_DO_VENDOR }),
        ...aviso,
      },
      resumo: `Alterou a seção "${dados.title}" da loja`,
    };
  },
};

/* ========================================================== remover_secao */

type CargaDaRemocao = { id: string; titulo: string; tipo: string };

export const removerSecao: FerramentaDeEscrita<CargaDaRemocao> = {
  nome: "remover_secao",
  titulo: "Remover uma seção da página inicial",
  descricao:
    "Apaga uma seção da página inicial da loja (com a lista de produtos dela, se for de produtos escolhidos). Os produtos e as categorias continuam existindo. Não tem volta: para só esconder, use alterar_secao com ativa=false. A seção Todos os produtos não é apagada (a tela também não deixa): desligue-a.",
  campos: { secao_id: { tipo: "id", descricao: "Id da seção (de ver_estrutura_da_loja).", obrigatorio: true } },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: true,
  permissoes: [ESCREVER_NA_LOJA],
  async preparar(ctx, args) {
    exigir(ctx, ESCREVER_NA_LOJA, "mexer nas seções da loja");
    const { secao, lida } = await acharSecao(ctx, args.secao_id);
    if (lida.modo === "all_products") recusar("A seção Todos os produtos não pode ser apagada (é a grade da loja). Para tirá-la do lugar, use reorganizar_secoes; para esconder, alterar_secao com ativa=false.");
    const categorias = await lerCategorias(ctx).catch(() => [] as Categoria[]);
    const itens = lida.modo === "manual_products" ? await lerProdutosDasSecoes(ctx, [secao.id]) : [];
    return {
      previa: {
        o_que_vai_acontecer: `A seção "${lida.titulo}" será apagada da loja. Não tem volta.`,
        secao: {
          ...previaDaSecao(lida.modo, dadosDe(secao, lida), [], new Map(categorias.map((c) => [c.id, c]))),
          ativa: secao.active !== false,
          ...(lida.modo === "manual_products" ? { produtos_na_lista: itens.length } : {}),
        },
      },
      resumo: `Prévia: remover a seção "${lida.titulo}"`,
      avisos: itens.length ? [`A lista de ${contagem(itens.length, "produto", "produtos")} da seção some junto (os produtos continuam na loja).`] : [],
      carga: { id: secao.id, titulo: lida.titulo, tipo: NOME_DO_TIPO[lida.modo] },
    };
  },
  async gravar(ctx, carga) {
    const { data, error } = await ctx.supabase
      .from("catalog_sections")
      .delete()
      .eq("id", carga.id)
      .eq("user_id", perfilDaLoja(ctx))
      .select("id");
    if (error) falha("remover a seção", error);
    if (!(data ?? []).length) recusar("A seção não existe mais.");
    const aviso = await avisar(ctx);
    return { dados: { removida: true, titulo: carga.titulo, tipo: carga.tipo, ...aviso }, resumo: `Removeu a seção "${carga.titulo}" da loja` };
  },
};

/* ===================================================== reorganizar_secoes */

type CargaDaOrdem = { ordem: string[]; criarGrade: boolean };

export const reorganizarSecoes: FerramentaDeEscrita<CargaDaOrdem> = {
  nome: "reorganizar_secoes",
  titulo: "Reorganizar as seções da página inicial",
  descricao:
    'Muda a ordem dos blocos da página inicial da loja. Mande em ordem os ids de TODAS as seções ligadas (de ver_estrutura_da_loja), de cima para baixo. A grade de todos os produtos entra pelo id dela ou pela palavra "produtos". As desligadas vão para o fim, guardadas.',
  campos: {
    ordem: { tipo: "textos", descricao: 'Ids das seções ligadas, de cima para baixo; "produtos" = a grade dos produtos.', minItens: 1, maxItens: LIMITE_DE_SECOES + 1, max: 40 },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: [ESCREVER_NA_LOJA],
  async preparar(ctx, args) {
    exigir(ctx, ESCREVER_NA_LOJA, "mexer nas seções da loja");
    const secoes = await lerSecoes(ctx);
    const lidas = secoes.map((s) => ({ secao: s, ...lerSecao(s) }));
    const ativas = lidas.filter((l) => l.secao.active !== false);
    const grade = ativas.find((l) => l.modo === "all_products") ?? null;
    const conferida = conferirOrdemPedida(args.ordem as string[], ativas.map((l) => l.secao.id), grade?.secao.id ?? null);
    if (!conferida.ok) recusar(conferida.erro);
    const inventada = conferida.ordem.indexOf(PRODUTOS_INVENTADOS);
    const criarGradeAgora = inventada >= 0 && inventada !== conferida.ordem.length - 1;
    const nomes = new Map(lidas.map((l) => [l.secao.id, l.titulo]));
    const { tema } = await temaDaLoja(ctx);
    const avisos = criarGradeAgora ? ['A seção "Todos os produtos" será criada para a grade ficar no lugar pedido (hoje a loja a monta sozinha, no fim).'] : [];
    if (!templateMostraSecoes(tema)) avisos.push(AVISO_DO_VENDOR);
    return {
      previa: {
        o_que_vai_acontecer: "A página inicial da loja vai ficar nesta ordem.",
        antes: descricaoDaOrdem(ordemDaPagina(lidas.map((l) => ({ id: l.secao.id, modo: l.modo, ativa: l.secao.active !== false }))), nomes),
        depois: descricaoDaOrdem(conferida.ordem, nomes),
      },
      resumo: "Prévia da nova ordem das seções da loja",
      avisos,
      carga: { ordem: conferida.ordem, criarGrade: criarGradeAgora },
    };
  },
  async gravar(ctx, carga) {
    const trocas: Record<string, string> = {};
    if (carga.criarGrade) trocas[PRODUTOS_INVENTADOS] = await criarGrade(ctx, 9999);
    await gravarOrdem(ctx, carga.ordem, trocas);
    const aviso = await avisar(ctx);
    return {
      dados: { reorganizada: true, ordem: carga.ordem.map((id) => trocas[id] ?? (id === PRODUTOS_INVENTADOS ? "produtos" : id)), ...aviso },
      resumo: "Reorganizou as seções da página inicial da loja",
    };
  },
};

/* ================================================ definir_produtos_da_secao */

type CargaDosProdutosDaSecao = { id: string; titulo: string; antes: string[]; depois: string[] };

export const definirProdutosDaSecao: FerramentaDeEscrita<CargaDosProdutosDaSecao> = {
  nome: "definir_produtos_da_secao",
  titulo: "Escolher os produtos de uma seção",
  descricao: `Muda os produtos de uma seção de "produtos escolhidos": acao=adicionar (no fim, ou na posicao, 1 = primeiro), remover, ou trocar (a lista mandada vira a lista inteira, na ordem mandada; lista vazia esvazia). Até ${LIMITE_DE_PRODUTOS_POR_SECAO} produtos por seção. A ordem dos que ficam é mantida.`,
  campos: {
    secao_id: { tipo: "id", descricao: "Id da seção (de ver_estrutura_da_loja).", obrigatorio: true },
    acao: { tipo: "opcao", descricao: "adicionar (padrão), remover ou trocar.", opcoes: ["adicionar", "remover", "trocar"] },
    produto_ids: { tipo: "textos", descricao: "Ids dos produtos (de buscar_produtos).", maxItens: LIMITE_DE_PRODUTOS_POR_SECAO, max: 40, obrigatorio: true },
    posicao: { tipo: "inteiro", descricao: "Ao adicionar: em que lugar entram (1 = primeiro). Padrão: no fim.", min: 1, max: LIMITE_DE_PRODUTOS_POR_SECAO },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: [ESCREVER_NA_LOJA],
  async preparar(ctx, args) {
    exigir(ctx, ESCREVER_NA_LOJA, "mexer nas seções da loja");
    const { secao, lida } = await acharSecao(ctx, args.secao_id);
    if (lida.modo !== "manual_products") {
      recusar(`A seção "${lida.titulo}" é de ${NOME_DO_TIPO[lida.modo]}: os produtos dela vêm sozinhos. Só seções de produtos escolhidos têm lista própria.`);
    }
    const acao = (args.acao as "adicionar" | "remover" | "trocar" | undefined) ?? "adicionar";
    const ids = idsDeProdutos(args.produto_ids);
    const antes = (await lerProdutosDasSecoes(ctx, [secao.id])).map((i) => i.product_id);
    const nova = novaListaDaSecao(antes, { acao, ids, posicao: args.posicao as number | undefined });
    if (!nova.ok) recusar(nova.erro);
    const avisos: string[] = [];
    const conferir = acao === "remover" ? [] : ids.filter((id) => !antes.includes(id) || acao === "trocar");
    const achados = conferir.length ? await conferirProdutos(ctx, conferir) : new Map<string, Produto>();
    const nomes = await produtosPorIds(ctx, [...new Set([...antes, ...nova.lista])]);
    for (const [id, p] of achados) nomes.set(id, p);
    const inativos = nova.lista.filter((id) => nomes.get(id)?.active === false);
    if (inativos.length) avisos.push(`${contagem(inativos.length, "produto está inativo", "produtos estão inativos")} e não ${inativos.length === 1 ? "aparece" : "aparecem"} na loja.`);
    if (nova.ignorados.length) avisos.push(`${contagem(nova.ignorados.length, "produto foi ignorado", "produtos foram ignorados")} (${acao === "remover" ? "não estava" : "já estava"} na seção).`);
    if (JSON.stringify(antes) === JSON.stringify(nova.lista)) recusar("A lista da seção já está assim; nada para mudar.");
    const nome = (id: string) => nomes.get(id)?.name ?? id;
    return {
      previa: {
        o_que_vai_acontecer: `A seção "${lida.titulo}" vai ficar com ${contagem(nova.lista.length, "produto", "produtos")}.`,
        entram: nova.lista.filter((id) => !antes.includes(id)).map(nome),
        saem: antes.filter((id) => !nova.lista.includes(id)).map(nome),
        lista_final: nova.lista.slice(0, 20).map(nome),
        ...(nova.lista.length > 20 ? { e_mais: nova.lista.length - 20 } : {}),
      },
      resumo: `Prévia dos produtos da seção "${lida.titulo}"`,
      avisos,
      carga: { id: secao.id, titulo: lida.titulo, antes, depois: nova.lista },
    };
  },
  async gravar(ctx, carga) {
    /* A trava do banco confere só a seção: o produto é conferido aqui de novo. */
    const novos = carga.depois.filter((id) => !carga.antes.includes(id));
    if (novos.length) await conferirProdutos(ctx, novos);
    const saem = carga.antes.filter((id) => !carga.depois.includes(id));
    if (saem.length) {
      const { error } = await ctx.supabase.from("catalog_section_products").delete().eq("section_id", carga.id).in("product_id", saem);
      if (error) falha("tirar produtos da seção", error);
    }
    const atuais = await lerProdutosDasSecoes(ctx, [carga.id]);
    const gravada = new Map(atuais.map((i) => [i.product_id, i.position]));
    const mudam = carga.depois.map((id, posicao) => ({ id, posicao })).filter((p) => gravada.has(p.id) && gravada.get(p.id) !== p.posicao);
    const resultados = await Promise.all(
      mudam.map((p) => ctx.supabase.from("catalog_section_products").update({ position: p.posicao }).eq("section_id", carga.id).eq("product_id", p.id))
    );
    const erro = resultados.find((r) => r.error)?.error;
    if (erro) falha("reordenar os produtos da seção", erro);
    const entram = carga.depois.map((id, posicao) => ({ id, posicao })).filter((p) => !gravada.has(p.id));
    if (entram.length) {
      const { error } = await ctx.supabase
        .from("catalog_section_products")
        .insert(entram.map((p) => ({ section_id: carga.id, product_id: p.id, position: p.posicao })));
      if (error?.code === "23505") recusar("Outra pessoa mexeu nesta seção agora. Veja de novo e repita.");
      if (error) falha("pôr produtos na seção", error);
    }
    const aviso = await avisar(ctx);
    return {
      dados: { alterada: true, secao_id: carga.id, produtos: carga.depois.length, entraram: entram.length, sairam: saem.length, lista_de_antes: carga.antes, ...aviso },
      resumo: `Mudou os produtos da seção "${carga.titulo}" (${contagem(carga.depois.length, "produto", "produtos")})`,
    };
  },
};

/* ============================================================= categorias */

/* O serviço responde em inglês técnico e sem acento: aqui vira recado. */
async function pelaRotaDasCategorias<T>(oQue: string, chamada: () => Promise<T>): Promise<T> {
  try {
    return await chamada();
  } catch (erro) {
    if (!isApiV1Error(erro)) throw erro;
    if (erro.status === 403) recusar("Esta pessoa não tem permissão para mexer nas categorias. Quem libera é o dono da empresa, no painel, em Funcionários.");
    if (erro.status === 404) recusar("Categoria não encontrada nesta empresa. Use ver_estrutura_da_loja para ver as categorias.");
    if (/duplicate key|categories_user_id_slug_key/i.test(erro.message)) recusar("Já existe outra categoria com esse endereço (slug). Escolha outro endereço.");
    if (erro.code === "invalid_category_hierarchy") recusar("Essa troca de categoria-mãe criaria um círculo (uma categoria dentro dela mesma). Escolha outra.");
    if (erro.code === "invalid_parent_category") recusar("A categoria-mãe escolhida não serve: ela precisa ser desta empresa e diferente da própria categoria.");
    if (erro.status === 400 && /row-level security|permission/i.test(erro.message)) {
      recusar("O banco não deixou gravar: falta a permissão de categorias para esta pessoa. Quem libera é o dono, em Funcionários.");
    }
    falha(oQue, erro);
  }
}

const CAMPOS_DA_REGRA: Campos = {
  margem_alvo: { tipo: "numero", descricao: "Regra de preço: margem de lucro alvo, em %.", min: 0, max: 1000 },
  margem_minima: { tipo: "numero", descricao: "Regra de preço: margem mínima, em %. Não pode passar da alvo.", min: 0, max: 1000 },
  arredondamento: { tipo: "opcao", descricao: "Regra de preço: sem, inteiro (inteiro mais próximo) ou centavos (termina com os centavos escolhidos).", opcoes: Object.keys(ARREDONDAMENTOS) },
  centavos: { tipo: "inteiro", descricao: "Com arredondamento centavos: os centavos do fim do preço (ex.: 90 para R$ 19,90).", min: 0, max: 99 },
};

function pedidoDeRegra(args: Record<string, unknown>) {
  return {
    margem_alvo: args.margem_alvo as number | undefined,
    margem_minima: args.margem_minima as number | undefined,
    arredondamento: args.arredondamento as keyof typeof ARREDONDAMENTOS | undefined,
    centavos: args.centavos as number | undefined,
  };
}

function enderecoPedido(valor: unknown): string {
  const slug = slugDaCategoria(String(valor ?? "")).replace(/_/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
  if (!slug) recusar("Endereço (slug) inválido: use letras, números e hífen.");
  if (slug.length > 80) recusar("Endereço (slug) longo demais: até 80 letras.");
  return slug;
}

type CargaDaCategoria = { corpo: Record<string, unknown>; nome: string };

export const criarCategoria: FerramentaDeEscrita<CargaDaCategoria> = {
  nome: "criar_categoria",
  titulo: "Criar uma categoria de produtos",
  descricao:
    "Cria uma categoria (ou subcategoria, com dentro_de) de produtos, como em Produtos > Categorias. Aparece no menu e nos filtros da loja quando visível. O endereço (slug) sai do nome se não for mandado. A regra de preço (margens e arredondamento) é opcional; sem ela vale a padrão (alvo 50%, mínima 35%, terminando em ,90).",
  campos: {
    nome: { tipo: "texto", descricao: "Nome da categoria.", obrigatorio: true, max: 80 },
    endereco: { tipo: "texto", descricao: "O endereço (slug) na loja, como camisetas-femininas. Padrão: sai do nome.", max: 80 },
    visivel: { tipo: "booleano", descricao: "Aparece na loja. Padrão: sim." },
    dentro_de: { tipo: "texto", descricao: "A categoria-mãe, para criar uma subcategoria (id, nome, endereço ou caminho).", max: 200 },
    ...CAMPOS_DA_REGRA,
  },
  tipo: "escrita",
  permissoes: ["categories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "categories.write", "mexer nas categorias");
    const nome = String(args.nome).trim();
    semContatoNovo(null, nome, "em Produtos > Categorias");
    const categorias = await lerCategorias(ctx);
    const slug = args.endereco !== undefined ? enderecoPedido(args.endereco) : slugDaCategoria(nome);
    if (!slug) recusar("O nome precisa ter letras ou números para virar o endereço da categoria.");
    const dono = ctx.donoDosDados;
    const repetida = categorias.find((c) => c.user_id === dono && c.slug === slug);
    if (repetida) recusar(`Já existe a categoria ${repetida.name} com o endereço "${slug}" (id ${repetida.id}). Mande outro endereço ou use essa.`);
    let mae: Categoria | null = null;
    if (args.dentro_de !== undefined) {
      const achada = acharCategoria(args.dentro_de, categorias);
      if (!achada.ok) recusar(achada.erro);
      mae = achada.categoria as Categoria;
    }
    const regra = montarRegraDePreco(REGRA_PADRAO, pedidoDeRegra(args));
    if (!regra.ok) recusar(regra.erro);
    const corpo: Record<string, unknown> = { name: nome, slug, is_visible: args.visivel !== false, parent_id: mae?.id ?? null, pricing_rule: regra.regra };
    const avisos: string[] = [];
    if (mae && mae.is_visible === false && args.visivel !== false) avisos.push(`A categoria-mãe ${mae.name} está escondida da loja.`);
    const porId = new Map(categorias.map((c) => [c.id, c]));
    return {
      previa: {
        o_que_vai_acontecer: "Uma categoria nova será criada.",
        categoria: {
          nome,
          endereco: slug,
          caminho: mae ? `${caminhoDaCategoria(mae.id, porId)} / ${nome}` : nome,
          visivel: corpo.is_visible,
          regra_de_preco: regraDePrecoEmPalavras(regra.regra),
        },
      },
      resumo: `Prévia da categoria ${nome}`,
      avisos,
      carga: { corpo, nome },
    };
  },
  async gravar(ctx, carga) {
    const resposta = await pelaRotaDasCategorias("criar a categoria", () =>
      createCategory({ supabase: ctx.supabase, user: ctx.user, body: carga.corpo, request: new Request(`${ctx.origemDoAdmin}/api/mcp`) })
    );
    const criada = (resposta as { category?: { id?: string } }).category;
    const aviso = await avisar(ctx);
    return {
      dados: { criada: true, categoria_id: criada?.id ?? null, nome: carga.nome, endereco: carga.corpo.slug, para_desfazer: criada?.id ? `remover_categoria com categoria ${criada.id}` : undefined, ...aviso },
      resumo: `Criou a categoria ${carga.nome}`,
    };
  },
};

type CargaDaAlteracaoDeCategoria = { id: string; nome: string; patch: Record<string, unknown>; antes: Record<string, unknown> };

async function secoesQueUsam(ctx: ContextoDoAssistente, categoriaId: string) {
  const secoes = await lerSecoes(ctx, true);
  return secoes
    .map((s) => ({ secao: s, ...lerSecao(s) }))
    .filter((l) => {
      if (l.modo === "category_products" || l.modo === "subcategory_products") return l.config.target_category_id === categoriaId;
      if (l.modo === "category_navigator") return Array.isArray(l.config.items) && (l.config.items as Array<Record<string, unknown>>).some((i) => i.target_category_id === categoriaId);
      return false;
    })
    .map((l) => ({ id: l.secao.id, titulo: l.titulo, tipo: NOME_DO_TIPO[l.modo] }));
}

export const alterarCategoria: FerramentaDeEscrita<CargaDaAlteracaoDeCategoria> = {
  nome: "alterar_categoria",
  titulo: "Alterar uma categoria",
  descricao:
    "Muda uma categoria: nome, endereço (slug), visível na loja, categoria-mãe (dentro_de; sem_mae=true deixa ela no primeiro nível) e a regra de preço (margens e arredondamento). Mande só o que muda.",
  campos: {
    categoria: { tipo: "texto", descricao: "A categoria (id, nome, endereço ou caminho).", obrigatorio: true, max: 200 },
    novo_nome: { tipo: "texto", descricao: "Nome novo.", max: 80 },
    endereco: { tipo: "texto", descricao: "Endereço (slug) novo. Links antigos para a categoria deixam de funcionar.", max: 80 },
    visivel: { tipo: "booleano", descricao: "true mostra na loja; false esconde (os produtos continuam, sem a categoria no menu)." },
    dentro_de: { tipo: "texto", descricao: "Nova categoria-mãe (id, nome, endereço ou caminho).", max: 200 },
    sem_mae: { tipo: "booleano", descricao: "true tira a categoria de dentro da mãe (vai para o primeiro nível)." },
    ...CAMPOS_DA_REGRA,
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["categories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "categories.write", "mexer nas categorias");
    const categorias = await lerCategorias(ctx);
    const achada = acharCategoria(args.categoria, categorias);
    if (!achada.ok) recusar(achada.erro);
    const atual = achada.categoria as Categoria;
    const porId = new Map(categorias.map((c) => [c.id, c]));
    const patch: Record<string, unknown> = {};
    const antes: Record<string, unknown> = {};
    const linhas: Array<{ campo: string; antes: unknown; depois: unknown }> = [];
    const avisos: string[] = [];

    if (args.novo_nome !== undefined) {
      const nome = String(args.novo_nome).trim();
      semContatoNovo(atual.name, nome, "em Produtos > Categorias");
      if (nome !== atual.name) {
        patch.name = nome;
        antes.name = atual.name;
        linhas.push({ campo: "nome", antes: atual.name, depois: nome });
      }
    }
    if (args.endereco !== undefined) {
      const slug = enderecoPedido(args.endereco);
      if (slug !== atual.slug) {
        const repetida = categorias.find((c) => c.id !== atual.id && c.user_id === atual.user_id && c.slug === slug);
        if (repetida) recusar(`O endereço "${slug}" já é da categoria ${repetida.name}.`);
        patch.slug = slug;
        antes.slug = atual.slug;
        linhas.push({ campo: "endereço", antes: atual.slug, depois: slug });
        avisos.push("Links antigos para esta categoria (compartilhados ou salvos) deixam de abrir.");
      }
    }
    if (args.visivel !== undefined && (args.visivel === true) !== (atual.is_visible !== false)) {
      patch.is_visible = args.visivel === true;
      antes.is_visible = atual.is_visible !== false;
      linhas.push({ campo: "visível na loja", antes: atual.is_visible !== false ? "sim" : "não", depois: args.visivel ? "sim" : "não" });
      if (args.visivel === false) {
        const usam = await secoesQueUsam(ctx, atual.id);
        if (usam.length) avisos.push(`Escondida, ela some destas seções da página inicial: ${usam.map((s) => `"${s.titulo}"`).join(", ")}.`);
      }
    }
    if (args.dentro_de !== undefined && args.sem_mae === true) recusar("Mande dentro_de ou sem_mae, não os dois.");
    if (args.dentro_de !== undefined || args.sem_mae === true) {
      let mae: Categoria | null = null;
      if (args.dentro_de !== undefined) {
        const achadaMae = acharCategoria(args.dentro_de, categorias);
        if (!achadaMae.ok) recusar(achadaMae.erro);
        mae = achadaMae.categoria as Categoria;
        if (descendentes(atual.id, categorias).has(mae.id)) recusar(`${mae.name} está dentro de ${atual.name}: não dá para pôr uma categoria dentro dela mesma.`);
      }
      if ((mae?.id ?? null) !== atual.parent_id) {
        patch.parent_id = mae?.id ?? null;
        antes.parent_id = atual.parent_id;
        linhas.push({
          campo: "dentro de",
          antes: atual.parent_id ? caminhoDaCategoria(atual.parent_id, porId) : "(primeiro nível)",
          depois: mae ? caminhoDaCategoria(mae.id, porId) : "(primeiro nível)",
        });
      }
    }
    const pedido = pedidoDeRegra(args);
    if (Object.values(pedido).some((v) => v !== undefined)) {
      const regra = montarRegraDePreco(atual.pricing_rule, pedido);
      if (!regra.ok) recusar(regra.erro);
      if (JSON.stringify(regra.regra) !== JSON.stringify(atual.pricing_rule)) {
        patch.pricing_rule = regra.regra;
        antes.pricing_rule = atual.pricing_rule;
        linhas.push({ campo: "regra de preço", antes: regraDePrecoEmPalavras(atual.pricing_rule), depois: regraDePrecoEmPalavras(regra.regra) });
        avisos.push("A regra de preço vale para as próximas sugestões de preço; os preços já gravados nos produtos não mudam.");
      }
    }
    if (!linhas.length) recusar("Nada para mudar: mande ao menos um campo com valor novo.");
    return {
      previa: { o_que_vai_acontecer: `A categoria ${atual.name} será alterada.`, mudancas: linhas },
      resumo: `Prévia da alteração da categoria ${atual.name}`,
      avisos,
      carga: { id: atual.id, nome: String(patch.name ?? atual.name), patch, antes },
    };
  },
  async gravar(ctx, carga) {
    await pelaRotaDasCategorias("alterar a categoria", () =>
      updateCategory({ supabase: ctx.supabase, user: ctx.user, id: carga.id, patch: carga.patch, request: new Request(`${ctx.origemDoAdmin}/api/mcp`) })
    );
    const aviso = await avisar(ctx);
    return {
      dados: { alterada: true, categoria_id: carga.id, nome: carga.nome, como_era: carga.antes, ...aviso },
      resumo: `Alterou a categoria ${carga.nome}`,
    };
  },
};

type CargaDaRemocaoDeCategoria = { id: string; nome: string };

export const removerCategoria: FerramentaDeEscrita<CargaDaRemocaoDeCategoria> = {
  nome: "remover_categoria",
  titulo: "Remover uma categoria",
  descricao:
    "Apaga uma categoria. Os produtos dela NÃO são apagados: ficam sem categoria. As subcategorias sobem para o primeiro nível. Seções da página inicial que mostravam essa categoria ficam vazias (a prévia lista quais). Não tem volta: para só tirar da loja, use alterar_categoria com visivel=false.",
  campos: { categoria: { tipo: "texto", descricao: "A categoria (id, nome, endereço ou caminho).", obrigatorio: true, max: 200 } },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: true,
  permissoes: ["categories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "categories.write", "mexer nas categorias");
    const categorias = await lerCategorias(ctx);
    const achada = acharCategoria(args.categoria, categorias);
    if (!achada.ok) recusar(achada.erro);
    const categoria = achada.categoria as Categoria;
    const { count, error } = await ctx.supabase
      .from("products")
      .select("id", { count: "exact", head: true })
      .in("user_id", ctx.tenantIds)
      .eq("category_id", categoria.id);
    if (error) falha("contar os produtos da categoria", error);
    const filhas = categorias.filter((c) => c.parent_id === categoria.id);
    const usam = pode(ctx, LER_A_LOJA) ? await secoesQueUsam(ctx, categoria.id) : [];
    const avisos: string[] = [];
    if (count) avisos.push(`${contagem(count, "produto fica", "produtos ficam")} sem categoria (não ${count === 1 ? "é apagado" : "são apagados"}).`);
    if (filhas.length) avisos.push(`${contagem(filhas.length, "subcategoria sobe", "subcategorias sobem")} para o primeiro nível: ${filhas.map((f) => f.name).join(", ")}.`);
    if (usam.length) avisos.push(`Estas seções da página inicial ficam vazias ou com um atalho quebrado: ${usam.map((s) => `"${s.titulo}" (${s.tipo})`).join(", ")}. Ajuste-as depois com alterar_secao ou remover_secao.`);
    avisos.push("Regras de comissão ligadas a esta categoria perdem a categoria.");
    return {
      previa: {
        o_que_vai_acontecer: `A categoria ${caminhoDaCategoria(categoria.id, new Map(categorias.map((c) => [c.id, c])))} será apagada. Não tem volta.`,
        produtos_que_ficam_sem_categoria: count ?? 0,
        subcategorias: filhas.map((f) => f.name),
        secoes_afetadas: usam,
      },
      resumo: `Prévia: remover a categoria ${categoria.name}`,
      avisos,
      carga: { id: categoria.id, nome: categoria.name ?? "" },
    };
  },
  async gravar(ctx, carga) {
    await pelaRotaDasCategorias("remover a categoria", () =>
      deleteCategory({ supabase: ctx.supabase, user: ctx.user, id: carga.id, request: new Request(`${ctx.origemDoAdmin}/api/mcp`) })
    );
    const aviso = await avisar(ctx);
    return { dados: { removida: true, nome: carga.nome, ...aviso }, resumo: `Removeu a categoria ${carga.nome}` };
  },
};

/* ===================================================== ordem dos produtos */

type CargaDaOrdemDosProdutos = { mudancas: Array<{ id: string; posicao: number }>; descricao: string };

async function gravarPosicoes(ctx: ContextoDoAssistente, mudancas: Array<{ id: string; posicao: number }>) {
  for (let i = 0; i < mudancas.length; i += 25) {
    const lote = mudancas.slice(i, i + 25);
    const resultados = await Promise.all(
      lote.map((m) => ctx.supabase.from("products").update({ catalog_position: m.posicao }).eq("id", m.id).in("user_id", ctx.tenantIds))
    );
    const erro = resultados.find((r) => r.error)?.error;
    if (erro) falha("gravar a ordem dos produtos", erro);
  }
}

export const ordenarProdutosNaLoja: FerramentaDeEscrita<CargaDaOrdemDosProdutos> = {
  nome: "ordenar_produtos_na_loja",
  titulo: "Mudar a ordem dos produtos na loja",
  descricao: `Muda a ordem em que os produtos aparecem na loja (a mesma de arrastar em Loja > Produtos). Duas formas: ordem = lista de ids que vão para a frente, nessa ordem (até ${LIMITE_DE_PRODUTOS_POR_VEZ}; o resto segue como estava); ou produto_id + posicao (1 = primeiro da loja; padrão 1, o topo). A posição conta só os produtos que aparecem na loja.`,
  campos: {
    ordem: { tipo: "textos", descricao: "Ids dos produtos que vão para a frente, nesta ordem.", maxItens: LIMITE_DE_PRODUTOS_POR_VEZ, max: 40 },
    produto_id: { tipo: "id", descricao: "Ou um produto só, para mover." },
    posicao: { tipo: "inteiro", descricao: "Com produto_id: o lugar na loja (1 = primeiro). Padrão: 1.", min: 1, max: 100_000 },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: [ESCREVER_NA_LOJA],
  async preparar(ctx, args) {
    exigir(ctx, ESCREVER_NA_LOJA, "mudar a ordem dos produtos da loja");
    if ((args.ordem !== undefined) === (args.produto_id !== undefined)) recusar("Mande ordem (lista de ids) ou produto_id com posicao, um dos dois.");
    if (args.posicao !== undefined && args.produto_id === undefined) recusar("posicao só vale junto com produto_id.");
    const { produtos, cortado } = await lerProdutos(ctx);
    if (cortado) recusar("A loja tem mais de 10 mil produtos: mude a ordem pelo painel, em Loja > Produtos.");
    const atual = produtos.map((p) => p.id);
    const porId = new Map(produtos.map((p) => [p.id, p]));
    const visiveis = produtos.filter(naLoja).map((p) => p.id);
    const avisos: string[] = [];
    let ordem: string[];
    let pedidoInicio: string[] | null = null;
    let descricao: string;
    if (args.ordem !== undefined) {
      const ids = idsDeProdutos(args.ordem);
      const nova = novaOrdemDosProdutos(atual, { inicio: ids });
      if (!nova.ok) recusar(nova.erro);
      ordem = nova.ordem;
      pedidoInicio = ids;
      descricao = `${contagem(ids.length, "produto vai", "produtos vão")} para a frente da loja`;
      const escondidos = ids.filter((id) => !naLoja(porId.get(id)!));
      if (escondidos.length) avisos.push(`${contagem(escondidos.length, "produto não aparece", "produtos não aparecem")} na loja hoje (escondido ou inativo); a posição vale quando aparecer.`);
    } else {
      const id = String(args.produto_id);
      const produto = porId.get(id);
      if (!produto) recusar("Produto não encontrado nesta loja (ou é digital). Use buscar_produtos para achar o id.");
      const para = (args.posicao as number | undefined) ?? 1;
      /* A posição pedida conta os produtos que o visitante vê. */
      const vistosSemEle = visiveis.filter((x) => x !== id);
      const ancora = vistosSemEle[para - 1];
      const semEle = atual.filter((x) => x !== id);
      const onde = ancora ? semEle.indexOf(ancora) : vistosSemEle.length ? semEle.indexOf(vistosSemEle[vistosSemEle.length - 1]) + 1 : 0;
      const nova = novaOrdemDosProdutos(atual, { mover: id, para: onde + 1 });
      if (!nova.ok) recusar(nova.erro);
      ordem = nova.ordem;
      descricao = `${produto.name} vai para a posição ${Math.min(para, vistosSemEle.length + 1)} da loja`;
      if (!naLoja(produto)) avisos.push(`${produto.name} não aparece na loja hoje (escondido ou inativo); a posição vale quando aparecer.`);
    }
    const posicoesDeHoje = produtos.map((p) => ({ id: p.id, posicao: p.catalog_position }));
    const mudancas = (pedidoInicio && posicoesParaAFrente(posicoesDeHoje, pedidoInicio)) || posicoesDosProdutos(posicoesDeHoje, ordem);
    /* Pedido que já está atendido não é erro: responde que nada precisou mudar. */
    if (!mudancas.length) {
      descricao = `${descricao} (a ordem já estava assim; nada precisou mudar)`;
      avisos.push("A ordem já estava assim; nada precisou mudar.");
    }
    const visiveisDepois = ordem.filter((id) => naLoja(porId.get(id)!));
    return {
      previa: {
        o_que_vai_acontecer: descricao,
        primeiros_da_loja_depois: visiveisDepois.slice(0, 15).map((id, i) => `${i + 1}. ${porId.get(id)?.name ?? id}`),
        produtos_regravados: mudancas.length,
      },
      resumo: "Prévia da nova ordem dos produtos da loja",
      avisos,
      carga: { mudancas, descricao },
    };
  },
  async gravar(ctx, carga) {
    if (!carga.mudancas.length) {
      return { dados: { ordenado: true, o_que_mudou: carga.descricao, produtos_regravados: 0 }, resumo: "A ordem dos produtos já estava como pedido" };
    }
    await gravarPosicoes(ctx, carga.mudancas);
    const aviso = await avisar(ctx);
    return { dados: { ordenado: true, o_que_mudou: carga.descricao, produtos_regravados: carga.mudancas.length, ...aviso }, resumo: `Mudou a ordem dos produtos da loja: ${carga.descricao}` };
  },
};

/* ================================================ mostrar / esconder na loja */

type CargaDaVisibilidade = { ids: string[]; mostrar: boolean; nomes: string[] };

function visibilidade(mostrar: boolean): FerramentaDeEscrita<CargaDaVisibilidade> {
  return {
    nome: mostrar ? "mostrar_produtos_na_loja" : "esconder_produtos_da_loja",
    titulo: mostrar ? "Mostrar produtos na loja" : "Esconder produtos da loja",
    descricao: mostrar
      ? `Liga produtos na loja virtual (a chave "Visível" de Loja > Produtos), até ${LIMITE_DE_PRODUTOS_POR_VEZ} por vez. Produto inativo continua fora da loja até ser ativado.`
      : `Tira produtos da loja virtual (a chave "Visível" de Loja > Produtos), até ${LIMITE_DE_PRODUTOS_POR_VEZ} por vez. O produto continua cadastrado, no estoque, nos orçamentos e no PDV.`,
    campos: {
      produto_ids: { tipo: "textos", descricao: "Ids dos produtos (de buscar_produtos).", obrigatorio: true, minItens: 1, maxItens: LIMITE_DE_PRODUTOS_POR_VEZ, max: 40 },
    },
    tipo: "escrita",
    sobrescreve: !mostrar,
    permissoes: [ESCREVER_NA_LOJA],
    async preparar(ctx, args) {
      exigir(ctx, ESCREVER_NA_LOJA, mostrar ? "mostrar produtos na loja" : "esconder produtos da loja");
      const ids = idsDeProdutos(args.produto_ids);
      const achados = await conferirProdutos(ctx, ids);
      const mudam = ids.filter((id) => (achados.get(id)?.show_in_catalog === true) !== mostrar);
      if (!mudam.length) recusar(mostrar ? "Todos esses produtos já estão visíveis na loja." : "Todos esses produtos já estão escondidos da loja.");
      const avisos: string[] = [];
      const jaEstavam = ids.length - mudam.length;
      if (jaEstavam) avisos.push(`${contagem(jaEstavam, "produto já estava", "produtos já estavam")} ${mostrar ? "visível(is)" : "escondido(s)"}.`);
      if (mostrar) {
        const inativos = mudam.filter((id) => achados.get(id)?.active === false);
        if (inativos.length) avisos.push(`${contagem(inativos.length, "produto está inativo", "produtos estão inativos")}: só aparece(m) na loja depois de ativado(s) no cadastro.`);
      }
      const nomes = mudam.map((id) => achados.get(id)?.name ?? id);
      return {
        previa: {
          o_que_vai_acontecer: `${contagem(mudam.length, "produto vai", "produtos vão")} ${mostrar ? "aparecer na" : "sair da"} loja.`,
          produtos: nomes.slice(0, 30),
          ...(nomes.length > 30 ? { e_mais: nomes.length - 30 } : {}),
        },
        resumo: `Prévia: ${mostrar ? "mostrar" : "esconder"} ${contagem(mudam.length, "produto", "produtos")} na loja`,
        avisos,
        carga: { ids: mudam, mostrar, nomes },
      };
    },
    async gravar(ctx, carga) {
      const { data, error } = await ctx.supabase
        .from("products")
        .update({ show_in_catalog: carga.mostrar })
        .in("id", carga.ids)
        .in("user_id", ctx.tenantIds)
        .neq("product_kind", "digital")
        .select("id");
      if (error) falha(carga.mostrar ? "mostrar os produtos na loja" : "esconder os produtos da loja", error);
      const feitos = (data ?? []).length;
      if (!feitos) recusar("O banco não deixou gravar: falta a permissão de produtos para esta pessoa. Quem libera é o dono, em Funcionários.");
      const aviso = await avisar(ctx);
      return {
        dados: {
          alterados: feitos,
          visiveis_na_loja: carga.mostrar,
          produtos: carga.nomes.slice(0, 30),
          para_desfazer: `${carga.mostrar ? "esconder_produtos_da_loja" : "mostrar_produtos_na_loja"} com os mesmos produto_ids`,
          ...aviso,
        },
        resumo: `${carga.mostrar ? "Mostrou" : "Escondeu"} ${contagem(feitos, "produto", "produtos")} na loja`,
      };
    },
  };
}

export const mostrarProdutosNaLoja = visibilidade(true);
export const esconderProdutosDaLoja = visibilidade(false);

export const FERRAMENTAS_DA_ESTRUTURA_DA_LOJA: Ferramenta[] = [
  verEstruturaDaLoja,
  criarSecao,
  alterarSecao,
  removerSecao,
  reorganizarSecoes,
  definirProdutosDaSecao,
  criarCategoria,
  alterarCategoria,
  removerCategoria,
  ordenarProdutosNaLoja,
  mostrarProdutosNaLoja,
  esconderProdutosDaLoja,
] as Ferramenta[];
