"""
元素定位路由

提供本地元素定位功能，通过 Playwright 访问被测页面，
提取可交互元素，匹配步骤描述，返回候选定位器列表。
支持单个步骤定位和批量定位（一键获取用例所有步骤的定位）。

@author czj
@date 2026-08-03
"""

from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import logging
import asyncio
import threading
import subprocess
import json
import os
import sys

from app.executors.playwright_executor import PlaywrightExecutor
from app.config import settings

logger = logging.getLogger(__name__)
router = APIRouter(tags=["元素定位"])


# ==================== Schema 定义 ====================

class LocateRequest(BaseModel):
    """元素定位请求"""
    step_description: str
    page_url: str
    expected: str = ""
    auto_login: bool = True


class LocateCandidate(BaseModel):
    """定位候选"""
    locator_type: str
    locator_value: str
    confidence: float
    element_info: Dict[str, Any]


class LocateResponse(BaseModel):
    """元素定位响应"""
    success: bool
    candidates: List[LocateCandidate]
    message: str
    screenshot: Optional[str] = None


class BatchLocateRequest(BaseModel):
    """批量元素定位请求"""
    case_id: str
    page_url: str = settings.TARGET_URL
    auto_login: bool = True
    # === 新增参数（Phase 1: API 增强）===
    page_load_timeout: int = 10000  # 页面加载超时时间（毫秒）
    extra_wait_time: int = 5000    # 页面加载后额外等待时间（毫秒）
    wait_for_selector: str = ""    # 等待特定元素出现（CSS选择器）
    retry_on_empty: bool = True    # 元素为空时是否重试
    max_retries: int = 2           # 最大重试次数
    # === 新增参数（导航功能）===
    navigate_menu: str = ""        # 目标菜单名称（如"会议管理"、"信息管理"）


class StepLocateResult(BaseModel):
    """单个步骤的定位结果"""
    order: int
    description: str
    success: bool
    locator_type: str = ""
    locator_value: str = ""
    confidence: float = 0.0
    message: str = ""


class BatchLocateResponse(BaseModel):
    """批量元素定位响应"""
    success: bool
    case_id: str
    total_steps: int
    located_steps: int
    results: List[StepLocateResult]
    message: str
    # === 新增字段（Phase 1: API 增强）===
    elements_extracted: int = 0    # 提取到的元素总数
    retries: int = 0               # 实际重试次数
    page_load_time: float = 0.0    # 页面加载耗时（秒）


# ==================== 元素提取函数 ====================

def extract_interactive_elements(page) -> list:
    """
    提取页面中所有可交互元素的信息，支持 micro-app 微前端容器

    Phase 2 增强：
    - 更精确的元素去重（使用多重属性组合）
    - 扩展 Element UI 组件支持
    - 支持 Vuetify、Ant Design 等组件库
    - 提取更多元素属性（data-*, title, alt 等）
    """
    return page.evaluate('''() => {
        const elements = [];
        const seen = new Set();

        function getElementKey(el) {
            // 使用多重属性组合作为去重 key
            const parts = [
                el.tagName,
                el.id || '',
                el.name || '',
                el.placeholder || '',
                (el.innerText || '').substring(0, 30).trim(),
                el.getAttribute('aria-label') || '',
                el.className && typeof el.className === 'string' ? el.className.substring(0, 50) : ''
            ];
            return parts.join('|');
        }

        function addElement(el, extra = {}) {
            const key = getElementKey(el);
            if (seen.has(key)) return;
            seen.add(key);

            // 提取 data-* 属性
            const dataAttrs = {};
            for (const attr of el.attributes || []) {
                if (attr.name.startsWith('data-')) {
                    dataAttrs[attr.name] = attr.value;
                }
            }

            elements.push({
                tag: el.tagName,
                type: el.type || '',
                placeholder: el.placeholder || '',
                text: (el.innerText || '').substring(0, 80).trim(),
                id: el.id || '',
                name: el.name || '',
                className: (el.className && typeof el.className === 'string') ? el.className.substring(0, 100) : '',
                ariaLabel: el.getAttribute('aria-label') || '',
                dataTestid: el.getAttribute('data-testid') || el.getAttribute('data-test-id') || '',
                title: el.title || el.getAttribute('title') || '',
                alt: el.alt || '',
                href: el.href || '',
                value: el.value || '',
                role: el.getAttribute('role') || '',
                dataAttrs: dataAttrs,
                ...extra
            });
        }

        // 1. 主文档元素 - 扩展选择器
        document.querySelectorAll('input,button,a,select,textarea,label,[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="tab"],[role="menuitem"],[class*="btn"],[class*="button"],[class*="click"]').forEach(el => addElement(el));

        // 2. micro-app 微前端容器 - 多种方式尝试提取
        document.querySelectorAll('micro-app').forEach(microApp => {
            // 方式1: shadowRoot
            const shadowBody = microApp.querySelector('micro-app-body');
            if (shadowBody && shadowBody.shadowRoot) {
                shadowBody.shadowRoot.querySelectorAll('input,button,a,select,textarea,[role="button"],[role="link"],[class*="el-"]').forEach(el => addElement(el, {inMicroApp: true}));
            }
            // 方式2: 直接在 micro-app 内查找（非 shadow DOM 模式）
            microApp.querySelectorAll('input,button,a,select,textarea,[role="button"],[role="link"],[class*="el-"]').forEach(el => addElement(el, {inMicroApp: true}));
            // 方式3: 查找 micro-app-body 的子元素（无 shadow）
            if (shadowBody && !shadowBody.shadowRoot) {
                shadowBody.querySelectorAll('input,button,a,select,textarea,[role="button"],[role="link"],[class*="el-"]').forEach(el => addElement(el, {inMicroApp: true}));
            }
        });

        // 3. iframe 内容
        document.querySelectorAll('iframe').forEach(iframe => {
            try {
                const iframeDoc = iframe.contentDocument;
                if (iframeDoc) {
                    iframeDoc.querySelectorAll('input,button,a,select,textarea,[role="button"]').forEach(el => addElement(el, {inIframe: true}));
                }
            } catch (e) {}
        });

        // 4. Element UI 组件 - 扩展选择器
        const elSelectors = [
            '.el-input__inner', '.el-input', '.el-select', '.el-button',
            '.el-radio', '.el-checkbox', '.el-switch', '.el-slider',
            '.el-date-editor', '.el-cascader', '.el-transfer',
            '.el-table .el-button', '.el-form-item input',
            '.el-dialog input', '.el-dialog button',
            '.el-dropdown', '.el-dropdown-menu__item'
        ];
        elSelectors.forEach(sel => {
            document.querySelectorAll(sel).forEach(el => {
                const input = el.matches('input, button, a, select, textarea') ? el : el.querySelector('input, button, a, select, textarea');
                if (input) {
                    addElement(input, {isComponent: true, componentClass: el.className ? el.className.substring(0, 50) : '', framework: 'ElementUI'});
                } else {
                    addElement(el, {isComponent: true, componentClass: el.className ? el.className.substring(0, 50) : '', framework: 'ElementUI'});
                }
            });
        });

        // 5. Vuetify 组件
        const vuetifySelectors = [
            '.v-btn', '.v-input', '.v-select', '.v-text-field input',
            '.v-checkbox', '.v-radio', '.v-switch', '.v-slider'
        ];
        vuetifySelectors.forEach(sel => {
            document.querySelectorAll(sel).forEach(el => addElement(el, {isComponent: true, framework: 'Vuetify'}));
        });

        // 6. Ant Design 组件
        const antdSelectors = [
            '.ant-btn', '.ant-input', '.ant-select', '.ant-checkbox',
            '.ant-radio', '.ant-switch', '.ant-slider'
        ];
        antdSelectors.forEach(sel => {
            document.querySelectorAll(sel).forEach(el => addElement(el, {isComponent: true, framework: 'AntDesign'}));
        });

        return elements;
    }''')


def extract_keywords(text: str) -> list:
    """从步骤描述中提取关键词，支持中文语义拆分"""
    action_words = ['输入', '填写', '填入', '键入', '写入', '点击', '按下', '选择',
                    '单击', '双击', '打开', '导航', '访问', '进入', '跳转', '前往',
                    '等待', '延时', '断言', '验证', '检查', '确认', '勾选', '取消',
                    '悬停', '滚动', '截图', '按钮', '链接', '输入框', '下拉框']

    cleaned = text
    for action in sorted(action_words, key=len, reverse=True):
        cleaned = cleaned.replace(action, '')
    cleaned = cleaned.strip()

    keywords = [text]
    if cleaned:
        keywords.append(cleaned)
    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:
                keywords.append(sub)
    for ch in cleaned:
        if ch.strip() and ch not in ['的', '在', '是', '和', '有', '等', '中', '为', '了', '与', '或']:
            keywords.append(ch)

    seen = set()
    return [kw for kw in keywords if kw not in seen and not seen.add(kw)]


def build_locator_candidates(step_description: str, elements: list, page_title: str = "", page_url: str = "", skip_claude: bool = False) -> list:
    """
    关键词算法匹配元素，返回候选定位器列表（按置信度降序）

    Args:
        step_description: 步骤描述
        elements: 页面元素列表
        page_title: 页面标题
        page_url: 页面 URL
        skip_claude: 是否跳过 Claude CLI 调用（批量定位时已在外面批量调用）
    """
    candidates = []
    desc_lower = step_description.lower()
    is_input_action = any(kw in desc_lower for kw in ['输入', '填写', '键入', '写入', '填入'])
    is_click_action = any(kw in desc_lower for kw in ['点击', '按下', '选择', '单击', '双击'])
    keywords = extract_keywords(step_description)
    logger.info(f"步骤描述: {step_description}, 关键词: {keywords}")

    for el in elements:
        score = 0.0
        locator_type = 'css'
        locator_value = ''

        if el.get('id') and any(kw in el['id'] for kw in keywords):
            score += 0.4; locator_value = f"#{el['id']}"
        if el.get('placeholder') and any(kw in el['placeholder'] for kw in keywords):
            score += 0.3; locator_value = f"[placeholder*='{el['placeholder']}']"
        if el.get('ariaLabel') and any(kw in el['ariaLabel'] for kw in keywords):
            score += 0.3; locator_value = f"[aria-label*='{el['ariaLabel']}']"
        if el.get('text') and el['tag'] in ['BUTTON', 'A'] and any(kw in el['text'] for kw in keywords):
            score += 0.25; locator_value = f"{el['tag'].lower()}:has-text('{el['text']}')"
        if el.get('name') and any(kw in el['name'] for kw in keywords):
            score += 0.2; locator_value = f"[name='{el['name']}']"
        if el.get('dataTestid') and any(kw in el['dataTestid'] for kw in keywords):
            score += 0.3; locator_value = f"[data-testid='{el['dataTestid']}']"
        if is_input_action and el['tag'] in ['INPUT', 'TEXTAREA']:
            score += 0.15
        if is_click_action and el['tag'] in ['BUTTON', 'A']:
            score += 0.15
        if el.get('type') and el['tag'] == 'INPUT' and el['type'] in ['text', 'password', 'email', 'tel', 'number']:
            if is_input_action:
                score += 0.1

        if locator_value and score > 0.15:
            candidates.append({
                'locator_type': locator_type, 'locator_value': locator_value,
                'confidence': min(score, 1.0),
                'element_info': {'tag': el.get('tag', ''), 'type': el.get('type', ''),
                                 'placeholder': el.get('placeholder', ''), 'text': el.get('text', ''), 'id': el.get('id', '')}
            })

    candidates.sort(key=lambda x: x['confidence'], reverse=True)

    # 如果 skip_claude=True，跳过 Claude CLI 调用（批量定位时已在外面批量调用）
    if skip_claude:
        return candidates[:5]

    # 单个定位时调用 Claude CLI 进行精确匹配
    logger.info("调用宿主机 Claude CLI 进行精确匹配...")
    claude_result = call_claude_code_via_ssh(step_description, page_url, elements=elements, page_title=page_title)
    if claude_result and claude_result.get('locator_type') and claude_result.get('locator_value'):
        confidence = claude_result.get('confidence', 0.8)
        if confidence > 0.3:  # 降低阈值，只要 Claude 返回有效结果就采纳
            # 检查是否与已有候选重复
            existing_values = [c['locator_value'] for c in candidates]
            if claude_result['locator_value'] not in existing_values:
                candidates.insert(0, {
                    'locator_type': claude_result['locator_type'],
                    'locator_value': claude_result['locator_value'],
                    'confidence': confidence,
                    'element_info': {'tag': '', 'type': '', 'placeholder': '', 'text': '', 'id': ''},
                    '_claude_reason': claude_result.get('reason', 'Claude Code 分析匹配')
                })
                logger.info(f"Claude Code 匹配: {claude_result['locator_value']} (置信度: {confidence})")
            else:
                # 如果重复，提升该候选的置信度
                for c in candidates:
                    if c['locator_value'] == claude_result['locator_value']:
                        c['confidence'] = max(c['confidence'], confidence)
                        c['_claude_reason'] = claude_result.get('reason', 'Claude Code 确认')
                        break
                logger.info(f"Claude Code 确认已有候选: {claude_result['locator_value']}")
        else:
            logger.info(f"Claude Code 置信度过低: {confidence}")
    else:
        logger.warning("Claude Code 未返回有效结果，使用关键词算法候选")

    return candidates[:5]


def call_claude_batch_via_ssh(step_descriptions: list, page_url: str, elements: list, page_title: str = "") -> dict:
    """
    批量调用 Claude CLI 一次性匹配多个步骤（优化性能，减少 SSH 调用次数）

    Args:
        step_descriptions: 步骤描述列表 [{"order": 1, "description": "输入用户名"}, ...]
        page_url: 页面 URL
        elements: 页面元素列表
        page_title: 页面标题

    Returns:
        dict: {order: {"locator_type": ..., "locator_value": ..., "confidence": ..., "reason": ...}}
    """
    HOST = "192.168.5.60"
    HOST_USER = "ubains"
    SSH_PASSWORD = "Ubains@123"

    if not elements:
        logger.warning("没有元素列表可传给 Claude CLI")
        return {}

    if not step_descriptions:
        return {}

    # 构造批量匹配的 prompt
    element_summary = json.dumps(elements[:80], ensure_ascii=False, indent=2)  # 增加到 80 个元素
    steps_json = json.dumps(step_descriptions, ensure_ascii=False, indent=2)

    prompt = f"""你是一个UI自动化测试的定位助手。根据多个步骤描述和页面元素列表，批量匹配所有步骤对应的元素。

页面标题：{page_title}
页面URL：{page_url}

步骤列表（JSON格式）：
{steps_json}

可交互元素列表（JSON格式）：
{element_summary}

请为每个步骤找到最匹配的元素，返回JSON格式结果：
{{
  "results": [
    {{
      "order": 步骤序号,
      "locator_type": "css 或 xpath 或 text 或 id",
      "locator_value": "定位值",
      "confidence": 0.0-1.0（小数表示置信度）,
      "reason": "简要说明为什么匹配这个元素"
    }},
    ...
  ]
}}

注意：
1. 必须为所有步骤返回结果，即使置信度为 0
2. 如果找不到匹配元素，locator_type 和 locator_value 设为空字符串，confidence 设为 0
3. 优先使用 CSS 选择器，语义化定位器优先（如 [placeholder*='xxx']）"""

    try:
        import paramiko
        import base64

        # 将 prompt 编码为 base64 避免特殊字符问题
        prompt_b64 = base64.b64encode(prompt.encode('utf-8')).decode('ascii')

        # 通过 SSH 调用 Claude CLI（增加超时时间到 120 秒）
        cmd = f'echo {prompt_b64} | base64 -d | claude -p --print 2>/dev/null'

        logger.info(f"通过 SSH 调用宿主机 Claude CLI 进行批量元素匹配（{len(step_descriptions)} 个步骤）...")
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(HOST, username=HOST_USER, password=SSH_PASSWORD, timeout=10)

        stdin, stdout, stderr = ssh.exec_command(cmd, timeout=120)
        output = stdout.read().decode('utf-8', errors='replace')
        err = stderr.read().decode('utf-8', errors='replace')
        ssh.close()

        if output:
            # 提取 JSON 结果
            json_start = output.find('{')
            json_end = output.rfind('}') + 1
            if json_start >= 0 and json_end > json_start:
                parsed = json.loads(output[json_start:json_end])
                results = parsed.get('results', [])

                # 转换为以 order 为 key 的字典
                result_map = {}
                for r in results:
                    order = r.get('order', 0)
                    result_map[order] = {
                        'locator_type': r.get('locator_type', ''),
                        'locator_value': r.get('locator_value', ''),
                        'confidence': r.get('confidence', 0),
                        'reason': r.get('reason', 'Claude Code 批量匹配')
                    }

                logger.info(f"Claude CLI 批量匹配完成: {len(result_map)}/{len(step_descriptions)} 个步骤")
                return result_map
            else:
                logger.warning(f"Claude CLI 输出中未找到 JSON: {output[:300]}")
        else:
            logger.warning(f"Claude CLI 无输出, stderr: {err[:200]}")

        return {}

    except Exception as e:
        logger.warning(f"Claude CLI 批量调用异常: {e}")
        return {}


def call_claude_code_via_ssh(step_description: str, page_url: str, expected: str = "",
                             elements: list = None, page_title: str = "") -> dict:
    """
    通过 SSH 调用宿主机上的 Claude CLI 进行元素匹配（单个步骤）

    容器内已通过 Playwright 登录并提取了页面元素，
    这里只把元素列表传给宿主机的 Claude CLI 做精确匹配，
    不再让宿主机重新访问页面（避免登录状态问题）。
    """
    HOST = "192.168.5.60"
    HOST_USER = "ubains"
    SSH_PASSWORD = "Ubains@123"

    if not elements:
        logger.warning("没有元素列表可传给 Claude CLI")
        return {"locator_type": "", "locator_value": "", "confidence": 0, "reason": "无元素数据"}

    # 构造 Claude CLI 的 prompt
    element_summary = json.dumps(elements[:50], ensure_ascii=False, indent=2)
    prompt = f"""你是一个UI自动化测试的定位助手。根据步骤描述和页面元素列表，找到最匹配的元素。

步骤描述：{step_description}
页面标题：{page_title}
页面URL：{page_url}

可交互元素列表（JSON格式）：
{element_summary}

请分析这些元素，找到与步骤描述最匹配的元素，返回JSON格式结果：
{{
  "locator_type": "css 或 xpath 或 text 或 id",
  "locator_value": "定位值",
  "confidence": 0.0-1.0（小数表示置信度）,
  "reason": "简要说明为什么匹配这个元素"
}}

如果找不到匹配的元素，返回：
{{"locator_type": "", "locator_value": "", "confidence": 0, "reason": "未找到匹配元素"}}"""

    try:
        import paramiko
        import base64

        # 将 prompt 编码为 base64 避免特殊字符问题
        prompt_b64 = base64.b64encode(prompt.encode('utf-8')).decode('ascii')

        # 通过 SSH 调用 Claude CLI
        cmd = f'echo {prompt_b64} | base64 -d | claude -p --print 2>/dev/null'

        logger.info(f"通过 SSH 调用宿主机 Claude CLI 进行元素匹配...")
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(HOST, username=HOST_USER, password=SSH_PASSWORD, timeout=10)

        stdin, stdout, stderr = ssh.exec_command(cmd, timeout=60)
        output = stdout.read().decode('utf-8', errors='replace')
        err = stderr.read().decode('utf-8', errors='replace')
        ssh.close()

        if output:
            # 提取 JSON 结果
            json_start = output.find('{')
            json_end = output.rfind('}') + 1
            if json_start >= 0 and json_end > json_start:
                parsed = json.loads(output[json_start:json_end])
                locator_type = parsed.get('locator_type', '')
                locator_value = parsed.get('locator_value', '')
                confidence = parsed.get('confidence', 0)
                reason = parsed.get('reason', 'Claude Code 分析匹配')

                if locator_type and locator_value:
                    logger.info(f"Claude CLI 匹配: {locator_value} (置信度: {confidence})")
                    return {
                        'locator_type': locator_type,
                        'locator_value': locator_value,
                        'confidence': confidence,
                        'reason': reason
                    }
                else:
                    logger.info(f"Claude CLI 未找到匹配: {reason}")
                    return {"locator_type": "", "locator_value": "", "confidence": 0, "reason": reason}
            else:
                logger.warning(f"Claude CLI 输出中未找到 JSON: {output[:300]}")
        else:
            logger.warning(f"Claude CLI 无输出, stderr: {err[:200]}")

        return {"locator_type": "", "locator_value": "", "confidence": 0, "reason": "Claude调用失败"}

    except Exception as e:
        logger.warning(f"Claude CLI 调用异常: {e}")
        return {"locator_type": "", "locator_value": "", "confidence": 0, "reason": f"Claude调用异常: {str(e)}"}


def _navigate_to_menu(page, menu_name: str) -> bool:
    """
    在已登录的页面上导航到指定菜单

    被测系统导航方式：
    1. 首页菜单卡片（.block 元素，包含分类和子菜单）
    2. 直接通过 URL 路由导航（#hash 路由）
    3. 功能中心抽屉（.home_nav_left 打开，再点击菜单项）
    4. 通用 text 选择器

    Args:
        page: Playwright Page 对象
        menu_name: 目标菜单名称（如"会议管理"、"信息管理"）

    Returns:
        bool: 是否成功导航
    """
    if not menu_name:
        return True

    logger.info(f"尝试导航到菜单: {menu_name}")

    # 首页菜单卡片 URL 映射（基于实际探索，2026-08-04）
    # 探索结果：22/24 成功，2 个超时（RSE相关）
    MENU_ROUTES = {
        # 会议管理相关
        '新建会议': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FCreateMeeting',
        '会议模板': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FMeetingtemplate',
        '个人日程': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FtodayMeeting',
        '会议室列表': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FMeetingRoomList',
        '会议列表': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FmeetingAll',

        # 数据统计
        '预定数据': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FStatisticsModule',
        '使用数据': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FUsedataModule',
        '管理看板': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FManagePanel',
        '故障数据': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FErrorModule',

        # 运维管理
        '运维数据': '/#/monitor?monitor=%2Fmonitor%2F%23%2Flogin',
        '运维统计': '/#/backend/monitor?monitor=%2Fmonitor%2F%23%2Flogin',
        '巡检报表': '/#/backend/monitor?monitor=%2Fmonitor%2F%23%2Flogin',

        # 统计报表
        '会议统计': '/#/backend/backstage?backstage=%2Fbackstage%2F%23%2FBackend%2FStatistics%2FMeetingStatistics',
        '通知统计': '/#/backend/backstage?backstage=%2Fbackstage%2F%23%2FBackend%2FStatistics%2FMeetingStatistics',
        '会议服务统计': '/#/backend/backstage?backstage=%2Fbackstage%2F%23%2FBackend%2FStatistics%2FMeetingStatistics',
        '会议室统计': '/#/backend/backstage?backstage=%2Fbackstage%2F%23%2FBackend%2FStatistics%2FMeetingStatistics',

        # 第三方集成
        'Welink会议统计': '/#/backend/backstage?backstage=%2Fbackstage%2F%23%2FBackend%2FStatistics%2FOnlineMeetingStatistics%3FonlineType%25M22',
        '历史记录-WeLink': '/#/backend/meetingV2?meetingV2=%2FmeetingV2%2F%23%2Fmeeting%2Frecord%3FonlineType%25M2Welink',
        'SMC3.0会议统计': '/#/backend/backstage?backstage=%2Fbackstage%2F%23%2FBackend%2FStatistics%2FOnlineMeetingStatistics%3FonlineType%25M21',
        '历史记录-SMC3.0': '/#/backend/meetingV2?meetingV2=%2FmeetingV2%2F%23%2Fmeeting%2Frecord%3FonlineType%25M2SMC3',

        # 概览
        '会议概览': '/#/platform?platform=%2Fplatform%2F%23%2FHome',
        '会议室概览': '/#/platform?platform=%2Fplatform%2F%23%2FHome',

        # 一级菜单别名映射（兼容旧代码）
        '会议管理': '/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FmeetingAll',
        '信息管理': '/#/backend/backstage?backstage=%2Fbackstage%2F%23%2FBackend%2FStatistics%2FMeetingStatistics',
    }

    # 方式0: 通过 URL 路由直接导航（最可靠）
    if menu_name in MENU_ROUTES:
        try:
            base_url = settings.TARGET_URL
            target_url = base_url + MENU_ROUTES[menu_name]
            logger.info(f"通过 URL 路由导航: {target_url}")
            page.goto(target_url, wait_until="domcontentloaded", timeout=15000)
            page.wait_for_timeout(5000)  # 等待微前端子应用加载
            # 验证页面是否加载成功
            current_url = page.url
            logger.info(f"导航后 URL: {current_url}")
            return True
        except Exception as e:
            logger.warning(f"URL 路由导航失败: {e}")

    # 方式1: 点击首页菜单卡片（.block 元素）
    try:
        blocks = page.query_selector_all('.block')
        logger.info(f"找到 {len(blocks)} 个菜单卡片")
        for block in blocks:
            p_elem = block.query_selector('p')
            if p_elem:
                text = (p_elem.inner_text() or '').strip()
                if menu_name in text or text in menu_name:
                    block.click()
                    logger.info(f"通过首页菜单卡片导航成功: {text}")
                    page.wait_for_timeout(3000)
                    return True
    except Exception as e:
        logger.warning(f"首页菜单卡片导航失败: {e}")

    # 方式2: 点击左侧导航菜单（el-menu）
    try:
        menu_items = page.query_selector_all('.el-menu-item')
        for item in menu_items:
            text = (item.inner_text() or '').strip()
            if menu_name in text or text in menu_name:
                item.click()
                logger.info(f"通过左侧菜单导航成功: {text}")
                page.wait_for_timeout(3000)
                return True
    except Exception as e:
        logger.warning(f"左侧菜单导航失败: {e}")

    # 方式3: 点击子菜单标题展开
    try:
        sub_menus = page.query_selector_all('.el-sub-menu__title')
        for sub in sub_menus:
            text = (sub.inner_text() or '').strip()
            if menu_name in text or text in menu_name:
                sub.click()
                page.wait_for_timeout(1000)
                menu_items = page.query_selector_all('.el-menu-item')
                for item in menu_items:
                    item_text = (item.inner_text() or '').strip()
                    if menu_name in item_text or item_text in menu_name:
                        item.click()
                        logger.info(f"通过子菜单导航成功: {item_text}")
                        page.wait_for_timeout(3000)
                        return True
    except Exception as e:
        logger.warning(f"子菜单导航失败: {e}")

    # 方式4: 功能中心抽屉
    try:
        nav_btn = page.query_selector('.home_nav_left')
        if nav_btn and nav_btn.is_visible():
            nav_btn.click()
            page.wait_for_timeout(2000)
            drawer_items = page.query_selector_all('.el-drawer .menu-item, .el-drawer .block, .el-drawer p')
            for item in drawer_items:
                text = (item.inner_text() or '').strip()
                if menu_name in text or text in menu_name:
                    item.click()
                    logger.info(f"通过功能中心导航成功: {text}")
                    page.wait_for_timeout(3000)
                    return True
    except Exception as e:
        logger.warning(f"功能中心导航失败: {e}")

    # 方式5: 使用 Playwright 的 text 选择器点击
    try:
        page.click(f'text="{menu_name}"', timeout=5000)
        logger.info(f"通过 text 选择器导航成功: {menu_name}")
        page.wait_for_timeout(3000)
        return True
    except Exception as e:
        logger.warning(f"text 选择器导航失败: {e}")

    logger.warning(f"所有导航方式均失败，无法导航到: {menu_name}")
    return False


def _infer_menu_from_steps(steps: list) -> str:
    """
    从用例步骤描述中推断目标菜单名称

    根据步骤描述中的关键词匹配菜单名称。
    如果多个步骤指向不同菜单，返回第一个非导航步骤的菜单。

    Args:
        steps: 用例步骤列表

    Returns:
        str: 推断的菜单名称，空字符串表示无法推断
    """
    # 菜单关键词映射（基于实际首页菜单，2026-08-04）
    MENU_KEYWORDS = {
        # 会议管理
        '新建会议': ['新建会议', '创建会议', '快速预约', '预约会议'],
        '会议模板': ['会议模板', '模板', '快速创建'],
        '个人日程': ['个人日程', '我的会议', '今日会议'],
        '会议室列表': ['会议室', '会议室列表', '房间列表'],
        '会议列表': ['会议列表', '所有会议', '会议记录'],

        # 数据统计
        '预定数据': ['预定', '预定数据', '预订统计'],
        '使用数据': ['使用数据', '使用统计', '使用情况'],
        '管理看板': ['管理看板', '看板', '数据看板'],
        '故障数据': ['故障', '故障数据', '故障统计'],

        # 运维管理
        '运维数据': ['运维数据', '运维情况'],
        '运维统计': ['运维统计', '运维报表'],
        '巡检报表': ['巡检', '巡检报表', '巡检记录'],

        # 统计报表
        '会议统计': ['会议统计', '会议报表'],
        '通知统计': ['通知统计', '通知报表'],
        '会议服务统计': ['会议服务', '服务统计'],
        '会议室统计': ['会议室统计', '会议室报表'],

        # 第三方集成
        'Welink会议统计': ['Welink', 'WeLink', 'welink'],
        '历史记录-WeLink': ['WeLink历史', 'WeLink记录'],
        'SMC3.0会议统计': ['SMC', 'SMC3.0', 'smc'],
        '历史记录-SMC3.0': ['SMC历史', 'SMC记录'],
        'RSE会议统计': ['RSE', 'rse'],
        '历史记录-RSE': ['RSE历史', 'RSE记录'],

        # 概览
        '会议概览': ['会议概览', '概览'],
        '会议室概览': ['会议室概览'],

        # 一级菜单别名（兼容旧代码）
        '会议管理': ['会议', '预约', '会议室'],
        '信息管理': ['信息', '通知', '消息', '公告'],
    }

    # 收集所有步骤描述中的关键词
    all_keywords = []
    for step in steps:
        desc = step.get('name', '') or step.get('description', '')
        if not desc.strip():
            continue
        # 跳过导航和等待步骤
        action = step.get('action', '')
        if action in ['navigate', 'wait']:
            continue
        all_keywords.append(desc)

    # 匹配菜单
    menu_scores = {}
    for menu_name, keywords in MENU_KEYWORDS.items():
        score = 0
        for desc in all_keywords:
            for kw in keywords:
                if kw in desc:
                    score += 1
                    break  # 每个步骤只计一次
        if score > 0:
            menu_scores[menu_name] = score

    if not menu_scores:
        return ""

    # 返回得分最高的菜单
    best_menu = max(menu_scores, key=menu_scores.get)
    logger.info(f"根据步骤推断目标菜单: {best_menu} (得分: {menu_scores[best_menu]})")
    return best_menu


def _do_locate_with_executor(executor, page_url: str, auto_login: bool,
                              page_load_timeout: int = 10000,
                              extra_wait_time: int = 5000,
                              wait_for_selector: str = "",
                              retry_on_empty: bool = True,
                              max_retries: int = 2,
                              navigate_menu: str = "") -> dict:
    """
    使用已有的 PlaywrightExecutor 执行定位准备工作

    返回 { success, elements, page_title, current_url, message, elements_extracted, retries, page_load_time }
    """
    import time
    start_time = time.time()
    retries = 0
    elements_extracted = 0

    # 自动登录
    if auto_login and not executor._is_logged_in:
        logger.info("执行自动登录...")
        login_success = executor.do_login()
        if not login_success:
            return {'success': False, 'elements': [], 'page_title': '', 'current_url': '',
                    'message': '自动登录失败', 'elements_extracted': 0, 'retries': 0, 'page_load_time': 0}

    # 导航到目标菜单（如果指定）
    if navigate_menu and executor._is_logged_in:
        logger.info(f"导航到目标菜单: {navigate_menu}")
        nav_success = _navigate_to_menu(executor._page, navigate_menu)
        if not nav_success:
            logger.warning(f"导航到 {navigate_menu} 失败，将使用当前页面的元素")

    # 访问目标页面
    logger.info(f"访问页面: {page_url}")
    current_url = executor._page.url
    is_login_page = current_url.rstrip('/') == settings.TARGET_URL.rstrip('/')
    is_target_login = page_url.rstrip('/') == settings.TARGET_URL.rstrip('/') or 'login' in page_url.lower()

    if executor._is_logged_in and is_target_login and not is_login_page:
        logger.info(f"已在登录后页面，跳过导航。当前 URL: {current_url}")
    else:
        try:
            # 使用可配置的超时时间
            executor._page.goto(page_url, wait_until="domcontentloaded", timeout=page_load_timeout)
        except Exception as e:
            logger.warning(f"页面加载超时: {e}")
            try:
                executor._page.goto(page_url, wait_until="domcontentloaded", timeout=15000)
            except Exception as e2:
                logger.warning(f"页面加载超时(domcontentloaded)，继续: {e2}")

    # 等待页面稳定（使用可配置的额外等待时间）
    executor._page.wait_for_timeout(extra_wait_time)

    # 如果指定了等待特定元素，先等待该元素出现
    if wait_for_selector:
        logger.info(f"等待特定元素出现: {wait_for_selector}")
        try:
            executor._page.wait_for_selector(wait_for_selector, timeout=extra_wait_time)
            logger.info(f"元素已出现: {wait_for_selector}")
        except Exception as e:
            logger.warning(f"等待元素超时，继续执行: {e}")

    # 微前端页面需要额外等待子应用加载
    try:
        executor._page.wait_for_selector('micro-app', timeout=5000)
        logger.info("检测到 micro-app 容器，等待子应用加载...")
        executor._page.wait_for_timeout(3000)
    except Exception:
        pass

    # 提取元素（支持重试机制）
    elements = extract_interactive_elements(executor._page)
    elements_extracted = len(elements)

    if not elements and retry_on_empty:
        for retry_count in range(max_retries):
            retries += 1
            logger.info(f"元素提取为空，第 {retries} 次重试（共 {max_retries} 次）...")
            executor._page.wait_for_timeout(5000)
            elements = extract_interactive_elements(executor._page)
            elements_extracted = len(elements)
            if elements:
                logger.info(f"重试成功，提取到 {len(elements)} 个元素")
                break

    page_title = executor._page.title()
    current_url = executor._page.url
    page_load_time = time.time() - start_time

    logger.info(f"提取到 {len(elements)} 个元素，页面标题: {page_title}，耗时: {page_load_time:.1f}s，重试: {retries} 次")

    return {
        'success': True,
        'elements': elements,
        'page_title': page_title,
        'current_url': current_url,
        'message': 'ok',
        'elements_extracted': elements_extracted,
        'retries': retries,
        'page_load_time': page_load_time
    }


# ==================== 单个步骤定位 API ====================

@router.post("/locate", response_model=LocateResponse)
async def locate_element(request: LocateRequest):
    """
    单个步骤元素定位
    启动 Playwright → 登录 → 访问页面 → 提取元素 → 匹配定位器
    """
    logger.info(f"开始元素定位: {request.step_description} @ {request.page_url}")
    executor = None

    try:
        config = {"headless": settings.PLAYWRIGHT_HEADLESS, "timeout": 30000, "screenshot": False, "auto_login": request.auto_login}
        executor = PlaywrightExecutor(config)
        result_container = {'success': False, 'candidates': [], 'message': '', 'screenshot': None}

        def _playwright_worker():
            try:
                import threading as _threading
                if _threading.current_thread() is not _threading.main_thread():
                    asyncio.set_event_loop(None)

                executor.start()
                prep = _do_locate_with_executor(executor, request.page_url, request.auto_login)
                if not prep['success']:
                    result_container['message'] = prep['message']
                    return

                candidates = build_locator_candidates(request.step_description, prep['elements'], prep['page_title'], request.page_url)
                logger.info(f"匹配到 {len(candidates)} 个候选定位器")

                screenshot = None
                try:
                    import base64
                    screenshot = base64.b64encode(executor._page.screenshot()).decode('utf-8')
                except Exception as e:
                    logger.warning(f"截图失败: {e}")

                result_container.update({'success': True, 'candidates': candidates, 'message': f"找到 {len(candidates)} 个候选定位器", 'screenshot': screenshot})
            except Exception as e:
                logger.error(f"定位过程出错: {e}", exc_info=True)
                result_container['message'] = f"定位失败: {str(e)}"
            finally:
                try:
                    executor.stop()
                except Exception as e:
                    logger.warning(f"停止执行器失败: {e}")

        worker_thread = threading.Thread(target=_playwright_worker, daemon=True)
        worker_thread.start()
        worker_thread.join(timeout=70)

        if worker_thread.is_alive():
            return LocateResponse(success=False, candidates=[], message="定位超时")

        return LocateResponse(**result_container)

    except Exception as e:
        logger.error(f"元素定位异常: {e}", exc_info=True)
        return LocateResponse(success=False, candidates=[], message=f"定位异常: {str(e)}")


# ==================== 批量定位 API ====================

@router.post("/locate-batch", response_model=BatchLocateResponse)
async def locate_batch(request: BatchLocateRequest):
    """
    批量元素定位（一键获取用例所有步骤的定位）

    流程：
    1. 从数据库获取用例的所有步骤
    2. 启动 Playwright → 登录 → 访问页面
    3. 提取页面元素
    4. 为每个步骤匹配定位器
    5. 更新用例步骤到数据库
    6. 返回每个步骤的定位结果
    """
    logger.info(f"开始批量定位: 用例 {request.case_id}")

    # 延迟导入避免循环依赖
    from app.services.case_service import CaseService
    from app.database import async_session_maker

    # 获取用例数据
    async with async_session_maker() as session:
        case_service = CaseService(session)
        case = await case_service.get(request.case_id)
        if not case:
            return BatchLocateResponse(success=False, case_id=request.case_id, total_steps=0, located_steps=0, results=[], message="用例不存在")

    steps = case.steps if isinstance(case.steps, list) else []
    if not steps:
        return BatchLocateResponse(success=False, case_id=request.case_id, total_steps=0, located_steps=0, results=[], message="用例没有步骤")

    logger.info(f"用例有 {len(steps)} 个步骤")

    # 智能推断目标菜单（如果未指定）
    navigate_menu = request.navigate_menu
    if not navigate_menu and request.auto_login:
        navigate_menu = _infer_menu_from_steps(steps)
        if navigate_menu:
            logger.info(f"智能推断目标菜单: {navigate_menu}")

    # 在线程中执行 Playwright
    result_container = {
        'success': False, 'case_id': request.case_id,
        'total_steps': len(steps), 'located_steps': 0,
        'results': [], 'message': '',
        'elements_extracted': 0, 'retries': 0, 'page_load_time': 0.0
    }
    updated_steps = [dict(s) if not isinstance(s, dict) else s for s in steps]

    def _batch_worker():
        executor = None
        try:
            import threading as _threading
            if _threading.current_thread() is not _threading.main_thread():
                asyncio.set_event_loop(None)

            config = {"headless": settings.PLAYWRIGHT_HEADLESS, "timeout": 30000, "screenshot": False, "auto_login": request.auto_login}
            executor = PlaywrightExecutor(config)
            executor.start()

            # 访问页面并提取元素（只做一次，传递增强参数）
            prep = _do_locate_with_executor(
                executor, request.page_url, request.auto_login,
                page_load_timeout=request.page_load_timeout,
                extra_wait_time=request.extra_wait_time,
                wait_for_selector=request.wait_for_selector,
                retry_on_empty=request.retry_on_empty,
                max_retries=request.max_retries,
                navigate_menu=navigate_menu
            )
            if not prep['success']:
                result_container['message'] = prep['message']
                return

            elements = prep['elements']
            page_title = prep['page_title']
            result_container['elements_extracted'] = prep['elements_extracted']
            result_container['retries'] = prep['retries']
            result_container['page_load_time'] = prep['page_load_time']
            located_count = 0

            # === 性能优化：批量调用 Claude CLI（一次性匹配所有步骤）===
            # 1. 收集需要定位的步骤
            steps_to_locate = []
            step_index_map = {}  # order -> 索引
            for idx, step in enumerate(updated_steps):
                step_desc = step.get('name', '') or step.get('description', '')
                step_order = step.get('order', 0)
                action = step.get('action', '')

                # 跳过空描述和导航步骤
                if not step_desc.strip():
                    result_container['results'].append(StepLocateResult(
                        order=step_order, description=step_desc, success=False, message="步骤描述为空"
                    ))
                    continue

                if action == 'navigate':
                    result_container['results'].append(StepLocateResult(
                        order=step_order, description=step_desc, success=True,
                        locator_type="", locator_value="", confidence=1.0, message="导航步骤无需定位"
                    ))
                    located_count += 1
                    continue

                steps_to_locate.append({'order': step_order, 'description': step_desc})
                step_index_map[step_order] = idx

            # 2. 批量调用 Claude CLI 一次性匹配（核心优化）
            logger.info(f"批量调用 Claude CLI 匹配 {len(steps_to_locate)} 个步骤...")
            claude_results = {}
            if steps_to_locate:
                try:
                    claude_results = call_claude_batch_via_ssh(
                        step_descriptions=steps_to_locate,
                        page_url=request.page_url,
                        elements=elements,
                        page_title=page_title
                    )
                except Exception as e:
                    logger.warning(f"Claude 批量匹配失败，将使用关键词算法: {e}")

            # 3. 为每个步骤匹配定位器
            for step_info in steps_to_locate:
                step_order = step_info['order']
                step_desc = step_info['description']
                step_idx = step_index_map[step_order]
                step = updated_steps[step_idx]

                # 优先使用 Claude 批量匹配结果
                claude_result = claude_results.get(step_order, {})
                if claude_result and claude_result.get('locator_value'):
                    # 使用 Claude 结果
                    locator_type = claude_result['locator_type']
                    locator_value = claude_result['locator_value']
                    confidence = claude_result.get('confidence', 0.8)
                    message = f"Claude 批量匹配 (置信度 {confidence:.0%})"
                else:
                    # 回退到关键词算法（skip_claude=True 避免重复调用）
                    candidates = build_locator_candidates(step_desc, elements, page_title, request.page_url, skip_claude=True)
                    if candidates:
                        best = candidates[0]
                        locator_type = best['locator_type']
                        locator_value = best['locator_value']
                        confidence = best['confidence']
                        message = f"关键词匹配 (置信度 {confidence:.0%})"
                    else:
                        # 无匹配
                        result_container['results'].append(StepLocateResult(
                            order=step_order, description=step_desc, success=False,
                            message="未找到匹配元素"
                        ))
                        continue

                # 更新步骤的定位信息
                step['locatorType'] = locator_type
                step['locatorValue'] = locator_value
                step['locator_type'] = locator_type
                step['locator_value'] = locator_value
                if locator_value and 'params' in step:
                    step['params']['selector'] = locator_value
                elif locator_value:
                    step['params'] = {'selector': locator_value}

                result_container['results'].append(StepLocateResult(
                    order=step_order, description=step_desc, success=True,
                    locator_type=locator_type, locator_value=locator_value,
                    confidence=confidence, message=message
                ))
                located_count += 1

            result_container['located_steps'] = located_count
            result_container['success'] = True
            result_container['message'] = f"成功定位 {located_count}/{len(steps)} 个步骤"

        except Exception as e:
            logger.error(f"批量定位出错: {e}", exc_info=True)
            result_container['message'] = f"批量定位失败: {str(e)}"
        finally:
            if executor:
                try:
                    executor.stop()
                except:
                    pass

    worker_thread = threading.Thread(target=_batch_worker, daemon=True)
    worker_thread.start()
    worker_thread.join(timeout=600)  # 10 minutes for batch locate (login + menu navigation + element extraction + each step needs SSH + Claude CLI)

    if worker_thread.is_alive():
        return BatchLocateResponse(success=False, case_id=request.case_id, total_steps=len(steps), located_steps=0, results=[], message="批量定位超时")

    # 如果定位成功，更新数据库中的用例步骤
    if result_container['success'] and result_container['located_steps'] > 0:
        try:
            # 直接更新数据库中的 steps 字段，绕过 schema 校验
            from app.database import async_session_maker
            from app.models.test_case import TestCase
            from sqlalchemy import update as sql_update

            async with async_session_maker() as session:
                stmt = sql_update(TestCase).where(TestCase.id == request.case_id).values(steps=updated_steps)
                await session.execute(stmt)
                await session.commit()
            logger.info(f"用例 {request.case_id} 步骤已更新到数据库")
        except Exception as e:
            logger.warning(f"更新数据库失败: {e}")

    return BatchLocateResponse(**result_container)
