#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""目标基础数据 —— 唯一权威源读取 + 月度摊派规则（可复用）。

【唯一权威源】数据/目标基础数据_2026.json（由 脚本/build_target_source.py 生成 / 管理台页面导出后落盘）。
  本模块是**唯一读入口**：gen_dept_monthly_json.py（部门月度目标）与
  apply_salesperson_targets.py（个人年度/月度目标）都从这里取数。
  若新文件缺失 → 回退旧源（数据/月度目标_2026统一.json + 数据/kpi_scheme.json）并报警，
  旧源已标记 deprecation（保留不删）。

月度摊派规则（rule），把「年度目标」摊成 12 个月（万元），任一规则下**合计精确等于年度目标**
（舍入差由最后一个有值月吸收）：
  shape  = 按部门/区域月形态等比摊：个人月值 = 个人年度 × 部门(区域)当月 / Σ部门(区域)月
  even   = 年度 / 12
  manual = 手填值优先（无手填的月份 → 用 shape 兜底；仍无源 → null）
铁律：**无源的月份保持 null，绝不写 0**（部门当月形态为 0/缺失 → 该月 null，不参与求和）。
"""
import io
import json
import os

W = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TARGET_SRC = os.path.join(W, '数据', '目标基础数据_2026.json')
# 旧源（降为历史，仅回退用；文件保留不删）
LEGACY_MONTHLY = os.path.join(W, '数据', '月度目标_2026统一.json')
LEGACY_SCHEME = os.path.join(W, '数据', 'kpi_scheme.json')

MONTHS = ['2026-%02d' % m for m in range(1, 13)]
RULES = ('shape', 'even', 'manual')
WAN = 1e4


# ────────────────────────────────────────────── 规则函数
def taper(annual, shape, rule='shape', manual=None):
    """把年度目标（万元）摊成 12 个月 → {月: 万元 或 None}。

    · 合计（忽略 None）**精确等于 annual**（舍入差由最后一个有值月吸收）。
    · 部门/区域当月形态缺失或为 0 → 该月 null（无源），绝不写 0。
    · annual 为 None → 全年 12 月均为 None。
    """
    months = list(shape.keys()) if shape else list(MONTHS)
    if annual is None:
        return {m: None for m in months}
    annual = float(annual)
    if rule == 'even':
        vals = {m: round(annual / len(months), 2) for m in months}
    elif rule == 'manual' and manual:
        tot = sum(float(v) for v in (shape or {}).values() if isinstance(v, (int, float)))
        vals = {}
        for m in months:
            mv = manual.get(m)
            if mv is not None:
                vals[m] = round(float(mv), 2)
            else:                                     # 手填缺 → shape 兜底
                sv = (shape or {}).get(m)
                vals[m] = round(annual * sv / tot, 2) if (sv and tot > 0) else None
    else:                                             # shape
        tot = sum(float(v) for v in (shape or {}).values() if isinstance(v, (int, float)))
        vals = {}
        for m in months:
            sv = (shape or {}).get(m)
            vals[m] = round(annual * float(sv) / tot, 2) if (sv and tot > 0) else None
    # 舍入差 → 最后一个有值月
    keys = [m for m in months if vals.get(m) is not None]
    if keys:
        s = sum(vals[m] for m in keys[:-1])
        vals[keys[-1]] = round(annual - s, 2)
    return vals


def close_sum(months):
    """有值月之和（None 跳过）。"""
    return round(sum(v for v in (months or {}).values() if v is not None), 2)


# ────────────────────────────────────────────── 读取
def load():
    """读新权威源；返回 dict 或 None（文件缺失/损坏）。"""
    if not os.path.exists(TARGET_SRC):
        return None
    try:
        with io.open(TARGET_SRC, encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return None


def is_authoritative():
    return load() is not None


def dept_annual(sd, dept):
    d = ((sd or {}).get('departments') or {}).get(dept) or {}
    return d.get('target_annual')


def dept_months(sd, dept):
    """部门 12 个月目标（万元）→ {月: 万}；无 → （回退）空 dict。"""
    d = ((sd or {}).get('departments') or {}).get(dept) or {}
    return dict(d.get('target_monthly') or {})


def region_months(sd, dept, region):
    d = ((sd or {}).get('departments') or {}).get(dept) or {}
    r = (d.get('regions') or {}).get(region) or {}
    return dict(r.get('target_monthly') or {})


def region_annual(sd, dept, region):
    d = ((sd or {}).get('departments') or {}).get(dept) or {}
    r = (d.get('regions') or {}).get(region) or {}
    return r.get('target_annual')


def people_map(sd):
    """姓名 → person dict（含离职者；用于归并/合账）。"""
    return {p.get('name'): p for p in ((sd or {}).get('people') or []) if p.get('name')}


def merge_map(sd):
    """离职/管培归属：被并人 → 归属经理（来自 people[].merged_into）。"""
    m = {}
    for p in ((sd or {}).get('people') or []):
        if p.get('merged_into'):
            m[p['name']] = p['merged_into']
    return m


def person_months_for(sd, name, rule=None):
    """返回该人 12 月目标（万元）；取文件里显式 target_monthly，缺则由规则现摊。"""
    pm = people_map(sd)
    p = pm.get(name)
    if not p:
        return {}
    rule = rule or ((sd or {}).get('meta') or {}).get('monthly_rule') or 'shape'
    tm = p.get('target_monthly')
    if tm:
        return dict(tm)
    annual = p.get('target_annual')
    dept, region = p.get('dept'), p.get('region')
    shape = region_months(sd, dept, region) or dept_months(sd, dept)
    return taper(annual, shape, rule, p.get('monthly_manual'))


def total_annual(sd):
    return sum(v for v in (dept_annual(sd, d) or 0 for d in ((sd or {}).get('departments') or {})))


def check_closure(sd):
    """三级闭合自检：人 → 区域 → 部门 → 全司。返回 (ok, report dict)。"""
    rep = {'depts': {}, 'regions': {}, 'total': {}, 'errors': []}
    depts = (sd or {}).get('departments') or {}
    grand_annual = 0.0
    grand_mon = {m: 0.0 for m in MONTHS}
    for dept, dv in depts.items():
        da = dv.get('target_annual')
        dm = dv.get('target_monthly') or {}
        ds = close_sum(dm)
        grand_annual += float(da or 0)
        for m in MONTHS:
            grand_mon[m] += float(dm.get(m) or 0)
        rows = [p for p in ((sd or {}).get('people') or [])
                if p.get('dept') == dept and p.get('count_in_stats') and p.get('target_annual') is not None]
        ppl_sum = round(sum(float(p['target_annual']) for p in rows), 2)
        rep['depts'][dept] = {'annual': da, 'monthly_sum': ds, 'people_sum': ppl_sum,
                              'people_n': len(rows),
                              'unallocated': dv.get('unallocated'),
                              'alloc_diff': round(float(da or 0) - ppl_sum - float(dv.get('unallocated') or 0), 2)}
        # 部门「12 月目标合计 = 年度」：容忍 ≤0.05（权威月度序列源自元→万元 2 位小数舍入，
        # 逐月 rounding 不可逆；若强行把差额塞进 12 月会改变 Q4 展示口径 → 反而漂移）
        if ds is not None and abs(ds - float(da or 0)) > 0.05:
            rep['errors'].append('部门 %s 月度合计 %s != 年度 %s' % (dept, ds, da))
        if abs(rep['depts'][dept]['alloc_diff']) > 0.05:
            rep['errors'].append('部门 %s 年度 %s != Σ人 %s + 未分摊 %s'
                                 % (dept, da, ppl_sum, dv.get('unallocated')))
        # 区域（若有）
        for rg, rv in (dv.get('regions') or {}).items():
            ra, rm = rv.get('target_annual'), (rv.get('target_monthly') or {})
            rppl = [p for p in ((sd or {}).get('people') or [])
                    if p.get('dept') == dept and p.get('region') == rg
                    and p.get('count_in_stats') and p.get('target_annual') is not None]
            rp_sum = round(sum(float(p['target_annual']) for p in rppl), 2)
            rep['regions']['%s/%s' % (dept, rg)] = {'annual': ra, 'monthly_sum': close_sum(rm),
                                                    'people_sum': rp_sum, 'people_n': len(rppl)}
            if abs(close_sum(rm) - float(ra or 0)) > 1e-6:
                rep['errors'].append('区域 %s/%s 月度合计 %s != 年 %s' % (dept, rg, close_sum(rm), ra))
            if abs(rp_sum - float(ra or 0)) > 1e-6:
                rep['errors'].append('区域 %s/%s 逐人合计 %s != 年 %s' % (dept, rg, rp_sum, ra))
    rep['total'] = {'annual': grand_annual, 'monthly_sum': round(sum(grand_mon.values()), 2),
                    'dept_sum': round(sum((rep['depts'][d]['annual'] or 0) for d in rep['depts']), 2)}
    if abs(rep['total']['monthly_sum'] - grand_annual) > 0.05:
        rep['errors'].append('全司月度合计 %s != 年 %s' % (rep['total']['monthly_sum'], grand_annual))
    if abs(rep['total']['dept_sum'] - grand_annual) > 0.05:
        rep['errors'].append('全司年度 %s != Σ部门 %s' % (grand_annual, rep['total']['dept_sum']))
    return (not rep['errors']), rep


if __name__ == '__main__':
    sd = load()
    if not sd:
        print('❌ 找不到权威源', TARGET_SRC)
        raise SystemExit(1)
    ok, rep = check_closure(sd)
    print(json.dumps(rep, ensure_ascii=False, indent=2))
    print('闭合自检:', 'PASS' if ok else 'FAIL')
