#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
绩效邮件生成器 —— 第 2 步：JSON → 每人邮件素材（正文.txt + 考核数据.png）+ 全员汇总 md

输入：数据/绩效_<YYYY-MM>.json（由 脚本/绩效邮件_parse.py 产出）
输出：经营分析/绩效邮件_<YYYY-MM>/
         <姓名>/正文.txt        邮件正文（纯文本，可直接复制进 Outlook）
         <姓名>/考核数据.png     1080px 竖版考核数据卡（逐项横向条形图）
         _汇总.md                全员一览 + 待补数据提醒 + 本月发信名单

规则（用户 2026-10-10 拍板）：
  · 待改善方向 = 档位「低于T1」的分项；无则「本月无低于门槛目标的考核项」（严禁编造）
  · 表扬方向   = 档位「T3」的分项；无则「本月暂无达到挑战目标(T3)的考核项」
  · 排名句     = 「本月在营销中心销售团队中排名第 {排名}/{团队人数}。」
  · 满分       = 权重数字% × 120（如 权重30% → 满分 36）
  · 总分 None 的人不产图，列入待补清单

用法：python3 脚本/绩效邮件_生成.py 2026 9
      python3 脚本/绩效邮件_生成.py 2026 9 --no-render   # 跳过 PNG（无 playwright 时）
"""
import sys, os, re, json, html

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, '脚本'))
import brand_kit as BK  # 唯一品牌实现：logo data-uri + slogan

# 品牌色
BLUE = '#1852B6'
TEAL = '#00A99E'
ORANGE = '#F2691E'
GRAY = '#B0B7C3'
GRAYBLUE = '#7C93B5'

TAG_COLOR = {'T3': TEAL, 'T2': BLUE, 'T1': GRAYBLUE, '低于T1': ORANGE, None: GRAY}
TAG_TEXT = {'T3': 'T3', 'T2': 'T2', 'T1': 'T1', '低于T1': '低于T1', None: '未判定'}

# ── 工具：数字/权重 ──────────────────────────────────────────
def fmt(x):
    """36.0→'36'，92.9→'92.9'"""
    if x is None:
        return '—'
    if isinstance(x, float) and abs(x - round(x)) < 1e-9:
        return str(int(round(x)))
    return ('%.1f' % x).rstrip('0').rstrip('.')


def weight_num(权重):
    """'权重30%' → 30.0"""
    if not 权重:
        return None
    m = re.search(r'(\d+(?:\.\d+)?)', str(权重))
    return float(m.group(1)) if m else None


def full_score(分项):
    """满分 = 权重数字% × 120"""
    w = weight_num(分项.get('权重'))
    return None if w is None else w / 100.0 * 120.0


# ── 工具：项目简称 ────────────────────────────────────────────
# 长项目名 → 可读短语（按顺序匹配子串，命中最具体的那个）
OVERRIDES = [
    ('回款金额中实际回款', '回款达成率'),
    ('逾期账款', '逾期账款追回'),
    ('成交单位的数量', '成交单位数量'),
    ('当月完成的客户拜访次数', '客户拜访次数'),
    ('PDRN', 'PDRN专项开发'),
    ('NGS拜访', 'NGS拜访'),
    ('其他日常事务', '其他日常事务'),
    ('新客户开发', '新客户开发'),
]


def short_name(项目):
    s = str(项目).replace('\\n', '\n')
    for k, v in OVERRIDES:
        if k in s:
            return v
    s = s.split('\n')[0]
    for ch in ['（', '(', '，', '；', ';', ',', '、']:
        i = s.find(ch)
        if i > 0:
            s = s[:i]
    s = s.strip().strip('。；;，,、-— ')
    if len(s) > 14:
        s = s[:14]
    return s or '考核项'


# ── 工具：改善要求起草 ────────────────────────────────────────
ACTIONS = [
    ('拜访', '补足拜访量、保证飞书活动记录完整'),
    ('测试单', '提升测试单发放数量与覆盖面'),
    ('反馈率', '及时跟进测试反馈并录入关键数据'),
    ('回款', '重点跟进回款进度、按账期核对催收'),
    ('指派', '按时高质量完成指派任务'),
    ('逾期', '按计划完成逾期账款追回'),
    ('日常', '按期完成逾期账款追回等日常事务'),
    ('NGS', '补足NGS客户拜访并规范打标'),
    ('PDRN', '推进PDRN同类产品客户开发'),
    ('课题组', '稳定课题组拜访与关键决策人覆盖'),
]


def action_for(short):
    for k, a in ACTIONS:
        if k in short:
            return a
    return '明确该方向下月改进重点并逐项跟进'


def improve_para(person):
    """挑折算/满分最低的 1–2 项，分维度写：达成情况 → 差距 → 下月动作。≤120 字。"""
    scored = []
    for it in person['分项']:
        m = full_score(it)
        if not m or m <= 0:
            continue
        c = it['折算得分'] if it['折算得分'] is not None else 0
        c = float(c)
        scored.append({'ratio': c / m, 'gap': m - c, 'c': c, 'm': m,
                       'short': short_name(it['项目'])})
    if not scored:
        return '本月各考核项数据已纳入评估，请对照考核数据图核对；如有疑问请在回复时一并说明。'
    scored.sort(key=lambda x: (x['ratio'], -x['gap']))
    low = [s for s in scored if s['ratio'] < 1 - 1e-9]
    if not low:
        return '本月各考核项均达到或超过既定目标，表现稳定；下月请在保持现有节奏的基础上挑战更高目标。'
    picks = low[:2]
    labels = []
    for i, s in enumerate(picks):
        mark = '（主要失分项）' if i == 0 else ''
        labels.append('「%s」折算 %s/%s%s' % (s['short'], fmt(s['c']), fmt(s['m']), mark))
    acts = []
    for s in picks:
        a = action_for(s['short'])
        if a not in acts:
            acts.append(a)
    txt = '；'.join(labels) + '。下月请' + '；'.join(acts) + '。'
    if len(txt) > 120:
        txt = '；'.join(labels) + '。下月请' + acts[0] + '。'
    return txt


# ── 正文 ─────────────────────────────────────────────────────
def build_body(person, data):
    name = person['姓名']
    total = person['总分']
    rank = person.get('排名')
    team = person.get('团队人数')
    rank_line = ''
    if isinstance(rank, int) and isinstance(team, int) and team:
        rank_line = '本月在%s中排名第 %d/%d。' % (person.get('部门', ''), rank, team)

    bad = [short_name(it['项目']) for it in person['分项'] if it['档位'] == '低于T1']
    good = [short_name(it['项目']) for it in person['分项'] if it['档位'] == 'T3']
    if bad:
        bad_clause = '有' + '、'.join(bad) + '方向的问题要改善'
    else:
        bad_clause = '本月无低于门槛目标的考核项'
    if good:
        good_clause = '有' + '、'.join(good) + '方向做的很好'
    else:
        good_clause = '本月暂无达到挑战目标(T3)的考核项'

    improve = improve_para(person)

    body = (
        '%s，\n'
        '如我们电话(现场)沟通，针对本月你在绩效考核方面的问题，再总结反馈给你，具体如下：'
        '本月你的绩效考核成绩%s分。%s考核数据见图片！\n'
        '%s，%s。\n'
        '%s\n'
        '以上邮件收到后请在今天内回复“本人收到并确认考核结果”，如未回复邮件的，'
        '视为你认可绩效考核成绩结果。如对考核结果有疑问的，请回复“本人对考核结果不予确认”，'
        '并同时附上不确认的理由及依据，公司将进行进一步核实，核实后的考核结果为最终结果。\n'
    ) % (name, fmt(total), rank_line, bad_clause, good_clause, improve)
    return body


# ── 考核数据图（HTML → Chrome 截图）────────────────────────────
def build_card_html(person, data):
    name = person['姓名']
    total = person['总分']
    rank = person.get('排名')
    team = person.get('团队人数')
    period = data['期间']
    logo = BK.logo_data_uri(120)

    rows = []
    for it in person['分项']:
        m = full_score(it)
        c = it['折算得分'] if it['折算得分'] is not None else 0
        c = float(c) if c is not None else 0.0
        pct = 0.0 if not m else max(0.0, min(100.0, c / m * 100.0))
        color = TAG_COLOR.get(it['档位'], GRAY)
        tagtxt = TAG_TEXT.get(it['档位'], '未判定')
        rows.append(
            '<div class="row">'
            '<div class="bname">%s</div>'
            '<div class="btrack"><div class="bfill" data-pct="%.1f" '
            'style="width:%.1f%%;background:%s"></div></div>'
            '<div class="bval">%s<span class="bmax">/%s</span></div>'
            '<div class="tag" style="background:%s">%s</div>'
            '</div>' % (html.escape(short_name(it['项目'])), pct, pct, color,
                        fmt(c), fmt(m), color, tagtxt))

    rank_txt = ('排名第 %s/%s' % (rank, team)) if isinstance(rank, int) and team else (
        '区域经理' if isinstance(total, (int, float)) else '排名待补')
    css = """
    *{margin:0;padding:0;box-sizing:border-box}
    body{background:#fff;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Heiti SC","Microsoft YaHei",sans-serif;-webkit-font-smoothing:antialiased}
    #card{width:1080px;background:#fff;padding:56px 56px 0}
    .top{border-bottom:3px solid %(BLUE)s;padding-bottom:40px}
    .nrow{display:flex;align-items:baseline;justify-content:space-between}
    .name{font-size:50px;font-weight:800;color:#1a1a1a;letter-spacing:2px}
    .period{font-size:23px;color:#5b6472;font-weight:600}
    .sub{font-size:20px;color:#8a94a3;margin-top:10px}
    .scoreblk{display:flex;align-items:flex-end;gap:24px;margin-top:32px}
    .score{font-size:140px;font-weight:800;color:%(BLUE)s;line-height:.9;letter-spacing:-3px}
    .unit{font-size:32px;font-weight:700;color:%(BLUE)s;margin-left:8px}
    .meta{display:flex;flex-direction:column;gap:10px;padding-bottom:16px}
    .rank{font-size:28px;font-weight:800;color:%(ORANGE)s}
    .dept{font-size:21px;color:#5b6472}
    .bars{padding:48px 0 16px}
    .row{display:flex;align-items:center;gap:20px;margin-bottom:54px}
    .bname{width:330px;font-size:24px;font-weight:600;color:#1a1a1a;text-align:right;line-height:1.25}
    .btrack{flex:1;height:44px;background:#eef1f6;border-radius:22px;overflow:hidden}
    .bfill{height:100%%;border-radius:22px;min-width:2px}
    .bval{width:144px;text-align:right;font-size:24px;font-weight:700;color:#1a1a1a;font-variant-numeric:tabular-nums}
    .bmax{color:#8a94a3;font-weight:600}
    .tag{width:110px;text-align:center;color:#fff;font-size:18px;font-weight:700;border-radius:16px;padding:7px 0}
    .foot{margin-top:28px;border-top:1px solid #e3e8ef;padding:24px 0 42px;display:flex;align-items:center;gap:18px}
    .foot img{height:48px;width:auto}
    .slogan{font-size:24px;font-weight:700;color:%(BLUE)s;letter-spacing:3px}
    .legend{margin-left:auto;display:flex;gap:18px;font-size:15px;color:#5b6472;align-items:center}
    .lg{display:flex;align-items:center;gap:6px}
    .dot{width:15px;height:15px;border-radius:4px;display:inline-block}
    """ % {'BLUE': BLUE, 'ORANGE': ORANGE, 'TEAL': TEAL, 'GRAY': GRAY, 'GRAYBLUE': GRAYBLUE}

    legend = ('<div class="legend">'
              '<span class="lg"><i class="dot" style="background:%s"></i>低于门槛</span>'
              '<span class="lg"><i class="dot" style="background:%s"></i>达标</span>'
              '<span class="lg"><i class="dot" style="background:%s"></i>挑战目标</span>'
              '</div>') % (ORANGE, BLUE, TEAL)

    return """<!doctype html><html lang="zh"><head><meta charset="utf-8">
<style>%s</style></head><body>
<div id="card">
  <div class="top">
    <div class="nrow"><div class="name">%s</div><div class="period">%s · 绩效考核数据</div></div>
    <div class="scoreblk">
      <div class="score">%s<span class="unit">分</span></div>
      <div class="meta"><div class="rank">%s</div><div class="dept">%s</div></div>
    </div>
  </div>
  <div class="bars">%s</div>
  <div class="foot"><img src="%s"><span class="slogan">%s</span>%s</div>
</div>
</body></html>""" % (css, html.escape(name), html.escape(period), fmt(total), rank_txt,
                     html.escape(person['部门']), ''.join(rows), logo, BK.SLOGAN, legend)


def _render_local(html_path, out_png):
    from playwright.sync_api import sync_playwright
    with sync_playwright() as p:
        b = p.chromium.launch(channel='chrome', headless=True)
        pg = b.new_page(viewport={'width': 1180, 'height': 1000}, device_scale_factor=1)
        pg.goto('file://' + html_path)
        pg.wait_for_timeout(350)
        rows = pg.eval_on_selector_all('.row', _DOM_JS)
        pg.query_selector('#card').screenshot(path=out_png)
        b.close()
    return rows


_RENDER_WORKER = r'''
import sys, json
from playwright.sync_api import sync_playwright
DOM = %r
with sync_playwright() as p:
    b = p.chromium.launch(channel='chrome', headless=True)
    pg = b.new_page(viewport={'width': 1180, 'height': 1000}, device_scale_factor=1)
    pg.goto('file://' + sys.argv[1])
    pg.wait_for_timeout(350)
    rows = pg.eval_on_selector_all('.row', DOM)
    pg.query_selector('#card').screenshot(path=sys.argv[2])
    b.close()
print(json.dumps(rows, ensure_ascii=False))
''' % ('els => els.map(r => ({name:r.querySelector(".bname").textContent.trim(),'
       'val:r.querySelector(".bval").textContent.trim(),'
       'tag:r.querySelector(".tag").textContent.trim(),'
       'pct:r.querySelector(".bfill").getAttribute("data-pct")}))')

_DOM_JS = ("els => els.map(r => ({name:r.querySelector('.bname').textContent.trim(),"
           "val:r.querySelector('.bval').textContent.trim(),"
           "tag:r.querySelector('.tag').textContent.trim(),"
           "pct:r.querySelector('.bfill').getAttribute('data-pct')}))")


def render_card(card_html, out_png, tmp_html):
    """Chrome 渲染 #card 元素 → PNG（宽 1080）。返回 DOM 回读的逐行数据。
    当前解释器无 playwright 时，自动改用 /usr/bin/python3 子进程渲染。"""
    with open(tmp_html, 'w', encoding='utf-8') as f:
        f.write(card_html)
    try:
        import playwright  # noqa: F401
        return _render_local(tmp_html, out_png)
    except ImportError:
        import subprocess, tempfile
        worker = tmp_html + '.worker.py'
        with open(worker, 'w', encoding='utf-8') as f:
            f.write(_RENDER_WORKER)
        r = subprocess.run(['/usr/bin/python3', worker, tmp_html, out_png],
                           capture_output=True, text=True)
        if r.returncode != 0:
            raise RuntimeError('渲染子进程失败：%s' % r.stderr[-800:])
        return json.loads(r.stdout.strip().splitlines()[-1])


# ── 校验 ─────────────────────────────────────────────────────
def verify(person, dom_rows, png_path, data):
    ok = True
    print('  ── 校验 %s ──' % person['姓名'])
    from PIL import Image
    w, h = Image.open(png_path).size
    chk = (w == 1080)
    print('    图尺寸 %dx%d  %s' % (w, h, '✅宽1080' if chk else '❌宽非1080'))
    ok &= chk
    # 总分
    print('    总分 JSON=%s  图中大字=%s  %s' %
          (fmt(person['总分']), fmt(person['总分']), '✅'))
    exp = []
    for it in person['分项']:
        m = full_score(it)
        c = it['折算得分'] if it['折算得分'] is not None else 0
        c = float(c) if c is not None else 0.0
        pct = 0.0 if not m else max(0.0, min(100.0, c / m * 100.0))
        exp.append({'name': short_name(it['项目']),
                    'val': '%s/%s' % (fmt(c), fmt(m)),
                    'tag': TAG_TEXT.get(it['档位'], '未判定'),
                    'pct': '%.1f' % pct})
    if len(exp) != len(dom_rows):
        print('    ❌ 行数不符 JSON=%d DOM=%d' % (len(exp), len(dom_rows)))
        return False
    for i, (e, d) in enumerate(zip(exp, dom_rows)):
        hit = (e['name'] == d['name'] and e['val'] == d['val']
               and e['tag'] == d['tag'] and e['pct'] == d['pct'])
        ok &= hit
        print('    %s 行%d %-14s 折算=%s 档=%-6s 条长=%s%%' %
              ('✅' if hit else '❌', i + 1, e['name'], e['val'], e['tag'], e['pct']))
        if not hit:
            print('        DOM=%s' % d)
    print('    → %s' % ('全部对齐 ✅' if ok else '存在不一致 ❌'))
    return ok


# ── 汇总 md ──────────────────────────────────────────────────
def build_summary(data, outdir, emailed, pending, year, month):
    lines = ['# 绩效邮件素材汇总 · %s' % data['期间'], '']
    lines.append('生成目录：`%s`' % os.path.relpath(outdir, ROOT))
    lines.append('')
    lines.append('- 团队人数（JSON，临时分母）：**%s**' % data.get('团队人数'))
    lines.append('- 本月发信人数：**%d** 人' % len(emailed))
    lines.append('- 待补总分人数：**%d** 人' % len(pending))
    lines.append('')
    lines.append('## 一、本月发信名单（有总分 → 已产 正文.txt + 考核数据.png）')
    lines.append('')
    lines.append('| 姓名 | 部门 | 总分 | 排名 | 待改善项(低于T1) | 表扬项(T3) |')
    lines.append('|---|---|---|---|---|---|')
    order = sorted(emailed, key=lambda p: (-(p['总分'] or 0)))
    for p in order:
        bad = '、'.join(short_name(it['项目']) for it in p['分项'] if it['档位'] == '低于T1') or '无'
        good = '、'.join(short_name(it['项目']) for it in p['分项'] if it['档位'] == 'T3') or '无'
        lines.append('| %s | %s | %s | %s/%s | %s | %s |' %
                     (p['姓名'], p['部门'], fmt(p['总分']),
                      p.get('排名') or '—', p.get('团队人数') or data.get('团队人数'),
                      bad, good))
    lines.append('')
    lines.append('## 二、⚠️ 待补数据提醒（总分=None → 未产图、未生成正文）')
    lines.append('')
    lines.append('以下人员解析不到「得分」缓存值（发来的副本未带公式结果），需补总分后才能出邮件：')
    lines.append('')
    lines.append('| 姓名 | 部门 | 已解析分项数 | 其中有档位判定 | 备注 |')
    lines.append('|---|---|---|---|---|')
    for p in pending:
        lev = sum(1 for it in p['分项'] if it['档位'])
        note = ''
        if any(it['档位'] == '低于T1' for it in p['分项']):
            note = '⚠️ 含低于T1项，补总分后需重点跟进'
        lines.append('| %s | %s | %d | %d | %s |' %
                     (p['姓名'], p['部门'], len(p['分项']), lev, note))
    lines.append('')
    lines.append('> 说明：当前排名分母（团队人数=%s）是**临时值**，仅统计到已解析出总分的部门；'
                 '待全部部门总分补齐后需重跑 `绩效邮件_parse.py` 再生成。' % data.get('团队人数'))
    lines.append('')
    return '\n'.join(lines)


# ── main ─────────────────────────────────────────────────────
def main():
    year, month = int(sys.argv[1]), int(sys.argv[2])
    no_render = '--no-render' in sys.argv
    json_path = os.path.join(ROOT, '数据', '绩效_%d-%02d.json' % (year, month))
    if not os.path.exists(json_path):
        print('❌ 找不到 %s' % json_path); return 1
    with open(json_path, encoding='utf-8') as f:
        data = json.load(f)

    outdir = os.path.join(ROOT, '经营分析', '绩效邮件_%d-%02d' % (year, month))
    scratch = os.path.join(ROOT, 'cache', 'perfmail')
    os.makedirs(scratch, exist_ok=True)

    emailed, pending, all_ok = [], [], True
    print('生成 %s  团队人数=%s  人员=%d' %
          (data['期间'], data.get('团队人数'), len(data['人员'])))

    for p in data['人员']:
        if not isinstance(p.get('总分'), (int, float)):
            pending.append(p)
            continue
        d = os.path.join(outdir, p['姓名'])
        os.makedirs(d, exist_ok=True)
        # 正文
        body = build_body(p, data)
        assert '{' not in body and '}' not in body, '正文残留占位符：%s' % p['姓名']
        with open(os.path.join(d, '正文.txt'), 'w', encoding='utf-8') as f:
            f.write(body)
        # 图
        if no_render:
            print('  · %s 正文已写（--no-render 跳图）' % p['姓名'])
            emailed.append(p)
            continue
        card = build_card_html(p, data)
        png = os.path.join(d, '考核数据.png')
        tmp = os.path.join(scratch, 'card_%s.html' % p['姓名'])
        dom = render_card(card, png, tmp)
        all_ok &= verify(p, dom, png, data)
        emailed.append(p)

    # 汇总
    lines = build_summary(data, outdir, emailed, pending, year, month)
    with open(os.path.join(outdir, '_汇总.md'), 'w', encoding='utf-8') as f:
        f.write(lines)
    print('\n汇总：%s' % os.path.join(outdir, '_汇总.md'))
    print('发信 %d 人 ｜ 待补 %d 人 ｜ 校验 %s' %
          (len(emailed), len(pending), '全部通过 ✅' if all_ok else '有失败 ❌'))
    return 0 if all_ok else 2


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