#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：selector_extractor.py
模块描述：选择器提取模块，从 Playwright 元素对象中提取多种候选选择器

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

Phase 3 更新：
- 新增语义定位器支持（role:/text:/placeholder:/label:/testid:）
- 统一选择器优先级体系
"""

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

logger = logging.getLogger(__name__)


# ==================== 选择器优先级（Phase 3 统一）====================
# 优先级越高，稳定性越好，执行时优先使用

SELECTOR_PRIORITY = {
    # 语义定位器（最高优先级，Playwright原生API）
    'role': 1,
    'text': 1,
    'placeholder': 1,
    'label': 1,
    'testid': 1,

    # data-testid 属性
    'data_testid': 2,

    # ID 属性
    'id': 3,

    # 组合选择器
    'combined': 4,

    # 其他属性
    'name': 5,
    'placeholder_attr': 6,
    'aria_label': 7,
    'text_selector': 8,
    'class': 9,
    'xpath': 10,
}


# ==================== 语义定位器提取（Phase 3）====================

def extract_semantic_selectors(element) -> List[Dict[str, Any]]:
    """
    提取语义定位器（Playwright 原生 API）

    语义定位器格式：
    - role:button[name="登录"]
    - text:新建会议
    - placeholder:请输入会议名称
    - label:用户名
    - testid:submit-btn

    Args:
        element: Playwright Locator 对象

    Returns:
        List[Dict]: 语义选择器列表
    """
    semantic_selectors = []

    try:
        tag = element.evaluate('el => el.tagName')

        # 1. role 定位器（最高优先级）
        role = element.get_attribute('role') or ''
        aria_label = element.get_attribute('aria-label') or ''
        text = element.inner_text().strip() if tag in ['BUTTON', 'A', 'DIV', 'SPAN', 'I'] else ''

        if role:
            # 隐式角色推断
            implicit_roles = {
                'BUTTON': 'button',
                'A': 'link',
                'INPUT': 'textbox' if element.get_attribute('type') in ['text', 'email', 'tel', 'password', ''] else None,
                'SELECT': 'combobox',
                'CHECKBOX': 'checkbox',
                'RADIO': 'radio',
            }

            effective_role = role
            if not effective_role and tag in implicit_roles:
                effective_role = implicit_roles[tag]

            if effective_role:
                if aria_label:
                    semantic_selectors.append({
                        'type': 'semantic',
                        'format': 'role',
                        'value': f'role:{effective_role}[name="{aria_label}"]',
                        'confidence': 0.95,
                        'priority': SELECTOR_PRIORITY['role'],
                        'match_type': 'semantic'
                    })
                elif text and len(text) <= 50:
                    semantic_selectors.append({
                        'type': 'semantic',
                        'format': 'role',
                        'value': f'role:{effective_role}[name="{text}"]',
                        'confidence': 0.90,
                        'priority': SELECTOR_PRIORITY['role'],
                        'match_type': 'semantic'
                    })
                else:
                    semantic_selectors.append({
                        'type': 'semantic',
                        'format': 'role',
                        'value': f'role:{effective_role}',
                        'confidence': 0.75,
                        'priority': SELECTOR_PRIORITY['role'],
                        'match_type': 'semantic'
                    })

        # 2. text 定位器（按钮、链接、文本元素）
        if text and len(text) <= 50:
            # 过滤掉特殊字符
            clean_text = text.replace('"', "'").strip()
            if clean_text:
                semantic_selectors.append({
                    'type': 'semantic',
                    'format': 'text',
                    'value': f'text:{clean_text}',
                    'confidence': 0.85,
                    'priority': SELECTOR_PRIORITY['text'],
                    'match_type': 'semantic'
                })

        # 3. placeholder 定位器（输入框）
        placeholder = element.get_attribute('placeholder') or ''
        if placeholder and tag == 'INPUT':
            clean_placeholder = placeholder.replace('"', "'").strip()
            if clean_placeholder:
                semantic_selectors.append({
                    'type': 'semantic',
                    'format': 'placeholder',
                    'value': f'placeholder:{clean_placeholder}',
                    'confidence': 0.80,
                    'priority': SELECTOR_PRIORITY['placeholder'],
                    'match_type': 'semantic'
                })

        # 4. label 定位器（关联 label 的表单元素）
        # 通过 aria-labelledby 或 for 属性查找关联的 label
        aria_labelledby = element.get_attribute('aria-labelledby') or ''
        element_id = element.get_attribute('id') or ''

        if aria_labelledby:
            # 通过 aria-labelledby 关联
            semantic_selectors.append({
                'type': 'semantic',
                'format': 'label',
                'value': f'label:#{aria_labelledby}',
                'confidence': 0.85,
                'priority': SELECTOR_PRIORITY['label'],
                'match_type': 'semantic'
            })

        # 5. testid 定位器（data-testid 属性）
        data_testid = element.get_attribute('data-testid') or element.get_attribute('data-test-id') or ''
        if data_testid:
            semantic_selectors.append({
                'type': 'semantic',
                'format': 'testid',
                'value': f'testid:{data_testid}',
                'confidence': 0.95,
                'priority': SELECTOR_PRIORITY['testid'],
                'match_type': 'semantic'
            })

    except Exception as e:
        logger.debug(f"提取语义选择器失败: {e}")

    return semantic_selectors


# ==================== 选择器提取 ====================

def extract_selectors(element) -> Dict[str, Any]:
    """
    从 Playwright 元素对象中提取多种候选选择器（Phase 3 版本）

    按优先级提取：
    1. 语义定位器（role/text/placeholder/label/testid）
    2. data-testid / data-test-id
    3. ID
    4. name
    5. placeholder
    6. aria-label
    7. 文本内容（按钮、链接）
    8. class 组合选择器
    9. XPath

    Args:
        element: Playwright Locator 对象

    Returns:
        Dict: {
            'primary': str,  # 主选择器（最高优先级）
            'candidates': [  # 候选选择器列表
                {'type': str, 'value': str, 'confidence': float, 'priority': int}
            ]
        }

    Example:
        >>> extract_selectors(button_element)
        {
            'primary': 'role:button[name="登录"]',
            'candidates': [
                {'type': 'semantic', 'value': 'role:button[name="登录"]', 'confidence': 0.95, 'priority': 1},
                {'type': 'css', 'value': 'button:has-text("登录")', 'confidence': 0.85, 'priority': 8}
            ]
        }
    """
    candidates = []

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

        # ==================== Phase 3: 优先提取语义选择器 ====================
        semantic_selectors = extract_semantic_selectors(element)
        candidates.extend(semantic_selectors)

        # ==================== CSS 选择器 ====================

        # 1. data-testid 选择器（最稳定）
        if data_testid:
            candidates.append({
                'type': 'css',
                'value': f'[data-testid="{data_testid}"]',
                'confidence': 0.90,
                'priority': SELECTOR_PRIORITY['data_testid']
            })

        # 2. ID 选择器
        if el_id:
            escaped_id = _css_escape_id(el_id)
            candidates.append({
                'type': 'css',
                'value': f'#{escaped_id}',
                'confidence': 0.85,
                'priority': SELECTOR_PRIORITY['id']
            })

        # 3. name 选择器（表单元素）
        if name and tag in ['INPUT', 'SELECT', 'TEXTAREA']:
            candidates.append({
                'type': 'css',
                'value': f'{tag.lower()}[name="{name}"]',
                'confidence': 0.80,
                'priority': SELECTOR_PRIORITY['name']
            })

        # 4. placeholder 选择器（输入框）
        if placeholder and tag == 'INPUT':
            candidates.append({
                'type': 'css',
                'value': f'input[placeholder*="{placeholder}"]',
                'confidence': 0.75,
                'priority': SELECTOR_PRIORITY['placeholder_attr']
            })

        # 5. aria-label 选择器
        if aria_label:
            candidates.append({
                'type': 'css',
                'value': f'[{tag.lower()}][aria-label*="{aria_label}"]',
                'confidence': 0.70,
                'priority': SELECTOR_PRIORITY['aria_label']
            })

        # 6. 文本选择器（按钮、链接）
        if text and tag in ['BUTTON', 'A']:
            candidates.append({
                'type': 'css',
                'value': f'{tag.lower()}:has-text("{text}")',
                'confidence': 0.80,
                'priority': SELECTOR_PRIORITY['text_selector']
            })

        # 7. role 选择器（CSS）
        if role:
            candidates.append({
                'type': 'css',
                'value': f'[role="{role}"]',
                'confidence': 0.65,
                'priority': SELECTOR_PRIORITY['class']
            })

        # 8. href 选择器（链接）
        if href and tag == 'A':
            href_part = href.split('/')[-1] or href.split('?')[0].split('/')[-1]
            if href_part:
                candidates.append({
                    'type': 'css',
                    'value': f'a[href*="{href_part}"]',
                    'confidence': 0.60,
                    'priority': SELECTOR_PRIORITY['class']
                })

        # 9. class 组合选择器
        if class_name and len(class_name.split()) > 0:
            classes = class_name.split()
            unique_classes = [c for c in classes if any(kw in c.lower() for kw in ['id', 'unique', 'hash', 'uuid'])]
            if unique_classes:
                candidates.append({
                    'type': 'css',
                    'value': f'.{unique_classes[0]}',
                    'confidence': 0.55,
                    'priority': SELECTOR_PRIORITY['class']
                })
            elif any('el-' in c or 'ant-' in c or 'v-' in c for c in classes):
                component_classes = [c for c in classes if 'el-' in c or 'ant-' in c or 'v-' in c]
                if component_classes:
                    candidates.append({
                        'type': 'css',
                        'value': f'{tag.lower()}.{component_classes[0]}',
                        'confidence': 0.50,
                        'priority': SELECTOR_PRIORITY['class']
                    })

        # 10. type 选择器（输入框）
        if el_type and tag == 'INPUT':
            candidates.append({
                'type': 'css',
                'value': f'input[type="{el_type}"]',
                'confidence': 0.45,
                'priority': SELECTOR_PRIORITY['xpath']
            })

        # 11. XPath 回退
        xpath = _generate_xpath(element)
        if xpath:
            candidates.append({
                'type': 'xpath',
                'value': xpath,
                'confidence': 0.40,
                'priority': SELECTOR_PRIORITY['xpath']
            })

        # ==================== Phase 4: 组合选择器 ====================
        combined = _generate_combined_selectors(element, tag, text, class_name)
        candidates.extend(combined)

    except Exception as e:
        logger.warning(f"提取选择器失败: {e}")

    # 按优先级排序（数字越小优先级越高）
    candidates.sort(key=lambda x: x['priority'])

    # 选择主选择器（最高优先级）
    primary = candidates[0]['value'] if candidates else None

    return {
        'primary': primary,
        'candidates': candidates
    }


def extract_element_info(element) -> Dict[str, Any]:
    """
    提取元素的详细信息

    Args:
        element: Playwright Locator 对象

    Returns:
        Dict: 元素信息字典
    """
    try:
        tag = element.evaluate('el => el.tagName')
        return {
            'tag': tag,
            'type': element.get_attribute('type') or '',
            'placeholder': element.get_attribute('placeholder') or '',
            'text': element.inner_text().strip() if tag in ['BUTTON', 'A', 'LABEL'] else '',
            'id': element.get_attribute('id') or '',
            'name': element.get_attribute('name') or '',
            'className': element.get_attribute('class') or '',
            'ariaLabel': element.get_attribute('aria-label') or '',
            'href': element.get_attribute('href') or '',
            'role': element.get_attribute('role') or '',
        }
    except Exception as e:
        logger.warning(f"提取元素信息失败: {e}")
        return {}


# ==================== 辅助函数 ====================

def _generate_combined_selectors(element, tag: str, text: str, class_name: str) -> List[Dict[str, Any]]:
    """
    生成组合选择器，提高同名元素的区分度（Phase 4）

    策略：
    1. 弹窗/对话框内的元素 → .el-dialog + 元素选择器
    2. 抽屉内的元素 → .el-drawer + 元素选择器
    3. 表格行内的元素 → .el-table__row:has-text() + 元素选择器
    4. Tab 内的元素 → .el-tabs [aria-selected="true"] + 元素选择器
    5. 表单区域内的输入框 → form:has-text() + input

    Args:
        element: Playwright Locator 对象
        tag: 元素标签名
        text: 元素文本
        class_name: 元素class

    Returns:
        List[Dict]: 组合选择器列表
    """
    combined = []

    try:
        # 使用 JS 检测元素的父容器信息
        parent_info = element.evaluate('''el => {
            const result = {
                inDialog: false,
                inDrawer: false,
                inTable: false,
                inTab: false,
                inForm: false,
                dialogTitle: '',
                tableRowText: '',
                tabName: '',
                formText: ''
            };

            let current = el.parentElement;
            while (current && current !== document.body) {
                const cls = current.className || '';
                const tagName = current.tagName.toLowerCase();

                // 检测弹窗/对话框
                if (cls.includes('el-dialog') || cls.includes('modal')) {
                    result.inDialog = true;
                    const titleEl = current.querySelector('.el-dialog__title, .el-dialog__header');
                    if (titleEl) result.dialogTitle = titleEl.textContent.trim().substring(0, 30);
                    break;
                }

                // 检测抽屉
                if (cls.includes('el-drawer')) {
                    result.inDrawer = true;
                    const titleEl = current.querySelector('.el-drawer__title, .el-drawer__header');
                    if (titleEl) result.drawerTitle = titleEl.textContent.trim().substring(0, 30);
                    break;
                }

                // 检测表格行
                if (cls.includes('el-table__row') || cls.includes('table-row')) {
                    result.inTable = true;
                    result.tableRowText = current.textContent.trim().substring(0, 40);
                    break;
                }

                // 检测 Tab
                if (cls.includes('el-tabs') || cls.includes('tab-pane')) {
                    result.inTab = true;
                    const activeTab = current.querySelector('.el-tabs__item.is-active, [aria-selected="true"]');
                    if (activeTab) result.tabName = activeTab.textContent.trim().substring(0, 20);
                    break;
                }

                // 检测表单
                if (tagName === 'form' || cls.includes('el-form')) {
                    result.inForm = true;
                    result.formText = current.textContent.trim().substring(0, 40);
                    break;
                }

                current = current.parentElement;
            }

            return result;
        }''')

        # 生成基本元素选择器
        def _get_element_selector():
            if tag in ['BUTTON', 'A'] and text:
                clean_text = text.replace('"', "'")[:30]
                return f'{tag.lower()}:has-text("{clean_text}")'
            elif tag in ['INPUT', 'TEXTAREA']:
                if class_name:
                    first_class = class_name.split()[0]
                    if first_class:
                        return f'{tag.lower()}.{first_class}'
                return tag.lower()
            elif text:
                clean_text = text.replace('"', "'")[:30]
                return f'{tag.lower()}:visible:has-text("{clean_text}")'
            return tag.lower()

        element_selector = _get_element_selector()

        # 策略1：弹窗/对话框内的元素
        if parent_info.get('inDialog'):
            dialog_title = parent_info.get('dialogTitle', '')
            if dialog_title:
                combined.append({
                    'type': 'css',
                    'value': f'.el-dialog:has-text("{dialog_title}") >> {element_selector}',
                    'confidence': 0.80,
                    'priority': SELECTOR_PRIORITY['combined']
                })
            else:
                combined.append({
                    'type': 'css',
                    'value': f'.el-dialog {element_selector}',
                    'confidence': 0.75,
                    'priority': SELECTOR_PRIORITY['combined']
                })

        # 策略2：抽屉内的元素
        if parent_info.get('inDrawer'):
            combined.append({
                'type': 'css',
                'value': f'.el-drawer {element_selector}',
                'confidence': 0.75,
                'priority': SELECTOR_PRIORITY['combined']
            })

        # 策略3：表格行内的元素
        if parent_info.get('inTable'):
            row_text = parent_info.get('tableRowText', '')
            if row_text and len(row_text) >= 2:
                # 取前20个字符作为行标识
                row_text_short = row_text[:20].replace('"', "'")
                combined.append({
                    'type': 'css',
                    'value': f'.el-table__row:has-text("{row_text_short}") >> {element_selector}',
                    'confidence': 0.70,
                    'priority': SELECTOR_PRIORITY['combined']
                })

        # 策略4：Tab 内的元素
        if parent_info.get('inTab'):
            tab_name = parent_info.get('tabName', '')
            if tab_name:
                combined.append({
                    'type': 'css',
                    'value': f'.el-tabs:has-text("{tab_name}") {element_selector}',
                    'confidence': 0.70,
                    'priority': SELECTOR_PRIORITY['combined']
                })

        # 策略5：表单区域内的输入框
        if parent_info.get('inForm') and tag in ['INPUT', 'TEXTAREA', 'SELECT']:
            form_text = parent_info.get('formText', '')
            if form_text and len(form_text) >= 2:
                form_text_short = form_text[:20].replace('"', "'")
                combined.append({
                    'type': 'css',
                    'value': f'form:has-text("{form_text_short}") {element_selector}',
                    'confidence': 0.70,
                    'priority': SELECTOR_PRIORITY['combined']
                })

    except Exception as e:
        logger.debug(f"生成组合选择器失败: {e}")

    return combined

def _css_escape_id(id_value: str) -> str:
    """
    CSS ID 转义（处理特殊字符）

    Args:
        id_value (str): 原始 ID 值

    Returns:
        str: 转义后的 ID
    """
    # 简单实现：只保留字母、数字、连字符、下划线
    import re
    escaped = re.sub(r'[^a-zA-Z0-9_-]', '-', id_value)
    return escaped


def _generate_xpath(element) -> Optional[str]:
    """
    为元素生成 XPath（回退策略）

    Args:
        element: Playwright Locator 对象

    Returns:
        Optional[str]: XPath 字符串
    """
    try:
        # 使用 JavaScript 生成 XPath
        xpath = element.evaluate('''el => {
            if (el.id) return '//*[@id="' + el.id + '"]';

            // 简单路径生成
            const parts = [];
            let current = el;
            while (current && current.nodeType === Node.ELEMENT_NODE) {
                let index = 0;
                let sibling = current.previousSibling;
                while (sibling) {
                    if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
                        index++;
                    }
                    sibling = sibling.previousSibling;
                }
                const tagName = current.tagName.toLowerCase();
                const indexStr = index > 0 ? `[${index + 1}]` : '';
                parts.unshift(tagName + indexStr);
                current = current.parentNode;
            }
            return '/' + parts.join('/');
        }''')
        return xpath
    except Exception as e:
        logger.debug(f"生成 XPath 失败: {e}")
        return None


def build_step_with_selectors(
    order: int,
    name: str,
    action: str,
    params: Dict[str, Any],
    selectors: Dict[str, Any]
) -> Dict[str, Any]:
    """
    构建包含选择器的步骤定义

    Args:
        order (int): 步骤顺序
        name (str): 步骤名称
        action (str): 动作类型
        params (dict): 动作参数
        selectors (dict): 选择器信息（extract_selectors 的返回值）

    Returns:
        Dict: 完整的步骤定义
    """
    # 将主选择器添加到 params
    params_with_selector = params.copy()
    if selectors.get('primary'):
        params_with_selector['selector'] = selectors['primary']

    # 构建候选选择器列表（用于回退）
    candidate_selectors = []
    for cand in selectors.get('candidates', []):
        candidate_selectors.append(cand['value'])

    return {
        'order': order,
        'name': name,
        'action': action,
        'params': params_with_selector,
        'expected': '',
        'actual': '',
        'selectors': candidate_selectors,
        'locator_type': 'css',
        'locator_value': selectors.get('primary', '')
    }