"""
元素定位路由

提供本地元素定位功能，通过 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 = "https://192.168.5.44"
    auto_login: bool = True


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


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

def extract_interactive_elements(page) -> list:
    """提取页面中所有可交互元素的信息，支持 micro-app 微前端容器"""
    return page.evaluate('''() => {
        const elements = [];
        document.querySelectorAll('input,button,a,select,textarea,label,[role="button"]').forEach(el => {
            elements.push({
                tag: el.tagName, type: el.type || '', placeholder: el.placeholder || '',
                text: (el.innerText || '').substring(0, 50), id: el.id || '', name: el.name || '',
                className: el.className || '', ariaLabel: el.getAttribute('aria-label') || '',
                dataTestid: el.getAttribute('data-testid') || '', href: el.href || '', value: el.value || '',
            });
        });
        document.querySelectorAll('micro-app').forEach(microApp => {
            const shadowBody = microApp.querySelector('micro-app-body');
            if (shadowBody && shadowBody.shadowRoot) {
                shadowBody.shadowRoot.querySelectorAll('input,button,a,select,textarea').forEach(el => {
                    elements.push({
                        tag: el.tagName, type: el.type || '', placeholder: el.placeholder || '',
                        text: (el.innerText || '').substring(0, 50), id: el.id || '', name: el.name || '',
                        inMicroApp: true,
                    });
                });
            }
        });
        document.querySelectorAll('iframe').forEach(iframe => {
            try {
                const iframeDoc = iframe.contentDocument;
                if (iframeDoc) {
                    iframeDoc.querySelectorAll('input,button,a,select,textarea').forEach(el => {
                        elements.push({
                            tag: el.tagName, type: el.type || '', placeholder: el.placeholder || '',
                            text: (el.innerText || '').substring(0, 50), id: el.id || '', name: el.name || '',
                            inIframe: true,
                        });
                    });
                }
            } catch (e) {}
        });
        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 = "") -> list:
    """关键词算法匹配元素，返回候选定位器列表（按置信度降序）"""
    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)

    # 始终调用宿主机上的 ai_locate_worker.py，让 Claude CLI 进行精确匹配
    # 不再依赖置信度判断，确保每次都使用 Claude Code 能力
    logger.info("调用宿主机 ai_locate_worker.py 进行 Claude CLI 精确匹配...")
    claude_result = call_claude_code_via_ssh(step_description, page_url)
    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_code_via_ssh(step_description: str, page_url: str, expected: str = "") -> dict:
    """
    通过 SSH 调用宿主机上的 ai_locate_worker.py
    宿主机直接使用 Playwright 访问页面获取真实元素 + Claude CLI 分析

    利用已有的 ai_locate_worker.py 脚本（宿主机上已有），
    它已经在宿主机上安装了 Playwright + Claude Code CLI。
    """
    HOST = "192.168.5.60"
    HOST_USER = "ubains"
    SSH_PASSWORD = "Ubains@123"
    DEPLOY_DIR = "/data/third_party/plat-auto-test"

    # 在宿主机上执行 ai_locate_worker.py 脚本
    # 这个脚本会：
    # 1. Playwright 打开目标页面，提取真实 DOM 元素
    # 2. 关键词算法 + Claude Code 精确匹配
    # 3. 返回定位结果
    cmd = (
        f'cd {DEPLOY_DIR}/backend && '
        f'python3 scripts/ai_locate_worker.py '
        f'--step-desc "{step_description}" '
        f'--page-url "{page_url}" '
        f'--expected "{expected}"'
    )

    try:
        logger.info(f"通过 SSH 调用宿主机 {HOST} 上的 ai_locate_worker.py...")
        import paramiko

        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=90)
        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])
                logger.info(f"ai_locate_worker.py 返回结果: success={parsed.get('success')}, candidates={len(parsed.get('candidates', []))}")
                # 从结果中提取第一个候选（最优匹配）
                candidates = parsed.get('candidates', [])
                if candidates:
                    best = candidates[0]
                    return {
                        'locator_type': best.get('locator_type', ''),
                        'locator_value': best.get('locator_value', ''),
                        'confidence': best.get('confidence', 0.8),
                        'reason': best.get('reasons', ['Claude Code 分析匹配'])[0] if best.get('reasons') else 'Claude Code 分析匹配'
                    }
            else:
                logger.warning(f"脚本输出中未找到 JSON: {output[:300]}")
        else:
            logger.warning(f"SSH 命令无输出, stderr: {err[:200]}")

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

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


def _do_locate_with_executor(executor, page_url: str, auto_login: bool) -> dict:
    """使用已有的 PlaywrightExecutor 执行定位准备工作
    返回 { success, elements, page_title, current_url, message }
    """
    # 自动登录
    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': '自动登录失败'}

    # 访问目标页面
    logger.info(f"访问页面: {page_url}")
    current_url = executor._page.url
    is_login_page = current_url.rstrip('/') == 'https://192.168.5.44'
    is_target_login = page_url.rstrip('/') == 'https://192.168.5.44' 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=30000)
        except Exception as e:
            logger.warning(f"页面加载超时(networkidle): {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(3000)

    # 提取元素
    elements = extract_interactive_elements(executor._page)
    page_title = executor._page.title()
    current_url = executor._page.url
    logger.info(f"提取到 {len(elements)} 个元素，页面标题: {page_title}")

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


# ==================== 单个步骤定位 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)} 个步骤")

    # 在线程中执行 Playwright
    result_container = {
        'success': False, 'case_id': request.case_id,
        'total_steps': len(steps), 'located_steps': 0,
        'results': [], 'message': ''
    }
    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)
            if not prep['success']:
                result_container['message'] = prep['message']
                return

            elements = prep['elements']
            page_title = prep['page_title']
            located_count = 0

            # 为每个步骤匹配定位器
            for step in updated_steps:
                step_desc = step.get('name', '') or step.get('description', '')
                step_order = step.get('order', 0)

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

                # 跳过 navigate 类型的步骤（不需要定位器）
                action = step.get('action', '')
                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

                # 匹配定位器
                candidates = build_locator_candidates(step_desc, elements, page_title, request.page_url)

                if candidates:
                    best = candidates[0]
                    # 更新步骤的定位信息（使用驼峰命名匹配前端 IntentStepEditor）
                    step['locatorType'] = best['locator_type']
                    step['locatorValue'] = best['locator_value']
                    # 兼容蛇形命名（如果其他地方需要）
                    step['locator_type'] = best['locator_type']
                    step['locator_value'] = best['locator_value']
                    # 同步到 params.selector
                    if best['locator_value'] and 'params' in step:
                        step['params']['selector'] = best['locator_value']
                    elif best['locator_value']:
                        step['params'] = {'selector': best['locator_value']}

                    result_container['results'].append(StepLocateResult(
                        order=step_order, description=step_desc, success=True,
                        locator_type=best['locator_type'], locator_value=best['locator_value'],
                        confidence=best['confidence'], message=f"置信度 {best['confidence']:.0%}"
                    ))
                    located_count += 1
                else:
                    result_container['results'].append(StepLocateResult(
                        order=step_order, description=step_desc, success=False,
                        message="未找到匹配元素"
                    ))

            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=300)  # 5 minutes for batch locate (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)
