#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""集团映射同步：以 CRM（飞书客户表 mcp_customers.json 的 field_c3224b）为最新权威，
   合并既有 diag_mappings.json 的人工判定，归一集团名、滤除噪声值，
   回填 _key_customers.json 的 group / group_name 并标记「集团汇总行」is_group_row。

   守恒门禁：只改归属标记，不改任何金额字段（前后逐行金额必须完全一致）。
   用法：env -u PYTHONPATH /usr/bin/python3 脚本/sync_group_map.py [--dry]
"""
import io
import json
import os
import re
import shutil
import sys
from collections import Counter
from datetime import datetime

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
DM = W + '/数据/diag_mappings.json'
CRM = W + '/数据/mcp_customers.json'
KEY = W + '/数据/pricing/_key_customers.json'
BK = W + '/数据/backup'
DRY = '--dry' in sys.argv
TS = datetime.now().strftime('%Y%m%d_%H%M%S')

NOISE = {'', '无', '-', '/', '`', '1', 'none', 'nan', 'null', '未知', '0', '待确认', '无集团',
         '暂无', '待定', '其他', '未填', '未知集团', 'n/a', 'na', 'ng', '待补充', '无归属'}
NOISE_RE = re.compile(r'^(暂无|待[定补]|其他|未[填知]|无[归属效]?|n/?a$|none|null|未知)', re.I)
SUF = re.compile(r'(集团|控股|股份有限公司|有限公司|股份公司|公司|集团股份|\(.*?\)|（.*?）|（.*?\)|\(.*?）|\s)+$')


def norm(g):
    g = str(g or '').strip()
    prev = None
    while prev != g:
        prev = g
        g = SUF.sub('', g).strip()
    return g


def is_noise(g, cust=None):
    g = str(g or '').strip()
    if g in NOISE or g.lower() in NOISE:
        return True
    if len(norm(g)) < 2:                     # 过短（如单字、纯符号）不是集团名
        return True
    if NOISE_RE.match(g):                    # 暂无/待定/无/其他/n.a 类
        return True
    if not re.search(r'[\u4e00-\u9fa5A-Za-z]', g):
        return True
    if cust and g == str(cust).strip():
        return True
    return False


# ── ① 读三个源 ──
crm_rows = json.load(io.open(CRM, encoding='utf-8'))['customers']
crm = {}
for c in crm_rows:
    n = str(c.get('name') or '').strip()
    g = str(c.get('field_c3224b') or '').strip()
    if n:
        crm[n] = g
old = {str(k).strip(): str(v).strip() for k, v in
       (json.load(io.open(DM, encoding='utf-8')).get('cust_to_group') or {}).items()}
rows = json.load(io.open(KEY, encoding='utf-8'))

# ── ② 既有权威集团名（用于把 CRM 的别名归一到同一集团）──
existing = {v for v in old.values() if v and not is_noise(v)}
canon_by_key = {}
for g in sorted(existing):
    k = norm(g)
    if k and k not in canon_by_key:
        canon_by_key[k] = g
# CRM 内部同名归一：同一 norm key 取出现次数最多的写法
crm_grp = Counter(v for n, v in crm.items() if v and not is_noise(v, n))
crm_canon = {}
for g, _ in crm_grp.most_common():
    k = norm(g)
    if not k:
        continue
    crm_canon.setdefault(k, g)


def canonical(crm_name):
    k = norm(crm_name)
    if k in canon_by_key:          # 既有集团名优先（人工判定 = 报表沿用名）
        return canon_by_key[k]
    return crm_canon.get(k, crm_name)


# ── ③ 合并：CRM 优先，既有映射补位 ──
new_map = {}
src_stat = Counter()
for n, g in old.items():
    if g and not is_noise(g, n):
        new_map[n] = g
        src_stat['既有映射'] += 1
changed = []
for n, g in crm.items():
    if is_noise(g, n):
        continue
    cg = canonical(g)
    if new_map.get(n) == cg:
        continue
    if n in new_map:
        changed.append((n, new_map[n], cg))
    new_map[n] = cg
    src_stat['CRM 归属'] += 1

# ── ④ 报告 ──
print('═' * 92)
print('集团映射同步（CRM = 最新权威）  生成 %s' % TS)
print('═' * 92)
print('  CRM 客户 %d ｜ CRM 有集团 %d ｜ CRM 集团去重 %d'
      % (len(crm), sum(1 for v in crm.values() if v and not is_noise(v)), len(set(v for v in crm.values() if v and not is_noise(v)))))
print('  既有映射（真归属）%d 客户 / %d 集团' % (len({k: v for k, v in old.items() if v and not is_noise(v, k)}),
                                       len({v for k, v in old.items() if v and not is_noise(v)})))
print('  合并后：%d 客户带集团 ｜ %d 个集团 ｜ 归类来源 %s' % (len(new_map), len(set(new_map.values())), dict(src_stat)))
print()
print('  集团名归一（CRM 别名 → 既有权威名）%d 条:' % len(changed))
for n, a, b in changed[:15]:
    print('     %-38s %s → %s' % (n[:38], a, b))
print()
sl = {k: v for k, v in new_map.items() if '生凌' in k}
print('  生凌系：%d 个主体 → %s' % (len(sl), sorted(set(sl.values()))))
for k, v in sorted(sl.items()):
    print('     %-36s → %s' % (k, v))
top = Counter(new_map.values()).most_common(8)
print('  成员最多的集团:', top)
print()

# ── ⑤ 回填 _key_customers.json ──
gset = set(new_map.values())
before_amt = sum((r.get('total') or 0) for r in rows)
filled = marked = 0
for r in rows:
    n = str(r.get('name') or '').strip()
    g = new_map.get(n, '')
    if g and not r.get('group'):
        r['group'] = g
        r['group_name'] = g
        filled += 1
    if str(n).endswith('集团') or n in gset:
        if not r.get('is_group_row'):
            marked += 1
        r['is_group_row'] = True
after_amt = sum((r.get('total') or 0) for r in rows)
assert abs(before_amt - after_amt) < 1e-6, '金额守恒门禁失败！%s vs %s' % (before_amt, after_amt)

with_group = [r for r in rows if r.get('group')]
print('  回填 _key_customers.json：新填 group %d 行 ｜ 标记集团汇总行 %d 行' % (filled, marked))
print('  回填后：%d/%d 行有 group ｜ 集团汇总行 %d' % (len(with_group), len(rows), sum(1 for r in rows if r.get('is_group_row'))))
print('  金额守恒门禁：%.2f == %.2f ✅（只改标记，不动金额）' % (before_amt, after_amt))
print()

if DRY:
    print('（--dry 模式，未写盘）')
    sys.exit(0)

os.makedirs(BK, exist_ok=True)
for f in (DM, KEY):
    shutil.copy2(f, '%s/%s.bak-%s' % (BK, os.path.basename(f), TS))
print('  已备份 → 数据/backup/*.bak-%s' % TS)

d = json.load(io.open(DM, encoding='utf-8'))
d['cust_to_group'] = new_map
d['_meta'] = {'updated': TS, 'source': 'CRM field_c3224b ∪ 既有 diag_mappings，集团名已归一',
              'note': '权威集团映射：转出脚本 apply_group_rollup.py / build_customer_360.py 均读此文件'}
with io.open(DM, 'w', encoding='utf-8') as f:
    json.dump(d, f, ensure_ascii=False, indent=1)
with io.open(KEY, 'w', encoding='utf-8') as f:
    json.dump(rows, f, ensure_ascii=False)
print('  ✅ 已写 %s（cust_to_group %d 条）' % (DM.split('/')[-1], len(new_map)))
print('  ✅ 已写 %s' % KEY.split('/')[-1])
