#!/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
import re
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")
                # 注册表按 id 存储全部页面（含无直达 URL 的登录/主页，供运行时识别 scope）
                if pid:
                    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 _replace_base_url(self, url: str) -> str:
        """
        运行时替换 URL 的 base 部分（scheme + host）

        page_url_mapping.json 中的 URL 是静态数据（含旧环境地址），
        返回前根据当前系统配置 TARGET_URL 动态替换，
        保证切换目标环境后直达 URL 同步生效。

        Args:
            url (str): 映射表中的原始 URL

        Returns:
            str: 替换 base 后的 URL（无 host 时原样返回）
        """
        if not url:
            return url
        from app.config import settings
        if settings.TARGET_URL:
            from urllib.parse import urlparse
            parsed = urlparse(url)
            if parsed.netloc:
                old_base = f"{parsed.scheme}://{parsed.netloc}"
                return url.replace(old_base, settings.TARGET_URL.rstrip('/'), 1)
        return url

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

    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))

        # 对每个页面匹配规则打分（仅考虑有直达 URL 的页面）
        matches: List[Dict[str, Any]] = []
        for pid, page in self._pages.items():
            if not page.get("url"):
                continue
            conf = self._score_page(page, entry_keywords)
            if conf > 0:
                matches.append({"page_id": pid, "name": page.get("name", ""),
                                "url": self._replace_base_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 recognize_current_page(self, page) -> Optional[Dict[str, Any]]:
        """
        运行时根据当前浏览器 URL + DOM 指纹识别页面身份（语义化 scope）

        识别策略：
        1. 先用 url_patterns（正则数组）对当前 URL 计分，唯一命中直接返回
        2. 0 或歧义时，用 fingerprint（DOM 指纹，min_count 个匹配）判存
        3. 都失败返回 None（执行器零行为变化）

        Args:
            page: Playwright Page 对象（鸭子类型：.url / .query_selector_all）

        Returns:
            {"id", "name", "confidence"} 或 None
        """
        if not self._enabled:
            return None

        try:
            current_url = str(page.url or "")
        except Exception:
            return None

        # 1. URL patterns 计分
        candidates: List[Dict[str, Any]] = []
        for pid, page_conf in self._pages.items():
            score = 0
            for pat in page_conf.get("url_patterns", []):
                try:
                    if re.search(pat, current_url):
                        score += 1
                except re.error:
                    continue
            if score > 0:
                candidates.append((score, pid, page_conf))

        if len(candidates) == 1:
            pid = candidates[0][1]
            p = candidates[0][2]
            return {"id": pid, "name": p.get("name", ""), "confidence": 1.0}

        # 2. fingerprint DOM 指纹判存（URL 缺失/歧义时）
        if page is not None:
            for pid, page_conf in self._pages.items():
                fp = page_conf.get("fingerprint", [])
                if not fp:
                    continue
                hits = 0
                required = 0
                for item in fp:
                    sel = item.get("selector")
                    if not sel:
                        continue
                    try:
                        min_count = int(item.get("min_count", 1))
                    except (TypeError, ValueError):
                        min_count = 1
                    required += 1
                    try:
                        matched = page.query_selector_all(sel)
                        if len(matched) < min_count:
                            continue
                        # 文本校验（首个匹配的文本必须一致）
                        expected_text = item.get("text")
                        if expected_text:
                            if not matched or (matched[0].text_content() or "").strip() != expected_text:
                                continue
                        hits += 1
                    except Exception:
                        continue
                if required > 0 and hits >= required:
                    return {"id": pid, "name": page_conf.get("name", ""), "confidence": 0.8}

        return None

    def get_scope_page(self, scope: str) -> Optional[Dict[str, Any]]:
        """
        按 scope id 返回页面注册表配置（语义解析器容器约束用）

        Args:
            scope: 页面注册表 id（如 create_meeting）

        Returns:
            页面配置 dict 或 None
        """
        return self._pages.get(scope)

    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