/**
 * O mapa do site (sitemap.xml): a lista de páginas que a loja publica para o
 * Google. É como achar todos os produtos de uma loja que não entrega o
 * catálogo em dados. O mapa pode ser um índice de outros mapas; os que têm
 * "produto" no nome vêm primeiro.
 */

import { absoluto, semEntidades, semRepetir } from "../modelo.ts";

export type MapaLido = { mapas: string[]; paginas: string[] };

/*
 * O mapa vem de site alheio. Nada aqui relê o texto: os <loc> são achados
 * por posição (indexOf), e o espaço em volta e o CDATA saem no código. (Uma
 * expressão com vários \s* em volta de um grupo preguiçoso levava horas com
 * um mapa de 1 KB montado de propósito.)
 */
function valoresDeLoc(xml: string): string[] {
  const minusculo = xml.toLowerCase();
  const saida: string[] = [];
  let posicao = 0;
  while (saida.length < 50_000) {
    const inicio = minusculo.indexOf("<loc>", posicao);
    if (inicio < 0) break;
    const fim = minusculo.indexOf("</loc>", inicio + 5);
    if (fim < 0) break;
    let valor = xml.slice(inicio + 5, Math.min(fim, inicio + 5 + 4000)).trim();
    if (valor.startsWith("<![CDATA[")) valor = valor.slice(9);
    if (valor.endsWith("]]>")) valor = valor.slice(0, -3);
    saida.push(valor.trim());
    posicao = fim + 6;
  }
  return saida;
}

export function lerSitemap(xml: string, base: string): MapaLido {
  const enderecos = semRepetir(valoresDeLoc(xml).map((valor) => absoluto(semEntidades(valor), base)).filter((endereco): endereco is string => Boolean(endereco)));
  /* O índice de mapas tem <sitemapindex> na raiz; a lista de páginas, <urlset>. */
  const minusculo = xml.toLowerCase();
  const indice = minusculo.indexOf("<sitemapindex");
  const lista = minusculo.indexOf("<urlset");
  const ehIndice = indice >= 0 && (lista < 0 || indice < lista);
  return ehIndice ? { mapas: ordenarMapas(enderecos), paginas: [] } : { mapas: [], paginas: enderecos };
}

/** Mapas de produto primeiro; os de blog, página, imagem e categoria por último (ou fora). */
export function ordenarMapas(mapas: string[]): string[] {
  const peso = (endereco: string) => {
    /* Só o nome do arquivo, sem a palavra "sitemap" (que tem "item" dentro). */
    const nome = (endereco.toLowerCase().split(/[?#]/)[0].split("/").pop() ?? "").replace(/sitemap/g, "");
    if (/produto|product|(^|[^a-z])prod([^a-z]|$)|(^|[^a-z])items?([^a-z]|$)/.test(nome)) return 0;
    if (/blog|post|page|pagina|image|imagem|video|categor|collection|colec|tag|autor|author|marca|brand/.test(nome)) return 2;
    return 1;
  };
  return [...mapas].sort((a, b) => peso(a) - peso(b));
}

/** O mapa é só de produtos (pelo nome do arquivo)? Aí toda página dele é produto. */
export function mapaDeProdutos(endereco: string): boolean {
  return /produto|product/.test(endereco.toLowerCase());
}

const NAO_E_PRODUTO = /\/(blog|blogs|pages?|pagina|paginas|categorias?|categoria|category|categories|collections?|colecao|colecoes|departamento|tag|tags|search|busca|cart|carrinho|checkout|account|conta|login|contato|contact|sobre|about|politica|policies|termos|faq|ajuda|marcas?|brands?)(\/|$)/i;

/** O endereço tem cara de página de produto? */
export function pareceProduto(endereco: string): boolean {
  let caminho: string;
  try {
    caminho = new URL(endereco).pathname.toLowerCase();
  } catch {
    return false;
  }
  if (NAO_E_PRODUTO.test(caminho)) return false;
  return (
    /\/(produto|produtos|product|products|item|itens|prod)\/[^/]+/.test(caminho) ||
    /^\/[^/]+\/p\/?$/.test(caminho) ||
    /\/p\/[^/]+/.test(caminho) ||
    /-p\d+(\.html?)?$/.test(caminho) ||
    /\/[^/]+-\d{3,}(\.html?)?$/.test(caminho)
  );
}

/** Os links de uma página que têm cara de produto, do mesmo site. */
export function linksDeProduto(html: string, base: string): string[] {
  let site: string;
  try {
    site = new URL(base).hostname.replace(/^www\./, "");
  } catch {
    return [];
  }
  const enderecos: string[] = [];
  /* A marcação inteira do link, com teto e sem atravessar outra tag; o href sai dela. */
  for (const achado of html.matchAll(/<a\b[^<>]{0,2000}>/gi)) {
    if (enderecos.length >= 5000) break;
    const href = /\bhref\s*=\s*(?:"([^"<>]{1,2000})"|'([^'<>]{1,2000})')/i.exec(achado[0]);
    const endereco = href ? absoluto(semEntidades(href[1] ?? href[2] ?? ""), base) : null;
    if (!endereco) continue;
    try {
      const url = new URL(endereco);
      if (url.hostname.replace(/^www\./, "") !== site) continue;
      url.search = "";
      if (pareceProduto(url.toString())) enderecos.push(url.toString());
    } catch {
      /* link quebrado */
    }
  }
  return semRepetir(enderecos);
}
