"""Core commands: capture, list, see, find, learn, tools.

Implements the Phase 1 "See" CLI commands for screen capture,
window listing, UI element tree inspection, and element search.
"""
from __future__ import annotations

import json as json_module
import platform

import click

from naturo.cli.error_helpers import json_error as _json_error_str
from naturo.errors import WindowNotFoundError
from naturo.cli.fuzzy_group import FuzzyGroup


def _get_backend(json_output: bool = False):
    """Get the platform-appropriate backend.

    Performs a pre-flight check for an interactive desktop session on Windows
    so that see/find/capture give the same clear error as click/type/press
    instead of a vague 'No window found' message.

    Args:
        json_output: When True, emit JSON-formatted error and sys.exit
            instead of raising an exception for NoDesktopSessionError.

    Returns:
        A Backend instance for the current platform.

    Raises:
        click.UsageError: If no interactive desktop session or no backend.
    """
    from naturo.cli.interaction import _check_desktop_session
    try:
        _check_desktop_session()
    except Exception as exc:
        if json_output:
            click.echo(_json_error_str("NO_DESKTOP_SESSION", str(exc)))
            raise SystemExit(1)
        raise click.UsageError(str(exc))
    from naturo.backends.base import get_backend
    return get_backend()


def _platform_supports_gui() -> bool:
    """Check if the current platform has a GUI automation backend.

    Returns:
        True if Windows or macOS with Peekaboo installed.
    """
    system = platform.system()
    if system == "Windows":
        return True
    if system == "Darwin":
        import shutil
        return shutil.which("peekaboo") is not None
    return False








def _platform_error_msg(feature: str) -> str:
    """Build a user-friendly platform error message.

    Args:
        feature: Description of the feature (e.g. 'Screen capture').

    Returns:
        Error message string.
    """
    system = platform.system()
    if system == "Darwin":
        return (
            f"{feature} requires Peekaboo on macOS. "
            "Install it from https://github.com/AcePeak/peekaboo"
        )
    if system == "Linux":
        return f"{feature} is not yet supported on Linux (coming in Phase 7)."
    return f"{feature} is not supported on {system}."


# ── capture ─────────────────────────────────────


@click.command("capture")
@click.option("--app", help="Target application (name or partial match)")
@click.option("--window", "window_title", default=None, help="Window title pattern (substring match)")
@click.option("--window-title", "window_title", default=None, hidden=True, help="")
@click.option("--hwnd", type=int, default=None, help="Window handle (HWND)")
@click.option("--screen", "-s", type=int, default=0, help="Screen/monitor index")
@click.option("--path", "-p", "-o", default=None, help="Output file path (default: capture.<format>)")
@click.option("--format", "fmt", type=click.Choice(["png", "jpg", "bmp"]), default="png", help="Image format (default: png)")
@click.option("--element", "-e", "--ref", "element_ref", default=None,
              help="Crop to element ref (eN) from most recent snapshot, e.g. --element e5 or --ref e5")
@click.option("--region", default=None, metavar="X,Y,W,H",
              help="Crop to region: x,y,width,height (e.g. --region 100,50,400,300)")
@click.option("--padding", type=int, default=0,
              help="Extra padding (px) added around --element or --region crop")
@click.option("--snapshot/--no-snapshot", "store_snapshot", default=True, help="Store result in snapshot (default: on)")
@click.option("--session", default=None, envvar="NATURO_SESSION",
              help="Snapshot session for isolation (default: NATURO_SESSION env or 'default')")
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
def capture(app, window_title, hwnd, screen, path, fmt, store_snapshot, session,
            element_ref, region, padding, json_output):
    """Capture a live screenshot, optionally cropped to an element or region.

    Captures the screen or a specific window and saves to a file.
    Use --hwnd to capture a specific window, or --screen to select a monitor.
    The screenshot is automatically stored in a snapshot (use --no-snapshot to skip).
    Output format is PNG by default (matching Peekaboo).

    Use --element eN (or --ref eN) to crop to a specific element from the last 'naturo see' snapshot.
    Use --region X,Y,W,H to crop to arbitrary coordinates.
    Use --padding N to add N pixels of padding around the crop area.

    \b
    Examples:
        naturo capture                                  # full screen
        naturo capture --element e5                     # crop to element e5
        naturo capture --ref e5 --padding 20            # with 20px padding
        naturo capture --region 100,50,400,300          # crop to region
        naturo capture --app feishu --element e12       # element in specific app
        naturo capture -o output.png                    # save to output.png
    """
    if not _platform_supports_gui():
        msg = _platform_error_msg("Screen capture")
        if json_output:
            click.echo(_json_error_str("PLATFORM_ERROR", msg))
        else:
            click.echo(msg)
        raise SystemExit(1)

    # Resolve output path: use --path if given, else timestamped name
    if path is None:
        from datetime import datetime
        timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        app_label = (app or window_title or "screen").lower().replace(" ", "-")
        path = f"naturo-{app_label}-{timestamp}.{fmt}"

    try:
        backend = _get_backend(json_output)
        if hwnd or app or window_title:
            # Resolve app/window_title to hwnd if needed
            target_hwnd = hwnd
            if not target_hwnd and hasattr(backend, '_resolve_hwnd'):
                target_hwnd = backend._resolve_hwnd(app=app, window_title=window_title)
            result = backend.capture_window(hwnd=target_hwnd or 0, output_path=path)
        else:
            # Validate screen index against available monitors
            if screen < 0:
                msg = f"--screen must be >= 0, got {screen}"
                if json_output:
                    click.echo(_json_error_str("INVALID_INPUT", msg))
                else:
                    click.echo(f"Error: {msg}", err=True)
                raise SystemExit(1)
            try:
                monitors = backend.list_monitors()
                if monitors and screen >= len(monitors):
                    msg = f"Screen index {screen} out of range (0-{len(monitors) - 1}). Use 'naturo list screens' to see available monitors."
                    if json_output:
                        click.echo(_json_error_str("INVALID_INPUT", msg))
                    else:
                        click.echo(f"Error: {msg}", err=True)
                    raise SystemExit(1)
            except NotImplementedError:
                pass  # Non-Windows: skip validation, let backend handle it
            result = backend.capture_screen(screen_index=screen, output_path=path)

        # ── Element / region crop (issue #160) ───────────────────────────────
        crop_box = None  # (left, top, right, bottom) in image coordinates

        # Track whether the capture is window-relative (for element crop offset)
        _is_window_capture = bool(hwnd or app or window_title)

        if element_ref:
            # Resolve eN ref → bounds from most recent snapshot
            from naturo.snapshot import get_snapshot_manager
            _mgr = get_snapshot_manager(session=session)
            resolved = _mgr.resolve_ref(element_ref)
            if resolved is None:
                msg = (
                    f"Element ref '{element_ref}' not found in recent snapshots. "
                    "Run 'naturo see' first to create a snapshot."
                )
                if json_output:
                    click.echo(_json_error_str("REF_NOT_FOUND", msg))
                else:
                    click.echo(f"Error: {msg}", err=True)
                raise SystemExit(1)
            cx, cy, _snap_id = resolved
            el_result = _mgr.resolve_ref_element(element_ref)
            if el_result:
                element, snap_id = el_result
                ex, ey, ew, eh = element.frame

                # Check for zero-size bounds (cannot crop)
                if ew <= 0 or eh <= 0:
                    msg = (
                        f"Element {element_ref} (role={element.role!r} name={element.name!r}) "
                        f"has zero-size bounds ({ew}x{eh}) at ({ex},{ey}) and cannot be cropped. "
                        "This element may be off-screen, hidden, or in a virtualized container."
                    )
                    if json_output:
                        click.echo(_json_error_str("ZERO_SIZE_ELEMENT", msg))
                    else:
                        click.echo(f"Error: {msg}", err=True)
                    raise SystemExit(1)

                # When capturing a specific window (--app/--hwnd), the
                # screenshot is window-relative (origin 0,0) but element
                # coords from the snapshot are screen-absolute.  Subtract
                # the window origin so the crop aligns with the image.
                win_offset_x, win_offset_y = 0, 0
                if _is_window_capture:
                    # Try 1: query current window rect via Win32 API
                    try:
                        import platform as _plat
                        _cap_hwnd = target_hwnd if target_hwnd else 0
                        if _cap_hwnd and _plat.system() == "Windows":
                            import ctypes
                            import ctypes.wintypes as wt
                            rect = wt.RECT()
                            if ctypes.windll.user32.GetWindowRect(
                                _cap_hwnd, ctypes.byref(rect)
                            ):
                                win_offset_x = rect.left
                                win_offset_y = rect.top
                    except Exception:
                        pass
                    # Try 2: fall back to snapshot window_bounds
                    if win_offset_x == 0 and win_offset_y == 0:
                        try:
                            snap_data = _mgr.get_snapshot(snap_id)
                            if snap_data.window_bounds:
                                win_offset_x = snap_data.window_bounds[0]
                                win_offset_y = snap_data.window_bounds[1]
                        except Exception:
                            pass  # Best-effort; absolute coords as last resort

                crop_box = (
                    max(0, ex - win_offset_x - padding),
                    max(0, ey - win_offset_y - padding),
                    ex - win_offset_x + ew + padding,
                    ey - win_offset_y + eh + padding,
                )
        elif region:
            # Parse X,Y,W,H
            try:
                parts = [int(v.strip()) for v in region.split(",")]
                if len(parts) != 4:
                    raise ValueError("need 4 values")
                rx, ry, rw, rh = parts
                crop_box = (
                    max(0, rx - padding),
                    max(0, ry - padding),
                    rx + rw + padding,
                    ry + rh + padding,
                )
            except (ValueError, TypeError) as exc:
                msg = f"--region must be X,Y,W,H (e.g. 100,50,400,300): {exc}"
                if json_output:
                    click.echo(_json_error_str("INVALID_INPUT", msg))
                else:
                    click.echo(f"Error: {msg}", err=True)
                raise SystemExit(1)

        # Apply crop if requested
        if crop_box is not None:
            try:
                from PIL import Image as _PILImage
                img = _PILImage.open(result.path)
                # Clamp to image bounds
                iw, ih = img.size
                left = max(0, crop_box[0])
                top = max(0, crop_box[1])
                right = min(iw, crop_box[2])
                bottom = min(ih, crop_box[3])
                if right <= left or bottom <= top:
                    msg = f"Crop region ({left},{top},{right},{bottom}) has zero size."
                    if json_output:
                        click.echo(_json_error_str("INVALID_INPUT", msg))
                    else:
                        click.echo(f"Error: {msg}", err=True)
                    raise SystemExit(1)
                cropped = img.crop((left, top, right, bottom))
                cropped.save(result.path)
                # Update result dimensions (best-effort via dataclass copy)
                from dataclasses import replace as _dc_replace
                result = _dc_replace(result, width=right - left, height=bottom - top)
            except ImportError:
                msg = "Pillow required for --element/--region crop. Install: pip install naturo[annotate]"
                if json_output:
                    click.echo(_json_error_str("MISSING_DEPENDENCY", msg))
                else:
                    click.echo(f"Error: {msg}", err=True)
                raise SystemExit(1)

        snapshot_id = None
        if store_snapshot:
            from naturo.snapshot import get_snapshot_manager
            mgr = get_snapshot_manager(session=session)
            snapshot_id = mgr.create_snapshot()
            metadata = {
                "window_handle": hwnd,
                "application_name": app,
                "window_title": window_title,
            }
            mgr.store_screenshot(snapshot_id, result.path, metadata)

        if json_output:
            out: dict = {
                "success": True,
                "path": result.path,
                "width": result.width,
                "height": result.height,
                "format": result.format,
                "scale_factor": result.scale_factor,
                "dpi": result.dpi,
            }
            if crop_box is not None:
                out["cropped"] = True
                out["crop_source"] = "element" if element_ref else "region"
            if snapshot_id:
                out["snapshot_id"] = snapshot_id
            click.echo(json_module.dumps(out))
        else:
            import os
            full_path = os.path.abspath(result.path)
            crop_note = ""
            if element_ref:
                crop_note = f" [cropped to {element_ref}]"
            elif region:
                crop_note = f" [cropped to {region}]"
            click.echo(f"Saved: {full_path} ({result.width}x{result.height}){crop_note}")
    except WindowNotFoundError as e:
        if json_output:
            click.echo(_json_error_str("WINDOW_NOT_FOUND", str(e)))
        else:
            click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)
    except Exception as e:
        if json_output:
            click.echo(_json_error_str("CAPTURE_ERROR", str(e)))
        else:
            click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)


# ── list ────────────────────────────────────────


@click.group("list", cls=FuzzyGroup)
def list_cmd():
    """List apps, windows, screens, or permissions."""
    pass


@list_cmd.command()
@click.option("--all", "show_all", is_flag=True, help="Show all processes (not just apps with windows)")
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
@click.pass_context
def apps(ctx, show_all, json_output):
    """List running applications (delegates to 'app list')."""
    from naturo.cli.app_cmd import app_list
    ctx.invoke(app_list, show_all=show_all, json_output=json_output)


@list_cmd.command()
@click.option("--app", help="Target application (name or partial match)")
@click.option("--process-name", "app", default=None, hidden=True, help="")
@click.option("--pid", type=int, help="Process ID")
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
def windows(app, pid, json_output):
    """List open windows.

    Shows all visible top-level windows with their handles, titles,
    process names, and dimensions.
    """
    if not _platform_supports_gui():
        msg = _platform_error_msg("Window listing")
        if json_output:
            click.echo(_json_error_str("PLATFORM_ERROR", msg))
        else:
            click.echo(msg)
        raise SystemExit(1)

    try:
        backend = _get_backend(json_output)
        win_list = backend.list_windows()

        # Exclude own process and parent (terminal) to avoid matching
        # the terminal running the command (#358)
        import os as _os
        _own_pid = _os.getpid()
        _parent_pid = _os.getppid()
        win_list = [w for w in win_list if w.pid not in (_own_pid, _parent_pid)]

        # Apply filters
        if app:
            app_lower = app.lower()
            win_list = [w for w in win_list
                        if app_lower in w.title.lower()
                        or app_lower in w.process_name.lower()]
        if pid:
            win_list = [w for w in win_list if w.pid == pid]

        # Warn if empty result on Windows (may indicate no desktop session)
        if not win_list and platform.system() == "Windows":
            import os
            session_warning = ""
            session_id = os.environ.get("SESSIONNAME", "")
            if not session_id or session_id.lower() == "services":
                session_warning = " (Warning: no interactive desktop session detected — running via SSH or service?)"
            click.echo(
                f"Warning: no windows found{session_warning}",
                err=True,
            )

        if json_output:
            data = [
                {
                    "hwnd": w.handle,
                    "title": w.title,
                    "process_name": w.process_name,
                    "pid": w.pid,
                    "x": w.x,
                    "y": w.y,
                    "width": w.width,
                    "height": w.height,
                    "is_visible": w.is_visible,
                    "is_minimized": w.is_minimized,
                }
                for w in win_list
            ]
            click.echo(json_module.dumps({"success": True, "windows": data}, indent=2))
        else:
            if not win_list:
                click.echo("No windows found.")
                return
            # Table-like output
            click.echo(f"{'HWND':<16} {'PID':<8} {'SIZE':<14} {'TITLE'}")
            click.echo("-" * 70)
            for w in win_list:
                size = f"{w.width}x{w.height}"
                title = w.title[:40] if len(w.title) > 40 else w.title
                click.echo(f"{w.handle:<16} {w.pid:<8} {size:<14} {title}")
            click.echo(f"\n{len(win_list)} windows found.")
    except Exception as e:
        click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)


@list_cmd.command()
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
def screens(json_output):
    """List connected screens/monitors.

    Shows monitor index, resolution, position, DPI scale factor, and
    whether the monitor is the primary display.  On Windows, shows the
    human-readable monitor model name when available (#359).
    """
    try:
        backend = _get_backend(json_output)
        monitors = backend.list_monitors()

        if json_output:
            items = []
            for m in monitors:
                # (#359) Use model_name as 'name', keep device path separate
                display_name = getattr(m, "model_name", None) or m.name
                item = {
                    "index": m.index,
                    "name": display_name,
                    "device_path": getattr(m, "device_path", None) or m.name,
                    "x": m.x,
                    "y": m.y,
                    "width": m.width,
                    "height": m.height,
                    "is_primary": m.is_primary,
                    "scale_factor": m.scale_factor,
                    "dpi": m.dpi,
                }
                if m.work_area:
                    item["work_area"] = m.work_area
                items.append(item)
            click.echo(json_module.dumps({"success": True, "monitors": items}, indent=2))
        else:
            from naturo.cli.table import print_table

            if not monitors:
                click.echo("No monitors detected.")
                return

            headers = ["Index", "Name", "Resolution", "Position", "Scale", "DPI", "Primary"]
            rows = []
            for m in monitors:
                display_name = getattr(m, "model_name", None) or m.name
                res = f"{m.width}x{m.height}"
                pos = f"({m.x}, {m.y})"
                primary = "✓" if m.is_primary else ""
                scale = f"{m.scale_factor}x"
                rows.append([str(m.index), display_name, res, pos, scale, str(m.dpi), primary])

            print_table(
                headers, rows,
                count_label=f"{len(monitors)} monitor(s) found.",
            )
    except NotImplementedError:
        msg = f"Monitor listing is not supported on {platform.system()} yet."
        if json_output:
            click.echo(_json_error_str("NOT_IMPLEMENTED", msg))
        else:
            click.echo(f"Error: {msg}", err=True)
        raise SystemExit(1)
    except Exception as e:
        if json_output:
            click.echo(_json_error_str("UNKNOWN_ERROR", str(e)))
        else:
            click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)


@list_cmd.command(hidden=True)
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
def permissions(json_output):
    """List automation permissions status (UIAccess, admin, etc.)."""
    msg = "Permission listing is not implemented yet — coming in a future release."
    if json_output:
        click.echo(_json_error_str("NOT_IMPLEMENTED", msg))
    else:
        click.echo(f"Error: {msg}", err=True)
    raise SystemExit(1)


# ── see ─────────────────────────────────────────


@click.command()
@click.option("--app", help="Target application (name or partial match)")
@click.option("--window", "window_title", default=None, help="Window title pattern (substring match)")
@click.option("--window-title", "window_title", default=None, hidden=True, help="")
@click.option("--hwnd", type=int, default=None, help="Window handle (HWND)")
@click.option("--pid", type=int, help="Process ID")
@click.option(
    "--mode",
    type=click.Choice(["full", "interactive", "fast"]),
    default="full",
    help="Analysis mode: full (all elements), interactive (clickable only), fast (quick scan)",
)
@click.option("--depth", "-d", type=int, default=7, help="Maximum tree depth (1-50)")
@click.option("--path", "-p", help="Save screenshot to path")
@click.option("--annotate", is_flag=True, help="Annotate screenshot with element labels")
@click.option("--snapshot/--no-snapshot", "store_snapshot", default=True, help="Store result in snapshot (default: on)")
@click.option("--session", default=None, envvar="NATURO_SESSION",
              help="Snapshot session name for isolation (default: NATURO_SESSION env or 'default')")
@click.option("--cascade", is_flag=True,
              help="Progressive recognition: try UIA, then CDP (Electron/CEF), then AI vision")
@click.option("--fill-gaps", "fill_gaps", is_flag=True,
              help="Use AI vision to fill uncovered UI regions (requires AI provider)")
@click.option("--stats", "show_stats", is_flag=True,
              help="Show per-provider recognition statistics after output")
@click.option("--coverage", "coverage_target", type=float, default=0.0,
              help="Coverage target (0.0–1.0) before trying next provider (default: 0 = UIA only)")
@click.option("--visible-only", is_flag=True, help="Hide offscreen/zero-bounds elements")
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
@click.option(
    "--backend", "--method", "-b", "-m",
    type=click.Choice(["uia", "msaa", "ia2", "jab", "win32", "auto"]),
    default="auto",
    help="Accessibility backend / interaction method: auto (default: tries all), uia, msaa (legacy apps), ia2 (Firefox/Thunderbird), jab (Java/Swing), win32 (VB6/ActiveX)",
)
def see(app, window_title, hwnd, pid, mode, depth, path, annotate, store_snapshot, session,
        cascade, fill_gaps, show_stats, coverage_target, visible_only, json_output, backend):
    """Capture screenshot and analyze UI elements.

    Inspects the UI element tree of the foreground window (or a specific
    window identified by --hwnd). Shows the element hierarchy with roles,
    names, and bounding rectangles.  Results are stored in a snapshot so
    subsequent commands can reference elements by ID.

    Use --backend msaa for legacy applications (MFC, VB6, Delphi) that
    don't expose UIAutomation elements. Use --backend ia2 for IA2-enabled
    applications (Firefox, Thunderbird, LibreOffice). Use --backend auto to
    try UIA first, then IA2, then MSAA automatically.

    Use --cascade to progressively try multiple providers (UIA → CDP → AI vision).
    This maximizes coverage for Electron apps (Feishu, Slack, VS Code, etc.)
    that render content in a WebView.

    \b
    Examples:
        naturo see --app feishu --cascade      # UIA + CDP for Electron content
        naturo see --app feishu --cascade --fill-gaps  # Also use AI vision
        naturo see --app feishu --cascade --stats      # Show provider breakdown
        naturo see --app feishu --backend auto         # Try all A11y backends
    """
    # BUG-028: Validate --depth range (before platform check — input validation first)
    if depth < 1 or depth > 50:
        msg = f"--depth must be between 1 and 50, got {depth}"
        if json_output:
            click.echo(_json_error_str("INVALID_INPUT", msg))
        else:
            click.echo(f"Error: {msg}", err=True)
        raise SystemExit(1)

    if not _platform_supports_gui():
        msg = _platform_error_msg("UI inspection")
        if json_output:
            click.echo(_json_error_str("PLATFORM_ERROR", msg))
        else:
            click.echo(msg)
        raise SystemExit(1)

    try:
        be = _get_backend(json_output)

        # ── Cascade mode: progressive multi-provider recognition (issue #140) ──
        cascade_stats = None
        if cascade or backend == "auto":
            # (#275) Auto-capture screenshot for cascade mode so AI vision
            # fallback can trigger when UIA tree is too shallow.
            cascade_screenshot = path
            _cascade_tmp_screenshot = None
            if not cascade_screenshot and cascade:
                import tempfile as _tmpfile
                _cascade_tmp_screenshot = _tmpfile.NamedTemporaryFile(
                    suffix=".png", prefix="naturo_cascade_", delete=False,
                )
                _cascade_tmp_screenshot.close()
                try:
                    _cap_result = be.capture_screen(
                        output_path=_cascade_tmp_screenshot.name
                    )
                    cascade_screenshot = _cap_result.path
                except Exception:
                    cascade_screenshot = None

            from naturo.cascade import run_cascade
            cascade_result = run_cascade(
                be,
                app=app,
                window_title=window_title,
                hwnd=hwnd,
                pid=pid,
                depth=depth,
                backend_name=backend,
                coverage_target=coverage_target,
                fill_gaps_ai=fill_gaps,
                screenshot_path=cascade_screenshot,
            )
            tree = cascade_result.tree
            cascade_stats = cascade_result.stats
        else:
            # (#304) When --app is used without --hwnd, enumerate ALL windows
            # of the application and merge their UI trees.
            if app and not hwnd and hasattr(be, "_resolve_hwnds"):
                hwnds = be._resolve_hwnds(app=app)
                if not hwnds:
                    msg = f"No windows found for app '{app}'."
                    if json_output:
                        click.echo(_json_error_str("WINDOW_NOT_FOUND", msg))
                    else:
                        click.echo(msg)
                    raise SystemExit(1)

                # Get element tree for each window
                from naturo.backends.base import ElementInfo as BaseElementInfo
                window_trees = []
                for h in hwnds:
                    subtree = be.get_element_tree(
                        hwnd=h, depth=depth, backend=backend,
                    )
                    if subtree:
                        window_trees.append((h, subtree))

                if not window_trees:
                    msg = "All windows have empty UI trees."
                    if json_output:
                        click.echo(_json_error_str("WINDOW_NOT_FOUND", msg))
                    else:
                        click.echo(msg)
                    raise SystemExit(1)

                # Merge into a single root: create a virtual root node
                # with each window's tree as a child
                tree = BaseElementInfo(
                    id="app_root",
                    role="Application",
                    name=app,
                    value=None,
                    x=0, y=0, width=0, height=0,
                    children=[],
                    properties={},
                )
                for h, subtree in window_trees:
                    # Wrap each window tree with a "Window" group node
                    # to preserve window identity in output
                    window_node = BaseElementInfo(
                        id=f"window_{h}",
                        role="WindowGroup",
                        name=f"{subtree.name} (HWND:{h})",
                        value=None,
                        x=subtree.x,
                        y=subtree.y,
                        width=subtree.width,
                        height=subtree.height,
                        children=[subtree],
                        properties={},
                    )
                    tree.children.append(window_node)
            else:
                tree = be.get_element_tree(
                    app=app, window_title=window_title, hwnd=hwnd, depth=depth,
                    backend=backend,
                )

        if tree is None:
            msg = "No window found or UI tree is empty."
            if json_output:
                click.echo(_json_error_str("WINDOW_NOT_FOUND", msg))
            else:
                click.echo(msg)
            raise SystemExit(1)

        snapshot_id = None
        ref_map = {}  # Maps "eN" → backend element id; built when snapshot is stored
        if store_snapshot:
            from naturo.snapshot import get_snapshot_manager
            from naturo.models.snapshot import UIElement

            mgr = get_snapshot_manager(session=session)
            snapshot_id = mgr.create_snapshot()

            # Flatten element tree into ui_map and build ref→element mapping
            # (BUG-071: sequential refs e1, e2, ... for click integration)
            ui_map = {}
            _ref_seq = [0]
            ref_map = {}  # "e1" → element_id (backend id)

            import re as _re_mod

            def _flatten(el, parent_id=None):
                _ref_seq[0] += 1
                ref = f"e{_ref_seq[0]}"
                props = getattr(el, "properties", {})
                # (#229) Only store identifier if it's a real UIA AutomationId,
                # not a tree-assigned sequential ID like "e1", "e2", etc.
                # populate_hierarchy assigns "eN" to elements with empty ids,
                # so we must filter those out to avoid false AutomationId lookups.
                _raw_id = str(el.id) if el.id else None
                if _raw_id and _re_mod.fullmatch(r"e\d+", _raw_id):
                    _raw_id = None  # Tree-assigned, not a real AutomationId
                # (#237) Always use the sequential ref as canonical key in
                # ui_map.  Multiple elements can share the same UIA
                # AutomationId (e.g. Notepad status bar texts), so using
                # el.id as key caused overwrites and duplicate display IDs.
                # The original AutomationId is preserved in `identifier`.
                child_refs = []
                for child in el.children:
                    child_refs.append(_flatten(child, parent_id=ref))
                ui_map[ref] = UIElement(
                    id=ref,
                    element_id=f"element_{ref}",
                    role=el.role,
                    title=el.name,
                    label=el.name,
                    value=el.value,
                    identifier=_raw_id,
                    frame=(el.x, el.y, el.width, el.height),
                    is_actionable=getattr(el, "is_actionable", False),
                    parent_id=parent_id,
                    children=child_refs,
                    keyboard_shortcut=props.get("keyboard_shortcut"),
                )
                ref_map[ref] = el.id
                return ref

            _flatten(tree)
            mgr.store_detection_result(snapshot_id, ui_map)
            # Persist ref mapping so click/type can resolve e<N> refs
            mgr.store_ref_map(snapshot_id, ref_map)

            # Store window bounds in the snapshot metadata for coordinate
            # offset calculations (e.g. capture --element crop).
            _win_bounds = None
            if tree:
                _win_bounds = (tree.x, tree.y, tree.width, tree.height)
            snap_obj = mgr.get_snapshot(snapshot_id)
            snap_obj.window_bounds = _win_bounds
            snap_obj.window_handle = hwnd
            snap_obj.application_name = app
            snap_obj.window_title = window_title
            mgr._write_json_atomic(
                mgr._snap_dir(snapshot_id) / "snapshot.json",
                snap_obj.to_dict(),
            )

            # Optionally capture screenshot into snapshot
            if path:
                result = backend.capture_screen(output_path=path)
                metadata = {
                    "window_handle": hwnd,
                    "application_name": app,
                    "window_title": window_title,
                    "window_bounds": _win_bounds,
                }
                mgr.store_screenshot(snapshot_id, result.path, metadata)

        if json_output:
            # (#237) Use a sequential counter matching _flatten() DFS order
            # to assign unique display IDs.  The previous reverse-map approach
            # was lossy: when multiple elements share the same backend
            # AutomationId, the dict comprehension kept only the last ref,
            # causing duplicate display IDs in JSON output.
            _json_ref_seq = [0]

            def to_dict(el, parent_ref=None):
                """Convert ElementInfo tree to a JSON-serializable dict.

                Args:
                    el: Backend ElementInfo node.
                    parent_ref: The naturo ref (eN) of the parent element,
                        ensuring parent references are always in the same
                        ID space as element refs (#295).
                """
                _json_ref_seq[0] += 1
                display_id = f"e{_json_ref_seq[0]}"

                # (#295) Expose AutomationId separately.  The raw
                # el.id from the bridge may be an AutomationId or a
                # tree-assigned "eN" placeholder.  Filter placeholders.
                import re as _re_local
                raw_id = str(el.id) if el.id else ""
                automation_id = raw_id if raw_id and not _re_local.fullmatch(r"e\d+", raw_id) else ""

                # (#365) Zero-bounds elements are offscreen
                _is_offscreen = (el.x == 0 and el.y == 0 and el.width == 0 and el.height == 0)

                # (#365) --visible-only: skip offscreen elements entirely
                if visible_only and _is_offscreen:
                    return None

                children_raw = [to_dict(c, parent_ref=display_id) for c in el.children]
                children = [c for c in children_raw if c is not None]

                d = {
                    "id": display_id,
                    "automation_id": automation_id,
                    "role": el.role,
                    "name": el.name,
                    "value": el.value,
                    "x": el.x,
                    "y": el.y,
                    "width": el.width,
                    "height": el.height,
                    "children": children,
                }

                # (#365) Mark offscreen elements
                if _is_offscreen:
                    d["offscreen"] = True

                # (#372) Value preview for Document/Edit/Text elements
                if el.role and el.role.lower() in ("document", "edit", "text") and el.value:
                    d["value_preview"] = el.value[:100]
                    d["value_length"] = len(el.value)

                # (#295) Always use naturo ref for parent, never raw
                # AutomationId — keeps a single consistent ID space.
                if parent_ref:
                    d["parent_ref"] = parent_ref
                # Keep deprecated "parent_id" as alias for backward compat
                if parent_ref:
                    d["parent_id"] = parent_ref
                if props.get("keyboard_shortcut"):
                    d["keyboard_shortcut"] = props["keyboard_shortcut"]
                if props.get("source"):
                    d["source"] = props["source"]
                # (#372) Text preview for Document/Edit elements
                if el.role and el.role.lower() in ("document", "edit", "text") and el.value:
                    d["value_preview"] = el.value[:100]
                    d["value_length"] = len(el.value)
                return d
            out = to_dict(tree)
            if snapshot_id:
                out["snapshot_id"] = snapshot_id
            if cascade_stats:
                out["cascade_stats"] = cascade_stats.to_dict()

            # Add DPI context so AI agents know coordinate scaling
            try:
                dpi_scale = be.get_dpi_scale(0) if hasattr(be, "get_dpi_scale") else 1.0
                monitors = be.list_monitors()
                primary = monitors[0] if monitors else None
                out["dpi_context"] = {
                    "scale_factor": primary.scale_factor if primary else dpi_scale,
                    "dpi": primary.dpi if primary else 96,
                    "note": "Element coordinates are in physical (pixel) space.",
                }
            except Exception:
                out["dpi_context"] = {"scale_factor": 1.0, "dpi": 96, "note": "Element coordinates are in physical (pixel) space."}



            click.echo(json_module.dumps(out, indent=2))
        else:
            # BUG-071: include short element IDs (e1, e2, ...) that can be
            # passed to ``naturo click e3`` for quick interaction.
            _ref_counter = [0]

            def print_tree(el, indent=0):
                """Print element tree with short element refs."""
                _ref_counter[0] += 1
                ref = f"e{_ref_counter[0]}"

                # (#365) Zero-bounds = offscreen
                _is_offscreen = (el.x == 0 and el.y == 0 and el.width == 0 and el.height == 0)

                # (#365) --visible-only: skip offscreen elements
                if visible_only and _is_offscreen:
                    for child in el.children:
                        print_tree(child, indent)
                    return

                prefix = "  " * indent
                name_str = f' "{el.name}"' if el.name else ""
                pos_str = f" ({el.x},{el.y} {el.width}x{el.height})"
                props = getattr(el, "properties", {})
                source_str = f" [{props['source']}]" if props.get("source") else ""
                offscreen_str = " [offscreen]" if _is_offscreen else ""
                click.echo(f"{prefix}[{el.role}]{name_str}{pos_str} {ref}{source_str}{offscreen_str}")

                # (#372) Show text preview for Document/Edit elements
                _vp = props.get("value_preview")
                if _vp:
                    click.echo(f"{prefix}  » {_vp}")
                elif el.role and el.role.lower() in ("document", "edit", "text") and el.value:
                    preview = el.value[:100].replace("\n", "\\n").replace("\r", "")
                    suffix = "…" if len(el.value) > 100 else ""
                    click.echo(f"{prefix}  » {preview}{suffix}")

                for child in el.children:
                    print_tree(child, indent + 1)

            print_tree(tree)
            if snapshot_id:
                click.echo(f"\nSnapshot: {snapshot_id}")
                click.echo("Tip: use 'naturo click e<N>' to click an element by its ref.")

            # Print cascade stats when --stats is requested
            if show_stats and cascade_stats:
                click.echo("\nRecognition Stats:")
                click.echo(f"  Total elements: {cascade_stats.total_elements}")
                click.echo(f"  Coverage:       {cascade_stats.coverage_estimate:.1%}")
                click.echo("  Providers:")
                for p in cascade_stats.providers:
                    click.echo(f"    {p.name:<12} {p.elements:>4} elements  {p.elapsed_ms:>6.0f}ms  [{p.status}]")

        # Generate annotated screenshot
        if annotate and store_snapshot and snapshot_id:
            try:
                from naturo.annotate import annotate_screenshot
                snap = mgr.get_snapshot(snapshot_id)
                if snap.screenshot_path:
                    elements = list(snap.ui_map.values())
                    annotated_path = annotate_screenshot(
                        snap.screenshot_path,
                        elements,
                    )
                    mgr.store_annotated(snapshot_id, annotated_path)
                    if not json_output:
                        click.echo(f"Annotated: {annotated_path}")
                else:
                    if not json_output:
                        click.echo("Warning: --annotate requires a screenshot (use --path)")
            except ImportError:
                click.echo("Warning: Pillow required for --annotate. Install: pip install naturo[annotate]", err=True)

        # Capture screenshot (when not already done above)
        if path and not store_snapshot:
            result = backend.capture_screen(output_path=path)
            click.echo(f"\nScreenshot saved: {result.path}")

    except WindowNotFoundError as e:
        if json_output:
            click.echo(_json_error_str("WINDOW_NOT_FOUND", str(e)))
        else:
            click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)
    except Exception as e:
        if json_output:
            click.echo(_json_error_str("UNKNOWN_ERROR", str(e)))
        else:
            click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)


# ── find ────────────────────────────────────────


@click.command("find")
@click.argument("query", required=False, default=None)
@click.option("--query", "-q", "query_opt", default=None,
              help="Search query (alternative to positional arg; survives shell glob expansion)")
@click.option("--all", "find_all", is_flag=True,
              help="Find all elements (equivalent to query \"*\"). Safe from shell glob expansion.")
@click.option("--role", help="Filter by element role (e.g., Button, Edit)")
@click.option("--actionable", is_flag=True, help="Only show actionable elements")
@click.option("--depth", "-d", type=int, default=20, help="Maximum tree depth (default 20; use lower values for performance)")
@click.option("--limit", type=int, default=50, help="Maximum number of results")
@click.option("--ai", is_flag=True, help="Use AI vision to find element by natural language")
@click.option("--screenshot", type=click.Path(), default=None,
              help="Use existing screenshot (for --ai mode)")
@click.option("--app", default=None, help="Target app window")
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
@click.option(
    "--backend", "--method", "-b", "-m",
    type=click.Choice(["uia", "msaa", "ia2", "jab", "win32", "auto"]),
    default="auto",
    help="Accessibility backend / interaction method: auto (default: tries all), uia, msaa (legacy apps), ia2 (Firefox/Thunderbird), jab (Java/Swing), win32 (VB6/ActiveX)",
)
@click.option("--provider", "ai_provider",
              type=click.Choice(["auto", "anthropic", "openai", "ollama"]),
              default="auto", help="AI provider for --ai mode (auto, anthropic, openai, ollama)")
@click.option("--model", "ai_model", default=None, envvar="NATURO_AI_MODEL",
              help="AI model name override (e.g. claude-sonnet-4-20250514, gpt-4o)")
@click.option("--api-key", "ai_api_key", default=None,
              help="AI provider API key (overrides env var)")
def find_cmd(query, query_opt, find_all, role, actionable, depth, limit, ai,
             ai_provider, ai_model, ai_api_key, screenshot, app, json_output, backend):
    """Search for UI elements matching a query.

    Supports fuzzy name matching, role filtering, and combined queries.
    Use --ai for natural language element finding powered by AI vision.
    Use --backend msaa for legacy applications that lack UIA support.
    Use --backend ia2 for IA2-enabled apps (Firefox, Thunderbird, LibreOffice).

    \b
    Examples:
        naturo find "Save"                      # fuzzy name search
        naturo find "Button:Save"               # role + name
        naturo find "role:Edit"                  # by role only
        naturo find --all --actionable           # all actionable elements
        naturo find --all --role Button          # all buttons
        naturo find "the save button" --ai       # AI vision search
        naturo find "Save" --app "Notepad"              # search in specific app
        naturo find "search field" --ai --app "Chrome"  # AI + specific app
        naturo find "OK" --backend msaa          # MSAA for legacy apps
    """
    # Resolve query: --all flag → wildcard, --query option → override positional
    if find_all:
        query = "*"
    elif query_opt is not None:
        query = query_opt
    # else: query is the positional arg (may be None)

    if query is None:
        # When --actionable or --role is set, treat missing query as wildcard
        if actionable or role:
            query = "*"
        else:
            msg = "Missing argument 'QUERY'. Provide as positional arg or --query/-q option."
            if json_output:
                click.echo(_json_error_str("INVALID_INPUT", msg))
            else:
                click.echo(f"Error: {msg}", err=True)
            raise SystemExit(1)

    # Auto-enable AI mode when --provider or --model is explicitly set (#287)
    if not ai and (ai_provider != "auto" or ai_model is not None or ai_api_key is not None):
        ai = True

    # AI vision mode — natural language element finding
    if ai:
        _find_with_ai(query, ai_provider, screenshot, app, json_output,
                      model=ai_model, api_key=ai_api_key)
        return

    # Validate --depth range (find supports deeper traversal than see)
    if depth < 1 or depth > 50:
        msg = f"--depth must be between 1 and 50, got {depth}"
        if json_output:
            click.echo(_json_error_str("INVALID_INPUT", msg))
        else:
            click.echo(f"Error: {msg}", err=True)
        raise SystemExit(1)

    if not _platform_supports_gui():
        msg = _platform_error_msg("UI inspection")
        if json_output:
            click.echo(_json_error_str("PLATFORM_ERROR", msg))
        else:
            click.echo(msg)
        raise SystemExit(1)

    try:
        be = _get_backend(json_output)
        tree = be.get_element_tree(app=app, depth=depth, backend=backend)
        if tree is None:
            msg = "No window found or UI tree is empty."
            if json_output:
                click.echo(_json_error_str("WINDOW_NOT_FOUND", msg))
            else:
                click.echo(msg)
            raise SystemExit(1)

        from naturo.search import search_elements
        # Convert backend ElementInfo tree to bridge ElementInfo for search
        from naturo.bridge import ElementInfo as BridgeElementInfo

        def to_bridge(el):
            """Convert backend ElementInfo to bridge ElementInfo."""
            return BridgeElementInfo(
                id=el.id,
                role=el.role,
                name=el.name,
                value=el.value,
                x=el.x,
                y=el.y,
                width=el.width,
                height=el.height,
                children=[to_bridge(c) for c in el.children],
                parent_id=el.properties.get("parent_id"),
                keyboard_shortcut=el.properties.get("keyboard_shortcut"),
            )

        bridge_tree = to_bridge(tree)
        results = search_elements(
            bridge_tree,
            query,
            role_filter=role,
            actionable_only=actionable,
            max_results=limit,
        )

        # (#369) Store snapshot with refs so users can `naturo click e3` after find
        from naturo.snapshot import get_snapshot_manager
        from naturo.models.snapshot import UIElement

        mgr = get_snapshot_manager()
        snapshot_id = mgr.create_snapshot()

        ui_map = {}
        _ref_seq = [0]
        ref_map = {}
        # Build a mapping from backend element id → eN ref
        _element_id_to_ref = {}

        import re as _re_mod

        def _flatten_for_find(el, parent_id=None):
            _ref_seq[0] += 1
            ref = f"e{_ref_seq[0]}"
            _element_id_to_ref[id(el)] = ref
            _raw_id = str(el.id) if el.id else None
            if _raw_id and _re_mod.fullmatch(r"e\d+", _raw_id):
                _raw_id = None
            child_refs = []
            for child in el.children:
                child_refs.append(_flatten_for_find(child, parent_id=ref))
            ui_map[ref] = UIElement(
                id=ref,
                element_id=f"element_{ref}",
                role=el.role,
                title=el.name,
                label=el.name,
                value=el.value,
                identifier=_raw_id,
                frame=(el.x, el.y, el.width, el.height),
                is_actionable=getattr(el, "is_actionable", False),
                parent_id=parent_id,
                children=child_refs,
                keyboard_shortcut=getattr(el, "keyboard_shortcut", None),
            )
            ref_map[ref] = el.id
            return ref

        _flatten_for_find(bridge_tree)
        mgr.store_detection_result(snapshot_id, ui_map)
        mgr.store_ref_map(snapshot_id, ref_map)

        if json_output:
            data = [
                {
                    "ref": _element_id_to_ref.get(id(r.element), r.element.id),
                    "id": r.element.id,
                    "role": r.element.role,
                    "name": r.element.name,
                    "value": r.element.value,
                    "x": r.element.x,
                    "y": r.element.y,
                    "width": r.element.width,
                    "height": r.element.height,
                    "breadcrumb": r.breadcrumb_str,
                    "keyboard_shortcut": r.element.keyboard_shortcut,
                }
                for r in results
            ]
            click.echo(json_module.dumps({
                "success": True,
                "elements": data,
                "count": len(data),
                "snapshot_id": snapshot_id,
            }, indent=2))
        else:
            if not results:
                click.echo(f"No elements found matching: {query}")
                return

            for i, r in enumerate(results):
                el = r.element
                ref = _element_id_to_ref.get(id(el), "?")
                name_str = f' "{el.name}"' if el.name else ""
                pos_str = f"({el.x},{el.y} {el.width}x{el.height})"
                shortcut = f" [{el.keyboard_shortcut}]" if el.keyboard_shortcut else ""
                click.echo(f"  {ref}. [{el.role}]{name_str} {pos_str}{shortcut}")
                click.echo(f"     {r.breadcrumb_str}")

            click.echo(f"\n{len(results)} element(s) found.")
            click.echo(f"Snapshot: {snapshot_id}")
            click.echo("Tip: use 'naturo click e<N>' to interact with a found element.")

    except Exception as e:
        if json_output:
            click.echo(_json_error_str("UNKNOWN_ERROR", str(e)))
        else:
            click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)


def _find_with_ai(
    query,
    provider_name,
    screenshot,
    app,
    json_output,
    *,
    model: str | None = None,
    api_key: str | None = None,
):
    """AI-powered element finding via naturo find --ai.

    Args:
        query: Natural language description of the element.
        provider_name: AI provider name.
        screenshot: Optional screenshot path.
        app: Optional target application window.
        json_output: Whether to output JSON.
        model: Optional AI model name override (from --model).
        api_key: Optional API key override (from --api-key).
    """
    try:
        from naturo.ai_find import ai_find_element
    except ImportError as e:
        msg = f"AI find dependencies not available: {e}"
        if json_output:
            click.echo(_json_error_str("MISSING_DEPENDENCY", msg))
        else:
            click.echo(f"Error: {msg}", err=True)
        raise SystemExit(1)

    # Validate screenshot path
    if screenshot and not __import__("os").path.exists(screenshot):
        msg = f"Screenshot file not found: {screenshot}"
        if json_output:
            click.echo(_json_error_str("FILE_NOT_FOUND", msg))
        else:
            click.echo(f"Error: {msg}", err=True)
        raise SystemExit(1)

    try:
        result = ai_find_element(
            query,
            provider_name=provider_name,
            window_title=app,
            screenshot_path=screenshot,
            model=model,
            api_key=api_key,
        )
    except Exception as e:
        msg = str(e)
        code = "AI_FIND_FAILED"
        if "unavailable" in msg.lower() or "api key" in msg.lower():
            code = "AI_PROVIDER_UNAVAILABLE"
        elif "capture" in msg.lower():
            code = "CAPTURE_FAILED"
        if json_output:
            click.echo(json_module.dumps({"success": False, "error": {"code": code, "message": msg}}))
        else:
            click.echo(f"Error: {msg}", err=True)
        raise SystemExit(1)

    if json_output:
        output = {
            "success": result.found,
            "description": result.description,
            "confidence": result.confidence,
            "method": result.method,
            "model": result.model,
            "tokens_used": result.tokens_used,
        }
        if result.ai_bounds:
            output["ai_bounds"] = result.ai_bounds
        if result.element:
            output["element"] = result.element
        if not result.found:
            output["error"] = {
                "code": "ELEMENT_NOT_FOUND",
                "message": f"AI could not locate: {query}",
            }
        click.echo(json_module.dumps(output, indent=2))
    else:
        if not result.found:
            click.echo(f"Element not found: {query}")
            if result.description:
                click.echo(f"  AI says: {result.description}")
            raise SystemExit(1)

        click.echo(f"Found: {result.description}")
        click.echo(f"  Confidence: {result.confidence:.0%}")
        click.echo(f"  Method: {result.method}")
        if result.ai_bounds:
            b = result.ai_bounds
            click.echo(f"  AI bounds: ({b.get('x', '?')}, {b.get('y', '?')}) "
                        f"{b.get('width', '?')}x{b.get('height', '?')}")
        if result.element:
            el = result.element
            click.echo(f"  UIA match: [{el.get('role', '')}] \"{el.get('name', '')}\"")
            eb = el.get("bounds", {})
            click.echo(f"  UIA bounds: ({eb.get('x', '?')}, {eb.get('y', '?')}) "
                        f"{eb.get('width', '?')}x{eb.get('height', '?')}")
            click.echo(f"  Match distance: {el.get('match_distance', '?')} px")
        click.echo(f"  [{result.model}, {result.tokens_used} tokens]", err=True)

    if not result.found:
        raise SystemExit(1)


# ── menu (standalone) ───────────────────────────


@click.command("menu_cmd")
@click.option("--app", help="Application name")
@click.option("--flat", is_flag=True, help="Flatten menu tree into paths")
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
def menu_inspect(app, flat, json_output):
    """List the menu bar structure of the foreground application.

    Traverses the application's MenuBar via UIAutomation and displays
    all menu items with their keyboard shortcuts.
    """
    if not _platform_supports_gui():
        msg = _platform_error_msg("Menu inspection")
        if json_output:
            click.echo(_json_error_str("PLATFORM_ERROR", msg))
        else:
            click.echo(msg)
        raise SystemExit(1)

    try:
        backend = _get_backend(json_output)

        # BUG-026: Check if app exists before inspecting menus
        if app:
            try:
                from naturo.process import find_process
                app_info = find_process(app)
                if not app_info:
                    msg = f"Application not found: {app}"
                    if json_output:
                        click.echo(_json_error_str("APP_NOT_FOUND", msg))
                    else:
                        click.echo(f"Error: {msg}", err=True)
                    raise SystemExit(1)
            except ImportError:
                pass  # find_process not available, fall through to get_menu_items
            except SystemExit:
                raise
            except Exception:
                pass  # find_process failed for other reasons, fall through

        items = backend.get_menu_items(window_title=app)

        if not items:
            msg = "No menu items found."
            if json_output:
                click.echo(_json_error_str("NO_MENU_ITEMS", msg))
            else:
                click.echo(msg)
            raise SystemExit(1)

        if json_output:
            if flat:
                flat_items = []
                for item in items:
                    flat_items.extend(item.flatten())
                click.echo(json_module.dumps({"success": True, "menu_items": flat_items}, indent=2))
            else:
                click.echo(json_module.dumps({"success": True, "menu_items": [item.to_dict() for item in items]}, indent=2))
        else:
            if flat:
                for item in items:
                    for entry in item.flatten():
                        shortcut = f"  [{entry['shortcut']}]" if entry.get("shortcut") else ""
                        click.echo(f"  {entry['path']}{shortcut}")
            else:
                def print_menu(item, indent=0):
                    """Recursively print a menu item and its submenus with indentation."""
                    prefix = "  " * indent
                    shortcut = f" [{item.shortcut}]" if item.shortcut else ""
                    state = ""
                    if not item.enabled:
                        state += " (disabled)"
                    if item.checked:
                        state += " (✓)"
                    click.echo(f"{prefix}{item.name}{shortcut}{state}")
                    if item.submenu:
                        for sub in item.submenu:
                            print_menu(sub, indent + 1)

                for item in items:
                    print_menu(item)

    except NotImplementedError:
        msg = "Menu inspection not supported on this platform."
        if json_output:
            click.echo(_json_error_str("NOT_SUPPORTED", msg))
        else:
            click.echo(msg)
        raise SystemExit(1)
    except Exception as e:
        if json_output:
            click.echo(_json_error_str("UNKNOWN_ERROR", str(e)))
        else:
            click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)


# ── learn ───────────────────────────────────────


@click.command()
@click.argument("topic", required=False)
def learn(topic):
    """Show usage guide and tutorials.

    Without TOPIC, shows an overview. With TOPIC, shows detailed help
    including common commands, examples, and tips.
    """
    topics = {
        "capture": {
            "summary": "Capture screenshots, video, or watch for changes.",
            "guide": """\
  Screenshots
  -----------
    naturo capture live --path screenshot.png   Save a screenshot
    naturo capture live --json                  Screenshot with metadata (JSON)
    naturo capture live --app "Notepad"         Capture a specific app window

  Snapshots (element-annotated screenshots)
  -----------------------------------------
    naturo capture live --path snap.png          Capture + store snapshot
    naturo snapshot list                         List saved snapshots
    naturo snapshot clean                        Remove old snapshots

  Recording
  ---------
    naturo record start                         Start screen recording
    naturo record stop                          Stop and save recording
    naturo record list                          List recordings

  Watch for Changes
  -----------------
    naturo diff --snapshot ID1 --snapshot ID2    Compare two snapshots
    naturo diff --window "Notepad"              Capture before/after diff

  Tips
  ----
    • Add --json to any command for structured output
    • Use --app or --window-title to capture a specific window
    • Snapshots annotate UI elements for AI-assisted automation""",
        },
        "interaction": {
            "summary": "Click, type, press, hotkey, scroll, drag, move, paste.",
            "guide": """\
  Mouse
  -----
    naturo click --coords 500 300               Click at coordinates (x, y)
    naturo click --coords 500 300 --right        Right-click
    naturo click --coords 500 300 --double       Double-click
    naturo click "Submit"                        Click element by text
    naturo drag --from-coords 100 200 --to-coords 400 500
                                                Drag from (100,200) to (400,500)
    naturo move --coords 500 300                Move mouse cursor
    naturo scroll down                          Scroll down
    naturo scroll up --amount 5                 Scroll up 5 clicks

  Keyboard
  --------
    naturo type "Hello, World!"                 Type text
    naturo press enter                          Press a single key
    naturo hotkey ctrl+s                        Key combination (Ctrl+S)
    naturo hotkey alt+f4                        Close active window
    naturo hotkey ctrl+shift+esc                Open Task Manager

  AI-Powered Interaction
  ----------------------
    naturo find "Submit button"                 Find element by description
    naturo see                                  Describe what's on screen

  Tips
  ----
    • Use naturo find to locate elements without knowing coordinates
    • Combine with naturo capture to verify actions visually
    • All commands support --json for automation pipelines""",
        },
        "system": {
            "summary": "App, window, menu, clipboard, dialog, open.",
            "guide": """\
  Applications
  ------------
    naturo app list                             List running applications
    naturo app launch notepad                   Launch an application
    naturo app quit notepad                     Close an application
    naturo app switch "Google Chrome"            Switch to an app
    naturo app find "Visual Studio"             Find apps by name

  Windows
  -------
    naturo list windows                         List all windows
    naturo window focus --title "Untitled"       Focus a window by title
    naturo window minimize --title "Notepad"     Minimize a window
    naturo window maximize --title "Notepad"     Maximize a window
    naturo window close --title "Notepad"        Close a window
    naturo window move --title "Notepad" --x 100 --y 100
    naturo window resize --title "Notepad" --width 800 --height 600

  Clipboard
  ---------
    naturo clipboard get                        Read clipboard content
    naturo clipboard set "copied text"          Write to clipboard

  Dialogs
  -------
    naturo dialog detect                        Detect open dialogs
    naturo dialog accept                        Accept/OK a dialog
    naturo dialog dismiss                       Cancel/dismiss a dialog

  Opening Files & URLs
  --------------------
    naturo open https://example.com             Open URL in browser
    naturo open document.pdf                    Open file with default app

  Tips
  ----
    • naturo list screens shows monitor info
    • Use --json on any command for structured output
    • App names are case-insensitive for most commands""",
        },
        "windows": {
            "summary": "Windows-specific: taskbar, tray, desktop, registry, service.",
            "guide": """\
  Taskbar & System Tray
  ---------------------
    naturo taskbar list                         List taskbar items
    naturo taskbar click "Chrome"               Click a taskbar icon
    naturo tray list                            List system tray icons
    naturo tray click "Volume"                  Click a tray icon

  Registry
  --------
    naturo registry list HKCU\\Software          List registry subkeys
    naturo registry get HKCU\\Software\\MyApp -v Setting
    naturo registry set HKCU\\Software\\MyApp -v Key -d "value"
    naturo registry search HKCU\\Software "keyword"

  Services
  --------
    naturo service list                         List all services
    naturo service list --state running         Only running services
    naturo service status Spooler               Get service details
    naturo service start Spooler                Start a service
    naturo service stop Spooler                 Stop a service
    naturo service restart Spooler              Restart a service

  Virtual Desktops
  ----------------
    naturo desktop list                         List virtual desktops
    naturo desktop switch 2                     Switch to desktop 2
    (Requires pyvda: pip install pyvda)

  Tips
  ----
    • Registry operations support HKCU, HKLM, HKCR, HKU, HKCC
    • Service commands require appropriate permissions
    • Use --json for automation-friendly output""",
        },
        "extensions": {
            "summary": "Enterprise: excel, java, sap automation.",
            "guide": """\
  Excel (COM Automation)
  ----------------------
    naturo excel open report.xlsx               Open a workbook
    naturo excel read report.xlsx A1             Read a cell
    naturo excel read report.xlsx "A1:D10"       Read a range
    naturo excel write report.xlsx B2 "Hello"    Write to a cell
    naturo excel list-sheets report.xlsx         List sheets
    naturo excel run-macro data.xlsm "MyMacro"   Run a VBA macro
    naturo excel info report.xlsx                Used range info
    (Requires Microsoft Excel and pywin32)

  Electron/Chrome (Removed in v0.2.0)
  ------------------------------------
    Use Playwright or browser automation tools for Electron/Chrome.
    Backend modules retained for Unified App Model internal use.

  Java Access Bridge (planned)
  ----------------------------
    Java UI automation via JAB is on the roadmap.
    Will enable inspection and control of Swing/AWT applications.

  SAP GUI Scripting (planned)
  ---------------------------
    SAP GUI automation via scripting API is on the roadmap.
    Will enable transaction execution and form interaction.

  Tips
  ----
    • Electron automation unlocks DOM access for desktop apps
    • Excel operations preserve formatting and formulas
    • Use --json for all commands for pipeline integration""",
        },
        "ai": {
            "summary": "AI agent and MCP server integration.",
            "guide": """\
  MCP Server (Model Context Protocol)
  ------------------------------------
    naturo mcp start                            Start MCP server (stdio)
    naturo mcp tools                            List all MCP tools
    naturo mcp tools --json                     Tool list as JSON

  AI Agent
  --------
    naturo agent "Open Notepad and type hello"
    naturo agent --model gpt-4o "Fill in the form"
    (Provides autonomous UI automation via AI vision)

  AI-Powered Commands
  -------------------
    naturo see                                  Describe current screen
    naturo find "Login button"                  Find UI element by description
    naturo describe                             Detailed screen analysis

  Integration with LLM Frameworks
  --------------------------------
    Use naturo as an MCP server in Claude Desktop, Cursor, or any
    MCP-compatible client:

    {
      "mcpServers": {
        "naturo": {
          "command": "naturo",
          "args": ["mcp", "start"]
        }
      }
    }

  Tips
  ----
    • MCP server exposes all naturo capabilities as tools
    • 82 tools covering capture, interaction, system, and more
    • Use --json output format for reliable LLM parsing
    • AI find works best with descriptive element names""",
        },
    }
    topic_names = list(topics.keys())
    if topic and topic in topics:
        info = topics[topic]
        click.echo(f"\n  {topic}: {info['summary']}\n")
        click.echo(info["guide"])
        click.echo()
    elif topic and topic not in topics:
        click.echo(f"Error: Unknown topic: {topic}", err=True)
        click.echo(f"Available topics: {', '.join(topic_names)}", err=True)
        raise SystemExit(1)
    else:
        click.echo("\nNaturo — Windows desktop automation engine\n")
        click.echo("Available topics:")
        for name in topic_names:
            click.echo(f"  {name:15s} {topics[name]['summary']}")
        click.echo("\nRun: naturo learn <topic> for details.")


# ── tools ───────────────────────────────────────


@click.command()
@click.argument("positional_refs", nargs=-1)
@click.option("--on", "on_ref", help="Target element ref (eN) to highlight")
@click.option("--ref", "-r", "ref_option", multiple=True, help="Specific refs to highlight (e.g. -r e5 -r e10). Omit for all.")
@click.option("--app", "-a", help="Application name (partial match)")
@click.option("--hwnd", type=int, help="Direct window handle")
@click.option("--depth", "-d", type=int, default=30, help="Tree depth for element discovery")
@click.option("--duration", type=float, default=5.0, help="Highlight duration in seconds")
@click.option(
    "--backend", "-b",
    type=click.Choice(["uia", "win32"]),
    default="uia",
    help="Highlight backend: uia (default, uses UIA element tree from snapshot), win32 (Win32 HWND enumeration)",
)
def highlight(positional_refs, on_ref, ref_option, app, hwnd, depth, duration, backend):
    """Highlight UI elements on screen with colored borders and labels.

    By default uses the UIA element tree from the most recent snapshot
    (or captures a fresh one). Refs match those from 'naturo see'.

    Use --backend win32 for VB6/ActiveX apps where UIA fails.

    \b
    Examples:

        naturo highlight e11 --app notepad               # Highlight specific ref (positional)
        naturo highlight --app EnterprisePortal           # Highlight all elements
        naturo highlight --hwnd 10697004 -r e69 -r e77   # Highlight specific refs (option)
        naturo highlight e5 e10 --app notepad             # Multiple positional refs
        naturo highlight --on e5 --app notepad            # Using --on
        naturo highlight --app notepad --duration 10      # Show for 10 seconds
        naturo highlight --app legacy --backend win32     # Win32 HWND fallback
    """
    be = _get_backend()
    handle = be._resolve_hwnd(app=app, hwnd=hwnd)

    # Merge positional refs, --on ref, and --ref option refs
    all_refs = list(positional_refs) + list(ref_option)
    if on_ref:
        all_refs.append(on_ref)
    refs_list = all_refs if all_refs else None

    if backend == "win32":
        # Legacy Win32 HWND enumeration fallback
        from naturo.bridge import highlight_elements
        click.echo(f"Highlighting elements (win32) for {duration}s... (switch to the target window)")
        import time
        time.sleep(1.5)
        highlight_elements(hwnd=handle, depth=depth, duration=duration, refs=refs_list)
    else:
        # UIA mode: use snapshot element tree (#364)
        from naturo.bridge import highlight_elements_uia
        click.echo(f"Highlighting elements (uia) for {duration}s... (switch to the target window)")
        import time
        time.sleep(1.5)
        highlight_elements_uia(
            backend=be, app=app, hwnd=handle, depth=depth,
            duration=duration, refs=refs_list,
        )
    click.echo("Done.")


@click.command(hidden=True)
@click.option("--json", "-j", "json_output", is_flag=True, help="JSON output")
def tools(json_output):
    """List available automation tools and backends.

    Shows which native backends are available (UIA, MSAA, Java Bridge, etc.).
    """
    msg = "Tools listing is not implemented yet — coming in a future release."
    if json_output:
        click.echo(_json_error_str("NOT_IMPLEMENTED", msg))
    else:
        click.echo(f"Error: {msg}", err=True)
    raise SystemExit(1)
