/*
 * Regras puras da estrutura da loja (seções da página inicial, categorias e
 * ordem dos produtos). Sem banco e sem "@/": o teste roda sozinho com
 * node --test (src/lib/assistentes/loja-estrutura.test.ts).
 *
 * O desenho segue a tela Catálogo > Produtos > Página inicial
 * (app/dashboard/catalog/products/page.tsx) e a loja que mostra as seções
 * (apps/public/src/components/StoreContent.tsx):
 *
 * - o tipo de verdade mora em config.mode; a coluna section_type só guarda
 *   "all_products" ou "manual_products" (a regra antiga do banco aceitava só
 *   esses dois), e a tela grava assim;
 * - seções antigas guardavam a configuração dentro do título, entre
 *   [[KTCFG: e ]]; a leitura junta as duas, com a coluna valendo mais;
 * - a loja ordena as seções só pela posição. "Todos os produtos" é a grade
 *   dos produtos: sem ela, a loja inventa uma no fim.
 */

import { MODOS_DOS_WIDGETS, NOME_DO_WIDGET, widgetNovo, type ModoDoWidget } from "../../widgets-da-home.ts";
import { ehFamiliaVendor } from "../../pagina-inicial.ts";

export type Modo =
  /* Os blocos novos (oferta, cupom, vídeo, perguntas...): lib/widgets-da-home.ts. */
  | ModoDoWidget
  | "manual_products"
  | "all_products"
  | "category_products"
  | "subcategory_products"
  | "image_banner"
  | "category_navigator"
  /* Os tipos do editor da página inicial (migration 196). */
  | "image_grid"
  | "banner_carousel"
  | "featured_products"
  | "category_circles";

export const MODOS: readonly Modo[] = [
  "manual_products",
  "all_products",
  "category_products",
  "subcategory_products",
  "image_banner",
  "category_navigator",
  "image_grid",
  "banner_carousel",
  "featured_products",
  "category_circles",
  ...MODOS_DOS_WIDGETS,
];

/* O nome que a pessoa usa (e que o assistente manda) para cada tipo. */
export const NOME_DO_TIPO: Record<Modo, string> = {
  manual_products: "produtos escolhidos",
  all_products: "todos os produtos",
  category_products: "produtos de uma categoria",
  subcategory_products: "produtos de uma subcategoria",
  image_banner: "banner de imagem",
  category_navigator: "atalhos de categorias",
  image_grid: "imagens lado a lado",
  banner_carousel: "carrossel de banners",
  featured_products: "primeiros produtos",
  category_circles: "categorias em círculos",
  ...(Object.fromEntries(MODOS_DOS_WIDGETS.map((modo) => [modo, NOME_DO_WIDGET[modo].toLowerCase()])) as Record<ModoDoWidget, string>),
};

export const TIPOS_DE_SECAO = MODOS.map((modo) => NOME_DO_TIPO[modo]);

/* O título que a tela sugere ao criar cada tipo. */
export const TITULO_PADRAO: Record<Modo, string> = {
  manual_products: "Nova Seção",
  all_products: "Todos os Produtos",
  category_products: "Seção por Categoria",
  subcategory_products: "Seção por Subcategoria",
  image_banner: "Banner Promocional",
  category_navigator: "Navegue por categoria",
  image_grid: "Imagens lado a lado",
  banner_carousel: "Carrossel de banners",
  featured_products: "Mais vendidos",
  category_circles: "Categorias",
  ...(Object.fromEntries(MODOS_DOS_WIDGETS.map((modo) => [modo, widgetNovo(modo).title])) as Record<ModoDoWidget, string>),
};

export const LIMITE_DE_SECOES = 30;
export const LIMITE_DE_PRODUTOS_POR_SECAO = 60;
export const LIMITE_DE_ATALHOS = 12;
export const LIMITE_DE_PRODUTOS_POR_VEZ = 200;

/* As medidas das imagens, iguais às do ImageUpload da tela. */
export const IMAGEM_DO_BANNER = { largura: 1920, altura: 240, qualidade: 84, pasta: "catalog/sections" } as const;
export const IMAGEM_DO_ATALHO = { largura: 240, altura: 240, qualidade: 86, pasta: "catalog/sections/category-navigator" } as const;

/* As medidas do editor da página inicial (iguais às de apps/admin/src/lib/pagina-inicial.ts). */
export type MedidaDeImagem = { largura: number; altura: number; qualidade: number; pasta: string };
const PASTA_DO_EDITOR = "catalog/sections/pagina-inicial";
export const FORMATOS_DE_BANNER: Record<string, MedidaDeImagem & { proporcao: string }> = {
  faixa: { largura: 1920, altura: 240, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "8 por 1" },
  largo: { largura: 1920, altura: 600, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "16 por 5" },
  panoramico: { largura: 1920, altura: 540, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "32 por 9" },
  cinema: { largura: 1920, altura: 823, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "21 por 9" },
};
export const PROPORCOES_DA_IMAGEM: Record<string, MedidaDeImagem & { proporcao: string }> = {
  quadrado: { largura: 1200, altura: 1200, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "1 por 1" },
  retrato: { largura: 960, altura: 1200, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "4 por 5" },
  paisagem: { largura: 1440, altura: 900, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "8 por 5" },
  faixa: { largura: 1800, altura: 600, qualidade: 84, pasta: PASTA_DO_EDITOR, proporcao: "3 por 1" },
};
export const ARRUMACOES = ["2", "3", "4", "1+2", "2+1"] as const;
export const QUANTAS_NA_ARRUMACAO: Record<string, number> = { "2": 2, "3": 3, "4": 4, "1+2": 3, "2+1": 3 };
export const NO_CELULAR = { deslizar: "deslizar", "dois por linha": "lado_a_lado", "uma embaixo da outra": "empilhar" } as const;
export const LIMITE_DO_CARROSSEL = 8;

export function semAcento(valor: unknown) {
  return String(valor ?? "")
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .toLowerCase()
    .replace(/\s+/g, " ")
    .trim();
}

/** "Banner de imagem", "banner", "image_banner"... vira o modo. Null = não reconhecido. */
export function modoPeloNome(valor: unknown): Modo | null {
  const texto = semAcento(valor).replace(/_/g, " ");
  if (!texto) return null;
  for (const modo of MODOS) {
    if (texto === semAcento(NOME_DO_TIPO[modo]) || texto === modo.replace(/_/g, " ")) return modo;
  }
  const atalhos: Array<[RegExp, Modo]> = [
    /* Os blocos novos primeiro: "imagem com pontos" não é banner, "lista de mais
       vendidos de verdade" não é "primeiros produtos". */
    [/(oferta|relampago)/, "flash_offer"],
    [/cupo/, "coupon_highlight"],
    [/(corte|peca ate|pedido ate)/, "order_cutoff"],
    [/live/, "next_live"],
    [/(lista automatica|lancamento|vendidos de verdade)/, "auto_list"],
    [/(kit|combo|compre junto)/, "combo_highlight"],
    [/visto/, "recently_viewed"],
    [/video/, "video_section"],
    [/(texto com imagem|^texto)/, "text_image"],
    [/(pergunta|faq)/, "faq"],
    [/(onde estamos|endereco|mapa)/, "store_location"],
    [/(faixa que|letreiro|desliza)/, "marquee"],
    [/(pontos|clicave)/, "shoppable_image"],
    [/antes e depois/, "before_after"],
    [/^(manual|escolhid|selecionad)/, "manual_products"],
    [/^todos/, "all_products"],
    [/carross/, "banner_carousel"],
    [/(lado a lado|^imagens|^grade de imagens|^mosaico)/, "image_grid"],
    [/^(primeiros|mais vendidos|destaques)/, "featured_products"],
    [/circulo/, "category_circles"],
    [/subcategoria/, "subcategory_products"],
    [/^(atalho|navega)/, "category_navigator"],
    [/categoria/, "category_products"],
    [/^(banner|imagem)/, "image_banner"],
  ];
  for (const [regra, modo] of atalhos) if (regra.test(texto)) return modo;
  return null;
}

/** O que vai na coluna section_type, como a tela grava. */
export function tipoGravado(modo: Modo): "all_products" | "manual_products" {
  return modo === "all_products" ? "all_products" : "manual_products";
}

/**
 * Atalhos e o banner antigo (sem formato) ficam sempre na largura "caixa",
 * como a tela antiga travava. O banner com formato (editor da página inicial)
 * pode ir de ponta a ponta.
 */
export function larguraEfetiva(modo: Modo, largura: "full" | "contained", config?: Record<string, unknown>): "full" | "contained" {
  if (modo === "image_banner" && config && typeof config.formato === "string" && config.formato) return largura;
  return modo === "image_banner" || modo === "category_navigator" ? "contained" : largura;
}

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

const PREFIXO_ANTIGO = "[[KTCFG:";
const SUFIXO_ANTIGO = "]]";

/** Separa o título limpo da configuração antiga escondida nele. */
export function metadadosAntigos(tituloBruto: unknown): { titulo: string; config: Record<string, unknown> } {
  const titulo = String(tituloBruto ?? "");
  const inicio = titulo.indexOf(PREFIXO_ANTIGO);
  if (inicio === -1) return { titulo, config: {} };
  const fim = titulo.indexOf(SUFIXO_ANTIGO, inicio + PREFIXO_ANTIGO.length);
  if (fim === -1) return { titulo, config: {} };
  const limpo = `${titulo.slice(0, inicio)}${titulo.slice(fim + SUFIXO_ANTIGO.length)}`.trim();
  try {
    const lido = JSON.parse(decodeURIComponent(titulo.slice(inicio + PREFIXO_ANTIGO.length, fim)));
    if (lido && typeof lido === "object" && !Array.isArray(lido)) return { titulo: limpo, config: lido as Record<string, unknown> };
  } catch {
    /* configuração estragada: fica só o título */
  }
  return { titulo: limpo, config: {} };
}

function configDaColuna(config: unknown): Record<string, unknown> {
  if (config && typeof config === "object" && !Array.isArray(config)) return config as Record<string, unknown>;
  if (typeof config === "string") {
    try {
      const lido = JSON.parse(config);
      if (lido && typeof lido === "object" && !Array.isArray(lido)) return lido as Record<string, unknown>;
    } catch {
      return {};
    }
  }
  return {};
}

export type SecaoBruta = {
  id: string;
  title?: string | null;
  config?: unknown;
  section_type?: string | null;
  type?: string | null;
  position?: number | null;
  active?: boolean | null;
  placement?: string | null;
  layout_width?: string | null;
  title_align?: string | null;
  title_size?: string | null;
  background_color?: string | null;
  text_color?: string | null;
};

/** Título limpo, configuração juntada e o modo, como a tela e a loja leem. */
export function lerSecao(secao: SecaoBruta): { titulo: string; config: Record<string, unknown>; modo: Modo; legado: boolean } {
  const antigo = metadadosAntigos(secao.title);
  const config = { ...antigo.config, ...configDaColuna(secao.config) };
  const bruto = config.mode ?? secao.section_type ?? secao.type;
  const modo = MODOS.includes(bruto as Modo) ? (bruto as Modo) : "manual_products";
  return { titulo: antigo.titulo, config, modo, legado: antigo.titulo !== String(secao.title ?? "") };
}

/* ---------------------------------------------------------- aparência */

export const LARGURAS = { total: "full", caixa: "contained" } as const;
export const ALINHAMENTOS = { esquerda: "left", centro: "center", direita: "right" } as const;
export const TAMANHOS = { pequeno: "sm", medio: "md", grande: "lg", enorme: "xl" } as const;

export function nomeDe<T extends Record<string, string>>(mapa: T, valor: unknown): string | null {
  const achado = Object.entries(mapa).find(([, v]) => v === valor);
  return achado ? achado[0] : null;
}

/** "#abc", "ABCDEF", "#AbCdEf" viram "#aabbcc"/"#abcdef" (o seletor de cor da tela grava minúsculo). */
export function corHex(valor: unknown): string | null {
  const texto = String(valor ?? "").trim().replace(/^#/, "");
  if (/^[0-9a-f]{3}$/i.test(texto)) return `#${texto.split("").map((c) => c + c).join("")}`.toLowerCase();
  if (/^[0-9a-f]{6}$/i.test(texto)) return `#${texto}`.toLowerCase();
  return null;
}

/* --------------------------------------------------------- link do banner */

/**
 * O link do banner leva o visitante para algum lugar. O assistente só grava
 * caminho da própria loja ("/aurora/produto/caneca", "#produtos") ou endereço
 * completo da própria loja (o endereço do Ktalogu ou o domínio próprio). Link
 * de fora a pessoa cola pelo painel: um texto lido pelo assistente não
 * consegue mandar o visitante para outro site.
 */
export function linkDoBanner(valor: unknown, enderecosDaLoja: string[], jaEstava?: string | null): { ok: true; link: string } | { ok: false; erro: string } {
  const texto = String(valor ?? "").trim();
  if (!texto) return { ok: true, link: "" };
  if (texto.length > 500) return { ok: false, erro: "O link do banner passou de 500 letras." };
  if (jaEstava && texto === jaEstava) return { ok: true, link: texto };
  if (/^\/(?!\/)[^\s<>"']*$/.test(texto) || /^#[\w-]{1,80}$/.test(texto)) return { ok: true, link: texto };
  let endereco: URL;
  try {
    endereco = new URL(texto);
  } catch {
    return { ok: false, erro: 'Link do banner inválido. Use um caminho da loja, como "/minha-loja/produto/caneca", ou o endereço completo da loja.' };
  }
  if (endereco.protocol !== "https:" && endereco.protocol !== "http:") return { ok: false, erro: "O link do banner precisa começar com https://." };
  const hosts = new Set(enderecosDaLoja.map((h) => h.toLowerCase().replace(/^www\./, "")).filter(Boolean));
  if (!hosts.has(endereco.hostname.toLowerCase().replace(/^www\./, ""))) {
    return {
      ok: false,
      erro: `Por segurança, o assistente só coloca no banner link da própria loja (${[...hosts].join(" ou ") || "o endereço da loja"}) ou um caminho como "/minha-loja/produto/caneca". Link de outro site a própria pessoa cola pelo painel, em Catálogo > Produtos > Página inicial. Mostre isso a ela; não tente de novo com o link disfarçado.`,
    };
  }
  return { ok: true, link: endereco.toString() };
}

/* ---------------------------------------------------- lugar na página */

/* A grade dos produtos quando a empresa não tem a seção "Todos os produtos":
   a loja põe uma no fim, sempre. */
export const PRODUTOS_INVENTADOS = "__produtos__";

export const LUGARES = ["no topo", "acima dos produtos", "abaixo dos produtos", "no fim"] as const;
export type Lugar = (typeof LUGARES)[number];

type ItemDaOrdem = { id: string; modo: Modo; ativa: boolean };

/**
 * A ordem da página como o visitante vê: as seções ativas por posição, com a
 * grade dos produtos (a de verdade ou a inventada no fim).
 */
export function ordemDaPagina(secoes: ItemDaOrdem[]): string[] {
  const ativas = secoes.filter((s) => s.ativa);
  const ids = ativas.map((s) => s.id);
  if (!ativas.some((s) => s.modo === "all_products")) ids.push(PRODUTOS_INVENTADOS);
  return ids;
}

/** Onde fica a grade dos produtos numa ordem (id real ou a inventada). */
export function indiceDosProdutos(ordem: string[], idDosProdutos: string | null) {
  const real = idDosProdutos ? ordem.indexOf(idDosProdutos) : -1;
  return real >= 0 ? real : ordem.indexOf(PRODUTOS_INVENTADOS);
}

/**
 * Encaixa uma seção na ordem. `posicao` (1 = primeiro bloco da página) vale
 * mais que `lugar`. Devolve a ordem nova (com a grade dos produtos, real ou
 * inventada) e se a grade inventada deixou de ser a última — aí é preciso
 * criar a seção "Todos os produtos" de verdade para a loja respeitar a ordem.
 */
export function encaixar(
  ordem: string[],
  idNovo: string,
  pedido: { lugar?: Lugar; posicao?: number },
  idDosProdutos: string | null
): { ordem: string[]; precisaDaGradeDeVerdade: boolean } {
  const base = ordem.filter((id) => id !== idNovo);
  let indice: number;
  if (pedido.posicao !== undefined) {
    indice = Math.min(Math.max(Math.trunc(pedido.posicao) - 1, 0), base.length);
  } else {
    const produtos = indiceDosProdutos(base, idDosProdutos);
    switch (pedido.lugar ?? "acima dos produtos") {
      case "no topo":
        indice = 0;
        break;
      case "abaixo dos produtos":
        indice = produtos >= 0 ? produtos + 1 : base.length;
        break;
      case "no fim":
        indice = base.length;
        break;
      default:
        indice = produtos >= 0 ? produtos : base.length;
    }
  }
  const nova = [...base.slice(0, indice), idNovo, ...base.slice(indice)];
  const inventada = nova.indexOf(PRODUTOS_INVENTADOS);
  return { ordem: nova, precisaDaGradeDeVerdade: inventada >= 0 && inventada !== nova.length - 1 };
}

/** "acima"/"abaixo" (a coluna placement) pela ordem final. */
export function placementNaOrdem(ordem: string[], id: string, idDosProdutos: string | null): "above" | "below" {
  const produtos = indiceDosProdutos(ordem, idDosProdutos);
  const meu = ordem.indexOf(id);
  return produtos >= 0 && meu > produtos ? "below" : "above";
}

/**
 * A ordem pedida em reorganizar_secoes: todas as seções ativas, uma vez cada.
 * "produtos" vale pela grade dos produtos (a de verdade ou a inventada).
 */
export function conferirOrdemPedida(
  pedida: string[],
  ativas: string[],
  idDosProdutos: string | null
): { ok: true; ordem: string[] } | { ok: false; erro: string } {
  const traduzida = pedida.map((id) => {
    const limpo = id.trim().toLowerCase();
    if (limpo === "produtos" || limpo === "todos os produtos") return idDosProdutos ?? PRODUTOS_INVENTADOS;
    return limpo;
  });
  const esperadas = new Set(idDosProdutos ? ativas : [...ativas, PRODUTOS_INVENTADOS]);
  const repetida = traduzida.find((id, i) => traduzida.indexOf(id) !== i);
  if (repetida) return { ok: false, erro: `A seção ${repetida === PRODUTOS_INVENTADOS ? "produtos" : repetida} aparece duas vezes na ordem.` };
  const estranha = traduzida.find((id) => !esperadas.has(id));
  if (estranha) return { ok: false, erro: `A seção ${estranha} não é uma seção ativa desta loja. Use os ids de ver_estrutura_da_loja.` };
  const faltando = [...esperadas].filter((id) => !traduzida.includes(id));
  if (faltando.length) {
    const nomes = faltando.map((id) => (id === PRODUTOS_INVENTADOS ? '"produtos"' : id));
    return { ok: false, erro: `Mande todas as seções ativas na ordem nova. Faltaram: ${nomes.join(", ")}.` };
  }
  return { ok: true, ordem: traduzida };
}

/**
 * As posições a gravar: as seções na ordem da página primeiro (0, 1, 2...),
 * as desligadas depois, na ordem em que estavam. A grade inventada não é
 * gravada (não existe no banco).
 */
export function posicoesDasSecoes(ordemDaPaginaNova: string[], todas: Array<{ id: string; position?: number | null }>) {
  const reais = ordemDaPaginaNova.filter((id) => id !== PRODUTOS_INVENTADOS);
  const resto = [...todas]
    .filter((s) => !reais.includes(s.id))
    .sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
    .map((s) => s.id);
  const final = [...reais, ...resto];
  const antes = new Map(todas.map((s) => [s.id, s.position ?? null]));
  return final.map((id, posicao) => ({ id, posicao, mudou: antes.get(id) !== posicao }));
}

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

export type PedidoDeProdutos = { acao: "trocar" | "adicionar" | "remover"; ids: string[]; posicao?: number };

/**
 * A lista nova de produtos de uma seção "produtos escolhidos". Trocar
 * substitui tudo; adicionar põe no fim (ou na posição, 1 = primeiro) e ignora
 * quem já está; remover tira. Repetidos contam uma vez.
 */
export function novaListaDaSecao(atual: string[], pedido: PedidoDeProdutos): { ok: true; lista: string[]; ignorados: string[] } | { ok: false; erro: string } {
  const ids = [...new Set(pedido.ids.map((id) => id.trim().toLowerCase()).filter(Boolean))];
  let lista: string[];
  const ignorados: string[] = [];
  if (pedido.acao === "trocar") {
    lista = ids;
  } else if (pedido.acao === "remover") {
    for (const id of ids) if (!atual.includes(id)) ignorados.push(id);
    lista = atual.filter((id) => !ids.includes(id));
  } else {
    const novos = ids.filter((id) => {
      if (atual.includes(id)) {
        ignorados.push(id);
        return false;
      }
      return true;
    });
    const onde = pedido.posicao === undefined ? atual.length : Math.min(Math.max(Math.trunc(pedido.posicao) - 1, 0), atual.length);
    lista = [...atual.slice(0, onde), ...novos, ...atual.slice(onde)];
  }
  if (lista.length > LIMITE_DE_PRODUTOS_POR_SECAO) {
    return { ok: false, erro: `Uma seção aceita até ${LIMITE_DE_PRODUTOS_POR_SECAO} produtos; ficaria com ${lista.length}. Para mostrar mais, use uma seção de categoria.` };
  }
  return { ok: true, lista, ignorados };
}

/* ---------------------------------------------------- ordem dos produtos */

export type PosicaoDoProduto = { id: string; posicao: number | null };

/**
 * A ordem nova dos produtos da loja. `inicio`: estes ids vão para a frente,
 * nesta ordem, e o resto segue como estava. `mover` + `para` (1 = primeiro):
 * um produto só, para aquele lugar.
 */
export function novaOrdemDosProdutos(
  atual: string[],
  pedido: { inicio?: string[]; mover?: string; para?: number }
): { ok: true; ordem: string[] } | { ok: false; erro: string } {
  const existe = new Set(atual);
  if (pedido.inicio?.length) {
    const ids = [...new Set(pedido.inicio.map((id) => id.trim().toLowerCase()))];
    const estranho = ids.find((id) => !existe.has(id));
    if (estranho) return { ok: false, erro: `O produto ${estranho} não é desta loja (ou é digital). Use buscar_produtos para achar o id.` };
    return { ok: true, ordem: [...ids, ...atual.filter((id) => !ids.includes(id))] };
  }
  if (pedido.mover) {
    const id = pedido.mover.trim().toLowerCase();
    if (!existe.has(id)) return { ok: false, erro: `O produto ${id} não é desta loja (ou é digital). Use buscar_produtos para achar o id.` };
    const sem = atual.filter((outro) => outro !== id);
    const onde = Math.min(Math.max(Math.trunc(pedido.para ?? 1) - 1, 0), sem.length);
    return { ok: true, ordem: [...sem.slice(0, onde), id, ...sem.slice(onde)] };
  }
  return { ok: false, erro: "Diga a ordem: mande ordem (lista de ids que vão para a frente) ou produto_id com posicao (1 = primeiro)." };
}

/**
 * As posições a gravar, mexendo no menor número de produtos: quando só um
 * produto mudou de lugar e há um número livre entre os vizinhos (ou antes do
 * primeiro), só ele é gravado. Senão a lista toda é renumerada 0, 1, 2... e
 * só os que mudaram são gravados, como a tela faz ao arrastar.
 */
export function posicoesDosProdutos(atual: PosicaoDoProduto[], ordem: string[]): Array<{ id: string; posicao: number }> {
  const antes = new Map(atual.map((p) => [p.id, p.posicao]));
  const ordemAtual = atual.map((p) => p.id);
  const mudados = ordem.filter((id, i) => ordemAtual[i] !== id);
  if (mudados.length === 0) return [];

  /* Um produto só que andou: os outros mantêm a ordem relativa. */
  const fora = ordem.find((id) => {
    const semEle = ordem.filter((x) => x !== id);
    const atualSemEle = ordemAtual.filter((x) => x !== id);
    return semEle.every((x, i) => atualSemEle[i] === x);
  });
  const todosNumerados = atual.every((p) => p.posicao !== null && Number.isFinite(p.posicao));
  if (fora && todosNumerados) {
    const i = ordem.indexOf(fora);
    const anterior = i > 0 ? (antes.get(ordem[i - 1]) as number) : null;
    const seguinte = i < ordem.length - 1 ? (antes.get(ordem[i + 1]) as number) : null;
    let livre: number | null = null;
    if (anterior === null && seguinte !== null) livre = seguinte - 1;
    else if (seguinte === null && anterior !== null) livre = anterior + 1;
    else if (anterior !== null && seguinte !== null && seguinte - anterior >= 2) livre = anterior + Math.floor((seguinte - anterior) / 2);
    if (livre !== null && Math.abs(livre) < 2_000_000_000) return [{ id: fora, posicao: livre }];
  }
  return ordem.map((id, posicao) => ({ id, posicao })).filter((p) => antes.get(p.id) !== p.posicao);
}

/**
 * Para levar alguns produtos para a frente sem renumerar a loja inteira:
 * eles ganham números antes do menor que existe (menor - n, ..., menor - 1).
 * Null quando há produto sem posição gravada (aí a lista toda é numerada).
 */
export function posicoesParaAFrente(atual: PosicaoDoProduto[], ids: string[]): Array<{ id: string; posicao: number }> | null {
  if (!atual.length || atual.some((p) => p.posicao === null || !Number.isFinite(p.posicao))) return null;
  const outros = atual.filter((p) => !ids.includes(p.id));
  const menor = outros.length ? Math.min(...outros.map((p) => p.posicao as number)) : 0;
  if (menor - ids.length < -2_000_000_000) return null;
  const antes = new Map(atual.map((p) => [p.id, p.posicao]));
  return ids.map((id, i) => ({ id, posicao: menor - ids.length + i })).filter((p) => antes.get(p.id) !== p.posicao);
}

/** A ordem que a loja usa: posição crescente (sem posição no fim), depois os mais novos. */
export function ordenarComoALoja<T extends { catalog_position?: number | null; created_at?: string | null }>(produtos: T[]): T[] {
  return [...produtos].sort((a, b) => {
    const pa = a.catalog_position ?? null;
    const pb = b.catalog_position ?? null;
    if (pa !== pb) {
      if (pa === null) return 1;
      if (pb === null) return -1;
      return pa - pb;
    }
    return String(b.created_at ?? "").localeCompare(String(a.created_at ?? ""));
  });
}

/* ------------------------------------------------------------ categorias */

export type CategoriaBruta = { id: string; name: string | null; slug: string | null; parent_id: string | null; is_visible: boolean | null };

/** O mesmo endereço que a tela e o serviço de categorias montam a partir do nome. */
export function slugDaCategoria(nome: string) {
  return nome
    .toLowerCase()
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .replace(/[^\w\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-");
}

/** "Roupas / Camisetas": o caminho até a raiz, sem cair em ciclo. */
export function caminhoDaCategoria(id: string, porId: Map<string, CategoriaBruta>) {
  const nomes: string[] = [];
  const vistos = new Set<string>();
  let atual: string | null = id;
  while (atual && !vistos.has(atual)) {
    vistos.add(atual);
    const categoria = porId.get(atual);
    if (!categoria) break;
    nomes.unshift(categoria.name ?? "");
    atual = categoria.parent_id;
  }
  return nomes.join(" / ");
}

/** A própria categoria e todas as de dentro dela (a seção "categoria" mostra todas). */
export function descendentes(id: string, categorias: CategoriaBruta[]) {
  const achadas = new Set<string>([id]);
  const pilha = [id];
  while (pilha.length) {
    const atual = pilha.pop() as string;
    for (const c of categorias) {
      if (c.parent_id === atual && !achadas.has(c.id)) {
        achadas.add(c.id);
        pilha.push(c.id);
      }
    }
  }
  return achadas;
}

/**
 * Acha a categoria pelo id, pelo endereço (slug), pelo nome ou pelo caminho
 * ("Roupas / Camisetas"). Nome repetido em lugares diferentes pede o caminho.
 */
export function acharCategoria(valor: unknown, categorias: CategoriaBruta[]): { ok: true; categoria: CategoriaBruta } | { ok: false; erro: string } {
  const texto = String(valor ?? "").trim();
  if (!texto) return { ok: false, erro: "Diga a categoria (id, nome ou endereço)." };
  const porId = new Map(categorias.map((c) => [c.id, c]));
  const direta = porId.get(texto.toLowerCase());
  if (direta) return { ok: true, categoria: direta };
  const alvo = semAcento(texto);
  const pelaLinha = categorias.filter((c) => semAcento(c.slug) === alvo);
  if (pelaLinha.length === 1) return { ok: true, categoria: pelaLinha[0] };
  const peloCaminho = !texto.includes("/") ? [] : categorias.filter((c) => semAcento(caminhoDaCategoria(c.id, porId)).replace(/\s*\/\s*/g, "/") === alvo.replace(/\s*\/\s*/g, "/"));
  if (peloCaminho.length === 1) return { ok: true, categoria: peloCaminho[0] };
  const peloNome = categorias.filter((c) => semAcento(c.name) === alvo);
  if (peloNome.length === 1) return { ok: true, categoria: peloNome[0] };
  if (peloNome.length > 1) {
    return { ok: false, erro: `Há ${peloNome.length} categorias chamadas "${texto}": ${peloNome.map((c) => `${caminhoDaCategoria(c.id, porId)} (${c.id})`).join("; ")}. Mande o id ou o caminho.` };
  }
  const parecidas = categorias.filter((c) => semAcento(c.name).includes(alvo)).slice(0, 8);
  return {
    ok: false,
    erro: `Categoria "${texto}" não encontrada.${parecidas.length ? ` Parecidas: ${parecidas.map((c) => caminhoDaCategoria(c.id, porId)).join(", ")}.` : " Use ver_estrutura_da_loja para ver as categorias."}`,
  };
}

/* A regra de preço da categoria, com os mesmos campos do formulário. */
export type RegraDePreco = {
  marginTargetPct: number;
  marginMinPct: number;
  rounding: { type: "NONE" | "END_WITH" | "NEAREST_INT" | "MULTIPLE_OF"; cents?: number; step?: number };
  [outro: string]: unknown;
};

export const REGRA_PADRAO: RegraDePreco = { marginTargetPct: 50, marginMinPct: 35, rounding: { type: "END_WITH", cents: 90 } };

export const ARREDONDAMENTOS = { sem: "NONE", inteiro: "NEAREST_INT", centavos: "END_WITH" } as const;

/** Junta o que mudou na regra de preço, mantendo o resto (faixas por quantidade, passo...). */
export function montarRegraDePreco(
  atual: unknown,
  pedido: { margem_alvo?: number; margem_minima?: number; arredondamento?: keyof typeof ARREDONDAMENTOS; centavos?: number }
): { ok: true; regra: RegraDePreco; mudou: boolean } | { ok: false; erro: string } {
  const base = (atual && typeof atual === "object" && !Array.isArray(atual) ? atual : REGRA_PADRAO) as RegraDePreco;
  const regra: RegraDePreco = { ...base, rounding: { ...(base.rounding ?? REGRA_PADRAO.rounding) } };
  let mudou = false;
  if (pedido.margem_alvo !== undefined) {
    regra.marginTargetPct = pedido.margem_alvo;
    mudou = true;
  }
  if (pedido.margem_minima !== undefined) {
    regra.marginMinPct = pedido.margem_minima;
    mudou = true;
  }
  if (pedido.arredondamento !== undefined) {
    regra.rounding = { ...regra.rounding, type: ARREDONDAMENTOS[pedido.arredondamento] };
    if (regra.rounding.type === "END_WITH" && regra.rounding.cents === undefined) regra.rounding.cents = 90;
    mudou = true;
  }
  if (pedido.centavos !== undefined) {
    if (regra.rounding.type !== "END_WITH") return { ok: false, erro: 'Centavos só valem com arredondamento "centavos" (o preço termina com esses centavos).' };
    regra.rounding = { ...regra.rounding, cents: Math.min(99, Math.max(0, Math.trunc(pedido.centavos))) };
    mudou = true;
  }
  if (Number(regra.marginMinPct) > Number(regra.marginTargetPct)) {
    return { ok: false, erro: `A margem mínima (${regra.marginMinPct}%) não pode ser maior que a margem alvo (${regra.marginTargetPct}%).` };
  }
  return { ok: true, regra, mudou };
}

export function regraDePrecoEmPalavras(regra: unknown) {
  const r = (regra && typeof regra === "object" ? regra : REGRA_PADRAO) as RegraDePreco;
  const arredonda =
    r.rounding?.type === "END_WITH"
      ? `preço terminando em ,${String(r.rounding.cents ?? 90).padStart(2, "0")}`
      : r.rounding?.type === "NEAREST_INT"
        ? "inteiro mais próximo"
        : r.rounding?.type === "MULTIPLE_OF"
          ? `múltiplo de ${r.rounding.step ?? "?"}`
          : "sem arredondamento";
  return `margem alvo ${r.marginTargetPct ?? "?"}%, mínima ${r.marginMinPct ?? "?"}%, ${arredonda}`;
}

/* ------------------------------------------------------------- template */

export const NOME_DO_TEMPLATE: Record<string, string> = {
  default: "Padrão",
  neo: "Neo",
  vitrine: "Vitrine",
  vendor: "Vendor",
  boutique: "Ateliê",
};

/**
 * A família Vendor (Vendor e Boutique) monta a página inicial sozinha e só
 * passa a mostrar as seções depois que a loja publica a página pelo editor
 * (Catálogo > Produtos > Página inicial; migration 196). Os outros modelos
 * sempre mostram.
 */
export function templateMostraSecoes(tema: unknown, homeEditada = false) {
  return !ehFamiliaVendor(tema) || homeEditada;
}

export const AVISO_DO_VENDOR =
  "A loja usa o modelo Vendor ou Ateliê, que monta a página inicial sozinho e NÃO mostra estas seções até a página inicial ser publicada pelo editor do painel (Catálogo > Produtos > Página inicial). Elas ficam guardadas. Diga à pessoa para abrir o editor e publicar (lá ela vê a loja de verdade enquanto arruma), ou para voltar ao modelo Padrão ou Vitrine (alterar_aparencia_da_loja, campo modelo).";
