// services/analytics/src/site-crawl.ts — adaptateur mince du job `site-crawl`.
// Câble les vraies dépendances (db du service, fetch réseau, wasm) au cœur
// orchestrable `runSiteCrawl` (src/lib/pages/crawl-runner.ts, testé par test:all).

import * as wasm from "import-core";
import { fetchHtml } from "@/lib/keywords/onpage-fetch";
import { runSiteCrawl } from "@/lib/pages/crawl-runner";
import type { PageMeta } from "@/lib/pages/types";
import { db } from "./db";
import { completeJob, failJob, type Job, updateJobTotals } from "./queue";

/** Timeout par requête sitemap (mêmes ordres de grandeur que fetchHtmlWith). */
const SITEMAP_TIMEOUT_MS = 15_000;
/** Cap de lecture d'un fichier sitemap (10 Mo — un sitemap.xml reste texte). */
const SITEMAP_MAX_BYTES = 10 * 1024 * 1024;

const SITEMAP_HEADERS = {
  "User-Agent":
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36",
  Accept: "application/xml,text/xml,text/plain,*/*",
};

/**
 * Fetch texte assoupli pour la découverte sitemap : accepte les content-types
 * xml/text/plain (robots.txt + sitemap.xml), là où `fetchHtmlWith` n'accepte
 * que du HTML. Tout échec → null (le discover retombe sur le fallback / vide).
 */
async function fetchSitemapText(url: string): Promise<string | null> {
  try {
    const res = await fetch(url, {
      signal: AbortSignal.timeout(SITEMAP_TIMEOUT_MS),
      redirect: "follow",
      headers: SITEMAP_HEADERS,
    });
    if (!res.ok) return null;
    const type = (res.headers.get("content-type") ?? "").toLowerCase();
    // Accepte xml, text/*, et les serveurs qui ne renvoient aucun content-type.
    if (
      type &&
      !type.includes("xml") &&
      !type.includes("text/") &&
      !type.includes("octet-stream")
    ) {
      return null;
    }
    const reader = res.body?.getReader();
    if (!reader) return null;
    const chunks: Uint8Array[] = [];
    let total = 0;
    while (total < SITEMAP_MAX_BYTES) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
      total += value.byteLength;
    }
    if (total >= SITEMAP_MAX_BYTES) await reader.cancel().catch(() => {});
    const body = new Uint8Array(Math.min(total, SITEMAP_MAX_BYTES));
    let offset = 0;
    for (const chunk of chunks) {
      const room = body.length - offset;
      if (room <= 0) break;
      body.set(
        room >= chunk.byteLength ? chunk : chunk.subarray(0, room),
        offset,
      );
      offset += Math.min(room, chunk.byteLength);
    }
    return new TextDecoder().decode(body);
  } catch {
    return null;
  }
}

/** Handler `site-crawl` : orchestration déléguée au runner, statut du job géré
 *  ici (progress → running, succès → done, url manquante/erreur dure → failed). */
export async function processSiteCrawl(job: Job): Promise<void> {
  try {
    const totals = await runSiteCrawl(job.siteId, {
      db,
      fetchText: fetchSitemapText,
      fetchHtml,
      parseMeta: (html: string) => wasm.parse_page_meta(html) as PageMeta,
      onProgress: async (t) => {
        await updateJobTotals(job.id, t);
      },
    });
    await completeJob(job.id, totals as unknown as Job["totals"]);
  } catch (e) {
    await failJob(job.id, (e as Error).message);
  }
}
