"""Minimal, route-guarded Playwright renderer (not a universal downloader)."""

from __future__ import annotations

import logging
from dataclasses import dataclass
from urllib.parse import urlsplit

from app.adapters.base import SiteAdapter
from app.errors import (
    AccessDeniedError,
    BrowserUnavailableError,
    ContentTooLargeError,
    NetworkError,
    NotFoundError,
    PermanentSiteError,
    RateLimitedError,
)
from app.services.http_client import parse_retry_after
from app.services.url_validator import NetworkGuard

logger = logging.getLogger(__name__)


@dataclass(frozen=True, slots=True)
class RenderedPage:
    requested_url: str
    final_url: str
    html: str


class BrowserRenderer:
    """Render JavaScript-heavy pages with strict scheme/domain/IP checks."""

    def __init__(
        self,
        guard: NetworkGuard,
        *,
        headless: bool = True,
        user_agent: str = "TelegramVideoBot/1.0",
        timeout: int = 30,
        max_html_bytes: int = 5 * 1024 * 1024,
    ) -> None:
        self.guard = guard
        self.headless = headless
        self.user_agent = user_agent
        self.timeout = timeout
        self.max_html_bytes = max_html_bytes
        self._playwright = None
        self._browser = None
        self._context = None
        self._started = False

    async def start(self) -> None:
        if self._started:
            return
        try:
            from playwright.async_api import async_playwright
        except ImportError as exc:  # pragma: no cover - dependency is pinned
            raise BrowserUnavailableError("Playwright Python package is not installed") from exc
        try:
            self._playwright = await async_playwright().start()
            self._browser = await self._playwright.chromium.launch(
                headless=self.headless,
                args=[
                    "--disable-background-networking",
                    "--disable-component-update",
                    "--disable-default-apps",
                    "--disable-extensions",
                    "--disable-sync",
                    "--metrics-recording-only",
                    "--no-first-run",
                ],
            )
            self._context = await self._browser.new_context(
                accept_downloads=False,
                java_script_enabled=True,
                ignore_https_errors=False,
                permissions=[],
                service_workers="block",
                user_agent=self.user_agent,
                viewport={"width": 1366, "height": 900},
            )
            await self._context.route("**/*", self._route_request)
            self._started = True
        except Exception as exc:  # noqa: BLE001 - normalize Playwright startup errors
            await self.close()
            raise BrowserUnavailableError(
                "Chromium could not be started; install it with 'playwright install chromium'"
            ) from exc

    async def close(self) -> None:
        for resource in (self._context, self._browser):
            if resource is not None:
                try:
                    await resource.close()
                except Exception:
                    pass
        self._context = None
        self._browser = None
        if self._playwright is not None:
            try:
                await self._playwright.stop()
            except Exception:
                pass
        self._playwright = None
        self._started = False

    async def _route_request(self, route, request) -> None:  # type: ignore[no-untyped-def]
        url = request.url
        scheme = urlsplit(url).scheme.lower()
        if scheme in {"data", "blob", "about"}:
            await route.continue_()
            return
        if scheme not in {"http", "https"}:
            await route.abort("blockedbyclient")
            return
        # Do not fetch large non-document resources in the browser. Media is
        # downloaded by the dedicated downloader after URL validation.
        if request.resource_type in {"media", "image", "font"}:
            await route.abort("blockedbyclient")
            return
        try:
            await self.guard.validate(url, resolve_dns=True)
        except Exception:
            await route.abort("blockedbyclient")
            return
        await route.continue_()

    async def render(self, url: str, adapter: SiteAdapter) -> RenderedPage:
        await self.start()
        assert self._context is not None
        validated = await self.guard.validate(url, resolve_dns=True)
        page = await self._context.new_page()
        page.set_default_timeout(self.timeout * 1000)
        page.set_default_navigation_timeout(self.timeout * 1000)
        try:
            try:
                response = await page.goto(
                    validated.url, wait_until="domcontentloaded", timeout=self.timeout * 1000
                )
            except Exception as exc:  # noqa: BLE001 - Playwright timeout/navigation
                raise NetworkError(f"browser navigation failed ({type(exc).__name__})") from exc
            if response is not None:
                status = response.status
                if status == 429:
                    headers = await response.all_headers()
                    raise RateLimitedError(parse_retry_after(headers.get("retry-after")))
                if status in {401, 402, 403, 407, 451}:
                    raise AccessDeniedError(status)
                if status in {404, 410}:
                    raise NotFoundError()
                if status >= 500:
                    raise NetworkError(f"upstream HTTP {status}")
                if not 200 <= status < 300:
                    raise PermanentSiteError(f"unexpected HTTP status {status}")

            if adapter.wait_for_selector:
                try:
                    await page.wait_for_selector(
                        adapter.wait_for_selector, timeout=min(self.timeout, 10) * 1000
                    )
                except Exception:
                    logger.debug("adapter wait selector was not found")

            if adapter.load_more_selector and adapter.max_load_more_clicks > 0:
                for _ in range(min(adapter.max_load_more_clicks, 20)):
                    locator = page.locator(adapter.load_more_selector).first
                    try:
                        if await locator.count() == 0 or not await locator.is_visible():
                            break
                        await locator.click(timeout=5000)
                        await page.wait_for_timeout(750)
                    except Exception:
                        break

            scrolls = getattr(adapter, "scroll_loads", 0)
            for _ in range(min(max(0, int(scrolls)), 10)):
                await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
                await page.wait_for_timeout(700)

            final_url = page.url
            # A JS redirect must not escape the allow-list or public address space.
            await self.guard.validate(final_url, resolve_dns=True)
            html = await page.content()
            if len(html.encode("utf-8", errors="ignore")) > self.max_html_bytes:
                raise ContentTooLargeError("rendered HTML exceeds configured limit")
            return RenderedPage(requested_url=url, final_url=final_url, html=html)
        finally:
            try:
                await page.close()
            except Exception:
                pass


# Descriptive alias for callers that prefer the architecture's module naming.
BrowserAdapter = BrowserRenderer
