#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：semantic_resolver.py
模块描述：语义解析器 — 把语义目标（semantic）解析为选择器链，注入执行器

复杂用例通用执行机制（语义化用例执行）核心：
- 结构化 semantic 目标 → 候选选择器链（走现有 _do_click/_do_fill 成熟回退策略）
- 解析优先级（规则优先，Claude 按需）：
    1. scope 校验（页面注册表身份，根治跨层级误配）
    2. 映射表 v2 精确键 lookup（阶段 5 接入）
    3. 强结构类型模式选择器（tab / row_checkbox / checkbox / button 等）
    4. keyword_matcher 规则候选（scope 已知可收缩容器）
    5. DOM 过滤（contains/container/nth）
    6. 候选歧义时 Claude 仲裁（rank_candidates min_confidence 短路）
    7. save_as 提取元素值写入上下文变量
- legacy 自然语言步骤（USE_KEYWORD_MATCHER=true）走 keyword_matcher facade

作者：czj
创建日期：2026-08-18
"""

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

from app.config import settings
from app.services.element_mapping_service import get_element_mapping_service
from app.services.claude_service import get_claude_service
from app.services.page_url_service import get_page_url_service
from app.services.keyword_matcher import (
    extract_keywords,
    match_element_by_keywords,
    get_candidate_details,
    resolve_selectors,
)

logger = logging.getLogger(__name__)

# 语义类型 → 动作映射
TYPE_ACTION_MAP = {
    "button": "click",
    "icon": "click",
    "link": "click",
    "menu": "click",
    "tab": "click",
    "row": "click",
    "row_checkbox": "click",
    "checkbox": "check",
    "input": "fill",
    "textarea": "fill",
    "text": "wait",
    "dialog": "assert",
}

# 强结构类型：优先用模式选择器（无需 DOM 扫描）
_STRUCTURED_TYPES = {"tab", "row_checkbox", "checkbox", "row"}


class SemanticResolver:
    """
    语义解析器

    resolve_step(page, step, scope, ctx) → Optional[List[str]] 选择器链
    无法解析返回 None（执行器零影响，走原有定位路径）。
    """

    def __init__(self):
        self._element_mapping = get_element_mapping_service()
        self._claude = get_claude_service()

    # ==================== 入口 ====================

    def resolve_step(
        self,
        page,
        step: Dict[str, Any],
        scope: Optional[str],
        ctx,
    ) -> Optional[List[str]]:
        """
        解析步骤 → 选择器链（语义目标优先；legacy 步骤可走 keyword facade）

        Args:
            page: Playwright Page
            step: 步骤 dict（含 semantic 可选字段）
            scope: 当前页面 scope id（页面注册表，可为 None）
            ctx: ExecutionContext（渲染语义文本）

        Returns:
            List[str] 选择器链（高优先级在前），无法解析返回 None
        """
        semantic = step.get("semantic")
        if isinstance(semantic, dict):
            return self._resolve_semantic(page, semantic, scope, ctx)
        # legacy 自然语言步骤（过渡路径）
        if settings.USE_KEYWORD_MATCHER:
            return self._resolve_by_keyword(page, step, scope, ctx)
        return None

    # ==================== 语义目标解析 ====================

    def _resolve_semantic(
        self,
        page,
        target: Dict[str, Any],
        scope: Optional[str],
        ctx,
    ) -> Optional[List[str]]:
        """semantic 目标 → 选择器链"""
        ctype = str(target.get("type", "button") or "button")

        # 1. 文本渲染（上下文变量 {__CTX:key__} 等）
        raw_text = target.get("text")
        text = ""
        if raw_text:
            text = ctx.render(raw_text) if ctx else str(raw_text)
            text = str(text).strip()
        contains = target.get("contains")
        if contains and ctx:
            contains = ctx.render(contains)
            contains = str(contains).strip() if contains else None

        # 2. scope 校验：期望 scope 与实际不符 → 软处理。
        #    执行器 _current_scope 只在登录/URL直达后同步，普通 click 跳转（如
        #    「查看详情」→会议详情页）后不更新 → 这里重新识别当前页面一次：
        #    页面确实已跳转到期望 scope 则更新继续解析；否则才拦截（防跨页误配）。
        expected_scope = target.get("scope")
        if expected_scope and scope and expected_scope != scope:
            fresh_scope = None
            try:
                rec = get_page_url_service().recognize_current_page(page)
                fresh_scope = rec["id"] if rec else None
            except Exception:
                fresh_scope = None
            if fresh_scope and fresh_scope == expected_scope:
                logger.info(
                    f"[semantic] scope 软更新: 期望 {expected_scope}, 页面实际识别为 "
                    f"{fresh_scope}（原 {scope}），继续解析"
                )
                scope = fresh_scope
            else:
                logger.debug(
                    f"[semantic] scope 不符，跳过语义解析: 期望 {expected_scope}, "
                    f"实际 {scope}, 重识别 {fresh_scope or '未识别'}"
                )
                return None

        # 3. 映射表 v2 精确键（阶段 5 提供 lookup_by_key；未实现时走规则）
        v2 = self._mapping_v2_lookup(target, text, ctype)
        if v2:
            return v2

        # 4. 强结构类型模式选择器（tab / row_checkbox 等）
        if ctype in _STRUCTURED_TYPES:
            pattern = self._pattern_selectors(ctype, text, contains)
            if pattern:
                return pattern

        # 5. 关键词规则候选
        if text:
            action = TYPE_ACTION_MAP.get(ctype, "click")
            keywords = extract_keywords(text)
            if keywords:
                elements, selectors = match_element_by_keywords(page, keywords, action)
                if elements and selectors:
                    chain = self._filter_selectors(selectors, contains, target)
                    if chain:
                        return self._arbitrate(
                            page,
                            step_name=text,
                            action=action,
                            elements=elements,
                            selectors=selectors,
                            chain=chain,
                        )
        return None

    def _mapping_v2_lookup(
        self, target: Dict[str, Any], text: str, ctype: str
    ) -> Optional[List[str]]:
        """
        映射表 v2 精确键查找（阶段 5 实现 lookup_by_key 后自动生效）
        语义文本作为显式键；scope 未知时不查（避免跨页误配）。
        """
        if not text:
            return None
        lookup = getattr(self._element_mapping, "lookup_by_key", None)
        if not lookup:
            return None
        try:
            scope = target.get("scope")
            if not scope:
                return None
            res = lookup(scope, text)
            if not res:
                # 共享元素回退
                res = lookup("shared", text)
            if res:
                sels = res.get("selectors") or []
                chain = [s for s in sels if s]
                if chain:
                    logger.info(f"[semantic] 映射表 v2 命中: {scope}/{text} → {chain[0]}")
                    return chain
        except Exception as e:
            logger.debug(f"[semantic] 映射表 v2 查找异常: {e}")
        return None

    # ==================== 模式选择器（纯字符串，可单测） ====================

    def _pattern_selectors(
        self, ctype: str, text: str, contains: Optional[str]
    ) -> Optional[List[str]]:
        """强结构类型 → 候选选择器链（锚点优先用 contains 唯一值）"""
        anchor = contains or text or ""
        if not anchor:
            return None
        # 转义选择器中的引号
        anchor = anchor.replace('"', '\\"')

        if ctype == "row_checkbox":
            return [
                f'.el-table__row:has-text("{anchor}") .el-checkbox',
                f'.el-table__row:has-text("{anchor}") input[type="checkbox"]',
                f'.el-checkbox:has-text("{anchor}")',
                f'li:has-text("{anchor}") .el-checkbox',
            ]
        if ctype == "checkbox":
            return [
                f'.el-checkbox:has-text("{anchor}")',
                f'.el-checkbox__inner:has-text("{anchor}")',
                f'input[type="checkbox"][value="{anchor}"]',
            ]
        if ctype == "tab":
            return [
                f'.el-tabs__item:has-text("{anchor}")',
                f'[role="tab"]:has-text("{anchor}")',
                f'.tab-item:has-text("{anchor}")',
                f'li[role="tab"]:has-text("{anchor}")',
            ]
        if ctype == "row":
            return [
                f'.el-table__row:has-text("{anchor}")',
                f'tr:has-text("{anchor}")',
            ]
        return None

    # ==================== DOM 候选过滤 ====================

    def _filter_selectors(
        self,
        selectors: List[Dict[str, Any]],
        contains: Optional[str],
        target: Dict[str, Any],
    ) -> List[str]:
        """按 contains/container/nth 过滤候选选择器，产出去重选择器链"""
        container = target.get("container") or {}
        nth = target.get("nth")

        chain: List[str] = []
        for s in selectors:
            val = s.get("value")
            if not val:
                continue
            val = str(val)

            # contains 次级过滤：选择器字符串未提及 contains 且候选多于 1 时降权
            if contains and len(selectors) > 1 and contains not in val:
                continue

            # container 父容器约束 → 组合选择器
            csel = container.get("selector")
            if csel and csel not in val:
                val = f"{csel} {val}"

            # nth（1 起）→ Playwright `>> nth=N`（0 起）
            if nth:
                try:
                    n = int(nth)
                    if n > 1:
                        val = f"{val} >> nth={n - 1}"
                except (TypeError, ValueError):
                    pass

            if val not in chain:
                chain.append(val)
        return chain[:4]

    # ==================== 歧义仲裁（规则优先，Claude 按需） ====================

    def _arbitrate(
        self,
        page,
        step_name: str,
        action: str,
        elements: List[Any],
        selectors: List[Dict[str, Any]],
        chain: List[str],
    ) -> List[str]:
        """
        候选歧义仲裁：
        - 去重后唯一候选 → 直接返回
        - 置信度差距 ≥ 0.1 → 规则可判，直接返回
        - 否则 Claude 仲裁（候选数 ≥ 2 时）
        """
        dedup: List[str] = []
        for s in chain:
            if s not in dedup:
                dedup.append(s)
        if len(dedup) <= 1:
            return dedup[:1]

        confs = []
        for s in selectors:
            try:
                confs.append(float(s.get("confidence", 0.5)))
            except (TypeError, ValueError):
                confs.append(0.5)
        if len(confs) >= 2 and confs[0] - confs[1] >= 0.1:
            logger.debug(f"[semantic] 规则可判，跳过 Claude: 置信度 {confs[0]:.2f} vs {confs[1]:.2f}")
            return dedup[:1]

        if self._claude.enabled and len(elements) >= 2:
            try:
                candidates = get_candidate_details(page, elements)
                selected, confidence, reason, confirmed = self._claude.rank_candidates(
                    step_name=step_name,
                    action=action,
                    params={},
                    candidates=candidates,
                    page_url=getattr(page, "url", "") or "",
                    previous_steps=None,
                    min_confidence=0.0,
                )
                if confirmed and 0 <= selected < len(dedup):
                    logger.info(f"[semantic] Claude 仲裁: index={selected}, reason={reason}")
                    return dedup[selected : selected + 1]
            except Exception as e:
                logger.warning(f"[semantic] Claude 仲裁失败，回退第一个候选: {e}")

        return dedup[:1]

    # ==================== legacy 步骤 keyword facade ====================

    def _resolve_by_keyword(
        self, page, step: Dict[str, Any], scope: Optional[str], ctx
    ) -> Optional[List[str]]:
        """legacy 自然语言步骤 → keyword_matcher.resolve_selectors facade"""
        chain = resolve_selectors(page, step, max_chain=4)
        return chain or None

    # ==================== save_as（提取值写上下文） ====================

    def extract_save_as(self, page, target: Dict[str, Any], chain: List[str], ctx) -> None:
        """
        从解析链命中的元素提取值，写入上下文变量（save_as）

        - input/textarea → input_value
        - 其他 → inner_text
        """
        save_as = target.get("save_as") if isinstance(target, dict) else None
        if not save_as or not ctx:
            return
        selector = chain[0] if chain else None
        if not selector:
            return
        try:
            locator = page.locator(selector)
            if locator.count() == 0:
                return
            el = locator.first
            ctype = target.get("type", "")
            if ctype in ("input", "textarea"):
                value = el.input_value()
            else:
                value = el.inner_text()
            value = (value or "").strip()
            if isinstance(save_as, dict):
                # {"name": "meetingName"} 键名可自定义
                for key, path in save_as.items():
                    _ = path
                    ctx.set(key, value)
            else:
                ctx.set(str(save_as), value)
            logger.info(f"[semantic] save_as: {save_as} ← '{value}'")
        except Exception as e:
            logger.debug(f"[semantic] save_as 提取失败: {e}")


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


def get_semantic_resolver() -> SemanticResolver:
    """获取全局 SemanticResolver 单例"""
    global _global_semantic_resolver
    if _global_semantic_resolver is None:
        _global_semantic_resolver = SemanticResolver()
    return _global_semantic_resolver
