"""知识库问答服务 — 调用 knowledge/ 轻量检索 + Ollama 生成答案"""
import logging

import httpx

from knowledge import (
    OLLAMA_MODEL,
    OLLAMA_URL,
    SITE_URL,
    get_cached_answer,
    search as kb_search,
    set_cached_answer,
)

logger = logging.getLogger("spam_guard")

KNOWLEDGE_PROMPT = """你是收割机（go-harvest）文档助手。请根据知识库内容，给用户一个准确、紧凑、可执行的中文回答。

相关文档内容：
{context_text}

用户问题：{query}

硬性要求（非常重要）：
1. 只能使用上方知识库内容回答。不要编造镜像名、环境变量、端口、命令、配置文件或链接。
2. 如果知识库里有代码块/YAML/env/bash 示例，涉及配置时必须原样引用或忠实摘录，不要自行重写完整配置，不要改写变量名。
3. 如果资料没有给出完整示例，不要自己补全完整示例；应说明“文档未给出完整示例”，再列出文档中已有的片段。
4. 对 Go Harvest 安装配置，若资料中出现 `newptools/go-harvest:latest`、`EMAIL`、`TOKEN`、`GO_WEB_PORT`、`POSTGRES_DB` 等，请保持这些原名，不要改成其他名字。
5. 不要把常识当成文档内容。可以说“文档没有说明”，不能猜。

回答格式要求：
1. 尽量控制在 Telegram 单条消息内，优先 1200 字以内，最多不要超过 3000 字。
2. 使用 Telegram 兼容 Markdown 排版：标题用 *粗体*，列表用 - 或 1.，命令/变量用反引号，配置示例用代码块。
3. 如果问题是“怎么做/如何配置/怎么安装/报错怎么办”，请使用紧凑结构：
   *结论*
   *步骤*
   *关键配置/命令*（仅限资料中出现的内容）
   *注意*
   *文档*
4. 不要展开无关背景，不要重复用户问题，不要写长篇解释。
5. 如果知识库信息不足，要明确说“文档里没有直接说明”，只列可确认部分。
6. 最后列出相关文档链接，最多 3 个。"""


def build_context(docs: list[dict], max_chars: int = 12000) -> str:
    """构建给 LLM 的上下文，保留来源、标题、分数。"""
    parts = []
    used = 0
    for i, d in enumerate(docs, 1):
        text = d.get("text", "").strip()
        if not text:
            continue
        block = (
            f"【资料 {i}】\n"
            f"标题：{d.get('title', '')}\n"
            f"来源：{d.get('source', '')}\n"
            f"链接：{d.get('url', '')}\n"
            f"相关度：{d.get('score', 0):.2f}\n"
            f"内容：\n{text}\n"
        )
        if used + len(block) > max_chars and parts:
            break
        parts.append(block)
        used += len(block)
    return "\n---\n".join(parts)


def extract_verbatim_snippets(query: str, docs: list[dict], limit: int = 2, max_chars: int = 4200) -> str:
    """配置/安装类问题优先展示知识库原文代码块，避免 LLM 重写配置时幻觉。"""
    q = query.lower()
    if not any(k in q for k in ["compose", "docker", "postgres", "postgresql", "配置", "安装", "部署", "环境变量"]):
        return ""

    snippets = []
    used = 0
    need_postgres = any(k in q for k in ["postgres", "postgresql", "pgsql"])

    for d in docs:
        text = d.get("text", "")
        text_lower = text.lower()
        if "[代码块]" not in text:
            continue
        # PostgreSQL 问题优先只展示 PostgreSQL 相关代码块，避免混入 SQLite 示例
        if need_postgres and "postgres" not in text_lower:
            continue
        # 优先 yaml/env/bash/text 配置块
        if not any(mark in text_lower for mark in ["```yaml", "```env", "```bash", "```text"]):
            continue
        block = text.replace("[代码块]", "").strip()
        rendered = (
            f"📌 文档原文配置片段（{d.get('title', '')}）\n"
            f"来源：{d.get('url', '')}\n\n"
            f"{block}"
        )
        if used + len(rendered) > max_chars and snippets:
            continue
        snippets.append(rendered)
        used += len(rendered)
        if len(snippets) >= limit:
            break

    if not snippets:
        return ""
    return "\n\n".join(snippets)


def build_source_text(docs: list[dict], limit: int = 5) -> str:
    seen = []
    for d in docs:
        url = d.get("url")
        title = d.get("doc_title") or d.get("title") or url
        if not url or url in [x[1] for x in seen]:
            continue
        seen.append((title, url))
        if len(seen) >= limit:
            break
    return "\n".join([f"📎 {title}: {url}" for title, url in seen])


async def ask_knowledge_base(query: str) -> str:
    """知识库问答"""
    cached = get_cached_answer(query)
    if cached:
        logger.info("📦 命中问答缓存")
        return cached

    # top_k 提高 + search 内部扩展相邻 chunk，让回答不再只基于碎片
    relevant = kb_search(query, top_k=8, expand=True, context_window=1)
    if not relevant:
        return f"❌ 未找到相关内容，请换个关键词试试\n\n文档: {SITE_URL}"

    context_text = build_context(relevant, max_chars=12000)
    prompt = KNOWLEDGE_PROMPT.format(context_text=context_text, query=query)

    try:
        import time as _time
        t0 = _time.time()
        logger.info(f"🤖 调用 LLM 生成回答... (模型: {OLLAMA_MODEL}, 上下文: {len(context_text)}字, 文档块: {len(relevant)})")
        async with httpx.AsyncClient(timeout=120, proxy=None) as client:
            resp = await client.post(
                f"{OLLAMA_URL}/api/chat",
                json={
                    "model": OLLAMA_MODEL,
                    "messages": [{"role": "user", "content": prompt}],
                    "stream": False,
                    "options": {
                        "num_predict": 3000,
                        "temperature": 0.25,
                        "top_p": 0.9,
                    },
                    "think": False,
                },
            )
            resp.raise_for_status()
            answer = resp.json()["message"]["content"].strip()
            elapsed = _time.time() - t0
            logger.info(f"✅ LLM 回答完成 (耗时: {elapsed:.1f}s, 长度: {len(answer)}字)")

            source_text = build_source_text(relevant, limit=5)
            verbatim = extract_verbatim_snippets(query, relevant, limit=2)
            body = f"{verbatim}\n\n---\n\n{answer}" if verbatim else answer
            full_answer = f"{body}\n\n📚 相关文档:\n{source_text}" if source_text else body

            set_cached_answer(query, full_answer)
            return full_answer

    except Exception as e:
        logger.error(f"❌ 知识库问答失败: {e}")
        return f"❌ 回答失败: {e}"
