import sharp from "sharp";

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 } from "./comum";
import {
  LIMITES_DA_PROVA_SOCIAL,
  MAXIMO_DE_BANNERS,
  MAXIMO_DE_GRUPOS_DE_STORIES,
  MAXIMO_DE_STORIES_POR_GRUPO,
  NOMES_DOS_ICONES,
  caminhoNoDeposito,
  corHex,
  descreverDestino,
  instanteBr,
  instanteEmBrasilia,
  limitar,
  linkHttps,
  montarSelos,
  montarTarja,
  moverPara,
  nomeDoIcone,
  novaOrdem,
  posicoesQueMudam,
  selosGravados,
  slugValido,
  tarjaGravada,
  textoCurto,
} from "./loja-vitrine-regras";
import { codigoDoCupom } from "./operacao-regras";
import type { Ferramenta, FerramentaDeEscrita, FerramentaDeLeitura, Saida } from "./tipos";

/**
 * A vitrine da loja pelo assistente: o carrossel de banners, a tarja de
 * vantagens, os selos da página do produto (tela Catálogo › Banners), a faixa
 * de anúncio e a prova social (Catálogo › Marketing) e os stories
 * (Catálogo › Stories). Grava o mesmo que essas telas, nas mesmas colunas.
 *
 * Onde mora cada coisa e com que chave se grava:
 * - As colunas do perfil do dono (catalog_*) gravam com a sessão da pessoa,
 *   como as telas: as travas do banco valem.
 * - Banners e stories gravam com a chave de serviço, DEPOIS de conferir a
 *   permissão da tela e SEMPRE filtrando pelos donos desta empresa. Motivo:
 *   no banco, banner inativo só é visível ao público quando ativo (a sessão
 *   não enxerga o banner desligado para religar), e os stories só aceitam o
 *   próprio dono (um funcionário com a permissão da tela não conseguiria).
 *
 * Depois de gravar, a loja é avisada para mostrar na hora.
 */

/* ================================================================ comuns */

/* O perfil que a loja mostra: o do dono (funcionário tem perfil próprio, vazio). */
function perfilDaLoja(ctx: ContextoDoAssistente) {
  return resolveCompanyPublicProfileId(ctx.empresa, ctx.user.id);
}

/* Os donos possíveis das linhas desta loja (como as telas leem). */
function donos(ctx: ContextoDoAssistente) {
  return Array.from(new Set([...ctx.tenantIds, perfilDaLoja(ctx)].filter(Boolean)));
}

const TRAVADO =
  "O banco não deixou gravar: estas opções da loja moram no perfil do dono da empresa, e para mudá-las a pessoa precisa também da permissão de configurações da empresa. Quem libera é o dono, no painel, em Funcionários.";

const COLUNAS_DO_PERFIL = [
  "id",
  "slug",
  "catalog_slide_enabled",
  "catalog_info_strip_enabled",
  "catalog_info_strip_items",
  "catalog_product_trust_items",
  "catalog_announcement_enabled",
  "catalog_announcement_text",
  "catalog_announcement_countdown_enabled",
  "catalog_announcement_countdown_end_at",
  "catalog_announcement_coupon_id",
  "catalog_announcement_show_coupon_badge",
  "catalog_announcement_coupon_badge_color",
  "catalog_announcement_background_color",
  "catalog_announcement_text_color",
  "catalog_announcement_button_label",
  "catalog_announcement_button_url",
  "catalog_announcement_sticky",
  "catalog_social_proof_enabled",
  "catalog_social_proof_interval_seconds",
  "catalog_social_proof_position",
  "catalog_social_proof_duration_seconds",
  "catalog_social_proof_max_visible",
  "catalog_stories_enabled",
].join(",");

type Perfil = Record<string, unknown> & { id: string; slug: string | null };

async function lerPerfil(ctx: ContextoDoAssistente): Promise<Perfil> {
  const { data, error } = await ctx.supabase.from("profiles").select(COLUNAS_DO_PERFIL).eq("id", perfilDaLoja(ctx)).maybeSingle();
  if (error) falha("ler as opções da loja", error);
  if (!data) recusar("A loja desta empresa ainda não foi criada. Ela é criada no painel, em Catálogo.");
  return data as unknown as Perfil;
}

async function gravarPerfil(ctx: ContextoDoAssistente, perfilId: string, colunas: Record<string, unknown>) {
  const { data, error } = await ctx.supabase.from("profiles").update(colunas).eq("id", perfilId).select("id");
  if (error) falha("gravar as opções da loja", error);
  if (!(data ?? []).length) recusar(TRAVADO);
}

function linkDaLoja(ctx: ContextoDoAssistente, slug: unknown) {
  return slug ? `${baseDaLoja(ctx)}/${String(slug)}` : null;
}

/* Avisa a loja e diz, no resultado, se ela ouviu. */
async function avisar(ctx: ContextoDoAssistente, perfilId: string) {
  const ouviu = await avisarLojaEEsperar(perfilId, new Request(`${ctx.origemDoAdmin}/api/mcp`));
  return ouviu ? {} : { observacao: "A loja pode levar até dois minutos para mostrar a mudança." };
}

/* Texto que o cliente lê: o assistente não escreve contato que já não estava lá. */
function conferirContatos(antes: Array<string | null | undefined>, depois: Array<string | null | undefined>, onde: string) {
  const novos = contatosNovos(antes.filter(Boolean).join("\n"), depois.filter(Boolean).join("\n"));
  if (novos.length) recusar(recusaDeContatos(novos, onde));
}

async function produtoDaLoja(ctx: ContextoDoAssistente, id: string): Promise<{ id: string; name: string }> {
  const { data, error } = await ctx.supabase.from("products").select("id,name").eq("id", id).in("user_id", ctx.tenantIds).maybeSingle();
  if (error) falha("ler o produto", error);
  if (!data) recusar("Produto não encontrado nesta loja. Use buscar_produtos para achar o id.");
  return data as { id: string; name: string };
}

async function nomesDosProdutos(ctx: ContextoDoAssistente, ids: Array<string | null | undefined>) {
  const lista = Array.from(new Set(ids.filter((id): id is string => Boolean(id))));
  const nomes = new Map<string, string>();
  if (!lista.length) return nomes;
  const { data } = await ctx.supabase.from("products").select("id,name").in("id", lista).in("user_id", ctx.tenantIds);
  for (const linha of (data ?? []) as Array<{ id: string; name: string | null }>) nomes.set(linha.id, linha.name ?? "produto");
  return nomes;
}

/* ------------------------------------------------------------- imagens */

type ImagemPronta = { conteudo: Buffer; tipo: string; extensao: string; largura: number; altura: number; origem: string };

/** Banner até 1920 de largura, story e capa até 1600 / 800: WebP 85, como a tela. */
async function prepararImagem(ctx: ContextoDoAssistente, valor: unknown, para: "banner" | "story" | "capa"): Promise<ImagemPronta> {
  const recebida = await receberImagem(ctx, valor, { minimo: para === "capa" ? 64 : 200 });
  const lado = para === "banner" ? 1920 : para === "story" ? 1600 : 800;
  const saida = await sharp(recebida.conteudo, { limitInputPixels: 60_000_000, failOn: "error", animated: false })
    .rotate()
    .resize({ width: lado, height: para === "banner" ? undefined : lado, fit: "inside", withoutEnlargement: true })
    .webp({ quality: 85 })
    .toBuffer({ resolveWithObject: true });
  return { conteudo: saida.data, tipo: "image/webp", extensao: "webp", largura: saida.info.width, altura: saida.info.height, origem: recebida.origem };
}

function avisoDoFormatoDoBanner(imagem: ImagemPronta): string[] {
  const proporcao = imagem.largura / Math.max(imagem.altura, 1);
  const avisos: string[] = [];
  if (proporcao < 2.9 || proporcao > 4.2) {
    avisos.push(
      `A loja mostra o banner no formato 1920 × 540 (bem largo). Esta imagem ficou ${imagem.largura} × ${imagem.altura} e pode aparecer cortada em cima e embaixo.`
    );
  }
  if (imagem.largura < 1200) avisos.push(`A imagem tem só ${imagem.largura} pixels de largura; em tela grande o banner pode ficar borrado.`);
  return avisos;
}

/* Apaga do depósito os arquivos que ninguém mais usa (só da pasta permitida). */
async function apagarArquivos(ctx: ContextoDoAssistente, enderecos: Array<string | null | undefined>, pasta: string, aindaUsados: Set<string>) {
  const caminhos = Array.from(
    new Set(
      enderecos
        .filter((endereco): endereco is string => Boolean(endereco) && !aindaUsados.has(String(endereco)))
        .map((endereco) => caminhoNoDeposito(endereco, pasta))
        .filter((caminho): caminho is string => Boolean(caminho))
    )
  );
  if (!caminhos.length) return 0;
  const { error } = await ctx.admin.storage.from("products").remove(caminhos);
  if (error) {
    console.error("[mcp] apagar arquivos da vitrine", error.message);
    return 0;
  }
  return caminhos.length;
}

/* ================================================================ banners */

type Banner = {
  id: string;
  user_id: string;
  image_url: string;
  link_type: string | null;
  product_id: string | null;
  link_url: string | null;
  open_in_new_tab: boolean | null;
  position: number | null;
  active: boolean | null;
  created_at: string;
};

async function lerBanners(ctx: ContextoDoAssistente): Promise<Banner[]> {
  const { data, error } = await ctx.admin
    .from("catalog_banners")
    .select("id,user_id,image_url,link_type,product_id,link_url,open_in_new_tab,position,active,created_at")
    .in("user_id", donos(ctx))
    .eq("type", "slide")
    .order("position", { ascending: true })
    .order("created_at", { ascending: true })
    .limit(200);
  if (error) falha("ler os banners", error);
  return (data ?? []) as Banner[];
}

function bannerDaLista(banners: Banner[], id: string) {
  const banner = banners.find((b) => b.id === id);
  if (!banner) recusar("Banner não encontrado nesta loja. Veja os ids em ver_vitrine_da_loja.");
  return banner;
}

function linhaDoBanner(banner: Banner, indice: number, produtos: Map<string, string>) {
  const tipo = banner.link_type || "none";
  return {
    id: banner.id,
    posicao: indice + 1,
    imagem: banner.image_url,
    ativo: banner.active !== false,
    leva_para:
      tipo === "product" && banner.product_id
        ? `produto: ${produtos.get(banner.product_id) ?? "produto"} (${banner.product_id})`
        : tipo === "url" && banner.link_url
          ? `link: ${banner.link_url}`
          : "nada (só imagem)",
    abre_em_nova_aba: banner.open_in_new_tab === true,
  };
}

async function gravarPosicoesDosBanners(ctx: ContextoDoAssistente, banners: Banner[], ordem: string[]) {
  const mudam = posicoesQueMudam(banners, ordem, 1);
  const dono = donos(ctx);
  for (const linha of mudam) {
    const { error } = await ctx.admin.from("catalog_banners").update({ position: linha.position }).eq("id", linha.id).in("user_id", dono);
    if (error) falha("reordenar os banners", error);
  }
}

/* O link do banner a partir dos campos do assistente. `undefined` = não mexe. */
type LinkDoBanner = { link_type: "none" | "product" | "url"; product_id: string | null; link_url: string | null; descricao: string };

async function lerLinkDoBanner(ctx: ContextoDoAssistente, args: Record<string, unknown>): Promise<LinkDoBanner | undefined> {
  const destino = args.destino as string | undefined;
  const produtoId = args.produto_id as string | undefined;
  const url = args.url as string | undefined;
  if (produtoId && url) recusar("Escolha um destino só: produto_id OU url.");
  if (destino === "nenhum") {
    if (produtoId || url) recusar('Com destino "nenhum" não mande produto_id nem url.');
    return { link_type: "none", product_id: null, link_url: null, descricao: "nada (só imagem)" };
  }
  if (produtoId || destino === "produto") {
    if (!produtoId) recusar('Para destino "produto", mande produto_id (ache com buscar_produtos).');
    const produto = await produtoDaLoja(ctx, produtoId);
    return { link_type: "product", product_id: produto.id, link_url: null, descricao: `produto: ${produto.name}` };
  }
  if (url || destino === "link") {
    const limpo = linkHttps(url);
    if (!limpo) recusar("O link precisa começar com https:// e ser um endereço completo, sem espaços.");
    return { link_type: "url", product_id: null, link_url: limpo, descricao: `link: ${limpo}` };
  }
  return undefined;
}

const CAMPOS_DO_LINK_DO_BANNER: Campos = {
  destino: {
    tipo: "opcao",
    descricao: 'Para onde o banner leva ao ser clicado: "nenhum" (só imagem), "produto" (mande produto_id) ou "link" (mande url).',
    opcoes: ["nenhum", "produto", "link"],
  },
  produto_id: { tipo: "id", descricao: "O produto que o banner abre (id de buscar_produtos)." },
  url: { tipo: "texto", descricao: "Link de fora que o banner abre, começando com https://. Link de fora sempre passa pela aprovação da pessoa.", max: 2000 },
  abrir_em_nova_aba: { tipo: "booleano", descricao: "Abre o destino numa aba nova." },
};

type CargaDoNovoBanner = {
  perfilId: string;
  slug: string | null;
  imagem: ImagemPronta;
  link: LinkDoBanner;
  novaAba: boolean;
  ativo: boolean;
  posicao: number | null;
  ligarCarrossel: boolean;
};

export const adicionarBanner: FerramentaDeEscrita<CargaDoNovoBanner> = {
  nome: "adicionar_banner",
  titulo: "Adicionar banner à loja",
  descricao: `Põe um banner novo no carrossel do topo da loja (tela Catálogo › Banners). A imagem ideal é 1920 × 540 (bem larga). Se a pessoa anexou a imagem na conversa, chame antes pedir_imagem e use o código "envio:..." que voltar. O banner pode levar a um produto (produto_id), a um link https (url) ou a nada. Entra no fim do carrossel, ou na posição pedida (1 = primeiro). Se o carrossel estava desligado, ele é ligado (a não ser que ligar_carrossel seja falso) e a resposta avisa. Até ${MAXIMO_DE_BANNERS} banners.`,
  campos: {
    imagem: { tipo: "texto", descricao: DESCRICAO_DO_CAMPO_DE_IMAGEM, obrigatorio: true, max: 2000 },
    ...CAMPOS_DO_LINK_DO_BANNER,
    posicao: { tipo: "inteiro", descricao: "Posição no carrossel (1 = primeiro). Padrão: no fim.", min: 1, max: MAXIMO_DE_BANNERS },
    ativo: { tipo: "booleano", descricao: "Já aparece na loja. Padrão: sim." },
    ligar_carrossel: { tipo: "booleano", descricao: "Liga o carrossel de banners se estiver desligado. Padrão: sim." },
  },
  tipo: "escrita",
  abertoAoMundo: true,
  sensivel: (args) => Boolean(args.url),
  permissoes: ["catalog.banners.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.banners.write", "mexer nos banners da loja");
    const perfil = await lerPerfil(ctx);
    const banners = await lerBanners(ctx);
    if (banners.length >= MAXIMO_DE_BANNERS) {
      recusar(`A loja já tem ${banners.length} banners, o máximo por aqui. Remova algum (remover_banner) antes de pôr outro.`);
    }
    const link = (await lerLinkDoBanner(ctx, args)) ?? { link_type: "none" as const, product_id: null, link_url: null, descricao: "nada (só imagem)" };
    const imagem = await prepararImagem(ctx, args.imagem, "banner");
    const carrosselLigado = perfil.catalog_slide_enabled === true;
    const ligarCarrossel = !carrosselLigado && args.ligar_carrossel !== false;
    const posicao = typeof args.posicao === "number" ? Math.min(args.posicao, banners.length + 1) : null;
    const avisos = avisoDoFormatoDoBanner(imagem);
    if (!carrosselLigado && !ligarCarrossel) avisos.push("O carrossel de banners está desligado: o banner fica guardado, mas só aparece quando ligar o carrossel.");
    return {
      previa: {
        o_que_vai_acontecer: "Um banner novo vai entrar no carrossel da loja.",
        imagem: imagem.origem,
        tamanho_final: `${imagem.largura} × ${imagem.altura} pixels`,
        leva_para: link.descricao,
        abre_em_nova_aba: args.abrir_em_nova_aba === true,
        posicao: posicao ?? banners.length + 1,
        ativo: args.ativo !== false,
        ...(ligarCarrossel ? { carrossel: "estava desligado e vai ser ligado" } : {}),
        link_da_loja: linkDaLoja(ctx, perfil.slug),
      },
      resumo: "Prévia de banner novo na loja",
      avisos,
      carga: {
        perfilId: perfil.id,
        slug: perfil.slug,
        imagem,
        link,
        novaAba: args.abrir_em_nova_aba === true,
        ativo: args.ativo !== false,
        posicao,
        ligarCarrossel,
      },
    };
  },
  async gravar(ctx, carga) {
    const endereco = await guardarImagemDaLoja(ctx, carga.imagem, `banners/${carga.perfilId}`);
    const antes = await lerBanners(ctx);
    const proxima = antes.length ? Math.max(...antes.map((b) => b.position ?? 0)) + 1 : 1;
    const { data, error } = await ctx.admin
      .from("catalog_banners")
      .insert({
        user_id: carga.perfilId,
        type: "slide",
        image_url: endereco,
        link_type: carga.link.link_type,
        product_id: carga.link.product_id,
        link_url: carga.link.link_url,
        open_in_new_tab: carga.novaAba,
        position: proxima,
        active: carga.ativo,
      })
      .select("id")
      .single();
    if (error || !data) {
      await apagarArquivos(ctx, [endereco], `banners/${carga.perfilId}`, new Set());
      falha("gravar o banner", error);
    }
    const novoId = (data as { id: string }).id;
    let posicaoFinal = antes.length + 1;
    if (carga.posicao !== null && carga.posicao <= antes.length) {
      const depois = await lerBanners(ctx);
      const ordem = moverPara(
        depois.map((b) => b.id),
        novoId,
        carga.posicao
      );
      await gravarPosicoesDosBanners(ctx, depois, ordem);
      posicaoFinal = ordem.indexOf(novoId) + 1;
    }
    let carrossel: string | undefined;
    if (carga.ligarCarrossel) {
      const { data: ligado, error: erroAoLigar } = await ctx.supabase
        .from("profiles")
        .update({ catalog_slide_enabled: true })
        .eq("id", carga.perfilId)
        .select("id");
      carrossel =
        !erroAoLigar && (ligado ?? []).length
          ? "O carrossel de banners estava desligado e foi ligado."
          : "O banner foi gravado, mas o carrossel continua desligado: esta pessoa não pode mudar as opções do perfil da loja. O dono liga em Catálogo › Banners.";
    }
    return {
      dados: {
        criado: true,
        banner_id: novoId,
        posicao: posicaoFinal,
        imagem: endereco,
        leva_para: carga.link.descricao,
        ativo: carga.ativo,
        ...(carrossel ? { carrossel } : {}),
        link_da_loja: linkDaLoja(ctx, carga.slug),
        para_desfazer: `Chame remover_banner com id "${novoId}".`,
        ...(await avisar(ctx, carga.perfilId)),
      },
      resumo: `Adicionou um banner à loja (${carga.link.descricao})`,
    };
  },
};

type CargaDoBanner = {
  perfilId: string;
  slug: string | null;
  id: string;
  patch: Record<string, unknown>;
  imagem: ImagemPronta | null;
  imagemAntiga: string;
  posicao: number | null;
  linhas: Array<{ campo: string; antes: string; depois: string }>;
};

export const alterarBanner: FerramentaDeEscrita<CargaDoBanner> = {
  nome: "alterar_banner",
  titulo: "Alterar banner da loja",
  descricao:
    "Muda um banner do carrossel: troca a imagem (pedir_imagem quando a pessoa anexou na conversa), muda para onde leva (produto, link https ou nada), liga ou desliga (ativo) e muda a posição (1 = primeiro; os outros se ajeitam). Mande só o que muda. O id vem de ver_vitrine_da_loja.",
  campos: {
    id: { tipo: "id", descricao: "O id do banner (de ver_vitrine_da_loja).", obrigatorio: true },
    imagem: { tipo: "texto", descricao: `Imagem nova. ${DESCRICAO_DO_CAMPO_DE_IMAGEM}`, max: 2000 },
    ...CAMPOS_DO_LINK_DO_BANNER,
    ativo: { tipo: "booleano", descricao: "Aparece (verdadeiro) ou fica escondido (falso) na loja." },
    posicao: { tipo: "inteiro", descricao: "Nova posição no carrossel (1 = primeiro).", min: 1, max: MAXIMO_DE_BANNERS * 10 },
  },
  tipo: "escrita",
  sobrescreve: true,
  abertoAoMundo: true,
  sensivel: (args) => Boolean(args.url),
  permissoes: ["catalog.banners.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.banners.write", "mexer nos banners da loja");
    const perfil = await lerPerfil(ctx);
    const banners = await lerBanners(ctx);
    const banner = bannerDaLista(banners, String(args.id));
    const produtos = await nomesDosProdutos(ctx, [banner.product_id]);
    const linhaAntes = linhaDoBanner(banner, banners.indexOf(banner), produtos);
    const patch: Record<string, unknown> = {};
    const linhas: CargaDoBanner["linhas"] = [];

    const link = await lerLinkDoBanner(ctx, args);
    if (link) {
      patch.link_type = link.link_type;
      patch.product_id = link.product_id;
      patch.link_url = link.link_url;
      linhas.push({ campo: "leva para", antes: linhaAntes.leva_para, depois: link.descricao });
    }
    if (typeof args.abrir_em_nova_aba === "boolean") {
      patch.open_in_new_tab = args.abrir_em_nova_aba;
      linhas.push({ campo: "abre em nova aba", antes: linhaAntes.abre_em_nova_aba ? "sim" : "não", depois: args.abrir_em_nova_aba ? "sim" : "não" });
    }
    if (typeof args.ativo === "boolean") {
      patch.active = args.ativo;
      linhas.push({ campo: "ativo", antes: linhaAntes.ativo ? "sim" : "não", depois: args.ativo ? "sim" : "não" });
    }
    let imagem: ImagemPronta | null = null;
    const avisos: string[] = [];
    if (args.imagem !== undefined) {
      imagem = await prepararImagem(ctx, args.imagem, "banner");
      avisos.push(...avisoDoFormatoDoBanner(imagem));
      linhas.push({ campo: "imagem", antes: banner.image_url, depois: `${imagem.origem} (${imagem.largura} × ${imagem.altura})` });
    }
    let posicao: number | null = null;
    if (typeof args.posicao === "number") {
      posicao = Math.min(args.posicao, banners.length);
      if (posicao !== linhaAntes.posicao) linhas.push({ campo: "posição", antes: String(linhaAntes.posicao), depois: String(posicao) });
      else posicao = null;
    }
    if (!linhas.length) recusar("Nada para mudar: mande ao menos um campo (imagem, destino, produto_id, url, abrir_em_nova_aba, ativo ou posicao).");
    return {
      previa: { o_que_vai_acontecer: `O banner da posição ${linhaAntes.posicao} vai mudar.`, mudancas: linhas, link_da_loja: linkDaLoja(ctx, perfil.slug) },
      resumo: `Prévia de alteração de banner (${linhas.map((l) => l.campo).join(", ")})`,
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, id: banner.id, patch, imagem, imagemAntiga: banner.image_url, posicao, linhas },
    };
  },
  async gravar(ctx, carga) {
    const patch = { ...carga.patch };
    if (carga.imagem) patch.image_url = await guardarImagemDaLoja(ctx, carga.imagem, `banners/${carga.perfilId}`);
    if (Object.keys(patch).length) {
      const { data, error } = await ctx.admin.from("catalog_banners").update(patch).eq("id", carga.id).in("user_id", donos(ctx)).select("id");
      if (error) falha("gravar o banner", error);
      if (!(data ?? []).length) recusar("Este banner não existe mais. Veja a lista em ver_vitrine_da_loja.");
    }
    if (carga.posicao !== null) {
      const banners = await lerBanners(ctx);
      await gravarPosicoesDosBanners(ctx, banners, moverPara(banners.map((b) => b.id), carga.id, carga.posicao));
    }
    if (carga.imagem) {
      const banners = await lerBanners(ctx);
      await apagarArquivos(ctx, [carga.imagemAntiga], `banners/${carga.perfilId}`, new Set(banners.map((b) => b.image_url)));
    }
    return {
      dados: { alterado: true, banner_id: carga.id, mudancas: carga.linhas, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
      resumo: `Alterou um banner da loja (${carga.linhas.map((l) => l.campo).join(", ")})`,
    };
  },
};

type CargaDaRemocaoDoBanner = { perfilId: string; slug: string | null; id: string; imagem: string };

export const removerBanner: FerramentaDeEscrita<CargaDaRemocaoDoBanner> = {
  nome: "remover_banner",
  titulo: "Remover banner da loja",
  descricao:
    "Tira um banner do carrossel da loja de vez (apaga o banner e a imagem dele). Sempre passa pela prévia com código. Para só esconder, use alterar_banner com ativo falso.",
  campos: { id: { tipo: "id", descricao: "O id do banner (de ver_vitrine_da_loja).", obrigatorio: true } },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: true,
  permissoes: ["catalog.banners.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.banners.write", "mexer nos banners da loja");
    const perfil = await lerPerfil(ctx);
    const banners = await lerBanners(ctx);
    const banner = bannerDaLista(banners, String(args.id));
    const produtos = await nomesDosProdutos(ctx, [banner.product_id]);
    return {
      previa: {
        o_que_vai_acontecer: "Este banner vai ser apagado da loja, com a imagem. Não dá para desfazer (só pondo de novo com adicionar_banner).",
        banner: linhaDoBanner(banner, banners.indexOf(banner), produtos),
        banners_que_ficam: banners.length - 1,
      },
      resumo: "Prévia de remoção de banner",
      avisos: [],
      carga: { perfilId: perfil.id, slug: perfil.slug, id: banner.id, imagem: banner.image_url },
    };
  },
  async gravar(ctx, carga) {
    const { data, error } = await ctx.admin.from("catalog_banners").delete().eq("id", carga.id).in("user_id", donos(ctx)).select("id");
    if (error) falha("remover o banner", error);
    if (!(data ?? []).length) recusar("Este banner já não existe.");
    const restantes = await lerBanners(ctx);
    await gravarPosicoesDosBanners(ctx, restantes, restantes.map((b) => b.id));
    await apagarArquivos(ctx, [carga.imagem], `banners/${carga.perfilId}`, new Set(restantes.map((b) => b.image_url)));
    return {
      dados: { removido: true, banner_id: carga.id, banners_na_loja: restantes.length, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
      resumo: "Removeu um banner da loja",
    };
  },
};

type CargaDaOrdem = { perfilId: string; slug: string | null; ordem: string[]; grupoId: string | null; descricao: string[] };

export const reorganizarBanners: FerramentaDeEscrita<CargaDaOrdem> = {
  nome: "reorganizar_banners",
  titulo: "Reorganizar banners da loja",
  descricao:
    "Muda a ordem do carrossel de banners. Mande em ordem os ids (de ver_vitrine_da_loja): o primeiro da lista vira o primeiro banner. Pode mandar só alguns: eles vão para a frente e os outros seguem na ordem de antes.",
  campos: { ordem: { tipo: "textos", descricao: "Os ids dos banners na ordem nova.", obrigatorio: true, minItens: 1, maxItens: 200, max: 36 } },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["catalog.banners.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.banners.write", "mexer nos banners da loja");
    const perfil = await lerPerfil(ctx);
    const banners = await lerBanners(ctx);
    const pedida = (args.ordem as string[]).map((id) => id.trim().toLowerCase());
    const ordem = novaOrdem(
      banners.map((b) => b.id),
      pedida
    );
    if (!ordem.ok) recusar(ordem.erro);
    const produtos = await nomesDosProdutos(ctx, banners.map((b) => b.product_id));
    const porId = new Map(banners.map((b) => [b.id, b]));
    const descricao = ordem.valor.map((id, i) => {
      const linha = linhaDoBanner(porId.get(id)!, i, produtos);
      return `${i + 1}. ${linha.leva_para} (${id})`;
    });
    return {
      previa: { o_que_vai_acontecer: "O carrossel vai ficar nesta ordem.", ordem: descricao },
      resumo: "Prévia da nova ordem dos banners",
      avisos: [],
      carga: { perfilId: perfil.id, slug: perfil.slug, ordem: ordem.valor, grupoId: null, descricao },
    };
  },
  async gravar(ctx, carga) {
    const banners = await lerBanners(ctx);
    const atuais = new Set(banners.map((b) => b.id));
    const ordem = [...carga.ordem.filter((id) => atuais.has(id)), ...banners.map((b) => b.id).filter((id) => !carga.ordem.includes(id))];
    await gravarPosicoesDosBanners(ctx, banners, ordem);
    return {
      dados: { reorganizado: true, ordem: carga.descricao, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
      resumo: "Reorganizou os banners da loja",
    };
  },
};

/* ------------------------------------------- liga/desliga (perfil do dono) */

type CargaDoPerfil = { perfilId: string; slug: string | null; colunas: Record<string, unknown>; linhas: Array<{ campo: string; antes: string; depois: string }> };

async function gravarOpcoesDoPerfil(ctx: ContextoDoAssistente, carga: CargaDoPerfil, resumo: string): Promise<Saida> {
  await gravarPerfil(ctx, carga.perfilId, carga.colunas);
  return {
    dados: { alterado: true, mudancas: carga.linhas, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
    resumo,
  };
}

const simNao = (valor: unknown) => (valor === true ? "ligado" : "desligado");

export const ligarCarrosselDeBanners: FerramentaDeEscrita<CargaDoPerfil> = {
  nome: "ligar_carrossel_de_banners",
  titulo: "Ligar ou desligar o carrossel de banners",
  descricao: "Liga ou desliga o carrossel de banners do topo da loja. Desligar só esconde: os banners continuam guardados.",
  campos: { ligado: { tipo: "booleano", descricao: "Verdadeiro liga, falso desliga.", obrigatorio: true } },
  tipo: "escrita",
  permissoes: ["catalog.banners.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.banners.write", "mexer nos banners da loja");
    const perfil = await lerPerfil(ctx);
    const ligado = args.ligado === true;
    const avisos: string[] = [];
    if (ligado) {
      const ativos = (await lerBanners(ctx)).filter((b) => b.active !== false).length;
      if (!ativos) avisos.push("Não há banner ativo: o carrossel fica ligado, mas só aparece quando houver banner (adicionar_banner).");
    }
    const linhas = [{ campo: "carrossel de banners", antes: simNao(perfil.catalog_slide_enabled), depois: simNao(ligado) }];
    return {
      previa: { mudancas: linhas },
      resumo: `Prévia: ${ligado ? "ligar" : "desligar"} o carrossel de banners`,
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, colunas: { catalog_slide_enabled: ligado }, linhas },
    };
  },
  gravar: (ctx, carga) => gravarOpcoesDoPerfil(ctx, carga, `${carga.colunas.catalog_slide_enabled ? "Ligou" : "Desligou"} o carrossel de banners`),
};

/* --------------------------------------------- tarja de vantagens e selos */

export const alterarTarjaDeVantagens: FerramentaDeEscrita<CargaDoPerfil> = {
  nome: "alterar_tarja_de_vantagens",
  titulo: "Alterar a tarja de vantagens da loja",
  descricao: `Muda a tarja de vantagens da página inicial da loja (as 3 informações com ícone logo abaixo dos banners, como "Frete para todo o Brasil"). Liga/desliga com ligada; para mudar os textos mande os 3 itens, cada um com icone (um destes: ${NOMES_DOS_ICONES}), titulo e subtitulo. Veja os de hoje em ver_vitrine_da_loja.`,
  campos: {
    ligada: { tipo: "booleano", descricao: "Mostra (verdadeiro) ou esconde (falso) a tarja na loja." },
    itens: {
      tipo: "lista",
      descricao: "Exatamente 3 itens, na ordem em que aparecem.",
      minItens: 3,
      maxItens: 3,
      itens: {
        icone: { tipo: "texto", descricao: `O ícone: ${NOMES_DOS_ICONES}.`, obrigatorio: true, max: 40 },
        titulo: { tipo: "texto", descricao: "O título (até 60 letras).", obrigatorio: true, max: 60 },
        subtitulo: { tipo: "texto", descricao: "A linha de baixo (até 80 letras).", max: 80 },
      },
    },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["catalog.banners.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.banners.write", "mexer na tarja de vantagens da loja");
    if (args.ligada === undefined && args.itens === undefined) recusar("Nada para mudar: mande ligada e/ou os 3 itens.");
    const perfil = await lerPerfil(ctx);
    const colunas: Record<string, unknown> = {};
    const linhas: CargaDoPerfil["linhas"] = [];
    const ligadaAntes = perfil.catalog_info_strip_enabled !== false;
    if (typeof args.ligada === "boolean") {
      colunas.catalog_info_strip_enabled = args.ligada;
      linhas.push({ campo: "tarja de vantagens", antes: ligadaAntes ? "ligada" : "desligada", depois: args.ligada ? "ligada" : "desligada" });
    }
    if (Array.isArray(args.itens)) {
      const montada = montarTarja(args.itens as Array<Record<string, unknown>>);
      if (!montada.ok) recusar(montada.erro);
      const antes = tarjaGravada(perfil.catalog_info_strip_items);
      conferirContatos(
        antes.flatMap((i) => [i.title, i.subtitle]),
        montada.valor.flatMap((i) => [i.title, i.subtitle]),
        "em Catálogo › Banners, aba Informações"
      );
      colunas.catalog_info_strip_items = montada.valor;
      montada.valor.forEach((item, i) => {
        const velho = antes[i];
        linhas.push({
          campo: `item ${i + 1}`,
          antes: `${nomeDoIcone(velho.icon)} · ${velho.title}${velho.subtitle ? ` · ${velho.subtitle}` : ""}`,
          depois: `${nomeDoIcone(item.icon)} · ${item.title}${item.subtitle ? ` · ${item.subtitle}` : ""}`,
        });
      });
    }
    return {
      previa: { mudancas: linhas, link_da_loja: linkDaLoja(ctx, perfil.slug) },
      resumo: "Prévia da tarja de vantagens",
      avisos: [],
      carga: { perfilId: perfil.id, slug: perfil.slug, colunas, linhas },
    };
  },
  gravar: (ctx, carga) => gravarOpcoesDoPerfil(ctx, carga, "Alterou a tarja de vantagens da loja"),
};

export const alterarSelosDoProduto: FerramentaDeEscrita<CargaDoPerfil> = {
  nome: "alterar_selos_do_produto",
  titulo: "Alterar os selos da página do produto",
  descricao: `Muda os 2 selos de confiança que aparecem na página de cada produto (como "Entrega Rápida e Segura" e "Garantia de Qualidade"). Mande os 2, cada um com icone (${NOMES_DOS_ICONES}) e titulo. Veja os de hoje em ver_vitrine_da_loja.`,
  campos: {
    itens: {
      tipo: "lista",
      descricao: "Exatamente 2 selos, na ordem em que aparecem.",
      obrigatorio: true,
      minItens: 2,
      maxItens: 2,
      itens: {
        icone: { tipo: "texto", descricao: `O ícone: ${NOMES_DOS_ICONES}.`, obrigatorio: true, max: 40 },
        titulo: { tipo: "texto", descricao: "O texto do selo (até 60 letras).", obrigatorio: true, max: 60 },
      },
    },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["catalog.banners.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.banners.write", "mexer nos selos da página do produto");
    const perfil = await lerPerfil(ctx);
    const montados = montarSelos(args.itens as Array<Record<string, unknown>>);
    if (!montados.ok) recusar(montados.erro);
    const antes = selosGravados(perfil.catalog_product_trust_items);
    conferirContatos(
      antes.map((s) => s.title),
      montados.valor.map((s) => s.title),
      "em Catálogo › Banners, aba Informações"
    );
    const linhas = montados.valor.map((selo, i) => ({
      campo: `selo ${i + 1}`,
      antes: `${nomeDoIcone(antes[i].icon)} · ${antes[i].title}`,
      depois: `${nomeDoIcone(selo.icon)} · ${selo.title}`,
    }));
    return {
      previa: { mudancas: linhas, link_da_loja: linkDaLoja(ctx, perfil.slug) },
      resumo: "Prévia dos selos da página do produto",
      avisos: [],
      carga: { perfilId: perfil.id, slug: perfil.slug, colunas: { catalog_product_trust_items: montados.valor }, linhas },
    };
  },
  gravar: (ctx, carga) => gravarOpcoesDoPerfil(ctx, carga, "Alterou os selos da página do produto"),
};

/* ======================================================= faixa de anúncio */

const PADRAO_DA_FAIXA = { fundo: "#111827", texto: "#FFFFFF", selo: "#FB7185" };
const PARTES_DA_FAIXA = ["texto", "cupom", "contagem", "botao"] as const;

async function cupomPeloCodigo(ctx: ContextoDoAssistente, valor: unknown) {
  const codigo = codigoDoCupom(valor);
  if (!codigo) recusar("Código de cupom inválido: use letras, números e hífen (3 a 30).");
  const { data, error } = await ctx.supabase
    .from("catalog_coupons")
    .select("id,code,type,value,active,ends_at")
    .in("user_id", ctx.tenantIds)
    .ilike("code", codigo)
    .limit(1);
  if (error) falha("ler o cupom", error);
  const cupom = (data ?? [])[0] as { id: string; code: string; type: string; value: number; active: boolean | null; ends_at: string | null } | undefined;
  if (!cupom) recusar(`Não existe cupom "${codigo}" nesta loja. Crie antes com criar_cupom ou veja os que existem com listar_cupons.`);
  return cupom;
}

async function cupomPeloId(ctx: ContextoDoAssistente, id: unknown) {
  if (!id) return null;
  const { data } = await ctx.supabase.from("catalog_coupons").select("id,code,active").eq("id", String(id)).in("user_id", ctx.tenantIds).maybeSingle();
  return (data as { id: string; code: string; active: boolean | null } | null) ?? null;
}

async function categoriaDaLoja(ctx: ContextoDoAssistente, valor: unknown) {
  const pedido = String(valor ?? "").trim();
  const { data, error } = await ctx.supabase
    .from("categories")
    .select("id,name,slug,parent_id,is_visible")
    .in("user_id", ctx.tenantIds)
    .eq("is_visible", true)
    .limit(1000);
  if (error) falha("ler as categorias", error);
  const comparar = (texto: string) => texto.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
  const lista = (data ?? []) as Array<{ id: string; name: string; slug: string | null; parent_id: string | null }>;
  const slug = slugValido(pedido);
  const achada = lista.find((c) => c.slug && slug && c.slug === slug) ?? lista.find((c) => comparar(c.name) === comparar(pedido));
  if (!achada || !achada.slug) recusar(`Categoria "${pedido}" não encontrada entre as visíveis na loja. Veja as categorias com listar_categorias.`);
  return achada;
}

async function descreverBotao(ctx: ContextoDoAssistente, url: string | null | undefined) {
  const destino = descreverDestino(url);
  if (destino.tipo === "produto" && destino.valor) {
    const nomes = await nomesDosProdutos(ctx, [destino.valor]);
    return `produto: ${nomes.get(destino.valor) ?? destino.valor}`;
  }
  if (destino.tipo === "categoria" || destino.tipo === "subcategoria") return `${destino.tipo}: ${destino.valor}`;
  if (destino.tipo === "link") return `link: ${destino.valor}`;
  return "nenhum";
}

export const alterarFaixaDeAnuncio: FerramentaDeEscrita<CargaDoPerfil> = {
  nome: "alterar_faixa_de_anuncio",
  titulo: "Alterar a faixa de anúncio da loja",
  descricao:
    "Muda a faixa de anúncio do topo da loja (tela Catálogo › Marketing): liga/desliga, texto (use **assim** para negrito), cupom pelo código (com o selo do cupom), contagem regressiva até uma data e hora de Brasília, botão (texto + destino: link https, produto ou categoria), cores (código como #111827) e se fica fixa no topo. Mande só o que muda. Para tirar texto, cupom, contagem ou botão, ponha o nome em tirar. Botão com link de fora passa pela aprovação da pessoa.",
  campos: {
    ligada: { tipo: "booleano", descricao: "Mostra (verdadeiro) ou esconde (falso) a faixa." },
    texto: { tipo: "texto", descricao: "O texto da faixa. **palavra** fica em negrito. Até 200 letras.", max: 200 },
    cupom: { tipo: "texto", descricao: "O código de um cupom da loja (de listar_cupons) para divulgar na faixa.", max: 30 },
    mostrar_selo_do_cupom: { tipo: "booleano", descricao: "Mostra o selo colorido com o código do cupom." },
    contagem_ate_data: { tipo: "data", descricao: "Liga a contagem regressiva até este dia (horário de Brasília)." },
    contagem_ate_hora: { tipo: "hora", descricao: "A hora em que a contagem acaba. Padrão: 23:59." },
    contagem_regressiva: { tipo: "booleano", descricao: "Liga ou desliga a contagem regressiva (ligar exige uma data: a de hoje gravada ou contagem_ate_data)." },
    botao_texto: { tipo: "texto", descricao: "O texto do botão (até 40 letras).", max: 40 },
    botao_url: { tipo: "texto", descricao: "Destino do botão: um link https://.", max: 2000 },
    botao_produto_id: { tipo: "id", descricao: "Destino do botão: um produto da loja (id de buscar_produtos)." },
    botao_categoria: { tipo: "texto", descricao: "Destino do botão: uma categoria ou subcategoria visível (nome ou slug, de listar_categorias).", max: 120 },
    cor_de_fundo: { tipo: "texto", descricao: "Cor de fundo da faixa, como #111827.", max: 9 },
    cor_do_texto: { tipo: "texto", descricao: "Cor do texto, como #FFFFFF.", max: 9 },
    cor_do_selo_do_cupom: { tipo: "texto", descricao: "Cor do selo do cupom, como #FB7185.", max: 9 },
    fixa_no_topo: { tipo: "booleano", descricao: "A faixa acompanha a rolagem da página (fica fixa no topo)." },
    tirar: { tipo: "textos", descricao: 'O que tirar da faixa: "texto", "cupom", "contagem", "botao".', maxItens: 4, max: 20 },
  },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: (args) => Boolean(args.botao_url),
  permissoes: ["marketing.manage"],
  async preparar(ctx, args) {
    exigir(ctx, "marketing.manage", "mexer na faixa de anúncio da loja");
    const perfil = await lerPerfil(ctx);
    const colunas: Record<string, unknown> = {};
    const linhas: CargaDoPerfil["linhas"] = [];
    const avisos: string[] = [];
    const anotar = (campo: string, antes: unknown, depois: unknown) =>
      linhas.push({ campo, antes: antes === null || antes === undefined || antes === "" ? "(nada)" : String(antes), depois: depois === null || depois === undefined || depois === "" ? "(nada)" : String(depois) });

    const tirar = new Set<string>();
    for (const parte of (args.tirar as string[] | undefined) ?? []) {
      const limpa = parte.trim().toLowerCase().replace("botão", "botao");
      if (!(PARTES_DA_FAIXA as readonly string[]).includes(limpa)) recusar(`Em tirar, use só: ${PARTES_DA_FAIXA.join(", ")}.`);
      tirar.add(limpa);
    }

    if (typeof args.ligada === "boolean") {
      colunas.catalog_announcement_enabled = args.ligada;
      anotar("faixa", perfil.catalog_announcement_enabled === true ? "ligada" : "desligada", args.ligada ? "ligada" : "desligada");
    }

    /* texto */
    if (args.texto !== undefined && tirar.has("texto")) recusar("Mande texto ou tire o texto, não os dois.");
    if (args.texto !== undefined) {
      const novo = String(args.texto).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
      conferirContatos([perfil.catalog_announcement_text as string | null], [novo], "em Catálogo › Marketing");
      colunas.catalog_announcement_text = novo || null;
      anotar("texto", perfil.catalog_announcement_text, novo);
    } else if (tirar.has("texto")) {
      colunas.catalog_announcement_text = null;
      anotar("texto", perfil.catalog_announcement_text, null);
    }

    /* cupom */
    const cupomAntes = await cupomPeloId(ctx, perfil.catalog_announcement_coupon_id);
    if (args.cupom !== undefined && tirar.has("cupom")) recusar("Mande cupom ou tire o cupom, não os dois.");
    if (args.cupom !== undefined) {
      const cupom = await cupomPeloCodigo(ctx, args.cupom);
      if (cupom.active === false) avisos.push(`O cupom ${cupom.code} está desativado: a faixa mostra o código, mas ele não vai funcionar no carrinho.`);
      if (cupom.ends_at && new Date(cupom.ends_at).getTime() < Date.now()) avisos.push(`O cupom ${cupom.code} já venceu.`);
      colunas.catalog_announcement_coupon_id = cupom.id;
      anotar("cupom", cupomAntes?.code, cupom.code);
    } else if (tirar.has("cupom")) {
      colunas.catalog_announcement_coupon_id = null;
      anotar("cupom", cupomAntes?.code, null);
    }
    if (typeof args.mostrar_selo_do_cupom === "boolean") {
      colunas.catalog_announcement_show_coupon_badge = args.mostrar_selo_do_cupom;
      anotar("selo do cupom", perfil.catalog_announcement_show_coupon_badge !== false ? "aparece" : "escondido", args.mostrar_selo_do_cupom ? "aparece" : "escondido");
    }

    /* contagem regressiva */
    const fimAntes = instanteBr(perfil.catalog_announcement_countdown_end_at as string | null);
    const contagemAntes = perfil.catalog_announcement_countdown_enabled === true ? `até ${fimAntes ?? "?"}` : "desligada";
    if (tirar.has("contagem") && (args.contagem_ate_data !== undefined || args.contagem_regressiva === true)) recusar("Mande a contagem ou tire a contagem, não os dois.");
    if (args.contagem_ate_hora !== undefined && args.contagem_ate_data === undefined) recusar("Para mudar a hora da contagem, mande também contagem_ate_data.");
    if (args.contagem_ate_data !== undefined) {
      if (args.contagem_regressiva === false) recusar("Mande a data da contagem ou desligue a contagem, não os dois.");
      const fim = instanteEmBrasilia(String(args.contagem_ate_data), (args.contagem_ate_hora as string | undefined) ?? "23:59");
      if (!fim) recusar("Data ou hora da contagem inválida.");
      if (new Date(fim).getTime() <= Date.now()) recusar("A data da contagem regressiva já passou. Mande uma data e hora no futuro (horário de Brasília).");
      colunas.catalog_announcement_countdown_enabled = true;
      colunas.catalog_announcement_countdown_end_at = fim;
      anotar("contagem regressiva", contagemAntes, `até ${instanteBr(fim)} (Brasília)`);
    } else if (tirar.has("contagem") || args.contagem_regressiva === false) {
      colunas.catalog_announcement_countdown_enabled = false;
      colunas.catalog_announcement_countdown_end_at = null;
      anotar("contagem regressiva", contagemAntes, "desligada");
    } else if (args.contagem_regressiva === true) {
      const fim = perfil.catalog_announcement_countdown_end_at as string | null;
      if (!fim || new Date(fim).getTime() <= Date.now()) recusar("Para ligar a contagem, mande contagem_ate_data (e, se quiser, contagem_ate_hora) com um momento no futuro.");
      colunas.catalog_announcement_countdown_enabled = true;
      anotar("contagem regressiva", contagemAntes, `até ${instanteBr(fim)} (Brasília)`);
    }

    /* botão */
    const destinos = [args.botao_url, args.botao_produto_id, args.botao_categoria].filter((v) => v !== undefined).length;
    if (destinos > 1) recusar("Escolha um destino só para o botão: botao_url, botao_produto_id OU botao_categoria.");
    if (tirar.has("botao") && (destinos || args.botao_texto !== undefined)) recusar("Mande o botão ou tire o botão, não os dois.");
    const urlAntes = perfil.catalog_announcement_button_url as string | null;
    if (tirar.has("botao")) {
      colunas.catalog_announcement_button_label = null;
      colunas.catalog_announcement_button_url = null;
      anotar("botão", `${perfil.catalog_announcement_button_label ?? ""} → ${await descreverBotao(ctx, urlAntes)}`, null);
    } else {
      if (args.botao_texto !== undefined) {
        const rotulo = textoCurto(args.botao_texto, 40);
        conferirContatos([perfil.catalog_announcement_button_label as string | null], [rotulo], "em Catálogo › Marketing");
        colunas.catalog_announcement_button_label = rotulo || null;
        anotar("texto do botão", perfil.catalog_announcement_button_label, rotulo);
      }
      let novaUrl: string | null = null;
      let descricao = "";
      if (args.botao_url !== undefined) {
        novaUrl = linkHttps(args.botao_url);
        if (!novaUrl) recusar("O link do botão precisa começar com https:// e ser um endereço completo, sem espaços.");
        descricao = `link: ${novaUrl}`;
      } else if (args.botao_produto_id !== undefined) {
        const produto = await produtoDaLoja(ctx, String(args.botao_produto_id));
        novaUrl = `catalog://product/${produto.id}`;
        descricao = `produto: ${produto.name}`;
      } else if (args.botao_categoria !== undefined) {
        const categoria = await categoriaDaLoja(ctx, args.botao_categoria);
        novaUrl = `catalog://${categoria.parent_id ? "subcategory" : "category"}/${categoria.slug}`;
        descricao = `${categoria.parent_id ? "subcategoria" : "categoria"}: ${categoria.name}`;
      }
      if (novaUrl) {
        colunas.catalog_announcement_button_url = novaUrl;
        anotar("destino do botão", await descreverBotao(ctx, urlAntes), descricao);
      }
      const rotuloFinal = "catalog_announcement_button_label" in colunas ? colunas.catalog_announcement_button_label : perfil.catalog_announcement_button_label;
      const urlFinal = "catalog_announcement_button_url" in colunas ? colunas.catalog_announcement_button_url : urlAntes;
      if ((args.botao_texto !== undefined || novaUrl) && (!rotuloFinal || !urlFinal)) {
        avisos.push("O botão só aparece com texto E destino. Falta " + (!rotuloFinal ? "o texto (botao_texto)." : "o destino (botao_url, botao_produto_id ou botao_categoria)."));
      }
    }

    /* cores */
    const cores: Array<[string, string, string, string]> = [
      ["cor_de_fundo", "catalog_announcement_background_color", "cor de fundo", PADRAO_DA_FAIXA.fundo],
      ["cor_do_texto", "catalog_announcement_text_color", "cor do texto", PADRAO_DA_FAIXA.texto],
      ["cor_do_selo_do_cupom", "catalog_announcement_coupon_badge_color", "cor do selo do cupom", PADRAO_DA_FAIXA.selo],
    ];
    for (const [campo, coluna, rotulo, padrao] of cores) {
      if (args[campo] === undefined) continue;
      const cor = corHex(args[campo]);
      if (!cor) recusar(`${rotulo}: "${String(args[campo])}" não é uma cor. Use o código da cor, como #111827.`);
      colunas[coluna] = cor;
      anotar(rotulo, perfil[coluna] || padrao, cor);
    }
    if (typeof args.fixa_no_topo === "boolean") {
      colunas.catalog_announcement_sticky = args.fixa_no_topo;
      anotar("fixa no topo", perfil.catalog_announcement_sticky !== false ? "sim" : "não", args.fixa_no_topo ? "sim" : "não");
    }

    if (!linhas.length) recusar("Nada para mudar: mande ao menos um campo da faixa.");

    /* A faixa ligada precisa ter o que mostrar (a loja esconde faixa vazia). */
    const final = (coluna: string) => (coluna in colunas ? colunas[coluna] : perfil[coluna]);
    const ligadaFinal = final("catalog_announcement_enabled") === true;
    const temConteudo =
      Boolean(final("catalog_announcement_text")) ||
      Boolean(final("catalog_announcement_coupon_id")) ||
      (Boolean(final("catalog_announcement_button_label")) && Boolean(final("catalog_announcement_button_url"))) ||
      (final("catalog_announcement_countdown_enabled") === true && Boolean(final("catalog_announcement_countdown_end_at")));
    if (ligadaFinal && !temConteudo) avisos.push("A faixa está ligada mas sem texto, cupom, botão ou contagem: a loja não mostra faixa vazia.");
    if (!ligadaFinal && args.ligada === undefined) avisos.push("A faixa está desligada: as mudanças ficam guardadas e aparecem quando ligar (ligada: verdadeiro).");

    return {
      previa: { mudancas: linhas, link_da_loja: linkDaLoja(ctx, perfil.slug) },
      resumo: `Prévia da faixa de anúncio (${linhas.map((l) => l.campo).join(", ")})`,
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, colunas, linhas },
    };
  },
  gravar: (ctx, carga) => gravarOpcoesDoPerfil(ctx, carga, `Alterou a faixa de anúncio da loja (${carga.linhas.map((l) => l.campo).join(", ")})`),
};

/* ============================================================ prova social */

export const alterarProvaSocial: FerramentaDeEscrita<CargaDoPerfil> = {
  nome: "alterar_prova_social",
  titulo: "Alterar os avisos de compra (prova social)",
  descricao: `Muda os avisos de "fulano comprou tal produto há X min" que aparecem no canto da loja (tela Catálogo › Marketing): ligar/desligar, de quanto em quanto tempo aparecem (${LIMITES_DA_PROVA_SOCIAL.intervalo.min} a ${LIMITES_DA_PROVA_SOCIAL.intervalo.max} s), quanto tempo cada um fica (${LIMITES_DA_PROVA_SOCIAL.duracao.min} a ${LIMITES_DA_PROVA_SOCIAL.duracao.max} s), canto (esquerda ou direita) e quantos aparecem juntos (${LIMITES_DA_PROVA_SOCIAL.maximo_visivel.min} a ${LIMITES_DA_PROVA_SOCIAL.maximo_visivel.max}). Mande só o que muda; número fora da faixa é ajustado ao limite.`,
  campos: {
    ligada: { tipo: "booleano", descricao: "Liga (verdadeiro) ou desliga (falso) os avisos." },
    intervalo_segundos: { tipo: "inteiro", descricao: "Segundos entre um aviso e outro." },
    duracao_segundos: { tipo: "inteiro", descricao: "Segundos que cada aviso fica na tela." },
    posicao: { tipo: "opcao", descricao: "Canto da tela.", opcoes: ["esquerda", "direita"] },
    maximo_visivel: { tipo: "inteiro", descricao: "Quantos avisos aparecem ao mesmo tempo." },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["marketing.manage"],
  async preparar(ctx, args) {
    exigir(ctx, "marketing.manage", "mexer nos avisos de compra da loja");
    const perfil = await lerPerfil(ctx);
    const colunas: Record<string, unknown> = {};
    const linhas: CargaDoPerfil["linhas"] = [];
    const avisos: string[] = [];
    if (typeof args.ligada === "boolean") {
      colunas.catalog_social_proof_enabled = args.ligada;
      linhas.push({ campo: "avisos de compra", antes: simNao(perfil.catalog_social_proof_enabled), depois: simNao(args.ligada) });
    }
    const numeros: Array<[string, string, keyof typeof LIMITES_DA_PROVA_SOCIAL, string]> = [
      ["intervalo_segundos", "catalog_social_proof_interval_seconds", "intervalo", "intervalo (s)"],
      ["duracao_segundos", "catalog_social_proof_duration_seconds", "duracao", "duração (s)"],
      ["maximo_visivel", "catalog_social_proof_max_visible", "maximo_visivel", "máximo na tela"],
    ];
    for (const [campo, coluna, limite, rotulo] of numeros) {
      if (args[campo] === undefined) continue;
      const faixa = LIMITES_DA_PROVA_SOCIAL[limite];
      const valor = limitar(args[campo], faixa.min, faixa.max, faixa.padrao);
      if (valor !== args[campo]) avisos.push(`${rotulo}: ${String(args[campo])} está fora do permitido (${faixa.min} a ${faixa.max}); ficou ${valor}.`);
      colunas[coluna] = valor;
      linhas.push({ campo: rotulo, antes: String(perfil[coluna] ?? faixa.padrao), depois: String(valor) });
    }
    if (args.posicao !== undefined) {
      const lado = args.posicao === "direita" ? "right" : "left";
      colunas.catalog_social_proof_position = lado;
      linhas.push({ campo: "canto", antes: perfil.catalog_social_proof_position === "right" ? "direita" : "esquerda", depois: String(args.posicao) });
    }
    if (!linhas.length) recusar("Nada para mudar: mande ao menos um campo.");
    return {
      previa: { mudancas: linhas, link_da_loja: linkDaLoja(ctx, perfil.slug) },
      resumo: "Prévia dos avisos de compra",
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, colunas, linhas },
    };
  },
  gravar: (ctx, carga) => gravarOpcoesDoPerfil(ctx, carga, "Alterou os avisos de compra da loja"),
};

/* ================================================================ stories */

type ItemDoStory = { id: string; group_id: string; product_id: string | null; image_url: string; position: number | null };
type GrupoDeStories = {
  id: string;
  user_id: string;
  title: string;
  thumbnail_url: string | null;
  active: boolean | null;
  position: number | null;
  created_at: string;
  items: ItemDoStory[];
};

async function lerGrupos(ctx: ContextoDoAssistente): Promise<GrupoDeStories[]> {
  const { data, error } = await ctx.admin
    .from("catalog_story_groups")
    .select("id,user_id,title,thumbnail_url,active,position,created_at,items:catalog_story_items(id,group_id,product_id,image_url,position)")
    .in("user_id", donos(ctx))
    .order("position", { ascending: true })
    .order("created_at", { ascending: false })
    .limit(200);
  if (error) falha("ler os stories", error);
  return ((data ?? []) as GrupoDeStories[]).map((grupo) => ({
    ...grupo,
    items: [...(grupo.items ?? [])].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)),
  }));
}

function grupoDaLista(grupos: GrupoDeStories[], id: string) {
  const grupo = grupos.find((g) => g.id === id);
  if (!grupo) recusar("Grupo de stories não encontrado nesta loja. Veja os ids em ver_vitrine_da_loja.");
  return grupo;
}

/* Todas as imagens ainda usadas pelos stories da loja (para não apagar arquivo em uso). */
function imagensEmUso(grupos: GrupoDeStories[]) {
  const usadas = new Set<string>();
  for (const grupo of grupos) {
    if (grupo.thumbnail_url) usadas.add(grupo.thumbnail_url);
    for (const item of grupo.items) usadas.add(item.image_url);
  }
  return usadas;
}

async function gravarPosicoes(ctx: ContextoDoAssistente, tabela: "catalog_story_groups" | "catalog_story_items", linhas: Array<{ id: string; position: number | null }>, ordem: string[], grupoId?: string) {
  for (const linha of posicoesQueMudam(linhas, ordem, 0)) {
    let consulta = ctx.admin.from(tabela).update({ position: linha.position }).eq("id", linha.id);
    consulta = tabela === "catalog_story_groups" ? consulta.in("user_id", donos(ctx)) : consulta.eq("group_id", grupoId ?? "");
    const { error } = await consulta;
    if (error) falha("reordenar os stories", error);
  }
}

type CargaDoGrupo = { perfilId: string; slug: string | null; titulo: string; capa: ImagemPronta | null; ativo: boolean; posicao: number | null };

export const criarGrupoDeStories: FerramentaDeEscrita<CargaDoGrupo> = {
  nome: "criar_grupo_de_stories",
  titulo: "Criar grupo de stories",
  descricao: `Cria um grupo de stories (destaque, a bolinha no topo da loja), como "Novidades" ou "Promoções". Depois ponha as fotos com adicionar_story. A capa (imagem da bolinha) é opcional; se a pessoa anexou a imagem na conversa, chame pedir_imagem antes. Até ${MAXIMO_DE_GRUPOS_DE_STORIES} grupos.`,
  campos: {
    titulo: { tipo: "texto", descricao: "O nome do grupo (até 30 letras).", obrigatorio: true, max: 30 },
    capa: { tipo: "texto", descricao: `A imagem da bolinha (opcional). ${DESCRICAO_DO_CAMPO_DE_IMAGEM}`, max: 2000 },
    ativo: { tipo: "booleano", descricao: "Aparece na loja. Padrão: sim." },
    posicao: { tipo: "inteiro", descricao: "Posição entre os grupos (1 = primeiro). Padrão: no fim.", min: 1, max: MAXIMO_DE_GRUPOS_DE_STORIES },
  },
  tipo: "escrita",
  abertoAoMundo: true,
  permissoes: ["catalog.stories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.stories.write", "mexer nos stories da loja");
    const perfil = await lerPerfil(ctx);
    const grupos = await lerGrupos(ctx);
    if (grupos.length >= MAXIMO_DE_GRUPOS_DE_STORIES) recusar(`A loja já tem ${grupos.length} grupos de stories, o máximo por aqui. Remova algum antes.`);
    const titulo = textoCurto(args.titulo, 30);
    if (!titulo) recusar("Falta o título do grupo.");
    conferirContatos([], [titulo], "em Catálogo › Stories");
    const capa = args.capa !== undefined ? await prepararImagem(ctx, args.capa, "capa") : null;
    const avisos: string[] = ["O grupo nasce vazio: ponha as fotos com adicionar_story (sem foto ele não aparece na loja)."];
    if (perfil.catalog_stories_enabled === false) avisos.push("Os stories estão desligados na loja: ligue com ligar_stories para aparecerem.");
    return {
      previa: {
        o_que_vai_acontecer: `Um grupo de stories "${titulo}" vai ser criado.`,
        capa: capa ? capa.origem : "nenhuma",
        ativo: args.ativo !== false,
        posicao: typeof args.posicao === "number" ? Math.min(args.posicao, grupos.length + 1) : grupos.length + 1,
      },
      resumo: `Prévia do grupo de stories ${titulo}`,
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, titulo, capa, ativo: args.ativo !== false, posicao: typeof args.posicao === "number" ? args.posicao : null },
    };
  },
  async gravar(ctx, carga) {
    const grupos = await lerGrupos(ctx);
    const capa = carga.capa ? await guardarImagemDaLoja(ctx, carga.capa, "stories") : null;
    const { data, error } = await ctx.admin
      .from("catalog_story_groups")
      .insert({ user_id: carga.perfilId, title: carga.titulo, thumbnail_url: capa, active: carga.ativo, position: grupos.length })
      .select("id")
      .single();
    if (error || !data) {
      if (capa) await apagarArquivos(ctx, [capa], "stories", new Set());
      falha("criar o grupo de stories", error);
    }
    const novoId = (data as { id: string }).id;
    let posicao = grupos.length + 1;
    if (carga.posicao !== null && carga.posicao <= grupos.length) {
      const todos = await lerGrupos(ctx);
      const ordem = moverPara(todos.map((g) => g.id), novoId, carga.posicao);
      await gravarPosicoes(ctx, "catalog_story_groups", todos, ordem);
      posicao = ordem.indexOf(novoId) + 1;
    }
    return {
      dados: {
        criado: true,
        grupo_id: novoId,
        titulo: carga.titulo,
        posicao,
        capa,
        proximo_passo: "Ponha as fotos com adicionar_story usando este grupo_id.",
        link_da_loja: linkDaLoja(ctx, carga.slug),
        ...(await avisar(ctx, carga.perfilId)),
      },
      resumo: `Criou o grupo de stories ${carga.titulo}`,
    };
  },
};

type CargaDoStory = { perfilId: string; slug: string | null; grupoId: string; grupoTitulo: string; imagem: ImagemPronta; produto: { id: string; name: string } | null; posicao: number | null };

export const adicionarStory: FerramentaDeEscrita<CargaDoStory> = {
  nome: "adicionar_story",
  titulo: "Adicionar story",
  descricao: `Põe uma foto num grupo de stories da loja, opcionalmente ligada a um produto (aparece o botão de comprar). Se a pessoa anexou a imagem na conversa, chame pedir_imagem antes e use o código "envio:...". Foto em pé (9:16) fica melhor. Até ${MAXIMO_DE_STORIES_POR_GRUPO} fotos por grupo.`,
  campos: {
    grupo_id: { tipo: "id", descricao: "O grupo (de ver_vitrine_da_loja ou criar_grupo_de_stories).", obrigatorio: true },
    imagem: { tipo: "texto", descricao: DESCRICAO_DO_CAMPO_DE_IMAGEM, obrigatorio: true, max: 2000 },
    produto_id: { tipo: "id", descricao: "Produto mostrado com o story (id de buscar_produtos). Opcional." },
    posicao: { tipo: "inteiro", descricao: "Posição dentro do grupo (1 = primeira). Padrão: no fim.", min: 1, max: MAXIMO_DE_STORIES_POR_GRUPO },
  },
  tipo: "escrita",
  abertoAoMundo: true,
  permissoes: ["catalog.stories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.stories.write", "mexer nos stories da loja");
    const perfil = await lerPerfil(ctx);
    const grupo = grupoDaLista(await lerGrupos(ctx), String(args.grupo_id));
    if (grupo.items.length >= MAXIMO_DE_STORIES_POR_GRUPO) recusar(`O grupo "${grupo.title}" já tem ${grupo.items.length} fotos, o máximo por aqui. Remova alguma ou crie outro grupo.`);
    const produto = args.produto_id ? await produtoDaLoja(ctx, String(args.produto_id)) : null;
    const imagem = await prepararImagem(ctx, args.imagem, "story");
    const avisos: string[] = [];
    if (imagem.largura > imagem.altura) avisos.push("A foto está deitada; nos stories ela aparece cortada dos lados. Foto em pé (9:16) fica melhor.");
    if (grupo.active === false) avisos.push(`O grupo "${grupo.title}" está desativado: a foto só aparece quando ativar o grupo.`);
    return {
      previa: {
        o_que_vai_acontecer: `Uma foto vai entrar no grupo de stories "${grupo.title}".`,
        imagem: imagem.origem,
        tamanho_final: `${imagem.largura} × ${imagem.altura} pixels`,
        produto: produto?.name ?? "nenhum",
        posicao: typeof args.posicao === "number" ? Math.min(args.posicao, grupo.items.length + 1) : grupo.items.length + 1,
      },
      resumo: `Prévia de story no grupo ${grupo.title}`,
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, grupoId: grupo.id, grupoTitulo: grupo.title, imagem, produto, posicao: typeof args.posicao === "number" ? args.posicao : null },
    };
  },
  async gravar(ctx, carga) {
    const grupo = grupoDaLista(await lerGrupos(ctx), carga.grupoId);
    const endereco = await guardarImagemDaLoja(ctx, carga.imagem, "stories");
    const { data, error } = await ctx.admin
      .from("catalog_story_items")
      .insert({ group_id: grupo.id, image_url: endereco, product_id: carga.produto?.id ?? null, position: grupo.items.length })
      .select("id")
      .single();
    if (error || !data) {
      await apagarArquivos(ctx, [endereco], "stories", new Set());
      falha("gravar o story", error);
    }
    const novoId = (data as { id: string }).id;
    let posicao = grupo.items.length + 1;
    if (carga.posicao !== null && carga.posicao <= grupo.items.length) {
      const atualizado = grupoDaLista(await lerGrupos(ctx), grupo.id);
      const ordem = moverPara(atualizado.items.map((i) => i.id), novoId, carga.posicao);
      await gravarPosicoes(ctx, "catalog_story_items", atualizado.items, ordem, grupo.id);
      posicao = ordem.indexOf(novoId) + 1;
    }
    return {
      dados: {
        criado: true,
        story_id: novoId,
        grupo: carga.grupoTitulo,
        posicao,
        imagem: endereco,
        produto: carga.produto?.name ?? null,
        link_da_loja: linkDaLoja(ctx, carga.slug),
        para_desfazer: `Chame remover_story com story_id "${novoId}".`,
        ...(await avisar(ctx, carga.perfilId)),
      },
      resumo: `Adicionou um story ao grupo ${carga.grupoTitulo}`,
    };
  },
};

type CargaDaAlteracaoDoGrupo = {
  perfilId: string;
  slug: string | null;
  grupoId: string;
  patch: Record<string, unknown>;
  capa: ImagemPronta | null;
  tirarCapa: boolean;
  capaAntiga: string | null;
  posicao: number | null;
  linhas: Array<{ campo: string; antes: string; depois: string }>;
};

export const alterarGrupoDeStories: FerramentaDeEscrita<CargaDaAlteracaoDoGrupo> = {
  nome: "alterar_grupo_de_stories",
  titulo: "Alterar grupo de stories",
  descricao: "Muda um grupo de stories: título, capa (imagem da bolinha; pedir_imagem quando a pessoa anexou na conversa), tirar a capa, ativo (aparece ou não na loja) e posição entre os grupos (1 = primeiro). Mande só o que muda.",
  campos: {
    grupo_id: { tipo: "id", descricao: "O grupo (de ver_vitrine_da_loja).", obrigatorio: true },
    titulo: { tipo: "texto", descricao: "Novo nome do grupo (até 30 letras).", max: 30 },
    capa: { tipo: "texto", descricao: `Nova imagem da bolinha. ${DESCRICAO_DO_CAMPO_DE_IMAGEM}`, max: 2000 },
    tirar_capa: { tipo: "booleano", descricao: "Tira a capa (a loja usa a primeira foto)." },
    ativo: { tipo: "booleano", descricao: "Aparece (verdadeiro) ou fica escondido (falso) na loja." },
    posicao: { tipo: "inteiro", descricao: "Nova posição entre os grupos (1 = primeiro).", min: 1, max: 200 },
  },
  tipo: "escrita",
  sobrescreve: true,
  abertoAoMundo: true,
  permissoes: ["catalog.stories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.stories.write", "mexer nos stories da loja");
    const perfil = await lerPerfil(ctx);
    const grupos = await lerGrupos(ctx);
    const grupo = grupoDaLista(grupos, String(args.grupo_id));
    if (args.capa !== undefined && args.tirar_capa === true) recusar("Mande capa nova ou tirar_capa, não os dois.");
    const patch: Record<string, unknown> = {};
    const linhas: CargaDaAlteracaoDoGrupo["linhas"] = [];
    if (args.titulo !== undefined) {
      const titulo = textoCurto(args.titulo, 30);
      if (!titulo) recusar("O título não pode ficar vazio.");
      conferirContatos([grupo.title], [titulo], "em Catálogo › Stories");
      patch.title = titulo;
      linhas.push({ campo: "título", antes: grupo.title, depois: titulo });
    }
    if (typeof args.ativo === "boolean") {
      patch.active = args.ativo;
      linhas.push({ campo: "ativo", antes: grupo.active !== false ? "sim" : "não", depois: args.ativo ? "sim" : "não" });
    }
    const capa = args.capa !== undefined ? await prepararImagem(ctx, args.capa, "capa") : null;
    if (capa) linhas.push({ campo: "capa", antes: grupo.thumbnail_url || "nenhuma", depois: capa.origem });
    if (args.tirar_capa === true) {
      if (!grupo.thumbnail_url) recusar("Este grupo já está sem capa.");
      patch.thumbnail_url = null;
      linhas.push({ campo: "capa", antes: grupo.thumbnail_url, depois: "nenhuma (usa a primeira foto)" });
    }
    let posicao: number | null = null;
    if (typeof args.posicao === "number") {
      const atual = grupos.indexOf(grupo) + 1;
      posicao = Math.min(args.posicao, grupos.length);
      if (posicao === atual) posicao = null;
      else linhas.push({ campo: "posição", antes: String(atual), depois: String(posicao) });
    }
    if (!linhas.length) recusar("Nada para mudar: mande ao menos um campo (titulo, capa, tirar_capa, ativo ou posicao).");
    return {
      previa: { o_que_vai_acontecer: `O grupo de stories "${grupo.title}" vai mudar.`, mudancas: linhas },
      resumo: `Prévia de alteração do grupo de stories ${grupo.title}`,
      avisos: [],
      carga: {
        perfilId: perfil.id,
        slug: perfil.slug,
        grupoId: grupo.id,
        patch,
        capa,
        tirarCapa: args.tirar_capa === true,
        capaAntiga: grupo.thumbnail_url,
        posicao,
        linhas,
      },
    };
  },
  async gravar(ctx, carga) {
    const patch = { ...carga.patch };
    if (carga.capa) patch.thumbnail_url = await guardarImagemDaLoja(ctx, carga.capa, "stories");
    if (Object.keys(patch).length) {
      const { data, error } = await ctx.admin
        .from("catalog_story_groups")
        .update({ ...patch, updated_at: new Date().toISOString() })
        .eq("id", carga.grupoId)
        .in("user_id", donos(ctx))
        .select("id");
      if (error) falha("gravar o grupo de stories", error);
      if (!(data ?? []).length) recusar("Este grupo de stories não existe mais.");
    }
    if (carga.posicao !== null) {
      const grupos = await lerGrupos(ctx);
      await gravarPosicoes(ctx, "catalog_story_groups", grupos, moverPara(grupos.map((g) => g.id), carga.grupoId, carga.posicao));
    }
    if ((carga.capa || carga.tirarCapa) && carga.capaAntiga) {
      await apagarArquivos(ctx, [carga.capaAntiga], "stories", imagensEmUso(await lerGrupos(ctx)));
    }
    return {
      dados: { alterado: true, grupo_id: carga.grupoId, mudancas: carga.linhas, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
      resumo: `Alterou um grupo de stories (${carga.linhas.map((l) => l.campo).join(", ")})`,
    };
  },
};

type CargaDaRemocaoDoStory = { perfilId: string; slug: string | null; storyId: string; grupoId: string; imagem: string };

export const removerStory: FerramentaDeEscrita<CargaDaRemocaoDoStory> = {
  nome: "remover_story",
  titulo: "Remover story",
  descricao: "Apaga uma foto de um grupo de stories (e o arquivo dela). Sempre passa pela prévia com código. O id da foto vem de ver_vitrine_da_loja (com o grupo_id em detalhar_grupo).",
  campos: { story_id: { tipo: "id", descricao: "O id da foto (story).", obrigatorio: true } },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: true,
  permissoes: ["catalog.stories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.stories.write", "mexer nos stories da loja");
    const perfil = await lerPerfil(ctx);
    const grupos = await lerGrupos(ctx);
    const id = String(args.story_id);
    const grupo = grupos.find((g) => g.items.some((i) => i.id === id));
    if (!grupo) recusar("Story não encontrado nesta loja. Veja os ids em ver_vitrine_da_loja, com detalhar_grupo.");
    const item = grupo.items.find((i) => i.id === id)!;
    const produtos = await nomesDosProdutos(ctx, [item.product_id]);
    const avisos = grupo.items.length === 1 ? [`É a única foto do grupo "${grupo.title}": sem fotos o grupo some da loja.`] : [];
    return {
      previa: {
        o_que_vai_acontecer: `Esta foto vai ser apagada do grupo "${grupo.title}", com o arquivo. Não dá para desfazer.`,
        imagem: item.image_url,
        posicao: grupo.items.indexOf(item) + 1,
        produto: item.product_id ? produtos.get(item.product_id) ?? "produto" : "nenhum",
      },
      resumo: `Prévia de remoção de story do grupo ${grupo.title}`,
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, storyId: item.id, grupoId: grupo.id, imagem: item.image_url },
    };
  },
  async gravar(ctx, carga) {
    /* Confere de novo que o grupo é desta loja antes de apagar pela chave de serviço. */
    const grupo = grupoDaLista(await lerGrupos(ctx), carga.grupoId);
    const { data, error } = await ctx.admin.from("catalog_story_items").delete().eq("id", carga.storyId).eq("group_id", grupo.id).select("id");
    if (error) falha("remover o story", error);
    if (!(data ?? []).length) recusar("Este story já não existe.");
    const grupos = await lerGrupos(ctx);
    const atualizado = grupos.find((g) => g.id === grupo.id);
    if (atualizado) await gravarPosicoes(ctx, "catalog_story_items", atualizado.items, atualizado.items.map((i) => i.id), grupo.id);
    await apagarArquivos(ctx, [carga.imagem], "stories", imagensEmUso(grupos));
    return {
      dados: { removido: true, story_id: carga.storyId, fotos_no_grupo: atualizado?.items.length ?? 0, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
      resumo: `Removeu um story do grupo ${grupo.title}`,
    };
  },
};

type CargaDaRemocaoDoGrupo = { perfilId: string; slug: string | null; grupoId: string; titulo: string; imagens: string[] };

export const removerGrupoDeStories: FerramentaDeEscrita<CargaDaRemocaoDoGrupo> = {
  nome: "remover_grupo_de_stories",
  titulo: "Remover grupo de stories",
  descricao: "Apaga um grupo de stories inteiro, com todas as fotos e a capa (e os arquivos). Sempre passa pela prévia com código. Para só esconder, use alterar_grupo_de_stories com ativo falso.",
  campos: { grupo_id: { tipo: "id", descricao: "O grupo (de ver_vitrine_da_loja).", obrigatorio: true } },
  tipo: "escrita",
  sobrescreve: true,
  sensivel: true,
  permissoes: ["catalog.stories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.stories.write", "mexer nos stories da loja");
    const perfil = await lerPerfil(ctx);
    const grupo = grupoDaLista(await lerGrupos(ctx), String(args.grupo_id));
    return {
      previa: {
        o_que_vai_acontecer: `O grupo de stories "${grupo.title}" vai ser apagado, com ${contagem(grupo.items.length, "foto", "fotos")} e a capa. Não dá para desfazer.`,
        grupo: grupo.title,
        fotos: grupo.items.length,
        ativo: grupo.active !== false,
      },
      resumo: `Prévia de remoção do grupo de stories ${grupo.title}`,
      avisos: [],
      carga: {
        perfilId: perfil.id,
        slug: perfil.slug,
        grupoId: grupo.id,
        titulo: grupo.title,
        imagens: [grupo.thumbnail_url, ...grupo.items.map((i) => i.image_url)].filter((x): x is string => Boolean(x)),
      },
    };
  },
  async gravar(ctx, carga) {
    const { data, error } = await ctx.admin.from("catalog_story_groups").delete().eq("id", carga.grupoId).in("user_id", donos(ctx)).select("id");
    if (error) falha("remover o grupo de stories", error);
    if (!(data ?? []).length) recusar("Este grupo de stories já não existe.");
    const grupos = await lerGrupos(ctx);
    await gravarPosicoes(ctx, "catalog_story_groups", grupos, grupos.map((g) => g.id));
    const apagados = await apagarArquivos(ctx, carga.imagens, "stories", imagensEmUso(grupos));
    return {
      dados: { removido: true, grupo: carga.titulo, arquivos_apagados: apagados, grupos_na_loja: grupos.length, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
      resumo: `Removeu o grupo de stories ${carga.titulo}`,
    };
  },
};

export const reorganizarStories: FerramentaDeEscrita<CargaDaOrdem> = {
  nome: "reorganizar_stories",
  titulo: "Reorganizar stories",
  descricao:
    "Muda a ordem dos stories. Sem grupo_id, ordena os GRUPOS (as bolinhas). Com grupo_id, ordena as FOTOS dentro desse grupo. Mande os ids na ordem nova (de ver_vitrine_da_loja); pode mandar só alguns: eles vão para a frente e os outros seguem na ordem de antes.",
  campos: {
    grupo_id: { tipo: "id", descricao: "Para ordenar as fotos dentro de um grupo. Sem ele, ordena os grupos." },
    ordem: { tipo: "textos", descricao: "Os ids (de grupos ou de fotos) na ordem nova.", obrigatorio: true, minItens: 1, maxItens: 200, max: 36 },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["catalog.stories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.stories.write", "mexer nos stories da loja");
    const perfil = await lerPerfil(ctx);
    const grupos = await lerGrupos(ctx);
    const pedida = (args.ordem as string[]).map((id) => id.trim().toLowerCase());
    if (args.grupo_id) {
      const grupo = grupoDaLista(grupos, String(args.grupo_id));
      const ordem = novaOrdem(grupo.items.map((i) => i.id), pedida);
      if (!ordem.ok) recusar(ordem.erro);
      const descricao = ordem.valor.map((id, i) => `${i + 1}. ${id}`);
      return {
        previa: { o_que_vai_acontecer: `As fotos do grupo "${grupo.title}" vão ficar nesta ordem.`, ordem: descricao },
        resumo: `Prévia da nova ordem das fotos do grupo ${grupo.title}`,
        avisos: [],
        carga: { perfilId: perfil.id, slug: perfil.slug, ordem: ordem.valor, grupoId: grupo.id, descricao },
      };
    }
    const ordem = novaOrdem(grupos.map((g) => g.id), pedida);
    if (!ordem.ok) recusar(ordem.erro);
    const porId = new Map(grupos.map((g) => [g.id, g]));
    const descricao = ordem.valor.map((id, i) => `${i + 1}. ${porId.get(id)?.title ?? id}`);
    return {
      previa: { o_que_vai_acontecer: "Os grupos de stories vão ficar nesta ordem.", ordem: descricao },
      resumo: "Prévia da nova ordem dos grupos de stories",
      avisos: [],
      carga: { perfilId: perfil.id, slug: perfil.slug, ordem: ordem.valor, grupoId: null, descricao },
    };
  },
  async gravar(ctx, carga) {
    const grupos = await lerGrupos(ctx);
    const completar = (atuais: string[]) => [...carga.ordem.filter((id) => atuais.includes(id)), ...atuais.filter((id) => !carga.ordem.includes(id))];
    if (carga.grupoId) {
      const grupo = grupoDaLista(grupos, carga.grupoId);
      await gravarPosicoes(ctx, "catalog_story_items", grupo.items, completar(grupo.items.map((i) => i.id)), grupo.id);
    } else {
      await gravarPosicoes(ctx, "catalog_story_groups", grupos, completar(grupos.map((g) => g.id)));
    }
    return {
      dados: { reorganizado: true, ordem: carga.descricao, link_da_loja: linkDaLoja(ctx, carga.slug), ...(await avisar(ctx, carga.perfilId)) },
      resumo: carga.grupoId ? "Reorganizou as fotos de um grupo de stories" : "Reorganizou os grupos de stories",
    };
  },
};

export const ligarStories: FerramentaDeEscrita<CargaDoPerfil> = {
  nome: "ligar_stories",
  titulo: "Ligar ou desligar os stories da loja",
  descricao: "Mostra ou esconde todos os stories da loja de uma vez. Desligar só esconde: os grupos e as fotos continuam guardados.",
  campos: { ligado: { tipo: "booleano", descricao: "Verdadeiro mostra, falso esconde.", obrigatorio: true } },
  tipo: "escrita",
  permissoes: ["catalog.stories.write"],
  async preparar(ctx, args) {
    exigir(ctx, "catalog.stories.write", "mexer nos stories da loja");
    const perfil = await lerPerfil(ctx);
    const ligado = args.ligado === true;
    /* A tela trata "nunca gravado" como ligado. */
    const linhas = [{ campo: "stories na loja", antes: perfil.catalog_stories_enabled === false ? "desligado" : "ligado", depois: simNao(ligado) }];
    const avisos: string[] = [];
    if (ligado) {
      const comFoto = (await lerGrupos(ctx)).filter((g) => g.active !== false && g.items.length > 0).length;
      if (!comFoto) avisos.push("Não há grupo ativo com fotos: os stories ficam ligados, mas só aparecem quando houver (criar_grupo_de_stories e adicionar_story).");
    }
    return {
      previa: { mudancas: linhas },
      resumo: `Prévia: ${ligado ? "ligar" : "desligar"} os stories`,
      avisos,
      carga: { perfilId: perfil.id, slug: perfil.slug, colunas: { catalog_stories_enabled: ligado }, linhas },
    };
  },
  gravar: (ctx, carga) => gravarOpcoesDoPerfil(ctx, carga, `${carga.colunas.catalog_stories_enabled ? "Ligou" : "Desligou"} os stories da loja`),
};

/* ================================================================ leitura */

export const verVitrineDaLoja: FerramentaDeLeitura = {
  nome: "ver_vitrine_da_loja",
  titulo: "Ver a vitrine da loja",
  descricao:
    "Mostra tudo o que enfeita a loja virtual, de uma vez: o carrossel de banners (ligado ou não, e cada banner com id, posição, imagem, para onde leva e se está ativo), a tarja de vantagens, os 2 selos da página do produto, a faixa de anúncio (texto, cupom, contagem, botão, cores), os avisos de compra (prova social) e os stories (grupos com quantas fotos). Para ver as fotos de um grupo de stories com os ids, mande detalhar_grupo. Traz o link da loja. Use antes de mudar qualquer uma dessas coisas.",
  campos: { detalhar_grupo: { tipo: "id", descricao: "Opcional: o id de um grupo de stories para listar as fotos dele, com os ids." } },
  tipo: "leitura",
  permissoes: ["catalog.banners.read", "catalog.banners.write", "marketing.read", "marketing.manage", "catalog.stories.read", "catalog.stories.write"],
  async executar(ctx, args) {
    const perfil = await lerPerfil(ctx);
    const dados: Record<string, unknown> = { _observacao: AVISO_DE_DADOS, link_da_loja: linkDaLoja(ctx, perfil.slug) };
    const semPermissao = "Esta pessoa não tem permissão para ver esta parte.";

    if (pode(ctx, "catalog.banners.read") || pode(ctx, "catalog.banners.write")) {
      const banners = await lerBanners(ctx);
      const produtos = await nomesDosProdutos(ctx, banners.map((b) => b.product_id));
      dados.carrossel_de_banners = {
        ligado: perfil.catalog_slide_enabled === true,
        banners: banners.map((banner, i) => linhaDoBanner(banner, i, produtos)),
        maximo: MAXIMO_DE_BANNERS,
        formato_ideal: "1920 × 540 pixels",
      };
      dados.tarja_de_vantagens = {
        ligada: perfil.catalog_info_strip_enabled !== false,
        itens: tarjaGravada(perfil.catalog_info_strip_items).map((i) => ({ icone: nomeDoIcone(i.icon), titulo: i.title, subtitulo: i.subtitle })),
      };
      dados.selos_da_pagina_do_produto = selosGravados(perfil.catalog_product_trust_items).map((s) => ({ icone: nomeDoIcone(s.icon), titulo: s.title }));
      dados.icones_disponiveis = NOMES_DOS_ICONES;
    } else {
      dados.carrossel_de_banners = semPermissao;
    }

    if (pode(ctx, "marketing.read") || pode(ctx, "marketing.manage")) {
      const cupom = await cupomPeloId(ctx, perfil.catalog_announcement_coupon_id);
      dados.faixa_de_anuncio = {
        ligada: perfil.catalog_announcement_enabled === true,
        texto: perfil.catalog_announcement_text || null,
        cupom: cupom ? { codigo: cupom.code, ativo: cupom.active !== false } : null,
        mostrar_selo_do_cupom: perfil.catalog_announcement_show_coupon_badge !== false,
        contagem_regressiva:
          perfil.catalog_announcement_countdown_enabled === true
            ? `até ${instanteBr(perfil.catalog_announcement_countdown_end_at as string | null) ?? "?"} (Brasília)`
            : "desligada",
        botao: {
          texto: perfil.catalog_announcement_button_label || null,
          leva_para: await descreverBotao(ctx, perfil.catalog_announcement_button_url as string | null),
        },
        cor_de_fundo: perfil.catalog_announcement_background_color || PADRAO_DA_FAIXA.fundo,
        cor_do_texto: perfil.catalog_announcement_text_color || PADRAO_DA_FAIXA.texto,
        cor_do_selo_do_cupom: perfil.catalog_announcement_coupon_badge_color || PADRAO_DA_FAIXA.selo,
        fixa_no_topo: perfil.catalog_announcement_sticky !== false,
      };
      const lim = LIMITES_DA_PROVA_SOCIAL;
      dados.avisos_de_compra = {
        ligados: perfil.catalog_social_proof_enabled === true,
        intervalo_segundos: limitar(perfil.catalog_social_proof_interval_seconds, lim.intervalo.min, lim.intervalo.max, lim.intervalo.padrao),
        duracao_segundos: limitar(perfil.catalog_social_proof_duration_seconds, lim.duracao.min, lim.duracao.max, lim.duracao.padrao),
        posicao: perfil.catalog_social_proof_position === "right" ? "direita" : "esquerda",
        maximo_visivel: limitar(perfil.catalog_social_proof_max_visible, lim.maximo_visivel.min, lim.maximo_visivel.max, lim.maximo_visivel.padrao),
      };
    } else {
      dados.faixa_de_anuncio = semPermissao;
    }

    if (pode(ctx, "catalog.stories.read") || pode(ctx, "catalog.stories.write")) {
      const grupos = await lerGrupos(ctx);
      dados.stories = {
        ligados: perfil.catalog_stories_enabled !== false,
        grupos: grupos.map((g, i) => ({
          id: g.id,
          posicao: i + 1,
          titulo: g.title,
          capa: g.thumbnail_url || null,
          ativo: g.active !== false,
          fotos: g.items.length,
        })),
        maximo_de_grupos: MAXIMO_DE_GRUPOS_DE_STORIES,
        maximo_de_fotos_por_grupo: MAXIMO_DE_STORIES_POR_GRUPO,
      };
      if (args.detalhar_grupo) {
        const grupo = grupoDaLista(grupos, String(args.detalhar_grupo));
        const produtos = await nomesDosProdutos(ctx, grupo.items.map((i) => i.product_id));
        dados.fotos_do_grupo = {
          grupo: grupo.title,
          fotos: grupo.items.map((item, i) => ({
            id: item.id,
            posicao: i + 1,
            imagem: item.image_url,
            produto: item.product_id ? { id: item.product_id, nome: produtos.get(item.product_id) ?? null } : null,
          })),
        };
      }
    } else {
      dados.stories = semPermissao;
    }

    return { dados, resumo: "Consultou a vitrine da loja" };
  },
};

export const FERRAMENTAS_DA_VITRINE: Ferramenta[] = [
  verVitrineDaLoja,
  adicionarBanner,
  alterarBanner,
  removerBanner,
  reorganizarBanners,
  ligarCarrosselDeBanners,
  alterarTarjaDeVantagens,
  alterarSelosDoProduto,
  alterarFaixaDeAnuncio,
  alterarProvaSocial,
  criarGrupoDeStories,
  adicionarStory,
  alterarGrupoDeStories,
  removerStory,
  removerGrupoDeStories,
  reorganizarStories,
  ligarStories,
];
