"""窗口管理子 driver：窗口查找 / 激活 / 几何 / 截图 / 启动清场。

从 xdotool_driver.py 单类中拆出的窗口域方法，继承 BaseDriver 复用
_step / _parse_window_geometry / _click_at 公共能力。

方法清单（与原 XdotoolDriver 签名一致）：
- find_wechat_window() -> Optional[int]
- activate_window(window_id=None) -> None
- get_window_geometry(window_id=None) -> WindowGeom   （新增公开方法）
- screenshot() -> bytes                                （新增，对齐 backends 模式）
- _full_cleanup_on_startup() -> bool
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Optional

from woc_bridge.models import BridgeError
from woc_bridge.ui.drivers.base import BaseDriver, WindowGeom


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


class WindowDriver(BaseDriver):
    """窗口管理子 driver。"""

    def __init__(self, display: str, backend=None) -> None:
        super().__init__(display, backend)
        # 缓存最近一次找到的主窗口信息，用于抑制连续重复的 info 日志
        self._last_window_key: Optional[tuple[int, int, int, int, int, int]] = None

    # ------------------------------------------------------------------
    # 命令执行包装（BaseDriver._run_managed 返回 tuple 且抛 TimeoutError，
    # 这里包装为 BridgeError 以保持与原 XdotoolDriver._run 语义一致）
    # ------------------------------------------------------------------
    async def _run(
        self,
        args: list[str],
        *,
        input_bytes: Optional[bytes] = None,
    ) -> tuple[int, bytes, bytes]:
        """执行一条命令并返回 (returncode, stdout, stderr)。

        委托 BaseDriver._run_managed（持 _ui_lock 串行 + L1 超时兜底），
        TimeoutError 转 BridgeError(code=SEND_FAILED)，保持原 _run 语义。
        input_bytes 参数保留以兼容原签名，当前实现未使用（无方法依赖）。
        """
        try:
            return await self._run_managed(args)
        except asyncio.TimeoutError as exc:
            logger.error("[ui] command timeout: %s", args[0] if args else "?")
            raise BridgeError(
                code="SEND_FAILED",
                message=f"命令超时: {args[0] if args else '?'}",
            ) from exc

    async def _key(self, key: str, repeat: int = 1) -> None:
        """执行 xdotool key [--repeat N] <key>。"""
        logger.info("[ui] key: %s repeat=%d", key, repeat)
        args = ["xdotool", "key"]
        if repeat > 1:
            args.extend(["--repeat", str(repeat)])
        args.append(key)
        rc, _, _ = await self._run(args)
        logger.info("[ui] key %s -> rc=%s", key, rc)

    # ------------------------------------------------------------------
    # 窗口与进程检测
    # ------------------------------------------------------------------
    async def _find_wechat_pids(self) -> list[int]:
        """查找微信主进程 PID 列表。

        微信可执行文件常见路径：
        - /config/wechat/opt/wechat/wechat（安装后的实际路径）
        - /opt/wechat/wechat
        通过 pgrep 按命令行匹配，避免依赖固定路径。

        Returns:
            PID 列表（可能为空）
        """
        pids: list[int] = []
        for pattern in ["wechat", "/config/wechat/opt/wechat"]:
            rc, stdout, _ = await self._run(["pgrep", "-f", pattern])
            if rc != 0:
                continue
            for line in stdout.decode(errors="ignore").splitlines():
                line = line.strip()
                if not line:
                    continue
                try:
                    pids.append(int(line))
                except ValueError:
                    continue
        # 去重保持顺序
        seen = set()
        unique: list[int] = []
        for pid in pids:
            if pid not in seen:
                seen.add(pid)
                unique.append(pid)
        return unique

    async def find_wechat_window(self) -> Optional[int]:
        """查找微信主窗口 ID。

        策略：
        1. 优先通过 wechat 进程 PID 搜索窗口（最可靠，微信主窗口 name/class 可能为空）；
        2. 再用 name/class 搜索兜底；
        3. 过滤掉面积极小的隐藏/装饰/加载窗口；
        4. 多个候选时取面积最大者。

        Returns:
            窗口 ID（int），找不到返回 None
        """
        candidates: list[tuple[int, int, WindowGeom]] = []
        seen_wids: set[int] = set()

        async def add_candidate(wid: int) -> None:
            """获取窗口几何并加入候选（去重）。"""
            if wid in seen_wids:
                return
            seen_wids.add(wid)
            rc, gstdout, _ = await self._run(
                ["xdotool", "getwindowgeometry", "--shell", str(wid)]
            )
            if rc != 0:
                return
            geom = self._parse_window_geometry(gstdout.decode(errors="ignore"))
            if geom is None:
                return
            area = geom.width * geom.height
            # 过滤极小/无效的窗口
            if area < 10000 or geom.width < 100 or geom.height < 100:
                return
            candidates.append((area, wid, geom))

        # 1. 通过 wechat 进程 PID 搜索窗口（优先）
        for pid in await self._find_wechat_pids():
            rc, stdout, _ = await self._run(
                ["xdotool", "search", "--pid", str(pid)]
            )
            if rc != 0:
                continue
            for line in stdout.decode(errors="ignore").splitlines():
                wid_str = line.strip()
                if not wid_str:
                    continue
                try:
                    await add_candidate(int(wid_str))
                except ValueError:
                    continue

        # 2. name/class 兜底
        searches = [
            ["xdotool", "search", "--onlyvisible", "--name", "微信"],
            ["xdotool", "search", "--onlyvisible", "--class", "wechat"],
            ["xdotool", "search", "--name", "微信"],
            ["xdotool", "search", "--class", "wechat"],
        ]
        for args in searches:
            returncode, stdout, _ = await self._run(args)
            if returncode != 0:
                continue
            for line in stdout.decode(errors="ignore").splitlines():
                wid_str = line.strip()
                if not wid_str:
                    continue
                try:
                    await add_candidate(int(wid_str))
                except ValueError:
                    continue

        if not candidates:
            if self._last_window_key is not None:
                logger.warning("[window] 未找到有效微信主窗口")
                self._last_window_key = None
            else:
                logger.debug("[window] 仍未找到有效微信主窗口")
            return None

        # 取面积最大者（微信主窗口通常远大于通知/托盘窗口）
        candidates.sort(key=lambda x: x[0], reverse=True)
        area, wid, geom = candidates[0]
        key = (wid, geom.x, geom.y, geom.width, geom.height, area)
        if key == self._last_window_key:
            logger.debug(
                "[window] 主窗口未变化 id=%d area=%d geom=(%d,%d %dx%d)",
                wid, area, geom.x, geom.y, geom.width, geom.height,
            )
        else:
            logger.info(
                "[window] 选中微信主窗口 id=%d area=%d geom=(%d,%d %dx%d)",
                wid, area, geom.x, geom.y, geom.width, geom.height,
            )
            self._last_window_key = key
        return wid

    async def activate_window(self, window_id: Optional[int] = None) -> None:
        """激活微信窗口。

        使用非阻塞 windowactivate 避免 --sync 在 VNC 无人操作时死等，
        之后轮询 getactivewindow 确认焦点是否真正落到目标窗口。

        Args:
            window_id: 指定窗口 ID；为 None 时自动查找
        """
        if window_id is None:
            window_id = await self.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口，无法激活",
            )
        await self._run(["xdotool", "windowactivate", str(window_id)])

        deadline = time.monotonic() + 2.0
        while time.monotonic() < deadline:
            rc, stdout, _ = await self._run(["xdotool", "getactivewindow"])
            if rc == 0:
                active_text = stdout.decode(errors="ignore").strip()
                try:
                    if int(active_text) == window_id:
                        return
                except ValueError:
                    pass
            await asyncio.sleep(0.1)
        logger.warning(
            "[window] activate_window: focus verify timeout id=%d, continue",
            window_id,
        )

    async def get_window_geometry(
        self, window_id: Optional[int] = None
    ) -> WindowGeom:
        """获取微信窗口几何信息，返回 WindowGeom。

        新增公开方法：原 XdotoolDriver 仅有私有 _get_window_geometry（返回
        tuple），本方法对外暴露 WindowGeom 并复用 BaseDriver._parse_window_geometry
        消除内联解析重复。

        Args:
            window_id: 指定窗口 ID；为 None 时自动查找

        Raises:
            BridgeError(WINDOW_NOT_FOUND): 找不到微信窗口
            BridgeError(SEND_FAILED): 获取/解析几何信息失败
        """
        if window_id is None:
            window_id = await self.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口",
            )
        returncode, stdout, _ = await self._run(
            ["xdotool", "getwindowgeometry", "--shell", str(window_id)]
        )
        if returncode != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message="无法获取微信窗口几何信息",
            )
        geom = self._parse_window_geometry(stdout.decode(errors="ignore"))
        if geom is None:
            raise BridgeError(
                code="SEND_FAILED",
                message="解析窗口几何信息失败",
            )
        return geom

    async def screenshot(self) -> bytes:
        """截取整个 X 屏幕为 PNG bytes。

        新增方法：原 XdotoolDriver 无 screenshot，仅 _debug_screenshot 写盘。
        本方法对齐 woc_bridge.ui.backends.xdotool.XdotoolBackend.screenshot 的
        scrot -o - 模式，输出 PNG bytes 到内存，不落盘。
        """
        async with self._ui_lock:
            proc = await asyncio.create_subprocess_exec(
                "scrot",
                "-o",
                "-",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                env=self._env(),
            )
            try:
                stdout, stderr = await asyncio.wait_for(
                    proc.communicate(), timeout=5.0
                )
            except (asyncio.TimeoutError, asyncio.CancelledError):
                proc.kill()
                try:
                    await asyncio.wait_for(proc.wait(), timeout=5.0)
                except asyncio.TimeoutError:
                    logger.error("[ui-driver] scrot proc.wait() timeout after kill")
                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] scrot proc.wait() timeout after kill")
                raise
            if proc.returncode != 0:
                err = stderr.decode(errors="ignore").strip() if stderr else "unknown"
                raise BridgeError(
                    code="SEND_FAILED",
                    message=f"截图失败: {err}",
                )
            return stdout

    # ------------------------------------------------------------------
    # 内部几何工具
    # ------------------------------------------------------------------
    async def _get_window_geometry(self) -> tuple[int, int, int, int]:
        """获取微信窗口几何信息，返回 (win_x, win_y, win_w, win_h)。

        统一封装 getwindowgeometry --shell 解析逻辑，复用
        BaseDriver._parse_window_geometry 消除重复代码。
        """
        window_id = await self.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口",
            )
        returncode, stdout, _ = await self._run(
            ["xdotool", "getwindowgeometry", "--shell", str(window_id)]
        )
        if returncode != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message="无法获取微信窗口几何信息",
            )
        geom = self._parse_window_geometry(stdout.decode(errors="ignore"))
        if geom is None:
            raise BridgeError(
                code="SEND_FAILED",
                message="解析窗口几何信息失败",
            )
        return (geom.x, geom.y, geom.width, geom.height)

    # ------------------------------------------------------------------
    # 启动清场（P1 高可用修复）
    # ------------------------------------------------------------------
    async def _full_cleanup_on_startup(self) -> bool:
        """启动时全量清场（3 次重试）。

        上次崩溃可能残留脏状态（搜索框未关、弹窗未处理、焦点错位等），
        直接进入发送流程会放大故障。连续重置 3 次，仍失败则告警但不阻塞
        启动（返回 False，由调用方决定是否告警）。

        Returns:
            True 表示清场成功（或窗口未找到直接跳过）；False 表示 3 次重置均失败，
            可能需要人工 VNC 接入排查。
        """
        # 1. 获取窗口几何；窗口未找到时跳过清场（不阻塞启动）
        try:
            win_geom = await self._get_window_geometry()
        except BridgeError as e:
            logger.warning("启动清场: 微信窗口未找到，跳过 (%s)", e.message)
            return True

        # 2. 连续重置 3 次
        for attempt in range(3):
            if await self._reset_to_idle(win_geom):
                logger.info("启动清场成功 (attempt=%d)", attempt + 1)
                return True
            logger.warning("启动清场失败 (attempt=%d)", attempt + 1)

        # 3. 3 次均失败，告警但不抛异常（不阻塞启动）
        logger.error("启动清场 3 次失败，可能需要人工 VNC 接入")
        return False

    async def _reset_to_idle(self, win_geom: tuple[int, int, int, int]) -> bool:
        """安全重置到 IDLE 状态（幂等，失败不抛异常）。

        多策略清场，逐层剥离可能残留的脏状态：
          1. Esc×3：关闭弹窗/搜索覆盖层
          2. 点击窗口左上角空白区域：退 webview 焦点，避免焦点停在输入框
          3. BackSpace×30：清空可能残留的输入框内容
          4. Esc×2：兜底关闭剩余弹窗

        Args:
            win_geom: _get_window_geometry 返回的 (win_x, win_y, win_w, win_h)

        Returns:
            True 表示重置流程执行完成；False 表示中途异常（需重试或人工介入）
        """
        win_x, win_y, _win_w, _win_h = win_geom
        try:
            # 1. Esc×3 关闭弹窗/搜索覆盖层
            for _ in range(3):
                await self._key("Escape")
                await asyncio.sleep(0.3)
            # 2. 点击窗口左上角空白区域，退出 webview 焦点（链式命令点击）
            await self._click_at(win_x + 10, win_y + 10)
            await asyncio.sleep(0.3)
            # 3. BackSpace×30 清空可能残留的输入框内容
            await self._key("BackSpace", repeat=30)
            await asyncio.sleep(0.3)
            # 4. Esc×2 兜底关闭剩余弹窗
            for _ in range(2):
                await self._key("Escape")
                await asyncio.sleep(0.3)
            return True
        except Exception as e:
            logger.error("_reset_to_idle 异常: %r", e)
            return False
