from __future__ import annotations

import os
import random
from pathlib import Path
from urllib.parse import urldefrag, urljoin, urlparse, urlsplit

from playwright.sync_api import Browser, BrowserContext, Error, Page, Playwright, TimeoutError, sync_playwright

from .schema import ActionDecision


class BrowserRuntime:
    """封装 Playwright 浏览器生命周期与原子动作执行。"""

    def __init__(
        self,
        headless: bool = False,
        viewport_width: int = 1440,
        viewport_height: int = 900,
        navigation_timeout_ms: int = 90000,
    ):
        """初始化浏览器运行参数。"""
        self.headless = headless
        self.viewport_width = viewport_width
        self.viewport_height = viewport_height
        self.navigation_timeout_ms = navigation_timeout_ms
        self._playwright: Playwright | None = None
        self._browser: Browser | None = None
        self._context: BrowserContext | None = None
        self._page: Page | None = None
        self._home_page: Page | None = None

    def __enter__(self) -> "BrowserRuntime":
        """进入上下文并启动浏览器。"""
        self._playwright = sync_playwright().start()
        launch_kwargs: dict[str, object] = {
            "headless": self.headless,
            "args": ["--disable-blink-features=AutomationControlled"],
        }
        proxy = self._detect_proxy_settings()
        if proxy is not None:
            launch_kwargs["proxy"] = proxy

        self._browser = self._playwright.chromium.launch(**launch_kwargs)
        self._context = self._browser.new_context(
            viewport={"width": self.viewport_width, "height": self.viewport_height},
            user_agent=(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/123.0.0.0 Safari/537.36"
            ),
        )
        self._page = self._context.new_page()
        self._home_page = self._page
        self._page.set_default_navigation_timeout(self.navigation_timeout_ms)
        return self

    def _detect_proxy_settings(self) -> dict[str, str] | None:
        """检测本地代理环境变量并转换为 Playwright 代理配置。"""
        proxy_url = (
            os.getenv("HTTPS_PROXY")
            or os.getenv("https_proxy")
            or os.getenv("HTTP_PROXY")
            or os.getenv("http_proxy")
            or os.getenv("ALL_PROXY")
            or os.getenv("all_proxy")
        )
        if not proxy_url:
            return None

        parsed = urlparse(proxy_url)
        if not parsed.scheme:
            proxy_url = f"http://{proxy_url}"
            parsed = urlparse(proxy_url)

        if not parsed.hostname:
            return None

        server = f"{parsed.scheme}://{parsed.hostname}"
        if parsed.port:
            server = f"{server}:{parsed.port}"

        proxy: dict[str, str] = {"server": server}
        if parsed.username:
            proxy["username"] = parsed.username
        if parsed.password:
            proxy["password"] = parsed.password

        bypass = os.getenv("NO_PROXY") or os.getenv("no_proxy")
        if bypass:
            proxy["bypass"] = bypass
        return proxy

    def __exit__(self, exc_type, exc, tb) -> None:
        """退出上下文并释放浏览器资源。"""
        if self._browser is not None:
            self._browser.close()
        if self._playwright is not None:
            self._playwright.stop()

    @property
    def page(self) -> Page:
        """返回当前活动页面对象。"""
        if self._page is None:
            raise RuntimeError("浏览器尚未启动")
        return self._page

    def _ensure_page(self) -> Page:
        """确保存在可用页面，若已关闭则重新创建。"""
        page = self.page
        if page.is_closed():
            if self._context is None:
                raise RuntimeError("浏览器上下文不可用，无法恢复页面")
            self._page = self._context.new_page()
            self._page.set_default_navigation_timeout(self.navigation_timeout_ms)
            page = self._page
        return page

    def goto(self, url: str) -> None:
        """导航到指定页面地址，遇到拦截页时尽量继续执行。"""
        page = self._ensure_page()
        try:
            page.goto(url, wait_until="domcontentloaded", timeout=self.navigation_timeout_ms)
            return
        except TimeoutError:
            pass
        except Error:
            page = self._ensure_page()

        try:
            page.goto(url, wait_until="commit", timeout=30000)
            page.wait_for_timeout(6000)
            return
        except TimeoutError:
            self._ensure_page().wait_for_timeout(3000)
            return
        except Error:
            self._ensure_page().wait_for_timeout(3000)
            return

    def screenshot(
        self,
        path: str,
        full_page: bool = True,
        image_type: str = "png",
        quality: int | None = None,
    ) -> str:
        """对当前页面截图并返回截图路径。"""
        self.wait_until_page_ready()
        output = Path(path)
        output.parent.mkdir(parents=True, exist_ok=True)
        options: dict[str, object] = {
            "path": str(output),
            "full_page": full_page,
            "type": image_type,
        }
        if image_type == "jpeg":
            options["quality"] = int(quality or 55)
        self.page.screenshot(**options)
        return str(output)

    def close_current_tab(self) -> str:
        """关闭当前标签页并切换到其他可用标签页。"""
        if self._context is None:
            return "浏览器上下文不可用，无法关闭标签页"

        current = self._ensure_page()
        if self._home_page is not None and current == self._home_page:
            return "当前为起始标签页，保留不关闭"

        open_pages = [page for page in self._context.pages if not page.is_closed()]
        if len(open_pages) <= 1:
            return "仅剩一个标签页，保留不关闭"

        current.close()
        remaining = [page for page in self._context.pages if not page.is_closed()]
        if not remaining:
            self._page = self._context.new_page()
            self._page.set_default_navigation_timeout(self.navigation_timeout_ms)
            if self._home_page is None or self._home_page.is_closed():
                self._home_page = self._page
            return "已关闭当前标签页并新建空白页"

        if self._home_page is not None and not self._home_page.is_closed():
            self._page = self._home_page
        else:
            self._page = remaining[-1]
        return f"已关闭当前标签页，切换到 {self._page.url}"

    def open_url_in_new_tab(self, url: str) -> str:
        """在新标签页打开 URL，并切换到新标签页。"""
        if self._context is None:
            raise RuntimeError("浏览器上下文不可用")

        page = self._context.new_page()
        page.set_default_navigation_timeout(self.navigation_timeout_ms)
        self._page = page
        self.goto(url)
        return self.page.url

    def collect_same_domain_links(self, base_url: str, limit: int = 20) -> list[dict[str, str]]:
        """采集当前页可见同域链接，用于站点内探索。"""
        page = self._ensure_page()
        records = page.evaluate(
            """() => {
                const anchors = Array.from(document.querySelectorAll('a[href]'));
                const visible = anchors.filter((a) => {
                    const style = window.getComputedStyle(a);
                    const rect = a.getBoundingClientRect();
                    return (
                        style.visibility !== 'hidden' &&
                        style.display !== 'none' &&
                        rect.width > 0 &&
                        rect.height > 0
                    );
                });
                return visible.map((a) => ({
                    href: a.href || '',
                    text: (a.textContent || '').trim(),
                }));
            }"""
        )
        if not isinstance(records, list):
            return []

        current_url = page.url
        base_host = (urlsplit(base_url).hostname or "").lower()
        seen: set[str] = set()
        result: list[dict[str, str]] = []
        for item in records:
            if not isinstance(item, dict):
                continue
            href_raw = str(item.get("href", "")).strip()
            if not href_raw:
                continue

            absolute = urljoin(current_url, href_raw)
            absolute, _ = urldefrag(absolute)
            parsed = urlsplit(absolute)
            if parsed.scheme not in {"http", "https"}:
                continue
            if base_host and (parsed.hostname or "").lower() != base_host:
                continue
            if absolute in seen:
                continue

            seen.add(absolute)
            result.append(
                {
                    "url": absolute,
                    "title": str(item.get("text", "")).strip(),
                }
            )
            if len(result) >= max(1, limit):
                break
        return result

    def extract_page_content(self, max_chars: int = 60000) -> dict[str, str]:
        """提取页面原始正文与基础元信息，优先文章/通知正文区域。"""
        page = self._ensure_page()
        payload = page.evaluate(
            """() => {
                const textOf = (el) => (el?.innerText || '').replace(/\\s+\\n/g, '\\n').replace(/\\n\\s+/g, '\\n').trim();

                const title = document.title || '';
                const metaDescription = document.querySelector('meta[name="description"]')?.getAttribute('content') || '';
                const publishCandidates = [
                    document.querySelector('meta[property="article:published_time"]')?.getAttribute('content') || '',
                    document.querySelector('meta[name="publishdate"]')?.getAttribute('content') || '',
                    document.querySelector('time')?.getAttribute('datetime') || '',
                    document.querySelector('.pub-time, .publish-time, .time, .date')?.textContent || '',
                ].filter(Boolean);

                const container =
                    document.querySelector('article') ||
                    document.querySelector('.article, .article-content, .content, .detail, .news-content, .post-content') ||
                    document.querySelector('main') ||
                    document.body;

                const paragraphs = Array.from(container.querySelectorAll('p, li'))
                    .map((node) => textOf(node))
                    .filter(Boolean)
                    .filter((line) => line.length >= 8);

                const fullText = textOf(container);
                const articleHtml = (container && container !== document.body) ? container.outerHTML : '';

                return {
                    title,
                    meta_description: metaDescription,
                    publish_time: publishCandidates[0] || '',
                    paragraphs: paragraphs.join('\\n'),
                    full_text: fullText,
                    article_html: articleHtml,
                };
            }"""
        )

        if not isinstance(payload, dict):
            return {
                "title": page.title(),
                "meta_description": "",
                "publish_time": "",
                "paragraphs": "",
                "full_text": "",
                "article_html": "",
            }

        def clip(value: object, limit: int) -> str:
            text = str(value or "").strip()
            if len(text) <= limit:
                return text
            return text[:limit]

        return {
            "title": clip(payload.get("title"), 500),
            "meta_description": clip(payload.get("meta_description"), 2000),
            "publish_time": clip(payload.get("publish_time"), 120),
            "paragraphs": clip(payload.get("paragraphs"), max_chars),
            "full_text": clip(payload.get("full_text"), max_chars),
            "article_html": clip(payload.get("article_html"), max_chars),
        }

    def human_scroll_to_bottom(self) -> None:
        """先等待页面初载，再从顶部以人工速度单次滚动到底部。"""
        page = self._ensure_page()

        page.wait_for_timeout(random.randint(3000, 5000))
        page.evaluate("() => window.scrollTo(0, 0)")
        page.wait_for_timeout(random.randint(300, 700))

        metrics = page.evaluate(
            """() => ({
                innerHeight: window.innerHeight,
                docHeight: Math.max(
                    document.body.scrollHeight,
                    document.documentElement.scrollHeight
                )
            })"""
        )
        if not isinstance(metrics, dict):
            return

        inner_height = float(metrics.get("innerHeight", 0))
        doc_height = float(metrics.get("docHeight", 0))
        if inner_height <= 0 or doc_height <= 0:
            return

        target_bottom = max(0.0, doc_height - inner_height)
        current_y = 0.0

        while current_y < target_bottom:
            delta = min(target_bottom - current_y, float(random.randint(160, 280)))
            page.mouse.wheel(0, delta)
            page.wait_for_timeout(random.randint(320, 680))
            current_y = float(page.evaluate("() => window.scrollY"))

        page.wait_for_timeout(random.randint(700, 1200))

    def wait_until_page_ready(self, settle_ms: int = 1200) -> None:
        """在截图前等待页面尽量加载稳定，减少抓到半加载状态。"""
        page = self._ensure_page()
        try:
            page.wait_for_load_state("domcontentloaded", timeout=25000)
        except TimeoutError:
            pass

        try:
            page.wait_for_load_state("load", timeout=25000)
        except TimeoutError:
            pass

        try:
            page.wait_for_load_state("networkidle", timeout=12000)
        except TimeoutError:
            pass

        page.wait_for_timeout(settle_ms)

    def execute(self, decision: ActionDecision) -> str:
        """执行模型决策动作并返回执行观察结果。"""
        action = decision.action
        args = decision.args

        if action == "click":
            x = float(args.get("x", 0))
            y = float(args.get("y", 0))
            link_url = self.page.evaluate(
                """({x, y}) => {
                    const element = document.elementFromPoint(x, y);
                    if (!element) return null;
                    const anchor = element.closest('a[href]');
                    return anchor ? anchor.href : null;
                }""",
                {"x": x, "y": y},
            )
            if isinstance(link_url, str) and link_url.strip():
                opened = self.open_url_in_new_tab(link_url)
                return f"已根据点击目标在新标签页打开：{opened}"

            try:
                with self.page.expect_popup(timeout=2500) as popup_info:
                    self.page.mouse.click(x, y, modifiers=["Meta"])
                popup = popup_info.value
                popup.set_default_navigation_timeout(self.navigation_timeout_ms)
                try:
                    popup.wait_for_load_state("domcontentloaded", timeout=15000)
                except TimeoutError:
                    pass
                self._page = popup
                return f"已点击坐标 ({x}, {y}) 并切换到新标签页"
            except TimeoutError:
                self.page.mouse.click(x, y)
                return f"已点击坐标 ({x}, {y})，未触发新标签页"

        if action == "type":
            text = str(args.get("text", ""))
            self.page.keyboard.type(text)
            return f"已输入文本，长度 {len(text)}"

        if action == "scroll":
            return "已忽略滚动动作（当前策略禁用滚动）"

        if action == "wait":
            ms = int(args.get("ms", 1200))
            self.page.wait_for_timeout(ms)
            return f"已等待 {ms}ms"

        if action == "navigate":
            url = str(args.get("url", "")).strip()
            if not url:
                return "跳转失败：缺少 url 参数"
            opened = self.open_url_in_new_tab(url)
            return f"已在新标签页打开 {opened}"

        if action == "finish":
            return "任务完成，等待 agent 收敛输出"

        return f"未知动作：{action}"
