"""L2 LocatorRegistry：按 (app_version, resolution) 加载 YAML profile + 熔断接口。

加载优先级：精确匹配 > 版本匹配 > default 兜底。
熔断接口：record_image_failure / record_image_success / is_circuited，
P1 阶段 no-op（opencv=None），P2 启用图像路径后生效。
"""

from __future__ import annotations

import logging
import os
import time
from typing import Optional

import yaml

from woc_bridge.ui.locators.base import GeomSpec, Selector

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


class LocatorRegistry:
    """选择器注册表。

    Attributes:
        profile_dir: YAML profile 目录绝对路径
        opencv: OpenCVBackend 实例（P1 阶段为 None，P2 启用图像路径）
        selectors: kind -> Selector 映射
        theme: 当前主题（light / dark），P1 阶段固定 light
    """

    # 熔断参数：连续失败 10 次熔断 5 分钟
    _FAIL_THRESHOLD = 10
    _CIRCUIT_OPEN_SEC = 300.0

    def __init__(self, profile_dir: str, opencv=None) -> None:
        """初始化注册表。

        Args:
            profile_dir: YAML profile 目录绝对路径
            opencv: OpenCVBackend 实例（P1 阶段为 None）
        """
        self.profile_dir = profile_dir
        self.opencv = opencv
        self.selectors: dict[str, Selector] = {}
        self.theme: str = "light"
        self._fail_counts: dict[str, int] = {}
        self._circuit_open_until: dict[str, float] = {}

    # ------------------------------------------------------------------
    # Profile 加载
    # ------------------------------------------------------------------
    def load(self, app_version: str, resolution: tuple[int, int]) -> None:
        """按精确 > 版本 > default 优先级加载 YAML profile。

        Args:
            app_version: 微信版本号，如 "4.0"
            resolution: (width, height) 元组，如 (1920, 1080)
        """
        candidates = [
            f"wechat_{app_version}_{resolution[0]}x{resolution[1]}.yaml",
            f"wechat_{app_version}.yaml",
            "default.yaml",
        ]
        for name in candidates:
            path = os.path.join(self.profile_dir, name)
            if os.path.exists(path):
                logger.info(
                    "[locator] load profile: %s (app_version=%s resolution=%s)",
                    name,
                    app_version,
                    resolution,
                )
                self._load_yaml(path)
                return
        logger.warning(
            "[locator] no profile found in %s, fall back to built-in defaults "
            "(app_version=%s resolution=%s)",
            self.profile_dir,
            app_version,
            resolution,
        )
        self._load_defaults()

    def set_theme(self, theme: str) -> None:
        """切换主题（light / dark）。"""
        if theme != self.theme:
            self.theme = theme
            logger.info("[locator] theme switched to %s", theme)

    # ------------------------------------------------------------------
    # 选择器访问 + 熔断
    # ------------------------------------------------------------------
    def get(self, kind: str) -> Selector:
        """返回 kind 对应 Selector，无则返回 _default_selector(kind)。"""
        return self.selectors.get(kind) or self._default_selector(kind)

    def is_circuited(self, kind: str) -> bool:
        """检查 kind 是否处于熔断期。

        熔断到期时自动重置失败计数，使半开状态允许完整次数的试探，
        而非首次失败即重新熔断。
        """
        deadline = self._circuit_open_until.get(kind, 0)
        if time.monotonic() < deadline:
            return True
        # 熔断已到期：清理熔断标记和失败计数，进入半开状态
        if kind in self._circuit_open_until:
            self._circuit_open_until.pop(kind, None)
            self._fail_counts[kind] = 0
            logger.info(
                "[locator] kind=%s circuit recovered to HALF_OPEN (fail_count reset)",
                kind,
            )
        return False

    def record_image_failure(self, kind: str) -> None:
        """记录一次图像匹配失败。连续 _FAIL_THRESHOLD 次后熔断 _CIRCUIT_OPEN_SEC 秒。"""
        self._fail_counts[kind] = self._fail_counts.get(kind, 0) + 1
        if self._fail_counts[kind] >= self._FAIL_THRESHOLD:
            self._circuit_open_until[kind] = time.monotonic() + self._CIRCUIT_OPEN_SEC
            logger.warning(
                "[locator] kind=%s image match circuit OPEN for %.0fs "
                "(fail_count=%d)",
                kind,
                self._CIRCUIT_OPEN_SEC,
                self._fail_counts[kind],
            )

    def record_image_success(self, kind: str) -> None:
        """记录一次图像匹配成功，重置失败计数与熔断。"""
        self._fail_counts[kind] = 0
        self._circuit_open_until.pop(kind, None)

    # ------------------------------------------------------------------
    # YAML 解析与默认值
    # ------------------------------------------------------------------
    def _load_yaml(self, path: str) -> None:
        """用 yaml.safe_load 加载 YAML 并填充 self.selectors。

        YAML 结构（每个 key 对应一个 Selector）：
            search_box:
              by_image: search_box.png      # str，可省略
              by_geom:                       # dict，可省略
                relative_to: window
                x_ratio: 0.08
                y_ratio: 0.05
                x_offset: 0
                y_offset: 0
              threshold: 0.80
              require_image: false
              description: 搜索框
        """
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = yaml.safe_load(f) or {}
        except (OSError, yaml.YAMLError) as exc:
            logger.error("[locator] load yaml failed: %s (%s)", path, exc)
            self._load_defaults()
            return

        self.selectors.clear()
        for kind, spec in data.items():
            if not isinstance(spec, dict):
                continue
            by_image = spec.get("by_image")
            by_geom_raw = spec.get("by_geom")
            by_geom: Optional[GeomSpec] = None
            if isinstance(by_geom_raw, dict):
                by_geom = GeomSpec(
                    relative_to=by_geom_raw.get("relative_to", "window"),
                    x_ratio=float(by_geom_raw.get("x_ratio", 0.0)),
                    y_ratio=float(by_geom_raw.get("y_ratio", 0.0)),
                    x_offset=int(by_geom_raw.get("x_offset", 0)),
                    y_offset=int(by_geom_raw.get("y_offset", 0)),
                )
            self.selectors[kind] = Selector(
                kind=kind,
                by_image=by_image,
                by_geom=by_geom,
                threshold=float(spec.get("threshold", 0.80)),
                require_image=bool(spec.get("require_image", False)),
                description=str(spec.get("description", "")),
            )
        logger.info(
            "[locator] loaded %d selectors from %s", len(self.selectors), path
        )

    def _default_selector(self, kind: str) -> Selector:
        """返回 kind 的兜底 Selector（仅 description 标注 default）。"""
        return Selector(kind=kind, description=f"default {kind}")

    def _load_defaults(self) -> None:
        """从 xdotool_driver.py 现有硬编码常量迁移默认几何兜底。

        参考 spec 4.2 节 YAML 示例与项目记忆中的硬编码值，
        覆盖 6 个核心元素：search_box / send_button / input_box /
        input_box_empty / search_result_first / main_view。

        relative_to 字段仅作语义占位，实际坐标计算统一用 ratio + offset
        （offset 已含正负号区分方向，详见 Actions._geom_find）。
        """
        defaults = {
            "search_box": Selector(
                kind="search_box",
                by_geom=GeomSpec(
                    relative_to="window",
                    x_ratio=0.08,
                    y_ratio=0.05,
                    x_offset=0,
                    y_offset=0,
                ),
                description="左侧顶部搜索框",
            ),
            "send_button": Selector(
                kind="send_button",
                by_geom=GeomSpec(
                    relative_to="window_bottom_right",
                    x_ratio=1.0,
                    y_ratio=1.0,
                    x_offset=-60,
                    y_offset=-30,
                ),
                description="右下角发送按钮",
            ),
            "input_box": Selector(
                kind="input_box",
                by_geom=GeomSpec(
                    relative_to="window",
                    x_ratio=0.65,
                    y_ratio=0.93,
                    x_offset=0,
                    y_offset=0,
                ),
                description="聊天输入框",
            ),
            "input_box_empty": Selector(
                kind="input_box_empty",
                by_geom=GeomSpec(
                    relative_to="window",
                    x_ratio=0.65,
                    y_ratio=0.93,
                    x_offset=0,
                    y_offset=0,
                ),
                description="空输入框灰色提示态（发送成功后校验）",
            ),
            "search_result_first": Selector(
                kind="search_result_first",
                by_geom=GeomSpec(
                    relative_to="window",
                    x_ratio=0.08,
                    y_ratio=0.11,
                    x_offset=0,
                    y_offset=0,
                ),
                description="搜索结果第一项",
            ),
            "main_view": Selector(
                kind="main_view",
                by_geom=GeomSpec(
                    relative_to="window",
                    x_ratio=0.65,
                    y_ratio=0.50,
                    x_offset=0,
                    y_offset=0,
                ),
                description="主聊天视图（post_verify 校验用）",
            ),
        }
        self.selectors.clear()
        self.selectors.update(defaults)
        logger.info(
            "[locator] loaded %d built-in default selectors", len(self.selectors)
        )
