"""UI automation tools."""

import asyncio
import logging
from typing import Any, Callable

from mcp.server.fastmcp import Context, FastMCP

from ..session import SessionManager

from ..response import build_error_response, build_response
from ..session.state import DebugState

logger = logging.getLogger(__name__)


def register_ui_tools(
    mcp: FastMCP,
    session: SessionManager,
    check_session_access: Callable[[Any], str | None],
) -> None:
    """Register UI automation tools on the MCP server."""
    from mcp.types import ToolAnnotations

    # Lazy-loaded UI automation instance
    _ui_holder: dict[str, Any] = {"instance": None}

    # Cache for last annotated screenshot (used by ui_click_annotated)
    _last_annotation: dict[str, Any] | None = None
    _annotation_generation: int = 0

    def _get_ui() -> Any:
        """Get or create UI automation instance."""
        if _ui_holder["instance"] is None:
            from ..ui import UIAutomation
            _ui_holder["instance"] = UIAutomation()
        return _ui_holder["instance"]

    async def _ensure_ui_connected() -> Any:
        """Ensure UI automation is connected to the debug process.

        Raises:
            NoActiveSessionError: If no debug session is active
            NoProcessIdError: If process ID not available
        """
        from ..ui import NoActiveSessionError, NoProcessIdError

        if session.state.state == DebugState.IDLE:
            raise NoActiveSessionError("No debug session is active. Start debugging first.")

        process_id = session.state.process_id
        if not process_id:
            raise NoProcessIdError(
                "Process ID not available. Debug session may not have started the process yet."
            )

        ui = _get_ui()
        if ui.process_id != process_id:
            await ui.connect(process_id)
        return ui

    async def _find_ui_element(
        automation_id: str | None = None,
        name: str | None = None,
        control_type: str | None = None,
    ):
        """Helper to connect to UI and find an element."""
        ui = await _ensure_ui_connected()
        element = await ui.find_element(
            automation_id=automation_id,
            name=name,
            control_type=control_type,
        )
        return ui, element

    @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False))
    async def ui_get_window_tree(max_depth: int = 3, max_children: int = 50) -> dict:
        """
        Get the visual tree of the debugged application's main window.

        Use this to understand the UI structure before interacting with elements.
        Call after start_debug and wait for the application window to appear.

        Args:
            max_depth: Maximum depth to traverse (default 3)
            max_children: Maximum children per element (default 50)

        Returns:
            Visual tree with automationId, controlType, name, isEnabled, etc.
        """
        try:
            ui = await _ensure_ui_connected()
            tree = await ui.get_window_tree(max_depth, max_children)
            return build_response(data=tree.to_dict(), state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False))
    async def ui_find_element(
        automation_id: str | None = None,
        name: str | None = None,
        control_type: str | None = None,
    ) -> dict:
        """
        Find a UI element by AutomationId, name, or control type.

        At least one search criterion must be provided.
        Use ui_get_window_tree first to discover available elements.

        Args:
            automation_id: AutomationId property (most reliable for WPF)
            name: Element's Name/Title property
            control_type: Type like "Button", "TextBox", "MenuItem"

        Returns:
            Element info if found
        """
        try:
            ui = await _ensure_ui_connected()
            element = await ui.find_element(
                automation_id=automation_id,
                name=name,
                control_type=control_type,
            )
            info = await ui.get_element_info(element)
            return build_response(data=info.to_dict(), state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(idempotentHint=True, openWorldHint=False))
    async def ui_set_focus(
        ctx: Context,
        automation_id: str | None = None,
        name: str | None = None,
        control_type: str | None = None,
    ) -> dict:
        """
        Set keyboard focus to a UI element.

        Tries cached coordinates first (click to focus), then falls back to
        pywinauto element search.

        Call this before ui_send_keys to ensure keys go to the right element.

        Args:
            automation_id: AutomationId property
            name: Element's Name/Title property
            control_type: Control type
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            # Try cache first — click to set focus
            if automation_id:
                ui = await _ensure_ui_connected()
                rect = ui.get_cached_rect(automation_id)
                if rect:
                    cx = (rect["left"] + rect["right"]) // 2
                    cy = (rect["top"] + rect["bottom"]) // 2
                    await ui._click_at_coords(cx, cy)
                    return build_response(
                        data={"focused": True, "method": "cache"},
                        state=session.state.state,
                    )

            # Fallback to element search
            ui, element = await _find_ui_element(automation_id, name, control_type)
            await ui.set_focus(element)
            return build_response(data={"focused": True, "method": "element_search"}, state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_send_keys(
        ctx: Context,
        keys: str,
        automation_id: str | None = None,
        name: str | None = None,
        control_type: str | None = None,
    ) -> dict:
        """
        Send keyboard input to a UI element.

        Tries cached coordinates first (click to focus, then send keys),
        then falls back to pywinauto element search.

        Uses pywinauto keyboard syntax:
        - Regular text: "hello"
        - Enter: "{ENTER}"
        - Tab: "{TAB}"
        - Escape: "{ESC}"
        - Ctrl+C: "^c"
        - Alt+F4: "%{F4}"
        - Shift+Tab: "+{TAB}"
        - Ctrl+Shift+S: "^+s"

        Args:
            keys: Keys to send (pywinauto syntax)
            automation_id: Target element's AutomationId
            name: Target element's Name
            control_type: Target element's control type
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            # Try cache first — click to focus, then send keys without element search
            if automation_id:
                ui = await _ensure_ui_connected()
                rect = ui.get_cached_rect(automation_id)
                if rect:
                    cx = (rect["left"] + rect["right"]) // 2
                    cy = (rect["top"] + rect["bottom"]) // 2
                    await ui._click_at_coords(cx, cy)
                    await ui.send_keys_focused(keys)
                    return build_response(
                        data={"sent": keys, "method": "cache"},
                        state=session.state.state,
                    )

            # Fallback to element search
            ui, element = await _find_ui_element(automation_id, name, control_type)
            await ui.send_keys(element, keys)
            return build_response(data={"sent": keys, "method": "element_search"}, state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_send_keys_focused(ctx: Context, keys: str) -> dict:
        """
        Send keyboard input to the currently focused element.

        Use this AFTER ui_set_focus to avoid re-searching for complex elements
        like DataGrid that may timeout on repeated searches.

        Workflow:
        1. ui_set_focus(automation_id="MyElement")  # Focus the element
        2. ui_send_keys_focused(keys="^{END}")      # Send keys without re-search

        Uses pywinauto keyboard syntax:
        - Regular text: "hello"
        - Enter: "{ENTER}", Tab: "{TAB}", Escape: "{ESC}"
        - Ctrl+C: "^c", Alt+F4: "%{F4}", Shift+Tab: "+{TAB}"
        - Arrow keys: "{LEFT}", "{RIGHT}", "{UP}", "{DOWN}"
        - Ctrl+End: "^{END}", Ctrl+Home: "^{HOME}"

        Args:
            keys: Keys to send (pywinauto syntax)
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            ui = await _ensure_ui_connected()
            await ui.send_keys_focused(keys)
            return build_response(
                data={"sent": keys, "target": "focused"}, state=session.state.state,
            )
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_click(
        ctx: Context,
        automation_id: str | None = None,
        name: str | None = None,
        control_type: str | None = None,
    ) -> dict:
        """
        Click on a UI element.

        Tries cached coordinates first (from last ui_get_window_tree call),
        then falls back to pywinauto element search.

        Args:
            automation_id: AutomationId property
            name: Element's Name/Title property
            control_type: Control type
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            # Try cache first (fast, reliable)
            if automation_id:
                ui = await _ensure_ui_connected()
                rect = ui.get_cached_rect(automation_id)
                if rect:
                    cx = (rect["left"] + rect["right"]) // 2
                    cy = (rect["top"] + rect["bottom"]) // 2
                    await ui._click_at_coords(cx, cy)
                    return build_response(
                        data={"clicked": True, "method": "cache", "position": {"x": cx, "y": cy}},
                        state=session.state.state,
                    )

            # Fallback to element search
            ui, element = await _find_ui_element(automation_id, name, control_type)
            await ui.click(element)
            return build_response(data={"clicked": True, "method": "element_search"}, state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_click_at(ctx: Context, x: int, y: int) -> dict:
        """Click at absolute screen coordinates.

        Use with ui_get_window_tree rectangle data when element search fails.
        Get coordinates from the 'rectangle' field in tree output.
        Click goes to the center: x = (left + right) / 2, y = (top + bottom) / 2

        Args:
            x: Screen X coordinate
            y: Screen Y coordinate
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            ui = await _ensure_ui_connected()
            await ui._click_at_coords(x, y)
            return build_response(
                data={"clicked": True, "position": {"x": x, "y": y}},
                state=session.state.state,
            )
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    # -- Screenshot & annotation tools --

    @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False))
    async def ui_take_screenshot(
        ctx: Context,
        max_width: int = 1024,
        format: str = "webp",
    ) -> dict:
        """Take a screenshot of the debugged application's window.

        Returns the screenshot as a WebP image optimized for LLM vision.
        Default 1024px width + WebP q=75 keeps size under MCP limits (~80-120KB).

        Use this to see the actual UI state — what the user would see.

        Useful for:
        - Verifying UI rendered correctly after a debug step
        - Finding elements that don't appear in the automation tree
        - Understanding visual layout and spacing
        - Debugging rendering issues

        Args:
            max_width: Maximum image width (default 1024 — optimal for Claude vision)
            format: Image format: "webp" (smallest), "jpeg", "png"
        """
        try:
            from ..ui.screenshot import get_hwnd_for_pid, capture_window_for_llm

            pid = session.state.process_id
            if not pid:
                return build_error_response("No debug process. Start debugging first.", state=session.state.state)

            hwnd = get_hwnd_for_pid(pid)
            if not hwnd:
                return build_error_response(
                    f"No visible window found for process {pid}. The app may not have a UI yet.",
                    state=session.state.state,
                )

            loop = asyncio.get_running_loop()
            result = await loop.run_in_executor(
                None, lambda: capture_window_for_llm(hwnd, max_width=max_width, format=format),
            )

            return build_response(data=result, state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False))
    async def ui_take_annotated_screenshot(
        ctx: Context,
        max_depth: int = 3,
        interactive_only: bool = True,
        max_width: int = 1024,
        format: str = "webp",
        compact: bool = True,
    ) -> dict:
        """Take a screenshot with numbered UI elements overlaid (Set-of-Mark pattern).

        Returns annotated WebP image + compact element index.
        Each interactive element gets a numbered label on the screenshot.

        Use ui_click_annotated(element_id) to interact with elements by number.

        Args:
            max_depth: How deep to traverse the UI tree (default 3)
            interactive_only: Only interactive elements (default True)
            max_width: Max image width (default 1024)
            format: Image format: "webp" (smallest), "jpeg", "png"
            compact: Compact element index — id+name only (default True, saves ~60KB)
        """
        nonlocal _last_annotation, _annotation_generation

        try:
            from ..ui.screenshot import (
                get_hwnd_for_pid, capture_window, collect_visible_elements, annotate_screenshot,
            )
            import ctypes
            from ctypes import wintypes

            pid = session.state.process_id
            if not pid:
                return build_error_response("No debug process.", state=session.state.state)

            hwnd = get_hwnd_for_pid(pid)
            if not hwnd:
                return build_error_response(
                    f"No visible window for process {pid}.",
                    state=session.state.state,
                )

            ui = _get_ui()
            if ui.process_id != pid:
                await ui.connect(pid)

            loop = asyncio.get_running_loop()

            # Capture screenshot
            png_bytes, width, height = await loop.run_in_executor(
                None, lambda: capture_window(hwnd),
            )

            # Collect elements
            elements = await loop.run_in_executor(
                None, lambda: collect_visible_elements(ui._app, max_depth, interactive_only),
            )

            # Get window screen position for coordinate conversion
            rect = wintypes.RECT()
            ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect))
            window_rect = (rect.left, rect.top, rect.right, rect.bottom)

            # Annotate screenshot (with optional downsampling)
            annotated_bytes = await loop.run_in_executor(
                None, lambda: annotate_screenshot(png_bytes, elements, window_rect, max_width),
            )

            # Cache elements for ui_click_annotated
            _annotation_generation += 1
            _last_annotation = {
                "elements": elements,
                "window_rect": window_rect,
                "hwnd": hwnd,
                "generation": _annotation_generation,
            }

            # Convert to optimal format
            from ..ui.screenshot import _process_screenshot
            img_bytes, final_w, final_h, mime = _process_screenshot(
                annotated_bytes, max_width=max_width, format=format,
            )

            import base64

            # Build element index — compact (id+name) or full (id+name+type+automationId)
            if compact:
                elem_index = [
                    f"{e['id']}: {e['name'] or e['automationId'] or e['type']}"
                    for e in elements
                ]
            else:
                elem_index = [
                    {
                        "id": e["id"],
                        "name": e["name"],
                        "type": e["type"],
                        "automationId": e["automationId"],
                    }
                    for e in elements
                ]

            return build_response(
                data={
                    "image": base64.b64encode(img_bytes).decode("ascii"),
                    "mimeType": mime,
                    "width": final_w,
                    "height": final_h,
                    "elements": elem_index,
                    "element_count": len(elements),
                    "generation": _annotation_generation,
                },
                state=session.state.state,
            )
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_click_annotated(ctx: Context, element_id: int, generation: int | None = None) -> dict:
        """Click an element by its ID from ui_take_annotated_screenshot.

        Uses the numbered element from the last annotated screenshot.
        Call ui_take_annotated_screenshot first to get element IDs.

        Args:
            element_id: Element ID number from the annotated screenshot
            generation: Generation counter from the screenshot response (optional, warns if stale)
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            if _last_annotation is None:
                return build_error_response(
                    "No annotation data. Call ui_take_annotated_screenshot first.",
                    state=session.state.state,
                )

            # Warn if annotation data may be stale
            stale_warning = None
            current_gen = _last_annotation.get("generation", 0)
            if generation is not None and generation != current_gen:
                stale_warning = (
                    f"Annotation data may be stale: requested generation {generation}, "
                    f"current is {current_gen}. Consider retaking the screenshot."
                )
                logger.warning(stale_warning)

            elements = _last_annotation["elements"]
            target = None
            for e in elements:
                if e["id"] == element_id:
                    target = e
                    break

            if target is None:
                return build_error_response(
                    f"Element {element_id} not found. Valid IDs: {[e['id'] for e in elements]}",
                    state=session.state.state,
                )

            # Click center of element bounds using centralized SendInput implementation
            bounds = target["bounds"]
            center_x = bounds["x"] + bounds["width"] // 2
            center_y = bounds["y"] + bounds["height"] // 2

            ui = await _ensure_ui_connected()
            await ui._click_at_coords(center_x, center_y)

            response_data: dict[str, Any] = {
                "clicked": True,
                "element": target["name"],
                "position": {"x": center_x, "y": center_y},
            }
            if stale_warning:
                response_data["warning"] = stale_warning

            return build_response(
                data=response_data,
                state=session.state.state,
            )
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    # -- Advanced interaction tools --

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_select_items(
        ctx: Context,
        automation_id: str,
        indices: list[int],
        mode: str = "replace",
    ) -> dict:
        """Select items by index in a list/grid control (DataGrid, ListView, ListBox).

        Two strategies (tries both):
        1. UIA SelectionItemPattern — works for non-virtualized lists
        2. Coordinate click fallback — clicks items using cached rectangles
           (Ctrl+click for multi-select, plain click for first item)

        For WPF virtualized lists (VirtualizingStackPanel), strategy 1 may fail
        because off-screen items don't have UI containers. Strategy 2 uses visible
        item coordinates from the cache.

        Args:
            automation_id: AutomationId of the list/grid control
            indices: List of 0-based item indices to select
            mode: "replace" (clear existing, select these) or "add" (add to existing selection)
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            ui = await _ensure_ui_connected()

            # Strategy 1: UIA SelectionItemPattern
            def _select_via_pattern() -> int:
                window = ui._app.top_window()
                control = window.child_window(auto_id=automation_id)
                control.wait("exists", timeout=5)

                items = control.children()
                selected_count = 0

                for i, item in enumerate(items):
                    try:
                        iface = item.iface_selection_item
                    except Exception:
                        continue

                    if i in indices:
                        if mode == "replace" and selected_count == 0:
                            iface.select()
                        else:
                            iface.add_to_selection()
                        selected_count += 1
                    elif mode == "replace":
                        try:
                            iface.remove_from_selection()
                        except Exception:
                            pass

                return selected_count

            loop = asyncio.get_running_loop()
            selected = await asyncio.wait_for(
                loop.run_in_executor(None, _select_via_pattern), timeout=10.0,
            )

            # Strategy 2: If pattern selected 0, try coordinate click on cached items
            if selected == 0:
                # Get children from cache — look for ListItem children of this control
                cache = ui._element_cache
                parent_rect = cache.get(automation_id, {}).get("rect")
                if parent_rect:
                    # Find child items by their position within parent
                    child_items = []
                    for aid, data in cache.items():
                        if aid == automation_id:
                            continue
                        r = data.get("rect")
                        ct = data.get("control_type", "")
                        if not r or ct not in ("ListItem", "DataItem", "TreeItem"):
                            continue
                        # Check if child is within parent bounds
                        if (parent_rect["left"] <= r["left"] <= parent_rect["right"]
                                and parent_rect["top"] <= r["top"] <= parent_rect["bottom"]):
                            child_items.append((aid, r))

                    # Click on items at requested indices
                    for idx_num, target_idx in enumerate(indices):
                        if target_idx < len(child_items):
                            _, rect = child_items[target_idx]
                            cx = (rect["left"] + rect["right"]) // 2
                            cy = (rect["top"] + rect["bottom"]) // 2
                            if idx_num == 0 and mode == "replace":
                                await ui._click_at_coords(cx, cy)
                            else:
                                # Ctrl+click for multi-select
                                import ctypes
                                VK_CONTROL = 0x11
                                KEYEVENTF_KEYUP = 0x0002
                                ctypes.windll.user32.keybd_event(VK_CONTROL, 0, 0, 0)
                                await ui._click_at_coords(cx, cy)
                                ctypes.windll.user32.keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0)
                            selected += 1

            return build_response(
                data={"selected": selected, "indices": indices, "mode": mode},
                state=session.state.state,
            )
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_right_click(
        ctx: Context,
        automation_id: str | None = None,
        name: str | None = None,
        control_type: str | None = None,
    ) -> dict:
        """Right-click on a UI element to open context menu.

        Tries cached coordinates first, then falls back to pywinauto element search.

        Args:
            automation_id: AutomationId property
            name: Element's Name/Title property
            control_type: Control type
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            # Try cache first
            if automation_id:
                ui = await _ensure_ui_connected()
                rect = ui.get_cached_rect(automation_id)
                if rect:
                    cx = (rect["left"] + rect["right"]) // 2
                    cy = (rect["top"] + rect["bottom"]) // 2
                    await ui._right_click_at_coords(cx, cy)
                    return build_response(
                        data={"right_clicked": True, "method": "cache", "position": {"x": cx, "y": cy}},
                        state=session.state.state,
                    )

            # Fallback to element search
            ui, element = await _find_ui_element(automation_id, name, control_type)

            def _right_click() -> None:
                element.click_input(button="right")

            loop = asyncio.get_running_loop()
            await asyncio.wait_for(
                loop.run_in_executor(None, _right_click), timeout=5.0,
            )

            return build_response(data={"right_clicked": True, "method": "element_search"}, state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_double_click(
        ctx: Context,
        automation_id: str | None = None,
        name: str | None = None,
        control_type: str | None = None,
    ) -> dict:
        """Double-click on a UI element.

        Tries cached coordinates first, then falls back to pywinauto element search.

        Args:
            automation_id: AutomationId property
            name: Element's Name/Title property
            control_type: Control type
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            # Try cache first
            if automation_id:
                ui = await _ensure_ui_connected()
                rect = ui.get_cached_rect(automation_id)
                if rect:
                    cx = (rect["left"] + rect["right"]) // 2
                    cy = (rect["top"] + rect["bottom"]) // 2
                    await ui._double_click_at_coords(cx, cy)
                    return build_response(
                        data={"double_clicked": True, "method": "cache", "position": {"x": cx, "y": cy}},
                        state=session.state.state,
                    )

            # Fallback to element search
            ui, element = await _find_ui_element(automation_id, name, control_type)

            def _double_click() -> None:
                element.double_click_input()

            loop = asyncio.get_running_loop()
            await asyncio.wait_for(
                loop.run_in_executor(None, _double_click), timeout=5.0,
            )

            return build_response(data={"double_clicked": True, "method": "element_search"}, state=session.state.state)
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_scroll(
        ctx: Context,
        automation_id: str,
        direction: str = "down",
        amount: int = 3,
    ) -> dict:
        """Scroll a UI control.

        Args:
            automation_id: AutomationId of the scrollable control
            direction: "up", "down", "left", "right"
            amount: Number of scroll units (default 3)
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            ui = await _ensure_ui_connected()

            def _scroll() -> None:
                window = ui._app.top_window()
                control = window.child_window(auto_id=automation_id)
                control.wait("exists", timeout=5)
                control.scroll(direction, "page", amount)

            loop = asyncio.get_running_loop()
            await asyncio.wait_for(
                loop.run_in_executor(None, _scroll), timeout=5.0,
            )

            return build_response(
                data={"scrolled": True, "direction": direction, "amount": amount},
                state=session.state.state,
            )
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)

    @mcp.tool(annotations=ToolAnnotations(openWorldHint=False))
    async def ui_drag(
        ctx: Context,
        from_automation_id: str | None = None,
        to_automation_id: str | None = None,
        from_x: int | None = None,
        from_y: int | None = None,
        to_x: int | None = None,
        to_y: int | None = None,
    ) -> dict:
        """Drag from one position to another.

        Two modes:
        1. By AutomationId: from_automation_id + to_automation_id (uses cached rectangles)
        2. By coordinates: from_x, from_y, to_x, to_y (absolute screen coords)

        For mode 1, call ui_get_window_tree first to populate cache.

        Args:
            from_automation_id: Source element AutomationId
            to_automation_id: Target element AutomationId
            from_x: Source X coordinate (screen absolute)
            from_y: Source Y coordinate
            to_x: Target X coordinate
            to_y: Target Y coordinate
        """
        try:
            access_error = check_session_access(ctx)
            if access_error:
                return build_error_response(access_error, state=session.state.state)

            ui = await _ensure_ui_connected()

            # Resolve coordinates from automation IDs if needed
            fx, fy, tx, ty = from_x, from_y, to_x, to_y

            if from_automation_id and not (fx and fy):
                from_rect = ui.get_cached_rect(from_automation_id)
                if from_rect:
                    fx = (from_rect["left"] + from_rect["right"]) // 2
                    fy = (from_rect["top"] + from_rect["bottom"]) // 2
                else:
                    return build_error_response(
                        f"Element '{from_automation_id}' not in cache. Call ui_get_window_tree first.",
                        state=session.state.state,
                    )

            if to_automation_id and not (tx and ty):
                to_rect = ui.get_cached_rect(to_automation_id)
                if to_rect:
                    tx = (to_rect["left"] + to_rect["right"]) // 2
                    ty = (to_rect["top"] + to_rect["bottom"]) // 2
                else:
                    return build_error_response(
                        f"Element '{to_automation_id}' not in cache. Call ui_get_window_tree first.",
                        state=session.state.state,
                    )

            if not all([fx, fy, tx, ty]):
                return build_error_response(
                    "Provide either automation_ids or coordinates for both source and target.",
                    state=session.state.state,
                )

            await ui._drag_at_coords(fx, fy, tx, ty)

            return build_response(
                data={"dragged": True, "from": {"x": fx, "y": fy}, "to": {"x": tx, "y": ty}},
                state=session.state.state,
            )
        except Exception as e:
            return build_error_response(str(e), state=session.state.state)
