# -*- coding: utf-8 -*-
"""M12 知识库检索纯函数。

不依赖 fastmcp，可直接 import 单测。数据来自 data/guidelines.json 与 data/sops.json。
核心：按调用方身份切三套语料视图（full/popular/child），所有结果强制带 source 出处。
身份由部署注入（A207_CALLER），语料 profile 取自 a207_policy.knowledge_profile —— 模型
不能自选语料视图（P0-1：否则患儿伙伴可自称医生拿到全量专业语料）。
"""
from __future__ import annotations

import json
import os
from typing import Any, Dict, List, Optional

from a207_policy import enforce_read, get_caller, knowledge_profile

# 本包在权限矩阵中的登记名（enforce_* 查表键，唯一事实源在 a207_policy.matrix）。
MCP_NAME = "a207-knowledge-mcp"

# a207_policy 的 profile 名 → 本包语料字段名（data/guidelines.json 的条目键）。
# 角色→profile 的判定在 policy，本表只做「profile → 语料字段」的数据映射。
_PROFILE_VIEW = {
    "full": "full",
    "plain_language": "popular",
    "child_safe": "child",
}
# 未登记 profile 一律降级到最受限语料（fail-closed，宁可少给不可多给）。
_FALLBACK_VIEW = "child"
# SOP 缺 child 字段时的回退文案（fail-closed：绝不把完整临床处置下发给患儿）。
_CHILD_SOP_FALLBACK = "该 SOP 仅向临床角色提供；如有不适请告知家长或医生。"

_GUIDE_PATH = os.path.join(os.path.dirname(__file__), "data", "guidelines.json")
_SOP_PATH = os.path.join(os.path.dirname(__file__), "data", "sops.json")
_GUIDES: Optional[Dict[str, Any]] = None
_SOPS: Optional[Dict[str, Any]] = None


def _load_guides() -> Dict[str, Any]:
    global _GUIDES
    if _GUIDES is None:
        with open(_GUIDE_PATH, "r", encoding="utf-8") as f:
            data = json.load(f)
        # OD-014（P2-4）：加载时校验语料视图字段完整性——缺任一视图字段直接报错，
        # 而不是运行时回退到 full（fail-closed：宁可少给不可多给）。
        for e in data["entries"]:
            missing = [k for k in ("full", "popular", "child")
                       if not str(e.get(k) or "").strip()]
            if missing:
                raise ValueError(
                    f"指南条目 {e.get('id', '?')} 缺少语料视图字段 {missing}，"
                    f"拒绝加载（fail-closed：防止家长/患儿回退到 full 临床语料）")
        _GUIDES = data
    return _GUIDES


def _load_sops() -> Dict[str, Any]:
    global _SOPS
    if _SOPS is None:
        with open(_SOP_PATH, "r", encoding="utf-8") as f:
            _SOPS = json.load(f)
    return _SOPS


def _view_for_caller(caller: str) -> str:
    """按调用方身份取语料视图：profile 由 a207_policy 判定，本包只做字段映射。"""
    return _PROFILE_VIEW.get(knowledge_profile(caller), _FALLBACK_VIEW)


def _match(text: str, query: str) -> bool:
    q = query.lower()
    return q in text.lower()


def search_guideline(query: str, guideline_set: Optional[str] = None,
                     caller: Optional[str] = None) -> Dict[str, Any]:
    """检索指南/共识条文。按调用方身份切语料视图；所有结果带 source 出处。

    语料视图由部署注入的身份决定（doctor_assistant/nutritionist/risk_warning=全量 full；
    parent_assistant=popular 通俗；child_companion=child 科普），调用方不可自选。
    guideline_set: 可选过滤 KDIGO2024/PRNT2020/China2023/Growth2025。
    """
    caller = caller or get_caller()
    enforce_read(MCP_NAME)
    guides = _load_guides()
    view = _view_for_caller(caller)
    out = []
    for e in guides["entries"]:
        if guideline_set and e.get("set") != guideline_set:
            continue
        hay = " ".join([e["title"], e["tags"].__str__(), e["full"], e.get("popular", ""), e.get("child", "")])
        if not _match(hay, query):
            continue
        out.append({
            "id": e["id"],
            "title": e["title"],
            "set": e["set"],
            "strength": e["strength"],
            "evidence": e["evidence"],
            # OD-014（P2-4）：视图缺失不再回退 full（fail-closed）。
            # _load_guides 已保证 full/popular/child 齐全，此处仅防御性取 ""。
            "text": e.get(view, ""),
            "source": e["source"],
        })
    return {"query": query, "role": caller, "view": view, "count": len(out), "results": out}


def search_sop(query: str, caller: Optional[str] = None) -> Dict[str, Any]:
    """检索院内 SOP。按调用方身份切语料视图：患儿返回安全摘要，不暴露完整临床处置（MX-1）。

    语料视图由部署注入的身份决定（与 search_guideline 一致）；child 视图下仅返回 child 安全版，
    缺字段时回退到 _CHILD_SOP_FALLBACK，绝不把完整临床处置下发给患儿。
    """
    caller = caller or get_caller()
    enforce_read(MCP_NAME)
    view = _view_for_caller(caller)
    sops = _load_sops()
    out = []
    for s in sops["sops"]:
        hay = " ".join([s["title"], s["tags"].__str__(), s.get("child", ""), s["content"]])
        if _match(hay, query):
            if view == "child":
                content = s.get("child", _CHILD_SOP_FALLBACK)
            else:
                content = s["content"]
            out.append({
                "id": s["id"],
                "title": s["title"],
                "content": content,
                "source": s["source"],
                "view": view,
            })
    return {"query": query, "count": len(out), "results": out}


def get_citation(ref_id: str, caller: Optional[str] = None) -> Dict[str, Any]:
    """生成规范引用串。支持指南条目 id 与 SOP id。"""
    caller = caller or get_caller()
    enforce_read(MCP_NAME)
    guides = _load_guides()
    for e in guides["entries"]:
        if e["id"] == ref_id:
            citation = (f"[{e['id']}] {e['title']}. {e['source']} "
                        f"（推荐强度：{e['strength']}；证据级别：{e['evidence']}）")
            return {"ref_id": ref_id, "citation": citation, "source": e["source"],
                    "strength": e["strength"], "evidence": e["evidence"]}
    sops = _load_sops()
    for s in sops["sops"]:
        if s["id"] == ref_id:
            citation = f"[{s['id']}] {s['title']}. {s['source']}"
            return {"ref_id": ref_id, "citation": citation, "source": s["source"]}
    return {"ref_id": ref_id, "citation": None, "error": "未找到该引用 ID"}


def _self_test_refs() -> List[str]:
    """返回所有可被 get_citation 解析的 id（供校验出处完整性）。"""
    guides = _load_guides()
    sops = _load_sops()
    return [e["id"] for e in guides["entries"]] + [s["id"] for s in sops["sops"]]
