"""L3 Actions 原子动作库：find / click / type / wait / screenshot。

Actions 是 L1 Backend 与 L2 Locator 的组合层：给定 kind 与窗口几何，
Locator 解析为 ElementHandle（图像优先、几何兜底），Backend 据此执行点击/输入。
L4 Flow 通过 Actions 组合出完整业务流程（如 SendTextFlow）。

策略：
- 图像路径（P2 启用）：sel.by_image 非空且 locator.is_circuited 为 False 且 opencv 非 None 时优先
- 命中：record_image_success，返回 strategy=image
- 未命中：record_image_failure；require_image=True 抛 ElementNotFoundError；否则回退 _geom_find
- 几何兜底（P1 默认）：按 GeomSpec 计算绝对坐标
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Optional

from woc_bridge.ui.backends.base import BackendProtocol, WindowGeometry
from woc_bridge.ui.locators.base import ElementHandle, Selector
from woc_bridge.ui.locators.registry import LocatorRegistry

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


class ElementNotFoundError(Exception):
    """元素未找到（图像匹配失败且 require_image=True，或几何规格缺失）。

    TODO: P3 时统一到 errors.py 的 FlowError(code="ELEMENT_NOT_FOUND")。
    """

    pass


class Actions:
    """L3 原子动作库：组合 Backend（L1）与 Locator（L2）。

    所有方法均为 async，由 L4 Flow 编排调用。
    """

    def __init__(
        self, backend: BackendProtocol, locator: LocatorRegistry
    ) -> None:
        """初始化 Actions。

        Args:
            backend: L1 Backend 实例（如 XdotoolBackend）
            locator: L2 LocatorRegistry 实例
        """
        self.backend = backend
        self.locator = locator

    # ------------------------------------------------------------------
    # 元素查找
    # ------------------------------------------------------------------
    async def find_element(
        self, kind: str, win_geom: WindowGeometry
    ) -> ElementHandle:
        """查找元素，优先图像匹配，失败回退几何兜底。

        Args:
            kind: 元素类别键（如 search_box / send_button）
            win_geom: 当前窗口几何（绝对坐标 + 宽高）

        Returns:
            ElementHandle：命中元素（含绝对坐标与策略）

        Raises:
            ElementNotFoundError: require_image=True 且图像匹配失败，
                                  或 by_geom 为空且无图像命中
        """
        sel = self.locator.get(kind)

        # 图像路径（P2 启用，P1 阶段 opencv=None 直接跳过）
        if (
            sel.by_image
            and not self.locator.is_circuited(kind)
            and self.locator.opencv is not None
        ):
            shot = await self.backend.screenshot()
            roi = (win_geom.x, win_geom.y, win_geom.width, win_geom.height)
            try:
                result = await self.locator.opencv.find_template(
                    shot,
                    sel.by_image,
                    threshold=sel.threshold,
                    roi=roi,
                )
            except Exception as exc:
                logger.warning(
                    "[action] opencv.find_template exception kind=%s: %s",
                    kind,
                    exc,
                )
                result = None

            if result is not None:
                # result = (x, y, w, h, confidence)（绝对坐标，含 ROI 偏移）
                x, y, w, h, conf = result
                self.locator.record_image_success(kind)
                return ElementHandle(
                    selector=sel,
                    x=x + w // 2,
                    y=y + h // 2,
                    w=w,
                    h=h,
                    confidence=conf,
                    strategy="image",
                )

            # 图像未命中
            self.locator.record_image_failure(kind)
            logger.warning(
                "[action] image match failed kind=%s, fall back to geom", kind
            )
            if sel.require_image:
                raise ElementNotFoundError(
                    f"image_match_failed kind={kind}"
                )

        return self._geom_find(sel, win_geom)

    async def click_element(
        self, kind: str, win_geom: WindowGeometry
    ) -> ElementHandle:
        """查找并点击元素，返回 ElementHandle。

        Args:
            kind: 元素类别键
            win_geom: 当前窗口几何

        Returns:
            命中的 ElementHandle
        """
        elem = await self.find_element(kind, win_geom)
        await self.backend.click(elem.x, elem.y)
        logger.info(
            "[action] click %s at (%d,%d) strategy=%s conf=%.3f",
            kind,
            elem.x,
            elem.y,
            elem.strategy,
            elem.confidence,
        )
        return elem

    async def type_into(
        self,
        kind: str,
        text: str,
        win_geom: WindowGeometry,
        clear_first: bool = True,
    ) -> None:
        """点击元素聚焦后输入文本，可选先清空。

        Args:
            kind: 元素类别键（通常为 input_box）
            text: 要输入的文本
            win_geom: 当前窗口几何
            clear_first: 是否先 BackSpace×30 清空旧内容
        """
        await self.click_element(kind, win_geom)
        if clear_first:
            await self.backend.key_press("BackSpace", repeat=30)
        await self.backend.type_text(text)

    async def wait_for(
        self,
        kind: str,
        win_geom: WindowGeometry,
        timeout: float = 5.0,
        interval: float = 0.3,
    ) -> Optional[ElementHandle]:
        """轮询等待元素出现，超时返回 None。

        Args:
            kind: 元素类别键
            win_geom: 当前窗口几何
            timeout: 最大等待秒数
            interval: 轮询间隔秒数

        Returns:
            ElementHandle 或 None（超时）
        """
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            try:
                elem = await self.find_element(kind, win_geom)
                if elem:
                    return elem
            except ElementNotFoundError:
                pass
            await asyncio.sleep(interval)
        return None

    async def screenshot(self) -> bytes:
        """截图代理方法。"""
        return await self.backend.screenshot()

    # ------------------------------------------------------------------
    # 几何兜底
    # ------------------------------------------------------------------
    def _geom_find(
        self, selector: Selector, win_geom: WindowGeometry
    ) -> ElementHandle:
        """按 GeomSpec 计算绝对坐标，返回 ElementHandle。

        简化策略：所有 relative_to 模式统一用
            base_x = win_geom.x + win_geom.width * g.x_ratio + g.x_offset
            base_y = win_geom.y + win_geom.height * g.y_ratio + g.y_offset
        relative_to 仅作语义占位，offset 已含正负号区分方向
        （如 send_button 用 x_offset=-60 表示距右边缘 60px）。

        Args:
            selector: 元素选择器（必须含 by_geom）
            win_geom: 当前窗口几何

        Returns:
            ElementHandle（strategy=geom, confidence=1.0）

        Raises:
            ElementNotFoundError: selector.by_geom 为空
        """
        if selector.by_geom is None:
            raise ElementNotFoundError(
                f"no geom spec kind={selector.kind}"
            )
        g = selector.by_geom
        base_x = win_geom.x + win_geom.width * g.x_ratio + g.x_offset
        base_y = win_geom.y + win_geom.height * g.y_ratio + g.y_offset
        return ElementHandle(
            selector=selector,
            x=int(base_x),
            y=int(base_y),
            w=0,
            h=0,
            confidence=1.0,
            strategy="geom",
        )
