"""BrowserFetchTool — Two-stage URL fetcher with auto JS-gate detection.

Stage 1: httpx (fast, no overhead, works for >90% of URLs)
Stage 2: Playwright via BrowserController (auto-triggered for JS-gated pages)

Replaces WebFetchTool in the pipeline registry.

Detection heuristic for JS-gated pages:
  • Body text < 200 chars after HTML strip
  • React/Next/Vue/Angular root ``<div id="root|app|__nuxt">`` with no text children
  • HTML contains ``<script`` references to JS bundles but empty visible content
  • ``<noscript>`` tag with "enable JavaScript" text
"""

from __future__ import annotations

import logging
import re
import time
from typing import Any

from navig.tools.registry import BaseTool, StatusCallback, ToolResult

logger = logging.getLogger(__name__)

_MAX_CHARS = 8_000
_HTTPX_TIMEOUT = 15.0
_BROWSER_TIMEOUT_MS = 20_000

_USER_AGENT = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/124.0.0.0 Safari/537.36"
)


# ---------------------------------------------------------------------------
# JS-gate heuristics
# ---------------------------------------------------------------------------

_JS_GATE_PATTERNS = [
    # noscript "enable JavaScript"
    re.compile(r"<noscript[^>]*>.*?(enable|javascript|required)", re.DOTALL | re.IGNORECASE),
    # empty SPA root divs  (id = root | app | __nuxt | __next | main-content)
    re.compile(
        r'<div\s+id=["\'](?:root|app|__nuxt|__next|main[-_]content)["\'][^>]*>\s*</div>',
        re.IGNORECASE,
    ),
    # typical CRA/Vite JS bundle reference
    re.compile(r'<script[^>]+src=["\'][^"\']+(?:main|bundle|app)\.[a-z0-9]{8,}\.js["\']'),
    # Next.js data script tag
    re.compile(r'id=["\']__NEXT_DATA__["\']'),
    # Angular / Ember attributes
    re.compile(r"<app-root\b|<ember-app\b", re.IGNORECASE),
]


def _is_js_gated(html: str) -> bool:
    """Return True if the page likely needs JavaScript to render meaningful content."""
    # Quick text-content check
    bare = re.sub(r"<[^>]+>", "", html)
    bare = re.sub(r"\s+", " ", bare).strip()
    if len(bare) < 250:
        return True

    for pattern in _JS_GATE_PATTERNS:
        if pattern.search(html):
            return True

    return False


# ---------------------------------------------------------------------------
# Text extraction
# ---------------------------------------------------------------------------


def _extract_text(html: str) -> str:
    """Best-effort HTML → plain text. Prefers trafilatura when available."""
    try:
        import trafilatura

        text = trafilatura.extract(html, include_comments=False, include_tables=True)
        if text:
            return text.strip()
    except Exception:  # noqa: BLE001
        pass  # best-effort; failure is non-critical

    # Use html.parser for tag stripping (text extraction only — not a security sanitizer)
    import html as _html
    from html.parser import HTMLParser as _HTMLParser

    _SKIP_TAGS = frozenset({"script", "style", "noscript"})

    class _TextExtractor(_HTMLParser):
        def __init__(self) -> None:
            super().__init__(convert_charrefs=True)
            self._parts: list[str] = []
            self._skip = 0

        def handle_starttag(self, tag: str, attrs: list) -> None:
            if tag.lower() in _SKIP_TAGS:
                self._skip += 1

        def handle_endtag(self, tag: str) -> None:
            if tag.lower() in _SKIP_TAGS:
                self._skip = max(0, self._skip - 1)

        def handle_data(self, data: str) -> None:
            if not self._skip:
                self._parts.append(data)

    _extractor = _TextExtractor()
    try:
        _extractor.feed(html)
        text = " ".join(_extractor._parts)
    except Exception:  # noqa: BLE001
        text = _html.unescape(re.sub(r"<[^>]+>", " ", html))
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()


# ---------------------------------------------------------------------------
# Stage 2: Playwright via BrowserController
# ---------------------------------------------------------------------------


async def _browser_fetch(
    url: str, on_status: StatusCallback | None
) -> tuple[str, str, bytes | None]:
    """Use navig BrowserController to render the page.  Returns (html, method_used, screenshot)."""
    if on_status:
        await on_status("Launching browser…", "Playwright headless", 55)

    try:
        from navig.browser.controller import BrowserConfig, BrowserController  # lazy
    except ImportError as _exc:
        raise RuntimeError("navig.browser not available (playwright not installed?)") from _exc

    config = BrowserConfig(
        headless=True,
        timeout_ms=_BROWSER_TIMEOUT_MS,
    )
    browser = BrowserController(config=config)
    try:
        await browser.start()
        await browser._page.goto(url, wait_until="networkidle")
        html = await browser._page.content()
        screenshot_bytes = None
        try:
            screenshot_bytes = await browser._page.screenshot(
                type="jpeg", quality=60, full_page=False
            )
        except Exception as _e:
            logger.debug("Failed to take screenshot: %s", _e)
        return html, "playwright", screenshot_bytes
    except Exception as exc:
        raise RuntimeError(f"Playwright failed: {exc}") from exc
    finally:
        await browser.stop()


# ---------------------------------------------------------------------------
# Main tool
# ---------------------------------------------------------------------------


class BrowserFetchTool(BaseTool):
    """Fetch a URL — httpx first, Playwright upgrade if JS-gated."""

    name = "browser_fetch"
    description = (
        "Fetch a URL and return the main text content. "
        "Auto-upgrades to headless browser for JavaScript-rendered pages."
    )

    async def run(
        self,
        args: dict[str, Any],
        on_status: StatusCallback | None = None,
    ) -> ToolResult:
        url: str = args.get("url", "").strip()
        if not url:
            return ToolResult(name=self.name, success=False, error="url arg required")

        if not url.startswith(("http://", "https://")):
            url = "https://" + url

        await self._emit(on_status, "Connecting…", url[:70], 15)

        try:
            import httpx
        except ImportError:
            return ToolResult(name=self.name, success=False, error="httpx not installed")

        method_used = "httpx"
        html = ""
        elapsed_ms = 0.0

        # Stage 1 — httpx
        try:
            async with httpx.AsyncClient(
                follow_redirects=True,
                timeout=httpx.Timeout(_HTTPX_TIMEOUT),
                headers={"User-Agent": _USER_AGENT},
            ) as client:
                t0 = time.monotonic()
                resp = await client.get(url)
                elapsed_ms = (time.monotonic() - t0) * 1000
                status_code = resp.status_code
                html = resp.text
                final_url = str(resp.url)

        except httpx.TimeoutException:
            return ToolResult(name=self.name, success=False, error="request timed out (15s)")
        except httpx.ConnectError as exc:
            return ToolResult(name=self.name, success=False, error=f"connection failed: {exc}")
        except Exception as exc:  # noqa: BLE001
            return ToolResult(name=self.name, success=False, error=str(exc))

        await self._emit(
            on_status,
            "Analysing page…",
            f"HTTP {status_code} · {len(html):,} bytes",
            40,
        )

        screenshot_bytes: bytes | None = None

        # Stage 2 — upgrade to Playwright if JS-gated
        if _is_js_gated(html):
            await self._emit(on_status, "JS-gated — launching browser…", url[:70], 50)
            try:
                t0 = time.monotonic()
                html, method_used, screenshot_bytes = await _browser_fetch(url, on_status)
                elapsed_ms += (time.monotonic() - t0) * 1000
                final_url = url
                logger.debug("browser_fetch: upgraded to Playwright for %s", url)
            except RuntimeError as exc:
                logger.warning("browser_fetch: Playwright fallback failed for %s: %s", url, exc)
                # Continue with whatever httpx returned — partial content is better than nothing

        await self._emit(on_status, "Extracting text…", f"{len(html):,} bytes · {method_used}", 80)

        text = _extract_text(html)[:_MAX_CHARS]

        output = {
            "url": final_url,
            "content": text,
            "method": method_used,
            "elapsed_ms": round(elapsed_ms, 1),
            "chars": len(text),
        }

        if screenshot_bytes:
            output["_screenshot"] = screenshot_bytes

        return ToolResult(
            name=self.name,
            success=True,
            output=output,
        )
