"""Geboundeter Domain-Crawler für Link-Quellen — rendert JS via Playwright (Chromium).

Lädt ab einer Start-URL Seiten + PDFs — NUR dieselbe Domain, begrenzte Tiefe und
Seitenzahl. HTML-Seiten werden im Headless-Browser gerendert (für SPAs nötig), dann
Links + Text aus dem fertigen DOM gezogen. PDFs werden direkt als Bytes geladen.
Deterministisch, gebounded; läuft via asyncio.to_thread (Sync-API, kein Event-Loop).
"""

import hashlib
import logging
import re
from pathlib import Path
from urllib.parse import urldefrag, urlparse
from urllib.request import Request, urlopen

from fsutil import atomic_write_text

log = logging.getLogger("creator.crawl")

MAX_TIEFE = 3
MAX_SEITEN = 500
SEITE_TIMEOUT = 30        # Sekunden pro Seite (Render bzw. PDF-Download)
MAX_BYTES = 10_000_000    # 10 MB Deckel pro PDF
_UA = "Mozilla/5.0 (creator-lernbot)"


def _fetch_bytes(url: str) -> bytes | None:
    """PDF-Bytes per urllib laden (kein Rendering nötig). None bei Fehler/zu groß."""
    try:
        req = Request(url, headers={"User-Agent": _UA})
        with urlopen(req, timeout=SEITE_TIMEOUT) as resp:
            data = resp.read(MAX_BYTES + 1)
        return None if len(data) > MAX_BYTES else data
    except Exception as e:
        log.debug("crawl: PDF-Fetch fehlgeschlagen %s: %s", url, e)
        return None


def _name(url: str, ext: str) -> str:
    h = hashlib.md5(url.encode("utf-8")).hexdigest()[:8]
    slug = re.sub(r"[^a-zA-Z0-9]+", "-", urlparse(url).path).strip("-")[:60] or "index"
    return f"{h}-{slug}{ext}"


def _is_pdf(url: str) -> bool:
    return url.lower().split("?")[0].rstrip("/").endswith(".pdf")


def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten: int = MAX_SEITEN, cancelled=None) -> int:
    """Crawlt ab start_url (nur gleiche Domain), rendert JS und legt Seiten/PDFs in `ziel` ab.

    BFS bis `max_tiefe` / `max_seiten`. Fehler einzelner Seiten werden übersprungen.
    Schreibt am ENDE einen `.done`-Marker; ein Abbruch (`cancelled()` → True) lässt ihn weg,
    sodass ein Neustart neu crawlt. Gibt die Zahl gespeicherter Quellen zurück.
    """
    # Lazy: so startet das Backend auch ohne installiertes Playwright; nur das Crawlen schlägt dann fehl.
    from playwright.sync_api import sync_playwright

    ziel.mkdir(parents=True, exist_ok=True)
    domain = urlparse(start_url).netloc
    gesehen: set[str] = set()
    queue: list[tuple[str, int]] = [(urldefrag(start_url)[0], 0)]
    gespeichert = 0

    with sync_playwright() as pw:
        browser = pw.chromium.launch(args=["--no-sandbox"])  # non-root (Docker user app)
        page = browser.new_page(user_agent=_UA)
        try:
            while queue and gespeichert < max_seiten:
                if cancelled and cancelled():
                    return gespeichert  # Abbruch → KEIN .done-Marker → Neustart crawlt neu
                url, tiefe = queue.pop(0)
                if url in gesehen:
                    continue
                gesehen.add(url)

                # PDFs brauchen kein Rendering — direkt laden.
                if _is_pdf(url):
                    data = _fetch_bytes(url)
                    if data:
                        p = ziel / _name(url, ".pdf")
                        if not p.exists():
                            p.write_bytes(data)
                        gespeichert += 1
                    continue

                try:
                    page.goto(url, wait_until="networkidle", timeout=SEITE_TIMEOUT * 1000)
                except Exception as e:
                    log.debug("crawl: goto unvollständig %s: %s", url, e)  # trotzdem versuchen, Inhalt zu lesen
                try:
                    text = page.inner_text("body").strip()
                except Exception:
                    text = ""
                if text:
                    atomic_write_text(ziel / _name(url, ".txt"), f"QUELLE: {url}\n\n{text}")
                    gespeichert += 1
                if tiefe < max_tiefe:
                    try:
                        hrefs = page.eval_on_selector_all("a[href]", "els => els.map(e => e.href)")
                    except Exception:
                        hrefs = []
                    for href in hrefs:
                        nxt = urldefrag(href)[0]
                        if (nxt.startswith(("http://", "https://"))
                                and urlparse(nxt).netloc == domain and nxt not in gesehen):
                            queue.append((nxt, tiefe + 1))
        finally:
            browser.close()

    (ziel / ".done").write_text("ok", encoding="utf-8")  # sauber durchgelaufen
    log.info("crawl %s → %d Quellen in %s", start_url, gespeichert, ziel)
    return gespeichert
