#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：keyword_matcher.py
模块描述：关键词提取与元素匹配模块，用于自然语言步骤定位

作者：czj
创建日期：2026-08-05
最后修改：2026-08-05
"""

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

logger = logging.getLogger(__name__)


# ==================== 动作词表 ====================

ACTION_WORDS = [
    # 输入类
    "输入", "填写", "填入", "键入", "写入", "填写", "输入",
    # 点击类
    "点击", "按下", "单击", "双击", "选择",
    # 导航类
    "打开", "导航", "访问", "进入", "跳转", "前往",
    # 等待类
    "等待", "延时", "等待",
    # 验证类
    "断言", "验证", "检查", "确认",
    # 其他操作
    "勾选", "取消", "悬停", "滚动", "截图"
]

# 元素类型词表
ELEMENT_WORDS = [
    "按钮", "输入框", "下拉框", "复选框", "单选框",
    "链接", "菜单", "图标", "标签", "表格", "列表",
    "卡片", "对话框", "抽屉", "弹窗", "导航"
]


# ==================== 关键词提取 ====================

def extract_keywords(description: str) -> List[str]:
    """
    从自然语言描述中提取关键词

    策略：
    1. 去除特殊符号（【】《》等）
    2. 去除动作词和元素类型词
    3. 提取剩余的关键词（名词、修饰词等）
    4. 按 2-gram 分词获取更精确的关键词

    Args:
        description (str): 步骤描述，如 "输入用户名 admin@xty"

    Returns:
        List[str]: 关键词列表，如 ["用户名", "admin", "admin@xty"]

    Example:
        >>> extract_keywords("点击登录按钮")
        ["登录"]
        >>> extract_keywords("输入用户名 admin@xty")
        ["用户名", "admin", "admin@xty"]
        >>> extract_keywords("点击【功能中心】展开")
        ["功能中心"]
    """
    # 去除特殊符号
    cleaned = description
    special_chars = ['【', '】', '《', '》', '「', '」', '『', '』', '〔', '〕', '〈', '〉']
    for ch in special_chars:
        cleaned = cleaned.replace(ch, '')

    # 去除动作词
    for word in sorted(ACTION_WORDS + ELEMENT_WORDS, key=len, reverse=True):
        cleaned = cleaned.replace(word, "")
    cleaned = cleaned.strip()

    # 去除修饰词（这些词不是关键词，但会影响匹配）
    modifier_words = ['展开', '分类', '添加', '操作', '查看', '是否', '正确', '新增',
                      '条目', '列表', '数据', '验证', '成功', '完成', '页面', '系统']
    for word in modifier_words:
        cleaned = cleaned.replace(word, "")
    cleaned = cleaned.strip()

    # ⚠️ 关键修复：先提取【】内的核心词（优先级最高）
    core_keywords = []
    bracket_match = re.findall(r'【([^】]+)】', description)
    if bracket_match:
        for match in bracket_match:
            core_keywords.append(match)
            # 添加 2-gram
            for i in range(len(match) - 1):
                sub = match[i:i+2]
                if sub not in ACTION_WORDS and sub not in ELEMENT_WORDS:
                    core_keywords.append(sub)

    keywords = []

    # 添加清理后的关键词
    if cleaned:
        keywords.append(cleaned)

    # 2-gram 分词（提取连续两个字的关键词）
    if len(cleaned) >= 2:
        for i in range(len(cleaned) - 1):
            sub = cleaned[i:i+2]
            if len(sub) == 2 and sub not in ACTION_WORDS and sub not in ELEMENT_WORDS:
                keywords.append(sub)

    # 单字关键词（去掉停用词）
    stopwords = {'的', '在', '是', '和', '有', '等', '中', '为', '了', '与', '或', '、', '，', '。', ' '}
    for ch in cleaned:
        if ch.strip() and ch not in stopwords:
            keywords.append(ch)

    # ⚠️ 【】内的核心词放在最前面（优先级最高）
    result = []
    seen = set()

    # 先添加核心关键词
    for kw in core_keywords:
        if kw not in seen:
            seen.add(kw)
            result.append(kw)

    # 再添加其他关键词
    for kw in keywords:
        if kw not in seen:
            seen.add(kw)
            result.append(kw)

    return result


def extract_value_from_description(description: str) -> Optional[str]:
    """
    从步骤描述中提取操作值

    例如："输入用户名 admin@xty" -> "admin@xty"

    Args:
        description (str): 步骤描述

    Returns:
        Optional[str]: 提取的值，未找到返回 None
    """
    # 匹配模式：动作 + 目标 + 空格 + 值
    # 例如："输入用户名 admin@xty"
    match = re.search(r'(?:输入|填写|填入|键入|写入)\s*.*?\s+([\w@.\-]+)\s*$', description)
    if match:
        return match.group(1)

    # 匹配模式：值在引号中
    match = re.search(r'["\']([^"\']+)["\']', description)
    if match:
        return match.group(1)

    return None


def detect_action_type(description: str) -> str:
    """
    根据描述推断动作类型

    Args:
        description (str): 步骤描述

    Returns:
        str: 动作类型 (navigate/click/fill/wait/select/assert)
    """
    desc_lower = description.lower()

    # 导航
    if any(kw in desc_lower for kw in ['访问', '打开', '导航', '进入', '跳转', '前往']):
        return 'navigate'

    # 输入
    if any(kw in desc_lower for kw in ['输入', '填写', '填入', '键入', '写入']):
        return 'fill'

    # 选择
    if any(kw in desc_lower for kw in ['选择', '下拉']):
        return 'select'

    # 等待
    if any(kw in desc_lower for kw in ['等待', '延时']):
        return 'wait'

    # 验证
    if any(kw in desc_lower for kw in ['断言', '验证', '检查', '确认']):
        return 'assert'

    # 默认为点击
    return 'click'


# ==================== 元素匹配 ====================

def match_element_by_keywords(
    page,
    keywords: List[str],
    action: str,
    timeout: int = 5000
) -> Tuple[Optional[Any], List[Dict[str, Any]]]:
    """
    通过关键词在页面元素中直接匹配

    三级匹配策略：
    1. 精确匹配：ID, name, data-testid
    2. 包含匹配：placeholder, aria-label, text
    3. 回退策略：根据动作类型推断

    Args:
        page: Playwright Page 对象
        keywords (List[str]): 关键词列表
        action (str): 动作类型
        timeout (int): 超时时间（毫秒）

    Returns:
        Tuple[Optional[Any], List[Dict]]: (元素对象, 候选选择器列表)
            元素对象可能为 None（未找到）
            候选选择器列表按置信度降序排列
    """
    candidates = []

    # 获取所有可交互元素
    try:
        elements = page.locator(
            'input:visible, button:visible, a:visible, select:visible, textarea:visible, '
            '[role="button"]:visible, [role="link"]:visible, [role="checkbox"]:visible, '
            '[role="radio"]:visible, [role="tab"]:visible, [role="menuitem"]:visible'
        ).all()
    except Exception as e:
        logger.warning(f"获取页面元素失败: {e}")
        return None, []

    logger.info(f"页面找到 {len(elements)} 个可交互元素，关键词: {keywords}")

    for el in elements:
        try:
            # 提取元素属性
            tag = el.evaluate('el => el.tagName')
            el_type = el.get_attribute('type') or ''
            placeholder = el.get_attribute('placeholder') or ''
            text = el.inner_text().strip() if tag in ['BUTTON', 'A'] else ''
            el_id = el.get_attribute('id') or ''
            name = el.get_attribute('name') or ''
            aria_label = el.get_attribute('aria-label') or ''
            data_testid = el.get_attribute('data-testid') or el.get_attribute('data-test-id') or ''
            class_name = el.get_attribute('class') or ''

            score = 0.0
            selectors = []

            # 1. ID 匹配（最高优先级）
            if el_id:
                for kw in keywords:
                    if kw.lower() in el_id.lower():
                        score += 0.4
                        selectors.append({
                            'type': 'css',
                            'value': f'#{el_id}',
                            'confidence': 0.95,
                            'priority': 1
                        })

            # 2. data-testid 匹配（高优先级）
            if data_testid:
                for kw in keywords:
                    if kw.lower() in data_testid.lower():
                        score += 0.35
                        selectors.append({
                            'type': 'css',
                            'value': f'[data-testid="{data_testid}"]',
                            'confidence': 0.90,
                            'priority': 2
                        })

            # 3. placeholder 匹配（输入框）
            if placeholder and tag == 'INPUT':
                for kw in keywords:
                    if kw.lower() in placeholder.lower():
                        score += 0.3
                        selectors.append({
                            'type': 'css',
                            'value': f'input[placeholder*="{placeholder}"]',
                            'confidence': 0.85,
                            'priority': 3
                        })

            # 4. aria-label 匹配
            if aria_label:
                for kw in keywords:
                    if kw.lower() in aria_label.lower():
                        score += 0.3
                        selectors.append({
                            'type': 'css',
                            'value': f'[aria-label*="{aria_label}"]',
                            'confidence': 0.80,
                            'priority': 4
                        })

            # 5. 文本匹配（按钮、链接）
            if text and tag in ['BUTTON', 'A']:
                for kw in keywords:
                    if kw.lower() in text.lower():
                        score += 0.25
                        selectors.append({
                            'type': 'css',
                            'value': f'{tag.lower()}:has-text("{text}")',
                            'confidence': 0.75,
                            'priority': 5
                        })

            # 6. name 匹配
            if name:
                for kw in keywords:
                    if kw.lower() in name.lower():
                        score += 0.2
                        selectors.append({
                            'type': 'css',
                            'value': f'[name="{name}"]',
                            'confidence': 0.70,
                            'priority': 6
                        })

            # 7. 动作类型加分/减分
            if action == 'fill' and tag in ['INPUT', 'TEXTAREA']:
                score += 0.20
            elif action == 'click' and tag in ['BUTTON', 'A']:
                score += 0.25
            elif action == 'select' and tag == 'SELECT':
                score += 0.20

            # ⚠️ 关键修复：点击操作不应该匹配输入框（除非关键词明确包含输入相关）
            if action == 'click' and tag in ['INPUT', 'TEXTAREA']:
                # 只有关键词明确包含输入相关词汇时才保留输入框
                input_keywords = ['输入框', '输入', '框', '填写', 'input']
                if not any(kw in input_keywords for kw in keywords):
                    score *= 0.3  # 大幅降低分数，让按钮优先

            # 填充操作不应该匹配按钮（除非关键词明确包含按钮相关）
            if action == 'fill' and tag in ['BUTTON', 'A']:
                fill_keywords = ['按钮', '点击', 'click']
                if not any(kw in fill_keywords for kw in keywords):
                    score *= 0.3  # 大幅降低分数

            # 8. 类型匹配（password, email, tel 等）
            if el_type:
                if action == 'fill':
                    if el_type == 'password' and any(kw in '密码' for kw in keywords):
                        score += 0.2
                        selectors.append({
                            'type': 'css',
                            'value': 'input[type="password"]',
                            'confidence': 0.85,
                            'priority': 3
                        })

            if score > 0.15 and selectors:
                # 按优先级排序选择器
                selectors.sort(key=lambda x: x['priority'])
                candidates.append({
                    'element': el,
                    'score': score,
                    'selectors': selectors,
                    'info': {
                        'tag': tag,
                        'type': el_type,
                        'placeholder': placeholder,
                        'text': text,
                        'id': el_id
                    }
                })

        except Exception as e:
            logger.debug(f"处理元素时出错（跳过）: {e}")
            continue

    # 按分数降序排序
    candidates.sort(key=lambda x: x['score'], reverse=True)

    if not candidates:
        return None, []

    # 返回最佳匹配
    best = candidates[0]
    logger.info(f"最佳匹配元素: {best['info']}, 分数: {best['score']:.2f}")

    return best['element'], best['selectors']


def find_element_by_semantic(
    page,
    keywords: List[str],
    action: str,
    timeout: int = 5000
) -> Tuple[Optional[Any], List[Dict[str, Any]]]:
    """
    通过语义推断匹配元素（当关键词无法直接匹配时）

    根据动作类型推断目标元素类型：
    - fill + 有 value -> 找输入框
    - click + 包含"按钮" -> 找按钮
    - click + 包含"菜单" -> 找菜单项
    - wait + 包含"页面" -> 找容器元素

    Args:
        page: Playwright Page 对象
        keywords (List[str]): 关键词列表
        action (str): 动作类型
        timeout (int): 超时时间（毫秒）

    Returns:
        Tuple[Optional[Any], List[Dict]]: (元素对象, 候选选择器列表)
    """
    logger.info(f"语义推断匹配: action={action}, keywords={keywords}")

    # fill 动作：找输入框
    if action == 'fill':
        try:
            # 优先找可见的输入框
            inputs = page.locator('input:visible, textarea:visible').all()
            if inputs:
                # 如果只有一个输入框，直接返回
                if len(inputs) == 1:
                    return inputs[0], [{
                        'type': 'css',
                        'value': 'input:visible',
                        'confidence': 0.70,
                        'priority': 1
                    }]

                # 多个输入框时，优先找 type=text 或 placeholder 包含关键词的
                for inp in inputs:
                    inp_type = inp.get_attribute('type') or 'text'
                    placeholder = inp.get_attribute('placeholder') or ''

                    if inp_type in ['text', 'email', 'tel', '']:
                        if any(kw.lower() in placeholder.lower() for kw in keywords):
                            return inp, [{
                                'type': 'css',
                                'value': f'input[placeholder*="{placeholder}"]',
                                'confidence': 0.80,
                                'priority': 1
                            }]

                # 默认返回第一个输入框
                return inputs[0], [{
                    'type': 'css',
                    'value': 'input:visible',
                    'confidence': 0.60,
                    'priority': 1
                }]
        except Exception as e:
            logger.warning(f"语义推断查找输入框失败: {e}")

    # click 动作：找按钮
    if action == 'click':
        # 如果描述包含"按钮"，优先找按钮
        if any('按钮' in kw or 'button' in kw.lower() for kw in keywords):
            try:
                buttons = page.locator('button:visible').all()
                if buttons:
                    # 找文本匹配的按钮
                    for btn in buttons:
                        text = btn.inner_text().strip()
                        if any(kw.lower() in text.lower() for kw in keywords):
                            return btn, [{
                                'type': 'css',
                                'value': f'button:has-text("{text}")',
                                'confidence': 0.80,
                                'priority': 1
                            }]

                    # 默认返回第一个按钮
                    return buttons[0], [{
                        'type': 'css',
                        'value': 'button:visible',
                        'confidence': 0.60,
                        'priority': 1
                    }]
            except Exception as e:
                logger.warning(f"语义推断查找按钮失败: {e}")

        # 如果描述包含"菜单"或"导航"，找菜单项
        if any('菜单' in kw or '导航' in kw or 'menu' in kw.lower() for kw in keywords):
            try:
                menu_items = page.locator('[role="menuitem"]:visible, .el-menu-item:visible').all()
                if menu_items:
                    for item in menu_items:
                        text = item.inner_text().strip()
                        if any(kw in text for kw in keywords):
                            return item, [{
                                'type': 'css',
                                'value': f'[role="menuitem"]:has-text("{text}"), .el-menu-item:has-text("{text}")',
                                'confidence': 0.75,
                                'priority': 1
                            }]
            except Exception as e:
                logger.warning(f"语义推断查找菜单失败: {e}")

    # wait 动作：找容器元素
    if action == 'wait':
        container_selectors = [
            '.container', '.el-main', '.main', '#app',
            '[class*="content"]', '[class*="page"]'
        ]
        for sel in container_selectors:
            try:
                el = page.locator(sel).first
                if el:
                    return el, [{
                        'type': 'css',
                        'value': sel,
                        'confidence': 0.50,
                        'priority': 1
                    }]
            except Exception:
                continue

    return None, []