"""Phase L: 슬라이드 렌더링 측정 에이전트.

Selenium headless Chrome으로 HTML을 실제 렌더링하고
각 zone/block의 px 높이를 정확히 측정한다.

LLM 추정이 아닌 브라우저 엔진 측정. 결정론적.
"""
from __future__ import annotations

import logging
from typing import Any

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service

from src.config import settings

logger = logging.getLogger(__name__)

# JavaScript: 각 zone과 블록의 실제 높이를 측정
_MEASURE_SCRIPT = """
    var slide = document.querySelector('.slide');
    if (!slide) return {error: 'slide not found'};

    var result = {
        slide: {
            scrollHeight: slide.scrollHeight,
            clientHeight: slide.clientHeight,
            overflowed: slide.scrollHeight > slide.clientHeight,
            excess_px: Math.max(0, slide.scrollHeight - slide.clientHeight)
        },
        zones: {},
        containers: {}
    };

    // Zone 측정 (area-* 클래스)
    var areaDivs = slide.querySelectorAll('[class*="area-"]');
    for (var i = 0; i < areaDivs.length; i++) {
        var zone = areaDivs[i];
        var areaMatch = zone.className.match(/area-(\\w+)/);
        if (!areaMatch) continue;
        var areaName = areaMatch[1];

        var blocks = [];
        var blockDivs = zone.querySelectorAll('[class*="block-"]');
        for (var j = 0; j < blockDivs.length; j++) {
            var block = blockDivs[j];
            var blockMatch = block.className.match(/block-([\\w-]+)/);
            var blockName = blockMatch ? blockMatch[1] : block.className;
            blocks.push({
                block_type: blockName,
                scrollHeight: Math.round(block.scrollHeight),
                clientHeight: Math.round(block.clientHeight),
                offsetHeight: Math.round(block.offsetHeight),
                overflowed: block.scrollHeight > block.clientHeight + 2,
                excess_px: Math.max(0, Math.round(block.scrollHeight - block.clientHeight))
            });
        }

        result.zones[areaName] = {
            scrollHeight: Math.round(zone.scrollHeight),
            clientHeight: Math.round(zone.clientHeight),
            overflowed: zone.scrollHeight > zone.clientHeight + 2,
            excess_px: Math.max(0, Math.round(zone.scrollHeight - zone.clientHeight)),
            block_count: blocks.length,
            blocks: blocks
        };
    }

    // Phase O: 컨테이너 측정 (container-* 클래스)
    var containerDivs = slide.querySelectorAll('[class*="container-"]');
    for (var k = 0; k < containerDivs.length; k++) {
        var container = containerDivs[k];
        var containerMatch = container.className.match(/container-(.+)/);
        if (!containerMatch) continue;
        var containerName = containerMatch[1];

        var cBlocks = [];
        var cBlockDivs = container.querySelectorAll('[class*="block-"]');
        for (var m = 0; m < cBlockDivs.length; m++) {
            var cBlock = cBlockDivs[m];
            var cBlockMatch = cBlock.className.match(/block-([\\w-]+)/);
            var cBlockName = cBlockMatch ? cBlockMatch[1] : cBlock.className;
            cBlocks.push({
                block_type: cBlockName,
                scrollHeight: Math.round(cBlock.scrollHeight),
                clientHeight: Math.round(cBlock.clientHeight),
                overflowed: cBlock.scrollHeight > cBlock.clientHeight + 2,
                excess_px: Math.max(0, Math.round(cBlock.scrollHeight - cBlock.clientHeight))
            });
        }

        result.containers[containerName] = {
            scrollHeight: Math.round(container.scrollHeight),
            clientHeight: Math.round(container.clientHeight),
            allocatedHeight: parseInt(container.style.height) || 0,
            overflowed: container.scrollHeight > container.clientHeight + 2,
            excess_px: Math.max(0, Math.round(container.scrollHeight - container.clientHeight)),
            block_count: cBlocks.length,
            blocks: cBlocks
        };
    }

    return result;
"""


def measure_rendered_heights(html: str) -> dict[str, Any]:
    """렌더링된 HTML의 각 zone/block 실제 px 높이를 측정한다.

    Selenium headless Chrome 사용. 결정론적.
    viewport 크기는 config에서 읽음 (하드코딩 아님).

    Args:
        html: 렌더링할 완성 HTML 문자열

    Returns:
        {
            "slide": {"scrollHeight": 750, "clientHeight": 720, "overflowed": true, ...},
            "zones": {
                "body": {"scrollHeight": 520, "clientHeight": 490, "overflowed": true, "blocks": [...]},
                "sidebar": {"scrollHeight": 400, "clientHeight": 490, "overflowed": false, ...},
                ...
            }
        }
    """
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--disable-gpu")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument(
        f"--window-size={settings.slide_width},{settings.slide_height + 200}"
    )

    driver = None
    try:
        driver = webdriver.Chrome(options=options)

        # HTML을 data URI로 로드
        import urllib.parse
        encoded = urllib.parse.quote(html)
        driver.get(f"data:text/html;charset=utf-8,{encoded}")

        # 폰트 로딩 대기 (Pretendard CDN)
        try:
            driver.execute_script("return document.fonts.ready")
        except Exception:
            pass  # 폰트 API 미지원 시 무시

        # 측정 실행
        result = driver.execute_script(_MEASURE_SCRIPT)

        if result and "error" not in result:
            _log_measurement(result)
            return result

        logger.warning(f"[측정] 실패: {result}")
        return {"slide": {}, "zones": {}}

    except Exception as e:
        logger.warning(f"[측정] Selenium 오류: {e}")
        return {"slide": {}, "zones": {}}

    finally:
        if driver:
            try:
                driver.quit()
            except Exception:
                pass


def format_measurement_for_kei(
    measurement: dict[str, Any],
    allocation: dict[int, int] | None = None,
) -> str:
    """측정 결과를 Kei 검수에 전달할 텍스트로 포맷한다.

    Args:
        measurement: measure_rendered_heights() 결과
        allocation: allocate_height_budget() 결과 (있으면 할당 대비 비교)

    Returns:
        Kei에게 전달할 측정 결과 텍스트
    """
    lines = ["## 실제 렌더링 측정 결과 (Selenium)"]

    slide = measurement.get("slide", {})
    if slide:
        status = "OK" if not slide.get("overflowed") else f"+{slide.get('excess_px', 0)}px 초과"
        lines.append(
            f"- 슬라이드 전체: {slide.get('scrollHeight', '?')}px / "
            f"{slide.get('clientHeight', '?')}px — {status}"
        )

    for zone_name, zone_data in measurement.get("zones", {}).items():
        status = "OK" if not zone_data.get("overflowed") else f"+{zone_data.get('excess_px', 0)}px 초과"
        lines.append(
            f"- {zone_name} zone: 실제 {zone_data.get('scrollHeight', '?')}px / "
            f"가용 {zone_data.get('clientHeight', '?')}px — {status}"
        )

        for block in zone_data.get("blocks", []):
            block_status = "OK" if not block.get("overflowed") else f"+{block.get('excess_px', 0)}px 잘림"
            height = block.get("scrollHeight", "?")

            # zone 내 비중 계산
            zone_height = zone_data.get("clientHeight", 1)
            ratio_pct = round(height / zone_height * 100) if isinstance(height, (int, float)) and zone_height > 0 else "?"

            lines.append(
                f"  - {block.get('block_type', '?')}: "
                f"{height}px ({ratio_pct}%) — {block_status}"
            )

    return "\n".join(lines)


def measure_candidate_block(html: str) -> dict[str, Any]:
    """Phase P: 단일 후보 블록을 렌더링하여 높이 측정 + 스크린샷 캡처.

    Args:
        html: render_block_in_container()로 생성된 완전한 HTML

    Returns:
        {
            "scrollHeight": 실제 콘텐츠 높이,
            "containerHeight": 컨테이너 높이,
            "overflowed": 넘침 여부,
            "excess_px": 초과 px,
            "screenshot_b64": base64 PNG 문자열
        }
    """
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--disable-gpu")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--force-device-scale-factor=1")
    options.add_argument("--window-size=1400,900")

    driver = None
    try:
        driver = webdriver.Chrome(options=options)

        import urllib.parse
        encoded = urllib.parse.quote(html)
        driver.get(f"data:text/html;charset=utf-8,{encoded}")

        try:
            driver.execute_script("return document.fonts.ready")
        except Exception:
            pass

        result = driver.execute_script("""
            var container = document.querySelector('.candidate-container');
            if (!container) return {error: 'container not found'};
            return {
                scrollHeight: container.scrollHeight,
                containerHeight: parseInt(container.style.height) || container.clientHeight,
                overflowed: container.scrollHeight > container.clientHeight + 2,
                excess_px: Math.max(0, container.scrollHeight - container.clientHeight)
            };
        """)

        if not result or "error" in result:
            return {"scrollHeight": 0, "containerHeight": 0, "overflowed": False, "excess_px": 0, "screenshot_b64": None}

        # 스크린샷 캡처
        from selenium.webdriver.common.by import By
        container = driver.find_element(By.CSS_SELECTOR, ".candidate-container")
        screenshot_b64 = container.screenshot_as_base64

        result["screenshot_b64"] = screenshot_b64
        return result

    except Exception as e:
        logger.warning(f"[Phase P] 후보 블록 측정 실패: {e}")
        return {"scrollHeight": 0, "containerHeight": 0, "overflowed": False, "excess_px": 0, "screenshot_b64": None}

    finally:
        if driver:
            try:
                driver.quit()
            except Exception:
                pass


def capture_slide_screenshot(html: str) -> str | None:
    """Phase N-4: 렌더링된 슬라이드의 스크린샷을 base64 PNG로 캡처한다.

    Selenium 4.x WebElement.screenshot_as_base64 사용.
    반환: 순수 base64 문자열 (data URI prefix 없음). 실패 시 None.
    """
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--disable-gpu")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--force-device-scale-factor=1")
    options.add_argument(
        f"--window-size={settings.slide_width},{settings.slide_height + 200}"
    )

    driver = None
    try:
        driver = webdriver.Chrome(options=options)

        import urllib.parse
        encoded = urllib.parse.quote(html)
        driver.get(f"data:text/html;charset=utf-8,{encoded}")

        # 폰트 로딩 대기
        try:
            driver.execute_script("return document.fonts.ready")
        except Exception:
            pass

        from selenium.webdriver.common.by import By
        slide = driver.find_element(By.CSS_SELECTOR, ".slide")
        screenshot_b64 = slide.screenshot_as_base64

        logger.info(f"[스크린샷] 캡처 완료: {len(screenshot_b64)}자 base64")
        return screenshot_b64

    except Exception as e:
        logger.warning(f"[스크린샷] Selenium 캡처 실패: {e}")
        return None

    finally:
        if driver:
            try:
                driver.quit()
            except Exception:
                pass


def _log_measurement(result: dict[str, Any]) -> None:
    """측정 결과를 로그에 출력한다."""
    slide = result.get("slide", {})
    overflow_status = "OK" if not slide.get("overflowed") else f"초과 +{slide.get('excess_px')}px"
    logger.info(f"[측정] 슬라이드: {slide.get('scrollHeight')}px / {slide.get('clientHeight')}px — {overflow_status}")

    for zone_name, zone_data in result.get("zones", {}).items():
        zone_status = "OK" if not zone_data.get("overflowed") else f"초과 +{zone_data.get('excess_px')}px"
        logger.info(
            f"[측정] {zone_name}: {zone_data.get('scrollHeight')}px / "
            f"{zone_data.get('clientHeight')}px — {zone_status} "
            f"({zone_data.get('block_count', 0)}개 블록)"
        )
