"""브라우저 선택 라우터 — purpose에 따라 Lightpanda/Chrome 자동 선택

Usage:
    from tools.browser_router import get_browser, BrowserChoice, BrowserEngine

    # 브라우저 선택 (메타데이터만)
    choice = await get_browser("crawl")
    # choice.engine == BrowserEngine.LIGHTPANDA
    # choice.endpoint == "ws://127.0.0.1:9333"

    # 실제 Playwright BrowserContext 생성
    async with create_browser_context("crawl") as (browser, context):
        page = await context.new_page()
        ...
"""

from __future__ import annotations

import logging
from contextlib import asynccontextmanager
from dataclasses import dataclass
from enum import Enum
from typing import AsyncGenerator, Tuple

logger = logging.getLogger("browser_router")

# ---------------------------------------------------------------------------
# 상수
# ---------------------------------------------------------------------------

LIGHTPANDA_ENDPOINT: str = "ws://127.0.0.1:9333"
CHROME_ENDPOINT: str = "ws://127.0.0.1:9222"

# ---------------------------------------------------------------------------
# 열거형 및 데이터클래스
# ---------------------------------------------------------------------------


class BrowserEngine(str, Enum):
    """지원하는 브라우저 엔진."""

    LIGHTPANDA = "lightpanda"
    CHROME = "chrome"


@dataclass
class BrowserChoice:
    """브라우저 선택 결과 — 엔진, 접속 엔드포인트, 선택 사유를 담는다."""

    engine: BrowserEngine
    endpoint: str
    reason: str


# ---------------------------------------------------------------------------
# Purpose → Engine 매핑
# ---------------------------------------------------------------------------

PURPOSE_MAP: dict[str, BrowserEngine] = {
    "crawl": BrowserEngine.LIGHTPANDA,  # 대량 텍스트 크롤링
    "bulk": BrowserEngine.LIGHTPANDA,  # 대량 병렬
    "text": BrowserEngine.LIGHTPANDA,  # 텍스트 추출
    "login": BrowserEngine.CHROME,  # 쿠키/세션
    "screenshot": BrowserEngine.CHROME,  # 렌더링
    "pdf": BrowserEngine.CHROME,  # PDF 생성
    "spa": BrowserEngine.CHROME,  # SPA/React
    "stealth": BrowserEngine.CHROME,  # 봇 감지 우회
}

# ---------------------------------------------------------------------------
# 헬스 체크
# ---------------------------------------------------------------------------


async def check_lightpanda_health(
    endpoint: str = LIGHTPANDA_ENDPOINT,
    timeout: float = 2.0,
) -> bool:
    """Lightpanda 서비스 가용 여부를 확인한다.

    ``endpoint`` (ws://host:port 형식)에서 호스트와 포트를 파싱하여
    ``http://host:port/json/version`` 으로 GET 요청을 보낸다.
    200 응답이 오면 True, 그 외 모든 경우(타임아웃·연결 실패·비200)엔 False를 반환한다.
    예외는 절대 호출자에게 전파되지 않는다.

    Args:
        endpoint: Lightpanda WebSocket 엔드포인트 (ws://host:port 형식).
        timeout: HTTP 요청 타임아웃(초).

    Returns:
        서비스가 정상이면 True, 아니면 False.
    """
    from urllib.parse import urlparse  # noqa: PLC0415

    try:
        parsed = urlparse(endpoint)
        host = parsed.hostname or "127.0.0.1"
        port = parsed.port or 9333
    except Exception:  # noqa: BLE001
        host, port = "127.0.0.1", 9333

    health_url = f"http://{host}:{port}/json/version"

    # aiohttp 우선 사용, 없으면 urllib.request 로 fallback
    try:
        import aiohttp  # noqa: PLC0415

        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(health_url, timeout=aiohttp.ClientTimeout(total=timeout)) as resp:
                    healthy = resp.status == 200
                    logger.debug("Lightpanda health check → status=%d healthy=%s", resp.status, healthy)
                    return healthy
        except Exception as exc:  # noqa: BLE001
            logger.debug("Lightpanda health check failed (aiohttp): %s", exc)
            return False

    except ImportError:
        # aiohttp 미설치 → urllib.request 동기 fallback
        import urllib.request  # noqa: PLC0415

        try:
            req = urllib.request.Request(health_url)
            with urllib.request.urlopen(req, timeout=timeout) as resp:  # type: ignore[arg-type]
                healthy = resp.status == 200
                logger.debug("Lightpanda health check → status=%d healthy=%s", resp.status, healthy)
                return healthy
        except Exception as exc:  # noqa: BLE001
            logger.debug("Lightpanda health check failed (urllib): %s", exc)
            return False


# ---------------------------------------------------------------------------
# 핵심 라우팅 함수
# ---------------------------------------------------------------------------


async def get_browser(
    purpose: str,
    *,
    lightpanda_endpoint: str = LIGHTPANDA_ENDPOINT,
    chrome_endpoint: str = CHROME_ENDPOINT,
) -> BrowserChoice:
    """purpose에 맞는 브라우저를 선택하여 BrowserChoice를 반환한다.

    PURPOSE_MAP에 정의된 purpose이면 해당 엔진을 우선 선택한다.
    알 수 없는 purpose는 Chrome으로 fallback한다(안전한 기본 선택).
    Lightpanda가 preferred이나 서비스가 불가하면 Chrome으로 자동 fallback한다.

    Args:
        purpose: 사용 목적 식별자 (예: "crawl", "screenshot", "pdf" 등).
        lightpanda_endpoint: Lightpanda CDP WebSocket 엔드포인트 (기본: ws://127.0.0.1:9333).
        chrome_endpoint: Chrome CDP WebSocket 엔드포인트 (기본: ws://127.0.0.1:9222).

    Returns:
        선택된 엔진 정보를 담은 BrowserChoice 인스턴스.
    """
    preferred = PURPOSE_MAP.get(purpose, BrowserEngine.CHROME)
    logger.debug("purpose='%s' → preferred engine=%s", purpose, preferred.value)

    if preferred is BrowserEngine.LIGHTPANDA:
        healthy = await check_lightpanda_health(lightpanda_endpoint)
        if healthy:
            return BrowserChoice(
                engine=BrowserEngine.LIGHTPANDA,
                endpoint=lightpanda_endpoint,
                reason=f"purpose '{purpose}' → Lightpanda (텍스트 전용, 고성능)",
            )
        else:
            logger.warning(
                "purpose='%s': Lightpanda 불가 → Chrome fallback (endpoint=%s)",
                purpose,
                chrome_endpoint,
            )
            return BrowserChoice(
                engine=BrowserEngine.CHROME,
                endpoint=chrome_endpoint,
                reason=f"purpose '{purpose}' → Chrome fallback (Lightpanda 서비스 불가)",
            )

    # preferred == CHROME (또는 unknown purpose)
    return BrowserChoice(
        engine=BrowserEngine.CHROME,
        endpoint=chrome_endpoint,
        reason=f"purpose '{purpose}' → Chrome (렌더링/상호작용 필요)",
    )


# ---------------------------------------------------------------------------
# Playwright Context Manager
# ---------------------------------------------------------------------------


@asynccontextmanager
async def create_browser_context(
    purpose: str,
    *,
    lightpanda_endpoint: str = LIGHTPANDA_ENDPOINT,
    chrome_endpoint: str = CHROME_ENDPOINT,
) -> AsyncGenerator[Tuple, None]:
    """purpose에 맞는 브라우저를 선택하고 Playwright BrowserContext를 생성한다.

    선택된 엔진에 관계없이 ``chromium.connect_over_cdp()`` 를 사용하여 CDP로 접속한다.
    컨텍스트 매니저 종료 시 browser.close()가 자동 호출된다.

    Args:
        purpose: 사용 목적 식별자 (get_browser()와 동일).
        lightpanda_endpoint: Lightpanda CDP WebSocket 엔드포인트.
        chrome_endpoint: Chrome CDP WebSocket 엔드포인트.

    Yields:
        (browser, context) 튜플.

    Example:
        async with create_browser_context("crawl") as (browser, context):
            page = await context.new_page()
            await page.goto("https://example.com")
    """
    from playwright.async_api import async_playwright  # noqa: PLC0415

    choice = await get_browser(purpose, lightpanda_endpoint=lightpanda_endpoint, chrome_endpoint=chrome_endpoint)
    logger.info(
        "create_browser_context: engine=%s endpoint=%s reason=%s",
        choice.engine.value,
        choice.endpoint,
        choice.reason,
    )

    async with async_playwright() as pw:
        browser = await pw.chromium.connect_over_cdp(choice.endpoint)
        context = browser.contexts[0] if browser.contexts else await browser.new_context()
        try:
            yield browser, context
        finally:
            await browser.close()
            logger.debug("browser.close() 완료 (engine=%s)", choice.engine.value)
