// src/lib/pages/sitemap.ts — découverte d'URLs par sitemap (robots.txt → sitemaps).
// TOUT en DI fetch : aucune requête réseau réelle ici.

/** Cap dur d'URLs collectées par crawl. */
export const SITEMAP_URL_CAP = 5_000;

/** Récursion maximale sur les sitemapindex (nombre de niveaux de fichiers sitemap). */
const MAX_SITEMAP_DEPTH = 3;

/** Récupère le texte d'une URL, ou `null` en cas d'échec. */
export type FetchText = (url: string) => Promise<string | null>;

/** Host sans préfixe `www.`, en minuscules — pour comparaison « même host » tolérante au www. */
function bareHost(host: string): string {
  return host.toLowerCase().replace(/^www\./, "");
}

/** true si `url` partage le host du site (www toléré des deux côtés) ; false si URL invalide. */
function sameSiteHost(url: string, siteHost: string): boolean {
  try {
    return bareHost(new URL(url).host) === siteHost;
  } catch {
    return false;
  }
}

/** Extrait les URLs des lignes `Sitemap:` d'un robots.txt (insensible casse, espaces tolérés). */
export function parseRobotsSitemaps(robotsTxt: string): string[] {
  const out: string[] = [];
  for (const line of robotsTxt.split(/\r?\n/)) {
    const m = line.match(/^\s*sitemap\s*:\s*(\S+)/i);
    if (m?.[1]) out.push(m[1].trim());
  }
  return out;
}

/** Décode un contenu `<loc>` : strip CDATA, décode les entités XML de base, trim. */
function cleanLoc(raw: string): string {
  return raw
    .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#0*39;|&apos;/g, "'")
    .trim();
}

/** Extrait les `<loc>` contenus dans les blocs matchant `blockTag` (`url` ou `sitemap`). */
function locsInBlocks(xml: string, blockTag: string): string[] {
  // `(?:[\w-]+:)?` tolère les préfixes de namespace (ex. `<ns:url>`, `<ns:loc>`).
  const blockRe = new RegExp(
    `<(?:[\\w-]+:)?${blockTag}\\b[^>]*>([\\s\\S]*?)<\\/(?:[\\w-]+:)?${blockTag}\\s*>`,
    "gi",
  );
  const locRe = /<(?:[\w-]+:)?loc\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?loc\s*>/gi;
  const out: string[] = [];
  for (const block of xml.matchAll(blockRe)) {
    const body = block[1] ?? "";
    for (const loc of body.matchAll(locRe)) {
      const value = cleanLoc(loc[1] ?? "");
      if (value) out.push(value);
    }
  }
  return out;
}

/**
 * Parse un XML de sitemap. Distingue urlset (pages, via `<url>`) de sitemapindex
 * (enfants, via `<sitemap>`) par la présence des balises. XML pourri → tout vide,
 * jamais de throw.
 */
export function parseSitemapXml(xml: string): {
  urls: string[];
  children: string[];
} {
  if (!xml) return { urls: [], children: [] };
  return {
    urls: locsInBlocks(xml, "url"),
    children: locsInBlocks(xml, "sitemap"),
  };
}

/**
 * Découvre les URLs de pages d'un site via ses sitemaps :
 * robots.txt (`Sitemap:`) puis fallback `/sitemap.xml` ; récursion sitemapindex
 * ≤ 3 niveaux avec anti-boucle ; filtre même-host (www toléré) ; dédup ; cap 5 000.
 */
export async function discoverSitemapUrls(
  siteUrl: string,
  fetchText: FetchText,
): Promise<{ urls: string[]; truncated: boolean }> {
  let origin: string;
  let siteHost: string;
  try {
    const site = new URL(siteUrl);
    origin = site.origin;
    siteHost = bareHost(site.host);
  } catch {
    return { urls: [], truncated: false };
  }

  // 1. Sitemaps déclarés dans robots.txt (filtrés même-host), sinon /sitemap.xml.
  const robotsTxt = await fetchText(`${origin}/robots.txt`);
  let seeds = robotsTxt
    ? parseRobotsSitemaps(robotsTxt).filter((u) => sameSiteHost(u, siteHost))
    : [];
  if (seeds.length === 0) seeds = [`${origin}/sitemap.xml`];

  const pages = new Set<string>();
  const visited = new Set<string>();

  const crawl = async (sitemapUrl: string, depth: number): Promise<void> => {
    if (depth > MAX_SITEMAP_DEPTH) return;
    if (visited.has(sitemapUrl)) return;
    visited.add(sitemapUrl);
    if (!sameSiteHost(sitemapUrl, siteHost)) return;

    const xml = await fetchText(sitemapUrl);
    if (!xml) return;
    const { urls, children } = parseSitemapXml(xml);

    for (const u of urls) {
      if (sameSiteHost(u, siteHost)) pages.add(u);
    }
    for (const child of children) {
      await crawl(child, depth + 1);
    }
  };

  for (const seed of seeds) {
    await crawl(seed, 1);
  }

  const all = [...pages];
  const truncated = all.length > SITEMAP_URL_CAP;
  return {
    urls: truncated ? all.slice(0, SITEMAP_URL_CAP) : all,
    truncated,
  };
}
