#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""达成进度包 v2 —— 给「达成进度看板」供数

数据源（全部本机已有，日更）：
  erp_amount.json            日/月×人×部门 销售额   ← ERP销售订单导出，每日 20:01
  daily_activities_mcp.json  飞书活动条数（按人/按日/课题组）← MCP，每日 21:00
  kpi_actual_pack.json       官方通报月度实绩（用于「参考值 vs 官方值」对照）
  kpi_scheme.json            目标/权重/项名（当月协议书）

产出 数据/kpi_progress_pack.json，注入看板「达成进度」视图。
口径声明：活动条数 ≠ 考核口径（考核＝打卡记录×活动记录匹配），
         故拜访类一律标 mode='ref'（参考·口径待校准），不计入预估得分。
"""
import json, os, re, datetime

B = os.path.dirname(os.path.abspath(__file__))
J = lambda f: json.load(open(os.path.join(B, f), encoding='utf-8'))

SCHEME = J('kpi_scheme.json')
AMT    = J('erp_amount.json')
ACT    = J('daily_activities_mcp.json')
ACTUAL = J('kpi_actual_pack.json')

TODAY = datetime.date.today()
TODAY_S = TODAY.isoformat()


def num(x):
    try:
        return float(x)
    except Exception:
        return None


# ---------------- 飞书活动：按人×月×周 计数 ----------------
INVALID_PAT = re.compile(r'送奶茶|奶茶|不在|无人|没见到|未见到|路过|取消|作废|无效')
KEY_PAT     = re.compile(r'关键人|关键决策人|决策人|PI\b|负责人拜访')
GRP_PAT     = re.compile(r'课题组|实验室|大学|学院|研究所|医院')

acts = ACT.get('items', ACT if isinstance(ACT, list) else []) or []
REC = []
for a in acts:
    ow = a.get('owner') or {}
    nm = ow.get('name_cn') or ow.get('name') or ow.get('user_name')
    st = (a.get('start_time') or a.get('created_at') or '')[:10]
    if not nm or not st:
        continue
    tx = ' '.join(str(x) for x in [a.get('name'), a.get('field_b99055'), a.get('field_1e7d80'),
                                   a.get('field_5f20fc', {}).get('label') if isinstance(a.get('field_5f20fc'), dict) else '',
                                   a.get('summary'), a.get('desc')] if x)
    grp = a.get('field_5f20fc')
    grp = grp.get('label') if isinstance(grp, dict) else (grp or '')
    REC.append({'name': nm, 'date': st, 'month': st[:7], 'txt': tx, 'grp': grp,
                'invalid': bool(INVALID_PAT.search(tx)), 'key': bool(KEY_PAT.search(tx))})

VALID = [r for r in REC if not r['invalid']]


def week_key(ds):
    """自然周（周一为始）标签"""
    d = datetime.date(*[int(x) for x in ds.split('-')])
    mon = d - datetime.timedelta(days=d.weekday())
    return mon.isoformat()


def month_weeks(m):
    """该月各周标签（按周一划分，落在月内的天为主）"""
    y, mo = int(m[:4]), int(m[5:7])
    d0 = datetime.date(y, mo, 1)
    d1 = (datetime.date(y + (mo == 12), (mo % 12) + 1, 1) if True else None) - datetime.timedelta(days=1)
    out, cur = [], d0
    while cur <= d1:
        wk = week_key(cur.isoformat())
        if wk not in out:
            out.append(wk)
        cur += datetime.timedelta(days=1)
    # 未来的周不输出（进度看板只看已发生的周）
    today = datetime.date.today()
    return [w for w in out if datetime.date(*[int(x) for x in w.split('-')]) <= today]


def act_count(name, month, kind='visit'):
    rs = [r for r in VALID if r['name'] == name and r['month'] == month]
    if kind == 'visit':
        return len(rs)
    if kind == 'key':
        return len([r for r in rs if r['key']])
    if kind == 'grp':
        return len({(r['date'], r['grp'] or r['txt'][:20]) for r in rs})
    return len(rs)


def act_weekly(name, month, kind='visit'):
    wl = month_weeks(month)
    out = []
    for wk in wl:
        if kind == 'visit':
            n = len([r for r in VALID if r['name'] == name and r['month'] == month and week_key(r['date']) == wk])
        elif kind == 'key':
            n = len([r for r in VALID if r['name'] == name and r['month'] == month and week_key(r['date']) == wk and r['key']])
        else:
            n = len({(r['date'], r['grp'] or r['txt'][:20]) for r in VALID
                     if r['name'] == name and r['month'] == month and week_key(r['date']) == wk})
        out.append([wk[5:], n])
    return out


# ---------------- 销售：按人×月×周 ----------------
dp   = AMT.get('daily_person') or {}
mp   = AMT.get('monthly_person') or {}
mtot = AMT.get('monthly') or {}
mdept= AMT.get('monthly_dept') or {}


def sales_cur(name, month):
    d = dp.get(month + '-01')  # 占位，真实用逐日累加
    tot = 0.0
    for ds, ppl in dp.items():
        if ds[:7] == month and name in ppl:
            tot += ppl[name] or 0
    return round(tot, 2)


def sales_weekly(name, month):
    out = []
    for wk in month_weeks(month):
        tot = 0.0
        for ds, ppl in dp.items():
            if ds[:7] == month and week_key(ds) == wk:
                tot += ppl.get(name) or 0
        out.append([wk[5:], round(tot, 2)])
    return out


def all_names(month):
    s = set()
    for ds, ppl in dp.items():
        if ds[:7] == month:
            s |= set(ppl)
    return s


# ---------------- 组装 ----------------
def prev_month(m):
    y, mo = int(m[:4]), int(m[5:7])
    return f"{y-1}-12" if mo == 1 else f"{y}-{mo-1:02d}"


REGION = lambda rg: '浙江' if (rg or '').startswith('浙江') else ('广州' if (rg or '').startswith('广州') else (rg or ''))
PEOPLE = SCHEME['people']
SCHEME_ROLES = SCHEME['roles']

months = [m for m in sorted(set(SCHEME_ROLES.get('tz', {}).get('months', {})) &
                            set(x['month'] for x in REC) | {TODAY_S[:7]})][-3:]
if not months:
    months = [TODAY_S[:7]]
DATA_MONTH = months[-1]

roles_out = {}
for rk, rv in SCHEME_ROLES.items():
    plist = [p for p in PEOPLE if p['role'] == rk]
    rmonths = {}
    for m in months:
        mm = rv.get('months', {}).get(m)
        if not mm:
            continue
        ppl_out = []
        for p in plist:
            nm, rg = p['name'], REGION(p.get('region'))
            # ---- 销售：本人；区域经理按区域合计 ----
            if rk == 'ls_mgr':
                mem = [q['name'] for q in PEOPLE if q['role'] == 'ls_eng' and REGION(q.get('region')) == rg]
                sc = round(sum(sales_cur(x, m) for x in mem), 2)
                swk = []
                for wk in month_weeks(m):
                    t = 0.0
                    for x in mem:
                        for ds, ppl in dp.items():
                            if ds[:7] == m and week_key(ds) == wk:
                                t += ppl.get(x) or 0
                    swk.append([wk[5:], round(t, 2)])
                prev = round(sum(sales_cur(x, prev_month(m)) for x in mem), 2)
                sc_src = 'ERP·区域合计'
            else:
                sc = sales_cur(nm, m)
                swk = sales_weekly(nm, m)
                prev = round(mp.get(prev_month(m), {}).get(nm) or 0, 2)
                sc_src = 'ERP·本人'
            items_out = []
            for i, it in enumerate(mm.get('items', []), 1):
                nmz = (it.get('item') or '') + (('（' + it['desc'][:0]) if False else '')
                desc = it.get('desc') or ''
                t1, t2, t3 = num(it.get('t1')), num(it.get('t2')), num(it.get('t3'))
                rec = {'no': it.get('no') or i, 'item': nmz, 'desc': desc, 'w': it.get('weight'),
                       'type': it.get('type'), 't1': t1, 't2': t2, 't3': t3,
                       'tl': it.get('tierLabel') or {}, 'src0': it.get('source') or '',
                       'mode': 'fill', 'cur': None, 'src': it.get('source') or '人工',
                       'freq': '月更', 'wks': [], 'off': None}
                if re.search(r'有效客户拜访次数', nmz):
                    rec.update(mode='ref', cur=act_count(nm, m, 'visit'), wks=act_weekly(nm, m, 'visit'),
                               src='飞书活动条数', freq='日更', note='考核口径＝打卡×活动匹配，活动条数仅作活跃度参考')
                elif re.search(r'协助拜访课题组|课题组数量', nmz):
                    rec.update(mode='ref', cur=act_count(nm, m, 'grp'), wks=act_weekly(nm, m, 'grp'),
                               src='飞书活动(按日×课题组去重)', freq='日更', note='活动口径，非考核口径')
                elif re.search(r'关键决策人|关键人', nmz):
                    rec.update(mode='ref', cur=act_count(nm, m, 'key'), wks=act_weekly(nm, m, 'key'),
                               src='飞书活动(关键词命中)', freq='日更', note='活动口径，非考核口径')
                elif (it.get('type') == 'x2') or re.search(r'促销成果|销售业绩|销售额|回款额', nmz):
                    rec.update(mode='auto', cur=sc, wks=swk, src=sc_src, freq='日更')
                items_out.append(rec)
            # 官方通报对照（若有）：通报项名是长描述，用关键词映射协议书项 -> 通报项
            off_items = ((ACTUAL.get('roles', {}).get(rk) or {}).get(m) or {}).get('items') or []
            PATT = [
                (r'有效客户拜访次数', lambda on: ('客户拜访次数' in on) and ('协助拜访课题组' not in on)),
                (r'测试单发放数量', lambda on: '测试单发放数量' in on),
                (r'测试反馈率|反馈率', lambda on: '测试反馈率' in on),
                (r'回款', lambda on: '回款' in on),
                (r'成交客户数量', lambda on: ('成交单位' in on) or ('成交客户' in on)),
                (r'新客户开发数量', lambda on: '新客户开发数量' in on),
                (r'协助拜访课题组', lambda on: '协助拜访课题组' in on),
                (r'周会', lambda on: '周会' in on),
                (r'关键决策人', lambda on: '关键决策人' in on),
                (r'促销成果', lambda on: '促销' in on),
            ]
            for r in items_out:
                for pat, test in PATT:
                    if not re.search(pat, r['item']):
                        continue
                    for oi in off_items:
                        on = oi.get('n') or ''
                        if test(on):
                            v = (oi.get('v') or {}).get(nm)
                            if v is not None:
                                r['off'] = v
                            break
                    break
            # 预估得分（仅 auto/fill 值有效者参与）
            score = None
            kt = 0.0
            for r in items_out:
                w = r['w'] or 0
                if r['mode'] == 'ref' or r['cur'] is None:
                    continue
                v, a1, a2, a3 = num(r['cur']), r['t1'], r['t2'], r['t3']
                if v is None or not a2:
                    continue
                if r['mode'] == 'auto' and re.search(r'促销成果', r['item']):
                    sc_i = round(min(v / a2, 4) ** 2 * 100, 1)      # 促销 x²，超出 T2 继续加分
                    if v < (a1 or 0):
                        sc_i = 0
                else:
                    sc_i = 120 if (a3 and v >= a3) else (100 if v >= a2 else (60 if (a1 and v >= a1) else 0))
                r['score'] = sc_i
                kt += sc_i * w
            kt = round(kt, 1)
            sup = [dict(s, cur=None) for s in (mm.get('superiors') or [])]
            asg = [{'no': a.get('no'), 'text': a.get('text'), 'shareLabel': a.get('shareLabel'),
                    'exec': a.get('exec'), 't1': a.get('T1'), 't2': a.get('T2'), 't3': a.get('T3')}
                   for a in (mm.get('assign') or [])]
            ppl_out.append({'name': nm, 'region': p.get('region'), 'level': (rv.get('level') or ''),
                            'sales': {'cur': sc, 'prev': prev, 'wks': swk, 'src': sc_src, 'month': m,
                                      'dept': rv.get('dept'), 'rate': (round(sc / prev * 100 - 100, 1) if prev else None)},
                            'items': items_out, 'kt': kt if kt else None,
                            'sup': sup, 'assign': asg})
        rmonths[m] = {'people': ppl_out,
                      'dept_sales': round(sum(sales_cur(p['name'], m) for p in plist), 2) if rk != 'ls_mgr'
                                    else round(sum(x['sales']['cur'] for x in ppl_out), 2),
                      'n': len(ppl_out)}
    roles_out[rk] = {'name': rv.get('name'), 'level': rv.get('level'), 'dept': rv.get('dept'), 'months': rmonths}

days_left = None
try:
    y, mo = int(DATA_MONTH[:4]), int(DATA_MONTH[5:7])
    last = (datetime.date(y + (mo == 12), (mo % 12) + 1, 1) - datetime.timedelta(days=1)).day
    days_left = max(0, last - TODAY.day) if DATA_MONTH == TODAY_S[:7] else 0
except Exception:
    pass

SOURCES = [
    {'n': 'ERP 销售订单（日/月×人×部门）', 'f': '日更', 'u': (AMT.get('updated_at') or '')[:16].replace('T', ' '), 's': '已接入'},
    {'n': '飞书活动（按人×日×课题组）', 'f': '日更', 'u': (ACT.get('updated_at') or ACT.get('sync_time') or '')[:16].replace('T', ' '), 's': '已接入 · 口径待校准'},
    {'n': 'ERP 测试单发放 / 反馈', 'f': '月更', 'u': '随月度通报', 's': '待接入'},
    {'n': '回款及时率（财务回款计划）', 'f': '月更', 'u': '随月度通报', 's': '待接入'},
    {'n': '上级评分 / 周会 / workshop', 'f': '月更', 'u': '人工填写', 's': '人工'},
]
# 活动数据实际最后日期
if REC:
    SOURCES[1]['u'] = max(r['date'] for r in REC)

OUT = {'updated': datetime.datetime.now().strftime('%Y-%m-%d %H:%M'),
       'data_month': DATA_MONTH, 'months': months, 'days_left': days_left,
       'months_total': {m: mtot.get(m) for m in months},
       'dept_month': {m: mdept.get(m) for m in months},
       'sources': SOURCES, 'roles': roles_out}

json.dump(OUT, open(os.path.join(B, 'kpi_progress_pack.json'), 'w', encoding='utf-8'),
          ensure_ascii=False, separators=(',', ':'))

# ---- 自检输出 ----
print('months:', months, '| data_month:', DATA_MONTH, '| days_left:', days_left)
print('活动覆盖:', {m: sum(1 for r in REC if r['month'] == m) for m in sorted({r['month'] for r in REC})})
print('全月累计:', {m: mtot.get(m) for m in months})
for rk in ('tz', 'dk', 'ls_eng', 'ls_mgr'):
    r = roles_out.get(rk) or {}
    md = (r.get('months') or {}).get(DATA_MONTH)
    if not md:
        continue
    print(f"## {rk} {r.get('name')} 部门月业绩={md['dept_sales']:,.0f}")
    for p in md['people'][:3]:
        print(f"   {p['name']:6s} 业绩{p['sales']['cur']:>12,.0f} 环比{p['sales']['rate']}% 预估关键任务{p['kt']}")
        for it in p['items']:
            print(f"      [{it['mode']:4s}] {it['item'][:22]:24s} 目标{it['t1']}/{it['t2']}/{it['t3']} 当前{it['cur']} 官方{it['off']} 周{it['wks'][:2]}")
