"""Generic article-text fetcher.

Fetches a URL and extracts its main body via trafilatura. The single source
of truth for article-content extraction across the codebase — used by the
``read_article`` LLM tool and the ``lookup`` tool's web-result enrichment.

Trafilatura/lxml is NOT safe to call concurrently — running it via
``run_in_executor`` from multiple coroutines can trip a libxml2 double-free.
Callers must serialize their fetches (await one before starting the next).
"""
from __future__ import annotations

import asyncio
import logging

import httpx

logger = logging.getLogger(__name__)


async def fetch_article_text(url: str) -> str | None:
    """Return the clean article body for *url*, or None on failure.

    Returns None when the HTTP fetch fails or trafilatura yields nothing
    useful. Callers should treat None as "no article content available."
    """
    try:
        async with httpx.AsyncClient(timeout=15.0, follow_redirects=True, headers={
            "User-Agent": "Mozilla/5.0 (compatible; FabledScribe/1.0; +https://fabledsword.com)",
        }) as client:
            resp = await client.get(url)
            resp.raise_for_status()
            raw_html = resp.text
    except Exception:
        logger.debug("Failed to fetch article URL %s", url)
        return None

    loop = asyncio.get_event_loop()
    try:
        import trafilatura
        text = await loop.run_in_executor(
            None,
            lambda: trafilatura.extract(
                raw_html,
                include_comments=False,
                include_tables=True,
                favor_recall=True,
            ),
        )
        return text or None
    except Exception:
        logger.debug("trafilatura extraction failed for %s", url, exc_info=True)
        return None
