/*
  Os blocos novos da página inicial (os "widgets" do editor): o que cada um
  guarda em catalog_sections.config, a leitura segura dessa configuração e
  as contas que não dependem de tela (relógio de Brasília, contagem
  regressiva, corte de produção, texto com negrito e link, vídeo, mapa).

  Regras puras, sem "@/": o teste roda sozinho (widgets-da-home.test.ts). O
  painel tem a cópia do que precisa em apps/admin/src/lib/widgets-da-home.ts;
  mudou aqui, muda lá.

  Tudo o que a pessoa escreve é conteúdo dela: sai como texto (o React
  escapa), nunca como HTML. O "negrito" e o "link" do texto com imagem são
  lidos aqui e viram elementos, não marcação.

  Os tipos (config.mode):
  - flash_offer       Oferta relâmpago: produtos escolhidos e um relógio até o fim.
  - coupon_highlight  Cupom em destaque: um cupom ativo da loja, com "Copiar".
  - order_cutoff      "Peça até as 16h": o corte de produção da loja, ao vivo.
  - next_live         Próxima live: a próxima live de venda marcada na vitrine.
  - auto_list         Listas automáticas: lançamentos ou mais vendidos de verdade.
  - combo_highlight   Kits e combos: os "compre junto" da loja.
  - recently_viewed   Vistos recentemente: guardado só no navegador do visitante.
  - video_section     Vídeo do YouTube ou do Vimeo, com texto ao lado.
  - text_image        Texto com imagem e botão.
  - faq               Perguntas frequentes (com os dados para o Google).
  - store_location    Onde estamos: endereço, horários, mapa e rota.
  - marquee           Faixa de texto que desliza.
  - shoppable_image   Imagem com pontos clicáveis que mostram produtos.
  - before_after      Antes e depois, com a barra de arrastar.
*/

import { destinoDoLink } from "./destino-do-link.ts";
import { lerLinkDeVideo, type LinkDeVideo } from "./digitais/curso.ts";
import { momentoDaSala } from "./live/regras.ts";
import { condicoesDoCupom as condicoesDaRegra, lerCupom, valeNoDiaENoHorario } from "./regras-do-cupom.ts";

export const MODOS_DOS_WIDGETS = [
  "flash_offer",
  "coupon_highlight",
  "order_cutoff",
  "next_live",
  "auto_list",
  "combo_highlight",
  "recently_viewed",
  "video_section",
  "text_image",
  "faq",
  "store_location",
  "marquee",
  "shoppable_image",
  "before_after",
] as const;

export type ModoDoWidget = (typeof MODOS_DOS_WIDGETS)[number];

export function ehWidget(valor: unknown): valor is ModoDoWidget {
  return typeof valor === "string" && (MODOS_DOS_WIDGETS as readonly string[]).includes(valor);
}

/* --------------------------------------------------------- leitura */

function escolha<T extends string>(valor: unknown, opcoes: readonly T[], padrao: T): T {
  return typeof valor === "string" && (opcoes as readonly string[]).includes(valor) ? (valor as T) : padrao;
}

function inteiro(valor: unknown, minimo: number, maximo: number, padrao: number) {
  const numero = Math.trunc(Number(valor));
  if (!Number.isFinite(numero) || valor === null || valor === "" || valor === undefined) return padrao;
  return Math.min(maximo, Math.max(minimo, numero));
}

/** Uma linha de texto: sem quebra, sem espaço sobrando, com teto. */
export function linha(valor: unknown, maximo: number) {
  return String(valor ?? "").replace(/\s+/g, " ").trim().slice(0, maximo);
}

/** Texto de várias linhas: mantém as quebras, tira o resto. */
export function paragrafos(valor: unknown, maximo: number) {
  return String(valor ?? "")
    .replace(/\r\n?/g, "\n")
    .replace(/[^\S\n]+/g, " ")
    .replace(/\n{3,}/g, "\n\n")
    .trim()
    .slice(0, maximo);
}

function urlDeImagem(valor: unknown): string {
  const bruto = String(valor ?? "").trim();
  if (!bruto || bruto.length > 2000) return "";
  if (/^https?:\/\//i.test(bruto)) return bruto;
  if (bruto.startsWith("/") && !bruto.startsWith("//")) return bruto;
  return "";
}

function idDeProduto(valor: unknown) {
  const texto = String(valor ?? "").trim();
  return /^[0-9a-f-]{8,40}$/i.test(texto) ? texto : "";
}

function listaDeIds(valor: unknown, maximo: number): string[] {
  if (!Array.isArray(valor)) return [];
  const vistos = new Set<string>();
  const lista: string[] = [];
  for (const item of valor) {
    const id = idDeProduto(item);
    if (!id || vistos.has(id)) continue;
    vistos.add(id);
    lista.push(id);
    if (lista.length >= maximo) break;
  }
  return lista;
}

/** Cor em hexadecimal (#abc ou #aabbcc); o resto vira o padrão. */
export function corHex(valor: unknown, padrao: string) {
  const texto = String(valor ?? "").trim();
  return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(texto) ? texto : padrao;
}

export type EspacoDoWidget = "nenhum" | "pequeno" | "normal";
function margem(config: Record<string, unknown>): EspacoDoWidget {
  return escolha<EspacoDoWidget>(config.margem, ["nenhum", "pequeno", "normal"], "normal");
}

/* ------------------------------------------------ relógio de Brasília */

export const FUSO_DA_LOJA = "America/Sao_Paulo";

type PartesDaHora = { ano: number; mes: number; dia: number; hora: number; minuto: number; segundo: number; semana: number };

const FORMATO_DAS_PARTES = new Intl.DateTimeFormat("en-US", {
  timeZone: FUSO_DA_LOJA,
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
  weekday: "short",
  hourCycle: "h23",
});

const DIAS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

/** A hora de Brasília num instante (dia da semana: 0 = domingo). */
export function horaEmBrasilia(instante: number): PartesDaHora {
  const partes = Object.fromEntries(FORMATO_DAS_PARTES.formatToParts(new Date(instante)).map((p) => [p.type, p.value]));
  return {
    ano: Number(partes.year),
    mes: Number(partes.month),
    dia: Number(partes.day),
    hora: Number(partes.hour) % 24,
    minuto: Number(partes.minute),
    segundo: Number(partes.second),
    semana: Math.max(0, DIAS.indexOf(String(partes.weekday))),
  };
}

/** Quanto Brasília está atrás do UTC nesse instante, em milissegundos. */
function diferencaDoFuso(instante: number) {
  const p = horaEmBrasilia(instante);
  const comoUtc = Date.UTC(p.ano, p.mes - 1, p.dia, p.hora, p.minuto, p.segundo);
  return comoUtc - Math.floor(instante / 1000) * 1000;
}

/** "2026-10-20T18:00" (hora de Brasília) → o instante. Inválido: null. */
export function instanteDeBrasilia(valor: unknown): number | null {
  const texto = String(valor ?? "").trim();
  const achado = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(texto);
  if (!achado) return null;
  const [ano, mes, dia, hora, minuto, segundo] = achado.slice(1).map((n) => Number(n || 0));
  if (mes < 1 || mes > 12 || dia < 1 || dia > 31 || hora > 23 || minuto > 59) return null;
  const comoUtc = Date.UTC(ano, mes - 1, dia, hora, minuto, segundo || 0);
  /* Duas voltas: a diferença do fuso pode mudar perto da troca de horário. */
  let instante = comoUtc - diferencaDoFuso(comoUtc);
  instante = comoUtc - diferencaDoFuso(instante);
  return Number.isFinite(instante) ? instante : null;
}

/** O instante como o campo de data e hora do painel mostra (hora de Brasília). */
export function textoDeBrasilia(instante: number) {
  const p = horaEmBrasilia(instante);
  const dois = (n: number) => String(n).padStart(2, "0");
  return `${p.ano}-${dois(p.mes)}-${dois(p.dia)}T${dois(p.hora)}:${dois(p.minuto)}`;
}

/** "02:14:33"; com mais de um dia, "2d 03:14:33". */
export function relogioDaContagem(milissegundos: number) {
  const total = Math.max(0, Math.floor(milissegundos / 1000));
  const dias = Math.floor(total / 86400);
  const horas = Math.floor((total % 86400) / 3600);
  const minutos = Math.floor((total % 3600) / 60);
  const segundos = total % 60;
  const dois = (n: number) => String(n).padStart(2, "0");
  const relogio = `${dois(horas)}:${dois(minutos)}:${dois(segundos)}`;
  return dias > 0 ? `${dias}d ${relogio}` : relogio;
}

/** As partes da contagem para os quadradinhos (dias, horas, minutos, segundos). */
export function partesDaContagem(milissegundos: number) {
  const total = Math.max(0, Math.floor(milissegundos / 1000));
  return {
    dias: Math.floor(total / 86400),
    horas: Math.floor((total % 86400) / 3600),
    minutos: Math.floor((total % 3600) / 60),
    segundos: total % 60,
  };
}

/* ---------------------------------------------------------- oferta */

export const LIMITE_DA_OFERTA = 12;

export type OfertaRelampago = {
  produtos: string[];
  terminaEm: number | null;
  selo: string;
  subtitulo: string;
  margem: EspacoDoWidget;
};

export function lerOferta(config: Record<string, unknown>): OfertaRelampago {
  return {
    produtos: listaDeIds(config.produtos, LIMITE_DA_OFERTA),
    terminaEm: instanteDeBrasilia(config.termina_em),
    selo: linha(config.selo, 30) || "Oferta",
    subtitulo: linha(config.subtitulo, 140),
    margem: margem(config),
  };
}

/** A oferta ainda está de pé? (sem data não aparece: relógio sem fim seria mentira) */
export function ofertaNoAr(oferta: OfertaRelampago, agora: number) {
  return oferta.terminaEm !== null && oferta.terminaEm > agora;
}

/* ----------------------------------------------------------- cupom */

export type CupomDaLoja = {
  id: string;
  code: string;
  type: string;
  value: number;
  min_total: number | null;
  min_items: number | null;
  active: boolean;
  starts_at: string | null;
  ends_at: string | null;
  /* As regras da migration 200 (lib/regras-do-cupom.ts). Cupom sem elas
     vale como sempre. */
  name?: string | null;
  max_discount?: number | null;
  shipping_max?: number | null;
  shipping_states?: string[] | null;
  scope?: string | null;
  scope_category_ids?: string[] | null;
  scope_product_ids?: string[] | null;
  exclude_category_ids?: string[] | null;
  exclude_product_ids?: string[] | null;
  min_scope?: string | null;
  audience?: string | null;
  usage_limit?: number | null;
  usage_limit_per_client?: number | null;
  weekdays?: number[] | null;
  hours_start?: string | null;
  hours_end?: string | null;
  application?: string | null;
};

/** As colunas que o bloco "Cupom em destaque" lê (sem a lista de clientes). */
export const COLUNAS_DO_CUPOM_DO_WIDGET =
  "id,code,type,value,min_total,min_items,active,starts_at,ends_at,name,max_discount,shipping_max,shipping_states,scope," +
  "scope_category_ids,scope_product_ids,exclude_category_ids,exclude_product_ids,min_scope,audience,usage_limit," +
  "usage_limit_per_client,weekdays,hours_start,hours_end,application";

/** O cupom é aplicado sozinho no carrinho (sem código para copiar)? */
export function cupomAutomatico(cupom: Pick<CupomDaLoja, "application"> | null | undefined) {
  return cupom?.application === "automatic";
}

export type CupomEmDestaque = { cupomId: string; chamada: string; mostrarValidade: boolean; margem: EspacoDoWidget };

export function lerCupomEmDestaque(config: Record<string, unknown>): CupomEmDestaque {
  return {
    cupomId: linha(config.cupom_id, 60),
    chamada: linha(config.chamada, 120),
    mostrarValidade: config.mostrar_validade !== false,
    margem: margem(config),
  };
}

const MOEDA = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });
export function emReais(valor: number) {
  return MOEDA.format(Number(valor) || 0);
}

/** O cupom pode ser usado agora? (ativo, dentro do período) */
export function cupomValeAgora(cupom: CupomDaLoja | null | undefined, agora: number) {
  if (!cupom || cupom.active === false) return false;
  const inicio = cupom.starts_at ? new Date(cupom.starts_at).getTime() : NaN;
  const fim = cupom.ends_at ? new Date(cupom.ends_at).getTime() : NaN;
  if (Number.isFinite(inicio) && inicio > agora) return false;
  if (Number.isFinite(fim) && fim <= agora) return false;
  /* Dia da semana e faixa de horário (Brasília), quando a loja escolheu. */
  return valeNoDiaENoHorario(lerCupom(cupom as unknown as Record<string, unknown>), agora).ok;
}

/** "15% OFF", "R$ 20,00 OFF" ou "Frete grátis". */
export function descontoDoCupom(cupom: Pick<CupomDaLoja, "type" | "value">) {
  const tipo = String(cupom.type || "").toLowerCase();
  const valor = Number(cupom.value) || 0;
  if (tipo.includes("frete") || tipo.includes("shipping")) return "Frete grátis";
  if (tipo === "percentage" || tipo === "percent" || tipo === "porcentagem") {
    return `${valor.toLocaleString("pt-BR", { maximumFractionDigits: 2 })}% OFF`;
  }
  return `${emReais(valor)} OFF`;
}

/** As condições do cupom em português (compra mínima, itens, validade). */
export function condicoesDoCupom(cupom: CupomDaLoja, mostrarValidade: boolean): string[] {
  const lista: string[] = [];
  if (Number(cupom.min_total) > 0) lista.push(`Em compras a partir de ${emReais(Number(cupom.min_total))}`);
  if (Number(cupom.min_items) > 1) lista.push(`A partir de ${Number(cupom.min_items)} itens no carrinho`);
  /* Onde vale, quem pode usar, dias, horário e limites (as regras novas):
     o mínimo e a validade já saem acima, no jeito deste bloco. */
  const regras = lerCupom(cupom as unknown as Record<string, unknown>);
  for (const linha of condicoesDaRegra({ ...regras, min_total: 0, min_items: 0 }, Date.now(), { validade: false })) lista.push(linha);
  if (regras.type === "free_shipping" && regras.shipping_max) lista.push(`Abate até ${emReais(regras.shipping_max)} do frete`);
  if (regras.type === "free_shipping" && regras.shipping_states.length) lista.push(`Frete grátis para ${regras.shipping_states.join(", ")}`);
  if (regras.type === "percent" && regras.max_discount) lista.push(`Desconto de até ${emReais(regras.max_discount)}`);
  if (mostrarValidade && cupom.ends_at) {
    const fim = new Date(cupom.ends_at).getTime();
    if (Number.isFinite(fim)) {
      const p = horaEmBrasilia(fim);
      const dois = (n: number) => String(n).padStart(2, "0");
      lista.push(`Válido até ${dois(p.dia)}/${dois(p.mes)} às ${dois(p.hora)}h${p.minuto ? dois(p.minuto) : ""}`);
    }
  }
  if (!lista.length) lista.push("Vale para qualquer compra na loja");
  return lista;
}

/* --------------------------------------------------- corte de produção */

export type CorteDaLoja = { ligado: boolean; hora: string | null };

export type ContaDoCorte =
  | { hoje: true; hora: string; faltaMs: number }
  | { hoje: false; hora: string; proximoDia: string; amanha: boolean };

const NOMES_DOS_DIAS = ["domingo", "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira", "sexta-feira", "sábado"];

/** "16:00" → [16, 0]; inválido → null. */
export function lerHoraDoCorte(valor: unknown): [number, number] | null {
  const achado = /^(\d{1,2}):(\d{2})$/.exec(String(valor ?? "").trim());
  if (!achado) return null;
  const h = Number(achado[1]);
  const m = Number(achado[2]);
  if (h > 23 || m > 59) return null;
  return [h, m];
}

/**
 * A mesma regra do aviso do produto (VendorCorteDeProducao): de segunda a
 * sexta, antes da hora limite, o pedido entra na produção de hoje; depois
 * dela, ou no fim de semana, entra no próximo dia útil. Aqui a conta é no
 * relógio de Brasília (a hora gravada é a da loja).
 */
export function contaDoCorte(horaLimite: string, agora: number): ContaDoCorte | null {
  const lida = lerHoraDoCorte(horaLimite);
  if (!lida) return null;
  const [h, m] = lida;
  const p = horaEmBrasilia(agora);
  const hora = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
  const fimDeSemana = p.semana === 0 || p.semana === 6;
  const segundosDeHoje = p.hora * 3600 + p.minuto * 60 + p.segundo;
  const segundosDoLimite = h * 3600 + m * 60;
  if (!fimDeSemana && segundosDeHoje < segundosDoLimite) {
    return { hoje: true, hora, faltaMs: (segundosDoLimite - segundosDeHoje) * 1000 };
  }
  let semana = p.semana;
  let dias = 0;
  do {
    semana = (semana + 1) % 7;
    dias += 1;
  } while (semana === 0 || semana === 6);
  return { hoje: false, hora, proximoDia: NOMES_DOS_DIAS[semana], amanha: dias === 1 };
}

export type AvisoDoCorte = { frase: string; destaque: string; margem: EspacoDoWidget };

export function lerAvisoDoCorte(config: Record<string, unknown>): AvisoDoCorte {
  return { frase: linha(config.frase, 120), destaque: linha(config.destaque, 60), margem: margem(config) };
}

/* ------------------------------------------------------------- live */

export type LiveDaLoja = {
  id: string;
  titulo: string;
  comeca_em: string;
  duracao_min: number;
  espera_min: number;
  status: "agendada" | "ao_vivo" | "encerrada" | "cancelada";
  iniciada_em: string | null;
  categoria: "venda" | "reuniao" | null;
  capa: string | null;
};

export type ProximaLive = { semLive: string; mostrarSemLive: boolean; margem: EspacoDoWidget };

export function lerProximaLive(config: Record<string, unknown>): ProximaLive {
  return {
    semLive: linha(config.mensagem_sem_live, 140) || "Fique de olho: logo tem live nova por aqui.",
    mostrarSemLive: config.mostrar_sem_live === true,
    margem: margem(config),
  };
}

const DIA_CURTO = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"];
export function quandoDaLive(instante: number) {
  const p = horaEmBrasilia(instante);
  const dois = (n: number) => String(n).padStart(2, "0");
  return `${DIA_CURTO[p.semana]}, ${dois(p.dia)}/${dois(p.mes)} às ${dois(p.hora)}h${p.minuto ? dois(p.minuto) : ""}`;
}

/** A próxima live que vale mostrar (no ar primeiro; depois a mais perto). */
export function liveDaVez(lives: LiveDaLoja[], agora: number) {
  const comMomento = lives.map((live) => ({
    live,
    momento: momentoDaSala(
      {
        status: live.status,
        comeca_em: live.comeca_em,
        duracao_min: live.duracao_min,
        espera_min: live.espera_min,
        participacao: "mao",
        cameras_alunos: false,
        categoria: live.categoria === "reuniao" ? "reuniao" : "venda",
        iniciada_em: live.iniciada_em,
      },
      agora
    ),
  }));
  return (
    comMomento.find((x) => x.momento === "ao_vivo") ??
    comMomento.find((x) => x.momento === "espera") ??
    comMomento.filter((x) => x.momento === "cedo").sort((a, b) => new Date(a.live.comeca_em).getTime() - new Date(b.live.comeca_em).getTime())[0] ??
    null
  );
}

/* ------------------------------------------------ listas automáticas */

export type ListaAutomatica = { lista: "lancamentos" | "mais_vendidos"; dias: 7 | 30 | 90; quantidade: number; margem: EspacoDoWidget };

export const DIAS_DOS_MAIS_VENDIDOS = [7, 30, 90] as const;

export function lerListaAutomatica(config: Record<string, unknown>): ListaAutomatica {
  const dias = Number(config.dias);
  return {
    lista: escolha(config.lista, ["lancamentos", "mais_vendidos"] as const, "lancamentos"),
    dias: dias === 7 || dias === 90 ? dias : 30,
    quantidade: inteiro(config.quantidade, 2, 24, 8),
    margem: margem(config),
  };
}

/** Os mais novos primeiro (data de cadastro), sem mexer na lista que veio. */
export function lancamentos<P extends object>(produtos: P[], quantidade: number): P[] {
  return [...produtos]
    .map((produto, indice) => ({ produto, indice, quando: new Date(String((produto as { created_at?: unknown }).created_at ?? "")).getTime() }))
    .sort((a, b) => {
      const qa = Number.isFinite(a.quando) ? a.quando : -Infinity;
      const qb = Number.isFinite(b.quando) ? b.quando : -Infinity;
      return qb - qa || a.indice - b.indice;
    })
    .slice(0, quantidade)
    .map((x) => x.produto);
}

/** Os produtos na ordem dos ids (os que sumiram da loja ficam de fora). */
export function produtosNaOrdem<P extends { id: string }>(ids: string[], produtos: P[]): P[] {
  const porId = new Map(produtos.map((p) => [String(p.id), p]));
  return ids.map((id) => porId.get(id)).filter((p): p is P => Boolean(p));
}

/* ------------------------------------------------------ kits e combos */

export type ComboDaLoja = {
  id: string;
  title: string | null;
  discount_type: "percentage" | "fixed" | "none" | string;
  discount_value: number;
  main_product_id: string;
  combo_product_id: string;
};

export type KitsEmDestaque = { combos: string[]; quantidade: number; margem: EspacoDoWidget };

export function lerKits(config: Record<string, unknown>): KitsEmDestaque {
  return { combos: listaDeIds(config.combos, 24), quantidade: inteiro(config.quantidade, 1, 12, 6), margem: margem(config) };
}

/** O preço do segundo produto com o desconto do combo (o mesmo cálculo da página do produto). */
export function precoComDesconto(preco: number, combo: Pick<ComboDaLoja, "discount_type" | "discount_value">) {
  const original = Number(preco) || 0;
  const valor = Number(combo.discount_value) || 0;
  if (combo.discount_type === "percentage") return Math.max(0, original - (original * valor) / 100);
  if (combo.discount_type === "fixed") return Math.max(0, original - valor);
  return original;
}

/** "10% OFF no segundo", "R$ 5,00 OFF no segundo" ou null (sem desconto). */
export function seloDoCombo(combo: Pick<ComboDaLoja, "discount_type" | "discount_value">) {
  const valor = Number(combo.discount_value) || 0;
  if (valor <= 0) return null;
  if (combo.discount_type === "percentage") return `${valor.toLocaleString("pt-BR", { maximumFractionDigits: 2 })}% OFF no segundo`;
  if (combo.discount_type === "fixed") return `${emReais(valor)} OFF no segundo`;
  return null;
}

/* -------------------------------------------- vistos recentemente */

export type VistosRecentemente = { quantidade: number; margem: EspacoDoWidget };

export function lerVistos(config: Record<string, unknown>): VistosRecentemente {
  return { quantidade: inteiro(config.quantidade, 2, 12, 8), margem: margem(config) };
}

export const LIMITE_DOS_VISTOS = 20;

/** A chave no navegador, por loja. */
export function chaveDosVistos(slug: string) {
  return `ktalogu:vistos:${String(slug || "").toLowerCase()}`;
}

/** Lê a lista guardada (qualquer coisa estranha vira lista vazia). */
export function lerListaDosVistos(bruto: unknown): string[] {
  if (typeof bruto !== "string" || !bruto) return [];
  try {
    return listaDeIds(JSON.parse(bruto), LIMITE_DOS_VISTOS);
  } catch {
    return [];
  }
}

/** Põe o produto na frente da lista (sem repetir) e corta no limite. */
export function juntarVisto(lista: string[], id: string): string[] {
  const limpo = idDeProduto(id);
  if (!limpo) return lista.slice(0, LIMITE_DOS_VISTOS);
  return [limpo, ...lista.filter((x) => x !== limpo)].slice(0, LIMITE_DOS_VISTOS);
}

/* ----------------------------------------------------------- vídeo */

export type LadoDoTexto = "esquerda" | "direita";

export type VideoDaSecao = {
  link: LinkDeVideo | null;
  url: string;
  texto: string;
  lado: LadoDoTexto | "inteiro";
  capa: string;
  margem: EspacoDoWidget;
};

export function lerVideo(config: Record<string, unknown>): VideoDaSecao {
  const url = linha(config.url, 500);
  const link = lerLinkDeVideo(url);
  return {
    /* Só YouTube e Vimeo na loja (o Panda é dos cursos). */
    link: link && (link.provedor === "youtube" || link.provedor === "vimeo") ? link : null,
    url,
    texto: paragrafos(config.texto, 600),
    lado: escolha(config.lado, ["inteiro", "esquerda", "direita"] as const, "inteiro"),
    capa: urlDeImagem(config.capa),
    margem: margem(config),
  };
}

/** O endereço do player, sem rastreio (youtube-nocookie, Vimeo com dnt), tocando ao abrir. */
export function enderecoDoPlayer(link: LinkDeVideo): string {
  const base = new URL(link.url);
  base.searchParams.set("autoplay", "1");
  base.searchParams.set("playsinline", "1");
  if (link.provedor === "youtube") {
    base.searchParams.set("rel", "0");
    base.searchParams.set("modestbranding", "1");
  } else {
    base.searchParams.set("dnt", "1");
  }
  return base.toString();
}

/** A miniatura do YouTube (o Vimeo não tem endereço público sem pedir à API). */
export function capaDoVideo(link: LinkDeVideo | null): string {
  if (!link || link.provedor !== "youtube") return "";
  return `https://i.ytimg.com/vi/${link.id}/hqdefault.jpg`;
}

/* -------------------------------------------- texto com negrito e link */

export type PedacoDoTexto = { tipo: "texto"; valor: string } | { tipo: "negrito"; valor: string } | { tipo: "link"; valor: string; href: string; externo: boolean };
export type BlocoDoTexto = { tipo: "paragrafo"; linhas: PedacoDoTexto[][] } | { tipo: "lista"; itens: PedacoDoTexto[][] };

/**
 * O texto do lojista com o mínimo de formatação: **negrito**, [texto](endereço),
 * linha com "- " vira item de lista e linha em branco separa parágrafos.
 * Nada além disso: é lido aqui e vira elemento na tela, nunca HTML.
 */
export function lerTextoSimples(bruto: unknown): BlocoDoTexto[] {
  const texto = paragrafos(bruto, 3000);
  if (!texto) return [];
  return texto
    .split(/\n\s*\n/)
    .map((bloco) => bloco.split("\n").map((l) => l.trim()).filter(Boolean))
    .filter((linhas) => linhas.length)
    .map((linhas): BlocoDoTexto => {
      if (linhas.every((l) => /^[-•*]\s+/.test(l))) {
        return { tipo: "lista", itens: linhas.map((l) => pedacos(l.replace(/^[-•*]\s+/, ""))) };
      }
      return { tipo: "paragrafo", linhas: linhas.map(pedacos) };
    });
}

function pedacos(texto: string): PedacoDoTexto[] {
  const lista: PedacoDoTexto[] = [];
  const padrao = /\*\*([^*]+?)\*\*|\[([^\]]{1,120})\]\(([^)\s]{1,500})\)/g;
  let ultimo = 0;
  for (let achado = padrao.exec(texto); achado; achado = padrao.exec(texto)) {
    if (achado.index > ultimo) lista.push({ tipo: "texto", valor: texto.slice(ultimo, achado.index) });
    if (achado[1] !== undefined) {
      lista.push({ tipo: "negrito", valor: achado[1] });
    } else {
      const destino = destinoDoLink(achado[3]);
      lista.push(destino ? { tipo: "link", valor: achado[2], href: destino.href, externo: destino.externo } : { tipo: "texto", valor: achado[2] });
    }
    ultimo = achado.index + achado[0].length;
  }
  if (ultimo < texto.length) lista.push({ tipo: "texto", valor: texto.slice(ultimo) });
  return lista;
}

/** O texto sem a marcação (para o Google e para leitores de tela). */
export function textoPuro(bruto: unknown) {
  return lerTextoSimples(bruto)
    .map((bloco) =>
      (bloco.tipo === "lista" ? bloco.itens : bloco.linhas).map((pedacos) => pedacos.map((p) => p.valor).join("")).join("\n")
    )
    .join("\n\n");
}

/* ------------------------------------------------- texto com imagem */

export type ProporcaoDoWidget = "quadrado" | "retrato" | "paisagem";
export const PROPORCOES_DO_WIDGET: Record<ProporcaoDoWidget, number> = { quadrado: 1, retrato: 4 / 5, paisagem: 8 / 5 };

export type BotaoDoWidget = { texto: string; link: Record<string, unknown> | null };

export type TextoComImagem = {
  chamada: string;
  texto: string;
  imagem: string;
  lado: LadoDoTexto;
  proporcao: ProporcaoDoWidget;
  botao: BotaoDoWidget;
  margem: EspacoDoWidget;
};

function lerBotao(config: Record<string, unknown>): BotaoDoWidget {
  const link = config.botao_link && typeof config.botao_link === "object" ? (config.botao_link as Record<string, unknown>) : null;
  return { texto: linha(config.botao_texto, 30), link };
}

export function lerTextoComImagem(config: Record<string, unknown>): TextoComImagem {
  return {
    chamada: linha(config.chamada, 60),
    texto: paragrafos(config.texto, 2000),
    imagem: urlDeImagem(config.imagem),
    /* "esquerda" = a imagem fica à esquerda do texto. */
    lado: escolha<LadoDoTexto>(config.lado, ["esquerda", "direita"], "esquerda"),
    proporcao: escolha<ProporcaoDoWidget>(config.proporcao, ["quadrado", "retrato", "paisagem"], "quadrado"),
    botao: lerBotao(config),
    margem: margem(config),
  };
}

/* --------------------------------------------------- perguntas frequentes */

export const LIMITE_DE_PERGUNTAS = 20;

export type PerguntasFrequentes = { itens: Array<{ pergunta: string; resposta: string }>; abrirPrimeira: boolean; margem: EspacoDoWidget };

export function lerPerguntas(config: Record<string, unknown>): PerguntasFrequentes {
  const itens = (Array.isArray(config.itens) ? config.itens : [])
    .filter((item) => item && typeof item === "object")
    .slice(0, LIMITE_DE_PERGUNTAS)
    .map((item) => {
      const bruto = item as Record<string, unknown>;
      return { pergunta: linha(bruto.pergunta, 200), resposta: paragrafos(bruto.resposta, 1500) };
    })
    .filter((item) => item.pergunta && item.resposta);
  return { itens, abrirPrimeira: config.abrir_primeira === true, margem: margem(config) };
}

/** O bloco FAQPage do schema.org com todas as perguntas das seções. */
export function dadosDasPerguntas(listas: PerguntasFrequentes[]): Record<string, unknown> | null {
  const itens = listas.flatMap((l) => l.itens).slice(0, 50);
  if (!itens.length) return null;
  return {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    mainEntity: itens.map((item) => ({
      "@type": "Question",
      name: item.pergunta,
      acceptedAnswer: { "@type": "Answer", text: textoPuro(item.resposta) },
    })),
  };
}

/* ----------------------------------------------------- onde estamos */

export type EnderecoDaLoja = {
  linhas: string[];
  busca: string;
};

export type PontoDeRetirada = { nome: string; endereco: EnderecoDaLoja; instrucoes: string };

export type DadosDoLocal = { endereco: EnderecoDaLoja | null; retiradas: PontoDeRetirada[]; telefone: string | null };

export type OndeEstamos = {
  usarDaLoja: boolean;
  endereco: string;
  horarios: string;
  mostrarMapa: boolean;
  mostrarRetiradas: boolean;
  margem: EspacoDoWidget;
};

export function lerOndeEstamos(config: Record<string, unknown>): OndeEstamos {
  return {
    usarDaLoja: config.usar_endereco_da_loja !== false,
    endereco: paragrafos(config.endereco, 300),
    horarios: paragrafos(config.horarios, 400),
    mostrarMapa: config.mostrar_mapa !== false,
    mostrarRetiradas: config.mostrar_retiradas === true,
    margem: margem(config),
  };
}

/** O endereço guardado em partes (rua, número, bairro...) em linhas e na busca do mapa. */
export function montarEndereco(partes: {
  rua?: unknown;
  numero?: unknown;
  complemento?: unknown;
  bairro?: unknown;
  cidade?: unknown;
  estado?: unknown;
  cep?: unknown;
}): EnderecoDaLoja | null {
  const rua = linha(partes.rua, 160);
  const numero = linha(partes.numero, 20);
  const complemento = linha(partes.complemento, 80);
  const bairro = linha(partes.bairro, 80);
  const cidade = linha(partes.cidade, 80);
  const estado = linha(partes.estado, 20);
  const cep = linha(partes.cep, 12);
  if (!rua && !cidade) return null;
  const primeira = [rua, numero].filter(Boolean).join(", ") + (complemento ? ` - ${complemento}` : "");
  const segunda = [bairro, [cidade, estado].filter(Boolean).join(" - ")].filter(Boolean).join(", ");
  const linhas = [primeira, segunda, cep ? `CEP ${cep}` : ""].filter(Boolean);
  const busca = [rua && numero ? `${rua}, ${numero}` : rua, bairro, cidade, estado, cep].filter(Boolean).join(", ");
  return { linhas, busca };
}

/** O endereço digitado no bloco (texto livre), em linhas e na busca. */
export function enderecoDigitado(texto: string): EnderecoDaLoja | null {
  const linhas = texto.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 5);
  if (!linhas.length) return null;
  return { linhas, busca: linhas.join(", ") };
}

/** O telefone como se lê: (81) 99876-5432. O que não for número brasileiro fica como veio. */
export function telefoneLegivel(valor: unknown): string {
  const texto = String(valor ?? "").trim();
  const digitos = texto.replace(/\D/g, "").replace(/^55(?=\d{10,11}$)/, "");
  if (digitos.length === 11) return `(${digitos.slice(0, 2)}) ${digitos.slice(2, 7)}-${digitos.slice(7)}`;
  if (digitos.length === 10) return `(${digitos.slice(0, 2)}) ${digitos.slice(2, 6)}-${digitos.slice(6)}`;
  return texto;
}

/** O mapa do Google sem chave nenhuma (o "output=embed" do próprio Google Maps). */
export function enderecoDoMapa(busca: string) {
  return `https://www.google.com/maps?q=${encodeURIComponent(busca)}&output=embed&hl=pt-BR&z=16`;
}

export function enderecoDaRota(busca: string) {
  return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(busca)}`;
}

/* ----------------------------------------------- faixa que desliza */

export type FaixaQueDesliza = {
  itens: string[];
  velocidade: "devagar" | "normal" | "rapido";
  fundo: string;
  cor: string;
  tamanho: "p" | "m" | "g";
};

export const LIMITE_DA_FAIXA = 12;
export const SEGUNDOS_DA_FAIXA = { devagar: 60, normal: 36, rapido: 20 } as const;

export function lerFaixa(config: Record<string, unknown>, corDaLoja = "#111827"): FaixaQueDesliza {
  const itens = (Array.isArray(config.itens) ? config.itens : []).map((item) => linha(item, 80)).filter(Boolean).slice(0, LIMITE_DA_FAIXA);
  return {
    itens,
    velocidade: escolha(config.velocidade, ["devagar", "normal", "rapido"] as const, "normal"),
    fundo: corHex(config.cor_de_fundo, corDaLoja),
    cor: corHex(config.cor_do_texto, "#ffffff"),
    tamanho: escolha(config.tamanho, ["p", "m", "g"] as const, "m"),
  };
}

/* ------------------------------------------ imagem com pontos clicáveis */

export const LIMITE_DE_PONTOS = 12;

export type PontoDaImagem = { x: number; y: number; produtoId: string };
export type ImagemComPontos = { imagem: string; proporcao: ProporcaoDoWidget | "faixa"; pontos: PontoDaImagem[]; margem: EspacoDoWidget };
export const PROPORCOES_DOS_PONTOS: Record<ImagemComPontos["proporcao"], number> = { ...PROPORCOES_DO_WIDGET, faixa: 16 / 9 };

function porcentagem(valor: unknown) {
  const numero = Number(valor);
  if (!Number.isFinite(numero)) return null;
  return Math.round(Math.min(100, Math.max(0, numero)) * 10) / 10;
}

export function lerImagemComPontos(config: Record<string, unknown>): ImagemComPontos {
  const pontos = (Array.isArray(config.pontos) ? config.pontos : [])
    .filter((p) => p && typeof p === "object")
    .map((p) => {
      const bruto = p as Record<string, unknown>;
      const x = porcentagem(bruto.x);
      const y = porcentagem(bruto.y);
      return x === null || y === null ? null : { x, y, produtoId: idDeProduto(bruto.produto_id) };
    })
    .filter((p): p is PontoDaImagem => Boolean(p))
    .slice(0, LIMITE_DE_PONTOS);
  return {
    imagem: urlDeImagem(config.imagem),
    proporcao: escolha(config.proporcao, ["quadrado", "retrato", "paisagem", "faixa"] as const, "paisagem"),
    pontos,
    margem: margem(config),
  };
}

/* ------------------------------------------------------ antes e depois */

export type AntesEDepois = {
  antes: string;
  depois: string;
  rotuloAntes: string;
  rotuloDepois: string;
  proporcao: ProporcaoDoWidget | "faixa";
  inicio: number;
  margem: EspacoDoWidget;
};

export function lerAntesEDepois(config: Record<string, unknown>): AntesEDepois {
  return {
    antes: urlDeImagem(config.antes),
    depois: urlDeImagem(config.depois),
    rotuloAntes: linha(config.rotulo_antes, 24) || "Antes",
    rotuloDepois: linha(config.rotulo_depois, 24) || "Depois",
    proporcao: escolha(config.proporcao, ["quadrado", "retrato", "paisagem", "faixa"] as const, "paisagem"),
    inicio: inteiro(config.inicio, 5, 95, 50),
    margem: margem(config),
  };
}

/** A barra anda com as setas (5 em 5), Home e End; o resto não mexe. */
export function posicaoPeloTeclado(atual: number, tecla: string): number | null {
  if (tecla === "ArrowLeft" || tecla === "ArrowDown") return Math.max(0, atual - 5);
  if (tecla === "ArrowRight" || tecla === "ArrowUp") return Math.min(100, atual + 5);
  if (tecla === "Home") return 0;
  if (tecla === "End") return 100;
  if (tecla === "PageDown") return Math.max(0, atual - 20);
  if (tecla === "PageUp") return Math.min(100, atual + 20);
  return null;
}

/* ---------------------------------------- o que a página precisa ler */

/**
 * Os dados dos blocos que vêm do banco (montados no servidor, em
 * lib/loja/dados-dos-widgets.ts). Só vem o que as seções da loja usam; loja
 * sem esses blocos não lê nada a mais.
 */
export type DadosDosWidgets = {
  agora: number;
  cupons: CupomDaLoja[];
  corte: CorteDaLoja;
  lives: LiveDaLoja[];
  maisVendidos: Partial<Record<"7" | "30" | "90", string[]>>;
  combos: ComboDaLoja[];
  local: DadosDoLocal;
  /* O visitante não vê preço (regras da loja): oferta, kit e cupom sem valor. */
  semPreco: boolean;
};

/** Quais dados cada tipo pede (para o servidor ler só o necessário). */
export function dadosPedidos(secoes: Array<{ config?: unknown }>): {
  cupons: boolean;
  lives: boolean;
  combos: boolean;
  local: boolean;
  maisVendidos: Array<7 | 30 | 90>;
} {
  const pedido = { cupons: false, lives: false, combos: false, local: false, maisVendidos: [] as Array<7 | 30 | 90> };
  for (const secao of secoes) {
    const config = secao?.config && typeof secao.config === "object" ? (secao.config as Record<string, unknown>) : {};
    const modo = config.mode;
    if (modo === "coupon_highlight") pedido.cupons = true;
    if (modo === "next_live") pedido.lives = true;
    if (modo === "combo_highlight") pedido.combos = true;
    if (modo === "store_location") pedido.local = true;
    if (modo === "auto_list") {
      const lista = lerListaAutomatica(config);
      if (lista.lista === "mais_vendidos" && !pedido.maisVendidos.includes(lista.dias)) pedido.maisVendidos.push(lista.dias);
    }
  }
  return pedido;
}

/* ------------------------------------------------ o preço nos cartões */

type ProdutoComPreco = {
  price?: unknown;
  price_on_request?: unknown;
  preco_oculto?: unknown;
  product_kind?: unknown;
  unit_type?: unknown;
  use_variation_price_only?: unknown;
  product_attributes?: Array<{ is_required?: unknown; options?: unknown }> | null;
  product_price_tiers?: Array<{ unit_price?: unknown; active?: unknown }> | null;
};

/**
 * O preço dos cartões pequenos dos blocos, com as mesmas regras do card da
 * loja (resolveProductPriceDisplay no StoreContent): sob consulta, digital
 * grátis, m² sob medida, "a partir de" das variações e das faixas.
 */
export function precoDoCartao(bruto: object, textoSemPreco = "Sob consulta"): { texto: string; valor: number | null; aPartir: boolean } {
  const produto = bruto as ProdutoComPreco;
  if (produto.price_on_request || produto.preco_oculto) return { texto: textoSemPreco, valor: null, aPartir: false };
  if (produto.product_kind === "digital" && !(Number(produto.price) > 0)) return { texto: "Grátis", valor: 0, aPartir: false };
  if (produto.unit_type === "square_meter") return { texto: "Preço sob medida", valor: null, aPartir: false };
  if (produto.use_variation_price_only) {
    const atributos = Array.isArray(produto.product_attributes) ? produto.product_attributes : [];
    const obrigatorios = atributos.some((a) => Boolean(a?.is_required)) ? atributos.filter((a) => Boolean(a?.is_required)) : atributos;
    let total = 0;
    for (const atributo of obrigatorios) {
      const precos = (Array.isArray(atributo?.options) ? atributo.options : [])
        .map((o: unknown) => (o && typeof o === "object" ? Number((o as { price?: unknown }).price) || 0 : 0))
        .filter((v: number) => Number.isFinite(v) && v >= 0);
      if (precos.length) total += Math.min(...precos);
    }
    return { texto: `A partir de ${emReais(total)}`, valor: total, aPartir: true };
  }
  const base = Number(produto.price) || 0;
  const faixas = (Array.isArray(produto.product_price_tiers) ? produto.product_price_tiers : [])
    .filter((f) => f?.active !== false)
    .map((f) => Number(f?.unit_price))
    .filter((v) => Number.isFinite(v) && v > 0);
  const menor = faixas.length ? Math.min(...faixas) : null;
  if (menor !== null && menor < base) return { texto: `A partir de ${emReais(menor)}`, valor: menor, aPartir: true };
  return { texto: emReais(base), valor: base, aPartir: false };
}

/** O preço é um número só (dá para somar no kit)? */
export function precoFixo(produto: object): number | null {
  const preco = precoDoCartao(produto);
  return preco.valor !== null && !preco.aPartir ? preco.valor : null;
}

/** A foto principal do produto (a marcada como principal, ou a primeira). */
export function fotoDoProdutoDoWidget(bruto: object): string {
  const produto = bruto as { image_url?: unknown; product_images?: Array<{ image_url?: unknown; is_primary?: unknown }> | null };
  const imagens = (Array.isArray(produto.product_images) ? produto.product_images : []).filter((i) => i?.image_url);
  const principal = imagens.find((i) => i.is_primary) || imagens[0];
  return String(principal?.image_url || produto.image_url || "");
}
