from fastapi import APIRouter, Depends, Query, Request
from ...core.database import get_db, get_system_db, _has_split_databases
from ...core.permission import get_current_user, is_admin_role
from ...core.config import settings, _encrypt_value, _decrypt_value
import json
import os
import hashlib
import aiohttp
from datetime import datetime
import yaml
from ...utils.request_utils import safe_json

router = APIRouter()

DEERFLOW_BASE = settings.DEERFLOW_GATEWAY_URL


async def _system_db():
    """Dependency: system.db when split databases exist, otherwise fallback to get_db()"""
    _db_gen = get_system_db() if _has_split_databases() else get_db()
    async for conn, cur in _db_gen:
        yield conn, cur


async def _proxy_to_deerflow(method: str, path: str, request: Request, body: dict | None = None):
    url = f"{DEERFLOW_BASE}{path}"
    async with aiohttp.ClientSession() as session:
        headers = {}
        if body is not None:
            headers["Content-Type"] = "application/json"
            resp = await session.request(method, url, json=body, headers=headers,
                                         timeout=aiohttp.ClientTimeout(total=60))
        else:
            resp = await session.request(method, url, headers=headers,
                                         timeout=aiohttp.ClientTimeout(total=60))
        if resp.status >= 400:
            error_text = await resp.text()
            return {"error": f"DeerFlow返回错误 {resp.status}", "detail": error_text[:500]}
        text = await resp.text()
        if not text:
            return None
        return json.loads(text)


# ============================================================
#  Agent 管理 — 透传 DeerFlow /api/agents
# ============================================================

@router.get("/agents")
async def list_agents(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    return await _proxy_to_deerflow("GET", "/api/agents", request)


@router.get("/agents/check")
async def check_agent_name(name: str = Query(""), request: Request = None):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    return await _proxy_to_deerflow("GET", f"/api/agents/check?name={name}", request)


@router.post("/agents")
async def create_agent(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    body = await safe_json(request)
    return await _proxy_to_deerflow("POST", "/api/agents", request, body)


@router.get("/agents/{agent_name}")
async def get_agent(agent_name: str, request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    return await _proxy_to_deerflow("GET", f"/api/agents/{agent_name}", request)


@router.put("/agents/{agent_name}")
async def update_agent(agent_name: str, request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    body = await safe_json(request)
    return await _proxy_to_deerflow("PUT", f"/api/agents/{agent_name}", request, body)


@router.delete("/agents/{agent_name}")
async def delete_agent(agent_name: str, request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    return await _proxy_to_deerflow("DELETE", f"/api/agents/{agent_name}", request)


# ============================================================
#  技能管理 — 透传 DeerFlow /api/skills
# ============================================================

@router.get("/skills")
async def list_skills(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    # 优先从 DeerFlow Gateway API 获取技能列表
    try:
        result = await _proxy_to_deerflow("GET", "/api/skills", request)
        if result and isinstance(result, dict) and "skills" in result:
            return {"skills": result["skills"]}
    except Exception:
        pass
    # Fallback: 扫描本地 deer-flow/skills/ 目录
    skills_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))), "deer-flow", "skills")
    skills = []
    for category in ["custom", "public"]:
        cat_dir = os.path.join(skills_dir, category)
        if not os.path.isdir(cat_dir):
            continue
        for entry in os.listdir(cat_dir):
            skill_dir = os.path.join(cat_dir, entry)
            skill_file = os.path.join(skill_dir, "SKILL.md")
            if os.path.isdir(skill_dir) and os.path.isfile(skill_file):
                skill_info = {"name": entry, "category": category}
                try:
                    with open(skill_file, "r", encoding="utf-8") as f:
                        content = f.read()
                    import re
                    # 解析 YAML frontmatter 中的 name 和 description
                    fm_match = re.search(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
                    if fm_match:
                        for line in fm_match.group(1).split('\n'):
                            if ':' in line:
                                key, _, val = line.partition(':')
                                key = key.strip()
                                val = val.strip()
                                if key == 'name':
                                    skill_info['title'] = val
                                elif key == 'description':
                                    skill_info['description'] = val
                    # 解析 skill_id
                    sid_match = re.search(r'skill_id[：:]\s*(S\d+)', content)
                    if sid_match:
                        skill_info["skill_id"] = sid_match.group(1)
                    # 如果 frontmatter 没有 description，尝试从正文提取
                    if 'description' not in skill_info:
                        desc_match = re.search(r'>\s*\*\*描述\*\*[：:]\s*(.+)', content)
                        if desc_match:
                            skill_info["description"] = desc_match.group(1).strip()
                except Exception:
                    pass
                skills.append(skill_info)
    return {"skills": skills}


@router.get("/skills/{skill_name}")
async def get_skill(skill_name: str, request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    return await _proxy_to_deerflow("GET", f"/api/skills/{skill_name}", request)


@router.put("/skills/{skill_name}")
async def update_skill(skill_name: str, request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    body = await safe_json(request)
    return await _proxy_to_deerflow("PUT", f"/api/skills/{skill_name}", request, body)


@router.delete("/skills/{skill_name}")
async def delete_skill(skill_name: str, request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    result = await _proxy_to_deerflow("DELETE", f"/api/skills/{skill_name}", request)
    if result and isinstance(result, dict) and result.get("error"):
        import shutil
        skills_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))), "deer-flow", "skills")
        for category in ["custom", "public"]:
            skill_dir = os.path.join(skills_dir, category, skill_name)
            if os.path.isdir(skill_dir):
                shutil.rmtree(skill_dir)
                return {"success": True, "message": f"技能 '{skill_name}' 已删除"}
        return {"error": f"技能 '{skill_name}' 不存在"}
    return result


@router.get("/skills/{skill_name}/content")
async def get_skill_content(skill_name: str, request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}

    skills_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))), "deer-flow", "skills")
    for category in ["custom", "public"]:
        skill_dir = os.path.join(skills_dir, category, skill_name)
        skill_file = os.path.join(skill_dir, "SKILL.md")
        if os.path.isfile(skill_file):
            with open(skill_file, "r", encoding="utf-8") as f:
                content = f.read()
            return {"skill_name": skill_name, "category": category, "content": content}

    for category in ["custom", "public"]:
        cat_dir = os.path.join(skills_dir, category)
        if os.path.isdir(cat_dir):
            for root, dirs, _ in os.walk(cat_dir):
                for d in dirs:
                    if d == skill_name:
                        skill_file = os.path.join(root, d, "SKILL.md")
                        if os.path.isfile(skill_file):
                            with open(skill_file, "r", encoding="utf-8") as f:
                                content = f.read()
                            return {"skill_name": skill_name, "category": category, "content": content}

    return {"error": f"技能 '{skill_name}' 不存在"}


@router.post("/skills/create")
async def create_skill(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}

    body = await safe_json(request)
    name = body.get("name", "").strip()
    description = body.get("description", "").strip()
    content = body.get("content", "").strip()

    if not name:
        return {"error": "技能名称不能为空"}
    if not description:
        return {"error": "技能描述不能为空"}

    skills_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))), "deer-flow", "skills")
    custom_dir = os.path.join(skills_dir, "custom", name)
    if os.path.exists(custom_dir):
        return {"error": f"技能 '{name}' 已存在"}

    os.makedirs(custom_dir, exist_ok=True)
    skill_md = f"---\nname: {name}\ndescription: {description}\n---\n\n{content}"
    with open(os.path.join(custom_dir, "SKILL.md"), "w", encoding="utf-8") as f:
        f.write(skill_md)

    return {"success": True, "skill_name": name, "message": f"技能 '{name}' 创建成功"}


@router.post("/skills/install")
async def install_skill(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    body = await safe_json(request)
    return await _proxy_to_deerflow("POST", "/api/skills/install", request, body)


# ============================================================
#  MCP 管理 — 透传 DeerFlow /api/mcp/config
# ============================================================

@router.get("/mcp/config")
async def get_mcp_config(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    return await _proxy_to_deerflow("GET", "/api/mcp/config", request)


@router.put("/mcp/config")
async def update_mcp_config(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    body = await safe_json(request)
    return await _proxy_to_deerflow("PUT", "/api/mcp/config", request, body)


@router.post("/mcp/config")
async def update_mcp_config_post(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    body = await safe_json(request)
    return await _proxy_to_deerflow("PUT", "/api/mcp/config", request, body)


@router.post("/mcp/test")
async def test_mcp_server(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    body = await safe_json(request)
    server_type = body.get("type", "stdio")
    command = body.get("command")
    args = body.get("args", [])
    url = body.get("url")
    env = body.get("env", {})

    if server_type == "stdio":
        if not command:
            return {"success": False, "message": "stdio 类型需要填写命令"}

        import asyncio
        import shutil

        cmd_path = shutil.which(command)
        if not cmd_path:
            return {"success": False, "message": f"命令 '{command}' 未找到，请确认已安装"}

        full_args = [command] + args
        env_vars = {**os.environ, **env}

        try:
            proc = await asyncio.create_subprocess_exec(
                *full_args,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                env=env_vars,
            )
            try:
                stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=8)
            except asyncio.TimeoutError:
                proc.kill()
                await proc.wait()
                return {"success": True, "message": "进程可以启动（8秒后自动终止，这是正常的stdio MCP行为）"}

            stderr_text = stderr.decode("utf-8", errors="replace")[:500]
            stdout_text = stdout.decode("utf-8", errors="replace")[:500]

            if proc.returncode == 0:
                return {"success": True, "message": "进程启动成功并正常退出", "stdout": stdout_text}
            elif "MCP" in stderr_text or "mcp" in stderr_text.lower() or "server" in stderr_text.lower():
                return {"success": True, "message": "MCP 服务器进程已启动（stdio 协议需要持续运行，这是正常的）", "stderr": stderr_text}
            else:
                return {"success": False, "message": f"进程退出码 {proc.returncode}", "stderr": stderr_text, "stdout": stdout_text}
        except Exception as e:
            return {"success": False, "message": f"启动失败: {str(e)}"}

    else:
        if not url:
            return {"success": False, "message": "SSE/HTTP 类型需要填写 URL"}

        try:
            import httpx
            async with httpx.AsyncClient(timeout=8, follow_redirects=True) as client:
                resp = await client.get(url.rstrip("/"), headers={"Accept": "text/event-stream,*/*"})
                return {"success": True, "message": f"URL 可访问 (HTTP {resp.status_code})", "status_code": resp.status_code}
        except Exception as e:
            return {"success": False, "message": f"无法连接到 {url}: {str(e)}"}


@router.post("/mcp/refresh")
async def refresh_mcp_cache(request: Request):
    """刷新 DeerFlow 的 MCP 工具缓存，使配置变更立即生效（无需重启 DeerFlow）。"""
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    try:
        import httpx
        async with httpx.AsyncClient(timeout=15) as client:
            resp = await client.post(
                f"{settings.DEERFLOW_GATEWAY_URL}/api/mcp/refresh",
                headers={"Content-Type": "application/json"},
            )
            if resp.status_code == 200:
                return {"success": True, "message": "MCP 缓存已刷新", "data": resp.json()}
            else:
                return {"success": False, "message": f"DeerFlow 返回错误: HTTP {resp.status_code}", "detail": resp.text[:500]}
    except Exception as e:
        return {"success": False, "message": f"刷新失败: {str(e)}"}


# ============================================================
#  工具管理 — DeerFlow 工具体系完整文档
# ============================================================

BUILTIN_TOOL_DOCS = [
    {
        "name": "view_image",
        "display_name": "图片视觉识别",
        "description": "使用视觉大模型识别图片中的文字、物体和场景。仅在配置了支持视觉的模型时可用",
        "category": "conditional",
        "group": "vision-tools",
        "tool_type": "视觉",
        "source": "DeerFlow 条件内置",
        "parameters": [
            {"name": "image", "type": "image/base64", "required": True, "desc": "图片数据"},
        ],
        "dependencies": ["视觉识别模型 (vision_ocr场景)"],
        "used_by_skills": ["现场证据分析", "文件照片识别"],
        "condition": "仅当模型 supports_vision=True 时自动加载",
        "workflow": "Agent获取图片 → 调用视觉模型API → 返回识别结果(文字/描述)",
    },
    {
        "name": "ask_clarification",
        "display_name": "向用户澄清问题",
        "description": "当AI不确定用户意图或需要更多信息时，主动向用户追问，确保执法判断的准确性",
        "category": "builtin",
        "group": "interaction",
        "tool_type": "交互",
        "source": "DeerFlow 内置",
        "parameters": [
            {"name": "question", "type": "string", "required": True, "desc": "对用户的澄清问题"},
        ],
        "dependencies": [],
        "used_by_skills": ["所有技能"],
        "workflow": "AI发现信息不足 → 构造澄清问题 → 暂停等待用户回复 → 继续推理",
    },
    {
        "name": "present_file",
        "display_name": "文件呈现",
        "description": "将AI生成的文档、报表、分析结果以文件形式呈现给用户",
        "category": "builtin",
        "group": "presentation",
        "tool_type": "展示",
        "source": "DeerFlow 内置",
        "parameters": [
            {"name": "file_path", "type": "string", "required": True, "desc": "文件路径"},
        ],
        "dependencies": [],
        "used_by_skills": ["文书生成", "报告输出"],
        "workflow": "Agent生成内容 → 写入文件 → present_file通知用户 → 用户下载/预览",
    },
    {
        "name": "task",
        "display_name": "子代理任务分发",
        "description": "将复杂任务拆分为子任务，分发给专门的子Agent并行处理，提高效率",
        "category": "subagent",
        "group": "task-dispatch",
        "tool_type": "分发",
        "source": "DeerFlow 子代理",
        "parameters": [
            {"name": "task_description", "type": "string", "required": True, "desc": "子任务描述"},
            {"name": "subagent_name", "type": "string", "required": False, "desc": "指定子Agent名称"},
        ],
        "dependencies": [],
        "used_by_skills": ["复杂案件分析", "批量数据处理"],
        "condition": "仅当 subagent_enabled=True 时可用",
        "workflow": "主Agent识别子任务 → 分发给子Agent → 子Agent独立执行 → 返回结果汇总",
    },
]

MCP_TOOL_DOCS = [
    {
        "name": "query_knowledge_graph",
        "display_name": "Cypher图谱查询",
        "description": "使用Cypher语言查询Neo4j知识图谱，支持法律实体、案件、人员等复杂关系查询",
        "category": "mcp",
        "group": "graphiti-tools",
        "mcp_server": "Graphiti MCP (端口8092)",
        "tool_type": "图谱查询",
        "parameters": [
            {"name": "query", "type": "string", "required": True, "desc": "Cypher查询语句"},
        ],
        "dependencies": ["Graphiti MCP Server", "Neo4j 图数据库"],
        "used_by_skills": ["关联关系分析", "案件溯源", "主体关联"],
    },
    {
        "name": "natural_language_query",
        "display_name": "自然语言图谱查询",
        "description": "用自然语言查询知识图谱（自动转换为Cypher），无需编写查询语句",
        "category": "mcp",
        "group": "graphiti-tools",
        "mcp_server": "Graphiti MCP (端口8092)",
        "tool_type": "自然语言查询",
        "parameters": [
            {"name": "question", "type": "string", "required": True, "desc": "自然语言问题"},
        ],
        "dependencies": ["Graphiti MCP Server"],
    },
    {
        "name": "search_nodes",
        "display_name": "图谱节点搜索",
        "description": "按标签和关键词搜索知识图谱中的节点（法律、案例、主体等）",
        "category": "mcp",
        "group": "graphiti-tools",
        "mcp_server": "Graphiti MCP (端口8092)",
        "tool_type": "节点搜索",
        "parameters": [
            {"name": "label", "type": "string", "required": False, "desc": "节点类型, 如 Law/Case/Company"},
            {"name": "search", "type": "string", "required": False, "desc": "搜索关键词"},
            {"name": "limit", "type": "integer", "required": False, "desc": "返回条数, 默认20"},
        ],
        "dependencies": ["Graphiti MCP Server"],
    },
    {
        "name": "find_shortest_path",
        "display_name": "最短路径查询",
        "description": "查找两个节点之间的最短关系路径，用于发现隐藏的关联",
        "category": "mcp",
        "group": "graphiti-tools",
        "mcp_server": "Graphiti MCP (端口8092)",
        "tool_type": "路径分析",
        "parameters": [
            {"name": "from_id", "type": "integer", "required": True, "desc": "起始节点ID"},
            {"name": "to_id", "type": "integer", "required": True, "desc": "目标节点ID"},
            {"name": "max_depth", "type": "integer", "required": False, "desc": "最大深度, 默认3"},
        ],
        "dependencies": ["Graphiti MCP Server"],
    },
    {
        "name": "get_recommendations",
        "display_name": "节点推荐",
        "description": "基于图谱结构推荐与指定节点关联的其他节点",
        "category": "mcp",
        "group": "graphiti-tools",
        "mcp_server": "Graphiti MCP (端口8092)",
        "tool_type": "推荐",
        "parameters": [
            {"name": "node_id", "type": "integer", "required": True, "desc": "节点ID"},
            {"name": "limit", "type": "integer", "required": False, "desc": "推荐数量, 默认5"},
        ],
        "dependencies": ["Graphiti MCP Server"],
    },
    {
        "name": "onlyoffice-generate",
        "display_name": "文书模板填充",
        "description": "基于Word模板和填充数据自动生成执法文书（行政处罚决定书等）",
        "category": "mcp",
        "group": "office-tools",
        "mcp_server": "OnlyOffice MCP (端口3025)",
        "tool_type": "文书生成",
        "parameters": [
            {"name": "template_id", "type": "string", "required": True, "desc": "文书模板ID"},
            {"name": "case_id", "type": "string", "required": False, "desc": "关联案件ID"},
            {"name": "fill_data", "type": "object", "required": False, "desc": "填充变量"},
        ],
        "dependencies": ["OnlyOffice Document Server"],
        "used_by_skills": ["行政处罚决定书生成", "案件文书制作"],
    },
    {
        "name": "onlyoffice-convert",
        "display_name": "文档格式转换",
        "description": "将文档在不同格式之间转换（docx→pdf等），支持水印和密码保护",
        "category": "mcp",
        "group": "office-tools",
        "mcp_server": "OnlyOffice MCP (端口3025)",
        "tool_type": "格式转换",
        "parameters": [
            {"name": "file_id", "type": "string", "required": True, "desc": "源文件ID"},
            {"name": "target_format", "type": "string", "required": False, "desc": "目标格式: pdf/docx等"},
        ],
        "dependencies": ["OnlyOffice Document Server"],
    },
]

TOOL_GROUPS = [
    {"group_name": "vision-tools", "display_name": "视觉识别", "tools": ["view_image"],
     "desc": "基于视觉大模型的图片识别能力"},
    {"group_name": "interaction", "display_name": "用户交互", "tools": ["ask_clarification"],
     "desc": "与用户的交互澄清机制"},
    {"group_name": "presentation", "display_name": "结果呈现", "tools": ["present_file"],
     "desc": "分析结果的展示和文件输出"},
    {"group_name": "task-dispatch", "display_name": "任务分发", "tools": ["task"],
     "desc": "复杂任务的子代理分发和并行处理"},
    # {"group_name": "wiki-tools", "display_name": "Wiki知识库", "tools": ["search_wiki", "read_wiki_page", "write_wiki_page", "save_answer_to_wiki"],
    #  "desc": "Wiki知识库的检索、读取、写入和沉淀"},
    {"group_name": "graphiti-tools", "display_name": "知识图谱", "tools": ["query_knowledge_graph", "natural_language_query", "search_nodes", "find_shortest_path", "get_recommendations"],
     "desc": "Neo4j知识图谱的复杂关系查询和分析"},
    {"group_name": "office-tools", "display_name": "文书处理", "tools": ["onlyoffice-generate", "onlyoffice-convert"],
     "desc": "执法文书的自动生成和格式转换"},
]

WORKFLOW_DIAGRAM = {
    "title": "Agent → Skill → Tool 工作流程",
    "steps": [
        {"step": 1, "actor": "用户", "action": "提出执法相关问题（如：某超市销售过期食品应如何处罚？）"},
        {"step": 2, "actor": "Agent（案件分析助手）", "action": "接收问题，理解执法场景，识别为「食品类」案件领域"},
        {"step": 3, "actor": "L2上下文注入", "action": "根据案件领域自动匹配食品执法相关的背景知识和记忆"},
        {"step": 4, "actor": "Skill（食品违法分析技能）", "action": "Agent根据案件领域自动激活「食品违法分析」技能"},
        {"step": 5, "actor": "Tool: query_knowledge_graph", "action": "Skill调用图谱工具，查询类似案例的处罚记录和判罚参考"},
        {"step": 7, "actor": "Agent", "action": "综合法律条文+类似案例+执法经验，生成处罚建议和依据"},
        {"step": 8, "actor": "用户", "action": "获得完整的处罚建议、法律依据和类似案例参考"},
    ],
}


@router.get("/tools-info")
async def get_tools_info(request: Request):
    """返回工具体系的完整信息：内置工具、MCP工具、工具组、工作流程"""
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限"}
    return {
        "builtin_tools": BUILTIN_TOOL_DOCS,
        "mcp_tools": MCP_TOOL_DOCS,
        "tool_groups": TOOL_GROUPS,
        "workflow": WORKFLOW_DIAGRAM,
    }


# ============================================================
#  统计 API（保留，操作 agent_calls 表）
# ============================================================


@router.get("/statistics")
async def get_ai_statistics(
    request: Request,
    period: str = Query("week"),
    agent_name: str = Query(""),
    db=Depends(get_db),
):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db

    try:
        date_conditions = {
            "day": "AND ac.created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
            "week": "AND ac.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)",
            "month": "AND ac.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
        }
        date_condition = date_conditions.get(period, "")

        agent_condition = "AND ac.agent_type = %s" if agent_name else ""
        query_params = [agent_name] if agent_name else []

        await cur.execute(
            f"""SELECT
                COUNT(*) as total_calls,
                SUM(CASE WHEN ac.status = 'success' THEN 1 ELSE 0 END) as success_count,
                SUM(CASE WHEN ac.status != 'success' THEN 1 ELSE 0 END) as error_count,
                ROUND(AVG(ac.latency_ms), 2) as avg_latency_ms,
                SUM(ac.total_tokens) as total_tokens,
                SUM(ac.input_tokens) as total_input_tokens,
                SUM(ac.output_tokens) as total_output_tokens,
                ROUND(SUM(ac.cost_usd), 4) as total_cost_usd,
                COUNT(DISTINCT ac.user_id) as unique_users,
                COUNT(DISTINCT ac.thread_id) as unique_threads
              FROM agent_calls ac
              WHERE 1=1 {date_condition} {agent_condition}""",
            query_params,
        )
        stats_row = await cur.fetchone()
        stats = dict(stats_row) if stats_row else {}

        await cur.execute(
            f"""SELECT
                ac.agent_type,
                ac.agent_name,
                COUNT(*) as call_count,
                SUM(CASE WHEN ac.status = 'success' THEN 1 ELSE 0 END) as success_count,
                ROUND(AVG(ac.latency_ms), 2) as avg_latency_ms,
                SUM(ac.total_tokens) as total_tokens,
                ROUND(SUM(ac.cost_usd), 4) as cost_usd,
                ROUND(SUM(CASE WHEN ac.status = 'success' THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as success_rate
              FROM agent_calls ac
              WHERE 1=1 {date_condition} {agent_condition}
              GROUP BY ac.agent_type, ac.agent_name
              ORDER BY call_count DESC
              LIMIT 20""",
            query_params,
        )
        by_agent = [dict(r) for r in await cur.fetchall()]

        time_formats = {
            "day": ('%Y-%m-%d %H:00', 'DATE_FORMAT(ac.created_at, "%Y-%m-%d %H:00")'),
            "month": ('%Y-%m-%d', 'DATE_FORMAT(ac.created_at, "%Y-%m-%d")'),
        }
        _, group_by = time_formats.get(period, ('%Y-%m-%d', 'DATE_FORMAT(ac.created_at, "%Y-%m-%d")'))

        await cur.execute(
            f"""SELECT
                {group_by} as time_label,
                COUNT(*) as call_count,
                SUM(CASE WHEN ac.status = 'success' THEN 1 ELSE 0 END) as success_count,
                SUM(CASE WHEN ac.status != 'success' THEN 1 ELSE 0 END) as error_count,
                ROUND(AVG(ac.latency_ms), 2) as avg_latency_ms,
                SUM(ac.total_tokens) as total_tokens
              FROM agent_calls ac
              WHERE 1=1 {date_condition} {agent_condition}
              GROUP BY time_label
              ORDER BY time_label ASC""",
            query_params,
        )
        time_series = [dict(r) for r in await cur.fetchall()]

        await cur.execute(
            f"""SELECT
                ac.status,
                COUNT(*) as count,
                ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM agent_calls ac2 WHERE 1=1 {date_condition.replace("ac.", "ac2.")} {agent_condition.replace("ac.", "ac2.")}), 2) as percentage
              FROM agent_calls ac
              WHERE 1=1 {date_condition} {agent_condition}
              GROUP BY ac.status""",
            query_params,
        )
        status_dist = [dict(r) for r in await cur.fetchall()]

        await cur.execute(
            f"""SELECT
                ac.user_id,
                u.username,
                u.real_name,
                COUNT(*) as call_count,
                SUM(ac.total_tokens) as total_tokens
              FROM agent_calls ac
              LEFT JOIN users u ON ac.user_id = u.id
              WHERE 1=1 {date_condition} {agent_condition}
              GROUP BY ac.user_id, u.username, u.real_name
              ORDER BY call_count DESC
              LIMIT 10""",
            query_params,
        )
        top_users = [dict(r) for r in await cur.fetchall()]

        return {
            "period": period,
            "summary": stats,
            "by_agent": by_agent,
            "time_series": time_series,
            "status_distribution": status_dist,
            "top_users": top_users,
        }
    except Exception:
        return {
            "period": period,
            "summary": {
                "total_calls": 0, "success_count": 0, "error_count": 0,
                "avg_latency_ms": 0, "total_tokens": 0, "total_input_tokens": 0,
                "total_output_tokens": 0, "total_cost_usd": 0, "unique_users": 0, "unique_threads": 0,
            },
            "by_agent": [],
            "time_series": [],
            "status_distribution": [],
            "top_users": [],
        }


# ============================================================
# 阶段二新增：供应商/模型/场景配置/模型代理服务 API
# ============================================================

def _mask_key(key: str) -> str:
    if not key or len(key) < 5:
        return "••••••••"
    return "••••••••" + key[-4:]


# ---- 2.1 模型管理 API（扁平化） ----

@router.get("/models")
async def list_ai_models(
    request: Request,
    keyword: str = Query(""),
    page: int = Query(1, ge=1),
    pageSize: int = Query(10, ge=1, le=100),
    db=Depends(_system_db),
):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    where = "WHERE 1=1"
    params = []
    if keyword:
        where += " AND (model_name LIKE %s OR display_name LIKE %s OR provider_name LIKE %s)"
        kw = f"%{keyword}%"
        params.extend([kw, kw, kw])

    try:
        await cur.execute(f"SELECT COUNT(*) as total FROM ai_models {where}", params)
        total = dict(await cur.fetchone())["total"]

        offset = (page - 1) * pageSize
        await cur.execute(
            f"SELECT * FROM ai_models {where} ORDER BY sort_order ASC, model_name ASC LIMIT %s OFFSET %s",
            params + [pageSize, offset],
        )
        rows = await cur.fetchall()
        models = []
        for r in rows:
            d = dict(r)
            d["api_key"] = _mask_key(d.get("api_key", ""))
            models.append(d)
        return {"list": models, "total": total, "page": page, "pageSize": pageSize}
    except Exception:
        return {"list": [], "total": 0, "page": page, "pageSize": pageSize}


@router.get("/models/list")
async def list_all_models_simple(
    request: Request,
    model_type: str = Query(""),
    db=Depends(_system_db),
):
    """简易列表（不分页），供场景配置选择模型使用"""
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    where = "WHERE enabled = 1"
    params = []
    if model_type:
        where += " AND (model_type = %s OR %s = 'chat')"
        params.extend([model_type, model_type])

    try:
        await cur.execute(f"SELECT * FROM ai_models {where} ORDER BY model_name ASC", params)
        rows = await cur.fetchall()
        return [dict(r) for r in rows]
    except Exception:
        return []


@router.post("/models")
async def create_ai_model(request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    body = await safe_json(request)
    provider_name = body.get("provider_name", "").strip()
    model_name = body.get("model_name", "").strip()
    api_base = body.get("api_base", "").strip()
    api_key_raw = body.get("api_key", "").strip()

    if not provider_name:
        return {"error": "厂商名称不能为空"}
    if not model_name:
        return {"error": "模型名称不能为空"}
    if not api_base:
        return {"error": "API端点不能为空"}
    if not api_key_raw:
        return {"error": "API Key不能为空"}

    encrypted_key = _encrypt_value(api_key_raw)

    try:
        await cur.execute(
            """INSERT INTO ai_models (provider_name, model_name, display_name,
               api_base, api_key, model_type, dimensions, supports_thinking, supports_reasoning_effort,
               thinking_default_on, supports_vision, is_local, status, enabled)
               VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'untested', 1)""",
            (
                provider_name, model_name,
                body.get("display_name", model_name),
                api_base, encrypted_key,
                body.get("model_type", "chat"),
                body.get("dimensions", 1024),
                body.get("supports_thinking", 0),
                body.get("supports_reasoning_effort", 0),
                body.get("thinking_default_on", 0),
                body.get("supports_vision", 0),
                body.get("is_local", 0),
            ),
        )
        new_id = cur.lastrowid
        await cur.execute("SELECT * FROM ai_models WHERE id = %s", (new_id,))
        result = dict(await cur.fetchone())
        result["api_key"] = _mask_key(result.get("api_key", ""))
        return result
    except Exception as e:
        return {"error": f"创建模型失败: {str(e)}"}


@router.get("/models/{model_id}")
async def get_ai_model(model_id: int, request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    await cur.execute("SELECT * FROM ai_models WHERE id = %s", (model_id,))
    row = await cur.fetchone()
    if not row:
        return {"error": "模型不存在"}
    result = dict(row)
    result["api_key"] = _mask_key(result.get("api_key", ""))
    return result


@router.put("/models/{model_id}")
async def update_ai_model(model_id: int, request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    body = await safe_json(request)
    await cur.execute("SELECT id FROM ai_models WHERE id = %s", (model_id,))
    if not await cur.fetchone():
        return {"error": "模型不存在"}

    updates = []
    values = []
    safe_fields = ["provider_name", "model_name", "display_name", "api_base", "model_type",
                   "dimensions", "supports_thinking", "supports_reasoning_effort", "thinking_default_on",
                   "supports_vision", "is_local", "enabled", "sort_order"]
    for f in safe_fields:
        if f in body:
            updates.append(f"{f} = %s")
            values.append(body[f])

    if "api_key" in body and body["api_key"]:
        updates.append("api_key = %s")
        values.append(_encrypt_value(body["api_key"]))

    if updates:
        values.append(model_id)
        await cur.execute(f"UPDATE ai_models SET {', '.join(updates)} WHERE id = %s", values)

    await cur.execute("SELECT * FROM ai_models WHERE id = %s", (model_id,))
    result = dict(await cur.fetchone())
    result["api_key"] = _mask_key(result.get("api_key", ""))
    return result


@router.delete("/models/{model_id}")
async def delete_ai_model(model_id: int, request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    await cur.execute("DELETE FROM ai_scene_model_configs WHERE model_id = %s", (model_id,))
    result = await cur.execute("DELETE FROM ai_models WHERE id = %s", (model_id,))
    if result == 0:
        return {"error": "模型不存在"}
    return None


# 预保存模型连通测试（不保存到数据库）
@router.post("/models/test")
async def test_model_before_save(request: Request):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    body = await safe_json(request)
    api_base = (body.get("api_base", "") or "").strip()
    api_key = body.get("api_key", "").strip()
    model_name = body.get("model_name", "").strip()
    provider_name = body.get("provider_name", "").strip()
    model_type = body.get("model_type", "chat").strip().lower()

    if not api_base:
        return {"success": False, "message": "API端点不能为空"}
    if not api_key:
        return {"success": False, "message": "API Key不能为空"}

    def _normalize_base_url(url: str) -> str:
        url = url.rstrip("/")
        for suffix in ("/embeddings", "/rerank", "/sentence-similarity", "/chat/completions",
                        "/v1/chat/completions", "/v1/embeddings", "/v1/rerank"):
            if url.endswith(suffix):
                url = url[: -len(suffix)]
                break
        return url

    api_base = _normalize_base_url(api_base)
    is_moark = "moark" in api_base.lower()

    # Embedding 模型测试
    if model_type == "embedding":
        try:
            async with aiohttp.ClientSession() as session:
                headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
                payload = {
                    "model": model_name,
                    "input": ["测试文本"],
                    "encoding_format": "float",
                }
                async with session.post(f"{api_base}/embeddings", json=payload, headers=headers,
                                        timeout=aiohttp.ClientTimeout(total=30)) as resp:
                    if resp.status == 200:
                        data = await resp.json()
                        if data.get("data") and len(data["data"]) > 0:
                            dim = len(data["data"][0]["embedding"])
                            return {"success": True, "message": f"Embedding 接口连接成功，维度={dim}"}
                        return {"success": True, "message": "Embedding API 响应正常"}
                    error_body = await resp.text()
                    return {"success": False, "message": f"HTTP {resp.status}: {error_body[:200]}"}
        except Exception as e:
            return {"success": False, "message": f"连接异常: {str(e)}"}

    # Rerank 模型测试
    if model_type == "rerank":
        try:
            async with aiohttp.ClientSession() as session:
                headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
                if is_moark:
                    payload = {
                        "inputs": {
                            "source_sentence": "测试查询",
                            "sentences": ["测试文档1", "测试文档2"],
                        },
                        "model": model_name,
                    }
                    rerank_url = f"{api_base}/sentence-similarity"
                else:
                    payload = {
                        "model": model_name,
                        "query": "测试查询",
                        "documents": ["测试文档1", "测试文档2"],
                        "top_n": 2,
                    }
                    rerank_url = f"{api_base}/rerank"

                async with session.post(rerank_url, json=payload, headers=headers,
                                        timeout=aiohttp.ClientTimeout(total=30)) as resp:
                    if resp.status == 200:
                        data = await resp.json()
                        if isinstance(data, list) and len(data) > 0:
                            return {"success": True, "message": f"Rerank 接口连接成功，得分: {data}"}
                        if data.get("results") and len(data["results"]) > 0:
                            return {"success": True, "message": "Rerank 接口连接成功，模型可用"}
                        return {"success": True, "message": "Rerank API 响应正常"}
                    error_body = await resp.text()
                    return {"success": False, "message": f"HTTP {resp.status}: {error_body[:200]}"}
        except Exception as e:
            return {"success": False, "message": f"连接异常: {str(e)}"}

    # 默认 LLM 测试 (model_type == "chat")
    try:
        async with aiohttp.ClientSession() as session:
            headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
            payload = {
                "model": model_name or "test",
                "messages": [{"role": "user", "content": "hi"}],
                "max_tokens": 5,
            }
            async with session.post(f"{api_base}/chat/completions", json=payload, headers=headers,
                                    timeout=aiohttp.ClientTimeout(total=30)) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    if data.get("choices") and len(data["choices"]) > 0:
                        return {"success": True, "message": "连接成功，模型可用"}
                    return {"success": True, "message": "API响应正常（未返回choices，可能模型名有误）"}
                error_body = await resp.text()
                return {"success": False, "message": f"HTTP {resp.status}: {error_body[:200]}"}
    except Exception as e:
        return {"success": False, "message": f"连接异常: {str(e)}"}


# 已保存模型的连通性测试
@router.post("/models/{model_id}/test")
async def test_model_connectivity(model_id: int, request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    await cur.execute("SELECT * FROM ai_models WHERE id = %s", (model_id,))
    row = await cur.fetchone()
    if not row:
        return {"error": "模型不存在"}

    model = dict(row)
    api_key = _decrypt_value(model.get("api_key", ""))
    api_base = (model.get("api_base") or "").strip()
    model_name = model.get("model_name", "")
    provider_name = model.get("provider_name", "")
    model_type = (model.get("model_type") or "chat").strip().lower()

    if not api_base:
        return {"success": False, "message": "API端点未配置"}
    if not api_key:
        return {"success": False, "message": "API Key未配置"}

    def _normalize(url: str) -> str:
        url = url.rstrip("/")
        for s in ("/embeddings", "/rerank", "/sentence-similarity", "/chat/completions",
                "/v1/chat/completions", "/v1/embeddings", "/v1/rerank"):
            if url.endswith(s):
                url = url[: -len(s)]
                break
        return url

    api_base = _normalize(api_base)
    is_moark = "moark" in api_base.lower()

    async def _update_status(status: str, error: str | None = None):
        await cur.execute(
            "UPDATE ai_models SET status = %s, last_test_at = NOW(), last_test_error = %s WHERE id = %s",
            (status, error, model_id),
        )

    # Embedding 模型测试
    if model_type == "embedding":
        try:
            async with aiohttp.ClientSession() as session:
                headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
                payload = {"model": model_name, "input": ["测试文本"], "encoding_format": "float"}
                async with session.post(f"{api_base}/embeddings", json=payload, headers=headers,
                                        timeout=aiohttp.ClientTimeout(total=30)) as resp:
                    if resp.status == 200:
                        data = await resp.json()
                        if data.get("data") and len(data["data"]) > 0:
                            await _update_status("available")
                            return {"success": True, "message": f"Embedding 接口连接成功，维度={len(data['data'][0]['embedding'])}"}
                        await _update_status("available")
                        return {"success": True, "message": "Embedding API 响应正常"}
                    error_body = await resp.text()
                    await _update_status("unavailable", f"HTTP {resp.status}: {error_body[:200]}")
                    return {"success": False, "message": f"HTTP {resp.status}: {error_body[:200]}"}
        except Exception as e:
            err_msg = str(e)[:200]
            await _update_status("unavailable", err_msg)
            return {"success": False, "message": f"连接异常: {err_msg}"}

    # Rerank 模型测试
    if model_type == "rerank":
        try:
            async with aiohttp.ClientSession() as session:
                headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
                if is_moark:
                    payload = {
                        "inputs": {
                            "source_sentence": "测试查询",
                            "sentences": ["测试文档1", "测试文档2"],
                        },
                        "model": model_name,
                    }
                    rerank_url = f"{api_base}/sentence-similarity"
                else:
                    payload = {"model": model_name, "query": "测试查询", "documents": ["测试文档1"], "top_n": 1}
                    rerank_url = f"{api_base}/rerank"

                async with session.post(rerank_url, json=payload, headers=headers,
                                        timeout=aiohttp.ClientTimeout(total=30)) as resp:
                    if resp.status == 200:
                        data = await resp.json()
                        if isinstance(data, list) and len(data) > 0:
                            await _update_status("available")
                            return {"success": True, "message": f"Rerank 接口连接成功，得分: {data}"}
                        if data.get("results") and len(data["results"]) > 0:
                            await _update_status("available")
                            return {"success": True, "message": "Rerank 接口连接成功"}
                        await _update_status("available")
                        return {"success": True, "message": "Rerank API 响应正常"}
                    error_body = await resp.text()
                    await _update_status("unavailable", f"HTTP {resp.status}: {error_body[:200]}")
                    return {"success": False, "message": f"HTTP {resp.status}: {error_body[:200]}"}
        except Exception as e:
            err_msg = str(e)[:200]
            await _update_status("unavailable", err_msg)
            return {"success": False, "message": f"连接异常: {err_msg}"}

    # 默认 LLM 测试
    try:
        async with aiohttp.ClientSession() as session:
            headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
            payload = {
                "model": model_name,
                "messages": [{"role": "user", "content": "hi"}],
                "max_tokens": 5,
            }
            async with session.post(f"{api_base}/chat/completions", json=payload, headers=headers,
                                    timeout=aiohttp.ClientTimeout(total=30)) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    if data.get("choices") and len(data["choices"]) > 0:
                        await _update_status("available")
                        return {"success": True, "message": "连接成功，模型可用"}
                    await _update_status("available")
                    return {"success": True, "message": "API响应正常（未返回choices，可能模型名有误）"}
                error_body = await resp.text()
                await _update_status("unavailable", f"HTTP {resp.status}: {error_body[:200]}")
                return {"success": False, "message": f"HTTP {resp.status}: {error_body[:200]}"}
    except Exception as e:
        err_msg = str(e)[:200]
        await _update_status("unavailable", err_msg)
        return {"success": False, "message": f"连接异常: {err_msg}"}


# ---- 2.3 场景配置 API ----

@router.get("/scenes")
async def list_scenes(request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    try:
        await cur.execute(
            """SELECT s.*,
               (SELECT COUNT(*) FROM ai_scene_model_configs sc WHERE sc.scene_key = s.scene_key) as model_count
               FROM ai_model_scenes s
               ORDER BY s.sort_order ASC"""
        )
        rows = await cur.fetchall()
        return {"scenes": [dict(r) for r in rows]}
    except Exception:
        return {"scenes": []}


@router.get("/scenes/{scene_key}/config")
async def get_scene_config(scene_key: str, request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    try:
        await cur.execute("SELECT * FROM ai_model_scenes WHERE scene_key = %s", (scene_key,))
        scene_row = await cur.fetchone()
        if not scene_row:
            return {"error": "场景不存在"}
        scene = dict(scene_row)

        await cur.execute(
            """SELECT sc.*, m.model_name, m.display_name as model_display_name,
               m.provider_name, m.status as model_status, m.api_base,
               m.supports_thinking, m.supports_reasoning_effort, m.supports_vision
               FROM ai_scene_model_configs sc
               JOIN ai_models m ON sc.model_id = m.id
               WHERE sc.scene_key = %s
               ORDER BY sc.priority ASC""",
            (scene_key,),
        )
        models = [dict(r) for r in await cur.fetchall()]
        scene["models"] = models
        return scene
    except Exception:
        return {"error": "获取场景配置失败"}


@router.put("/scenes/{scene_key}/config")
async def save_scene_config(scene_key: str, request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    body = await safe_json(request)
    models = body.get("models", [])

    try:
        await cur.execute("SELECT scene_key FROM ai_model_scenes WHERE scene_key = %s", (scene_key,))
        if not await cur.fetchone():
            return {"error": "场景不存在"}

        await cur.execute("DELETE FROM ai_scene_model_configs WHERE scene_key = %s", (scene_key,))

        for m in models:
            await cur.execute(
                """INSERT INTO ai_scene_model_configs (scene_key, model_id, priority, mode, config_json, enabled)
                   VALUES (%s, %s, %s, %s, %s, 1)""",
                (
                    scene_key,
                    m.get("model_id"),
                    m.get("priority", 0),
                    m.get("mode", "fast"),
                    json.dumps(m.get("config_json")) if m.get("config_json") else None,
                ),
            )

        await cur.execute(
            """SELECT sc.*, m.model_name, m.display_name as model_display_name,
               m.provider_name, m.status as model_status
               FROM ai_scene_model_configs sc
               JOIN ai_models m ON sc.model_id = m.id
               WHERE sc.scene_key = %s
               ORDER BY sc.priority ASC""",
            (scene_key,),
        )
        models_result = [dict(r) for r in await cur.fetchall()]
        return {"scene_key": scene_key, "models": models_result, "saved": True}
    except Exception as e:
        return {"error": f"保存场景配置失败: {str(e)}"}


# ---- 2.4 模型代理服务（供 DeerFlow 调用） ----

@router.get("/model-service/resolve/{scene_key}")
async def resolve_scene_model(scene_key: str, request: Request, db=Depends(_system_db)):
    """供 DeerFlow 调用，按场景解析模型配置（支持无鉴权内部调用）"""
    conn, cur = db
    try:
        # 检查开关状态
        await cur.execute(
            "SELECT config_value FROM system_configs WHERE config_key = 'model_selection_source'"
        )
        row = await cur.fetchone()
        source = dict(row)["config_value"] if row else "config_yaml"

        if source != "bff_api":
            return {
                "scene_key": scene_key,
                "models": [],
                "source": "config_yaml",
                "message": "当前使用 config.yaml 静态配置，请在管理后台开启 BFF API 动态选择"
            }

        await cur.execute(
            """SELECT sc.*, sc.config_json, m.model_name, m.api_base, m.api_key,
               m.provider_name, m.supports_thinking, m.supports_reasoning_effort,
               m.supports_vision, m.thinking_default_on
               FROM ai_scene_model_configs sc
               JOIN ai_models m ON sc.model_id = m.id
               WHERE sc.scene_key = %s AND m.enabled = 1
               ORDER BY sc.priority ASC""",
            (scene_key,),
        )
        models = []
        for r in await cur.fetchall():
            d = dict(r)
            api_key_enc = d.get("api_key", "")
            d["api_key_decrypted"] = _decrypt_value(api_key_enc) if api_key_enc else ""

            base_url = d.get("api_base") or ""
            d["effective_api_base"] = base_url

            config_raw = d.get("config_json")
            if config_raw:
                import json as _json
                if isinstance(config_raw, str):
                    config_raw = _json.loads(config_raw)
                d["reasoning_params"] = config_raw
            else:
                d["reasoning_params"] = {}

            models.append(d)
        return {"scene_key": scene_key, "models": models, "source": "bff_api"}
    except Exception:
        return {"scene_key": scene_key, "models": [], "source": "error"}


# ---- 阶段六：DeerFlow config.yaml 同步 ----

DEERFLOW_CONFIG_PATH = os.environ.get("DEERFLOW_CONFIG_PATH", os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), "deer-flow", "config.yaml"))

PROVIDER_USE_DEFAULTS = {
    "deepseek": "deerflow.models.patched_deepseek:PatchedChatDeepSeek",
    "volcano": "deerflow.models.patched_deepseek:PatchedChatDeepSeek",
    "minimax": "deerflow.models.patched_deepseek:PatchedChatDeepSeek",
    "kimi": "langchain_openai:ChatOpenAI",
    "qwen": "langchain_openai:ChatOpenAI",
    "siliconflow": "langchain_openai:ChatOpenAI",
    "baidu": "langchain_openai:ChatOpenAI",
    "tencent": "langchain_openai:ChatOpenAI",
    "glm": "langchain_community.chat_models:ChatZhipuAI",
}

@router.get("/sync-deerflow-config")
async def sync_deerflow_config(request: Request, db=Depends(_system_db)):
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}

    conn, cur = db
    try:
        await cur.execute(
            """SELECT * FROM ai_models
               WHERE enabled = 1 AND status IN ('available', 'untested')
               ORDER BY provider_name, model_name"""
        )
        rows = await cur.fetchall()

        model_entries = []
        for r in rows:
            m = dict(r)
            api_base = (m.get("api_base") or "").strip()
            provider_name = m.get("provider_name", "").lower()

            # Determine langchain class based on provider name
            if "zhipu" in provider_name or "glm" in provider_name:
                use_class = "langchain_community.chat_models:ChatZhipuAI"
            elif "deepseek" in provider_name:
                use_class = "deerflow.models.patched_deepseek:PatchedChatDeepSeek"
            else:
                use_class = "langchain_openai:ChatOpenAI"

            entry = {
                "name": m.get("model_name", ""),
                "display_name": m.get("display_name", "") or m.get("model_name", ""),
                "use": use_class,
                "model": m.get("model_name", ""),
                "api_base": api_base,
                "api_key": f"${provider_name.upper()}_API_KEY",
                "supports_thinking": bool(m.get("supports_thinking")),
                "supports_vision": bool(m.get("supports_vision")),
                "supports_reasoning_effort": bool(m.get("supports_reasoning_effort")),
                "max_tokens": 8192,
            }
            if bool(m.get("supports_thinking")):
                entry["when_thinking_enabled"] = {
                    "extra_body": {"thinking": {"type": "enabled"}}
                }
            model_entries.append(entry)

        # Read existing config
        existing_config = {}
        if os.path.exists(DEERFLOW_CONFIG_PATH):
            with open(DEERFLOW_CONFIG_PATH, "r", encoding="utf-8") as f:
                existing_config = yaml.safe_load(f) or {}

        existing_config["models"] = model_entries

        with open(DEERFLOW_CONFIG_PATH, "w", encoding="utf-8") as f:
            yaml.dump(existing_config, f, allow_unicode=True, default_flow_style=False, sort_keys=False)

        return {"success": True, "models_synced": len(model_entries), "config_path": DEERFLOW_CONFIG_PATH}
    except Exception as e:
        return {"error": f"同步失败: {str(e)}"}



# ---- 2.4 知识图谱实体提取 API ----

@router.post("/extract-entities")
async def extract_entities_with_llm(
    request: Request,
    db=Depends(_system_db)
):
    """
    使用entity_extract场景配置的LLM模型提取实体和关系
    支持多模型并行（按priority排序）
    """
    current_user = get_current_user(request)
    if not current_user or not is_admin_role(current_user["role"]):
        return {"error": "无权限", "detail": "admin required"}
    
    try:
        body = await safe_json(request)
    except:
        return {"error": "请求体格式错误"}
    
    title = body.get("title", "")
    content = body.get("content", "")
    scene_key = body.get("scene_key", "entity_extract")
    max_models = body.get("max_models", 2)
    
    if not content:
        return {"error": "content字段必填"}
    
    conn, cur = db
    
    await cur.execute(
        """SELECT sc.model_id, sc.priority, sc.mode,
           m.model_name, m.display_name, m.provider_name, m.api_base, m.api_key
           FROM ai_scene_model_configs sc
           JOIN ai_models m ON sc.model_id = m.id
           WHERE sc.scene_key = %s AND sc.enabled = 1 AND m.enabled = 1
           ORDER BY sc.priority ASC
           LIMIT %s""",
        (scene_key, max_models)
    )
    model_rows = await cur.fetchall()
    
    if not model_rows:
        return {"error": f"场景 {scene_key} 没有可用的模型配置"}
    
    truncated_content = content[:4000] if len(content) > 4000 else content

    prompt = f"""请从以下法规内容中提取实体和关系，返回严格的JSON格式。

法规名称：{title}
法规内容：
{truncated_content}

请返回以下格式的JSON：
{{
  "entities": [
    {{"name": "实体名", "type": "实体类型", "properties": {{}}}}
  ],
  "relationships": [
    {{"source": "源实体名", "target": "目标实体名", "type": "关系类型", "properties": {{}}}}
  ]
}}

实体类型说明：
- Law: 法规名称，格式为《法规全称》（如《中华人民共和国食品安全法》《中华人民共和国行政处罚法》）
- Article: 具体条款，命名格式必须为"法规简称 第X条"（如"食品安全法 第三十二条"、"行政处罚法 第五条"），绝对不能只写"第三十二条"或"第5条"，因为不同法规有相同编号的条款
- Concept: 法律概念（如食品安全追溯、行政许可、裁量基准、不罚情形、减轻处罚）
- Violation: 具体违法行为描述（如销售过期食品、无证经营、虚假宣传），不能只写"违法行为"或"违规行为"等泛化描述
- Penalty: 具体处罚措施（如罚款五万元以上十万元以下、吊销许可证、没收违法所得）
- Organization: 执法机构全称（如国家市场监督管理总局、某市市场监督管理局）
- Case: 典型案例名称
- DiscretionStandard: 裁量基准（如食品安全法轻微情节处罚标准）

关系类型说明：
- CONTAINS: 法规A包含条款B（Law → Article）
- CITES: 法规A引用了法规B（Law → Law）
- REGULATES: 法规A规制了概念/行为B（Law → Concept/Violation）
- VIOLATES: 行为B违反了法规A（Violation/Case → Law）
- BASED_ON: 案例B基于法规A（Case/Concept → Law）
- REFERENCES: 条款B引用了概念A（Article → Concept）
- DEFINES: 法规A定义了概念B（Law/Article → Concept）
- PROHIBITS: 法规A禁止行为B（Law → Violation）
- PENALIZES: 法规A处罚措施B（Law/Article → Penalty）
- APPLIES_TO: 法规A适用于对象B（Law → Organization/Concept）

提取要求：
1. Article实体的name必须包含法规简称前缀，格式为"法规简称 第X条"
2. Violation实体必须是具体的违法行为描述，不能是泛化词汇
3. 关系方向必须正确：源节点为主动方/上位概念，目标节点为被动方/下位概念
4. 特别关注：违法行为与对应法规的关系、处罚依据链、相关概念的关联
5. 每个实体的name必须全局唯一，避免不同法规的同名条款合并
6. 只返回JSON，不要其他内容。"""

    results = []
    
    for model_row in model_rows:
        model = dict(model_row)
        model_id = model["model_id"]
        model_name = model["model_name"]
        display_name = model.get("display_name") or model_name
        api_base = (model.get("api_base") or "").rstrip("/")
        encrypted_key = model.get("api_key", "")
        
        api_key = _decrypt_value(encrypted_key) if encrypted_key else ""
        
        if not api_key or not api_base:
            results.append({
                "model_id": model_id,
                "model_name": model_name,
                "display_name": display_name,
                "success": False,
                "error": "API Key或API Base未配置"
            })
            continue
        
        for suffix in ["/chat/completions", "/v1/chat/completions"]:
            if api_base.endswith(suffix):
                api_base = api_base[:-len(suffix)]
                break
        
        url = f"{api_base}/chat/completions"
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
        payload = {
            "model": model_name,
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 0.3,
            "max_tokens": 4096
        }
        
        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(url, headers=headers, json=payload,
                                       timeout=aiohttp.ClientTimeout(total=90)) as resp:
                    
                    if resp.status == 200:
                        data = await resp.json()
                        
                        if data.get("choices") and len(data["choices"]) > 0:
                            response_text = data["choices"][0].get("message", {}).get("content", "")
                            
                            import re
                            json_match = re.search(r'\{[\s\S]*\}', response_text)
                            
                            if json_match:
                                extracted = json.loads(json_match.group())
                                
                                entities = []
                                for e in extracted.get("entities", []):
                                    if e.get("name"):
                                        entities.append({
                                            "name": e["name"],
                                            "type": e.get("type", "Concept"),
                                            "properties": e.get("properties", {})
                                        })
                                
                                relations = []
                                for r in extracted.get("relations", []):
                                    if r.get("source") and r.get("target"):
                                        relations.append({
                                            "source": r["source"],
                                            "target": r["target"],
                                            "type": r.get("type", "RELATES_TO"),
                                            "properties": r.get("properties", {})
                                        })
                                
                                results.append({
                                    "model_id": model_id,
                                    "model_name": model_name,
                                    "display_name": display_name,
                                    "success": True,
                                    "entities": entities,
                                    "relations": relations,
                                    "entity_count": len(entities),
                                    "relation_count": len(relations)
                                })
                                
                                await cur.execute(
                                    "UPDATE ai_models SET status = 'available', last_test_at = NOW() WHERE id = %s",
                                    (model_id,)
                                )
                                
                                break
                            else:
                                results.append({
                                    "model_id": model_id,
                                    "model_name": model_name,
                                    "display_name": display_name,
                                    "success": False,
                                    "error": "LLM返回格式无效"
                                })
                        else:
                            results.append({
                                "model_id": model_id,
                                "model_name": model_name,
                                "display_name": display_name,
                                "success": False,
                                "error": "LLM未返回有效内容"
                            })
                    else:
                        error_text = await resp.text()
                        results.append({
                            "model_id": model_id,
                            "model_name": model_name,
                            "display_name": display_name,
                            "success": False,
                            "error": f"HTTP {resp.status}: {error_text[:200]}"
                        })
                        
                        await cur.execute(
                            "UPDATE ai_models SET status = 'unavailable', last_test_at = NOW(), last_test_error = %s WHERE id = %s",
                            (error_text[:200], model_id)
                        )
                        
        except asyncio.TimeoutError:
            results.append({
                "model_id": model_id,
                "model_name": model_name,
                "display_name": display_name,
                "success": False,
                "error": "请求超时"
            })
        except Exception as e:
            results.append({
                "model_id": model_id,
                "model_name": model_name,
                "display_name": display_name,
                "success": False,
                "error": str(e)[:200]
            })
    
    best_result = None
    max_score = 0
    
    for r in results:
        if r.get("success"):
            score = r.get("entity_count", 0) + r.get("relation_count", 0) * 0.5
            if score > max_score:
                max_score = score
                best_result = r
    
    if best_result:
        return {
            "success": True,
            "title": title,
            "source": "llm",
            "scene": scene_key,
            "model": best_result["model_name"],
            "display_name": best_result["display_name"],
            "entities": best_result["entities"],
            "relations": best_result["relations"],
            "entity_count": best_result["entity_count"],
            "relation_count": best_result["relation_count"],
            "all_attempts": results
        }
    else:
        return {
            "success": False,
            "title": title,
            "error": "所有模型提取失败",
            "all_attempts": results
        }
