"""
跨平台桌面 GUI 辅助：枚举/聚焦窗口、截图、鼠标键盘（Windows + Linux X11 为主）。

Wayland 下鼠标键盘与截图能力取决于会话与是否安装 grim/wmctrl 等，见工具返回的 hint。
"""

from __future__ import annotations

import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Callable

MAX_DESKTOP_STEPS = 35
MAX_TYPE_CHARS = 8000

# PyAutoGUI 左上角触发异常退出；自动化时关闭以免误触
_PYAG_FAILSAFE = False


def _need_deps() -> str | None:
    try:
        import mss  # noqa: F401
        import pyautogui  # noqa: F401
    except ImportError as e:
        return (
            f"missing dependency: {e}; "
            "install: pip install aicd[desktop]  (or pip install pyautogui mss)"
        )
    return None


def _platform() -> str:
    if sys.platform == "win32":
        return "win32"
    return "linux"


# --- Windows: window list / focus / rect ---

def _win_list_windows(max_n: int = 80) -> list[dict[str, Any]]:
    import ctypes
    from ctypes import wintypes

    class RECT(ctypes.Structure):
        _fields_ = [
            ("left", ctypes.c_long),
            ("top", ctypes.c_long),
            ("right", ctypes.c_long),
            ("bottom", ctypes.c_long),
        ]

    user32 = ctypes.windll.user32
    out: list[dict[str, Any]] = []

    @ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
    def _cb(hwnd, _lparam):
        if not user32.IsWindowVisible(hwnd):
            return True
        ln = user32.GetWindowTextLengthW(hwnd)
        if ln <= 0:
            return True
        buf = ctypes.create_unicode_buffer(ln + 1)
        user32.GetWindowTextW(hwnd, buf, ln + 1)
        title = (buf.value or "").strip()
        if not title:
            return True
        r = RECT()
        user32.GetWindowRect(hwnd, ctypes.byref(r))
        out.append(
            {
                "window_id": str(int(hwnd)),
                "title": title[:500],
                "left": int(r.left),
                "top": int(r.top),
                "width": int(r.right - r.left),
                "height": int(r.bottom - r.top),
            }
        )
        return len(out) < max_n

    user32.EnumWindows(_cb, 0)
    return out


def _win_focus(hwnd: int) -> None:
    import ctypes
    from ctypes import wintypes

    user32 = ctypes.windll.user32
    SW_RESTORE = 9
    user32.ShowWindow(wintypes.HWND(hwnd), SW_RESTORE)
    user32.SetForegroundWindow(wintypes.HWND(hwnd))


def _win_hwnd_from_id(s: str) -> int:
    return int(str(s).strip(), 10)


# --- Linux: wmctrl / xdotool ---

def _linux_run(argv: list[str], timeout: float = 8.0) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        argv,
        capture_output=True,
        text=True,
        timeout=timeout,
        errors="replace",
    )


def _linux_list_windows_wmctrl(max_n: int) -> list[dict[str, Any]]:
    cr = _linux_run(["wmctrl", "-l", "-G"])
    if cr.returncode != 0 or not cr.stdout.strip():
        return []
    rows: list[dict[str, Any]] = []
    for line in cr.stdout.splitlines():
        # wid desk gx gy gw gh hostname title…
        m = re.match(
            r"^(0x[0-9a-fA-F]+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(\S+)\s+(.*)$",
            line.strip(),
        )
        if not m:
            continue
        wid, _desk, gx, gy, gw, gh, _host, title = m.groups()
        rows.append(
            {
                "window_id": wid,
                "title": (title or "").strip()[:500],
                "left": int(gx),
                "top": int(gy),
                "width": int(gw),
                "height": int(gh),
            }
        )
        if len(rows) >= max_n:
            break
    if rows:
        return rows
    cr2 = _linux_run(["wmctrl", "-l"])
    if cr2.returncode != 0:
        return []
    for line in cr2.stdout.splitlines():
        parts = line.split(None, 3)
        if len(parts) < 4:
            continue
        rows.append(
            {
                "window_id": parts[0],
                "title": parts[3].strip()[:500],
                "left": 0,
                "top": 0,
                "width": 0,
                "height": 0,
                "geometry_note": "run wmctrl -l -G for x/y/w/h or use full-screen screenshot",
            }
        )
        if len(rows) >= max_n:
            break
    return rows


def _linux_focus_window_id(window_id: str) -> None:
    wid = window_id.strip()
    cr = _linux_run(["wmctrl", "-i", "-a", wid])
    if cr.returncode == 0:
        return
    # xdotool accepts decimal or hex with 0x
    xcr = _linux_run(["xdotool", "windowactivate", wid])
    if xcr.returncode != 0:
        raise RuntimeError(
            "focus_window failed (wmctrl -ia and xdotool windowactivate); "
            "install: apt install wmctrl xdotool (X11)"
        )


def list_windows(max_n: int = 80) -> dict[str, Any]:
    pf = _platform()
    if pf == "win32":
        return {"ok": True, "windows": _win_list_windows(max_n), "platform": pf}
    w = _linux_list_windows_wmctrl(max_n)
    if w:
        return {"ok": True, "windows": w, "platform": pf, "via": "wmctrl"}
    return {
        "ok": False,
        "error": "could not list windows (wmctrl -l failed or empty)",
        "hint": "Linux X11: install wmctrl (e.g. apt install wmctrl). Wayland may need compositor-specific tools.",
        "platform": pf,
        "windows": [],
    }


def focus_window(window_id: str) -> dict[str, Any]:
    pf = _platform()
    try:
        if pf == "win32":
            _win_focus(_win_hwnd_from_id(window_id))
        else:
            _linux_focus_window_id(window_id)
        return {"ok": True, "window_id": window_id}
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}


def _screenshot_region(
    path: Path,
    region: dict[str, Any] | None,
    policy_check: Callable[[Path], None],
) -> dict[str, Any]:
    import mss
    import mss.tools
    from PIL import Image

    policy_check(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    if path.suffix.lower() not in {".png"}:
        return {"ok": False, "error": "screenshot path must be .png"}

    with mss.mss() as sct:
        if region:
            try:
                left = int(region["left"])
                top = int(region["top"])
                w = int(region["width"])
                h = int(region["height"])
            except (KeyError, TypeError, ValueError):
                return {"ok": False, "error": "region needs left, top, width, height (integers)"}
            if w < 1 or h < 1 or w > 16_384 or h > 16_384:
                return {"ok": False, "error": "region width/height out of range"}
            mon = {"left": left, "top": top, "width": w, "height": h}
        else:
            mon = sct.monitors[0]
        shot = sct.grab(mon)
        img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
        img.save(str(path), format="PNG")

    if not path.is_file() or path.stat().st_size == 0:
        return {"ok": False, "error": "PNG not written"}
    return {"ok": True, "png_path": str(path)}


def run_desktop_gui_automation(
    *,
    steps: list[dict[str, Any]],
    policy_check: Callable[[Path], None],
    cwd: Path,
) -> dict[str, Any]:
    dep = _need_deps()
    if dep:
        return {"ok": False, "error": dep}

    import pyautogui

    pyautogui.FAILSAFE = _PYAG_FAILSAFE

    if not isinstance(steps, list) or not steps:
        return {"ok": False, "error": "steps must be a non-empty list"}
    if len(steps) > MAX_DESKTOP_STEPS:
        return {"ok": False, "error": f"max {MAX_DESKTOP_STEPS} steps"}

    step_results: list[dict[str, Any]] = []
    pf = _platform()

    for i, raw in enumerate(steps):
        if not isinstance(raw, dict):
            return {
                "ok": False,
                "error": f"steps[{i}] must be object",
                "step_results": step_results,
                "failed_step_index": i,
            }
        op = str(raw.get("op", "")).strip().lower()
        rec: dict[str, Any] = {"index": i, "op": op}
        try:
            if op == "list_windows":
                mx = int(raw.get("max", 80))
                mx = max(1, min(200, mx))
                r = list_windows(mx)
                rec["ok"] = r.get("ok", False)
                rec["windows_n"] = len(r.get("windows") or [])
                rec["windows"] = (r.get("windows") or [])[:30]
                if not r.get("ok"):
                    rec["error"] = r.get("error")
                    rec["hint"] = r.get("hint")
                step_results.append(rec)
                if not r.get("ok"):
                    return {
                        "ok": False,
                        "error": r.get("error", "list_windows failed"),
                        "hint": r.get("hint"),
                        "step_results": step_results,
                        "failed_step_index": i,
                    }
                continue

            elif op == "focus_window":
                wid = str(raw.get("window_id", "")).strip()
                if not wid:
                    sub = str(raw.get("title_contains", "")).strip()
                    if not sub:
                        raise ValueError("focus_window needs window_id or title_contains")
                    lr = list_windows(120)
                    if not lr.get("ok"):
                        raise RuntimeError(lr.get("error", "list_windows failed"))
                    low = sub.lower()
                    match = None
                    for w in lr.get("windows") or []:
                        if low in (w.get("title") or "").lower():
                            match = w
                            break
                    if not match:
                        raise ValueError(f"no window title contains {sub!r}")
                    wid = str(match["window_id"])
                fr = focus_window(wid)
                if not fr.get("ok"):
                    raise RuntimeError(fr.get("error", "focus failed"))
                rec["ok"] = True
                rec["window_id"] = wid

            elif op == "screenshot":
                rel = str(raw.get("path", "")).strip()
                if not rel:
                    raise ValueError("screenshot needs path (.png)")
                p = Path(rel).expanduser()
                if not p.is_absolute():
                    p = (cwd / p).resolve()
                else:
                    p = p.resolve()
                reg = raw.get("region")
                reg_d = reg if isinstance(reg, dict) else None
                sr = _screenshot_region(p, reg_d, policy_check)
                if not sr.get("ok"):
                    raise RuntimeError(sr.get("error", "screenshot failed"))
                rec["ok"] = True
                rec["png_path"] = sr.get("png_path")

            elif op == "click":
                x = int(raw["x"])
                y = int(raw["y"])
                btn = str(raw.get("button", "left")).lower()
                clicks = int(raw.get("clicks", 1))
                clicks = max(1, min(3, clicks))
                interval = float(raw.get("click_interval_sec", 0.1))
                interval = max(0.0, min(2.0, interval))
                pyautogui.click(x=x, y=y, clicks=clicks, interval=interval, button=btn)
                rec["ok"] = True

            elif op == "double_click":
                x = int(raw["x"])
                y = int(raw["y"])
                btn = str(raw.get("button", "left")).lower()
                pyautogui.doubleClick(x=x, y=y, button=btn)
                rec["ok"] = True

            elif op == "move":
                x = int(raw["x"])
                y = int(raw["y"])
                dur = float(raw.get("duration_sec", 0.2))
                dur = max(0.0, min(10.0, dur))
                pyautogui.moveTo(x, y, duration=dur)
                rec["ok"] = True

            elif op == "drag":
                x = int(raw["x"])
                y = int(raw["y"])
                dur = float(raw.get("duration_sec", 0.3))
                dur = max(0.05, min(30.0, dur))
                pyautogui.dragTo(x, y, duration=dur, button=str(raw.get("button", "left")))
                rec["ok"] = True

            elif op == "scroll":
                clicks = int(raw.get("clicks", -3))
                clicks = max(-500, min(500, clicks))
                pyautogui.scroll(clicks)
                rec["ok"] = True

            elif op == "type_text":
                text = str(raw.get("text", ""))
                if len(text) > MAX_TYPE_CHARS:
                    raise ValueError(f"text too long (max {MAX_TYPE_CHARS})")
                interval = float(raw.get("interval_sec", 0.0))
                interval = max(0.0, min(1.0, interval))
                pyautogui.write(text, interval=interval)
                rec["ok"] = True

            elif op == "press":
                key = str(raw.get("key", "")).strip()
                if not key:
                    raise ValueError("press needs key")
                presses = int(raw.get("presses", 1))
                presses = max(1, min(20, presses))
                interval = float(raw.get("interval_sec", 0.1))
                pyautogui.press(key, presses=presses, interval=interval)
                rec["ok"] = True

            elif op == "hotkey":
                keys = raw.get("keys")
                if not isinstance(keys, list) or not keys:
                    raise ValueError("hotkey needs keys: string array")
                ks = [str(k).strip() for k in keys if str(k).strip()]
                if not ks or len(ks) > 6:
                    raise ValueError("hotkey keys: 1–6 non-empty strings")
                pyautogui.hotkey(*ks)
                rec["ok"] = True

            elif op == "wait_ms":
                ms = int(raw.get("ms", 0))
                ms = max(0, min(120_000, ms))
                time.sleep(ms / 1000.0)
                rec["ok"] = True

            elif op == "pixel":
                x = int(raw["x"])
                y = int(raw["y"])
                im = pyautogui.screenshot(region=(x, y, 1, 1))
                rgb = im.getpixel((0, 0))
                rec["ok"] = True
                rec["rgb"] = rgb

            else:
                raise ValueError(
                    f"unknown op {op!r}; use list_windows, focus_window, screenshot, "
                    "click, double_click, move, drag, scroll, type_text, press, hotkey, wait_ms, pixel"
                )

            step_results.append(rec)

        except Exception as e:  # noqa: BLE001
            rec["ok"] = False
            rec["error"] = str(e)
            step_results.append(rec)
            return {
                "ok": False,
                "error": f"step {i} ({op}): {e}",
                "failed_step_index": i,
                "step_results": step_results,
                "platform": pf,
            }

    return {
        "ok": True,
        "step_results": step_results,
        "platform": pf,
        "note": "Coordinates are screen pixels. On Linux use X11 + wmctrl/xdotool; Wayland support varies. "
        "Combine screenshot png with vision_analyze_image for visual checks.",
    }
