#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：smart_locate_service.py
模块描述：智能定位服务，自动访问被测系统、执行操作、提取选择器

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

import logging
import os
import base64
from typing import List, Dict, Any, Optional
from datetime import datetime

from app.executors.playwright_executor import PlaywrightExecutor
from app.services.keyword_matcher import (
    extract_keywords,
    extract_value_from_description,
    detect_action_type,
    is_verification_step,
    match_element_by_keywords,
    find_element_by_semantic
)
from app.services.selector_extractor import (
    extract_selectors,
    extract_element_info,
    build_step_with_selectors
)
from app.services.login_template_service import LoginTemplateService
from app.config import settings

logger = logging.getLogger(__name__)


class SmartLocateService:
    """
    智能定位服务

    核心流程：
    1. 启动 Playwright 浏览器
    2. 执行登录模板（自动登录）
    3. 导航到目标菜单
    4. 逐步骤定位 + 执行
    5. 返回所有步骤的选择器

    特点：
    - 不依赖 Claude CLI，通过实际执行验证选择器
    - 支持登录模板自动执行
    - 支持功能中心导航
    - 提取多候选选择器，按优先级回退
    """

    def __init__(self):
        """初始化智能定位服务"""
        self.executor: Optional[PlaywrightExecutor] = None
        self.screenshot_dir = settings.SCREENSHOT_DIR

    def locate_steps(
        self,
        steps: List[Dict[str, Any]],
        auto_login: bool = True,
        navigate_menu: str = "",
        page_url: str = "https://192.168.5.44"
    ) -> List[Dict[str, Any]]:
        """
        智能定位主流程

        Args:
            steps (List[Dict]): 步骤列表，格式：
                [
                    {"order": 1, "name": "输入用户名", "action": "fill", "params": {"value": "admin@xty"}},
                    {"order": 2, "name": "点击登录按钮", "action": "click", "params": {}}
                ]
            auto_login (bool): 是否自动登录
            navigate_menu (str): 目标菜单名称（如"信息发布"）
            page_url (str): 被测系统基础 URL

        Returns:
            List[Dict]: 定位结果列表，格式：
                [
                    {
                        "order": 1,
                        "name": "输入用户名",
                        "success": True,
                        "action": "fill",
                        "params": {"selector": "...", "value": "admin@xty"},
                        "selectors": {...},
                        "screenshot": "base64..."
                    }
                ]
        """
        logger.info(f"开始智能定位: {len(steps)} 个步骤, auto_login={auto_login}, navigate_menu={navigate_menu}")

        results = []

        try:
            # 1. 启动 Playwright
            logger.info("启动 Playwright 浏览器...")
            self.executor = PlaywrightExecutor({
                'headless': settings.PLAYWRIGHT_HEADLESS,
                'timeout': 30000,
                'screenshot': True,
                'screenshot_dir': self.screenshot_dir,
                'ignore_https_errors': True,
            })
            self.executor.start()

            # 2. 自动登录
            if auto_login:
                logger.info("执行自动登录...")
                login_success = self._do_login_template()
                if not login_success:
                    logger.warning("自动登录失败，可能影响后续步骤定位")
                else:
                    logger.info("自动登录成功")

            # 3. 导航到目标菜单
            if navigate_menu:
                logger.info(f"导航到目标菜单: {navigate_menu}")
                nav_success = self._navigate_to_menu(navigate_menu)
                if not nav_success:
                    logger.warning(f"导航到菜单 '{navigate_menu}' 失败")

            # 4. 逐步骤定位
            for step in steps:
                result = self._locate_single_step(step)
                results.append(result)

                # 如果定位成功且操作类型需要执行，则实际执行（确保页面状态正确）
                if result.get('success') and result.get('action') != 'navigate':
                    self._execute_step(result)

            logger.info(f"智能定位完成: {len(results)} 个步骤")

        except Exception as e:
            logger.error(f"智能定位异常: {e}")
            # 将异常信息添加到结果
            if not results:
                results.append({
                    'order': 0,
                    'name': '智能定位',
                    'success': False,
                    'message': f'定位异常: {str(e)}'
                })

        finally:
            # 5. 关闭浏览器
            if self.executor:
                try:
                    self.executor.stop()
                    logger.info("Playwright 浏览器已关闭")
                except Exception as e:
                    logger.warning(f"关闭浏览器失败: {e}")

        return results

    def _do_login_template(self) -> bool:
        """
        执行登录模板（使用 PlaywrightExecutor 内置的 do_login 方法）

        Returns:
            bool: 是否登录成功
        """
        try:
            return self.executor.do_login()
        except Exception as e:
            logger.error(f"执行登录模板失败: {e}")
            return False

    def _navigate_to_menu(self, menu_name: str) -> bool:
        """
        导航到目标菜单（功能中心 → 抽屉 → 目标菜单）
        支持二级菜单：先点击分类展开，再点击子菜单

        Args:
            menu_name (str): 目标菜单名称

        Returns:
            bool: 是否导航成功
        """
        # 二级菜单映射：子菜单 → 分类（使用 menu_mapping 统一配置）
        try:
            from app.utils.menu_mapping import get_menu_parent, is_direct_menu
            category = get_menu_parent(menu_name)
        except ImportError:
            # 回退：本地映射
            category_map = {
                '资产信息': '资产管理', '资产故障': '资产管理', '资产设备': '资产管理',
                '壁纸推送': '集控控制', '脚本命令': '集控控制', '应用管理': '集控控制',
                '屏幕管理': '集控控制', '软件卸载': '集控控制', '设备网络': '集控控制',
                '门口屏发布': '集控控制', '播放器发布': '集控控制',
                '运维设备': '会议运维', '运维巡检': '会议运维',
                '工单列表': '维护工单', '我的工单': '维护工单', '告警工单': '维护工单',
                '信息窗管理': '信息管理', '消息通知': '信息管理',
                '会务统筹': '会务管理', '会务工单': '会务管理',
            }
            category = category_map.get(menu_name)

        try:
            page = self.executor._page

            # 步骤 1: 点击功能中心图标
            logger.debug("点击功能中心图标...")
            try:
                # XPath 选择器（已验证，指向 i 元素）
                page.click("//*[@id='Home']/div[1]/div[1]/i", timeout=5000)
                logger.info("成功点击功能中心图标（XPath选择器）")
            except Exception as e:
                logger.warning(f"XPath选择器点击失败: {e}")
                # 回退：尝试 class 选择器
                try:
                    page.click('.home_nav_left', timeout=5000)
                    logger.info("成功点击功能中心图标（class选择器回退）")
                except Exception as e2:
                    logger.error(f"无法点击功能中心图标: {e2}")
                    return False

            # 步骤 2: 等待功能抽屉打开并完成动画
            logger.debug("等待功能抽屉打开...")
            try:
                # 2.1 等待抽屉元素出现
                page.wait_for_selector('.el-drawer', timeout=5000)
                logger.debug("抽屉元素已出现")

                # 2.2 等待抽屉动画完成（检测抽屉是否展开）
                # Element Plus 抽屉展开动画约 300ms
                import time
                time.sleep(0.3)

                # 2.3 确认抽屉已完全展开
                for _ in range(10):  # 最多等待 1 秒
                    try:
                        drawer = page.query_selector('.el-drawer')
                        if drawer:
                            # 检查抽屉是否展开（宽度大于 0 且可见）
                            is_open = drawer.evaluate("""
                                el => {
                                    const style = window.getComputedStyle(el);
                                    const width = parseFloat(style.width);
                                    return width > 100;  // 抽屉展开后宽度应该大于 100px
                                }
                            """)
                            if is_open:
                                logger.debug("抽屉已完全展开")
                                break
                    except Exception:
                        pass
                    time.sleep(0.1)

                # 2.4 额外稳定等待
                time.sleep(0.2)

            except Exception as e:
                logger.warning(f"等待抽屉超时: {e}，继续尝试后续步骤")

            # 步骤 3: 如果是二级菜单，先点击分类展开
            if category:
                logger.debug(f"二级菜单导航: {category} -> {menu_name}")
                try:
                    category_selector = f'.el-drawer >> text="{category}"'
                    # 检查元素是否存在
                    cat_element = page.query_selector(category_selector)
                    if cat_element:
                        # 滚动到可见区域
                        cat_element.scroll_into_view_if_needed()
                        page.wait_for_timeout(300)
                        cat_element.click(timeout=5000)
                        page.wait_for_timeout(800)
                        logger.info(f"成功点击分类: {category}")
                    else:
                        logger.warning(f"未找到分类元素: {category}")
                except Exception as e:
                    logger.warning(f"点击分类失败: {e}，尝试直接点击菜单")

            # 步骤 4: 点击目标菜单（带滚动和重试）
            logger.debug(f"点击目标菜单: {menu_name}")

            # 菜单选择器候选列表（按优先级排序）
            menu_selectors = [
                f'.el-drawer >> text="{menu_name}"',           # 优先：直接文本匹配
                f'.el-drawer a:has-text("{menu_name}")',       # 备选：链接元素
                f'.el-drawer button:has-text("{menu_name}")',  # 备选：按钮元素
                f'.el-drawer [role="menuitem"]:has-text("{menu_name}")',  # 备选：菜单项
            ]

            click_success = False
            for i, menu_selector in enumerate(menu_selectors):
                try:
                    logger.debug(f"尝试选择器 {i+1}/{len(menu_selectors)}: {menu_selector}")

                    # 检查元素是否存在
                    element = page.query_selector(menu_selector)
                    if not element:
                        logger.debug(f"选择器 {i+1} 未找到元素")
                        continue

                    # 滚动到可见区域
                    element.scroll_into_view_if_needed()
                    page.wait_for_timeout(300)  # 滚动后稳定

                    # 等待元素可点击状态
                    page.wait_for_selector(menu_selector, state="visible", timeout=3000)

                    # 执行点击
                    element.click(timeout=5000)
                    click_success = True
                    logger.info(f"成功点击菜单（选择器 {i+1}）: {menu_name}")
                    break

                except Exception as e:
                    logger.warning(f"选择器 {i+1} 点击失败: {e}")
                    continue

            if click_success:
                page.wait_for_timeout(1500)  # 等待页面加载
                logger.info(f"成功导航到菜单: {menu_name}")
                return True
            else:
                logger.error(f"所有选择器都无法点击菜单: {menu_name}")
                return False

        except Exception as e:
            logger.error(f"导航到菜单异常: {e}")
            return False

    def _locate_single_step(self, step: Dict[str, Any]) -> Dict[str, Any]:
        """
        为单个步骤定位元素

        Args:
            step (Dict): 步骤定义

        Returns:
            Dict: 定位结果
        """
        order = step.get('order', 0)
        name = step.get('name', '')
        action = step.get('action', '')
        params = step.get('params', {})

        logger.info(f"定位步骤 {order}: {name} (action={action})")

        result = {
            'order': order,
            'name': name,
            'success': False,
            'action': action,
            'params': params,
            'selectors': {'primary': None, 'candidates': []},
            'element_info': {},
            'screenshot': None,
            'message': ''
        }

        try:
            # ⚠️ 关键修复：先获取 page 对象，验证步骤需要使用
            page = self.executor._page

            # navigate 类型：需要设置 url 参数
            if action == 'navigate':
                # 检查是否已有 url 参数
                url = params.get('url', '')
                if not url:
                    # 从步骤名称中提取 URL 或使用默认值
                    import re
                    url_match = re.search(r'https?://[^\s]+', name)
                    if url_match:
                        url = url_match.group(0)
                    else:
                        # 使用默认 URL
                        url = 'https://192.168.5.44'

                result['success'] = True
                result['params']['url'] = url
                result['selectors'] = {'primary': url, 'candidates': []}
                result['message'] = f'导航步骤: {url}'
                logger.info(f"步骤 {order} 导航到: {url}")
                return result

            # ⚠️ 新增：登录相关步骤优先使用已验证的选择器
            login_selectors = self._get_login_selector(name)
            if login_selectors:
                result['success'] = True
                result['selectors'] = {
                    'primary': login_selectors,
                    'candidates': [{'type': 'css', 'value': login_selectors, 'confidence': 0.95, 'priority': 1}]
                }
                result['params']['selector'] = login_selectors
                result['message'] = f'登录模板选择器: {login_selectors}'
                logger.info(f"步骤 {order} 使用登录模板选择器: {login_selectors}")

                # 自动修正 fill 步骤的 value
                if action == 'fill':
                    extracted_value = extract_value_from_description(name)
                    if extracted_value:
                        result['params']['value'] = extracted_value
                        logger.info(f"步骤 {order} 自动修正 value: '{params.get('value', '')}' -> '{extracted_value}'")

                return result

            # ⚠️ 新增：验证类步骤特殊处理
            # 验证类步骤（如"查看是否新增成功"）支持两种验证方式：
            # 1. 提示语验证：等待成功提示出现
            # 2. 列表搜索验证：在列表中搜索判断是否新增
            if is_verification_step(name):
                logger.info(f"步骤 {order} 为验证类步骤，尝试查找操作结果")

                # ===== 方式1：提示语验证 =====
                message_selectors = [
                    '.el-message--success .el-message__content',  # 成功提示
                    '.el-message__content',  # 通用提示内容
                    '//p[@class="el-message__content"]',  # XPath 方式
                    '.el-message',  # 整个提示框
                ]

                for sel in message_selectors:
                    try:
                        # 等待提示出现（操作后通常会有短暂延迟）
                        page.wait_for_selector(sel, timeout=5000)
                        element = page.locator(sel).first
                        if element:
                            # 提取提示文本用于验证
                            message_text = element.inner_text().strip()
                            logger.info(f"找到操作结果提示: '{message_text}'")

                            result['success'] = True
                            result['action'] = 'wait'  # 改为 wait 类型
                            result['selectors'] = {
                                'primary': sel,
                                'candidates': [
                                    {'type': 'css', 'value': sel, 'confidence': 0.95, 'priority': 1}
                                ]
                            }
                            result['params']['selector'] = sel
                            result['params']['timeout'] = 10000
                            result['params']['expected_text'] = message_text  # 记录提示文本
                            result['params']['verify_method'] = 'message'  # 验证方式
                            result['message'] = f'验证类步骤（提示语），找到提示: {message_text}'
                            logger.info(f"步骤 {order} 定位成功（提示验证）: {sel}")

                            # 截图
                            try:
                                screenshot_path = os.path.join(
                                    self.screenshot_dir,
                                    f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
                                )
                                page.screenshot(path=screenshot_path)
                                with open(screenshot_path, 'rb') as f:
                                    result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
                            except Exception as e:
                                logger.debug(f"截图失败（非致命）: {e}")

                            return result
                    except Exception as e:
                        logger.debug(f"提示语选择器 {sel} 未找到: {e}")
                        continue

                logger.info(f"未找到提示语，尝试列表搜索验证...")

                # ===== 方式2：列表搜索验证 =====
                # 查找页面上的搜索输入框
                search_input_selectors = [
                    'input[placeholder*="搜索"]',
                    'input[placeholder*="查询"]',
                    'input[placeholder*="查找"]',
                    'input[placeholder*="关键字"]',
                    '.search-input',
                    '.el-input__inner',
                ]

                for sel in search_input_selectors:
                    try:
                        element = page.locator(sel).first
                        if element and element.is_visible():
                            logger.info(f"找到搜索输入框: {sel}")

                            result['success'] = True
                            result['action'] = 'fill'  # 改为 fill 类型（输入搜索词）
                            result['selectors'] = {
                                'primary': sel,
                                'candidates': [
                                    {'type': 'css', 'value': sel, 'confidence': 0.85, 'priority': 1}
                                ]
                            }
                            result['params']['selector'] = sel
                            # 从步骤名称中提取搜索关键词（如"查看列表是否正确新增数据"）
                            # 用户需要手动指定搜索词，这里只提供选择器
                            result['params']['verify_method'] = 'search'  # 验证方式
                            result['message'] = f'验证类步骤（列表搜索），找到搜索框: {sel}'
                            logger.info(f"步骤 {order} 定位成功（列表搜索）: {sel}")

                            # 截图
                            try:
                                screenshot_path = os.path.join(
                                    self.screenshot_dir,
                                    f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
                                )
                                page.screenshot(path=screenshot_path)
                                with open(screenshot_path, 'rb') as f:
                                    result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
                            except Exception as e:
                                logger.debug(f"截图失败（非致命）: {e}")

                            return result
                    except Exception as e:
                        logger.debug(f"搜索框选择器 {sel} 未找到: {e}")
                        continue

                # 如果两种方式都没有找到
                logger.warning(f"步骤 {order} 为验证类但未找到提示元素或搜索框")
                result['success'] = False
                result['message'] = f'验证类步骤，但未找到操作结果提示元素或列表搜索框'
                return result

            # 提取关键词
            keywords = extract_keywords(name)
            logger.debug(f"提取关键词: {keywords}")

            # ⚠️ 新增：检查是否需要在抽屉内查找
            drawer_keywords = ['资产管理', '资产信息', '资产故障', '资产设备',
                              '会议预约', '会议运维', '会议转录', '信息发布',
                              '数据统计', '运维维护', '维护工单', '会务管理',
                              '信息管理', '集控控制', '预定']
            in_drawer = any(kw in name for kw in drawer_keywords)

            # 第一级：关键词直接匹配
            if in_drawer:
                # ⚠️ 优先在抽屉内查找
                logger.debug(f"检测到抽屉内元素，优先在 .el-drawer 内查找")
                element, selectors = match_element_by_keywords(page, keywords, action)

                if not element:
                    # 尝试直接用文本匹配抽屉内的元素
                    for kw in keywords:
                        if len(kw) >= 2:
                            try:
                                drawer_selector = f'.el-drawer >> text="{kw}"'
                                page.wait_for_selector(drawer_selector, timeout=2000)
                                element = page.locator(drawer_selector).first
                                selectors = [{
                                    'type': 'css',
                                    'value': drawer_selector,
                                    'confidence': 0.90,
                                    'priority': 1
                                }]
                                logger.info(f"在抽屉内找到元素: {drawer_selector}")
                                break
                            except Exception:
                                continue
            else:
                element, selectors = match_element_by_keywords(page, keywords, action)

            if element and selectors:
                result['success'] = True
                # 将 selectors 列表转换为字典格式
                primary_selector = selectors[0]['value'] if selectors else None
                result['selectors'] = {
                    'primary': primary_selector,
                    'candidates': selectors
                }
                result['element_info'] = extract_element_info(element)
                result['message'] = f'关键词匹配成功: {primary_selector}'
                logger.info(f"步骤 {order} 定位成功（关键词匹配）: {primary_selector}")
            else:
                # 第二级：语义推断
                element, selectors = find_element_by_semantic(page, keywords, action)

                if element and selectors:
                    result['success'] = True
                    # 将 selectors 列表转换为字典格式
                    primary_selector = selectors[0]['value'] if selectors else None
                    result['selectors'] = {
                        'primary': primary_selector,
                        'candidates': selectors
                    }
                    result['element_info'] = extract_element_info(element)
                    result['message'] = f'语义推断成功: {primary_selector}'
                    logger.info(f"步骤 {order} 定位成功（语义推断）: {primary_selector}")
                else:
                    result['success'] = False
                    result['message'] = f'未找到匹配元素: {name}'
                    logger.warning(f"步骤 {order} 定位失败: {result['message']}")

            # 更新 params 中的 selector
            if result['success'] and result['selectors'].get('primary'):
                result['params']['selector'] = result['selectors']['primary']

            # ⚠️ 自动修正 fill 步骤的 value
            if action == 'fill' and result['success']:
                current_value = result['params'].get('value', '')
                # 从步骤名称中提取正确的 value
                extracted_value = extract_value_from_description(name)
                if extracted_value and extracted_value != current_value:
                    logger.info(f"步骤 {order} 自动修正 value: '{current_value}' -> '{extracted_value}'")
                    result['params']['value'] = extracted_value

            # 截图
            try:
                screenshot_path = os.path.join(
                    self.screenshot_dir,
                    f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
                )
                page.screenshot(path=screenshot_path)
                with open(screenshot_path, 'rb') as f:
                    result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
            except Exception as e:
                logger.debug(f"截图失败（非致命）: {e}")

        except Exception as e:
            result['success'] = False
            result['message'] = f'定位异常: {str(e)}'
            logger.error(f"步骤 {order} 定位异常: {e}")

        return result

    def _execute_step(self, result: Dict[str, Any]) -> bool:
        """
        实际执行步骤（验证选择器是否有效）

        Args:
            result (Dict): 定位结果（包含 selector 和 params）

        Returns:
            bool: 是否执行成功
        """
        action = result.get('action', '')
        params = result.get('params', {})
        selector = params.get('selector', '')

        if not selector:
            return False

        logger.debug(f"执行步骤验证: action={action}, selector={selector}")

        try:
            page = self.executor._page

            if action == 'fill':
                value = params.get('value', '')
                # 等待元素可见
                page.wait_for_selector(selector, timeout=5000)
                # 填充
                page.fill(selector, str(value))
                logger.debug(f"填充成功: {selector} = {value}")

            elif action == 'click':
                # 等待元素可见
                page.wait_for_selector(selector, timeout=5000)
                # 点击
                page.click(selector)
                logger.debug(f"点击成功: {selector}")
                # 等待可能的页面变化
                page.wait_for_timeout(1000)

                # ⚠️ 新增：如果是点击功能中心，等待抽屉打开
                if '功能中心' in result.get('name', ''):
                    logger.debug("检测到功能中心点击，等待抽屉打开...")
                    try:
                        page.wait_for_selector('.el-drawer', timeout=5000)
                        page.wait_for_timeout(500)  # 额外等待动画
                        logger.debug("功能中心抽屉已打开")
                    except Exception as e:
                        logger.warning(f"等待抽屉打开失败: {e}")

            elif action == 'wait':
                timeout = params.get('timeout', 10000)
                page.wait_for_selector(selector, timeout=timeout)
                logger.debug(f"等待成功: {selector}")

            elif action == 'select':
                value = params.get('value', '')
                page.wait_for_selector(selector, timeout=5000)
                page.select_option(selector, str(value))
                logger.debug(f"选择成功: {selector} = {value}")

            return True

        except Exception as e:
            logger.warning(f"执行步骤验证失败: {e}")
            return False

    def _get_login_selector(self, step_name: str) -> Optional[str]:
        """
        获取登录相关步骤和导航步骤的选择器（使用已验证的模板）

        Args:
            step_name (str): 步骤名称

        Returns:
            Optional[str]: 选择器，如果不是已知步骤返回 None
        """
        # 登录相关关键词映射到选择器
        login_selector_map = {
            # 输入账号
            '账号': 'input[placeholder*="手机号"]',
            '用户名': 'input[placeholder*="手机号"]',
            '手机号': 'input[placeholder*="手机号"]',
            # 输入密码
            '密码': 'input[type="password"]',
            # 输入验证码
            '验证码': 'input[placeholder*="图"]',
            # 勾选协议
            '协议': '.el-checkbox',
            '勾选': '.el-checkbox',
            # 点击登录
            '登录': 'button:has-text("登录")',
        }

        # 检查步骤名称是否包含登录关键词
        for keyword, selector in login_selector_map.items():
            if keyword in step_name:
                return selector

        # ⚠️ 新增：功能中心导航相关步骤
        nav_selector_map = {
            '功能中心': '//*[@id="Home"]/div[1]/div[1]',
            '首页': '//*[@id="Home"]/div[1]/div[1]',
        }

        for keyword, selector in nav_selector_map.items():
            if keyword in step_name:
                return selector

        return None


# ==================== 工厂函数 ====================

def get_smart_locate_service() -> SmartLocateService:
    """
    获取智能定位服务实例

    Returns:
        SmartLocateService: 服务实例
    """
    return SmartLocateService()