"""UI driver 基类：抽取 _step / _parse_window_geometry / _click_at 公共能力。

设计原则：
- 优先委托 XdotoolBackend（若已构造），复用其 _ui_lock 串行保护
- legacy 模式自建轻量 _run_managed（兼容 ui_backend=legacy 场景）
- 子类继承 BaseDriver 后只需实现业务方法，无需重复造轮子

消除的重复（源自 xdotool_driver.py 单类 2375 行）：
- 11 处内联 `_step` 闭包：deadline 计算 + max(_STEP_MIN_TIMEOUT_SEC, remaining)
  + asyncio.wait_for 超时包装 + coro.close() 兜底
- 7+ 处 `getwindowgeometry --shell` 输出解析：geom = {} + split("=", 1) + int()
- _click_at 链式命令（mousemove --sync X Y click 1）的重复实现
"""

from __future__ import annotations

import asyncio
import logging
import os
import time
from dataclasses import dataclass
from typing import Any, Awaitable, Optional

logger = logging.getLogger("woc-bridge")

# ---------------------------------------------------------------------------
# 分层超时常量（与 backends/xdotool.py / xdotool_driver.py 保持一致）
#   L1 _CMD_TIMEOUT_SEC      : 单条 xdotool/pgrep/kill 子进程命令超时
#   L2 _STEP_MIN_TIMEOUT_SEC : 单个 UI 步骤（_step）超时下限 = L1 + 2s
# L2 必须 > L1，避免 _step 的 wait_for 先于 _run 内的 L1 触发导致子进程
# 未被 kill 而泄漏。
# ---------------------------------------------------------------------------
_CMD_TIMEOUT_SEC = 5.0
_STEP_MIN_TIMEOUT_SEC = 7.0


@dataclass
class WindowGeom:
    """窗口几何信息（对应 xdotool getwindowgeometry --shell 输出）。

    与 backends/base.py 的 WindowGeometry 区别：本类额外携带 window_id，
    供 driver 层在多次操作间复用窗口 ID，避免重复 search。
    """

    window_id: int
    x: int
    y: int
    width: int
    height: int


class BaseDriver:
    """UI driver 抽象基类。

    子类（WindowDriver / LoginDriver / MessageDriver / MomentDriver /
    ContactDriver）继承本类，自动获得 _step / _parse_window_geometry /
    _click_at 能力，无需重复造轮子。

    使用方式：
        # 优先委托 XdotoolBackend（推荐，复用其 _ui_lock 串行保护）
        backend = XdotoolBackend(display=":1")
        driver = MyDriver(display=":1", backend=backend)

        # legacy 模式（无 backend，自建 _ui_lock + _run_managed）
        driver = MyDriver(display=":1")
    """

    def __init__(
        self,
        display: str,
        backend: Optional[Any] = None,
    ) -> None:
        """初始化 driver。

        Args:
            display: X server display 地址，如 ":1"
            backend: 可选 XdotoolBackend 实例；提供时复用其 _ui_lock 串行保护
                     与 click/type/key 等能力，None 时走 legacy _run_managed
        """
        self._display = display
        self._backend = backend
        # legacy 模式自建 _ui_lock；若 backend 已存在则复用其 _ui_lock
        # （确保 driver 与 backend 共享同一把锁，避免并发点击焦点错乱）
        if backend is not None and hasattr(backend, "_ui_lock"):
            self._ui_lock = backend._ui_lock
        else:
            self._ui_lock = asyncio.Lock()

    # ------------------------------------------------------------------
    # 环境与子进程管理
    # ------------------------------------------------------------------
    def _env(self) -> dict:
        """返回带 DISPLAY 的环境副本。"""
        env = dict(os.environ)
        env["DISPLAY"] = self._display
        return env

    async def _run_managed(self, cmd: list[str]) -> tuple[int, bytes, bytes]:
        """legacy 模式执行 xdotool 命令（无 backend 时使用）。

        - 持有 _ui_lock 串行保护（与 XdotoolBackend 行为一致）
        - asyncio.create_subprocess_exec 异步执行，不阻塞 event loop
        - L1 (_CMD_TIMEOUT_SEC) 超时兜底，超时/取消时 kill+wait 收尸，
          避免僵尸进程残留占住管道/资源
        - 返回 (returncode, stdout, stderr)；超时抛 asyncio.TimeoutError
          （由调用方包装为 BridgeError）

        Args:
            cmd: 命令及其参数列表，如 ["xdotool", "search", "--name", "微信"]
        """
        async with self._ui_lock:
            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                env=self._env(),
            )
            try:
                stdout, stderr = await asyncio.wait_for(
                    proc.communicate(), timeout=_CMD_TIMEOUT_SEC
                )
            except (asyncio.TimeoutError, asyncio.CancelledError):
                # 先 kill 子进程再收尸，避免僵尸进程残留占住管道/资源
                proc.kill()
                try:
                    await asyncio.wait_for(proc.wait(), timeout=5.0)
                except asyncio.TimeoutError:
                    logger.error(
                        "[ui-driver] proc.wait() 超时，孤儿进程残留: %s",
                        cmd[0] if cmd else "?",
                    )
                # 抛原始 TimeoutError/CancelledError，交由调用方包装
                raise
            except Exception:
                # 兜底：确保任何异常路径下子进程都被回收
                if proc.returncode is None:
                    proc.kill()
                    try:
                        await asyncio.wait_for(proc.wait(), timeout=5.0)
                    except asyncio.TimeoutError:
                        logger.error(
                            "[ui-driver] proc.wait() 超时: %s",
                            cmd[0] if cmd else "?",
                        )
                raise
            return (proc.returncode, stdout, stderr)

    # ------------------------------------------------------------------
    # 公共能力：_step / _parse_window_geometry / _click_at
    # ------------------------------------------------------------------
    async def _step(
        self,
        coro: Awaitable,
        desc: str,
        deadline: Optional[float] = None,
    ) -> Any:
        """通用超时包装方法（替换 11 处内联 _step 闭包）。

        语义对齐 xdotool_driver.py 中的内联闭包：
        - deadline 为绝对时间戳（time.monotonic() 返回值），None 时使用
          _STEP_MIN_TIMEOUT_SEC 作为固定超时
        - 剩余时间 <= 0 时关闭未启动的协程并抛 asyncio.TimeoutError，
          避免 "coroutine was never awaited" 警告
        - 剩余时间 < _STEP_MIN_TIMEOUT_SEC 时取下限（max 兜底），
          避免 L2 先于 _run 内的 L1 触发导致子进程未被 kill 而泄漏
        - 超时抛 asyncio.TimeoutError（由调用方包装为 BridgeError，
          保持基类与 models.BridgeError 解耦）

        Args:
            coro: 待执行的协程/可等待对象
            desc: 步骤描述，用于日志与错误信息
            deadline: 绝对超时时间戳（time.monotonic() 返回值）；
                      None 时使用 _STEP_MIN_TIMEOUT_SEC

        Raises:
            asyncio.TimeoutError: 步骤超时或 deadline 已过
        """
        if deadline is None:
            # 无整体流程 deadline 时，使用 L2 下限作为固定超时
            timeout: float = _STEP_MIN_TIMEOUT_SEC
        else:
            # 必须用 time.monotonic() 与源文件 deadline 语义对齐
            # （asyncio.get_event_loop().time() 与 time.monotonic() epoch 不同，混用会误判）
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                # deadline 已过：关闭未启动的协程，避免 "never awaited" 警告
                # （对齐 xdotool_driver.py L1290-1294 / L1408-1412 的 coro.close() 兜底）
                if asyncio.iscoroutine(coro):
                    coro.close()
                logger.warning("[ui-driver] step 已超时（deadline 已过）: %s", desc)
                raise asyncio.TimeoutError()
            timeout = max(_STEP_MIN_TIMEOUT_SEC, remaining)

        logger.info("[ui-driver] step start: %s", desc)
        try:
            result = await asyncio.wait_for(coro, timeout=timeout)
        except asyncio.TimeoutError:
            logger.error("[ui-driver] step timeout: %s (timeout=%.2fs)", desc, timeout)
            raise
        logger.info("[ui-driver] step done: %s", desc)
        return result

    @staticmethod
    def _parse_window_geometry(output: str) -> Optional[WindowGeom]:
        """解析 `xdotool getwindowgeometry --shell` 输出（替换 7+ 处重复解析）。

        输入示例：
            WINDOW=12345
            X=10
            Y=20
            WIDTH=800
            HEIGHT=600

        对齐 xdotool_driver.py 中的内联解析模式：
            geom = {}
            for line in stdout.decode(errors="ignore").splitlines():
                if "=" in line:
                    k, v = line.split("=", 1)
                    geom[k.strip()] = v.strip()

        Args:
            output: getwindowgeometry --shell 的 stdout 文本（已 decode）

        Returns:
            WindowGeom 或 None（解析失败时返回 None，由调用方决定是否抛错）
        """
        geom: dict[str, str] = {}
        for line in output.splitlines():
            if "=" in line:
                k, v = line.split("=", 1)
                geom[k.strip()] = v.strip()
        try:
            return WindowGeom(
                window_id=int(geom.get("WINDOW", 0)),
                x=int(geom.get("X", 0)),
                y=int(geom.get("Y", 0)),
                width=int(geom.get("WIDTH", 0)),
                height=int(geom.get("HEIGHT", 0)),
            )
        except (ValueError, TypeError):
            return None

    async def _click_at(self, x: int, y: int) -> None:
        """链式命令点击（替换原 _click_at）。

        优先委托 backend.click(x, y)（若已构造，复用其 _ui_lock 串行保护），
        否则走 legacy xdotool 链式命令。

        链式命令（mousemove --sync X Y click 1）单条子进程完成移动+点击，
        相比分两条命令可少一次子进程创建与 L1 超时窗口，降低中途超时
        导致“鼠标移动到位但未点击”的半成品状态概率。

        Args:
            x: 目标 X 坐标（绝对坐标）
            y: 目标 Y 坐标（绝对坐标）

        Raises:
            RuntimeError: legacy 模式下 xdotool 命令失败
        """
        # 优先委托 backend（推荐路径，复用其 _ui_lock 与错误处理）
        if self._backend is not None and hasattr(self._backend, "click"):
            await self._backend.click(x, y)
            return

        # legacy 模式：单条 xdotool 链式命令完成移动+点击
        logger.info("[ui-driver] click at (%d, %d)", x, y)
        rc, _, stderr = await self._run_managed(
            ["xdotool", "mousemove", "--sync", str(x), str(y), "click", "1"]
        )
        if rc != 0:
            err = stderr.decode(errors="ignore").strip() if stderr else "unknown"
            # 基类不依赖 BridgeError，抛 RuntimeError；子类可 catch 后包装
            raise RuntimeError(f"点击 ({x},{y}) 失败: {err}")
        logger.info("[ui-driver] click done rc=%s", rc)
