# -*- coding: utf-8 -*-
"""
ai_service.py — Claude API 调用与 Prompt 构建（业务层）

从原 server.py 迁入：build_prompt / call_claude_api / call_claude_api_stream
/ generate_mock_response / split_mock_response + SYSTEM_TYPE_DOMAINS
/ APK_PRODUCT_HINTS 常量。

依赖注入：config 与 get_skill_md 从 container 取，logger 自建子 logger。
行为保持不变（含降级 mock 响应逻辑）。
"""

import json

import container
from utils.logger import get_logger

logger = get_logger(__name__)


# ============================================================
# 排查域映射表（P1 Prompt 增强优化）
# ============================================================

# 系统类型 → 重点排查域映射
SYSTEM_TYPE_DOMAINS = {
    "标准版预定2.0": "EMQX、Nacos、门口屏、无纸化、数据库、Redis",
    "标准版运维集控系统": "协议透传、中间件、设备管理、MQTT",
    "新统一平台": "统一门户、SMC、腾讯会议、单点登录",
    "统一平台": "用户中心、权限管理、数据同步",
}

# APK 产品 → 特性关注映射
APK_PRODUCT_HINTS = {
    "门口屏5.0": "设备主动上报心跳，内置默认 MQTT 凭据，只同步参会人人脸",
    "门口屏4.0": "Web 端主动刷新，并发量过大时可能断连，建议升级到 5.0",
    "无纸化": "座位编排需与会议室绑定一致，平板缓存需定期清理",
    "桌牌": "绑定授权码后才能使用，检查 JWT 校验是否正常",
}


# ============================================================
# Prompt 构建
# ============================================================

def build_prompt(query, project_name, system_type, apk_product, matched_cases):
    """构建发送给 Claude 的完整 Prompt"""

    # 基础系统提示
    system_prompt = """你是一个自动化测试团队的问题排查专家助手。你面对的是现场实施同事，他们可能不熟悉 Linux 命令和系统架构。

## 核心规则（必须遵守）

1. **只读操作原则**：给出的排查步骤必须是只读操作（查看状态、查看日志、探测连通性、查看配置）
2. **安全原则**：绝对不允许给出修改、删除、重启、停止、写入等操作
3. **相关性原则**：每步操作必须直接关联用户描述的问题，不允许泛化
4. **排序原则**：步骤按最可能→最不可能排序
5. **概率原则**：给出各可能根因的概率估计
6. **语言原则**：用简单易懂的语言，说明每步"查什么"、"查到什么说明是什么问题"
7. **危险操作原则**：如果需要执行危险操作来修复，用"请联系研发组执行：[具体操作]"代替

## 禁止的命令（违反则整条回复作废）

- 删除类：rm、docker rm、DROP、DELETE、TRUNCATE
- 重启类：reboot、systemctl restart、docker restart、shutdown
- 停止类：systemctl stop、docker stop、kill、pkill
- 修改类：chmod 777、chown、写入配置文件、iptables 修改
- 清空类：FLUSHALL、FLUSHDB

## 允许的命令（可放心输出）

- 查看状态：docker ps、systemctl status、ss -tln
- 查看日志：docker logs、tail、journalctl
- 连通性测试：nc -z、curl、ping
- 系统信息：df -h、free -h、date、ps aux
- 配置查看：cat、grep、ls
"""

    # 构建用户查询
    user_query_parts = []
    if project_name:
        user_query_parts.append(f"项目名称：{project_name}")
    if system_type:
        user_query_parts.append(f"系统类型：{system_type}")
        # 系统类型排查域提示
        domain_hint = SYSTEM_TYPE_DOMAINS.get(system_type, "")
        if domain_hint:
            user_query_parts.append(f"重点排查域：{domain_hint}")
    if apk_product:
        user_query_parts.append(f"APK产品：{apk_product}")
        # APK 产品特性提示
        apk_hint = APK_PRODUCT_HINTS.get(apk_product, "")
        if apk_hint:
            user_query_parts.append(f"{apk_product} 特性：{apk_hint}")
    user_query_parts.append(f"问题描述：{query}")

    user_query = "\n".join(user_query_parts)

    # 构建匹配案例
    cases_text = ""
    if matched_cases:
        cases_text = "## 历史相似案例\n\n"
        for c in matched_cases[:5]:
            rec = c.get('record', {})
            cases_text += f"### 案例 {c['rank']}（相似度：{c['score']:.0%}）\n"
            if rec.get('project'):
                cases_text += f"项目：{rec['project']}\n"
            cases_text += f"现象：{rec.get('phenomenon', rec.get('title', '未知'))}\n\n"
        cases_text += "---\n\n"

    # 排查套路参考（截取前 5000 字符，避免过长）
    skill_text = container.get_skill_md()
    if len(skill_text) > 5000:
        skill_text = skill_text[:5000] + "\n\n... （完整排查套路见 SKILL.md）"

    # 组装完整 Prompt
    full_prompt = f"""{system_prompt}

---

{cases_text}

## 排查套路参考

{skill_text}

---

## 用户查询

{user_query}

---

请根据以上信息，生成排查步骤和根因分析。输出格式：

### 📋 匹配到的历史案例
（简要列出匹配的案例，如：厦门银行-门口屏MQTT绑定失败）

### 🔬 排查步骤（只读操作）
Step 1: ...
Step 2: ...
...

### 🎯 最可能的根因（按概率排序）
- XX%：根因描述
- XX%：根因描述
...

### ⚠️ 注意事项
（提醒现场同事注意的事项）
"""

    return full_prompt

# ============================================================
# Claude API 调用
# ============================================================

def call_claude_api(prompt):
    """调用 Claude API — 支持 HTTP API 或 CLI 两种方式"""
    import requests

    # 从配置获取 API 信息
    config = container.get_config()
    api_base = config.get('claude_api_base', '')
    api_key = config.get('claude_api_key', '')
    model = config.get('claude_model', 'glm-5.1')

    # 方式1: HTTP API（优先）
    if api_base and api_key:
        try:
            headers = {
                'Content-Type': 'application/json',
                'Authorization': f'Bearer {api_key}',
            }
            data = {
                'model': model,
                'messages': [{'role': 'user', 'content': prompt}],
                'max_tokens': 4096,
            }
            resp = requests.post(
                f'{api_base}/v1/chat/completions',
                headers=headers,
                json=data,
                timeout=60,
            )
            if resp.status_code == 200:
                result = resp.json()
                return result['choices'][0]['message']['content']
            else:
                logger.error(f"API 错误: {resp.status_code} - {resp.text[:200]}")
        except requests.exceptions.RequestException as e:
            logger.error(f"HTTP API 调用失败（网络）: {e}")
        except (KeyError, ValueError) as e:
            logger.error(f"HTTP API 响应解析失败: {e}")
        except Exception as e:
            logger.exception(f"HTTP API 调用未知异常: {e}")

    # 方式2: CLI（降级）
    import subprocess
    import tempfile
    try:
        with tempfile.NamedTemporaryFile(
            mode='w', suffix='.txt', delete=False, encoding='utf-8'
        ) as f:
            f.write(prompt)
            temp_path = f.name

        result = subprocess.run(
            ['claude', '--print', '-p', f'@{temp_path}'],
            capture_output=True,
            text=True,
            timeout=120,
            encoding='utf-8',
        )

        import os as _os
        _os.unlink(temp_path)

        if result.returncode == 0:
            return result.stdout.strip()
        else:
            logger.error(f"Claude CLI 错误：{result.stderr}")
            return generate_mock_response(prompt)

    except FileNotFoundError:
        logger.warning("未找到 claude CLI，降级为模拟响应")
        return generate_mock_response(prompt)
    except subprocess.TimeoutExpired as e:
        logger.error(f"Claude CLI 调用超时: {e}")
        return generate_mock_response(prompt)
    except Exception as e:
        logger.exception(f"Claude 调用失败: {e}")
        return generate_mock_response(prompt)


def generate_mock_response(prompt):
    """生成模拟响应（用于开发测试）"""
    return """### 📋 匹配到的历史案例

1. 厦门银行-门口屏MQTT绑定失败（相似度：92%）
2. 展厅-门口屏不显示会议信息（相似度：75%）

### 🔬 排查步骤（只读操作）

Step 1: 检查门口屏版本
  命令：在门口屏设备上查看版本号
  说明：确认是否为最新版本（5.0+），旧版本可能缺少默认 MQTT 凭据

Step 2: 查看设备日志
  命令：查看门口屏日志文件
  说明：查找 MQTT 连接相关的错误信息

Step 3: 检查 EMQX 容器状态
  命令：docker ps -a | grep uemqx
  说明：确认 EMQX 容器是否正常运行

Step 4: 检查 MQTT 端口连通性
  命令：nc -z <服务器IP> 1883
  说明：测试 MQTT 端口是否可达

### 🎯 最可能的根因（按概率排序）

- 70%：旧版门口屏没有默认 MQTT 账号密码
- 20%：EMQX 容器未启动或异常
- 10%：授权码变化导致无效 token

### ⚠️ 注意事项

- 排查时只读操作，不要随意重启服务
- 更新门口屏包后务必重启设备 APP
- 如需执行 docker restart 等危险操作，请联系研发组
"""


def call_claude_api_stream(prompt, model=None):
    """
    流式调用 Claude API。

    参数：
      prompt: 提示文本
      model: 模型 ID（可选，默认使用配置中的模型）

    返回：生成器，每次 yield 一个文本片段
    """
    import requests

    config = container.get_config()
    api_base = config.get('claude_api_base', '')
    api_key = config.get('claude_api_key', '')
    if model is None:
        model = config.get('default_model', 'glm-5.1')

    # 方式1: HTTP API 流式（优先）
    if api_base and api_key:
        try:
            headers = {
                'Content-Type': 'application/json',
                'Authorization': f'Bearer {api_key}',
            }
            data = {
                'model': model,
                'messages': [{'role': 'user', 'content': prompt}],
                'max_tokens': 4096,
                'stream': True,  # 启用流式返回
            }

            resp = requests.post(
                f'{api_base}/v1/chat/completions',
                headers=headers,
                json=data,
                stream=True,  # requests 流式响应
                timeout=120,
            )

            if resp.status_code == 200:
                for line in resp.iter_lines():
                    if line:
                        line = line.decode('utf-8')
                        if line.startswith('data: '):
                            data_str = line[6:]  # 去掉 "data: " 前缀
                            if data_str == '[DONE]':
                                break
                            try:
                                chunk = json.loads(data_str)
                                delta = chunk.get('choices', [{}])[0].get('delta', {})
                                content = delta.get('content', '')
                                if content:
                                    yield content
                            except json.JSONDecodeError as e:
                                # 单个 chunk 解析失败不中断整个流，仅记录 debug
                                logger.debug(f"流式响应 chunk 解析失败（已跳过）: {e}")
            else:
                logger.error(f"API 错误: {resp.status_code}")
                # 降级：返回模拟响应
                for chunk in split_mock_response(generate_mock_response(prompt)):
                    yield chunk

        except requests.exceptions.RequestException as e:
            logger.error(f"流式 API 调用失败（网络）: {e}")
            # 降级：返回模拟响应
            for chunk in split_mock_response(generate_mock_response(prompt)):
                yield chunk
        except Exception as e:
            logger.exception(f"流式 API 调用失败: {e}")
            # 降级：返回模拟响应
            for chunk in split_mock_response(generate_mock_response(prompt)):
                yield chunk
    else:
        # 降级：返回模拟响应
        for chunk in split_mock_response(generate_mock_response(prompt)):
            yield chunk


# ============================================================
# 离线模式响应构建（PRD §3.1）
# ============================================================

def build_offline_response(matched_cases):
    """构建离线模式响应文本（跳过 Claude API，直接格式化匹配结果）

    接受两种输入格式：
    1. 原始格式：engine.search() 返回的列表，每项含 record dict（有 solution）
    2. 已格式化：前端 /api/analyze 传回的列表，每项含直接字段（无 solution）

    Args:
        matched_cases: 匹配结果列表，每项至少含 score/title/phenomenon

    Returns:
        dict: {"response": str（Markdown 文本）, "offline": True}
    """
    if not matched_cases:
        return {
            "response": (
                "## 📋 离线排查参考（基于知识库匹配）\n\n"
                "> 💡 当前为离线模式，结果基于历史知识库匹配，无 AI 分析。\n\n"
                "未匹配到相关案例。"
            ),
            "offline": True,
        }

    # 提取案例字段（兼容两种输入格式）
    def _get(case, key):
        """从原始格式或格式化格式提取字段"""
        if 'record' in case and isinstance(case['record'], dict):
            # 原始格式：case.record.key
            return case['record'].get(key, '') or case.get(key, '')
        # 已格式化格式：case.key
        return case.get(key, '')

    parts = [
        "## 📋 离线排查参考（基于知识库匹配）\n",
        "> 💡 当前为离线模式，结果基于历史知识库匹配，无 AI 分析。\n",
    ]

    for i, c in enumerate(matched_cases[:5], 1):
        score = c.get('score', 0)
        title = _get(c, 'title') or '未记录'
        project = _get(c, 'project') or '未记录'
        phenomenon = _get(c, 'phenomenon') or title
        solution = _get(c, 'solution') or ''
        filename = _get(c, 'file') or _get(c, 'filename') or '未记录'

        parts.extend([
            f"---\n",
            f"### 案例{i}（相似度：{score:.0%}）\n",
            f"- **项目**：{project}\n",
            f"- **标题**：{title}\n",
            f"- **现象**：{phenomenon}\n",
        ])
        if solution:
            parts.append(f"- **解决方案**：{solution}\n")
        parts.append(f"- **参考文件**：{filename}\n")

    parts.append(f"\n> 共匹配 {len(matched_cases)} 个相关案例。")

    return {
        "response": "".join(parts),
        "offline": True,
    }


def split_mock_response(text, chunk_size=20):
    """将模拟响应分割成小块，模拟流式返回"""
    for i in range(0, len(text), chunk_size):
        yield text[i:i + chunk_size]
