#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：page_url_service.py
模块描述：页面识别服务，根据用例步骤识别目标页面，返回直达 URL（跳过中间导航点击）

作者：czj
创建日期：2026-08-12

设计说明：
- 页面URL映射表（page_url_mapping.json）配置：页面识别规则 + 直达 URL + 中间导航步骤跳过规则
- 识别策略：扫描用例步骤，找"入口步骤"（如"点击进入新建会议"），
  用步骤名/选择器关键词匹配页面映射表，命中则返回该页面的直达 URL
- 容错：JSON 缺失/损坏时自动禁用，静默回退到原有逐步点击逻辑
"""

import json
import logging
import os
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# 默认映射表文件路径（相对 backend/ 工作目录）
DEFAULT_MAPPING_FILE = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "data",
    "page_url_mapping.json",
)


class PageUrlService:
    """
    页面识别服务

    从页面URL映射表 JSON 加载页面定义，根据用例步骤识别目标页面，
    返回直达 URL 和需要跳过的中间导航步骤。

    Attributes:
        mapping_file (str): 映射表 JSON 文件路径
        _pages (dict): 已加载的页面映射 {page_id: page_config}
        _enabled (bool): 映射表是否可用（JSON 缺失/损坏时为 False）
    """

    def __init__(self, mapping_file: str = DEFAULT_MAPPING_FILE):
        self.mapping_file = mapping_file
        self._pages: Dict[str, Dict[str, Any]] = {}
        self._enabled = False
        self._load()

    # ==================== 加载 ====================

    def _load(self) -> None:
        """加载页面URL映射表 JSON 文件，失败时自动禁用"""
        self._pages = {}
        self._enabled = False

        if not os.path.exists(self.mapping_file):
            logger.warning(f"[页面URL映射] 文件不存在，页面识别已禁用: {self.mapping_file}")
            return

        try:
            with open(self.mapping_file, "r", encoding="utf-8") as f:
                data = json.load(f)

            pages = data.get("pages", [])
            if not isinstance(pages, list) or not pages:
                logger.warning("[页面URL映射] pages 为空，页面识别已禁用")
                return

            for page in pages:
                pid = page.get("id")
                if pid and page.get("url"):
                    self._pages[pid] = page

            self._enabled = True
            logger.info(f"[页面URL映射] 加载成功: {len(self._pages)} 个页面直达配置")
        except json.JSONDecodeError as e:
            logger.error(f"[页面URL映射] JSON 解析失败，页面识别已禁用: {e}")
        except Exception as e:
            logger.error(f"[页面URL映射] 加载异常，页面识别已禁用: {e}")

    def reload(self) -> bool:
        """热重载映射表文件

        Returns:
            bool: 重载后映射表是否可用
        """
        self._load()
        return self._enabled

    # ==================== 识别 ====================

    def recognize_page(
        self, steps: List[Dict[str, Any]]
    ) -> Optional[Dict[str, Any]]:
        """
        从用例步骤中识别目标页面

        扫描所有步骤，对每个步骤名/选择器与页面映射表做关键词匹配，
        返回置信度最高且唯一命中的页面配置。

        Args:
            steps: 用例步骤列表

        Returns:
            {"page_id": "...", "name": "...", "url": "...", "skip_orders": [...],
             "confidence": 0.9} 或 None（未识别到任何页面）
        """
        if not self._enabled or not steps:
            return None

        # 收集所有入口步骤的关键词（步骤名 + 选择器 + 元素key）
        entry_keywords: List[str] = []
        for step in steps:
            name = step.get("name", "") or ""
            params = step.get("params", {}) or {}
            action = step.get("action", "")
            # 入口步骤：点击/等待/断言类，且包含元素定位
            keywords = [name]
            if params.get("selector"):
                keywords.append(str(params["selector"]))
            selectors = params.get("selectors")
            if isinstance(selectors, list):
                keywords.extend([str(s) for s in selectors])
            entry_keywords.append((action, keywords))

        # 对每个页面匹配规则打分
        matches: List[Dict[str, Any]] = []
        for pid, page in self._pages.items():
            conf = self._score_page(page, entry_keywords)
            if conf > 0:
                matches.append({"page_id": pid, "name": page.get("name", ""),
                                "url": page.get("url", ""), "confidence": conf,
                                "skip_config": page.get("skip_steps", {})})

        if not matches:
            return None

        # 取最高分
        matches.sort(key=lambda m: m["confidence"], reverse=True)
        best = matches[0]

        # 计算需要跳过的步骤序号
        skip_orders = self._calc_skip_orders(steps, best["skip_config"])

        return {
            "page_id": best["page_id"],
            "name": best["name"],
            "url": best["url"],
            "confidence": best["confidence"],
            "skip_orders": skip_orders,
        }

    def _score_page(self, page: Dict[str, Any], entry_keywords: List) -> float:
        """
        计算页面与用例步骤的匹配得分

        对页面每条匹配规则，检查步骤名/选择器是否包含关键词，命中的规则权值求和。
        保守策略：至少命中 1 条规则才返回 >0 的分数。
        """
        rules = page.get("match_rules", [])
        if not rules:
            return 0.0

        # 规则权值：step_name_contains=2.0, selector_contains=1.5
        WEIGHTS = {
            "step_name_contains": 2.0,
            "selector_contains": 1.5,
            "both_contains": 2.5,
        }

        score = 0.0
        for rule in rules:
            rtype = rule.get("type")
            value = rule.get("value", "")
            if not rtype or not value:
                continue
            for action, kws in entry_keywords:
                for kw in kws:
                    if not kw:
                        continue
                    if rtype == "step_name_contains" and value in kw:
                        score += WEIGHTS.get(rtype, 1.0)
                    elif rtype == "selector_contains" and value in kw:
                        score += WEIGHTS.get(rtype, 1.0)
                    elif rtype == "both_contains" and value in kw:
                        score += WEIGHTS.get(rtype, 1.0)
                    elif rtype == "action_is" and action == value:
                        score += 1.0

        return score if score > 0 else 0.0

    def _calc_skip_orders(self, steps: List[Dict[str, Any]], skip_config: Dict) -> List[int]:
        """
        计算需要跳过的中间导航步骤序号

        跳过规则（满足任一即跳过）：
        - 步骤名包含 skip_config.step_name_contains 中的关键词
        - 步骤 action 是 skip_config.action_is 中的动作且不是 fill/assert/wait

        保守策略：仅跳过 click 类中间导航步骤，不跳过 fill/assert/wait/screenshot/navigate
        """
        if not skip_config:
            return []

        name_kws = skip_config.get("step_name_contains", [])
        action_whitelist = set([
            "click", "hover", "scroll",  # 纯导航/操作类
        ])
        # 不可跳过的动作（涉及真实输入/断言/等待/导航）
        never_skip_actions = {"fill", "assert", "screenshot", "navigate", "check", "uncheck", "press"}

        skip_orders = []
        for step in steps:
            order = step.get("order", 0)
            action = step.get("action", "")
            name = step.get("name", "") or ""

            # 绝不跳过可能影响业务的操作
            if action in never_skip_actions:
                continue

            if action not in action_whitelist:
                continue

            # 步骤名包含导航关键词
            if name_kws and any(kw in name for kw in name_kws):
                skip_orders.append(order)

        return skip_orders

    def is_enabled(self) -> bool:
        """映射表是否可用"""
        return self._enabled

    def get_page_ids(self) -> List[str]:
        """返回所有页面 ID 列表（用于调试）"""
        return list(self._pages.keys())


# 全局单例（懒加载）
_global_page_url_service: Optional[PageUrlService] = None


def get_page_url_service() -> PageUrlService:
    """获取全局 PageUrlService 单例"""
    global _global_page_url_service
    if _global_page_url_service is None:
        _global_page_url_service = PageUrlService()
    return _global_page_url_service