#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""给 三部门逐月_2026.json 的客户行补「归属销售员」字段，并重建集团口径数组。

背景（2026-10-05）：
  看板页要上工作台，必须做行级数据授权（非管理员只能看自己的客户）。
  但 customers 数组原本只有 {month, dept, cust, amt, amt_2025, grp} —— **没有归属字段**，
  想按人过滤都无从下手。故从 数据/customer_sp_map.json 引入归属。

授权规则（对齐既定红线）：
  · 非 admin/gm：只看 sales 含自己姓名的客户
  · 无归属客户（sales=null）：仅 admin/gm 可见
  · **集团口径：仅 admin/gm 可见** —— 因为 49 个集团里有 7 个跨多个销售员
    （如 天隆/佰奥莱博/丽珠 = 刘子研+刘新元，圣湘 = 李江辉+赵云浩），
    按单人过滤会得出「只属于我的那个集团」这种会误导的假集团。
    → customers_grp 每行带 sales_list（成员归属集合）与 mixed 标记，供前端判定。

铁律：
  · 只加字段，不改任何金额 → Σ amt 与 Σ amt_2025 必须不变（自检）
  · 幂等：重复运行结果一致

用法：env -u PYTHONPATH -u VIRTUAL_ENV /usr/bin/python3 脚本/apply_sales_owner.py
"""
import io
import json
import os
import shutil
import sys
from collections import defaultdict

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import sp_merge as _spm   # 销售员归并映射（唯一权威源，见 脚本/sp_merge.py）

HOME = os.path.expanduser("~")
W = f"{HOME}/Desktop/Hermes输出-工作类"
DATA = f"{W}/数据/三部门逐月_2026.json"
SPMAP = f"{W}/数据/customer_sp_map.json"

# ── 客户级归属覆盖（持久化，唯一覆盖点；优先于 customer_sp_map.json）────────────
#   用途：当 customer_sp_map.json 的「最近月份最常见销售员」口径与实际下单人
#   不一致、且用户已拍板时，在这里钉死归属。**每次重跑管道都会重新生效**，
#   因此重建 customer_sp_map.json 或重跑 refresh_sandept.sh 都不会把它推翻。
#   规则（用户 2026-10-06 拍板）：谁下单归谁 —— 客户级归属为单值，整户归「金额主下单人」。
#   键=客户名（与 customers[].cust 完全一致），值=归属销售员（写出仍会过 sp_merge 归并）。
#   ── 主下单人判定规则（用户 2026-10-06 拍板，不得改变）─────────────────────────
#   对每个客户，在其【2026 年 · 非赠品 · 标准销售订单 · 已审核】里，按
#   「归并后的销售员」（应用 sp_merge 的 SP_MERGE + MGR_MERGE）汇总金额，
#   **取金额最大者为归属人**；客户级归属为单值，整户归该人，不拆分。
#   ────────────────────────────────────────────────────────────────────────────
CUSTOMER_OWNER_OVERRIDE = {
    # 2026-10-06 第一批：该户 2026 年订单实为 陈溪子 8.80 万 + 刘新元 0.99 万，
    #   map 里因「最近月份最常见」错记为 刘新元(→归并刘子研)，改归主下单人 陈溪子。
    "深圳市雅为世纪科技有限公司": "陈溪子",
    # ── 2026-10-06 第二批：15 家「有归属但错」全量修正（含上表雅为世纪，覆盖键共 15 个）──
    #   逐行即「原归属(归并后) → 新归属(实际主下单人)」，金额为 2026 年该户下单拆解(万)。
    "浙江大学": "吴云",                                      # 原 叶信福 → 吴云（吴云 7.26 / 叶信福 2.13 / 袁殿果 0.93 / 侯海瑜 0.72）
    "潮州凯普生物化学有限公司": "赵云浩",                     # 原 黄文才 → 赵云浩（赵云浩 1.64 / 黄文才 1.61）★低置信度：近乎对半
    "浙江农林大学": "杨天雄",                                 # 原 叶信福 → 杨天雄（杨天雄 1.76 / 吴云 1.06 / 叶信福 0.17）
    "优联瑞康(上海)基因科技有限公司": "刘欣",                 # 原 徐卓凡 → 刘欣（刘欣 0.77 / 徐卓凡 0.50）★主下单人为管理者（部门经理·非 KPI 销售员）
    "浙江省人民医院": "吴云",                                 # 原 侯海瑜 → 吴云（吴云 1.01 / 侯海瑜 0.13）
    "中山大学": "韩远怀",                                     # 原 王诗香 → 韩远怀（韩远怀 0.61 / 王诗香 0.27 / 黄建华 0.15）
    "杭州百虹商贸有限公司": "宋学伟",                         # 原 袁殿果 → 宋学伟（宋学伟 0.78 / 袁殿果 0.20）
    "成都泓盛生物有限公司": "任鑫",                           # 原 陈溪子 → 任鑫（任鑫 0.77 / 陈溪子 0.14）
    "杭州奕文生物科技有限公司": "吴云",                       # 原 叶信福 → 吴云（吴云 0.38 / 叶信福 0.35）☆近似平局（差额 7.9%）
    "生工生物工程(上海)股份有限公司": "张文蝶",               # 原 杜建 → 张文蝶（张文蝶 0.48 / 杜建 0.15）
    "浙江中医药大学": "吴云",                                 # 原 叶信福 → 吴云（吴云 0.49 / 叶信福 0.06）
    "横渠诺优生物科技（南通）有限公司上海分公司": "刘欣",     # 原 张文蝶 → 刘欣（刘欣 0.20 / 张文蝶 0.16）★主下单人为管理者（部门经理·非 KPI 销售员）
    "温州大学": "吴云",                                       # 原 袁殿果 → 吴云（吴云 0.25 / 袁殿果 0.09）
    "成都中医药大学": "陈溪子",                               # 原 任鑫 → 陈溪子（陈溪子 0.10 / 任鑫 0.10）☆近似平局（差额 ~0%）
}

# 覆盖理由（写入 meta，供审计）
OVERRIDE_REASON = {
    "深圳市雅为世纪科技有限公司": "用户 2026-10-06 拍板：谁下单归谁。2026 年该客户订单=陈溪子 8.80 万"
                                 "（2026-09 销售拓展部）+ 刘新元 0.99 万（2026-06 大客户部）；"
                                 "客户级归属为单值，整户（9.79 万）归金额主下单人 陈溪子。",
    "浙江大学": "2026 下单拆解 吴云 7.26 / 叶信福 2.13 / 袁殿果 0.93 / 侯海瑜 0.72 → 主下单人 吴云（生命科学部）。",
    "潮州凯普生物化学有限公司": "2026 下单拆解 赵云浩 1.64 / 黄文才 1.61 → 主下单人 赵云浩（大客户部）。"
                                "★低置信度：两人金额近乎对半（1.8% 差），当噪声；仍按规则判给金额大者。",
    "浙江农林大学": "2026 下单拆解 杨天雄 1.76 / 吴云 1.06 / 叶信福 0.17 → 主下单人 杨天雄（销售拓展部）。",
    "优联瑞康(上海)基因科技有限公司": "2026 下单拆解 刘欣 0.77 / 徐卓凡 0.50 → 主下单人 刘欣"
                                      "（销售拓展部·部门经理，非 KPI 销售员）。★主下单人为管理者："
                                      "按用户「全改」指令仍改，单独标注。",
    "浙江省人民医院": "2026 下单拆解 吴云 1.01 / 侯海瑜 0.13 → 主下单人 吴云（生命科学部）。",
    "中山大学": "2026 下单拆解 韩远怀 0.61 / 王诗香 0.27 / 黄建华 0.15 → 主下单人 韩远怀（生命科学部）。",
    "杭州百虹商贸有限公司": "2026 下单拆解 宋学伟 0.78 / 袁殿果 0.20 → 主下单人 宋学伟（销售拓展部）。",
    "成都泓盛生物有限公司": "2026 下单拆解 任鑫 0.77 / 陈溪子 0.14 → 主下单人 任鑫（销售拓展部）。",
    "杭州奕文生物科技有限公司": "2026 下单拆解 吴云 0.38 / 叶信福 0.35 → 主下单人 吴云（生命科学部）。"
                                "☆近似平局（差额 7.9%，未达「低置信度」阈值但一并披露）。",
    "生工生物工程(上海)股份有限公司": "2026 下单拆解 张文蝶 0.48 / 杜建 0.15 → 主下单人 张文蝶（销售拓展部）。",
    "浙江中医药大学": "2026 下单拆解 吴云 0.49 / 叶信福 0.06 → 主下单人 吴云（生命科学部）。",
    "横渠诺优生物科技（南通）有限公司上海分公司": "2026 下单拆解 刘欣 0.20 / 张文蝶 0.16 → 主下单人 刘欣"
                                                  "（销售拓展部·部门经理，非 KPI 销售员）。★主下单人为管理者："
                                                  "按用户「全改」指令仍改，单独标注。",
    "温州大学": "2026 下单拆解 吴云 0.25 / 袁殿果 0.09 → 主下单人 吴云（生命科学部）。",
    "成都中医药大学": "2026 下单拆解 陈溪子 0.10 / 任鑫 0.10 → 主下单人 陈溪子（销售拓展部）。"
                      "☆近似平局（差额 ~0%，因万元四舍五入；原数据按元分）。",
}

# 边界标注（写入 meta，供审计；不改变归属结果，只标记风险）
EDGE_FLAGS = {
    "潮州凯普生物化学有限公司": "low_confidence",             # 赵云浩 1.64 vs 黄文才 1.61，近乎对半 → 当噪声
    "优联瑞康(上海)基因科技有限公司": "owner_is_manager",      # 主下单人 刘欣 = 部门经理（非 KPI 销售员）
    "横渠诺优生物科技（南通）有限公司上海分公司": "owner_is_manager",  # 同上（任务仅点名优联瑞康，此处一并披露）
}
NEAR_TIE_NOTES = {
    "杭州奕文生物科技有限公司": "吴云 0.38 vs 叶信福 0.35（差 7.9%）",
    "成都中医药大学": "陈溪子 0.10 vs 任鑫 0.10（差 ~0%）",
}


def r2(x):
    return round(float(x), 2)


def load_gmap():
    """读集团映射：客户名 → 集团名（与 脚本/apply_group_rollup.py 同源，保证口径一致）。

    权威源：数据/diag_mappings.json 的 cust_to_group（官方《诊断原料&生命科学客户清单》
    「集团简称 → 成员客户」，真归属 k!=v）。兜底：数据/pricing/_key_customers.json 的 group。
    """
    dm = f"{W}/数据/diag_mappings.json"
    if os.path.exists(dm):
        try:
            c2g = (json.load(io.open(dm, encoding="utf-8")) or {}).get("cust_to_group") or {}
            gm = {str(k).strip(): str(v).strip() for k, v in c2g.items()
                  if k and v and str(k).strip() != str(v).strip()}   # 只取真归属
            if gm:
                return gm
        except Exception as e:
            print(f"  ⚠️ diag_mappings.json 读取失败({e})，回退 _key_customers.json")
    try:
        kdata = json.load(io.open(f"{W}/数据/pricing/_key_customers.json", encoding="utf-8"))
        rows = kdata if isinstance(kdata, list) else kdata.get("customers", [])
        gm = {}
        for r in rows:
            if isinstance(r, dict) and r.get("group"):
                nm = (r.get("name") or r.get("cust") or "").strip()
                if nm:
                    gm[nm] = str(r["group"]).strip()
        return gm
    except Exception:
        return {}


def main():
    spm = (json.load(io.open(SPMAP, encoding="utf-8")) or {}).get("map") or {}
    spm = {str(k).strip(): str(v).strip() for k, v in spm.items() if k and v}
    # 集团名的权威集合（来自集团映射表，非销售员名）：用于判定 customers_grp 行的 grp
    gmap = load_gmap()
    grpset = {v for k, v in gmap.items() if k != v}
    J = json.load(io.open(DATA, encoding="utf-8"))
    custs = J.get("customers") or []

    # ── ① 实体口径：补 sales（含销售员归并：黄明月/张立娅/刘新元 → 刘子研）──
    #   归属优先级：CUSTOMER_OWNER_OVERRIDE（用户拍板覆盖）> customer_sp_map.json
    _ov_hit = []
    for c in custs:
        nm = (c.get("cust") or "").strip()
        raw = CUSTOMER_OWNER_OVERRIDE.get(nm) or spm.get(nm)
        if nm in CUSTOMER_OWNER_OVERRIDE:
            _ov_hit.append(nm)
        c["sales"] = _spm.merge_sp(raw)   # 无归属 = None

    # ── ② 集团口径：重建，带上归属集合 ──
    bucket = defaultdict(lambda: {"amt": 0.0, "amt_2025": 0.0, "members": set(), "owners": set()})
    for c in custs:
        key = c.get("grp") or c.get("cust")
        b = bucket[(c["month"], c["dept"], key)]
        b["amt"] += c.get("amt") or 0.0
        b["amt_2025"] += c.get("amt_2025") or 0.0
        b["members"].add(c.get("cust"))
        b["owners"].add(c.get("sales") or "(无归属)")

    cgrp = []
    for (m, d, key), b in bucket.items():
        owners = sorted(b["owners"])
        cgrp.append({
            "month": m, "dept": d, "cust": key,
            "amt": r2(b["amt"]), "amt_2025": r2(b["amt_2025"]),
            "grp": key if key in grpset else None,
            "merged": len(b["members"]) if len(b["members"]) > 1 else 1,
            "sales_list": owners,
            "mixed": len(owners) > 1,           # 跨多个销售员 → 非 admin/gm 不可见
        })
    cgrp.sort(key=lambda x: (x["month"], x["dept"], -x["amt"]))

    # ── ③ 自检：金额必须守恒 ──
    e26 = r2(sum(c.get("amt") or 0 for c in custs))
    g26 = r2(sum(c["amt"] for c in cgrp))
    e25 = r2(sum(c.get("amt_2025") or 0 for c in custs))
    g25 = r2(sum(c["amt_2025"] for c in cgrp))
    owned = [c for c in custs if c.get("sales")]
    own_amt = r2(sum(c["amt"] or 0 for c in owned))
    tot_amt = e26
    print("================ 自检门禁 ================")
    print(f"  归属映射源: customer_sp_map.json（{len(spm)} 条）")
    print(f"  实体口径 {len(custs)} 行 ｜ Σ今年 {e26:,.2f} ｜ Σ去年 {e25:,.2f}")
    print(f"  集团口径 {len(cgrp)} 行 ｜ Σ今年 {g26:,.2f} ｜ Σ去年 {g25:,.2f}")
    print(f"  有归属客户: {len(owned)}/{len(custs)} 行 ｜ 金额 {own_amt:,.1f} 万 ({own_amt/tot_amt*100:.1f}%)")
    print(f"  无归属金额: {tot_amt-own_amt:,.1f} 万 ({(tot_amt-own_amt)/tot_amt*100:.1f}%) → 仅 admin/gm 可见")
    mixed = [c for c in cgrp if c.get("mixed")]
    gk = {c["cust"] for c in cgrp if c.get("grp")}
    mixedk = {c["cust"] for c in mixed}
    print(f"  跨销售员的集团: {len(mixedk)}/{len(gk)} → 仅 admin/gm 可见")
    # 客户级覆盖自检（真跑）
    for _cn, _to in CUSTOMER_OWNER_OVERRIDE.items():
        _rows = [c for c in custs if (c.get("cust") or "").strip() == _cn]
        _got = sorted({c["sales"] for c in _rows})
        _amt = r2(sum(c.get("amt") or 0 for c in _rows))
        print(f"  覆盖[{_cn}] → {_got}（{len(_rows)} 行，Σ {_amt} 万）"
              f" {'OK' if _got == [_spm.merge_sp(_to)] else 'MISMATCH'}")
    g1, g2 = abs(e26 - g26) < 0.05, abs(e25 - g25) < 0.05
    print(f"  ① Σ今年守恒 -> {'PASS' if g1 else 'FAIL'}")
    print(f"  ② Σ去年守恒 -> {'PASS' if g2 else 'FAIL'}")
    if not (g1 and g2):
        print("  ❌ 金额不守恒，拒绝写盘")
        return 1

    J["customers"] = custs
    J["customers_grp"] = cgrp
    J.setdefault("meta", {})["sales_scope"] = {
        "rule": "非 admin/gm 仅见 sales 含本人姓名的客户；无归属客户仅 admin/gm；集团口径仅 admin/gm",
        "source": "数据/customer_sp_map.json 的 map（method: 最近月份最常见销售员）",
        "owned_rows": len(owned), "total_rows": len(custs),
        "unowned_amount": r2(tot_amt - own_amt), "mixed_groups": len(mixedk),
    }
    # 客户级归属覆盖审计（持久，用户拍板）
    if CUSTOMER_OWNER_OVERRIDE:
        J["meta"]["customer_owner_override"] = {
            "rule": "客户级归属覆盖：键=客户名，值=归属销售员；优先于 customer_sp_map.json",
            "source": "脚本/apply_sales_owner.py 的 CUSTOMER_OWNER_OVERRIDE（唯一持久覆盖点，"
                       "每次重跑管道都会重新生效 → 重建 customer_sp_map 也不会被推翻）",
            "decided_by": "用户 2026-10-06 拍板（谁下单归谁）",
            "applied": {
                k: {
                    "to": _spm.merge_sp(v),
                    "from_raw": (spm.get(k) or None),
                    "from_effective": _spm.merge_sp(spm.get(k)),
                    "reason": OVERRIDE_REASON.get(k, ""),
                } for k, v in CUSTOMER_OWNER_OVERRIDE.items()
            },
            "rows_touched": {k: sum(1 for c in custs if (c.get("cust") or "").strip() == k)
                             for k in CUSTOMER_OWNER_OVERRIDE},
            "override_count": len(CUSTOMER_OWNER_OVERRIDE),
            "target_rule": "对每个客户，在其 2026 年非赠品·标准销售订单·已审核里，"
                           "按归并后销售员汇总金额，取金额最大者为归属人；单值归属，不拆分。",
            "edge_flags": dict(EDGE_FLAGS),
            "near_tie_notes": dict(NEAR_TIE_NOTES),
        }
    shutil.copy2(DATA, DATA + ".bak-pre-salesowner")
    io.open(DATA, "w", encoding="utf-8").write(json.dumps(J, ensure_ascii=False, separators=(",", ":")))
    print(f"\n✅ 已写回 {DATA}（备份 .bak-pre-salesowner）")
    return 0


if __name__ == "__main__":
    sys.exit(main())
