#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：claude_service.py
模块描述：Claude CLI 语义增强服务，用于智能定位元素选择

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

import logging
import subprocess
import json
import re
import os
from typing import List, Dict, Any, Tuple, Optional

from app.config import settings

logger = logging.getLogger(__name__)


# ==================== Prompt 模板 ====================

ELEMENT_SELECTION_PROMPT = """选择最匹配步骤的元素。

步骤: {step_name}
动作: {action}

候选元素:
{candidates_json}

选择规则:
1. fill动作选INPUT/TEXTAREA
2. click动作选BUTTON/A/DIV
3. 文本匹配优先

返回JSON: {{"selected_index": 0, "confidence": 0.95, "reason": "理由"}}"""


class ClaudeService:
    """
    Claude CLI 语义增强服务

    核心功能：
    1. 调用 Claude CLI 对候选元素进行语义排序
    2. 返回最匹配的元素索引、置信度和理由
    3. 自动处理超时、异常和格式错误
    """

    def __init__(self):
        """初始化 Claude 服务"""
        self.enabled = settings.CLAUDE_ENABLED
        self.timeout = settings.CLAUDE_TIMEOUT
        self.model = settings.CLAUDE_MODEL
        self.max_candidates = settings.CLAUDE_MAX_CANDIDATES
        logger.info(f"Claude 服务初始化: enabled={self.enabled}, timeout={self.timeout}s, model={self.model}")

    def rank_candidates(
        self,
        step_name: str,
        action: str,
        params: dict,
        candidates: List[Dict[str, Any]]
    ) -> Tuple[int, float, str]:
        """
        对候选元素进行语义排序

        Args:
            step_name: 步骤名称，如 "点击新建会议按钮"
            action: 动作类型，如 click/fill/wait
            params: 步骤参数
            candidates: 候选元素列表，格式：
                [
                    {
                        "index": 0,
                        "tag": "BUTTON",
                        "text": "新建会议",
                        "selector": "button:has-text('新建会议')",
                        "attributes": {...},
                        "position": {"x": 100, "y": 200, "width": 80, "height": 32}
                    },
                    ...
                ]

        Returns:
            Tuple[int, float, str]: (选中索引, 置信度, 选择理由)

        Raises:
            Exception: Claude 调用失败时抛出异常
        """
        if not self.enabled:
            # 未启用时，单候选直接返回，多候选返回第一个
            if len(candidates) == 1:
                return 0, 1.0, "唯一候选元素（Claude未启用）"
            return 0, 0.6, "关键词匹配第一个候选（Claude未启用）"

        if len(candidates) == 0:
            raise Exception("候选元素列表为空")

        if len(candidates) == 1:
            # 单候选直接返回
            return 0, 1.0, "唯一候选元素"

        logger.info(f"Claude 语义排序: step='{step_name}', candidates={len(candidates)}")

        try:
            # 1. 构建 Prompt
            prompt = self._build_prompt(step_name, action, params, candidates)
            logger.debug(f"Claude Prompt 长度: {len(prompt)} 字符")

            # 2. 调用 Claude CLI
            response = self._call_claude_cli(prompt)
            logger.debug(f"Claude 原始响应: {response}")

            # 3. 解析响应
            result = self._parse_response(response)

            selected_index = result.get('selected_index', 0)
            confidence = result.get('confidence', 0.8)
            reason = result.get('reason', '未提供理由')

            logger.info(f"Claude 选择结果: index={selected_index}, confidence={confidence:.2f}, reason={reason}")

            return selected_index, confidence, reason

        except Exception as e:
            logger.error(f"Claude 语义排序失败: {e}")
            raise

    def _build_prompt(
        self,
        step_name: str,
        action: str,
        params: dict,
        candidates: List[Dict[str, Any]]
    ) -> str:
        """
        构建 Claude Prompt

        Args:
            step_name: 步骤名称
            action: 动作类型
            params: 步骤参数
            candidates: 候选元素列表

        Returns:
            str: 完整的 Prompt
        """
        # 限制候选数量
        limited_candidates = candidates[:self.max_candidates]

        # 简化候选信息（只保留关键字段）
        simplified_candidates = []
        for c in limited_candidates:
            simplified_candidates.append({
                'index': c.get('index', 0),
                'tag': c.get('tag', ''),
                'text': c.get('text', '')[:50] if c.get('text') else '',  # 限制文本长度
                'selector': c.get('selector', ''),
                'type': c.get('attributes', {}).get('type', '')
            })

        # 构建候选元素 JSON
        candidates_json = json.dumps(simplified_candidates, ensure_ascii=False, indent=2)

        # 填充模板
        prompt = ELEMENT_SELECTION_PROMPT.format(
            step_name=step_name,
            action=action,
            candidates_json=candidates_json
        )

        return prompt

    def _call_claude_cli(self, prompt: str) -> str:
        """
        调用 Claude CLI

        支持三种调用方式（按优先级）：
        1. 本地 Claude CLI（Windows/Linux 本地环境）
        2. SSH 调用宿主机 Claude CLI（Docker 容器内环境）
        3. 容器内 Claude CLI（如果已安装）

        Args:
            prompt: Prompt 文本

        Returns:
            str: Claude 响应文本

        Raises:
            Exception: 调用失败时抛出异常
        """
        import platform
        import os

        # 设置环境变量确保 UTF-8 编码
        env = os.environ.copy()
        env['PYTHONIOENCODING'] = 'utf-8'
        env['LANG'] = 'en_US.UTF-8'

        is_windows = platform.system() == 'Windows'

        # ========== 方式1: 本地 Claude CLI ==========
        try:
            if is_windows:
                result = subprocess.run(
                    f'claude --print',
                    input=prompt,
                    capture_output=True,
                    text=True,
                    timeout=self.timeout,
                    shell=True,
                    env=env,
                    encoding='utf-8',
                    errors='replace'
                )
            else:
                result = subprocess.run(
                    ['claude', '--print'],
                    input=prompt,
                    capture_output=True,
                    text=True,
                    timeout=self.timeout,
                    env=env,
                    encoding='utf-8',
                    errors='replace'
                )

            if result.returncode == 0 and result.stdout and result.stdout.strip():
                logger.debug(f"Claude CLI 本地调用成功: {len(result.stdout)} 字符")
                return result.stdout.strip()
        except FileNotFoundError:
            logger.debug("本地 Claude CLI 未找到，尝试其他方式")
        except subprocess.TimeoutExpired:
            logger.warning("本地 Claude CLI 超时，尝试其他方式")
        except Exception as e:
            logger.debug(f"本地 Claude CLI 失败: {e}")

        # ========== 方式2: SSH 调用宿主机 Claude CLI（Docker 容器内使用） ==========
        try:
            response = self._call_claude_via_ssh(prompt)
            if response:
                logger.debug(f"Claude CLI SSH 调用成功: {len(response)} 字符")
                return response
        except Exception as e:
            logger.debug(f"SSH 调用 Claude CLI 失败: {e}")

        # ========== 方式3: 容器内 Claude CLI（如果已安装） ==========
        try:
            if not is_windows:
                result = subprocess.run(
                    ['claude', '--print'],
                    input=prompt,
                    capture_output=True,
                    text=True,
                    timeout=self.timeout,
                    env=env,
                    encoding='utf-8',
                    errors='replace'
                )
                if result.returncode == 0 and result.stdout and result.stdout.strip():
                    logger.debug(f"Claude CLI 容器内调用成功: {len(result.stdout)} 字符")
                    return result.stdout.strip()
        except Exception as e:
            logger.debug(f"容器内 Claude CLI 失败: {e}")

        # 所有方式都失败
        raise Exception("Claude CLI 不可用（本地/SSH/容器均无法调用）")

    def _call_claude_via_ssh(self, prompt: str) -> str:
        """
        通过 SSH 调用宿主机的 Claude CLI（适用于 Docker 容器内环境）

        容器内通过宿主机网关 IP 访问宿主机 SSH 服务

        Args:
            prompt: Prompt 文本

        Returns:
            str: Claude 响应文本

        Raises:
            Exception: SSH 调用失败时抛出异常
        """
        import tempfile

        # 获取宿主机网关 IP（Docker 容器内访问宿主机的方式）
        host_ip = os.environ.get('CLAUDE_SSH_HOST', '172.17.0.1')  # Docker 默认网关
        host_port = int(os.environ.get('CLAUDE_SSH_PORT', '22'))
        host_user = os.environ.get('CLAUDE_SSH_USER', 'ubains')
        host_pass = os.environ.get('CLAUDE_SSH_PASS', 'Ubains@123')

        logger.info(f"通过 SSH 调用宿主机 Claude CLI: {host_user}@{host_ip}:{host_port}")

        # 将 prompt 写入临时文件，避免 shell 转义问题
        with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
            f.write(prompt)
            prompt_file = f.name

        try:
            # 使用 SSH 执行 claude --print，通过 stdin 传递 prompt
            ssh_cmd = [
                'sshpass', '-p', host_pass,
                'ssh', '-o', 'StrictHostKeyChecking=no',
                '-o', 'ConnectTimeout=5',
                '-p', str(host_port),
                f'{host_user}@{host_ip}',
                'claude --print'
            ]

            result = subprocess.run(
                ssh_cmd,
                input=prompt,
                capture_output=True,
                text=True,
                timeout=self.timeout,
                encoding='utf-8',
                errors='replace'
            )

            if result.returncode == 0 and result.stdout and result.stdout.strip():
                return result.stdout.strip()

            # sshpass 不可用，尝试 paramiko
            if 'sshpass' in (result.stderr or ''):
                logger.debug("sshpass 不可用，尝试 paramiko")
                return self._call_claude_via_paramiko(prompt, host_ip, host_port, host_user, host_pass)

            error_msg = result.stderr.strip() if result.stderr else "未知错误"
            raise Exception(f"SSH Claude CLI 错误: {error_msg[:200]}")

        except subprocess.TimeoutExpired:
            raise Exception(f"SSH Claude CLI 超时（>{self.timeout}s）")
        except FileNotFoundError:
            # sshpass 不存在，回退到 paramiko
            logger.debug("sshpass 未安装，尝试 paramiko")
            return self._call_claude_via_paramiko(prompt, host_ip, host_port, host_user, host_pass)
        finally:
            # 清理临时文件
            try:
                os.unlink(prompt_file)
            except Exception:
                pass

    def _call_claude_via_paramiko(self, prompt: str, host: str, port: int, user: str, password: str) -> str:
        """
        通过 paramiko SSH 库调用宿主机 Claude CLI

        Args:
            prompt: Prompt 文本
            host: 宿主机 IP
            port: SSH 端口
            user: SSH 用户名
            password: SSH 密码

        Returns:
            str: Claude 响应文本
        """
        try:
            import paramiko
        except ImportError:
            raise Exception("paramiko 未安装，无法通过 SSH 调用 Claude CLI")

        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            ssh.connect(host, port=port, username=user, password=password, timeout=10)

            # 通过 stdin 传递 prompt
            stdin, stdout, stderr = ssh.exec_command(
                'claude --print',
                timeout=self.timeout
            )
            stdin.write(prompt)
            stdin.flush()
            stdin.channel.shutdown_write()

            out = stdout.read().decode('utf-8', errors='replace').strip()
            err = stderr.read().decode('utf-8', errors='replace').strip()

            if out:
                return out

            raise Exception(f"Paramiko Claude CLI 无输出: {err[:200]}")

        except Exception as e:
            raise Exception(f"Paramiko SSH 调用失败: {e}")
        finally:
            ssh.close()

    def _parse_response(self, response: str) -> dict:
        """
        解析 Claude 响应

        Args:
            response: Claude 原始响应文本

        Returns:
            dict: 解析后的结果 {"selected_index": 0, "confidence": 0.95, "reason": "..."}

        Raises:
            Exception: 解析失败时抛出异常
        """
        try:
            # 清理响应文本
            cleaned = response.strip()

            # 移除可能的 markdown 代码块标记
            if cleaned.startswith("```"):
                # 移除开头的 ```json 或 ```
                lines = cleaned.split("\n")
                if lines[0].startswith("```"):
                    lines = lines[1:]
                # 移除结尾的 ```
                if lines and lines[-1].strip() == "```":
                    lines = lines[:-1]
                cleaned = "\n".join(lines).strip()

            # 尝试提取 JSON
            # 方法1: 直接解析
            try:
                result = json.loads(cleaned)
                if isinstance(result, dict) and 'selected_index' in result:
                    return result
            except json.JSONDecodeError:
                pass

            # 方法2: 使用正则提取 JSON 对象
            json_match = re.search(r'\{[^{}]*"selected_index"[^{}]*\}', cleaned, re.DOTALL)
            if json_match:
                try:
                    result = json.loads(json_match.group())
                    if isinstance(result, dict) and 'selected_index' in result:
                        return result
                except json.JSONDecodeError:
                    pass

            # 方法3: 尝试提取 selected_index 数字
            index_match = re.search(r'"selected_index"\s*:\s*(\d+)', cleaned)
            if index_match:
                selected_index = int(index_match.group(1))
                confidence_match = re.search(r'"confidence"\s*:\s*([\d.]+)', cleaned)
                confidence = float(confidence_match.group(1)) if confidence_match else 0.8
                reason_match = re.search(r'"reason"\s*:\s*"([^"]*)"', cleaned)
                reason = reason_match.group(1) if reason_match else "解析得到"

                return {
                    'selected_index': selected_index,
                    'confidence': confidence,
                    'reason': reason
                }

            raise Exception(f"无法解析 Claude 响应: {cleaned[:200]}")

        except Exception as e:
            logger.error(f"Claude 响应解析失败: {e}")
            raise Exception(f"Claude 响应解析失败: {e}")


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

def get_claude_service() -> ClaudeService:
    """
    获取 Claude 服务实例

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


# ==================== 测试代码 ====================

if __name__ == "__main__":
    """测试 Claude 服务"""
    import sys

    # 设置日志
    logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    # 测试候选元素
    test_candidates = [
        {
            "index": 0,
            "tag": "DIV",
            "text": "会议预约",
            "selector": "div:has-text('会议预约')",
            "attributes": {"class": "menu-item"},
            "position": {"x": 50, "y": 150, "width": 200, "height": 40}
        },
        {
            "index": 1,
            "tag": "BUTTON",
            "text": "新建会议",
            "selector": "button:has-text('新建会议')",
            "attributes": {"class": "el-button el-button--primary", "type": "button"},
            "position": {"x": 100, "y": 200, "width": 80, "height": 32}
        },
        {
            "index": 2,
            "tag": "INPUT",
            "text": "",
            "selector": "input[placeholder='请输入会议名称']",
            "attributes": {"type": "text", "placeholder": "请输入会议名称"},
            "position": {"x": 200, "y": 300, "width": 200, "height": 32}
        }
    ]

    # 测试场景1: 点击按钮
    print("\n=== 测试场景1: 点击新建会议按钮 ===")
    service = ClaudeService()
    try:
        idx, conf, reason = service.rank_candidates(
            step_name="点击新建会议按钮",
            action="click",
            params={},
            candidates=test_candidates
        )
        print(f"结果: index={idx}, confidence={conf:.2f}, reason={reason}")
        print(f"选择器: {test_candidates[idx]['selector']}")
    except Exception as e:
        print(f"测试失败: {e}")

    # 测试场景2: 输入会议名称
    print("\n=== 测试场景2: 会议名称输入：自动化测试 ===")
    try:
        idx, conf, reason = service.rank_candidates(
            step_name="会议名称输入：自动化测试",
            action="fill",
            params={"value": "自动化测试"},
            candidates=test_candidates
        )
        print(f"结果: index={idx}, confidence={conf:.2f}, reason={reason}")
        print(f"选择器: {test_candidates[idx]['selector']}")
    except Exception as e:
        print(f"测试失败: {e}")

    # 测试场景3: 单候选
    print("\n=== 测试场景3: 单候选元素 ===")
    try:
        idx, conf, reason = service.rank_candidates(
            step_name="点击唯一按钮",
            action="click",
            params={},
            candidates=[test_candidates[1]]
        )
        print(f"结果: index={idx}, confidence={conf:.2f}, reason={reason}")
    except Exception as e:
        print(f"测试失败: {e}")
