"""Interactive browser tools backed by Playwright or Playwright MCP."""

from __future__ import annotations

import asyncio
import json
import os
import shutil
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from loguru import logger

from madpod_agent.agent.tools.base import Tool
from madpod_agent.agent.tools.registry import ToolRegistry
from madpod_agent.config.schema import BrowserToolsConfig

_REF_ATTR = "data-nanobot-ref"


def _json(payload: dict[str, Any]) -> str:
    return json.dumps(payload, ensure_ascii=False)


def _maybe_int(value: Any, default: int) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


@dataclass
class BrowserSnapshotState:
    """State cached from the latest browser snapshot."""

    refs: dict[str, str] = field(default_factory=dict)


class BrowserBackend:
    """Backend contract for interactive browser tools."""

    async def navigate(self, *, url: str) -> str:
        raise NotImplementedError

    async def snapshot(self, *, max_elements: int | None = None) -> str:
        raise NotImplementedError

    async def click(self, **kwargs: Any) -> str:
        raise NotImplementedError

    async def type(self, **kwargs: Any) -> str:
        raise NotImplementedError

    async def wait(self, **kwargs: Any) -> str:
        raise NotImplementedError

    async def back(self) -> str:
        raise NotImplementedError

    async def take_screenshot(self, **kwargs: Any) -> str:
        raise NotImplementedError

    async def execute(self, **kwargs: Any) -> str:
        raise NotImplementedError


class NativePlaywrightBrowserBackend(BrowserBackend):
    """Native Playwright-backed browser automation."""

    def __init__(self, workspace: Path, config: BrowserToolsConfig):
        self.workspace = workspace
        self.config = config
        self._lock = asyncio.Lock()
        self._playwright = None
        self._context = None
        self._page = None
        self._state = BrowserSnapshotState()

    def _profile_dir(self) -> Path:
        return self.workspace / self.config.profile_subdir

    def _output_dir(self) -> Path:
        return self.workspace / self.config.output_subdir

    def _headless(self) -> bool:
        if self.config.headless is not None:
            return self.config.headless
        return not bool(os.environ.get("DISPLAY"))

    @staticmethod
    def _normalize_url(url: str) -> str:
        value = (url or "").strip()
        if not value:
            return value
        if "://" not in value:
            return "https://" + value
        return value

    @staticmethod
    def _find_browser_binary() -> str | None:
        for candidate in (
            "google-chrome",
            "google-chrome-stable",
            "chrome",
            "chromium-browser",
            "chromium",
        ):
            path = shutil.which(candidate)
            if path:
                return path
        return None

    async def _ensure_page(self):
        async with self._lock:
            if self._page and not self._page.is_closed():
                return self._page

            try:
                from playwright.async_api import async_playwright
            except Exception as exc:
                raise RuntimeError(
                    "Playwright is not installed in this runtime. "
                    "Install the 'playwright' dependency or switch tools.browser.backend to 'playwright_mcp'."
                ) from exc

            if self._playwright is None:
                self._playwright = await async_playwright().start()

            self._profile_dir().mkdir(parents=True, exist_ok=True)
            self._output_dir().mkdir(parents=True, exist_ok=True)

            launch_kwargs: dict[str, Any] = {
                "user_data_dir": str(self._profile_dir()),
                "headless": self._headless(),
                "viewport": {
                    "width": self.config.viewport_width,
                    "height": self.config.viewport_height,
                },
                "args": ["--no-sandbox"],
            }
            if executable_path := self._find_browser_binary():
                launch_kwargs["executable_path"] = executable_path

            self._context = await self._playwright.chromium.launch_persistent_context(**launch_kwargs)
            self._context.set_default_timeout(self.config.action_timeout_s * 1000)
            self._context.set_default_navigation_timeout(self.config.navigation_timeout_s * 1000)
            self._page = self._context.pages[0] if self._context.pages else await self._context.new_page()
            try:
                await self._page.bring_to_front()
            except Exception:
                pass

            # Apply stealth scripts to reduce bot detection
            try:
                from madpod_agent.agent.browser.stealth import apply_stealth_scripts
                await apply_stealth_scripts(self._page)
            except Exception as stealth_exc:
                logger.debug("Stealth scripts not applied: {}", stealth_exc)

            return self._page

    async def _current_page(self):
        page = await self._ensure_page()
        if self._context:
            for candidate in self._context.pages:
                if not candidate.is_closed():
                    self._page = candidate
                    break
        return self._page or page

    async def _tabs(self) -> list[dict[str, Any]]:
        if not self._context:
            return []
        active = await self._current_page()
        tabs = []
        for index, page in enumerate(self._context.pages):
            if page.is_closed():
                continue
            tabs.append(
                {
                    "index": index,
                    "active": page == active,
                    "url": page.url,
                    "title": await page.title(),
                }
            )
        return tabs

    async def navigate(self, *, url: str) -> str:
        target = self._normalize_url(url)
        if not target:
            return "Error: 'url' is required"

        page = await self._current_page()
        await page.goto(target, wait_until="domcontentloaded")

        # Load domain-specific skill context if available
        skill_context = ""
        try:
            from urllib.parse import urlparse
            from madpod_agent.agent.browser.skill_loader import get_domain_skill
            domain = urlparse(target).netloc
            if domain:
                skill = get_domain_skill(domain)
                if skill:
                    skill_context = skill.format_metadata_only()
                    logger.debug("Loaded skill for domain: {}", domain)
        except Exception:
            pass

        result = {
            "ok": True,
            "action": "navigate",
            "url": page.url,
            "title": await page.title(),
            "headless": self._headless(),
            "message": "Browser opened. Run browser_snapshot to inspect the page.",
        }
        if skill_context:
            result["domain_skill"] = skill_context
        return _json(result)

    async def snapshot(self, *, max_elements: int | None = None) -> str:
        page = await self._current_page()
        limit = max(1, min(max_elements or self.config.snapshot_max_elements, 100))
        payload = await page.evaluate(
            """
            ({ attrName, maxElements, textChars }) => {
              const root = document.querySelector('main') || document.body || document.documentElement;
              const compact = (value, max = 160) => {
                const text = String(value || '').replace(/\\s+/g, ' ').trim();
                return text.length > max ? text.slice(0, max - 1) + '…' : text;
              };
              const visible = (el) => {
                if (!(el instanceof HTMLElement)) return false;
                const style = window.getComputedStyle(el);
                if (!style || style.visibility === 'hidden' || style.display === 'none') return false;
                const rect = el.getBoundingClientRect();
                if (rect.width < 2 || rect.height < 2) return false;
                return rect.bottom >= 0 && rect.right >= 0
                  && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
              };
              const labelFor = (el) => compact(
                el.getAttribute('aria-label')
                || el.getAttribute('placeholder')
                || el.getAttribute('title')
                || el.innerText
                || el.textContent
                || el.getAttribute('name')
                || el.getAttribute('value')
                || el.getAttribute('href')
              );
              window.__nanobotRefSeq = window.__nanobotRefSeq || 0;
              const selectors = [
                'a[href]',
                'button',
                'input',
                'textarea',
                'select',
                '[role="button"]',
                '[role="link"]',
                '[contenteditable="true"]',
                '[tabindex]'
              ];
              const seen = new Set();
              const elements = [];
              for (const el of document.querySelectorAll(selectors.join(','))) {
                if (elements.length >= maxElements) break;
                if (!visible(el) || seen.has(el)) continue;
                seen.add(el);
                let ref = el.getAttribute(attrName);
                if (!ref) {
                  ref = `e${++window.__nanobotRefSeq}`;
                  el.setAttribute(attrName, ref);
                }
                const rect = el.getBoundingClientRect();
                elements.push({
                  ref: `@${ref}`,
                  tag: (el.tagName || '').toLowerCase(),
                  role: el.getAttribute('role') || '',
                  type: el.getAttribute('type') || '',
                  label: labelFor(el),
                  text: compact(el.innerText || el.textContent || '', 120),
                  href: el.getAttribute('href') || '',
                  x: Math.round(rect.x),
                  y: Math.round(rect.y),
                });
              }
              const textSource = compact(root ? root.innerText || root.textContent || '' : '', textChars);
              return {
                url: location.href,
                title: document.title || '',
                elements,
                textExcerpt: textSource,
                note: 'Refs can change after navigation or DOM updates. Re-run browser_snapshot after the page changes.',
              };
            }
            """,
            {
                "attrName": _REF_ATTR,
                "maxElements": limit,
                "textChars": self.config.snapshot_text_chars,
            },
        )
        self._state.refs = {
            item["ref"]: f'[{_REF_ATTR}="{item["ref"].lstrip("@")}"]'
            for item in payload.get("elements", [])
        }
        payload["tabs"] = await self._tabs()
        return _json(payload)

    def _target_selector(self, *, ref: str | None = None, selector: str | None = None) -> str:
        if ref:
            key = ref if ref.startswith("@") else f"@{ref}"
            return self._state.refs.get(key) or f'[{_REF_ATTR}="{key.lstrip("@")}"]'
        if selector:
            return selector
        raise ValueError("Provide 'ref' or 'selector'")

    async def click(self, **kwargs: Any) -> str:
        page = await self._current_page()
        target = self._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        locator = page.locator(target).first

        # Anti-detection: add human-like delay before clicking
        try:
            from madpod_agent.agent.browser.stealth import human_delay
            await human_delay(min_ms=200, max_ms=800)
        except Exception:
            pass

        await locator.click(button=str(kwargs.get("button") or "left"))
        try:
            await page.wait_for_load_state("domcontentloaded", timeout=self.config.navigation_timeout_s * 1000)
        except Exception:
            pass
        return _json(
            {
                "ok": True,
                "action": "click",
                "url": page.url,
                "title": await page.title(),
                "target": kwargs.get("ref") or kwargs.get("selector"),
                "message": "Click completed. Re-run browser_snapshot if the page changed.",
            }
        )

    async def type(self, **kwargs: Any) -> str:
        text = str(kwargs.get("text") or "")
        if not text:
            return "Error: 'text' is required"
        page = await self._current_page()
        target = self._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        locator = page.locator(target).first

        # Anti-detection: add human-like delay before typing
        try:
            from madpod_agent.agent.browser.stealth import human_delay
            await human_delay(min_ms=100, max_ms=500)
        except Exception:
            pass

        if kwargs.get("clear_first", True):
            await locator.fill(text)
        else:
            await locator.type(text)
        if kwargs.get("submit"):
            await locator.press("Enter")
        return _json(
            {
                "ok": True,
                "action": "type",
                "url": page.url,
                "title": await page.title(),
                "target": kwargs.get("ref") or kwargs.get("selector"),
            }
        )

    async def wait(self, **kwargs: Any) -> str:
        page = await self._current_page()
        if kwargs.get("seconds") is not None:
            await asyncio.sleep(max(0, float(kwargs["seconds"])))
        elif kwargs.get("text"):
            await page.get_by_text(str(kwargs["text"])).first.wait_for(
                timeout=self.config.navigation_timeout_s * 1000
            )
        elif kwargs.get("text_gone"):
            await page.wait_for_function(
                """value => !document.body || !document.body.innerText.includes(value)""",
                str(kwargs["text_gone"]),
                timeout=self.config.navigation_timeout_s * 1000,
            )
        elif kwargs.get("ref") or kwargs.get("selector"):
            target = self._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
            await page.locator(target).first.wait_for(timeout=self.config.navigation_timeout_s * 1000)
        else:
            await asyncio.sleep(1.0)
        return _json({"ok": True, "action": "wait", "url": page.url, "title": await page.title()})

    async def back(self) -> str:
        page = await self._current_page()
        await page.go_back(wait_until="domcontentloaded")
        return _json({"ok": True, "action": "back", "url": page.url, "title": await page.title()})

    async def take_screenshot(self, **kwargs: Any) -> str:
        page = await self._current_page()
        output_dir = self._output_dir()
        output_dir.mkdir(parents=True, exist_ok=True)
        filename = str(kwargs.get("filename") or f"screenshot-{int(time.time())}.png")
        target = output_dir / Path(filename).name
        await page.screenshot(path=str(target), full_page=bool(kwargs.get("full_page")))
        return _json(
            {
                "ok": True,
                "action": "screenshot",
                "url": page.url,
                "title": await page.title(),
                "path": str(target),
            }
        )

    async def execute(self, **kwargs: Any) -> str:
        code = str(kwargs.get("code") or "").strip()
        if not code:
            return "Error: 'code' is required"
        page = await self._current_page()
        result = await page.evaluate(code)
        return _json(
            {
                "ok": True,
                "action": "execute",
                "url": page.url,
                "title": await page.title(),
                "result": result if isinstance(result, (str, int, float, bool, list, dict)) else str(result),
            }
        )


class MCPBrowserBackend(BrowserBackend):
    """Stable browser tool surface backed by hidden Playwright MCP tools."""

    TOOL_MAP = {
        "browser_navigate": "browser_navigate",
        "browser_snapshot": "browser_snapshot",
        "browser_click": "browser_click",
        "browser_type": "browser_type",
        "browser_wait": "browser_wait_for",
        "browser_back": "browser_navigate_back",
        "browser_take_screenshot": "browser_take_screenshot",
        "browser_execute": "browser_evaluate",
    }

    def __init__(self, registry: ToolRegistry, server_name: str):
        self.registry = registry
        self.server_name = server_name

    async def _call(self, stable_name: str, params: dict[str, Any]) -> str:
        mapped = self.TOOL_MAP[stable_name]
        tool_name = f"mcp_{self.server_name}_{mapped}"
        if not self.registry.has(tool_name):
            return (
                f"Error: MCP browser backend is enabled but '{tool_name}' is unavailable. "
                "Ensure the Playwright MCP server is configured and connected."
            )
        return await self.registry.execute(tool_name, params)

    async def navigate(self, *, url: str) -> str:
        return await self._call("browser_navigate", {"url": url})

    async def snapshot(self, *, max_elements: int | None = None) -> str:
        params = {}
        if max_elements is not None:
            params["maxElements"] = max_elements
        return await self._call("browser_snapshot", params)

    async def click(self, **kwargs: Any) -> str:
        params = {
            "element": kwargs.get("ref") or kwargs.get("selector") or "",
            "ref": kwargs.get("ref") or "",
        }
        return await self._call("browser_click", params)

    async def type(self, **kwargs: Any) -> str:
        params = {
            "element": kwargs.get("ref") or kwargs.get("selector") or "",
            "ref": kwargs.get("ref") or "",
            "text": kwargs.get("text") or "",
            "submit": bool(kwargs.get("submit")),
        }
        return await self._call("browser_type", params)

    async def wait(self, **kwargs: Any) -> str:
        params: dict[str, Any] = {}
        if kwargs.get("seconds") is not None:
            params["time"] = kwargs.get("seconds")
        if kwargs.get("text"):
            params["text"] = kwargs.get("text")
        if kwargs.get("text_gone"):
            params["textGone"] = kwargs.get("text_gone")
        return await self._call("browser_wait", params)

    async def back(self) -> str:
        return await self._call("browser_back", {})

    async def take_screenshot(self, **kwargs: Any) -> str:
        params = {}
        if kwargs.get("filename"):
            params["filename"] = kwargs.get("filename")
        if kwargs.get("full_page") is not None:
            params["fullPage"] = kwargs.get("full_page")
        return await self._call("browser_take_screenshot", params)

    async def execute(self, **kwargs: Any) -> str:
        return await self._call("browser_execute", {"function": kwargs.get("code") or ""})


class BrowserToolBase(Tool):
    """Shared base for first-class browser tools."""

    def __init__(self, backend: BrowserBackend):
        self.backend = backend


class BrowserNavigateTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_navigate"

    @property
    def description(self) -> str:
        return (
            "Open Chrome/Chromium and navigate to a URL. "
            "Use this for interactive, dynamic, or authenticated sites where web_search/web_fetch cannot see the real page state."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "The URL to open"},
            },
            "required": ["url"],
        }

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.navigate(url=kwargs["url"])


class BrowserSnapshotTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_snapshot"

    @property
    def description(self) -> str:
        return (
            "Return a compact snapshot of the current page with reusable element refs. "
            "Use this after navigation and after visible page changes so later click/type actions target the current DOM instead of stale refs."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "max_elements": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "description": "Optional limit for returned interactive elements",
                }
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.snapshot(max_elements=kwargs.get("max_elements"))


class BrowserClickTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_click"

    @property
    def description(self) -> str:
        return (
            "Click an element in the current browser page using a snapshot ref or selector. "
            "Prefer refs from browser_snapshot because they are more stable and easier for the model to reason about than raw selectors."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "Element ref from browser_snapshot, e.g. @e1"},
                "selector": {"type": "string", "description": "CSS selector fallback"},
                "button": {"type": "string", "enum": ["left", "right", "middle"]},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.click(**kwargs)


class BrowserTypeTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_type"

    @property
    def description(self) -> str:
        return (
            "Type into a field in the current browser page using a snapshot ref or selector. "
            "Use refs from browser_snapshot when possible, and re-snapshot first if navigation or DOM changes may have invalidated prior refs."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "Element ref from browser_snapshot, e.g. @e1"},
                "selector": {"type": "string", "description": "CSS selector fallback"},
                "text": {"type": "string", "description": "Text to type"},
                "clear_first": {"type": "boolean", "description": "Clear the field before typing"},
                "submit": {"type": "boolean", "description": "Press Enter after typing"},
            },
            "required": ["text"],
        }

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.type(**kwargs)


class BrowserWaitTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_wait"

    @property
    def description(self) -> str:
        return (
            "Wait for time, text, or an element while the browser page updates. "
            "Use this when a click, form submit, or navigation triggers async UI changes before the next snapshot or action."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "seconds": {"type": "number", "minimum": 0},
                "ref": {"type": "string"},
                "selector": {"type": "string"},
                "text": {"type": "string"},
                "text_gone": {"type": "string"},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.wait(**kwargs)


class BrowserBackTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_back"

    @property
    def description(self) -> str:
        return "Go back one step in browser history."

    @property
    def parameters(self) -> dict[str, Any]:
        return {"type": "object", "properties": {}}

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.back()


class BrowserTakeScreenshotTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_take_screenshot"

    @property
    def description(self) -> str:
        return (
            "Capture a screenshot of the current browser page. "
            "Use this when visual verification matters or when you need to inspect a layout/state that text extraction may miss."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "filename": {"type": "string"},
                "full_page": {"type": "boolean"},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.take_screenshot(**kwargs)


class BrowserExecuteTool(BrowserToolBase):
    @property
    def name(self) -> str:
        return "browser_execute"

    @property
    def description(self) -> str:
        return (
            "Run JavaScript in the current browser page and return the result. "
            "Use this for DOM inspection or state extraction when snapshot text is insufficient, not as a first-choice replacement for normal browser tools."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "code": {"type": "string", "description": "JavaScript expression or function body"},
            },
            "required": ["code"],
        }

    async def execute(self, **kwargs: Any) -> str:
        return await self.backend.execute(**kwargs)


class BrowserSelectTool(BrowserToolBase):
    """Select an option from a dropdown/select element."""

    @property
    def name(self) -> str:
        return "browser_select"

    @property
    def description(self) -> str:
        return (
            "Select an option from a <select> dropdown element. "
            "Use ref from browser_snapshot or a CSS selector, plus either the option value or visible label text."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "Element ref from browser_snapshot, e.g. @e1"},
                "selector": {"type": "string", "description": "CSS selector fallback"},
                "value": {"type": "string", "description": "Option value to select"},
                "label": {"type": "string", "description": "Visible text of the option to select (alternative to value)"},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return await self.backend.execute(
                code=f"""(() => {{
                    const el = document.querySelector('{kwargs.get("selector", "")}');
                    if (!el) return 'Error: element not found';
                    el.value = '{kwargs.get("value", "")}';
                    el.dispatchEvent(new Event('change', {{bubbles: true}}));
                    return 'selected';
                }})()"""
            )
        page = await self.backend._current_page()
        target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        locator = page.locator(target).first

        if kwargs.get("label"):
            await locator.select_option(label=kwargs["label"])
        elif kwargs.get("value"):
            await locator.select_option(value=kwargs["value"])
        else:
            return "Error: provide 'value' or 'label' to select"

        return _json({
            "ok": True,
            "action": "select",
            "target": kwargs.get("ref") or kwargs.get("selector"),
            "value": kwargs.get("value"),
            "label": kwargs.get("label"),
        })


class BrowserCheckboxTool(BrowserToolBase):
    """Toggle a checkbox or radio button."""

    @property
    def name(self) -> str:
        return "browser_checkbox"

    @property
    def description(self) -> str:
        return (
            "Check or uncheck a checkbox, or select a radio button. "
            "Use ref from browser_snapshot or a CSS selector."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "Element ref from browser_snapshot"},
                "selector": {"type": "string", "description": "CSS selector fallback"},
                "checked": {"type": "boolean", "description": "True to check, False to uncheck (default: True)"},
            },
        }

    async def execute(self, checked: bool = True, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            action = "check" if checked else "uncheck"
            return await self.backend.execute(
                code=f"""(() => {{
                    const el = document.querySelector('{kwargs.get("selector", "")}');
                    if (!el) return 'Error: element not found';
                    el.checked = {str(checked).lower()};
                    el.dispatchEvent(new Event('change', {{bubbles: true}}));
                    return '{action}ed';
                }})()"""
            )
        page = await self.backend._current_page()
        target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        locator = page.locator(target).first
        if checked:
            await locator.check()
        else:
            await locator.uncheck()
        return _json({"ok": True, "action": "checkbox", "checked": checked})


class BrowserUploadFileTool(BrowserToolBase):
    """Upload a file through a file input element."""

    def __init__(self, backend: BrowserBackend, workspace: Path):
        super().__init__(backend)
        self._workspace = workspace

    @property
    def name(self) -> str:
        return "browser_upload_file"

    @property
    def description(self) -> str:
        return (
            "Upload a file through a <input type='file'> element. "
            "Specify the file input element and the local file path to upload."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "File input ref from browser_snapshot"},
                "selector": {"type": "string", "description": "CSS selector for the file input"},
                "path": {"type": "string", "description": "Local file path to upload"},
            },
            "required": ["path"],
        }

    async def execute(self, path: str, **kwargs: Any) -> str:
        file_path = Path(path).expanduser()
        if not file_path.is_absolute():
            file_path = self._workspace / path
        file_path = file_path.resolve()

        if not file_path.exists():
            return _json({"error": f"File not found: {path}"})

        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "File upload requires native Playwright backend"})

        page = await self.backend._current_page()
        target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        locator = page.locator(target).first
        await locator.set_input_files(str(file_path))
        return _json({
            "ok": True,
            "action": "upload",
            "file": file_path.name,
            "size": file_path.stat().st_size,
        })


class BrowserKeyPressTool(BrowserToolBase):
    """Press keyboard keys or key combinations."""

    @property
    def name(self) -> str:
        return "browser_press"

    @property
    def description(self) -> str:
        return (
            "Press a keyboard key or combination in the current browser page. "
            "Supports special keys: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, "
            "ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, F1-F12. "
            "Combinations: Control+a, Meta+c, Shift+Tab, Alt+Enter."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "key": {
                    "type": "string",
                    "description": "Key to press (e.g. 'Enter', 'Tab', 'Escape', 'Control+a', 'Meta+v')",
                },
                "ref": {"type": "string", "description": "Optional element ref to focus first"},
                "selector": {"type": "string", "description": "Optional CSS selector to focus first"},
            },
            "required": ["key"],
        }

    async def execute(self, key: str, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Key press requires native Playwright backend"})

        page = await self.backend._current_page()
        if kwargs.get("ref") or kwargs.get("selector"):
            target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
            await page.locator(target).first.press(key)
        else:
            await page.keyboard.press(key)
        return _json({"ok": True, "action": "press", "key": key})


class BrowserFillFormTool(BrowserToolBase):
    """Fill multiple form fields at once."""

    @property
    def name(self) -> str:
        return "browser_fill_form"

    @property
    def description(self) -> str:
        return (
            "Fill multiple form fields in one action. Provide field mappings from CSS selectors or "
            "snapshot refs to values. Much faster than typing into each field individually. "
            "Use extract_forms first to discover the form structure, then fill all fields at once."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "fields": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "ref": {"type": "string", "description": "Element ref from snapshot"},
                            "selector": {"type": "string", "description": "CSS selector"},
                            "value": {"type": "string", "description": "Value to fill/select"},
                            "action": {
                                "type": "string",
                                "enum": ["fill", "select", "check", "uncheck"],
                                "description": "Action type (default: fill)",
                            },
                        },
                        "required": ["value"],
                    },
                    "description": "Array of field mappings to fill",
                },
                "submit": {
                    "type": "boolean",
                    "description": "Submit the form after filling (press Enter on last field)",
                },
            },
            "required": ["fields"],
        }

    async def execute(self, fields: list[dict[str, Any]], submit: bool = False, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Form fill requires native Playwright backend"})

        page = await self.backend._current_page()
        filled = []
        errors = []

        for i, field_spec in enumerate(fields):
            try:
                target = self.backend._target_selector(
                    ref=field_spec.get("ref"), selector=field_spec.get("selector")
                )
                locator = page.locator(target).first
                value = field_spec.get("value", "")
                action = field_spec.get("action", "fill")

                if action == "select":
                    await locator.select_option(value=value)
                elif action == "check":
                    await locator.check()
                elif action == "uncheck":
                    await locator.uncheck()
                else:
                    await locator.fill(value)

                filled.append(field_spec.get("ref") or field_spec.get("selector") or f"field_{i}")
            except Exception as exc:
                errors.append({"field": i, "error": str(exc)})

        if submit and filled:
            try:
                last_target = self.backend._target_selector(
                    ref=fields[-1].get("ref"), selector=fields[-1].get("selector")
                )
                await page.locator(last_target).first.press("Enter")
            except Exception as exc:
                errors.append({"field": "submit", "error": str(exc)})

        return _json({
            "ok": len(errors) == 0,
            "action": "fill_form",
            "filled": len(filled),
            "errors": errors,
            "url": page.url,
        })


class BrowserNewTabTool(BrowserToolBase):
    """Open a new browser tab."""

    @property
    def name(self) -> str:
        return "browser_new_tab"

    @property
    def description(self) -> str:
        return "Open a new browser tab, optionally navigating to a URL."

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "Optional URL to navigate to in the new tab"},
            },
        }

    async def execute(self, url: str | None = None, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Tab management requires native Playwright backend"})

        if not self.backend._context:
            await self.backend._ensure_page()

        page = await self.backend._context.new_page()
        self.backend._page = page

        if url:
            target = NativePlaywrightBrowserBackend._normalize_url(url)
            if target:
                await page.goto(target, wait_until="domcontentloaded")

        return _json({
            "ok": True,
            "action": "new_tab",
            "url": page.url,
            "title": await page.title(),
            "tab_count": len([p for p in self.backend._context.pages if not p.is_closed()]),
        })


class BrowserSwitchTabTool(BrowserToolBase):
    """Switch to a different browser tab."""

    @property
    def name(self) -> str:
        return "browser_switch_tab"

    @property
    def description(self) -> str:
        return "Switch to a different browser tab by index. Use browser_snapshot to see tab list with indices."

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "index": {"type": "integer", "description": "Tab index (from browser_snapshot's tabs list)"},
            },
            "required": ["index"],
        }

    async def execute(self, index: int, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Tab management requires native Playwright backend"})

        if not self.backend._context:
            return _json({"error": "No browser context"})

        pages = [p for p in self.backend._context.pages if not p.is_closed()]
        if index < 0 or index >= len(pages):
            return _json({"error": f"Tab index {index} out of range (0-{len(pages) - 1})"})

        self.backend._page = pages[index]
        try:
            await self.backend._page.bring_to_front()
        except Exception:
            pass

        return _json({
            "ok": True,
            "action": "switch_tab",
            "index": index,
            "url": self.backend._page.url,
            "title": await self.backend._page.title(),
        })


class BrowserCloseTabTool(BrowserToolBase):
    """Close the current or a specific browser tab."""

    @property
    def name(self) -> str:
        return "browser_close_tab"

    @property
    def description(self) -> str:
        return "Close the current browser tab (or a specific tab by index). Switches to the previous tab."

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "index": {"type": "integer", "description": "Optional: specific tab index to close"},
            },
        }

    async def execute(self, index: int | None = None, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Tab management requires native Playwright backend"})

        if not self.backend._context:
            return _json({"error": "No browser context"})

        pages = [p for p in self.backend._context.pages if not p.is_closed()]
        if len(pages) <= 1:
            return _json({"error": "Cannot close the last tab"})

        target_page = pages[index] if index is not None and 0 <= index < len(pages) else self.backend._page
        await target_page.close()

        remaining = [p for p in self.backend._context.pages if not p.is_closed()]
        self.backend._page = remaining[-1] if remaining else None

        return _json({
            "ok": True,
            "action": "close_tab",
            "remaining_tabs": len(remaining),
            "active_url": self.backend._page.url if self.backend._page else None,
        })


class BrowserScrollTool(BrowserToolBase):
    """Scroll the page up or down."""

    @property
    def name(self) -> str:
        return "browser_scroll"

    @property
    def description(self) -> str:
        return (
            "Scroll the page up, down, or to a specific element. "
            "Use this to load lazy content, find elements below the fold, or navigate long pages."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "direction": {
                    "type": "string",
                    "enum": ["up", "down"],
                    "description": "Scroll direction",
                },
                "amount": {
                    "type": "integer",
                    "description": "Pixels to scroll (default 600)",
                    "minimum": 100,
                    "maximum": 5000,
                },
                "ref": {"type": "string", "description": "Scroll to this element ref instead"},
                "selector": {"type": "string", "description": "Scroll to this CSS selector instead"},
            },
        }

    async def execute(self, direction: str = "down", amount: int = 600, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            scroll_js = f"window.scrollBy(0, {'-%d' % amount if direction == 'up' else '%d' % amount})"
            return await self.backend.execute(code=scroll_js)

        page = await self.backend._current_page()

        if kwargs.get("ref") or kwargs.get("selector"):
            target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
            await page.locator(target).first.scroll_into_view_if_needed()
        else:
            delta = -amount if direction == "up" else amount
            await page.evaluate(f"window.scrollBy(0, {delta})")

        await asyncio.sleep(0.3)  # brief pause for lazy-load triggers

        scroll_info = await page.evaluate(
            "() => ({ scrollY: window.scrollY, scrollHeight: document.documentElement.scrollHeight, "
            "viewportHeight: window.innerHeight })"
        )

        return _json({
            "ok": True,
            "action": "scroll",
            "direction": direction,
            "scroll_y": scroll_info.get("scrollY"),
            "scroll_height": scroll_info.get("scrollHeight"),
            "viewport_height": scroll_info.get("viewportHeight"),
            "at_bottom": scroll_info.get("scrollY", 0) + scroll_info.get("viewportHeight", 0) >= scroll_info.get("scrollHeight", 0) - 10,
        })


class BrowserDragTool(BrowserToolBase):
    """Drag an element to a target position."""

    @property
    def name(self) -> str:
        return "browser_drag"

    @property
    def description(self) -> str:
        return (
            "Drag an element from one position to another. "
            "Useful for drag-and-drop interfaces, sliders, and reordering lists."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "source_ref": {"type": "string", "description": "Source element ref"},
                "source_selector": {"type": "string", "description": "Source CSS selector"},
                "target_ref": {"type": "string", "description": "Target element ref"},
                "target_selector": {"type": "string", "description": "Target CSS selector"},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Drag requires native Playwright backend"})

        page = await self.backend._current_page()
        source = self.backend._target_selector(ref=kwargs.get("source_ref"), selector=kwargs.get("source_selector"))
        target = self.backend._target_selector(ref=kwargs.get("target_ref"), selector=kwargs.get("target_selector"))

        await page.locator(source).first.drag_to(page.locator(target).first)
        return _json({"ok": True, "action": "drag"})


class BrowserHoverTool(BrowserToolBase):
    """Hover over an element."""

    @property
    def name(self) -> str:
        return "browser_hover"

    @property
    def description(self) -> str:
        return (
            "Hover over an element to trigger hover menus, tooltips, or CSS hover states. "
            "Use this when you need to reveal hidden UI elements that appear on mouse hover."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "Element ref from browser_snapshot"},
                "selector": {"type": "string", "description": "CSS selector fallback"},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Hover requires native Playwright backend"})

        page = await self.backend._current_page()
        target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        await page.locator(target).first.hover()
        await asyncio.sleep(0.3)
        return _json({"ok": True, "action": "hover", "target": kwargs.get("ref") or kwargs.get("selector")})


class BrowserForwardTool(BrowserToolBase):
    """Navigate browser forward one page."""

    @property
    def name(self) -> str:
        return "browser_forward"

    @property
    def description(self) -> str:
        return "Go forward one step in browser history (opposite of browser_back)."

    @property
    def parameters(self) -> dict[str, Any]:
        return {"type": "object", "properties": {}}

    async def execute(self, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Forward requires native Playwright backend"})
        page = await self.backend._current_page()
        await page.go_forward(wait_until="domcontentloaded")
        return _json({"ok": True, "action": "forward", "url": page.url, "title": await page.title()})


class BrowserDoubleClickTool(BrowserToolBase):
    """Double-click an element on the page."""

    @property
    def name(self) -> str:
        return "browser_double_click"

    @property
    def description(self) -> str:
        return (
            "Double-click an element on the page. Use for selecting text, "
            "opening files in web apps, or editing cells in web spreadsheets."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "Element ref from browser_snapshot"},
                "selector": {"type": "string", "description": "CSS selector fallback"},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Double-click requires native Playwright backend"})
        page = await self.backend._current_page()
        target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        await page.locator(target).first.dblclick()
        return _json({"ok": True, "action": "double_click", "target": kwargs.get("ref") or kwargs.get("selector")})


class BrowserClearTool(BrowserToolBase):
    """Clear the content of an input field."""

    @property
    def name(self) -> str:
        return "browser_clear"

    @property
    def description(self) -> str:
        return (
            "Clear the content of an input field. Use before re-filling a form field "
            "or when you need to reset an input to empty."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "ref": {"type": "string", "description": "Element ref from browser_snapshot"},
                "selector": {"type": "string", "description": "CSS selector fallback"},
            },
        }

    async def execute(self, **kwargs: Any) -> str:
        if not isinstance(self.backend, NativePlaywrightBrowserBackend):
            return _json({"error": "Clear requires native Playwright backend"})
        page = await self.backend._current_page()
        target = self.backend._target_selector(ref=kwargs.get("ref"), selector=kwargs.get("selector"))
        await page.locator(target).first.clear()
        return _json({"ok": True, "action": "clear", "target": kwargs.get("ref") or kwargs.get("selector")})


def register_browser_tools(
    registry: ToolRegistry,
    *,
    workspace: Path,
    config: BrowserToolsConfig | None,
) -> None:
    """Register first-class browser tools when enabled."""
    if config is None or not config.enabled:
        return

    backend: BrowserBackend
    if config.backend == "playwright_mcp":
        backend = MCPBrowserBackend(registry, config.mcp_server_name)
    else:
        backend = NativePlaywrightBrowserBackend(workspace, config)

    for tool_cls in (
        BrowserNavigateTool,
        BrowserSnapshotTool,
        BrowserClickTool,
        BrowserTypeTool,
        BrowserWaitTool,
        BrowserBackTool,
        BrowserTakeScreenshotTool,
        BrowserExecuteTool,
        # New power tools
        BrowserSelectTool,
        BrowserCheckboxTool,
        BrowserKeyPressTool,
        BrowserFillFormTool,
        BrowserNewTabTool,
        BrowserSwitchTabTool,
        BrowserCloseTabTool,
        BrowserScrollTool,
        BrowserDragTool,
        BrowserHoverTool,
        # Wave-3 browser tools
        BrowserForwardTool,
        BrowserDoubleClickTool,
        BrowserClearTool,
    ):
        if tool_cls is BrowserUploadFileTool:
            continue  # handled separately below
        tool = tool_cls(backend)
        registry.register(tool)
        logger.debug("Registered browser tool '{}'", tool.name)

    # Upload tool needs workspace reference
    upload_tool = BrowserUploadFileTool(backend, workspace)
    registry.register(upload_tool)
    logger.debug("Registered browser tool '{}'", upload_tool.name)

    # Wave-3: browser-dependent standalone tools
    from .page_to_pdf import PageToPdfTool
    from .dom_query import DomQueryTool
    from .page_search import PageSearchTool

    registry.register(PageToPdfTool(workspace=workspace, browser_backend=backend))
    registry.register(DomQueryTool(browser_backend=backend))
    registry.register(PageSearchTool(browser_backend=backend))
