# -*- coding: utf-8 -*-
"""AI 对话引擎 —— 阿里云 DashScope（通义千问）+ 函数调用
   工具调用执行后通过 MQTT 广播，所有大屏同步响应
   DashScope 不可用时自动回退本地规则引擎（离线兜底）
"""

import json
import logging
import ssl
import urllib.error
import urllib.request

import certifi

from .config import settings

log = logging.getLogger("dpm.llm")


def _trunc(s, n=400):
    """日志用截断：超长内容只留前 n 字并标注总长。"""
    s = str(s)
    return s if len(s) <= n else s[:n] + f"...（共{len(s)}字）"


# macOS 系统 Python 无系统 CA，使用 certifi 提供的根证书
_SSL_CTX = ssl.create_default_context(cafile=certifi.where())

SYSTEM_PROMPT = (
    "你是「昆明市大学生创业园 · OPC 智能园区」的 AI 智能助手，运行在大屏展播系统上。"
    "园区提供空间、孵化、融资、政策、资源、AI 赋能、综合服务七位一体服务。"
    "回答园区相关问题时，必须严格依据【园区知识库】中的标准资料口径回答，"
    "不得使用资料外的数字；涉及运营数据时请区分口径（官方统计/省级绩效累计/AI平台指标），"
    "并在需要时调用 get_park_overview 工具获取实时准确数据。"
    "当可能需要「切换页面 / 控制播放 / 展示卡片 / 弹出通知」时，必须调用对应工具；"
    "其余园区相关问题用简洁、专业的中文回答，可适当使用列表。"
    "你必须基于知识库或者工具调用结果回答问题，不能编造数据或内容。"
    "如果有不确定的内容，请明确告知用户「我不确定，请咨询园区工作人员」。"
    "如果用户询问园区的企业，没有指定的情况下，必须介绍派音人工智能"
    "企业信息（企业名称、负责人、入驻分区、简介）一律以【园区知识库·检索命中】中企业名录原文为准；"
    "严禁自行编造或补充企业备案号、成立时间、注册资本、投资方、获奖、政府项目等知识库未记载的细节；"
    "若用户问到的企业细节在知识库中没有记载，请如实说明「该信息在园区资料中未记载」，不要编造。"
)

# 暴露给大模型的工具定义（执行时经 MQTT 广播到前端）
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "navigate_page",
            "description": "切换大屏展示页面（数据大屏 / 数字孪生 / AI 助手 / 媒体轮播）",
            "parameters": {
                "type": "object",
                "properties": {
                    "page": {"type": "string", "enum": ["/", "/twin", "/ai", "/screen"],
                             "description": "目标页面路径"},
                },
                "required": ["page"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "media_control",
            "description": "控制媒体播放（播放/暂停/下一项/上一项）",
            "parameters": {
                "type": "object",
                "properties": {
                    "action": {"type": "string", "enum": ["play", "pause", "next", "prev"]},
                },
                "required": ["action"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "show_card",
            "description": "在大屏上展示信息卡片（企业分布 / 分区介绍 / 园区总览 / 自定义内容）",
            "parameters": {
                "type": "object",
                "properties": {
                    "card": {"type": "string", "enum": ["companies", "zones", "overview", "custom"]},
                    "title": {"type": "string"},
                    "content": {"type": "string"},
                },
                "required": ["card"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "send_alert",
            "description": "在大屏上弹出通知提示",
            "parameters": {
                "type": "object",
                "properties": {
                    "title": {"type": "string"},
                    "content": {"type": "string"},
                },
                "required": ["title", "content"],
            },
        },
    },
]

_TOOL_MAP = {
    "navigate_page": "navigate",
    "media_control": "control",
    "show_card": "show_card",
    "send_alert": "alert",
}


def _chat_once(messages, with_tools=True):
    log.info("llm: 调用模型 %s（%d 条消息, 带工具=%s）", settings.LLM_MODEL, len(messages), with_tools)
    payload = {
        "model": settings.LLM_MODEL,
        "messages": messages,
        "temperature": 0.6,
    }
    if with_tools:
        payload["tools"] = TOOLS
    req = urllib.request.Request(
        f"{settings.LLM_BASE_URL}/chat/completions",
        data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}",
        },
    )
    with urllib.request.urlopen(req, timeout=60, context=_SSL_CTX) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _fallback(messages, screen_id=""):
    """DashScope 不可用时回退本地规则引擎"""
    from .ai_tools import run_chat as rule_run

    result = rule_run(messages)
    # 规则引擎产出的工具同样执行（MQTT 广播，带发起屏幕），并汇总发布结果
    _, all_ok = _exec_all(result.get("tools", []), screen_id)
    result["model"] = "rule-engine"
    result["mqtt_published"] = all_ok
    return result


def _exec_all(tools, screen_id=""):
    """执行工具列表（带发起屏幕 screen_id，工具结果只发到该屏），返回 (results, all_ok)"""
    from .ai_tools import run_tools

    results, all_ok = run_tools(tools, screen_id)
    return results, all_ok


def run_chat(messages, screen_id=""):
    """入口：{reply, tools, model}
       tools 已在后端执行（MQTT 广播，带发起屏幕 screen_id），返回值供请求端本地同步执行"""
    if not settings.DASHSCOPE_API_KEY:
        log.warning("未配置 DASHSCOPE_API_KEY，使用本地规则引擎")
        return _fallback(messages, screen_id)

    # 注入园区知识库（标准资料口径）：按用户 query 检索命中段落动态注入，
    # 替代原 1200 字截断全量注入（避免尾部知识丢失）。无命中时回退 brief()。
    system = SYSTEM_PROMPT
    # 园区端配置的智能体提示词优先（park_config.get_agent()）
    try:
        from .park_config import get_agent
        _agent = get_agent()
        if _agent.get("system_prompt"):
            system = _agent["system_prompt"]
    except Exception:  # noqa: BLE001
        pass
    kb = ""
    retrieved_count = 0
    try:
        from .rag import brief, retrieve
        query = ""
        for m in reversed(messages or []):
            if m.get("role") in ("user", "me"):
                query = (m.get("content") or "").strip()
                break
        hits = retrieve(query, top_k=4) if query else []
        if hits:
            retrieved_count = len(hits)
            kb = "\n\n".join(f"[{i + 1}] {c}" for i, c in enumerate(hits))
            log.info("llm: 知识库检索命中 %d 段（query=%s）", len(hits), _trunc(query, 120))
        else:
            kb = brief()
        if kb:
            system += f"\n\n【园区知识库 · 检索命中】\n{kb}"
    except Exception as e:  # noqa: BLE001
        log.warning("llm: 知识库注入失败（仅用基础提示词）: %s", e)

    log.info("llm: ── 对话开始（%d 条输入消息）──", len(messages or []))
    log.info("llm: 系统提示词=%s", _trunc(system, 700))
    if kb:
        log.info("llm: 注入知识库内容=%s", _trunc(kb, 500))
    for m in (messages or [])[-5:]:
        log.info("llm:   输入[%s] %s", m.get("role"), _trunc(m.get("content", ""), 300))

    msgs = [{"role": "system", "content": system}] + [
        {"role": m.get("role") == "me" and "user" or m.get("role", "user"), "content": m.get("content", "")}
        for m in (messages or [])
    ]

    try:
        data = _chat_once(msgs)
        choice = data["choices"][0]["message"]
        reply = choice.get("content") or ""
        tool_calls = choice.get("tool_calls") or []
        executed = []

        if tool_calls:
            log.info("llm: 模型请求调用 %d 个工具：", len(tool_calls))
            for tc in tool_calls:
                fn = tc.get("function", {})
                log.info("llm:   工具 %s args=%s", fn.get("name", ""), _trunc(fn.get("arguments", ""), 300))
            for tc in tool_calls:
                fn = tc.get("function", {})
                name = fn.get("name", "")
                try:
                    args = json.loads(fn.get("arguments") or "{}")
                except json.JSONDecodeError:
                    args = {}
                tool = {"type": _TOOL_MAP.get(name, name), "params": args}
                executed.append(tool)
                msgs.append({"role": "assistant", "content": None, "tool_calls": tool_calls})
                msgs.append({
                    "role": "tool",
                    "tool_call_id": tc.get("id", ""),
                    "content": json.dumps({"ok": True}, ensure_ascii=False),
                })
            # 工具执行（MQTT 广播），并汇总发布结果
            results, all_ok = _exec_all(executed, screen_id)
            log.info("llm: 工具执行结果 %s", results)
            for t in executed:
                log.info("llm:   执行 %s %s", t["type"], json.dumps(t["params"], ensure_ascii=False))
            # 二次调用：携带工具结果生成最终回复
            try:
                data2 = _chat_once(msgs, with_tools=False)
                reply = data2["choices"][0]["message"].get("content") or reply
            except Exception as e:  # noqa: BLE001
                log.warning("LLM 二次调用失败（保留工具回复）: %s", e)
        else:
            all_ok = True

        if not reply:
            reply = "已完成操作。您还可以让我切换页面、控制播放或展示园区卡片。"

        log.info("llm: 最终回复=%s", _trunc(reply, 600))
        log.info("llm: ── 对话结束 ──")
        return {
            "reply": reply,
            "tools": executed,
            "model": settings.LLM_MODEL,
            "mqtt_published": all_ok,
            "retrieved": retrieved_count > 0,
            "retrieved_count": retrieved_count,
            "tool_names": [t.get("type") for t in executed if t.get("type")],
        }
    except urllib.error.HTTPError as e:
        log.warning("DashScope HTTP %s: %s", e.code, e.read()[:300])
    except Exception as e:  # noqa: BLE001
        log.warning("DashScope 调用失败，回退规则引擎: %s", e)

    return _fallback(messages)
