/**
 * A faxina do HTML do boletim.
 *
 * O editor é uma folha de escrita, então o que ele entrega é HTML. Guardar
 * isso cru seria abrir a porta: a página do boletim é pública e sem login, e
 * um `<script>` ou um `onerror=` gravado aqui rodaria no navegador de quem
 * abrisse o link.
 *
 * A regra é lista de permitidos, e não lista de proibidos. Tag que não está na
 * lista some; atributo que não está na lista some; endereço que não seja
 * http, https ou mailto some. O que sobra é texto formatado, que é tudo o que
 * um boletim precisa ser.
 *
 * Sem biblioteca de fora de propósito: uma dependência nova precisaria entrar
 * no lockfile e viajar para a VPS a cada deploy, e a superfície aqui é pequena
 * o bastante para caber em cem linhas com teste.
 */

/** Tag -> atributos que ela pode manter. */
const PERMITIDAS: Record<string, string[]> = {
  p: [],
  br: [],
  strong: [],
  b: [],
  em: [],
  s: [],
  u: [],
  h2: [],
  h3: [],
  ul: [],
  ol: [],
  li: [],
  blockquote: [],
  hr: [],
  /* O bloco de video guarda so o ID, nunca um <iframe> montado pelo editor.
     Quem monta o player e a pagina, com o dominio que ela escolhe. */
  figure: ["class", "data-youtube", "data-video"],
  figcaption: ["class"],
  a: ["href", "target", "rel"],
  img: ["src", "alt"],
  div: ["class"],
  span: ["class"],
  small: [],
  /* A pergunta do help abre e fecha sem uma linha de script. */
  details: ["class"],
  summary: ["class"],
  /* Tabela para comparar: é a forma honesta de mostrar "antes e depois" sem
     virar imagem, que ninguém consegue ler no celular. */
  table: ["class"],
  thead: [],
  tbody: [],
  tr: [],
  th: [],
  td: [],
  /* `i` faz dois papéis: itálico no texto e a barra do gráfico. É a única tag
     que pode trazer `style`, e só para dizer a largura. Ver `larguraSegura`. */
  i: ["class", "style"],
};

/** Classes que o boletim usa de verdade. Qualquer outra é descartada. */
const CLASSES_PERMITIDAS = new Set([
  "boletim-destaque",
  "boletim-botao",
  "boletim-figura",
  /* Cartões de preço */
  "boletim-precos",
  "boletim-preco",
  "boletim-preco-destaque",
  "boletim-preco-nome",
  "boletim-preco-antes",
  "boletim-preco-valor",
  "boletim-preco-nota",
  "boletim-selo",
  /* Tabela de comparação */
  "boletim-tabela",
  /* Gráfico de barras */
  "boletim-barras",
  "boletim-barra",
  "boletim-barra-rotulo",
  "boletim-barra-trilho",
  "boletim-barra-preenche",
  "boletim-barra-valor",

  /* ---- Centro de aprendizado ----
     Os blocos do help passam pela MESMA faxina do boletim: as duas paginas
     abrem sem login e o HTML entra pela mesma porta. Duplicar a faxina seria
     duplicar a chance de esquecer uma delas. */
  "help-passos",
  "help-passo",
  "help-passo-corpo",
  "help-passo-tela",
  "help-passo-barra",
  "help-aviso",
  "help-aviso-dica",
  "help-aviso-atencao",
  "help-aviso-cuidado",
  "help-aviso-titulo",
  "help-caminho",
  "help-video",
  "help-video-quadro",
  "help-video-legenda",
  "help-tabela",
  "help-faq",
  "help-faq-pergunta",
  "help-faq-resposta",
  "help-figura",
  "help-link",
]);

const VAZIAS = new Set(["br", "hr", "img"]);

/**
 * A única declaração de estilo que atravessa a faxina.
 *
 * A barra do gráfico precisa de uma largura, e largura é dado, não desenho.
 * Aceitar `style` inteiro abriria a porta para `background:url(...)` e para
 * sobreposição de elemento em cima do resto da página, então aqui passa
 * exatamente uma coisa: uma porcentagem entre 0 e 100.
 */
function larguraSegura(valor: string) {
  const achado = /^\s*width\s*:\s*(\d{1,3}(?:\.\d{1,2})?)%\s*;?\s*$/.exec(valor);
  if (!achado) return null;
  const numero = Number(achado[1]);
  if (!Number.isFinite(numero) || numero < 0 || numero > 100) return null;
  return `width:${numero}%`;
}

function enderecoSeguro(valor: string) {
  const bruto = valor.trim();
  if (!bruto) return null;
  /* Endereço relativo do próprio site continua valendo; o resto tem de
     declarar um protocolo que a gente conhece. */
  if (bruto.startsWith("/") && !bruto.startsWith("//")) return bruto;
  try {
    const url = new URL(bruto);
    const protocolo = url.protocol.toLowerCase();
    if (protocolo === "http:" || protocolo === "https:" || protocolo === "mailto:") return bruto;
    return null;
  } catch {
    return null;
  }
}

function escaparTexto(valor: string) {
  /* O `&` só vira `&amp;` quando não é o começo de uma entidade que já existe.
     Sem essa ressalva, um `&lt;` que o editor já escreveu viraria `&amp;lt;` e
     a pessoa veria "&lt;" impresso na página em vez do sinal de menor. */
  return valor
    .replace(/&(?!#?[a-zA-Z0-9]{1,8};)/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function escaparAtributo(valor: string) {
  return escaparTexto(valor).replace(/"/g, "&quot;");
}

function limparClasses(valor: string) {
  const boas = valor
    .split(/\s+/)
    .map((c) => c.trim())
    .filter((c) => CLASSES_PERMITIDAS.has(c));
  return boas.length > 0 ? boas.join(" ") : null;
}

/** Lê os atributos de uma tag de abertura, sem regex frágil de par nome/valor. */
function lerAtributos(bruto: string) {
  const encontrados: Array<[string, string]> = [];
  const padrao = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
  let achado: RegExpExecArray | null;
  while ((achado = padrao.exec(bruto)) !== null) {
    const nome = achado[1].toLowerCase();
    const valor = achado[3] ?? achado[4] ?? achado[5] ?? "";
    encontrados.push([nome, valor]);
  }
  return encontrados;
}

export function faxinarHtmlDoBoletim(entrada: unknown): string {
  const bruto = typeof entrada === "string" ? entrada : "";
  if (!bruto.trim()) return "";

  /* Comentários saem inteiros antes de qualquer coisa: dentro deles cabe uma
     tag fechada de um jeito que confunde a leitura abaixo. */
  let texto = bruto.replace(/<!--[\s\S]*?-->/g, "");
  /* `<script>` e `<style>` saem com conteúdo e tudo, senão o corpo deles
     sobraria como texto solto no meio da página. */
  texto = texto.replace(/<(script|style|iframe|object|embed)[\s\S]*?<\/\1\s*>/gi, "");
  texto = texto.replace(/<(script|style|iframe|object|embed)\b[^>]*\/?>/gi, "");

  const abertas: string[] = [];
  let saida = "";
  let posicao = 0;

  const padraoDeTag = /<\/?([a-zA-Z][a-zA-Z0-9]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g;
  let achado: RegExpExecArray | null;

  while ((achado = padraoDeTag.exec(texto)) !== null) {
    saida += escaparTexto(texto.slice(posicao, achado.index));
    posicao = achado.index + achado[0].length;

    const tag = achado[1].toLowerCase();
    const fechamento = achado[0].startsWith("</");
    const permitidos = PERMITIDAS[tag];

    if (!permitidos) continue; // tag desconhecida: some, o conteúdo dela fica

    if (fechamento) {
      const ultima = abertas.lastIndexOf(tag);
      if (ultima === -1) continue; // fechamento sem abertura
      /* Fecha o que ficou aberto por dentro, na ordem certa. */
      while (abertas.length > ultima) {
        const pendente = abertas.pop() as string;
        saida += `</${pendente}>`;
      }
      continue;
    }

    const atributos = lerAtributos(achado[2] || "");
    let escrito = `<${tag}`;
    let temEndereco = tag !== "a" && tag !== "img";

    for (const [nome, valor] of atributos) {
      if (!permitidos.includes(nome)) continue;

      if (nome === "href" || nome === "src") {
        const seguro = enderecoSeguro(valor);
        if (!seguro) continue;
        escrito += ` ${nome}="${escaparAtributo(seguro)}"`;
        temEndereco = true;
        continue;
      }
      if (nome === "class") {
        const classes = limparClasses(valor);
        if (classes) escrito += ` class="${escaparAtributo(classes)}"`;
        continue;
      }
      if (nome === "style") {
        const largura = larguraSegura(valor);
        if (largura) escrito += ` style="${escaparAtributo(largura)}"`;
        continue;
      }
      if (nome === "target") {
        escrito += ` target="_blank"`;
        continue;
      }
      if (nome === "rel") continue; // reescrito abaixo
      /* O identificador do video do YouTube. So o ID entra, nunca uma URL
         inteira: quem monta o endereco do player e a pagina. */
      if (nome === "data-youtube") {
        if (/^[A-Za-z0-9_-]{6,20}$/.test(valor)) escrito += ` data-youtube="${escaparAtributo(valor)}"`;
        continue;
      }
      /* Video de arquivo proprio: e endereco, entao passa pela mesma peneira
         de `href` e `src`. */
      if (nome === "data-video") {
        const seguro = enderecoSeguro(valor);
        if (seguro) escrito += ` data-video="${escaparAtributo(seguro)}"`;
        continue;
      }
      escrito += ` ${nome}="${escaparAtributo(valor)}"`;
    }

    /* Link para fora sem `rel` deixa a página de destino mexer nesta pela
       janela que abriu. Aqui todo link externo sai blindado. */
    if (tag === "a") {
      if (!temEndereco) continue; // link sem destino válido não vira link
      escrito += ` rel="noopener noreferrer nofollow"`;
    }
    if (tag === "img" && !temEndereco) continue;

    if (VAZIAS.has(tag)) {
      saida += `${escrito} />`;
      continue;
    }

    saida += `${escrito}>`;
    abertas.push(tag);
  }

  saida += escaparTexto(texto.slice(posicao));
  while (abertas.length > 0) saida += `</${abertas.pop() as string}>`;

  return saida.trim();
}

/** O endereço do boletim: minúsculas, sem acento, palavras ligadas por hífen. */
export function faxinarSlug(entrada: unknown): string {
  return String(entrada ?? "")
    /* NFD separa a letra do acento; o intervalo abaixo é o dos acentos
       soltos, escrito em escapes para não depender de como o arquivo foi
       salvo. */
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80);
}
