"""Computer use tool — desktop automation via Peekaboo, Electron, or OS-native tools.

Dispatch chain (priority order):
1. Electron HTTP delegation (desktop app running)
2. Peekaboo CLI subprocess (macOS, installed)
3. OS-native fallback (osascript+cliclick / xdotool / PowerShell)
"""

from __future__ import annotations

import asyncio
import functools
import json
import shlex
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any

from loguru import logger

from flowly.agent.tools.base import Tool

_ELECTRON_API_FILE = Path.home() / ".flowly" / "electron-api.json"
_PLATFORM = sys.platform  # "darwin", "linux", "win32"


# ---------------------------------------------------------------------------
# AppleScript helpers (macOS native fallback)
# ---------------------------------------------------------------------------

def _escape_applescript(text: str) -> str:
    return text.replace("\\", "\\\\").replace('"', '\\"')


_MAC_KEY_CODES: dict[str, int] = {
    "return": 36, "enter": 36, "tab": 48, "space": 49,
    "delete": 51, "backspace": 51, "escape": 53, "esc": 53,
    "up": 126, "down": 125, "left": 123, "right": 124,
    "home": 115, "end": 119, "pageup": 116, "pagedown": 121,
    "f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97,
    "f7": 98, "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
}

_MAC_MODIFIERS: dict[str, str] = {
    "cmd": "command down", "command": "command down",
    "ctrl": "control down", "control": "control down",
    "alt": "option down", "option": "option down",
    "shift": "shift down",
}


def _build_mac_osascript_key(keys: str) -> list[str]:
    """Build osascript args for a key combo like 'cmd+c'."""
    parts = [k.strip().lower() for k in keys.split("+") if k.strip()]
    modifiers: list[str] = []
    key = ""
    for part in parts:
        if part in _MAC_MODIFIERS:
            modifiers.append(_MAC_MODIFIERS[part])
        else:
            key = part

    mod_str = f" using {{{', '.join(modifiers)}}}" if modifiers else ""

    if key in _MAC_KEY_CODES:
        script = f'tell application "System Events" to key code {_MAC_KEY_CODES[key]}{mod_str}'
    else:
        escaped = _escape_applescript(key)
        script = f'tell application "System Events" to keystroke "{escaped}"{mod_str}'

    return ["osascript", "-e", script]


def _build_xdotool_key(keys: str) -> str:
    """Convert key combo to xdotool format: 'cmd+c' → 'ctrl+c'."""
    mapping = {
        "cmd": "ctrl", "command": "ctrl", "option": "alt",
        "esc": "Escape", "enter": "Return", "return": "Return",
        "del": "BackSpace", "delete": "BackSpace", "backspace": "BackSpace",
        "tab": "Tab", "space": "space",
        "up": "Up", "down": "Down", "left": "Left", "right": "Right",
    }
    return "+".join(mapping.get(k.strip().lower(), k.strip()) for k in keys.split("+"))


def _escape_sendkeys(text: str) -> str:
    """Escape special SendKeys characters for Windows."""
    special = set("^%~+{}()")
    return "".join(f"{{{c}}}" if c in special else c for c in text)


# ---------------------------------------------------------------------------
# ComputerTool
# ---------------------------------------------------------------------------

class ComputerTool(Tool):
    """Desktop automation: Peekaboo (element-based) + OS-native fallback."""

    def __init__(self, config: Any = None, screenshot_tool: Any = None):
        from flowly.config.schema import ComputerConfig
        self._config = config or ComputerConfig()
        self._screenshot_tool = screenshot_tool

    @functools.cached_property
    def _has_peekaboo(self) -> bool:
        return _PLATFORM == "darwin" and shutil.which("peekaboo") is not None

    @functools.cached_property
    def _has_cliclick(self) -> bool:
        return shutil.which("cliclick") is not None

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

    @property
    def description(self) -> str:
        base = (
            "Control the desktop — mouse, keyboard, screen capture, and UI automation.\n\n"
            "Actions:\n"
            "- see: Get UI element map with IDs (macOS). Returns element list, NOT a screenshot. "
            "Use this before click/type for accurate element targeting.\n"
            "- activate_app: Bring an application window to the foreground. "
            "ALWAYS do this before interacting with an app.\n"
            "- click: Click by element ID (element_id='B3') or coordinates (x, y)\n"
            "- type: Type text into the focused element\n"
            "- key: Press key combination (keys='cmd+c', 'enter', 'tab', 'alt+tab')\n"
            "- screenshot: Capture the screen (use 'see' instead when available)\n"
            "- scroll: Scroll (direction=up/down, amount=3)\n"
            "- drag: Drag from A to B (start_x/y, end_x/y)\n"
            "- move: Move mouse cursor to (x, y)\n"
            "- cursor_position: Get current mouse position\n"
            "- screen_size: Get screen dimensions\n"
            "- clipboard_read: Read clipboard text\n"
            "- clipboard_write: Write text to clipboard\n"
            "- window_list: List windows of an app\n\n"
            "Workflow: activate_app → see (get element IDs) → click by ID → type → see (verify).\n"
            "ALWAYS call activate_app first. Prefer 'see' + element IDs over screenshot + coordinates."
        )
        return base

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "description": "The action to perform",
                    "enum": [
                        "see", "activate_app", "screenshot",
                        "click", "double_click", "move",
                        "type", "key", "scroll", "drag",
                        "cursor_position", "screen_size",
                        "clipboard_read", "clipboard_write",
                        "window_list",
                    ],
                },
                "app_name": {"type": "string", "description": "Application name (for see, activate_app, click, type, window_list)"},
                "element_id": {"type": "string", "description": "Element ID from 'see' output (e.g. B3, T2). Preferred over coordinates."},
                "x": {"type": "integer", "description": "X coordinate"},
                "y": {"type": "integer", "description": "Y coordinate"},
                "text": {"type": "string", "description": "Text to type or write to clipboard"},
                "keys": {"type": "string", "description": "Key combo: 'cmd+c', 'enter', 'tab', 'alt+tab', 'f1'"},
                "button": {"type": "string", "enum": ["left", "right", "middle"], "description": "Mouse button (default: left)"},
                "direction": {"type": "string", "enum": ["up", "down", "left", "right"], "description": "Scroll direction"},
                "amount": {"type": "integer", "description": "Scroll amount (default: 3)"},
                "start_x": {"type": "integer", "description": "Drag start X"},
                "start_y": {"type": "integer", "description": "Drag start Y"},
                "end_x": {"type": "integer", "description": "Drag end X"},
                "end_y": {"type": "integer", "description": "Drag end Y"},
                "display": {"type": "integer", "description": "Display index for screenshot (default: 0)"},
            },
            "required": ["action"],
        }

    async def execute(self, action: str = "", **kwargs: Any) -> str:
        if not self._config.enabled:
            return json.dumps({"error": "Computer use disabled. Enable: tools.computer.enabled = true"})

        # Screenshot delegates to the screenshot tool
        if action == "screenshot":
            if self._screenshot_tool:
                return await self._screenshot_tool.execute(
                    display=kwargs.get("display", 0),
                    filename=kwargs.get("filename"),
                    format=kwargs.get("format", "png"),
                )
            return json.dumps({"error": "Screenshot tool not available"})

        try:
            return await self._dispatch(action, kwargs)
        except Exception as exc:
            logger.error("Computer {} error: {}", action, exc)
            return json.dumps({"error": str(exc), "action": action})

    # -- Dispatch chain --------------------------------------------------------

    async def _dispatch(self, action: str, params: dict) -> str:
        # 1. Electron HTTP delegation
        result = await asyncio.to_thread(self._execute_electron, action, params)
        if result is not None:
            return json.dumps({"action": action, **result})

        # 2. Peekaboo CLI (macOS)
        if self._has_peekaboo:
            result = await asyncio.to_thread(self._execute_peekaboo, action, params)
            return json.dumps({"action": action, **result})

        # 3. OS-native fallback
        if _PLATFORM == "darwin":
            result = await asyncio.to_thread(self._execute_darwin, action, params)
        elif _PLATFORM == "linux":
            result = await asyncio.to_thread(self._execute_linux, action, params)
        elif _PLATFORM == "win32":
            result = await asyncio.to_thread(self._execute_win32, action, params)
        else:
            result = {"error": f"Unsupported platform: {_PLATFORM}"}

        return json.dumps({"action": action, **result})

    # -- Helpers ---------------------------------------------------------------

    def _run(self, cmd: list[str], timeout: int = 15) -> subprocess.CompletedProcess:
        return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)

    def _delay(self) -> None:
        if self._config.action_delay_ms > 0:
            time.sleep(self._config.action_delay_ms / 1000)

    # -- Electron delegation ---------------------------------------------------

    def _execute_electron(self, action: str, params: dict) -> dict | None:
        if not _ELECTRON_API_FILE.exists():
            return None
        try:
            api_data = json.loads(_ELECTRON_API_FILE.read_text())
            port, token = int(api_data["port"]), str(api_data["token"])
        except (ValueError, KeyError, json.JSONDecodeError, OSError):
            return None

        # Map actions for Electron endpoint
        electron_params = dict(params)
        electron_params["action"] = action

        # Electron /input doesn't handle see/activate_app/clipboard/window_list
        if action in ("see", "clipboard_read", "clipboard_write", "window_list"):
            return None

        if action == "activate_app":
            electron_params = {"action": "activate_app", "app_name": params.get("app_name", "")}

        payload = json.dumps(electron_params).encode()
        req = urllib.request.Request(
            f"http://127.0.0.1:{port}/input",
            data=payload,
            headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                return json.loads(resp.read())
        except (urllib.error.HTTPError, urllib.error.URLError, OSError):
            return None

    # ==========================================================================
    # PEEKABOO BACKEND (macOS, CLI subprocess)
    # ==========================================================================

    def _execute_peekaboo(self, action: str, params: dict) -> dict:
        """Execute action via Peekaboo CLI. Returns result dict."""
        app_name = params.get("app_name")
        element_id = params.get("element_id")

        try:
            if action == "see":
                return self._pk_see(app_name)
            elif action == "activate_app":
                return self._pk_activate(app_name or "")
            elif action == "click":
                return self._pk_click(params, app_name, element_id)
            elif action == "double_click":
                return self._pk_click(params, app_name, element_id, double=True)
            elif action == "type":
                return self._pk_type(params.get("text", ""), app_name)
            elif action == "key":
                return self._pk_key(params.get("keys", ""), app_name)
            elif action == "scroll":
                return self._pk_scroll(params, app_name)
            elif action == "drag":
                return self._pk_drag(params)
            elif action == "move":
                return self._pk_move(params)
            elif action == "clipboard_read":
                return self._pk_clipboard_read()
            elif action == "clipboard_write":
                return self._pk_clipboard_write(params.get("text", ""))
            elif action == "window_list":
                return self._pk_window_list(app_name)
            elif action == "cursor_position":
                return self._native_cursor_position()
            elif action == "screen_size":
                return self._native_screen_size()
            else:
                return {"error": f"Unknown action: {action}"}
        except subprocess.TimeoutExpired:
            return {"error": f"Peekaboo command timed out ({action})"}
        except FileNotFoundError:
            return {"error": "peekaboo not found. Install: brew install steipete/tap/peekaboo"}
        except Exception as e:
            return {"error": f"Peekaboo error: {e}"}

    def _pk_see(self, app_name: str | None) -> dict:
        cmd = ["peekaboo", "see", "--annotate", "--json"]
        if app_name:
            cmd.extend(["--app", app_name])
        r = self._run(cmd, timeout=25)
        if r.returncode != 0:
            return {"error": f"peekaboo see failed: {r.stderr.strip()[:500]}"}
        try:
            data = json.loads(r.stdout)
            # Extract key info for the LLM
            elements = data.get("data", data).get("ui_elements", [])
            snapshot_id = data.get("data", data).get("snapshot_id", "")
            element_summary = []
            for el in elements:
                entry = f"{el.get('id', '?')}: {el.get('role', '?')}"
                label = el.get("label") or el.get("title") or el.get("description")
                if label:
                    entry += f' "{label}"'
                if el.get("keyboard_shortcut"):
                    entry += f" [{el['keyboard_shortcut']}]"
                element_summary.append(entry)
            return {
                "success": True,
                "snapshot_id": snapshot_id,
                "element_count": len(elements),
                "elements": "\n".join(element_summary),
                "raw": data,
            }
        except json.JSONDecodeError:
            return {"success": True, "output": r.stdout[:2000]}

    def _pk_activate(self, app_name: str) -> dict:
        if not app_name:
            return {"error": "app_name is required for activate_app"}
        r = self._run(["peekaboo", "app", "switch", "--name", app_name, "--json"])
        self._delay()
        if r.returncode != 0:
            # Fallback to osascript
            r2 = self._run(["osascript", "-e", f'tell application "{_escape_applescript(app_name)}" to activate'])
            self._delay()
            if r2.returncode != 0:
                return {"error": f"Failed to activate {app_name}: {r.stderr.strip()[:300]}"}
        return {"success": True, "app": app_name}

    def _pk_click(self, params: dict, app_name: str | None, element_id: str | None, double: bool = False) -> dict:
        cmd = ["peekaboo", "click"]
        if double:
            cmd.append("--double")
        if element_id:
            cmd.extend(["--on", element_id])
        elif params.get("x") is not None and params.get("y") is not None:
            cmd.extend(["--coords", f"{int(params['x'])},{int(params['y'])}"])
        else:
            return {"error": "click requires element_id or x,y coordinates"}
        if app_name:
            cmd.extend(["--app", app_name])
        if params.get("button") == "right":
            cmd.append("--right")
        cmd.append("--json")
        r = self._run(cmd)
        self._delay()
        if r.returncode != 0:
            return {"error": f"click failed: {r.stderr.strip()[:300]}"}
        return {"success": True}

    def _pk_type(self, text: str, app_name: str | None) -> dict:
        if not text:
            return {"error": "text is required"}
        cmd = ["peekaboo", "type", text]
        if app_name:
            cmd.extend(["--app", app_name])
        cmd.append("--json")
        r = self._run(cmd, timeout=30)
        self._delay()
        if r.returncode != 0:
            return {"error": f"type failed: {r.stderr.strip()[:300]}"}
        return {"success": True}

    def _pk_key(self, keys: str, app_name: str | None) -> dict:
        if not keys:
            return {"error": "keys is required"}
        cmd = ["peekaboo", "hotkey", keys]
        if app_name:
            cmd.extend(["--app", app_name])
        cmd.append("--json")
        r = self._run(cmd)
        self._delay()
        if r.returncode != 0:
            # Fallback: try press for single keys
            cmd2 = ["peekaboo", "press", keys]
            if app_name:
                cmd2.extend(["--app", app_name])
            cmd2.append("--json")
            r2 = self._run(cmd2)
            if r2.returncode != 0:
                return {"error": f"key failed: {r.stderr.strip()[:300]}"}
        return {"success": True}

    def _pk_scroll(self, params: dict, app_name: str | None) -> dict:
        direction = params.get("direction", "down")
        amount = params.get("amount", 3)
        cmd = ["peekaboo", "scroll", "--direction", direction, "--amount", str(amount)]
        if app_name:
            cmd.extend(["--app", app_name])
        cmd.append("--json")
        r = self._run(cmd)
        self._delay()
        if r.returncode != 0:
            return {"error": f"scroll failed: {r.stderr.strip()[:300]}"}
        return {"success": True}

    def _pk_drag(self, params: dict) -> dict:
        sx, sy = params.get("start_x", 0), params.get("start_y", 0)
        ex, ey = params.get("end_x", 0), params.get("end_y", 0)
        if not (sx or sy or ex or ey):
            return {"error": "start_x, start_y, end_x, end_y required"}
        cmd = ["peekaboo", "drag", "--from-coords", f"{sx},{sy}", "--to-coords", f"{ex},{ey}", "--json"]
        r = self._run(cmd)
        self._delay()
        if r.returncode != 0:
            return {"error": f"drag failed: {r.stderr.strip()[:300]}"}
        return {"success": True}

    def _pk_move(self, params: dict) -> dict:
        x, y = params.get("x", 0), params.get("y", 0)
        cmd = ["peekaboo", "move", "--coords", f"{x},{y}", "--json"]
        r = self._run(cmd)
        if r.returncode != 0:
            return {"error": f"move failed: {r.stderr.strip()[:300]}"}
        return {"success": True}

    def _pk_clipboard_read(self) -> dict:
        r = self._run(["peekaboo", "clipboard", "read", "--json"])
        if r.returncode != 0:
            return {"error": f"clipboard read failed: {r.stderr.strip()[:300]}"}
        try:
            data = json.loads(r.stdout)
            return {"success": True, "text": data.get("data", {}).get("text", r.stdout.strip())}
        except json.JSONDecodeError:
            return {"success": True, "text": r.stdout.strip()}

    def _pk_clipboard_write(self, text: str) -> dict:
        if not text:
            return {"error": "text is required"}
        r = self._run(["peekaboo", "clipboard", "write", text, "--json"])
        if r.returncode != 0:
            return {"error": f"clipboard write failed: {r.stderr.strip()[:300]}"}
        return {"success": True}

    def _pk_window_list(self, app_name: str | None) -> dict:
        cmd = ["peekaboo", "window", "list", "--json"]
        if app_name:
            cmd.extend(["--app", app_name])
        r = self._run(cmd)
        if r.returncode != 0:
            return {"error": f"window list failed: {r.stderr.strip()[:300]}"}
        try:
            return {"success": True, "windows": json.loads(r.stdout)}
        except json.JSONDecodeError:
            return {"success": True, "output": r.stdout.strip()[:2000]}

    # -- Shared native helpers (cursor/screen) ---------------------------------

    def _native_cursor_position(self) -> dict:
        if _PLATFORM == "darwin" and self._has_cliclick:
            r = self._run(["cliclick", "p"])
            if r.returncode == 0:
                # cliclick p outputs: "x,y"
                parts = r.stdout.strip().split(",")
                if len(parts) == 2:
                    return {"success": True, "x": int(parts[0]), "y": int(parts[1])}
        if _PLATFORM == "linux" and shutil.which("xdotool"):
            r = self._run(["xdotool", "getmouselocation"])
            if r.returncode == 0:
                # "x:123 y:456 screen:0 ..."
                parts = {k: v for k, v in (p.split(":") for p in r.stdout.strip().split() if ":" in p)}
                return {"success": True, "x": int(parts.get("x", 0)), "y": int(parts.get("y", 0))}
        return {"error": "cursor_position not available on this platform"}

    def _native_screen_size(self) -> dict:
        if _PLATFORM == "darwin":
            r = self._run(["osascript", "-e",
                           'tell application "Finder" to get bounds of window of desktop'])
            if r.returncode == 0:
                parts = [p.strip() for p in r.stdout.strip().split(",")]
                if len(parts) >= 4:
                    return {"success": True, "width": int(parts[2]), "height": int(parts[3])}
        if _PLATFORM == "linux" and shutil.which("xdotool"):
            r = self._run(["xdotool", "getdisplaygeometry"])
            if r.returncode == 0:
                parts = r.stdout.strip().split()
                if len(parts) == 2:
                    return {"success": True, "width": int(parts[0]), "height": int(parts[1])}
        return {"error": "screen_size not available on this platform"}

    # ==========================================================================
    # macOS NATIVE FALLBACK (osascript + cliclick)
    # ==========================================================================

    def _execute_darwin(self, action: str, params: dict) -> dict:
        app_name = params.get("app_name")

        if action == "see":
            return {"error": "see requires Peekaboo. Install: brew install steipete/tap/peekaboo"}

        if action == "activate_app":
            if not app_name:
                return {"error": "app_name required"}
            r = self._run(["osascript", "-e", f'tell application "{_escape_applescript(app_name)}" to activate'])
            self._delay()
            return {"success": True, "app": app_name} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action in ("click", "double_click"):
            if not self._has_cliclick:
                return {"error": "cliclick not found. Install: brew install cliclick"}
            x, y = params.get("x", 0), params.get("y", 0)
            if not x and not y:
                return {"error": "x and y coordinates required (no Peekaboo for element IDs)"}
            prefix = "dc" if action == "double_click" else "c"
            if params.get("button") == "right":
                prefix = "rc"
            r = self._run(["cliclick", f"{prefix}:{x},{y}"])
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "move":
            if not self._has_cliclick:
                return {"error": "cliclick not found. Install: brew install cliclick"}
            r = self._run(["cliclick", f"m:{params.get('x', 0)},{params.get('y', 0)}"])
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "drag":
            if not self._has_cliclick:
                return {"error": "cliclick not found. Install: brew install cliclick"}
            sx, sy = params.get("start_x", 0), params.get("start_y", 0)
            ex, ey = params.get("end_x", 0), params.get("end_y", 0)
            r = self._run(["cliclick", f"dd:{sx},{sy}", f"du:{ex},{ey}"])
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "type":
            text = params.get("text", "")
            if not text:
                return {"error": "text is required"}
            escaped = _escape_applescript(text)
            r = self._run(["osascript", "-e", f'tell application "System Events" to keystroke "{escaped}"'])
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "key":
            keys = params.get("keys", "")
            if not keys:
                return {"error": "keys is required"}
            cmd = _build_mac_osascript_key(keys)
            r = self._run(cmd)
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "scroll":
            direction = params.get("direction", "down")
            amount = params.get("amount", 3)
            lines = amount if direction == "up" else -amount
            r = self._run(["osascript", "-e",
                           f'tell application "System Events" to scroll area 1 of process "Finder" by {lines}'])
            # Scroll via osascript is unreliable; try cliclick if available
            if self._has_cliclick:
                scroll_dir = "u" if direction == "up" else "d"
                for _ in range(amount):
                    self._run(["cliclick", f"k{scroll_dir}:"])
            self._delay()
            return {"success": True}

        if action == "cursor_position":
            return self._native_cursor_position()
        if action == "screen_size":
            return self._native_screen_size()

        if action == "clipboard_read":
            r = self._run(["osascript", "-e", "get the clipboard as text"])
            return {"success": True, "text": r.stdout.strip()} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}
        if action == "clipboard_write":
            text = params.get("text", "")
            r = self._run(["osascript", "-e", f'set the clipboard to "{_escape_applescript(text)}"'])
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "window_list":
            script = 'tell application "System Events" to get name of every window of every process whose visible is true'
            r = self._run(["osascript", "-e", script])
            return {"success": True, "output": r.stdout.strip()[:2000]} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        return {"error": f"Unknown action: {action}"}

    # ==========================================================================
    # LINUX NATIVE (xdotool)
    # ==========================================================================

    def _execute_linux(self, action: str, params: dict) -> dict:
        if not shutil.which("xdotool"):
            return {"error": "xdotool not found. Install: sudo apt install xdotool"}

        app_name = params.get("app_name")

        if action == "see":
            return {"error": "see requires Peekaboo (macOS only). Use screenshot + coordinates on Linux."}

        if action == "activate_app":
            if not app_name:
                return {"error": "app_name required"}
            r = self._run(["xdotool", "search", "--name", app_name, "windowactivate"])
            self._delay()
            return {"success": True, "app": app_name} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action in ("click", "double_click"):
            x, y = params.get("x", 0), params.get("y", 0)
            if not x and not y:
                return {"error": "x and y coordinates required"}
            button = {"left": "1", "middle": "2", "right": "3"}.get(params.get("button", "left"), "1")
            repeat = "--repeat 2" if action == "double_click" else ""
            cmd = f"xdotool mousemove {x} {y} click {repeat} {button}".split()
            r = self._run([c for c in cmd if c])
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "move":
            r = self._run(["xdotool", "mousemove", str(params.get("x", 0)), str(params.get("y", 0))])
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "drag":
            sx, sy = params.get("start_x", 0), params.get("start_y", 0)
            ex, ey = params.get("end_x", 0), params.get("end_y", 0)
            r = self._run(["xdotool", "mousemove", str(sx), str(sy),
                           "mousedown", "1", "mousemove", "--sync", str(ex), str(ey), "mouseup", "1"])
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "type":
            text = params.get("text", "")
            if not text:
                return {"error": "text is required"}
            r = self._run(["xdotool", "type", "--clearmodifiers", "--", text], timeout=30)
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "key":
            keys = params.get("keys", "")
            if not keys:
                return {"error": "keys is required"}
            r = self._run(["xdotool", "key", "--", _build_xdotool_key(keys)])
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "scroll":
            direction = params.get("direction", "down")
            amount = params.get("amount", 3)
            button = "5" if direction == "down" else "4"
            r = self._run(["xdotool", "click", "--repeat", str(amount), button])
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "cursor_position":
            return self._native_cursor_position()
        if action == "screen_size":
            return self._native_screen_size()

        if action == "clipboard_read":
            r = self._run(["xclip", "-selection", "clipboard", "-o"])
            return {"success": True, "text": r.stdout.strip()} if r.returncode == 0 else {"error": "xclip not available"}
        if action == "clipboard_write":
            text = params.get("text", "")
            r = subprocess.run(["xclip", "-selection", "clipboard"], input=text, capture_output=True, text=True, timeout=5)
            return {"success": True} if r.returncode == 0 else {"error": "xclip not available"}

        if action == "window_list":
            r = self._run(["xdotool", "search", "--name", app_name or "", "getwindowname"])
            return {"success": True, "output": r.stdout.strip()[:2000]} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        return {"error": f"Unknown action: {action}"}

    # ==========================================================================
    # WINDOWS NATIVE (PowerShell)
    # ==========================================================================

    def _execute_win32(self, action: str, params: dict) -> dict:
        app_name = params.get("app_name")

        if action == "see":
            return {"error": "see requires Peekaboo (macOS only). Use screenshot + coordinates on Windows."}

        def ps(script: str, timeout: int = 10) -> subprocess.CompletedProcess:
            return subprocess.run(
                ["powershell", "-NoProfile", "-Command", script],
                capture_output=True, text=True, timeout=timeout,
            )

        if action == "activate_app":
            if not app_name:
                return {"error": "app_name required"}
            r = ps(f'(New-Object -ComObject WScript.Shell).AppActivate("{app_name}")')
            self._delay()
            return {"success": True, "app": app_name} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action in ("click", "double_click"):
            x, y = params.get("x", 0), params.get("y", 0)
            if not x and not y:
                return {"error": "x and y coordinates required"}
            clicks = 2 if action == "double_click" else 1
            script = f"""
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public class Mouse {{
    [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
    [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
}}
'@
[Mouse]::SetCursorPos({x}, {y})
Start-Sleep -Milliseconds 50
"""
            for _ in range(clicks):
                script += "[Mouse]::mouse_event(0x0002, 0, 0, 0, [IntPtr]::Zero)\n"  # MOUSEEVENTF_LEFTDOWN
                script += "[Mouse]::mouse_event(0x0004, 0, 0, 0, [IntPtr]::Zero)\n"  # MOUSEEVENTF_LEFTUP
                script += "Start-Sleep -Milliseconds 50\n"
            r = ps(script)
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "move":
            x, y = params.get("x", 0), params.get("y", 0)
            script = f"""
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public class Mouse {{ [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); }}
'@
[Mouse]::SetCursorPos({x}, {y})"""
            r = ps(script)
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "type":
            text = params.get("text", "")
            if not text:
                return {"error": "text is required"}
            escaped = _escape_sendkeys(text)
            script = f"""
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("{escaped}")"""
            r = ps(script)
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "key":
            keys = params.get("keys", "")
            if not keys:
                return {"error": "keys is required"}
            # Map to SendKeys format
            key_map = {
                "cmd": "^", "ctrl": "^", "alt": "%", "shift": "+",
                "enter": "{ENTER}", "return": "{ENTER}", "tab": "{TAB}",
                "escape": "{ESC}", "esc": "{ESC}", "delete": "{DELETE}",
                "backspace": "{BACKSPACE}", "space": " ",
                "up": "{UP}", "down": "{DOWN}", "left": "{LEFT}", "right": "{RIGHT}",
                "home": "{HOME}", "end": "{END}", "pageup": "{PGUP}", "pagedown": "{PGDN}",
            }
            parts = [k.strip().lower() for k in keys.split("+")]
            modifiers = ""
            key_part = ""
            for p in parts:
                if p in ("cmd", "ctrl", "alt", "shift", "command", "control", "option"):
                    modifiers += key_map.get(p, "")
                else:
                    key_part = key_map.get(p, p)
            sendkeys = f"{modifiers}{key_part}"
            script = f"""
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("{sendkeys}")"""
            r = ps(script)
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "scroll":
            direction = params.get("direction", "down")
            amount = params.get("amount", 3)
            wheel_delta = -120 * amount if direction == "down" else 120 * amount
            script = f"""
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public class Mouse {{
    [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
}}
'@
[Mouse]::mouse_event(0x0800, 0, 0, {wheel_delta}, [IntPtr]::Zero)"""
            r = ps(script)
            self._delay()
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "cursor_position":
            r = ps("[System.Windows.Forms.Cursor]::Position | ConvertTo-Json")
            if r.returncode == 0:
                try:
                    pos = json.loads(r.stdout)
                    return {"success": True, "x": pos.get("X", 0), "y": pos.get("Y", 0)}
                except json.JSONDecodeError:
                    pass
            return {"error": "cursor_position failed"}

        if action == "screen_size":
            r = ps("[System.Windows.Forms.Screen]::PrimaryScreen.Bounds | ConvertTo-Json")
            if r.returncode == 0:
                try:
                    bounds = json.loads(r.stdout)
                    return {"success": True, "width": bounds.get("Width", 0), "height": bounds.get("Height", 0)}
                except json.JSONDecodeError:
                    pass
            return {"error": "screen_size failed"}

        if action == "clipboard_read":
            r = ps("Get-Clipboard")
            return {"success": True, "text": r.stdout.strip()} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}
        if action == "clipboard_write":
            text = params.get("text", "")
            r = ps(f'Set-Clipboard -Value "{text}"')
            return {"success": True} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        if action == "drag":
            return {"error": "drag not yet implemented on Windows"}
        if action == "window_list":
            r = ps("Get-Process | Where-Object {$_.MainWindowTitle} | Select-Object ProcessName,MainWindowTitle | ConvertTo-Json")
            return {"success": True, "output": r.stdout.strip()[:2000]} if r.returncode == 0 else {"error": r.stderr.strip()[:300]}

        return {"error": f"Unknown action: {action}"}
