/*
 * As regras da loja, o cabeçalho, o contato e o rodapé, o resto do SEO, o
 * corte de produção e o chat, pelo assistente: só as regras, sem banco. Sem
 * importação com "@/" para o teste rodar no `node --test` puro (ver
 * ../loja-regras-e-contato.test.ts).
 *
 * As colunas e os valores aceitos são os mesmos que a tela Catálogo grava
 * (app/dashboard/catalog/page.tsx, abas Personalização, Regras e SEO) e que o
 * banco confere (as CHECK de `profiles`, migration 159 em diante). Texto ou
 * número vazio vira NULL, que é como a loja sabe que deve usar o padrão dela.
 *
 * Cada grupo é uma tabela "nome que o assistente usa → coluna". Uma função só
 * (montarMudanca) confere o pedido, compara com o que está gravado e devolve
 * as colunas que mudam, o antes de cada uma e as linhas legíveis da prévia.
 */

import { contatosNovos } from "../contatos.ts";
import type { Campo, Campos } from "../esquema.ts";

export type Opcao = { valor: string; nome: string; significado: string };

type Base = {
  coluna: string;
  rotulo: string;
  /* O que o campo faz na loja, em português de gente (vai para a leitura e o esquema). */
  explicacao: string;
};

export type CampoDeAjuste = Base &
  (
    | { tipo: "opcao"; opcoes: readonly Opcao[]; padrao: string }
    | { tipo: "booleano"; padrao: boolean; invertido?: boolean }
    /* Texto que o cliente lê: passa pela peneira de contatos (../contatos.ts). */
    | { tipo: "texto"; max: number; doCliente: boolean; manterLinhas?: boolean; padraoLegivel?: string }
    /* zeroEhNulo: 0 grava NULL, que é "sem mínimo" (como a tela). */
    | { tipo: "inteiro" | "dinheiro"; min: number; max: number; zeroEhNulo?: boolean; padrao?: number | null }
    | { tipo: "data" }
    | { tipo: "hora" }
    /* Telefone com DDD, gravado com a máscara da tela: "(11) 98765-4321". */
    | { tipo: "telefone" }
    | { tipo: "rede"; rede: Rede }
    | { tipo: "usuario_x" }
  );

export type Grupo = Record<string, CampoDeAjuste>;

/* ======================================================= regras da loja */

export const OPCOES_DE_PRECO: readonly Opcao[] = [
  { valor: "public", nome: "preço para todos", significado: "Como sempre: o preço aparece para quem abrir a loja." },
  { valor: "hidden", nome: "esconder preço", significado: "Ninguém vê o preço: no lugar entra um texto (ex.: Sob consulta) e o cliente pede orçamento." },
  { valor: "clients", nome: "preço só para clientes logados", significado: "Visitante vê um botão de entrar; só o cliente com cadastro aprovado vê o preço." },
];

export const OPCOES_DE_COMPRA: readonly Opcao[] = [
  { valor: "cart", nome: "carrinho", significado: "Como sempre: carrinho, WhatsApp ou pagamento online conforme o modelo da loja." },
  { valor: "quote", nome: "orçamento", significado: "O cliente monta a lista e pede orçamento; o pedido chega ao painel com os preços do cadastro e a loja responde." },
  { valor: "whatsapp", nome: "WhatsApp", significado: "Sem carrinho: o botão do produto abre o WhatsApp da loja." },
  { valor: "showcase", nome: "só vitrine", significado: "Sem botão de compra: a loja só mostra os produtos." },
];

export const OPCOES_DE_ACESSO: readonly Opcao[] = [
  { valor: "public", nome: "loja aberta", significado: "Qualquer pessoa abre a loja e vê os produtos." },
  { valor: "clients", nome: "só clientes logados", significado: "Quem não entrou na conta vê só a tela de entrar. A loja sai do Google e do diretório." },
];

export const OPCOES_DE_CADASTRO: readonly Opcao[] = [
  { valor: "open", nome: "livre", significado: "Como sempre: o cliente se cadastra e já entra." },
  { valor: "approval", nome: "com aprovação", significado: "O cadastro fica pendente até a loja aprovar em Clientes." },
  { valor: "closed", nome: "fechado", significado: "Ninguém se cadastra pela loja: só a loja cria o acesso, em Clientes > Gerar primeiro acesso." },
];

export const OPCOES_DE_ESTOQUE: readonly Opcao[] = [
  { valor: "exact", nome: "mostrar o saldo", significado: "A página do produto mostra quantas unidades há." },
  { valor: "low_only", nome: "só avisar últimas unidades", significado: "Só aparece 'últimas unidades' quando o saldo fica abaixo do limite." },
  { valor: "never", nome: "não mostrar", significado: "A página do produto não fala do saldo." },
];

export const OPCOES_DE_ESGOTADO: readonly Opcao[] = [
  { valor: "show", nome: "mostrar como esgotado", significado: "O produto aparece com o selo Esgotado e não dá para comprar." },
  { valor: "hide", nome: "esconder da loja", significado: "O produto esgotado some da loja." },
  { valor: "quote", nome: "virar pedir orçamento", significado: "O produto esgotado continua na loja, com o botão de pedir orçamento." },
];

export const OPCOES_DE_SITUACAO: readonly Opcao[] = [
  { valor: "open", nome: "aberta", significado: "Como sempre: a loja vende." },
  { valor: "paused", nome: "em pausa", significado: "A loja aparece, mas ninguém fecha pedido. Mostra o recado." },
  { valor: "closed", nome: "fechada", significado: "Quem entra vê uma página 'em breve' com o recado. A equipe continua entrando para conferir." },
];

export const REGRAS_DA_LOJA: Grupo = {
  quem_ve_o_preco: { coluna: "catalog_price_mode", rotulo: "Quem vê o preço", tipo: "opcao", opcoes: OPCOES_DE_PRECO, padrao: "public", explicacao: "Vale para a loja inteira. A equipe vê os preços sempre, no navegador em que usa o painel." },
  texto_no_lugar_do_preco: { coluna: "catalog_price_hidden_text", rotulo: "Texto no lugar do preço", tipo: "texto", max: 60, doCliente: true, padraoLegivel: "Sob consulta", explicacao: "Aparece onde ficaria o preço escondido e nos produtos marcados como sob consulta." },
  texto_do_botao_de_entrar: { coluna: "catalog_price_login_text", rotulo: "Texto do botão de entrar", tipo: "texto", max: 60, doCliente: true, padraoLegivel: "Entre para ver o preço", explicacao: "O botão que leva o visitante a entrar, quando o preço é só para clientes logados." },
  aviso_embaixo_do_preco: { coluna: "catalog_price_notice", rotulo: "Aviso embaixo do preço", tipo: "texto", max: 140, doCliente: true, explicacao: "Uma linha curta embaixo do preço e no carrinho. Em branco, não aparece." },
  como_o_cliente_fecha: { coluna: "catalog_purchase_mode", rotulo: "Como o cliente fecha", tipo: "opcao", opcoes: OPCOES_DE_COMPRA, padrao: "cart", explicacao: "O que o botão do produto faz e como o pedido chega à loja." },
  texto_do_botao_do_produto: { coluna: "catalog_cta_label", rotulo: "Texto do botão do produto", tipo: "texto", max: 40, doCliente: true, padraoLegivel: "o texto padrão do modo escolhido", explicacao: "Em branco, vale o texto padrão do modo de compra." },
  texto_do_botao_de_fechar: { coluna: "catalog_checkout_label", rotulo: "Texto do botão de fechar", tipo: "texto", max: 40, doCliente: true, padraoLegivel: "o texto padrão do modo escolhido", explicacao: "O botão que fecha o carrinho ou envia o pedido de orçamento." },
  abrir_whatsapp_depois_do_orcamento: { coluna: "catalog_quote_open_whatsapp", rotulo: "Abrir o WhatsApp depois do pedido de orçamento", tipo: "booleano", padrao: true, explicacao: "O cliente cai na conversa com a loja, com a lista enviada (sem preços)." },
  quem_pode_entrar: { coluna: "catalog_access_mode", rotulo: "Quem pode entrar na loja", tipo: "opcao", opcoes: OPCOES_DE_ACESSO, padrao: "public", explicacao: "Loja aberta a todos ou só para clientes com conta." },
  cadastro: { coluna: "catalog_signup_mode", rotulo: "Cadastro de clientes", tipo: "opcao", opcoes: OPCOES_DE_CADASTRO, padrao: "open", explicacao: "Como o cliente ganha uma conta na loja." },
  cadastro_so_com_cnpj: { coluna: "catalog_signup_require_cnpj", rotulo: "Cadastro só com CNPJ", tipo: "booleano", padrao: false, explicacao: "O cadastro pela loja só aceita empresas (loja de revendedores)." },
  mensagem_na_tela_de_entrar: { coluna: "catalog_login_message", rotulo: "Mensagem na tela de entrar", tipo: "texto", max: 300, doCliente: true, manterLinhas: true, explicacao: "Aparece na tela de entrar e de cadastrar. Em branco, não aparece." },
  exigir_cadastro_para_fechar: { coluna: "catalog_require_login_to_order", rotulo: "Exigir cadastro para fechar pedido", tipo: "booleano", padrao: false, explicacao: "Sem conta, o cliente monta o carrinho mas não fecha." },
  pedido_minimo_em_reais: { coluna: "catalog_min_order_total", rotulo: "Pedido mínimo em reais", tipo: "dinheiro", min: 0, max: 10_000_000, zeroEhNulo: true, explicacao: "Abaixo disso o cliente não fecha. 0 = sem mínimo." },
  pedido_minimo_em_unidades: { coluna: "catalog_min_order_items", rotulo: "Pedido mínimo em unidades", tipo: "inteiro", min: 0, max: 1_000_000, zeroEhNulo: true, explicacao: "Soma as unidades de todos os itens do carrinho. 0 = sem mínimo." },
  estoque_na_loja: { coluna: "catalog_stock_display", rotulo: "Estoque na loja", tipo: "opcao", opcoes: OPCOES_DE_ESTOQUE, padrao: "exact", explicacao: "O que a página do produto diz sobre o saldo." },
  limite_de_ultimas_unidades: { coluna: "catalog_low_stock_threshold", rotulo: "Avisar 'últimas unidades' abaixo de", tipo: "inteiro", min: 0, max: 100_000, padrao: 5, explicacao: "Só vale com o estoque em 'só avisar últimas unidades'." },
  produto_esgotado: { coluna: "catalog_sold_out_mode", rotulo: "Produto esgotado", tipo: "opcao", opcoes: OPCOES_DE_ESGOTADO, padrao: "show", explicacao: "Só vale para produtos com controle de estoque ligado." },
  mostrar_avaliacoes: { coluna: "catalog_reviews_enabled", rotulo: "Mostrar avaliações", tipo: "booleano", padrao: true, explicacao: "As notas e os comentários dos clientes na página do produto." },
  mostrar_codigo_sku: { coluna: "catalog_show_sku", rotulo: "Mostrar o código (SKU) do produto", tipo: "booleano", padrao: false, explicacao: "O código do produto na página dele." },
  mostrar_prazo_de_producao: { coluna: "catalog_show_production_time", rotulo: "Mostrar o prazo de produção", tipo: "booleano", padrao: true, explicacao: "O prazo do produto e a contagem de 'entra em produção hoje'." },
  situacao_da_loja: { coluna: "catalog_status", rotulo: "Situação da loja", tipo: "opcao", opcoes: OPCOES_DE_SITUACAO, padrao: "open", explicacao: "Para férias, estoque parado ou reforma." },
  recado_da_situacao: { coluna: "catalog_status_message", rotulo: "Recado da loja pausada ou fechada", tipo: "texto", max: 500, doCliente: true, manterLinhas: true, explicacao: "Aparece na loja pausada ou fechada." },
  previsao_de_volta: { coluna: "catalog_status_until", rotulo: "Previsão de volta", tipo: "data", explicacao: "Mostrada junto com o recado." },
};

/* O pedaço das regras que abrir_ou_fechar_a_loja mexe. */
export const SITUACAO_DA_LOJA: Grupo = {
  situacao: REGRAS_DA_LOJA.situacao_da_loja,
  recado: REGRAS_DA_LOJA.recado_da_situacao,
  ate_quando: REGRAS_DA_LOJA.previsao_de_volta,
};

/* ============================================================= cabeçalho */

export const EFEITOS_DA_LOGO: readonly Opcao[] = [
  { valor: "none", nome: "sem efeito", significado: "A logo fica parada." },
  { valor: "rotate_3d", nome: "rotação 3D", significado: "A logo gira devagar, em 3D." },
  { valor: "pulse_glow", nome: "pulse premium", significado: "A logo pulsa com um brilho suave." },
  { valor: "blink", nome: "brilho ritmado", significado: "Um brilho passa pela logo de tempos em tempos." },
];

export const LAYOUTS_DA_VITRINE: readonly Opcao[] = [
  { valor: "mosaic", nome: "mosaico", significado: "Destaques maiores e a diagramação editorial do modelo Vitrine." },
  { valor: "standard", nome: "listagem normal", significado: "Todos os produtos com cards padrão, como no modelo tradicional." },
];

export const CABECALHO_DA_LOJA: Grupo = {
  mostrar_nome: { coluna: "catalog_show_name_in_header", rotulo: "Mostrar o nome no cabeçalho", tipo: "booleano", padrao: true, explicacao: "O nome da loja no topo, ao lado da logo." },
  mostrar_slogan: { coluna: "catalog_show_slogan_in_header", rotulo: "Mostrar o slogan no cabeçalho", tipo: "booleano", padrao: true, explicacao: "O slogan embaixo do nome." },
  efeito_da_logo: { coluna: "catalog_logo_effect", rotulo: "Efeito da logo", tipo: "opcao", opcoes: EFEITOS_DA_LOGO, padrao: "none", explicacao: "Uma animação discreta na logo do cabeçalho." },
  velocidade_do_efeito: { coluna: "catalog_logo_effect_speed", rotulo: "Velocidade do efeito (%)", tipo: "inteiro", min: 50, max: 200, padrao: 100, explicacao: "De 50 a 200; 100 é o normal. Só com um efeito ligado." },
  intensidade_do_efeito: { coluna: "catalog_logo_effect_intensity", rotulo: "Intensidade do efeito (%)", tipo: "inteiro", min: 40, max: 180, padrao: 100, explicacao: "De 40 a 180; 100 é o normal. Só com um efeito ligado." },
  listagem_da_vitrine: { coluna: "catalog_vitrine_product_layout", rotulo: "Listagem dos produtos (modelo Vitrine)", tipo: "opcao", opcoes: LAYOUTS_DA_VITRINE, padrao: "mosaic", explicacao: "Só faz diferença com o modelo Vitrine." },
};

/* ==================================================== contato e rodapé */

export type Rede = "instagram" | "facebook" | "twitter" | "linkedin" | "youtube";

export const CONTATO_E_RODAPE: Grupo = {
  whatsapp_da_loja: { coluna: "whatsapp", rotulo: "WhatsApp do catálogo", tipo: "telefone", explicacao: "O número que a loja usa no rodapé, na ficha do produto, no pedido e no botão flutuante." },
  botao_flutuante_do_whatsapp: { coluna: "catalog_whatsapp_float", rotulo: "Botão flutuante do WhatsApp", tipo: "booleano", padrao: true, explicacao: "O botão verde no canto da loja. Desligado, o número continua no rodapé e na ficha do produto." },
  telefone: { coluna: "company_phone", rotulo: "Telefone", tipo: "telefone", explicacao: "Telefone do rodapé." },
  whatsapp_da_empresa: { coluna: "company_whatsapp", rotulo: "WhatsApp da empresa", tipo: "telefone", explicacao: "No rodapé, tem preferência sobre o WhatsApp do catálogo." },
  endereco: { coluna: "address", rotulo: "Endereço", tipo: "texto", max: 300, doCliente: false, manterLinhas: true, explicacao: "Aparece no rodapé da loja. Ex.: Rua, número, bairro, cidade - UF." },
  mostrar_cpf_cnpj: { coluna: "catalog_show_document", rotulo: "Mostrar CPF/CNPJ no rodapé", tipo: "booleano", padrao: true, explicacao: "O documento da empresa no rodapé, para passar credibilidade." },
  instagram: { coluna: "social_instagram", rotulo: "Instagram", tipo: "rede", rede: "instagram", explicacao: "Link ou @ do perfil. Vira ícone no rodapé." },
  facebook: { coluna: "social_facebook", rotulo: "Facebook", tipo: "rede", rede: "facebook", explicacao: "Link ou nome da página. Vira ícone no rodapé." },
  x_twitter: { coluna: "social_twitter", rotulo: "X (Twitter)", tipo: "rede", rede: "twitter", explicacao: "Link ou @ da conta. Vira ícone no rodapé." },
  linkedin: { coluna: "social_linkedin", rotulo: "LinkedIn", tipo: "rede", rede: "linkedin", explicacao: "O link da página (linkedin.com/company/…). Vira ícone no rodapé." },
  youtube: { coluna: "social_youtube", rotulo: "YouTube", tipo: "rede", rede: "youtube", explicacao: "Link ou @ do canal. Vira ícone no rodapé." },
};

/* ================================================================== SEO */

export const SEO_DA_LOJA: Grupo = {
  palavras_chave: { coluna: "catalog_meta_keywords", rotulo: "Palavras-chave", tipo: "texto", max: 300, doCliente: true, explicacao: "Separadas por vírgula. Ex.: gráfica rápida, cartão de visita." },
  usuario_no_x: { coluna: "catalog_twitter_handle", rotulo: "Usuário no X (Twitter)", tipo: "usuario_x", explicacao: "Aparece no cartão quando o link da loja é compartilhado no X. Ex.: @sualoja." },
  aparecer_no_google: { coluna: "catalog_noindex", rotulo: "Aparecer nas buscas do Google", tipo: "booleano", padrao: true, invertido: true, explicacao: "Desligado, a loja pede ao Google para não aparecer nas buscas." },
};

/* ===================================================== corte e chat */

export const CORTE_DE_PRODUCAO: Grupo = {
  ligado: { coluna: "production_cutoff_enabled", rotulo: "Aviso de corte de produção", tipo: "booleano", padrao: false, explicacao: "A loja mostra 'entra em produção hoje se finalizado até HH:MM', com o relógio correndo." },
  hora: { coluna: "production_cutoff_time", rotulo: "Hora limite", tipo: "hora", explicacao: "Até que horas o pedido entra na fila do mesmo dia útil, no horário da loja." },
};

export const CHAT_DA_LOJA: Grupo = {
  ligado: { coluna: "store_chat_enabled", rotulo: "Chat na loja", tipo: "booleano", padrao: false, explicacao: "O balão de conversa na loja virtual (Ktalogu Chats)." },
  online: { coluna: "store_chat_online", rotulo: "Atendendo agora", tipo: "booleano", padrao: false, explicacao: "Mostra ao cliente que tem alguém atendendo. Offline o cliente escreve do mesmo jeito e a loja responde quando voltar." },
  saudacao: { coluna: "store_chat_welcome", rotulo: "Saudação", tipo: "texto", max: 400, doCliente: true, manterLinhas: true, explicacao: "A primeira mensagem que o cliente vê ao abrir o chat." },
  saudacao_ligada: { coluna: "store_chat_welcome_enabled", rotulo: "Saudação ligada", tipo: "booleano", padrao: false, explicacao: "Mostra ou não a saudação." },
};

/* ======================================================== utilidades */

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

/** A opção pelo nome amigável ou pelo valor gravado. */
export function opcaoPeloNome(opcoes: readonly Opcao[], valor: unknown): Opcao | null {
  const pedido = semAcento(String(valor ?? ""));
  if (!pedido) return null;
  return opcoes.find((opcao) => semAcento(opcao.nome) === pedido || opcao.valor.toLowerCase() === pedido) ?? null;
}

export function dataBrasileira(iso: string) {
  const [ano, mes, dia] = iso.slice(0, 10).split("-");
  return ano && mes && dia ? `${dia}/${mes}/${ano}` : iso;
}

function reaisBr(valor: number) {
  return valor.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}

/** "11987654321" → "(11) 98765-4321", a mesma máscara da tela. */
export function telefoneComMascara(valor: unknown): { ok: true; valor: string } | { ok: false; erro: string } {
  let digitos = String(valor ?? "").replace(/\D/g, "");
  if ((digitos.length === 12 || digitos.length === 13) && digitos.startsWith("55")) digitos = digitos.slice(2);
  if (digitos.length !== 10 && digitos.length !== 11) {
    return { ok: false, erro: "precisa ter DDD + número (10 ou 11 dígitos), como (11) 98765-4321." };
  }
  const mascarado = digitos.replace(/^(\d{2})(\d)/, "($1) $2").replace(/(\d)(\d{4})$/, "$1-$2");
  return { ok: true, valor: mascarado };
}

const DOMINIOS_DA_REDE: Record<Rede, string[]> = {
  instagram: ["instagram.com", "instagr.am"],
  facebook: ["facebook.com", "fb.com", "fb.me"],
  twitter: ["x.com", "twitter.com"],
  linkedin: ["linkedin.com"],
  youtube: ["youtube.com", "youtu.be"],
};

const NOME_DA_REDE: Record<Rede, string> = { instagram: "Instagram", facebook: "Facebook", twitter: "X (Twitter)", linkedin: "LinkedIn", youtube: "YouTube" };

/**
 * Link de rede social: só https e só do site daquela rede (o ícone do
 * Instagram nunca leva a outro lugar). "@loja" vira o link do perfil.
 */
export function linkDaRede(rede: Rede, valor: unknown): { ok: true; valor: string } | { ok: false; erro: string } {
  const bruto = String(valor ?? "").trim();
  const nome = NOME_DA_REDE[rede];
  /* "@minha.loja" é um @ (com ponto, como o Instagram deixa); "minhaloja",
     sem ponto nem barra, também. Com ponto e sem @, é um endereço. */
  const usuario = bruto.match(/^@?([A-Za-z0-9._-]{1,100})$/);
  if (usuario && (bruto.startsWith("@") || !bruto.includes("."))) {
    const handle = usuario[1];
    if (rede === "linkedin") return { ok: false, erro: `${nome}: mande o link da página (https://www.linkedin.com/company/...).` };
    if (rede === "instagram") return { ok: true, valor: `https://instagram.com/${handle}` };
    if (rede === "facebook") return { ok: true, valor: `https://facebook.com/${handle}` };
    if (rede === "twitter") return { ok: true, valor: `https://x.com/${handle}` };
    return { ok: true, valor: `https://youtube.com/@${handle}` };
  }
  if (/^http:\/\//i.test(bruto)) return { ok: false, erro: `${nome}: o link precisa ser seguro (https://).` };
  const comProtocolo = /^https:\/\//i.test(bruto) ? bruto : `https://${bruto}`;
  let endereco: URL;
  try {
    endereco = new URL(comProtocolo);
  } catch {
    return { ok: false, erro: `${nome}: "${bruto}" não é um link nem um @.` };
  }
  const host = endereco.hostname.toLowerCase().replace(/^(www\.|m\.|br\.|pt-br\.)/, "");
  if (!DOMINIOS_DA_REDE[rede].includes(host)) {
    return { ok: false, erro: `${nome}: o link precisa ser do ${DOMINIOS_DA_REDE[rede][0]} (veio ${host}).` };
  }
  if (endereco.username || endereco.password || endereco.port) return { ok: false, erro: `${nome}: link inválido.` };
  if (endereco.pathname === "/" || !endereco.pathname) return { ok: false, erro: `${nome}: falta o perfil no link (ex.: https://${DOMINIOS_DA_REDE[rede][0]}/sua_loja).` };
  const final = `https://${endereco.hostname.toLowerCase()}${endereco.pathname}${endereco.search}`;
  if (final.length > 300) return { ok: false, erro: `${nome}: link longo demais.` };
  return { ok: true, valor: final };
}

/** "@loja", "loja" ou "https://x.com/loja" → "@loja". */
export function usuarioDoX(valor: unknown): { ok: true; valor: string } | { ok: false; erro: string } {
  const bruto = String(valor ?? "").trim();
  const doLink = bruto.match(/^(?:https?:\/\/)?(?:www\.)?(?:x|twitter)\.com\/@?([A-Za-z0-9_]{1,15})\/?$/i);
  const handle = doLink ? doLink[1] : bruto.replace(/^@/, "");
  if (!/^[A-Za-z0-9_]{1,15}$/.test(handle)) return { ok: false, erro: "Usuário no X: use de 1 a 15 letras, números ou _ (ex.: @sualoja)." };
  return { ok: true, valor: `@${handle}` };
}

function limparTexto(valor: unknown, manterLinhas: boolean) {
  const semHtml = String(valor ?? "").replace(/<[^>]*>/g, " ").replace(/&nbsp;/gi, " ");
  return manterLinhas
    ? semHtml.replace(/[ \t]+/g, " ").replace(/ *\n */g, "\n").replace(/\n{3,}/g, "\n\n").trim()
    : semHtml.replace(/\s+/g, " ").trim();
}

/* ================================================ legível e comparação */

type Valor = string | number | boolean | null;

/** O valor gravado como a loja o entende (NULL → o padrão dela). */
export function valorEfetivo(campo: CampoDeAjuste, bruto: unknown): Valor {
  const vazio = bruto === null || bruto === undefined || bruto === "";
  switch (campo.tipo) {
    case "booleano":
      return vazio ? campo.padrao : bruto === true;
    case "opcao": {
      const valor = String(bruto ?? "");
      return campo.opcoes.some((opcao) => opcao.valor === valor) ? valor : campo.padrao;
    }
    case "inteiro":
    case "dinheiro": {
      if (vazio) return campo.padrao ?? null;
      const numero = Number(bruto);
      if (!Number.isFinite(numero)) return campo.padrao ?? null;
      if (campo.zeroEhNulo && numero <= 0) return null;
      return numero;
    }
    case "data":
      return vazio ? null : String(bruto).slice(0, 10);
    default:
      return vazio ? null : String(bruto);
  }
}

/** O valor como a pessoa lê. */
export function legivel(campo: CampoDeAjuste, bruto: unknown): string {
  const valor = valorEfetivo(campo, bruto);
  switch (campo.tipo) {
    case "booleano": {
      const ligado = campo.invertido ? valor === false : valor === true;
      return ligado ? "sim" : "não";
    }
    case "opcao":
      return campo.opcoes.find((opcao) => opcao.valor === valor)?.nome ?? String(valor);
    case "dinheiro":
      return valor === null ? "sem mínimo" : reaisBr(Number(valor));
    case "inteiro":
      return valor === null ? (campo.zeroEhNulo ? "sem mínimo" : "padrão") : String(valor);
    case "data":
      return valor === null ? "sem data" : dataBrasileira(String(valor));
    case "texto":
      return valor === null ? (campo.padraoLegivel ? `em branco (vale "${campo.padraoLegivel}")` : "em branco") : String(valor);
    default:
      return valor === null ? "em branco" : String(valor);
  }
}

/** O valor de um booleano "invertido" (aparecer_no_google ↔ catalog_noindex) visto pela pessoa. */
function booleanoPedido(campo: Extract<CampoDeAjuste, { tipo: "booleano" }>, pedido: boolean) {
  return campo.invertido ? !pedido : pedido;
}

/* ================================================== montar a mudança */

export type Mudanca = {
  /* Coluna → valor novo. */
  colunas: Record<string, Valor>;
  /* Coluna → valor de antes (o gravado, cru): é o que desfaz. */
  antes: Record<string, Valor>;
  linhas: Array<{ campo: string; antes: string; depois: string }>;
  avisos: string[];
};

export type Montagem = { ok: true; mudanca: Mudanca } | { ok: false; erro: string };

/**
 * Confere o pedido (os nomes do grupo → valores), compara com o perfil de
 * hoje e devolve só o que muda. `voltarAoPadrao` lista os campos que voltam
 * a NULL (o padrão da loja).
 */
export function montarMudanca(grupo: Grupo, pedido: Record<string, unknown>, voltarAoPadrao: string[], atual: Record<string, unknown>): Montagem {
  const colunas: Mudanca["colunas"] = {};
  const antes: Mudanca["antes"] = {};
  const linhas: Mudanca["linhas"] = [];
  const avisos: string[] = [];

  const anotar = (campo: CampoDeAjuste, novo: Valor) => {
    const velho = (atual[campo.coluna] ?? null) as Valor;
    /* Compara como a loja entende: NULL e o padrão são a mesma coisa. */
    if (valorEfetivo(campo, velho) === valorEfetivo(campo, novo)) return;
    colunas[campo.coluna] = novo;
    antes[campo.coluna] = velho;
    linhas.push({ campo: campo.rotulo, antes: legivel(campo, velho), depois: legivel(campo, novo) });
  };

  for (const [nome, bruto] of Object.entries(pedido)) {
    const campo = grupo[nome];
    if (!campo || bruto === undefined || bruto === null) continue;
    if (voltarAoPadrao.includes(nome)) return { ok: false, erro: `"${nome}" veio com um valor novo e também em voltar_ao_padrao. Escolha um dos dois.` };
    switch (campo.tipo) {
      case "opcao": {
        const opcao = opcaoPeloNome(campo.opcoes, bruto);
        if (!opcao) return { ok: false, erro: `${campo.rotulo}: use um destes: ${campo.opcoes.map((o) => o.nome).join(", ")}.` };
        anotar(campo, opcao.valor);
        break;
      }
      case "booleano": {
        if (typeof bruto !== "boolean") return { ok: false, erro: `${campo.rotulo}: precisa ser verdadeiro ou falso.` };
        anotar(campo, booleanoPedido(campo, bruto));
        break;
      }
      case "texto": {
        const texto = limparTexto(bruto, campo.manterLinhas === true);
        if (!texto) return { ok: false, erro: `${campo.rotulo}: veio vazio. Para apagar, ponha "${nome}" em voltar_ao_padrao.` };
        if (texto.length > campo.max) return { ok: false, erro: `${campo.rotulo}: até ${campo.max} letras (veio com ${texto.length}).` };
        anotar(campo, texto);
        break;
      }
      case "inteiro":
      case "dinheiro": {
        const numero = typeof bruto === "number" ? bruto : Number(String(bruto).replace(",", "."));
        if (!Number.isFinite(numero)) return { ok: false, erro: `${campo.rotulo}: precisa ser um número.` };
        if (campo.tipo === "inteiro" && !Number.isInteger(numero)) return { ok: false, erro: `${campo.rotulo}: precisa ser um número inteiro.` };
        if (numero < campo.min || numero > campo.max) return { ok: false, erro: `${campo.rotulo}: de ${campo.min} a ${campo.max}.` };
        const valor = campo.tipo === "dinheiro" ? Math.round(numero * 100) / 100 : numero;
        anotar(campo, campo.zeroEhNulo && valor <= 0 ? null : valor);
        break;
      }
      case "data": {
        const texto = String(bruto).trim().slice(0, 10);
        if (!/^\d{4}-\d{2}-\d{2}$/.test(texto)) return { ok: false, erro: `${campo.rotulo}: use o formato AAAA-MM-DD.` };
        anotar(campo, texto);
        break;
      }
      case "hora": {
        const texto = String(bruto).trim().slice(0, 5);
        if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(texto)) return { ok: false, erro: `${campo.rotulo}: use HH:MM, 24 horas (ex.: 16:00).` };
        anotar(campo, texto);
        break;
      }
      case "telefone": {
        const lido = telefoneComMascara(bruto);
        if (!lido.ok) return { ok: false, erro: `${campo.rotulo}: ${lido.erro}` };
        anotar(campo, lido.valor);
        break;
      }
      case "rede": {
        const lido = linkDaRede(campo.rede, bruto);
        if (!lido.ok) return { ok: false, erro: lido.erro };
        anotar(campo, lido.valor);
        break;
      }
      case "usuario_x": {
        const lido = usuarioDoX(bruto);
        if (!lido.ok) return { ok: false, erro: lido.erro };
        anotar(campo, lido.valor);
        break;
      }
    }
  }

  for (const nome of voltarAoPadrao) {
    const campo = grupo[nome];
    if (!campo) return { ok: false, erro: `voltar_ao_padrao: "${nome}" não é um destes campos: ${nomesQueVoltam(grupo).join(", ")}.` };
    if (!podeVoltarAoPadrao(campo)) {
      return { ok: false, erro: `${campo.rotulo} não fica em branco: mande o valor (${campo.tipo === "booleano" ? "verdadeiro ou falso" : "uma das opções"}).` };
    }
    anotar(campo, null);
  }

  if (!linhas.length) return { ok: false, erro: "Nada muda: a loja já está assim." };
  return { ok: true, mudanca: { colunas, antes, linhas, avisos } };
}

export function podeVoltarAoPadrao(campo: CampoDeAjuste) {
  if (campo.tipo === "booleano" || campo.tipo === "opcao") return false;
  if (campo.tipo === "inteiro" || campo.tipo === "dinheiro") return campo.zeroEhNulo === true;
  return true;
}

export function nomesQueVoltam(grupo: Grupo) {
  return Object.entries(grupo)
    .filter(([, campo]) => podeVoltarAoPadrao(campo))
    .map(([nome]) => nome);
}

/* ====================================== ajustes que dependem de outros */

/**
 * Efeito da logo: a tela grava "ligado" + o efeito, e desligado volta tudo
 * ao normal (efeito "none", 100% e 100%). Aqui é o mesmo: escolher um efeito
 * liga, "sem efeito" desliga e zera velocidade e intensidade.
 */
export function ajustarEfeitoDaLogo(mudanca: Mudanca, atual: Record<string, unknown>): { ok: true } | { ok: false; erro: string } {
  const ligadoHoje = atual.catalog_logo_effect_enabled === true && valorEfetivo(CABECALHO_DA_LOJA.efeito_da_logo, atual.catalog_logo_effect) !== "none";
  const efeito = "catalog_logo_effect" in mudanca.colunas ? mudanca.colunas.catalog_logo_effect : ligadoHoje ? atual.catalog_logo_effect : "none";
  const ligado = efeito !== "none" && efeito !== null;
  const mexeNoRitmo = "catalog_logo_effect_speed" in mudanca.colunas || "catalog_logo_effect_intensity" in mudanca.colunas;
  if (!ligado && mexeNoRitmo && !("catalog_logo_effect" in mudanca.colunas)) {
    return { ok: false, erro: "A logo está sem efeito: escolha um efeito_da_logo junto com a velocidade ou a intensidade." };
  }
  if (ligado !== (atual.catalog_logo_effect_enabled === true)) {
    mudanca.colunas.catalog_logo_effect_enabled = ligado;
    mudanca.antes.catalog_logo_effect_enabled = (atual.catalog_logo_effect_enabled ?? null) as Valor;
  }
  if (!ligado) {
    for (const coluna of ["catalog_logo_effect_speed", "catalog_logo_effect_intensity"]) {
      if (Number(atual[coluna] ?? 100) !== 100 || coluna in mudanca.colunas) {
        if (!(coluna in mudanca.antes)) mudanca.antes[coluna] = (atual[coluna] ?? null) as Valor;
        mudanca.colunas[coluna] = 100;
      }
    }
    if (mexeNoRitmo) mudanca.avisos.push("Sem efeito, a velocidade e a intensidade voltam a 100%.");
  }
  return { ok: true };
}

/**
 * Corte de produção: ligar sem hora usa a que já estava (ou pede uma);
 * desligar apaga a hora, como a tela.
 */
export function ajustarCorte(mudanca: Mudanca, atual: Record<string, unknown>): { ok: true } | { ok: false; erro: string } {
  const ligado = "production_cutoff_enabled" in mudanca.colunas ? mudanca.colunas.production_cutoff_enabled === true : atual.production_cutoff_enabled === true;
  const hora = "production_cutoff_time" in mudanca.colunas ? mudanca.colunas.production_cutoff_time : atual.production_cutoff_time;
  if (ligado && !hora) return { ok: false, erro: "Para ligar o aviso de corte, mande também a hora limite (ex.: 16:00)." };
  if (!ligado && "production_cutoff_time" in mudanca.colunas && mudanca.colunas.production_cutoff_time !== null) {
    return { ok: false, erro: "O aviso de corte está desligado: mande ligado=verdadeiro junto com a hora." };
  }
  if (!ligado && atual.production_cutoff_time) {
    mudanca.antes.production_cutoff_time = atual.production_cutoff_time as Valor;
    mudanca.colunas.production_cutoff_time = null;
  }
  return { ok: true };
}

/* ====================================================== sensível */

/**
 * Por que esta mudança precisa da aprovação da pessoa (prévia com código).
 * Vazio = grava direto. São as que tiram a loja do ar, escondem o preço,
 * mudam como o cliente compra ou fecham a porta para clientes novos.
 */
export function motivosSensiveis(colunas: Record<string, Valor>, atual: Record<string, unknown>): string[] {
  const motivos: string[] = [];
  const mudou = (coluna: string, padrao: Valor) => coluna in colunas && (colunas[coluna] ?? padrao) !== (atual[coluna] ?? padrao);
  if (mudou("catalog_status", "open") && (colunas.catalog_status === "paused" || colunas.catalog_status === "closed")) {
    motivos.push(colunas.catalog_status === "closed" ? "A loja fecha: ninguém compra até reabrir." : "A loja entra em pausa: ninguém fecha pedido até reabrir.");
  }
  if (mudou("catalog_access_mode", "public") && colunas.catalog_access_mode === "clients") motivos.push("A loja passa a ser só para clientes logados e sai do Google.");
  if (mudou("catalog_price_mode", "public")) motivos.push("Muda quem vê o preço na loja.");
  if (mudou("catalog_purchase_mode", "cart")) motivos.push("Muda como o cliente fecha o pedido.");
  if (mudou("catalog_signup_mode", "open") && colunas.catalog_signup_mode === "closed") motivos.push("O cadastro de clientes novos fecha.");
  if (mudou("catalog_noindex", false) && colunas.catalog_noindex === true) motivos.push("A loja pede para sair das buscas do Google.");
  return motivos;
}

/* ================================================ peneira de contatos */

/** Os contatos novos nos textos que o cliente lê (endereço, telefone e redes são campos próprios e passam). */
export function contatosNosTextos(grupo: Grupo, mudanca: Mudanca): string[] {
  const achados: string[] = [];
  for (const campo of Object.values(grupo)) {
    if (campo.tipo !== "texto" || !campo.doCliente || !(campo.coluna in mudanca.colunas)) continue;
    achados.push(...contatosNovos(mudanca.antes[campo.coluna] as string | null, mudanca.colunas[campo.coluna] as string | null));
  }
  return achados;
}

/* ===================================================== desfazer */

/** Os argumentos que voltam a loja ao que era (chamar a mesma ferramenta de novo com eles). */
export function comoDesfazer(grupo: Grupo, antes: Record<string, Valor>): Record<string, unknown> {
  const argumentos: Record<string, unknown> = {};
  const voltar: string[] = [];
  for (const [nome, campo] of Object.entries(grupo)) {
    if (!(campo.coluna in antes)) continue;
    const velho = antes[campo.coluna];
    const efetivo = valorEfetivo(campo, velho);
    if (campo.tipo === "booleano") argumentos[nome] = campo.invertido ? efetivo === false : efetivo === true;
    else if (campo.tipo === "opcao") argumentos[nome] = campo.opcoes.find((opcao) => opcao.valor === efetivo)?.nome ?? campo.padrao;
    else if (efetivo === null) {
      if (podeVoltarAoPadrao(campo)) voltar.push(nome);
    } else argumentos[nome] = efetivo;
  }
  if (voltar.length) argumentos.voltar_ao_padrao = voltar;
  return argumentos;
}

/* ================================================== leitura legível */

/** O grupo como a pessoa lê: cada campo com o valor de hoje e o que ele faz. */
export function lerGrupo(grupo: Grupo, perfil: Record<string, unknown>) {
  const saida: Record<string, unknown> = {};
  for (const [nome, campo] of Object.entries(grupo)) {
    const efetivo = valorEfetivo(campo, perfil[campo.coluna]);
    saida[nome] = {
      o_que_e: campo.rotulo,
      hoje: legivel(campo, perfil[campo.coluna]),
      o_que_faz: campo.explicacao,
      ...(campo.tipo === "opcao"
        ? {
            significado_de_hoje: campo.opcoes.find((opcao) => opcao.valor === efetivo)?.significado,
            opcoes: campo.opcoes.map((opcao) => `${opcao.nome}: ${opcao.significado}`),
          }
        : {}),
    };
  }
  return saida;
}

/* =============================================== esquema das ferramentas */

/** Os campos que a ferramenta aceita, a partir do grupo (mais voltar_ao_padrao quando cabe). */
export function camposDoGrupo(grupo: Grupo): Campos {
  const campos: Campos = {};
  for (const [nome, campo] of Object.entries(grupo)) {
    const descricao = `${campo.rotulo}. ${campo.explicacao}`;
    let definicao: Campo;
    switch (campo.tipo) {
      case "opcao":
        definicao = {
          tipo: "opcao",
          descricao: `${descricao} Opções: ${campo.opcoes.map((o) => `"${o.nome}" (${o.significado})`).join("; ")}`,
          opcoes: campo.opcoes.map((o) => o.nome),
        };
        break;
      case "booleano":
        definicao = { tipo: "booleano", descricao };
        break;
      case "texto":
        definicao = { tipo: "texto", descricao: `${descricao} Até ${campo.max} letras.`, max: campo.max };
        break;
      case "inteiro":
        definicao = { tipo: "inteiro", descricao, min: campo.min, max: campo.max };
        break;
      case "dinheiro":
        definicao = { tipo: "numero", descricao: `${descricao} Em reais.`, min: campo.min, max: campo.max };
        break;
      case "data":
        definicao = { tipo: "data", descricao };
        break;
      case "hora":
        definicao = { tipo: "hora", descricao };
        break;
      default:
        definicao = { tipo: "texto", descricao, max: 300 };
    }
    campos[nome] = definicao;
  }
  const voltam = nomesQueVoltam(grupo);
  if (voltam.length) {
    campos.voltar_ao_padrao = {
      tipo: "textos",
      descricao: `Nomes dos campos que ficam em branco (o padrão da loja). Aceita: ${voltam.join(", ")}.`,
      max: 60,
      maxItens: voltam.length,
    };
  }
  return campos;
}

/** Só os argumentos do grupo, separados de voltar_ao_padrao. */
export function pedidoDoGrupo(grupo: Grupo, args: Record<string, unknown>) {
  const valores: Record<string, unknown> = {};
  for (const nome of Object.keys(grupo)) if (args[nome] !== undefined) valores[nome] = args[nome];
  const voltar = Array.isArray(args.voltar_ao_padrao) ? (args.voltar_ao_padrao as unknown[]).map((nome) => String(nome)) : [];
  return { valores, voltar };
}

/* ================================================= pontos de retirada */

export type PontoDeRetirada = {
  name: string;
  price: number;
  ready_in_days: number;
  zip: string | null;
  street: string | null;
  number: string | null;
  complement: string | null;
  neighborhood: string | null;
  city: string | null;
  state: string | null;
  instructions: string | null;
  active: boolean;
};

const CAMPOS_DO_PONTO: Record<string, { coluna: keyof PontoDeRetirada; rotulo: string; max: number }> = {
  nome: { coluna: "name", rotulo: "Nome", max: 80 },
  rua: { coluna: "street", rotulo: "Rua", max: 160 },
  numero: { coluna: "number", rotulo: "Número", max: 20 },
  complemento: { coluna: "complement", rotulo: "Complemento", max: 120 },
  bairro: { coluna: "neighborhood", rotulo: "Bairro", max: 120 },
  cidade: { coluna: "city", rotulo: "Cidade", max: 120 },
  instrucoes: { coluna: "instructions", rotulo: "Instruções", max: 500 },
};

/**
 * O ponto como vai ao banco, a partir do que o assistente mandou (sobre o
 * ponto de hoje, numa alteração). As mesmas regras da tela de Logística:
 * nome, rua e cidade obrigatórios; UF em duas letras; CEP com 8 dígitos;
 * preço e prazo nunca negativos.
 */
export function montarPonto(
  args: Record<string, unknown>,
  atual: PontoDeRetirada | null
): { ok: true; ponto: PontoDeRetirada; contatos: string[] } | { ok: false; erro: string } {
  const ponto: PontoDeRetirada = atual
    ? { ...atual }
    : { name: "", price: 0, ready_in_days: 0, zip: null, street: null, number: null, complement: null, neighborhood: null, city: null, state: null, instructions: null, active: true };
  const contatos: string[] = [];

  for (const [nome, campo] of Object.entries(CAMPOS_DO_PONTO)) {
    if (args[nome] === undefined || args[nome] === null) continue;
    const texto = limparTexto(args[nome], campo.coluna === "instructions");
    if (texto.length > campo.max) return { ok: false, erro: `${campo.rotulo}: até ${campo.max} letras.` };
    /* Nome e instruções o cliente lê como texto livre: a peneira vale. */
    if (campo.coluna === "name" || campo.coluna === "instructions") {
      contatos.push(...contatosNovos(atual ? (atual[campo.coluna] as string | null) : null, texto));
    }
    (ponto as Record<string, unknown>)[campo.coluna] = texto || null;
  }
  if (args.cep !== undefined && args.cep !== null) {
    const cep = String(args.cep).replace(/\D/g, "");
    if (cep && cep.length !== 8) return { ok: false, erro: "CEP: precisa ter 8 dígitos." };
    ponto.zip = cep || null;
  }
  if (args.uf !== undefined && args.uf !== null) {
    const uf = String(args.uf).trim().toUpperCase();
    if (uf && !/^[A-Z]{2}$/.test(uf)) return { ok: false, erro: "UF: use a sigla do estado, com duas letras (ex.: SP)." };
    ponto.state = uf || null;
  }
  if (args.preco !== undefined && args.preco !== null) {
    const preco = Number(args.preco);
    if (!Number.isFinite(preco) || preco < 0) return { ok: false, erro: "Preço: precisa ser zero (grátis) ou mais." };
    ponto.price = Math.round(preco * 100) / 100;
  }
  if (args.pronto_em_dias !== undefined && args.pronto_em_dias !== null) {
    const dias = Number(args.pronto_em_dias);
    if (!Number.isInteger(dias) || dias < 0) return { ok: false, erro: "Pronto em dias: um número inteiro, zero ou mais." };
    ponto.ready_in_days = dias;
  }
  if (typeof args.ativo === "boolean") ponto.active = args.ativo;

  if (!ponto.name) return { ok: false, erro: "Dê um nome ao ponto de retirada: é ele que o cliente vê na loja." };
  if (!ponto.street || !ponto.city) return { ok: false, erro: "Informe pelo menos a rua e a cidade do ponto de retirada." };
  return { ok: true, ponto, contatos };
}

/** "Rua X, 10 • Centro • Campinas / SP • CEP 13010-000", como a tela mostra. */
export function enderecoDoPonto(ponto: Partial<PontoDeRetirada>) {
  const cep = String(ponto.zip || "").replace(/\D/g, "");
  return [
    [ponto.street, ponto.number].map((parte) => String(parte || "").trim()).filter(Boolean).join(", "),
    String(ponto.complement || "").trim(),
    String(ponto.neighborhood || "").trim(),
    [ponto.city, String(ponto.state || "").toUpperCase()].map((parte) => String(parte || "").trim()).filter(Boolean).join(" / "),
    cep.length === 8 ? `CEP ${cep.slice(0, 5)}-${cep.slice(5)}` : "",
  ]
    .filter(Boolean)
    .join(" • ");
}

/** O ponto como a pessoa lê. */
export function pontoLegivel(ponto: Partial<PontoDeRetirada> & { id?: string; source?: string | null }) {
  const preco = Number(ponto.price) || 0;
  const dias = Number(ponto.ready_in_days) || 0;
  return {
    ...(ponto.id ? { id: ponto.id } : {}),
    nome: ponto.name,
    endereco: enderecoDoPonto(ponto),
    preco: preco > 0 ? reaisBr(preco) : "grátis",
    pronto_em: dias === 0 ? "no mesmo dia" : `${dias} ${dias === 1 ? "dia" : "dias"}`,
    instrucoes: ponto.instructions || null,
    ativo: ponto.active !== false,
    ...(ponto.source === "company_address" ? { origem: "endereço da empresa" } : {}),
  };
}

/* ========================================================= avaliações */

export const SITUACAO_DA_AVALIACAO: Record<string, string> = { pending: "pendente", approved: "aprovada", rejected: "recusada" };

export const FILTROS_DE_AVALIACAO: Record<string, "pending" | "approved" | "rejected" | null> = {
  pendentes: "pending",
  aprovadas: "approved",
  recusadas: "rejected",
  todas: null,
};

/** As colunas que a moderação grava, como a rota do painel (/api/catalog/reviews). */
export function colunasDaModeracao(acao: "aprovar" | "recusar", agora: string) {
  const status = acao === "aprovar" ? "approved" : "rejected";
  return {
    status,
    moderated_at: agora,
    approved_at: status === "approved" ? agora : null,
    rejected_at: status === "rejected" ? agora : null,
  };
}
