# -*- coding: utf-8 -*-
"""M4 核心逻辑（纯函数，无 fastmcp 依赖，可单测）。

内容：
1. 随访记录时间线（get_followup_records）：随访计划摘要可见，原始医生备注仅医生/营养可见
2. 依从性评分（get_adherence_score / calc_adherence_score）：透明加权复合分
3. PEW 时间线 facade（get_pew_timeline）：数据归属 M3（ADR-007），M4 仅聚合读取
4. 随访计划（schedule_followup）：频率按 KDIGO 2024 儿科推荐

身份与权限（Plan A）：caller 由部署环境注入（a207_policy.get_caller，env A207_CALLER），
模型不得自证身份（P0-1）；放行集合取自 a207_policy，本包不维护副本（P1-1）；
运行时写库经 resolve_state_path 落到可写目录，不写安装目录（P1-3）。

数据来源（权威标尺）：
- KDIGO 2024 CKD Guideline, Kidney Int 2024;105(4S):S117-S314（Francis et al. JAMA Pediatr 2024 儿科要点）
- NICE NG203 Table 2（2021 发布，2024 复核）
- 儿科共识 Rec17（Scielo 初级保健儿科 2024，引 Furth 2018 / KDIGO 2012）
- PRNT 2025 人体测量频率（Pediatr Nephrol 40:69-84）
- 依从性测量：MMAS-8 为 CKD 最常用工具（Springer Med 系统综述 2024）；儿科 CKD 复合依从性评分无单一经验证量表（pubmed 24814533），故采用系统定义加权复合分。
"""
from __future__ import annotations

import json
import os
import uuid
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any

from a207_policy import (
    FOLLOWUP_CLINICIAN,
    FOLLOWUP_WRITE_ALLOWED,
    atomic_write_json,
    enforce_read,
    enforce_write,
    get_caller,
    resolve_state_path,
)

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

# 放行集合唯一事实源在 a207_policy.matrix（P1-1）；此处仅保留本地别名，下文逻辑零改动。
# 写权限（schedule_followup 收口）：仅医生/营养师/编排层
_WRITE_ALLOWED = FOLLOWUP_WRITE_ALLOWED
# 临床角色可见原始医生备注；患者/家属角色仅可见摘要
_CLINICIAN = FOLLOWUP_CLINICIAN

# ---------------------------------------------------------------------------
# KDIGO 2024 儿科随访频率（recommend_followup_interval）
# ---------------------------------------------------------------------------
_CITATION = (
    "KDIGO 2024 (Kidney Int 2024;105(4S):S117-S314); "
    "NICE NG203 Table 2 (2021, rev 2024); "
    "儿科共识 Rec17 (Scielo 2024, 引 Furth 2018); "
    "PRNT 2025 人体测量 CKD5D 每月 (Pediatr Nephrol 40:69-84)"
)
# 基础间隔（天）：儿科共识 Rec17 —— G1-2 每 1-2 次/年(180d)，G3-4 每 ≥3-4 次/年(90d)，
# G5 每 >4 次/年(60d)，G5D 每月(30d，PRNT 2025)
_BASE_INTERVAL_DAYS = {
    "G1": 180, "G2": 180, "G3a": 90, "G3b": 90, "G4": 90, "G5": 60, "G5D": 30,
}
# 白蛋白尿升级：A2 缩短 30 天，A3 缩短 60 天（下限 14 天）
_ALBUMINURIA_REDUCTION = {"A1": 0, "A2": 30, "A3": 60}
_MIN_INTERVAL = 14


# ---------------------------------------------------------------------------
# 存储助手
# ---------------------------------------------------------------------------
def store_path() -> Path:
    """可写写库路径（P1-3 修复：不再写只读安装目录）。

    - 若设 A207_FOLLOWUP_DATA_DIR（开发/测试态），落到该目录；
    - 否则经 a207_policy.resolve_state_path 落到 A207_DATA_DIR 或系统临时目录。
    """
    override = os.environ.get(DATA_DIR_ENV)
    if override:
        return Path(override) / STORE_FILENAME
    return resolve_state_path(STORE_FILENAME)


def _load_store() -> dict:
    try:
        with open(store_path(), "r", encoding="utf-8") as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return {}


def _save_store(store: dict) -> None:
    # OD-014（P2-3）：原子写，避免半写截断静默丢数据
    atomic_write_json(store_path(), store)


def _now_iso() -> str:
    return datetime.now().isoformat(timespec="seconds")


def _today() -> str:
    return date.today().isoformat()


def _add_days(iso_date: str, days: int) -> str:
    return (date.fromisoformat(iso_date) + timedelta(days=days)).isoformat()


def _short_id(prefix: str, patient_id: str) -> str:
    return f"{prefix}-{patient_id}-{uuid.uuid4().hex[:8]}"


def _visible_record(rec: dict, caller: str) -> dict:
    """患者/家属角色剔除原始医生备注（doctor_notes），仅留可见摘要。"""
    out = dict(rec)
    if caller not in _CLINICIAN:
        out.pop("doctor_notes", None)
    return out


def _visible_plan(plan: dict, caller: str) -> dict:
    out = dict(plan)
    if caller not in _CLINICIAN:
        out.pop("note_to_clinician", None)
    return out


# ---------------------------------------------------------------------------
# 1. KDIGO 2024 随访频率推荐
# ---------------------------------------------------------------------------
def recommend_followup_interval(ckd_stage: str, albuminuria_stage: str = "A1") -> dict[str, Any]:
    """按 KDIGO 2024 儿科推荐返回随访间隔（天）。

    :param ckd_stage: G1/G2/G3a/G3b/G4/G5/G5D
    :param albuminuria_stage: A1/A2/A3（白蛋白尿越重，随访越频）
    :return: {recommended_interval_days, basis, citation}
    """
    if ckd_stage not in _BASE_INTERVAL_DAYS:
        return {"ok": False, "error": "INVALID_INPUT",
                "detail": "ckd_stage 必须是 G1/G2/G3a/G3b/G4/G5/G5D"}
    base = _BASE_INTERVAL_DAYS[ckd_stage]
    red = _ALBUMINURIA_REDUCTION.get(albuminuria_stage, 0)
    interval = max(_MIN_INTERVAL, base - red)
    return {
        "ok": True,
        "ckd_stage": ckd_stage,
        "albuminuria_stage": albuminuria_stage,
        "recommended_interval_days": interval,
        "basis": "KDIGO 2024 儿科随访频率（NICE NG203 Table 2 + 儿科共识 Rec17 + PRNT 2025）",
        "citation": _CITATION,
    }


# ---------------------------------------------------------------------------
# 2. 随访计划（写，医生/营养师）
# ---------------------------------------------------------------------------
def schedule_followup(patient_id: str, ckd_stage: str, albuminuria_stage: str,
                      visit_type: str, anchor_date: str,
                      plan_summary: str = "", note_to_clinician: str = "") -> dict[str, Any]:
    """创建随访计划（写，MX 收口：仅医生/营养师/编排层）。频率来自 KDIGO 2024 推荐。

    :param visit_type: outpatient/phone/online/dialysis/nutrition_counsel
    :param anchor_date: 计划基准日期 YYYY-MM-DD（通常为本次就诊日）
    :param caller: 缺省取部署注入身份（A207_CALLER），模型不可自证（P0-1）
    :param plan_summary: 随访计划摘要（所有角色可见）
    :param note_to_clinician: 仅供医生/营养的备注（患者/家属不可见）
    """
    caller = get_caller()
    if caller not in _WRITE_ALLOWED:
        return {"ok": False, "error": "FORBIDDEN",
                "detail": "随访计划仅医生/营养师/编排层可创建（MX 写权收口）"}
    rec = recommend_followup_interval(ckd_stage, albuminuria_stage)
    if not rec["ok"]:
        return rec
    interval = rec["recommended_interval_days"]
    plan = {
        "plan_id": _short_id("FP", patient_id),
        "cadence": {
            "interval_days": interval,
            "anchor_date": anchor_date,
            "next_due_date": _add_days(anchor_date, interval),
            "basis": rec["basis"],
            "citation": rec["citation"],
        },
        "visit_type": visit_type,
        "status": "active",
        "plan_summary": plan_summary,
        "note_to_clinician": note_to_clinician,
        "created_by": caller,
        "created_at": _now_iso(),
    }
    store = _load_store()
    p = store.setdefault(patient_id, {"records": [], "plans": [], "adherence": []})
    p["plans"].append(plan)
    _save_store(store)
    # 写操作由临床角色发起，返回完整计划
    return {"ok": True, "plan": plan}


# ---------------------------------------------------------------------------
# 3. 随访记录时间线（读）
# ---------------------------------------------------------------------------
def get_followup_records(patient_id: str) -> dict[str, Any]:
    """读取某患者随访记录与计划（读，所有角色可读）。

    权限：临床角色（医生/营养/编排/风险）看到完整记录（含医生备注）；
    患者/家属角色（parent_assistant/child_companion）仅见摘要——原始医生备注被剔除。
    身份缺省取部署注入值（A207_CALLER），模型不可自证（P0-1）。
    """
    caller = get_caller()
    enforce_read(MCP_NAME)
    store = _load_store()
    p = store.get(patient_id)
    if not p:
        return {"ok": True, "patient_id": patient_id, "records": [], "plans": [],
                "message": "无随访数据", "visibility": "summary_only"}
    records = [_visible_record(r, caller) for r in p["records"]]
    plans = [_visible_plan(pl, caller) for pl in p["plans"]]
    return {
        "ok": True,
        "patient_id": patient_id,
        "records": records,
        "plans": plans,
        "visibility": "full" if caller in _CLINICIAN else "summary_only",
    }


def add_followup_record(patient_id: str, visit_date: str, visit_type: str, ckd_stage: str,
                        indicators_snapshot: dict, plan_summary: str, doctor_notes: str = "") -> dict[str, Any]:
    """追加一条随访记录（内部助手，供编排层/医生写入；不在 MCP 工具直接暴露，由 router 调 server 封装）。"""
    caller = get_caller()
    # OD-014：写路径统一走矩阵收口（与 schedule_followup 一致：仅医生/营养师/编排层）
    if caller not in _WRITE_ALLOWED:
        return {"ok": False, "error": "FORBIDDEN",
                "detail": "随访记录仅医生/营养师/编排层可写入（MX 写权收口）"}
    rec = {
        "record_id": _short_id("FR", patient_id),
        "visit_date": visit_date,
        "visit_type": visit_type,
        "ckd_stage": ckd_stage,
        "indicators_snapshot": indicators_snapshot,
        "plan_summary": plan_summary,
        "doctor_notes": doctor_notes,
        "created_by": caller,
        "created_at": _now_iso(),
    }
    store = _load_store()
    p = store.setdefault(patient_id, {"records": [], "plans": [], "adherence": []})
    p["records"].append(rec)
    _save_store(store)
    return {"ok": True, "record": rec}


# ---------------------------------------------------------------------------
# 4. 依从性评分（透明加权复合分）
# ---------------------------------------------------------------------------
def calc_adherence_score(diet_ratio: float, med_ratio: float, visit_ratio: float,
                         weights: tuple[float, float, float] = (1 / 3, 1 / 3, 1 / 3)
                         ) -> dict[str, Any]:
    """计算依从性复合分（0-100）。

    公式：composite = 100 × (w_diet·diet_ratio + w_med·med_ratio + w_visit·visit_ratio)
    三个域比率（0-1）分别来自：饮食日记完成率（M3/M11）、用药按时率、随访到场率。
    等级：≥80 好 / 50-79 中 / <50 差。

    依据：MMAS-8 是 CKD 依从性测量最常用、经验证工具（Springer Med 系统综述 2024），
    但其为问卷式（0=高、1-2=中、3-8=低）。儿科 CKD 尚无单一经验证的"营养+用药+就诊"复合评分
    （pubmed 24814533 综述：单一工具不充分，应多方法组合）。故本复合分为**系统定义、待临床验证**，
    默认等权（1/3 各），可通过 weights 调整。
    """
    if not (0.0 <= diet_ratio <= 1.0 and 0.0 <= med_ratio <= 1.0 and 0.0 <= visit_ratio <= 1.0):
        return {"ok": False, "error": "INVALID_INPUT", "detail": "各比率须在 0-1 之间"}
    composite = 100.0 * (weights[0] * diet_ratio + weights[1] * med_ratio + weights[2] * visit_ratio)
    level = "good" if composite >= 80 else "fair" if composite >= 50 else "poor"
    return {
        "ok": True,
        "composite_score": round(composite, 1),
        "level": level,
        "components": {
            "diet": round(diet_ratio * 100, 1),
            "medication": round(med_ratio * 100, 1),
            "visit": round(visit_ratio * 100, 1),
        },
        "weights": {"diet": weights[0], "medication": weights[1], "visit": weights[2]},
        "basis": "系统定义加权复合分（参考 MMAS-8 测量范式；儿科 CKD 复合评分无单一验证量表，待临床验证）",
    }


def get_adherence_score(patient_id: str, diet_ratio: float, med_ratio: float, visit_ratio: float,
                        weights: tuple[float, float, float] = (1 / 3, 1 / 3, 1 / 3)
                        ) -> dict[str, Any]:
    """计算并记录某患者依从性评分（M4 拥有依从性数据，落库快照）。

    身份缺省取部署注入值（A207_CALLER），模型不可自证（P0-1）。
    OD-014：本工具会落库（写操作），入口经 MX-3 收口——仅 doctor_assistant /
    nutritionist / orchestrator 可写；parent_assistant / child_companion 矩阵 M4=RL 只读，
    越权抛 PermissionDenied（此前漏校验，实测可越权写入依从性历史，污染后续报告）。
    """
    caller = get_caller()
    enforce_write(MCP_NAME, tool="get_adherence_score")
    res = calc_adherence_score(diet_ratio, med_ratio, visit_ratio, weights)
    if not res["ok"]:
        return res
    snap = {
        "date": _today(),
        "composite_score": res["composite_score"],
        "level": res["level"],
        "components": res["components"],
        "recorded_by": caller,
    }
    store = _load_store()
    p = store.setdefault(patient_id, {"records": [], "plans": [], "adherence": []})
    p["adherence"].append(snap)
    _save_store(store)
    res["patient_id"] = patient_id
    res["history"] = p["adherence"]
    return res


# ---------------------------------------------------------------------------
# 5. PEW 时间线 facade（ADR-007：存储归属 M3）
# ---------------------------------------------------------------------------
_PEW_ORDER = {"low": 0, "medium": 1, "high": 2}


def get_pew_timeline(patient_id: str, pew_history: list[dict] | None = None) -> dict[str, Any]:
    """PEW 时间线聚合 facade（ADR-007）。

    数据归属 M3（a207-nutrition-assessment-mcp-nfyy）：每次 assess_pew_risk 后由编排层调
    M3.record_pew_risk 落库，M3.get_pew_history 读取。M4 仅作 facade——本工具接受 M3
    返回的 pew_history（list of {date, score, level}）再并入统一随访时间线。零跨包 import。
    P1-2：本工具原名 get_pew_history，与 M3 同名接口易混淆双跳，已更名为 get_pew_timeline。
    """
    caller = get_caller()
    enforce_read(MCP_NAME)
    ph = pew_history or []
    trend = "no_data"
    if len(ph) >= 2:
        fo = _PEW_ORDER.get(ph[0].get("level", "low"), 0)
        lo = _PEW_ORDER.get(ph[-1].get("level", "low"), 0)
        trend = "worsening" if lo > fo else "improving" if lo < fo else "stable"
    return {
        "ok": True,
        "patient_id": patient_id,
        "source": "M3 (a207-nutrition-assessment-mcp-nfyy) — ADR-007 PEW 历史归属 M3",
        "count": len(ph),
        "points": ph,
        "trend": trend,
        "note": "M4 仅作 facade 聚合，PEW 历史存储由 M3 拥有；此工具接受 M3 get_pew_history 的输出再并入统一随访时间线。",
    }
