"""Bridge tools — MCP wrappers for runtime communication with the Godot addon."""

from __future__ import annotations

from mcp.server.fastmcp import Context

from godotiq.server import mcp
from godotiq.bridge.connection import BridgeConnectionError, BridgeTimeoutError, BridgeError

from godotiq.tools.bridge.screenshot import screenshot as _screenshot
from godotiq.tools.bridge.run import run as _run
from godotiq.tools.bridge.perf_snapshot import perf_snapshot as _perf_snapshot
from godotiq.tools.bridge.editor_context import editor_context as _editor_context
from godotiq.tools.bridge.scene_tree import scene_tree as _scene_tree
from godotiq.tools.bridge.node_ops import node_ops as _node_ops
from godotiq.tools.bridge.script_ops import script_ops as _script_ops
from godotiq.tools.bridge.file_ops import file_ops as _file_ops
from godotiq.tools.bridge.input_sim import input_sim as _input_sim
from godotiq.tools.bridge.exec_code import exec_code as _exec_code
from godotiq.tools.bridge.state_inspect import state_inspect as _state_inspect
from godotiq.tools.bridge.nav_query import nav_query as _nav_query
from godotiq.tools.bridge.watch import watch as _watch
from godotiq.tools.bridge.undo_history import undo_history as _undo_history
from godotiq.tools.bridge.save_scene import save_scene as _save_scene
from godotiq.tools.bridge.camera import camera as _camera
from godotiq.tools.bridge.ui_map import ui_map as _ui_map
from godotiq.tools.bridge.build_scene import build_scene as _build_scene
from godotiq.tools.bridge.check_errors import check_errors as _check_errors
from godotiq.tools.bridge.verify_motion import verify_motion as _verify_motion
from godotiq.tools.output_limit import apply_output_limit, _CHAR_LIMITS


@mcp.tool(
    name="godotiq_screenshot",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_screenshot(
    viewport: str = "game",
    scale: float = 0.25,
    format: str = "webp",
    quality: float = 0.5,
    camera_position: list[float] | None = None,
    camera_target: list[float] | None = None,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Visual verification of the game or editor viewport. Use scale=0.25, quality=0.3 for token-efficient checks. Prefer state_inspect for data — use screenshot only when you need visual confirmation.

    Args:
        viewport: "game" (requires running game) or "editor" (3D editor, no game needed).
        scale: Image scale (0.0-1.0). Default 0.25 for token efficiency.
        format: "webp" (default, smallest), "png", "jpg".
        quality: Encoding quality (0.1-1.0). Use 0.3 for routine checks, 0.8 for detail.
        camera_position: [x, y, z] — Editor only. Move camera before capture.
        camera_target: [x, y, z] — Editor only. Camera looks at this point.
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        return await _screenshot(
            viewport=viewport, scale=scale, format=format, quality=quality,
            camera_position=camera_position, camera_target=camera_target,
        )
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The game may not be running or the operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_run",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_run(
    action: str = "play",
    scene: str = "main",
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Start or stop the Godot game. Must call play before using any game-side tools (state_inspect, screenshot, input, nav_query, watch, ui_map). Waits for confirmed startup.

    Args:
        action: "play" to start, "stop" to halt the running game.
        scene: "main" (default), "current", or a res:// path.
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        return await _run(action=action, scene=scene)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The game may not be running or the operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_perf_snapshot",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_perf_snapshot(
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """FPS, draw calls, memory, node count from the running game. Use for performance profiling. Game must be running.

    Args:
        detail: "brief" (fps + draw_calls only), "normal" (all metrics), "full".
    """
    try:
        return await _perf_snapshot(detail=detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The game may not be running or the operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_editor_context",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def godotiq_editor_context(detail: str = "normal", ctx: Context = None) -> dict:
    """Call FIRST alongside project_summary. Returns editor state: open scenes, selected nodes, is game running, project path. Essential for understanding current context before any operations.

    Args:
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        result = await _editor_context()
        return apply_output_limit(result, detail)
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_scene_tree",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def godotiq_scene_tree(
    root: str = "",
    depth: int = 3,
    filter_type: str = "",
    include: list[str] | None = None,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Live editor scene tree — actual state with transforms, scripts, groups, visibility. Unlike scene_map (reads .tscn from disk), this reads the editor's live state. Call BEFORE node_ops to see current node names and paths.

    Args:
        root: Node path to start from (e.g. "Level/Enemies"). Empty = scene root.
        depth: Max tree depth (1-10, default 3).
        filter_type: Only include nodes of this type (e.g. "MeshInstance3D").
        include: Data fields per node. Default: ["transform","script","groups","visibility"].
        detail: "brief" (names + types, max 20 nodes), "normal" (default), "full" (+ script paths, signals).
    """
    try:
        return await _scene_tree(root=root, depth=depth, filter_type=filter_type, include=include, detail=detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The editor may not be responding or the scene is too large."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_node_ops",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_node_ops(
    operations: list[dict] | None = None,
    scene: str | None = None,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Core scene editing tool. Batch operations with Ctrl+Z undo: move, rotate, scale, set_property, add_child, delete, duplicate, reparent. All ops in one call = one undo action. ALWAYS add validate:true for spatial operations (move/scale/add_child) to prevent placing objects inside walls or on top of each other. If ANY validated op is BLOCKED, the entire batch is rejected (atomic).

    Args:
        operations: List of operation dicts, each with 'op' key. Max 50 per call.
        scene: Optional .tscn path for spatial validation (auto-detects main scene if omitted).
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        scene_data = None
        ops = operations or []

        # Resolve scene data for validation if any op has validate:true
        if any(op.get("validate") for op in ops):
            scene_data = _resolve_scene_data(ctx, scene)

        result = await _node_ops(operations=ops, scene_data=scene_data)
        return apply_output_limit(result, detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The operation timed out. Try fewer operations per batch."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except ValueError as e:
        return {"error": str(e)}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


def _resolve_scene_data(ctx: Context | None, scene: str | None) -> dict | None:
    """Resolve scene spatial data for validation. Returns None on any failure."""
    try:
        if ctx is None:
            return None
        session = ctx.request_context.lifespan_context.get("session")
        if session is None:
            return None

        # Determine res:// path
        if scene:
            res_path = session.to_res_path(scene)
        else:
            from godotiq.tools.memory import _read_main_scene
            res_path = _read_main_scene(session)
            if not res_path:
                return None

        result = session.resolve_scene_spatial(res_path)
        if result is None:
            return None

        _, world_nodes = result
        return {"nodes": [
            {
                "name": wn.node.name,
                "type": wn.node.type,
                "position": list(wn.world_position),
                "scale": list(wn.world_scale),
                "depth": wn.depth,
            }
            for wn in world_nodes
        ]}
    except Exception:
        return None


@mcp.tool(
    name="godotiq_script_ops",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": False,
    },
)
async def godotiq_script_ops(
    op: str = "read",
    path: str = "",
    content: str = "",
    patches: list[dict] | None = None,
    validate_before_write: bool = True,
    auto_fix: bool = False,
    dry_run: bool = False,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Read, write, or patch GDScript files with convention validation. ALWAYS prefer this over raw file writes for .gd files. Patch mode (find-and-replace) is safest — only changes what you specify.

    Args:
        op: "read", "write", "patch", or "create".
        path: File path (res:// or relative).
        content: File content for write/create.
        patches: List of {search, replace} dicts for patch mode.
        validate_before_write: Run convention checks before writing (default true).
        auto_fix: Auto-fix detected issues.
        dry_run: Validate without writing.
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        return await _script_ops(
            op=op,
            path=path,
            content=content,
            patches=patches,
            validate_before_write=validate_before_write,
            auto_fix=auto_fix,
            dry_run=dry_run,
        )
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_file_ops",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": False,
    },
)
async def godotiq_file_ops(
    op: str = "list",
    path: str = "",
    query: str = "",
    filter: str = "all",
    recursive: bool = False,
    context_lines: int = 2,
    max_depth: int = 3,
    show_sizes: bool = False,
    content: str = "",
    destination: str = "",
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Filesystem operations within the Godot project: list, read, write, move, delete, search, tree, uid_to_path, path_to_uid, rename (with reference updates). Respects protected files from .godotiq.json. For .gd files, prefer script_ops instead.

    Args:
        op: "list", "read", "write", "move", "delete", "search", "tree", "uid_to_path", "path_to_uid", "rename".
        path: Target path (res:// or relative).
        query: Search query (for search) or uid string (for uid_to_path).
        filter: Type filter ("all", "scenes", "scripts", "images", "audio", etc.).
        recursive: Include subdirectories in list.
        context_lines: Context around search matches (default 2).
        max_depth: Tree depth (default 3).
        show_sizes: Include file sizes in tree.
        content: File content for write. For rename: "false" to skip ref updates.
        destination: Destination for move/rename.
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        result = await _file_ops(
            op=op,
            path=path,
            query=query,
            filter=filter,
            recursive=recursive,
            context_lines=context_lines,
            max_depth=max_depth,
            show_sizes=show_sizes,
            content=content,
            destination=destination,
        )
        # Special case: truncate large string content for read ops
        if op == "read" and "content" in result and "error" not in result:
            limit = _CHAR_LIMITS.get(detail, _CHAR_LIMITS["normal"])
            if limit is not None and isinstance(result["content"], str) and len(result["content"]) > limit:
                original_len = len(result["content"])
                result["content"] = result["content"][:max(limit - 200, 0)]
                result["content_truncated"] = True
                result["content_original_chars"] = original_len
        return apply_output_limit(result, detail)
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_input",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_input(
    commands: list[dict] | None = None,
    track_side_effects: bool = False,
    continue_on_error: bool = False,
    wait_for: str = "",
    wait_for_timeout_ms: int = 5000,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Simulate player input in the running game: actions, keys, UI taps, waits. Use after ui_map to know what's on screen. Supports signal verification and side-effect tracking. Game must be running.

    Args:
        commands: Sequential list: {"actions": ["move_left"], "hold_ms": 500}, {"wait_ms": 200}, {"key": "space"}, {"tap": "AcceptButton"}.
        track_side_effects: Monitor autoload state changes before/after.
        continue_on_error: Keep going if a command fails.
        wait_for: Signal to wait for (e.g. "signal:OrderManager.order_accepted").
        wait_for_timeout_ms: Timeout for wait_for in ms.
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        result = await _input_sim(
            commands=commands,
            track_side_effects=track_side_effects,
            continue_on_error=continue_on_error,
            wait_for=wait_for,
            wait_for_timeout_ms=wait_for_timeout_ms,
        )
        return apply_output_limit(result, detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "Game may not be running or input simulation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_exec",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_exec(
    code: str = "",
    context: str = "game",
    timeout_ms: int = 5000,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Execute GDScript code. Last resort — prefer dedicated tools (node_ops, script_ops, state_inspect) over exec. Code MUST contain 'func run():'. In game context use Engine.get_main_loop() instead of get_tree().

    Args:
        code: GDScript with 'func run():'. Return value is stringified.
        context: "game" (sandbox, game must run) or "editor" (full EditorInterface access, no game needed).
        timeout_ms: Max execution time in ms (default 5000).
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        return await _exec_code(code=code, context=context, timeout_ms=timeout_ms)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "Game may not be running or execution timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_state_inspect",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def godotiq_state_inspect(
    queries: list[dict] | None = None,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Query runtime property values. CHEAPER than screenshots for checking state — use this first. Supports autoload lookup by name, node paths, nested properties, method calls like 'pending_orders.size()'. Game must be running.

    Args:
        queries: [{node: "/root/Main/Player", properties: ["health", "position"]}] or [{autoload: "EconomyManager", properties: ["balance"]}].
        detail: "brief" (values only, no metadata), "normal" (default), "full" (+ class name, node path).
    """
    try:
        return await _state_inspect(queries=queries, detail=detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "Game may not be running or query timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_nav_query",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def godotiq_nav_query(
    from_node: str = "",
    to_node: str = "",
    from_position: list[float] | None = None,
    to_position: list[float] | None = None,
    optimize: bool = True,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Live pathfinding via NavigationServer3D. "Can A reach B?" Returns path, distance, reachability, navmesh status. Game must be running.

    Args:
        from_node: Source node name (uses its global_position).
        to_node: Target node name.
        from_position: [x, y, z] alternative to from_node.
        to_position: [x, y, z] alternative to to_node.
        optimize: Optimize path (default true).
        detail: "brief" (reachable + distance only), "normal" (+ path points), "full".
    """
    try:
        return await _nav_query(
            from_node=from_node,
            to_node=to_node,
            from_position=from_position,
            to_position=to_position,
            optimize=optimize,
            detail=detail,
        )
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "Game may not be running or the operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_watch",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_watch(
    action: str = "read",
    watches: list[dict] | None = None,
    sample_interval_ms: int = 500,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Persistent property monitoring. Start watching, then read accumulated changes over time. Use for tracking values across game interactions. Game must be running.

    Args:
        action: "start", "stop", "read", "clear".
        watches: For "start": [{node: "/root/Main/Player", properties: ["health", "position"]}].
        sample_interval_ms: Sampling interval (default 500, min 50).
        detail: "brief" (last 10 events only), "normal" (default), "full" (all events + timestamps).
    """
    try:
        result = await _watch(action=action, watches=watches, sample_interval_ms=sample_interval_ms, detail=detail)
        return apply_output_limit(result, detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "Game may not be running or the operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_undo_history",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def godotiq_undo_history(detail: str = "normal", ctx: Context = None) -> dict:
    """Review what was changed. Shows undo/redo state and recent GodotIQ action history. Call after node_ops to verify changes. Editor-side — no game needed.

    Args:
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        result = await _undo_history()
        return apply_output_limit(result, detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_save_scene",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,
    },
)
async def godotiq_save_scene(detail: str = "normal", ctx: Context = None) -> dict:
    """Persist editor changes to disk. Call after node_ops or any scene modifications. In-place save. Editor-side — no game needed.

    Args:
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        return await _save_scene()
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_camera",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,
    },
)
async def godotiq_camera(
    action: str = "get_position",
    position: list[float] | None = None,
    target: list[float] | None = None,
    node: str = "",
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Editor 3D camera control: get current position, reposition, or focus on a node. Editor-side — no game needed.

    Args:
        action: "get_position", "look_at", or "focus_node".
        position: [x, y, z] for look_at — camera position.
        target: [x, y, z] for look_at — camera target.
        node: Node name for focus_node.
        detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
    """
    try:
        return await _camera(action=action, position=position, target=target, node=node)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The operation timed out."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_ui_map",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_ui_map(
    root: str = "",
    include_invisible: bool = False,
    max_depth: int = 10,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Map all UI elements on screen: positions, text, interactivity, visibility. Call BEFORE godotiq_input to know what buttons/controls exist. Game must be running.

    Args:
        root: Root node name (e.g. "GameHUD"). Empty = entire UI tree.
        include_invisible: Include hidden elements (default false).
        max_depth: Tree traversal depth (default 10).
        detail: "brief" (interactive elements only + count), "normal" (default), "full" (+ modulate, mouse_filter, script).
    """
    try:
        return await _ui_map(
            root=root,
            include_invisible=include_invisible,
            max_depth=max_depth,
            detail=detail,
        )
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The game must be running. Start it with godotiq_run first."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_build_scene",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_build_scene(
    target_scene: str = "",
    parent: str = "",
    detail: str = "brief",
    grid: dict | None = None,
    line: dict | None = None,
    scatter: dict | None = None,
    nodes: list[dict] | None = None,
    ctx: Context = None,
) -> dict:
    """Create multiple nodes in a scene using high-level patterns. One call = many nodes with Ctrl+Z undo. Use INSTEAD of repeated node_ops add_child calls.

    Modes (exactly one required):
        grid: Place nodes in a rows x cols grid. Keys: rows (int), cols (int), spacing (float, default 1.0), scene (res:// path), type (node type).
        line: Place nodes along a polyline path. Keys: points (list of [x,y,z]), spacing (float, default 1.0), scene, type.
        scatter: Place nodes at explicit positions. Keys: items (list of {scene, position, rotation, scale}).
        nodes: Explicit node list. Each dict: {type, name, position, rotation, scale, properties, scene}.

    Args:
        target_scene: .tscn path for context (auto-detects if empty).
        parent: Parent node path (e.g. "Level/Props"). Empty = scene root.
        detail: Output verbosity -- "brief" (default), "normal", "full".
        grid: Grid pattern config.
        line: Line pattern config.
        scatter: Scatter placement config.
        nodes: Explicit node definitions.
    """
    try:
        result = await _build_scene(
            grid=grid, line=line, scatter=scatter, nodes=nodes,
            parent=parent, target_scene=target_scene,
        )
        if "error" in result:
            return result
        return apply_output_limit(result, detail, list_keys=["errors"])
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "Build may have timed out. Check editor for partial results."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_check_errors",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def godotiq_check_errors(scope: str = "scene", ctx: Context = None) -> dict:
    """Check GDScript files for compilation/parse errors.

    Args:
        scope: "scene" (current scene + autoloads), "project" (all .gd files),
               or "res://path/to/script.gd" (single file)
    """
    try:
        result = await _check_errors(scope=scope)
        return apply_output_limit(result, "normal")
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The operation timed out. Try a smaller scope."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}


@mcp.tool(
    name="godotiq_verify_motion",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_verify_motion(
    node: str,
    property_name: str = "position",
    duration: float = 2.0,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Verify a node property changes over time (proves movement/animation).

    Takes two state_inspect snapshots separated by a sleep and compares values.
    Use instead of screenshots to verify motion. Game must be running.

    Args:
        node: Node path or name to inspect.
        property_name: Property to monitor (default "position").
        duration: Seconds to wait between snapshots (default 2.0).
        detail: Output verbosity — "brief", "normal", or "full".
    """
    try:
        result = await _verify_motion(
            node=node, property_name=property_name, duration=duration,
        )
        return apply_output_limit(result, detail)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The operation timed out. The game may have stopped."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}
