# -*- coding: utf-8 -*-
"""
server.py — 问题排查助手 Web 服务

功能：
  提供 Web API 接口：
    - POST /api/troubleshoot — 接收问题描述，返回排查指引
    - POST /api/submit        — 接收问题记录提交，写入问题库

启动：
  python server.py

访问：
  http://localhost:8088
"""

import os
import re
import sys
import json
import time
import uuid
import traceback
from pathlib import Path
from datetime import datetime

from flask import Flask, request, jsonify, render_template, send_from_directory

# ============================================================
# 配置
# ============================================================

SCRIPT_DIR = Path(__file__).resolve().parent  # .../web
DATA_DIR = SCRIPT_DIR.parent  # 部署时即 /opt/troubleshoot（web 上级目录）

# 项目根目录（优先使用环境变量，否则自动检测）
PROJECT_ROOT = os.environ.get('TROUBLESHOOT_ROOT')
if PROJECT_ROOT:
    PROJECT_ROOT = Path(PROJECT_ROOT)
elif (DATA_DIR / "Docs" / "PRD" / "问题知识库").exists():
    # 开发环境：项目仓库中
    PROJECT_ROOT = DATA_DIR  # code/ 的上级即项目根
    # 但 DATA_DIR 指向 code/，需要再往上找。
    # 实际项目根：从 code/web/ 往上 4 级 → 仓库根
    for _ in range(4):
        if (PROJECT_ROOT / "CLAUDE.md").exists():
            break
        PROJECT_ROOT = PROJECT_ROOT.parent
else:
    # 部署环境：PROJECT_ROOT 即 DATA_DIR（/opt/troubleshoot）
    PROJECT_ROOT = DATA_DIR

# 问题记录输出目录
RECORDS_DIR = PROJECT_ROOT / "Docs" / "PRD" / "问题知识库" / "问题记录"
if not RECORDS_DIR.exists():
    # 部署环境降级：使用 DATA_DIR 下的 问题记录/
    RECORDS_DIR = DATA_DIR / "问题记录"

# 系统 API 配置（从环境变量或配置文件读取）
CONFIG_FILE = SCRIPT_DIR / "config.json"

# ============================================================
# 初始化
# ============================================================

# 强制 UTF-8 编码
if hasattr(sys.stdout, 'reconfigure'):
    sys.stdout.reconfigure(encoding='utf-8')

app = Flask(
    __name__,
    template_folder=str(SCRIPT_DIR / "templates"),
    static_folder=str(SCRIPT_DIR / "static"),
)

# 加载配置
def load_config():
    """加载配置文件"""
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    return {
        "claude_api_base": os.environ.get("CLAUDE_API_BASE", ""),
        "claude_api_key": os.environ.get("CLAUDE_API_KEY", ""),
        "claude_model": "claude-sonnet-4-6",
        "system_types": [
            {"value": "std20", "label": "标准版预定2.0"},
            {"value": "ops", "label": "标准版运维集控系统"},
            {"value": "new_unified", "label": "新统一平台"},
            {"value": "unified", "label": "统一平台"},
        ],
    }

config = load_config()

# 延迟导入搜索引擎和过滤器
search_engine = None
safety_filter = None
cache_manager = None

def get_search_engine():
    """获取搜索引擎实例"""
    global search_engine
    if search_engine is None:
        from search_engine import SearchEngine
        search_engine = SearchEngine()
    return search_engine

def get_safety_filter():
    """获取安全过滤器模块"""
    global safety_filter
    if safety_filter is None:
        import safety_filter as sf
        safety_filter = sf
    return safety_filter

def get_cache_manager():
    """获取缓存管理器实例"""
    global cache_manager
    if cache_manager is None:
        from cache_manager import CacheManager
        cache_dir = SCRIPT_DIR / "cache"
        cache_manager = CacheManager(cache_dir, expire_hours=24)
    return cache_manager

# 延迟加载 SKILL.md
SKILL_MD = None
SKILL_MD_LOADED = False

def get_skill_md():
    """加载 Troubleshoot SKILL.md — 按优先级查找（部署环境 → 开发环境）"""
    global SKILL_MD, SKILL_MD_LOADED
    if SKILL_MD_LOADED:
        return SKILL_MD
    SKILL_MD_LOADED = True

    # 路径优先级（部署环境 → 开发环境 → 备用）
    skill_paths = [
        Path("/opt/troubleshoot/SKILL.md"),           # 部署环境（优先）
        SCRIPT_DIR.parent / "SKILL.md",               # 开发环境：code/SKILL.md
        SCRIPT_DIR.parent.parent / "SKILL.md",        # 备用：skills/Troubleshoot/SKILL.md
    ]

    for skill_file in skill_paths:
        if skill_file.exists():
            SKILL_MD = skill_file.read_text(encoding='utf-8')
            # 只保留排查套路部分（不包含 frontmatter）
            if '---' in SKILL_MD:
                parts = SKILL_MD.split('---', 2)
                if len(parts) >= 3:
                    SKILL_MD = parts[2].strip()
            print(f"[SKILL.md] 已加载：{skill_file}")
            return SKILL_MD

    SKILL_MD = ""
    print("[SKILL.md] 警告：未找到 SKILL.md 文件")
    return SKILL_MD

# ============================================================
# 排查域映射表（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 = 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 信息
    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:
                print(f"API 错误: {resp.status_code} - {resp.text[:200]}")
        except Exception as e:
            print(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:
            print(f"Claude CLI 错误：{result.stderr}")
            return generate_mock_response(prompt)

    except FileNotFoundError:
        print("未找到 claude CLI，降级为模拟响应")
        return generate_mock_response(prompt)
    except Exception as e:
        print(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

    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:
                                pass
            else:
                print(f"API 错误: {resp.status_code}")
                # 降级：返回模拟响应
                for chunk in split_mock_response(generate_mock_response(prompt)):
                    yield chunk

        except Exception as e:
            print(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


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

# ============================================================
# API 路由
# ============================================================

@app.route('/')
def index():
    """首页"""
    return render_template('index.html',
        projects=get_search_engine().get_projects(),
        system_types=config.get('system_types', []),
    )


@app.route('/api/troubleshoot', methods=['POST'])
def troubleshoot():
    """排查问题 API（原有完整流程，保持兼容）"""
    data = request.get_json() or {}

    project_name = data.get('project_name', '').strip()
    system_type = data.get('system_type', '').strip()
    apk_product = data.get('apk_product', '').strip()
    query = data.get('query', '').strip()

    if not query:
        return jsonify({
            'success': False,
            'error': '请输入问题描述',
        }), 400

    try:
        # 1. 搜索匹配案例
        engine = get_search_engine()
        matched_cases_data = engine.search(
            query,
            top_k=5,
            project_filter=project_name if project_name else None,
        )

        # 2. 构建 Prompt
        prompt = build_prompt(query, project_name, system_type, apk_product, matched_cases_data)

        # 3. 调用 Claude API
        start_time = time.time()
        raw_response = call_claude_api(prompt)
        api_time = time.time() - start_time

        # 4. 安全过滤
        sf = get_safety_filter()
        filter_result = sf.filter_output(raw_response, query)

        # 5. 审计日志
        log_audit({
            'timestamp': datetime.now().isoformat(),
            'project': project_name,
            'system_type': system_type,
            'apk_product': apk_product,
            'query': query,
            'matched_count': len(matched_cases_data),
            'api_time': round(api_time, 2),
            'removed_lines': len(filter_result.get('removed_lines', [])),
        })

        # 6. 返回结果
        return jsonify({
            'success': True,
            'response': filter_result['filtered_text'],
            'matched_cases': format_matched_cases(matched_cases_data),
            'filter_warnings': filter_result.get('removed_lines', []),
            'api_time': round(api_time, 2),
        })

    except Exception as e:
        traceback.print_exc()
        return jsonify({
            'success': False,
            'error': f'服务异常：{str(e)}',
        }), 500


@app.route('/api/search', methods=['POST'])
def search_cases():
    """渐进式排查 — 阶段一：仅搜索匹配案例（快速返回，<3s）"""
    data = request.get_json() or {}

    project_name = data.get('project_name', '').strip()
    query = data.get('query', '').strip()

    if not query:
        return jsonify({
            'success': False,
            'error': '请输入问题描述',
        }), 400

    try:
        start_time = time.time()
        engine = get_search_engine()
        matched_cases_data = engine.search(
            query,
            top_k=5,
            project_filter=project_name if project_name else None,
        )
        search_time = time.time() - start_time

        return jsonify({
            'success': True,
            'matched_cases': format_matched_cases(matched_cases_data),
            'matched_count': len(matched_cases_data),
            'search_time': round(search_time, 2),
        })

    except Exception as e:
        traceback.print_exc()
        return jsonify({
            'success': False,
            'error': f'搜索异常：{str(e)}',
        }), 500


@app.route('/api/analyze', methods=['POST'])
def analyze():
    """渐进式排查 — 阶段二：AI 深度分析（慢速返回，20-30s）"""
    data = request.get_json() or {}

    project_name = data.get('project_name', '').strip()
    system_type = data.get('system_type', '').strip()
    apk_product = data.get('apk_product', '').strip()
    query = data.get('query', '').strip()
    matched_cases = data.get('matched_cases', [])

    if not query:
        return jsonify({
            'success': False,
            'error': '请输入问题描述',
        }), 400

    try:
        # 1. 构建 Prompt（使用前端传来的匹配案例数据）
        prompt = build_prompt(query, project_name, system_type, apk_product, matched_cases)

        # 2. 调用 Claude API
        start_time = time.time()
        raw_response = call_claude_api(prompt)
        api_time = time.time() - start_time

        # 3. 安全过滤
        sf = get_safety_filter()
        filter_result = sf.filter_output(raw_response, query)

        # 4. 审计日志
        log_audit({
            'timestamp': datetime.now().isoformat(),
            'project': project_name,
            'system_type': system_type,
            'apk_product': apk_product,
            'query': query,
            'matched_count': len(matched_cases),
            'api_time': round(api_time, 2),
            'removed_lines': len(filter_result.get('removed_lines', [])),
        })

        return jsonify({
            'success': True,
            'response': filter_result['filtered_text'],
            'filter_warnings': filter_result.get('removed_lines', []),
            'api_time': round(api_time, 2),
        })

    except Exception as e:
        traceback.print_exc()
        return jsonify({
            'success': False,
            'error': f'分析异常：{str(e)}',
        }), 500


@app.route('/api/analyze/stream', methods=['GET'])
def analyze_stream():
    """
    流式返回 AI 分析结果（SSE）。

    参数通过 URL query string 传递：
      - project_name: 项目名称
      - system_type: 系统类型
      - apk_product: APK 产品
      - query: 问题描述
      - model: 模型选择（可选）

    返回格式（SSE）：
      data: {"type": "start", "message": "正在分析..."}
      data: {"type": "matched_cases", "cases": [...]}
      data: {"type": "chunk", "content": "..."}
      data: {"type": "done", "api_time": 45.2}
    """
    from flask import Response

    project_name = request.args.get('project_name', '').strip()
    system_type = request.args.get('system_type', '').strip()
    apk_product = request.args.get('apk_product', '').strip()
    query = request.args.get('query', '').strip()
    model = request.args.get('model', '') or config.get('default_model', 'glm-5.1')
    query = request.args.get('query', '').strip()

    if not query:
        def error_gen():
            yield f'data: {json.dumps({"type": "error", "message": "请输入问题描述"}, ensure_ascii=False)}\n\n'
        return Response(error_gen(), mimetype='text/event-stream')

    # 检查缓存
    cache = get_cache_manager()
    cached_result = cache.get(project_name, system_type, apk_product, query)

    if cached_result:
        # 返回缓存结果
        def generate_cached():
            yield f'data: {json.dumps({"type": "start", "message": "正在分析...", "cached": True}, ensure_ascii=False)}\n\n'
            yield f'data: {json.dumps({"type": "matched_cases", "cases": cached_result.get("matched_cases", [])}, ensure_ascii=False)}\n\n'

            # 流式推送缓存内容
            cached_response = cached_result.get("response", "")
            chunk_size = 30
            for i in range(0, len(cached_response), chunk_size):
                content_chunk = cached_response[i:i + chunk_size]
                yield f'data: {json.dumps({"type": "chunk", "content": content_chunk}, ensure_ascii=False)}\n\n'
                time.sleep(0.01)

            yield f'data: {json.dumps({"type": "done", "api_time": 0.5, "cached": True}, ensure_ascii=False)}\n\n'

        return Response(generate_cached(), mimetype='text/event-stream',
                       headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})

    def generate():
        """生成器：流式返回内容"""
        start_time = time.time()

        try:
            # 1. 发送开始标记
            yield f'data: {json.dumps({"type": "start", "message": "正在分析..."}, ensure_ascii=False)}\n\n'

            # 2. 搜索匹配案例
            engine = get_search_engine()
            matched_cases_data = engine.search(query, top_k=5, project_filter=project_name if project_name else None)
            matched_cases = format_matched_cases(matched_cases_data)

            # 3. 发送匹配案例
            yield f'data: {json.dumps({"type": "matched_cases", "cases": matched_cases}, ensure_ascii=False)}\n\n'

            # 4. 构建 Prompt
            prompt = build_prompt(query, project_name, system_type, apk_product, matched_cases_data)

            # 5. 收集完整响应（用于安全过滤）
            full_response = ""
            for chunk in call_claude_api_stream(prompt, model=model):
                full_response += chunk

            # 6. 安全过滤（包含敏感信息过滤）
            sf = get_safety_filter()
            filter_result = sf.filter_output_with_sensitive(full_response, query)
            filtered_text = filter_result['filtered_text']

            # 7. 存入缓存
            cache.set(project_name, system_type, apk_product, query, filtered_text, matched_cases)

            # 8. 流式推送过滤后的内容（分块发送，模拟打字效果）
            chunk_size = 30  # 每次发送的字符数
            for i in range(0, len(filtered_text), chunk_size):
                content_chunk = filtered_text[i:i + chunk_size]
                yield f'data: {json.dumps({"type": "chunk", "content": content_chunk}, ensure_ascii=False)}\n\n'
                time.sleep(0.01)  # 小延迟，模拟打字效果

            # 9. 发送完成标记
            api_time = time.time() - start_time
            yield f'data: {json.dumps({"type": "done", "api_time": round(api_time, 2)}, ensure_ascii=False)}\n\n'

            # 9. 审计日志
            log_audit({
                'timestamp': datetime.now().isoformat(),
                'project': project_name,
                'system_type': system_type,
                'apk_product': apk_product,
                'query': query,
                'matched_count': len(matched_cases),
                'api_time': round(api_time, 2),
                'removed_lines': len(filter_result.get('removed_lines', [])),
                'mode': 'stream',
            })

        except Exception as e:
            traceback.print_exc()
            yield f'data: {json.dumps({"type": "error", "message": f"分析异常：{str(e)}"}, ensure_ascii=False)}\n\n'

    return Response(
        generate(),
        mimetype='text/event-stream',
        headers={
            'Cache-Control': 'no-cache',
            'X-Accel-Buffering': 'no',  # 禁用 Nginx 缓冲
            'Connection': 'keep-alive',
        }
    )


def format_matched_cases(matched_cases_data):
    """格式化匹配案例数据为标准返回格式"""
    if not matched_cases_data:
        return []
    return [
        {
            'rank': c['rank'],
            'score': c['score'],
            'project': c.get('record', {}).get('project', ''),
            'title': c.get('record', {}).get('title', ''),
            'file': c.get('record', {}).get('file', ''),
            'phenomenon': c.get('record', {}).get('phenomenon', ''),
        }
        for c in matched_cases_data
    ]


@app.route('/api/health', methods=['GET'])
def health_check():
    """
    健康检查接口。

    返回服务状态、版本信息、知识库统计等。
    用于监控系统、负载均衡器健康检查。
    """
    try:
        engine = get_search_engine()
        cache = get_cache_manager()
        cache_stats = cache.get_stats()

        return jsonify({
            'status': 'ok',
            'timestamp': datetime.now().isoformat(),
            'version': '1.2.0',
            'knowledge_base': {
                'total_records': len(engine.records),
                'last_update': engine.index.get('last_update', 'unknown'),
            },
            'cache': {
                'total_files': cache_stats.get('total_files', 0),
                'total_size_mb': cache_stats.get('total_size_mb', 0),
            },
            'components': {
                'search_engine': 'ok',
                'safety_filter': 'ok',
                'cache_manager': 'ok',
            }
        })
    except Exception as e:
        return jsonify({
            'status': 'error',
            'timestamp': datetime.now().isoformat(),
            'error': str(e),
        }), 500


@app.route('/api/projects', methods=['GET'])
def get_projects():
    """获取项目列表（用于下拉建议）"""
    engine = get_search_engine()
    return jsonify({
        'success': True,
        'projects': engine.get_projects(),
    })


@app.route('/api/categories', methods=['GET'])
def get_categories():
    """获取分类列表"""
    engine = get_search_engine()
    return jsonify({
        'success': True,
        'categories': engine.get_categories(),
    })


@app.route('/api/submit', methods=['POST'])
def submit_record():
    """提交问题记录到知识库"""
    data = request.get_json() or {}

    # 接收字段
    project_name = data.get('project_name', '').strip()
    system_type = data.get('system_type', '').strip()
    apk_product = data.get('apk_product', '').strip()
    phenomenon = data.get('phenomenon', '').strip()
    troubleshoot_steps = data.get('troubleshoot_steps', '').strip()
    root_cause = data.get('root_cause', '').strip()
    solution = data.get('solution', '').strip()
    recorder = data.get('recorder', '').strip()

    # 必填校验
    if not project_name:
        return jsonify({'success': False, 'error': '项目名称不能为空'}), 400
    if not phenomenon:
        return jsonify({'success': False, 'error': '问题描述不能为空'}), 400
    if not recorder:
        return jsonify({'success': False, 'error': '记录人不能为空'}), 400

    try:
        # 1. 生成 Markdown 文件
        record_id = generate_record_id()
        md_content = build_markdown_content(
            record_id=record_id,
            project_name=project_name,
            system_type=system_type,
            apk_product=apk_product,
            phenomenon=phenomenon,
            troubleshoot_steps=troubleshoot_steps,
            root_cause=root_cause,
            solution=solution,
            recorder=recorder,
        )
        md_filename = build_md_filename(record_id, project_name, phenomenon)
        md_filepath = RECORDS_DIR / "项目" / md_filename

        # 确保目录存在
        md_filepath.parent.mkdir(parents=True, exist_ok=True)

        # 写入文件
        md_filepath.write_text(md_content, encoding='utf-8')

        # 2. 重建搜索索引
        rebuild_search_index()

        # 3. 审计日志
        log_audit({
            'timestamp': datetime.now().isoformat(),
            'action': 'submit_record',
            'record_id': record_id,
            'project': project_name,
            'recorder': recorder,
        })

        return jsonify({
            'success': True,
            'message': '问题记录已提交到知识库',
            'record_id': record_id,
            'file': str(md_filepath.relative_to(PROJECT_ROOT)),
        })

    except Exception as e:
        traceback.print_exc()
        return jsonify({
            'success': False,
            'error': f'提交失败：{str(e)}',
        }), 500


# ============================================================
# 入库辅助函数
# ============================================================

def generate_record_id():
    """生成记录 ID：RC-YYYYMMDD-NNNN"""
    today = datetime.now().strftime('%Y%m%d')
    # 查找今天已有的记录数
    project_dir = RECORDS_DIR / "项目"
    if project_dir.exists():
        today_files = list(project_dir.glob(f"RC-{today}-*.md"))
        seq = len(today_files) + 1
    else:
        seq = 1
    return f"RC-{today}-{seq:03d}"


def build_md_filename(record_id, project_name, phenomenon):
    """构建 Markdown 文件名"""
    # 从 phenomenon 提取关键词
    keywords = extract_keywords_from_text(phenomenon)
    keyword_str = keywords[0] if keywords else "问题"
    # 清理文件名
    keyword_str = re.sub(r'[\\/:*?"<>|\r\n\t]', '', keyword_str)[:20]
    return f"{record_id}-{project_name}-{keyword_str}.md"


def extract_keywords_from_text(text):
    """从文本提取关键词"""
    if not text:
        return []
    text = text.lower()
    keywords = []
    tech_keywords = [
        "redis", "mysql", "nacos", "emqx", "mqtt", "docker",
        "nginx", "java", "python", "smc", "exchange",
        "门口屏", "无纸化", "桌牌", "人脸", "签到", "白名单",
        "wss", "websocket", "授权", "激活", "token", "证书",
        "时区", "ntp", "磁盘", "内存", "oom",
        "配置", "config", "短信", "邮件", "同步",
        "钉钉", "企业微信", "welink", "oa",
        "连接失败", "启动失败", "未授权", "绑定失败",
    ]
    for kw in tech_keywords:
        if kw in text:
            keywords.append(kw)
    return keywords[:3]


def categorize_text(text):
    """自动分类"""
    if not text:
        return ["其他"]
    text = text.lower()
    categories = []
    rules = {
        "服务/容器异常": ["docker", "容器", "服务", "500", "502", "启动失败", "exited"],
        "设备终端-门口屏": ["门口屏", "门ロ屏"],
        "设备终端-无纸化": ["无纸化"],
        "设备终端-桌牌": ["桌牌"],
        "MQTT/EMQX": ["mqtt", "emqx", "wss", "websocket"],
        "数据库": ["mysql", "redis", "数据库", "连接失败"],
        "配置文件": ["配置", "config", "不生效"],
        "前端/UI": ["页面", "前端", "ui", "显示"],
        "部署/升级": ["部署", "升级", "更新"],
        "网络/SSH": ["网络", "ssh", "端口", "不通"],
        "授权/激活": ["授权", "激活", "token", "jwt", "证书"],
    }
    for cat, kws in rules.items():
        for kw in kws:
            if kw in text:
                categories.append(cat)
                break
    return categories if categories else ["其他"]


def build_markdown_content(record_id, project_name, system_type, apk_product,
                           phenomenon, troubleshoot_steps, root_cause, solution, recorder):
    """构建 Markdown 内容"""
    today = datetime.now().strftime('%Y-%m-%d')
    categories = categorize_text(phenomenon + " " + (root_cause or ""))
    keywords = extract_keywords_from_text(phenomenon + " " + (root_cause or "") + " " + (solution or ""))

    lines = []
    # Frontmatter
    lines.append("---")
    lines.append(f'id: "{record_id}"')
    lines.append(f'date: "{today}"')
    lines.append(f'source: "项目"')
    lines.append(f'project: "{project_name}"')
    lines.append(f'category: [{", ".join(categories)}]')
    lines.append(f'status: "未解决"')
    lines.append(f'keywords: [{", ".join(keywords)}]')
    lines.append(f'recorder: "{recorder}"')
    lines.append("---")
    lines.append("")

    # 标题
    lines.append(f"# [{project_name}] {phenomenon}")
    lines.append("")

    # 现象
    lines.append("## 现象")
    lines.append(phenomenon)
    lines.append("")

    # 系统类型和 APK 产品（如有）
    if system_type:
        lines.append(f"**系统类型**：{system_type}")
    if apk_product:
        lines.append(f"**APK 产品**：{apk_product}")
    if system_type or apk_product:
        lines.append("")

    # 排查过程
    if troubleshoot_steps:
        lines.append("## 排查过程")
        lines.append(troubleshoot_steps)
        lines.append("")

    # 根因
    if root_cause:
        lines.append("## 根因")
        lines.append(root_cause)
        lines.append("")

    # 解决方案
    if solution:
        lines.append("## 解决方案")
        lines.append(solution)
        lines.append("")

    # 元信息
    lines.append("## 元信息")
    lines.append(f"- 记录人：{recorder}")
    lines.append(f"- 记录时间：{today}")

    return "\n".join(lines)


def rebuild_search_index():
    """重建搜索索引"""
    # 复用 build_index.py 的逻辑
    import subprocess
    build_index_script = SCRIPT_DIR.parent / "build_index.py"
    if build_index_script.exists():
        result = subprocess.run(
            [sys.executable, str(build_index_script)],
            capture_output=True,
            text=True,
            timeout=60,
            cwd=str(PROJECT_ROOT),
        )
        if result.returncode != 0:
            print(f"索引重建失败：{result.stderr}")

        # 重新加载搜索引擎
        global search_engine
        search_engine = None
        get_search_engine()


# ============================================================
# 审计日志
# ============================================================

AUDIT_LOG_FILE = SCRIPT_DIR / "audit.log"

def log_audit(entry):
    """记录审计日志"""
    line = json.dumps(entry, ensure_ascii=False)
    with open(AUDIT_LOG_FILE, 'a', encoding='utf-8') as f:
        f.write(line + '\n')


# ============================================================
# 启动
# ============================================================

if __name__ == '__main__':
    print("="*50)
    print("🔍 问题排查助手 Web 服务")
    print("="*50)
    print(f"📁 项目根目录：{PROJECT_ROOT}")
    print(f"📊 知识库：{len(get_search_engine().records)} 条记录")
    print(f"🌐 服务地址：http://localhost:8088")
    print("="*50)

    app.run(
        host='0.0.0.0',
        port=8088,
        debug=True,
        threaded=True,
    )