// src/lib/keywords/onpage-fetch.ts — in-house HTML fetch for the on-page crawler.
// Any failure (network, timeout, non-200, non-HTML, body error) → null: a
// competitor page we cannot read is a skipped crawl, never a thrown action.

export type FetchHtml = (url: string) => Promise<string | null>;

/** Callable shape of `fetch` (bun-types' `typeof fetch` also demands `preconnect`). */
type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;

const TIMEOUT_MS = 15_000;
/** Read cap: pages bigger than 3 MB are truncated (enough for on-page signals). */
const MAX_BYTES = 3 * 1024 * 1024;

const 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: "text/html,application/xhtml+xml",
  "Accept-Language": "fr,en;q=0.8",
};

/** Factory over `fetch` so tests can inject a fake without mock.module. */
export function fetchHtmlWith(f: FetchLike): FetchHtml {
  return async (url) => {
    try {
      const res = await f(url, {
        signal: AbortSignal.timeout(TIMEOUT_MS),
        redirect: "follow",
        headers: HEADERS,
      });
      if (!res.ok) return null;
      const type = res.headers.get("content-type") ?? "";
      if (!type.includes("text/html") && !type.includes("xhtml")) return null;
      const reader = res.body?.getReader();
      if (!reader) return null;

      const chunks: Uint8Array[] = [];
      let total = 0;
      while (total < MAX_BYTES) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
        total += value.byteLength;
      }
      if (total >= MAX_BYTES) {
        // over the cap: stop the stream, keep what was read (truncated below)
        await reader.cancel().catch(() => {});
      }

      const body = new Uint8Array(Math.min(total, 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;
    }
  };
}

export const fetchHtml: FetchHtml = fetchHtmlWith(fetch);
