#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""按「集团归属键」补出集团口径客户数组（hybrid：有集团→集团名，无集团→原客户名）。

背景（用户 2026-10-05 拍板）：
  宝锐 ERP 客户分析历来是「实体口径」，同一集团被拆成多行（圣湘系 3 行、天隆系 2 行…），
  导致 Top10 集中度被低估、重要集团在榜单里隐身。
  映射源 数据/pricing/_key_customers.json 里 643 条仅 44 条标了 group（覆盖 25% 金额），
  用户确认：**只有这 44 个是「有集团归属」的，其余不需要归集**。
  故采用 hybrid —— 有集团并入集团，无集团保持独立实体。

产出：就地给 数据/三部门逐月_2026.json 增加
  · customers[i]['grp']  = 集团名（未映射为 null）
  · customers_grp        = 按归属键(grp or cust)聚合后的数组，字段同 customers
                           （amt / amt_2025 求和 → 驱动层瀑布图仍可勾稽）

铁律：
  · 归属键必须同时用于 2026 与 2025 两侧，否则同比会被误判成「新增+流失」
  · 聚合只合并、不丢弃 → Σ amt 与 Σ amt_2025 必须与实体口径完全相等（本脚本自检）
  · 幂等：重复运行结果一致

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

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


def load_gmap():
    """读集团映射：客户名 → 集团名。

    权威源优先级：
      ① 数据/diag_mappings.json 的 cust_to_group（源自官方《诊断原料&生命科学客户清单》
         「集团简称 → 成员客户」，自映射项=无集团；真归属 194 条 / 85 个集团）—— 这是
         与 工具/仪表盘/客户分析仪表盘_2024-2026.html 同一口径，必须优先。
      ② 兜底：数据/pricing/_key_customers.json 的 group 字段（仅 44 条，覆盖低）。
    """
    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:
                print(f"  集团映射源: diag_mappings.json/cust_to_group（{len(gm)} 个客户带集团，"
                      f"{len(set(gm.values()))} 个集团）")
                return gm
        except Exception as e:
            print(f"  ⚠️ diag_mappings.json 读取失败({e})，回退 _key_customers.json")

    k = json.load(io.open(KEYC, encoding="utf-8"))
    rows = k if isinstance(k, list) else k.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()
    print(f"  集团映射源: _key_customers.json（{len(gm)} 条，覆盖率低，建议补 diag_mappings.json）")
    return gm


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


def main():
    if not os.path.exists(DATA):
        print(f"❌ 找不到 {DATA}")
        return 1
    gmap = load_gmap()
    J = json.load(io.open(DATA, encoding="utf-8"))
    custs = J.get("customers") or []

    # ── ① 给每行打上集团标记（实体口径保留原样，只加字段）──
    for c in custs:
        c["grp"] = gmap.get(c.get("cust") or "")

    # ── ② 按归属键聚合出集团口径数组 ──
    bucket = defaultdict(lambda: {"amt": 0.0, "amt_2025": 0.0, "n": 0, "members": 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["n"] += 1
        b["members"].add(c.get("cust"))

    cgrp = []
    for (m, d, key), b in bucket.items():
        cgrp.append({"month": m, "dept": d, "cust": key,
                     "amt": r2(b["amt"]), "amt_2025": r2(b["amt_2025"]),
                     "grp": key if key in set(gmap.values()) else None,
                     "merged": b["n"] if b["n"] > 1 else 1})
    cgrp.sort(key=lambda x: (x["month"], x["dept"], -x["amt"]))

    # ── ③ 自检门禁（聚合只能合并，不能丢金额）──
    s_e26 = r2(sum(c.get("amt") or 0 for c in custs))
    s_g26 = r2(sum(c["amt"] for c in cgrp))
    s_e25 = r2(sum(c.get("amt_2025") or 0 for c in custs))
    s_g25 = r2(sum(c["amt_2025"] for c in cgrp))
    print("================ 自检门禁 ================")
    print(f"  实体口径: {len(custs)} 行 | Σ今年 {s_e26:,.2f} 万 | Σ去年 {s_e25:,.2f} 万")
    print(f"  集团口径: {len(cgrp)} 行 | Σ今年 {s_g26:,.2f} 万 | Σ去年 {s_g25:,.2f} 万")
    g1 = abs(s_e26 - s_g26) < 0.05
    g2 = abs(s_e25 - s_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", {})["group_rollup"] = {
        "rule": "归属键 = 有集团→集团名，无集团→原客户名（hybrid）",
        "source": "数据/diag_mappings.json 的 cust_to_group（源自官方《诊断原料&生命科学客户清单》"
                  "「集团简称→成员客户」映射；兜底才用 _key_customers.json 的 group 字段）",
        "mapped_names": len(gmap),
        "groups_with_amount": len({c["cust"] for c in cgrp if c.get("grp")}),
        "note": f"集团口径覆盖 Σ今年 {s_g26 - sum(c['amt'] for c in custs if not c.get('grp')):,.0f} 万；"
                "未映射客户保持独立实体，不做归集。",
    }

    shutil.copy2(DATA, DATA + ".bak-pre-rollup")
    io.open(DATA, "w", encoding="utf-8").write(json.dumps(J, ensure_ascii=False, separators=(",", ":")))
    print(f"\n✅ 已写回 {DATA}（备份 .bak-pre-rollup）")

    # ── ④ 口径对比 Top10 ──
    e, g = defaultdict(float), defaultdict(float)
    for c in custs:
        if (c.get("amt") or 0) > 0:
            e[c["cust"]] += c["amt"]
    for c in cgrp:
        if (c.get("amt") or 0) > 0:
            g[c["cust"]] += c["amt"]
    print("\n  ── 实体 Top10 ──            ── 集团口径 Top10 ──")
    et = sorted(e.items(), key=lambda x: -x[1])[:10]
    gt = sorted(g.items(), key=lambda x: -x[1])[:10]
    for i in range(10):
        print(f"  {i+1:2d}. {et[i][1]:>7,.1f} {et[i][0][:18]:<20} {gt[i][1]:>7,.1f} {gt[i][0][:20]}")
    print(f"  Top10 集中度: 实体 {sum(v for _,v in et)/3442.75*100:.1f}% | 集团 {sum(v for _,v in gt)/3442.75*100:.1f}%")
    return 0


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