#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成《P0 待办类型复核》自包含 HTML（38 条，旧类→新类逐条可判定）。

旧值取迁移前备份（按 id 匹配），不靠回忆。
输出：P0待办类型复核.html（品牌套件注入 + 自检）
"""
import io
import json
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__))))
import brand_kit as BK  # noqa: E402

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
BAK = os.path.join(ROOT, '_bak_todo_p0_20261007-212235', 'todos.json')
OUT = os.path.join(ROOT, 'P0待办类型复核.html')

OLD_LABEL = {'prep': '访前准备', 'post': '访后跟进', 'complaint': '客诉',
             'custom': '定制需求', 'meeting': '会议邀约', 'travel': '出差计划', 'other': '其他'}
NEW_LABEL = {'internal_project': '对内·立项跟进', 'internal_complaint': '对内·客诉跟进',
             'internal_test_plan': '对内·测试方案', 'internal_other': '对内·其他（客户需求）',
             'external_visit': '对外·拜访计划', 'external_test_req': '对外·测试需求',
             'external_test_result': '对外·测试结果', 'external_project': '对外·项目进度',
             'external_appointment': '对外·邀约及计划'}
NEW_TYPES = ['external_visit', 'external_test_req', 'external_test_result', 'external_project',
             'external_appointment', 'internal_project', 'internal_complaint',
             'internal_test_plan', 'internal_other']

# 建议改动（按文本前缀匹配，锚在内容上而不是序号）
SUGGEST = [
    ('以下是天隆质量客诉的内部排查任务清单', 'internal_complaint',
     '正文是「内部排查任务清单…今天内分派到位」，属对内协同；旧类选了「访后跟进」被迁到对外·项目进度，'
     '按内容更像一条客诉的后续跟进。'),
    ('【研发+生产】评估搭建 mNGS 做 NA 控制方案', 'internal_project',
     '这是研发/生产的方案立项评估，不对应某一个客户的客诉跟进，更像一条内部立项事项。'),
    ('【赵云浩+生产】圣湘 FM2181-4-NA-020303', 'internal_other',
     '正文是「研发采购发不出、必须给可发货时间节点」——交期/备货协调，不是客诉本身。'),
    ('【刘子研】天隆 刘小辉 PM202', 'external_test_result',
     '正文是「复测验证结果跟进」，属测试完成后回传/确认结果，落「对外·测试结果」更准。'),
    ('【赵云浩】万孚 FE20-50', 'external_test_req',
     '正文是「客户本周测试…目标做到与诺唯赞一致」，是客户侧的测试推进需求，不是客诉。'),
]
# 启发式提示里我判断「可保留」的（正文若含关键词，请以你的判断为准）
KEEP_NOTE = [
    ('【研发/质量】搭建 16S/18S 与 NGS 的对标数据', '正文明确写「用于 NA 类客诉处理时向客户展示」，服务客诉处理，保留客诉跟进合理。'),
    ('【刘子研+研发】复星 管慧慧 STD 项目', '项目推进中的测试环节，归「对外·项目进度」合理。'),
    ('【赵云浩】郑州安图：E10-000725 采购订单', '价格拉扯属项目推进中的商务环节，归「对外·项目进度」可接受。'),
]


def norm(t):
    return ' '.join((t or '').split())


def esc(s):
    return (str(s if s is not None else '')
            .replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
            .replace('"', '&quot;'))


def main():
    bak = json.load(io.open(BAK, encoding='utf-8'))
    cur = json.load(io.open(os.path.join(ROOT, '数据', 'todos.json'), encoding='utf-8'))
    old = {}
    for u, lst in (bak.get('users') or {}).items():
        for t in (lst or []):
            old[t.get('id')] = t.get('cat')
    for t in (bak.get('closed') or []):
        old[t.get('id')] = t.get('cat')
    arch = {t.get('id') for t in (cur.get('closed') or [])}

    rows = []
    for u, lst in (cur.get('users') or {}).items():
        for t in (lst or []):
            rows.append((u, t))
    rows.sort(key=lambda x: (x[1].get('created') or '', x[1].get('id') or ''))

    items = []
    for i, (owner, t) in enumerate(rows, 1):
        txt = norm(t.get('text'))
        sug = ''
        for pre, to, why in SUGGEST:
            if txt.startswith(pre):
                sug = to
                items.append({'i': i, 'owner': owner, 'text': txt,
                              'old': old.get(t.get('id'), '?'), 'type': t.get('type'),
                              'sug': to, 'why': why, 'arch': t.get('id') in arch})
                break
        if not sug:
            note = ''
            for pre, w in KEEP_NOTE:
                if txt.startswith(pre):
                    note = w
                    break
            items.append({'i': i, 'owner': owner, 'text': txt,
                          'old': old.get(t.get('id'), '?'), 'type': t.get('type'),
                          'sug': '', 'why': note, 'arch': t.get('id') in arch})

    n_sug = len([x for x in items if x['sug']])
    dist = {}
    for x in items:
        k = '%s → %s' % (x['old'], x['type'])
        dist[k] = dist.get(k, 0) + 1
    blank = [x for x in items if x['type'] == 'internal_other' and len(x['text']) < 6]

    opts = lambda sel: ''.join('<option value="%s"%s>%s</option>' % (k, ' selected' if k == sel else '', NEW_LABEL[k])
                              for k in NEW_TYPES)

    trs = []
    for x in items:
        trs.append(
            '<tr%s>' % (' class="sug"' if x['sug'] else '') +
            '<td class="c">%d</td>' % x['i'] +
            '<td class="c">%s</td>' % esc(x['owner']) +
            '<td>%s%s</td>' % (esc(x['text']),
                               '<div class="why">💡 %s</div>' % esc(x['why']) if x['why'] else '') +
            '<td class="c old">%s</td>' % esc(OLD_LABEL.get(x['old'], x['old'])) +
            '<td class="c now">%s</td>' % esc(NEW_LABEL.get(x['type'], x['type'])) +
            ('<td class="c bad">建议改</td>' if x['sug'] else '<td class="c ok">保留</td>') +
            '<td class="c"><select class="pk" data-i="%d" data-cur="%s">' % (x['i'], x['type']) +
            '<option value="">保持现状</option>' + opts(x['sug'] or '') + '</select></td>' +
            '<td class="c">%s</td>' % ('🗄 是' if x['arch'] else '—') +
            '</tr>')

    dist_html = ''.join('<tr><td>%s</td><td class="c">%d</td></tr>' % (esc(k), v) for k, v in sorted(dist.items()))

    html = '''<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>P0 待办类型复核 · 38 条</title>
<style>
:root{--pri:#1F4E9C;--ikb:#002FA7;--ink:#1f2328;--sub:#5f6368;--line:#e5e7eb;--bg:#f6f8fb}
*{box-sizing:border-box}
body{margin:0;font:14px/1.7 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--ink);background:var(--bg)}
.wrap{max-width:1280px;margin:0 auto;padding:22px}
.hero{background:linear-gradient(120deg,#1F4E9C,#002FA7);color:#fff;border-radius:14px;padding:24px 26px;margin-bottom:18px}
.hero h1{margin:0 0 8px;font-size:23px}
.hero p{margin:4px 0;opacity:.93;font-size:13.5px}
.card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin-bottom:16px}
.card h2{font-size:16px;margin:0 0 10px;padding-left:10px;border-left:4px solid var(--pri)}
.card h3{font-size:14px;margin:14px 0 6px;color:var(--pri)}
.kpi{display:flex;gap:12px;flex-wrap:wrap}
.kbox{flex:1;min-width:150px;border:1px solid var(--line);border-radius:10px;padding:12px 14px;border-left:4px solid var(--pri)}
.kbox .n{font-size:26px;font-weight:800;color:var(--pri);line-height:1.2}
.kbox .t{font-size:12px;color:var(--sub)}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top;text-align:left}
th{background:#f2f5fa;font-size:12px;color:var(--sub);position:sticky;top:0;z-index:2}
td.c{text-align:center;white-space:nowrap}
tr.sug{background:#fff8f2}
td.old{color:var(--sub)}
td.now{color:var(--ikb);font-weight:700}
td.bad{color:#d93025;font-weight:700}
td.ok{color:#0d904f}
.why{font-size:12px;color:#8a6d3b;background:#fffdf5;border-left:3px solid #f0c36d;padding:4px 8px;margin-top:4px;border-radius:0 4px 4px 0}
select.pk{padding:3px 6px;font-size:12px;border:1px solid var(--line);border-radius:6px;max-width:150px}
.bar{position:sticky;bottom:0;background:#fff;border-top:1px solid var(--line);padding:12px 18px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;border-radius:0 0 12px 12px}
.btn{background:var(--pri);color:#fff;border:none;border-radius:8px;padding:9px 18px;font-size:13.5px;font-weight:700;cursor:pointer}
.btn.ghost{background:#fff;color:var(--pri);border:1px solid var(--pri)}
pre.out{margin:0;width:100%;max-height:210px;overflow:auto;background:#f6f8fb;border:1px solid var(--line);border-radius:8px;padding:10px;font-size:12px;white-space:pre-wrap;display:none}
.warn{background:#fff4f4;border:1px solid #ffd6d6;border-radius:10px;padding:12px 14px;font-size:13px}
.note{font-size:12.5px;color:var(--sub)}
</style></head><body>
<div class="wrap">

<div class="hero">
  <h1>P0 待办类型复核 · 38 条</h1>
  <p>新模型：<b>对内 4 类 + 对外 5 类</b>（9 类）。迁移是把旧 7 类<b>机械映射</b>过来的，旧类是当时录入人自己选的。</p>
  <p>这一页让你<b>逐条确认</b>：只有「建议改」的 5 条需要你拍板，其余 33 条按现状即可。</p>
</div>

<div class="card">
  <h2>一、先说清口径（两处要更正）</h2>
  <div class="kpi">
    <div class="kbox"><div class="n">38</div><div class="t">真实待办条数（复核对象）</div></div>
    <div class="kbox"><div class="n">8</div><div class="t">归档快照（与其中 8 条<b>同 id</b> 的重复，不重复计）</div></div>
    <div class="kbox"><div class="n">5</div><div class="t">建议改动（需你拍板）</div></div>
    <div class="kbox"><div class="n">33</div><div class="t">按现状保留</div></div>
  </div>
  <p class="note" style="margin-top:12px">
    ① 我上一轮说的「46 条」是<b>把归档快照重复计了</b>：文件里 38 条待办 + 8 条归档，而归档的 8 条与待办列表里
    其中 8 条是<b>同一个 id</b>（关闭时复制了一份到归档）。真实对象就是 <b>38 条</b>。<br>
    ② 旧值不是我猜的：迁移把 <code>cat</code> 改写成了新值，所以旧值取自<b>迁移前备份</b>
    <code>_bak_todo_p0_20261007-212235/todos.json</code>，按 id 逐条匹配。
  </p>
  <h3>旧类 → 新类 的实际分布</h3>
  <table style="max-width:520px"><thead><tr><th>映射</th><th class="c" style="width:70px">条数</th></tr></thead>
  <tbody>__DIST__</tbody></table>
</div>

<div class="card">
  <h2>二、需要你拍板的 5 条（其余建议保留）</h2>
  <p class="note">判定依据是<b>正文内容</b>而非旧类：旧类由录入人当时选择，机械映射会把这个选择一并继承下来。</p>
  <table><thead><tr><th class="c" style="width:44px">#</th><th>正文</th><th class="c" style="width:120px">现类型</th><th class="c" style="width:130px">建议改为</th></tr></thead>
  <tbody>__SUG__</tbody></table>
</div>

<div class="card">
  <h2>三、全 38 条逐条复核</h2>
  <p class="note">最后一列是给你的判定下拉：<b>下拉留空 = 保持现状</b>。
  <b>5 条「建议改」的行已按我的建议预选</b>（不同意就把下拉改回「保持现状」或另选目标类型）；其余 33 行要改才动它。
  改完点底部「生成我的复核结果」再复制给我。</p>
  <table><thead><tr>
    <th class="c" style="width:44px">#</th><th class="c" style="width:64px">归属</th><th>正文</th>
    <th class="c" style="width:78px">旧类</th><th class="c" style="width:118px">新类</th>
    <th class="c" style="width:74px">我的判断</th><th class="c" style="width:150px">你的判定</th>
    <th class="c" style="width:62px">已归档</th>
  </tr></thead><tbody>__ROWS__</tbody></table>
</div>

<div class="card">
  <h2>四、顺手发现的数据问题</h2>
  <div class="warn">
    <b>1 条疑似无效数据</b>：<code>何超锦</code> 名下有一条待办正文只有一个 <code>1212</code>，
    被归到「对内·客诉跟进」。建议直接删除，或让本人补齐内容。<br>
    <b>6 条文本跨归属重复</b>：有几条待办正文一字不差地出现了两次，需要录入人确认是否重复录入（我未擅自合并）。
  </div>
</div>

<div class="card">
  <div class="bar" style="border:none;padding:0">
    <button class="btn" onclick="gen()">生成我的复核结果</button>
    <button class="btn ghost" onclick="copyOut()">复制</button>
    <span class="note" id="msg">未改动 = 全部按现状保留</span>
  </div>
  <pre class="out" id="out"></pre>
</div>

</div>
<script>
function picks(){
  var out=[], sels=document.querySelectorAll('select.pk');
  for (var i=0;i<sels.length;i++){
    var v=sels[i].value; if(!v) continue;
    out.push({i:sels[i].getAttribute('data-i'), cur:sels[i].getAttribute('data-cur'), to:v});
  }
  return out;
}
function gen(){
  var p=picks(), lines=[];
  if(!p.length){ lines.push('38 条类型复核：全部按现状保留，无需改动。'); }
  else{
    lines.push('38 条类型复核，需要改动的 '+p.length+' 条：');
    p.forEach(function(x){ lines.push('  #'+x.i+'  '+x.cur+' → '+x.to); });
    lines.push('其余 '+(38-p.length)+' 条按现状保留。');
  }
  document.getElementById('out').textContent=lines.join('\\n');
  document.getElementById('out').style.display='block';
  document.getElementById('msg').textContent='已生成 '+(p.length?p.length+' 条改动':'0 条改动');
}
function copyOut(){
  var el=document.getElementById('out');
  if(el.style.display==='none') gen();
  var t=el.textContent;
  function fb(){ var ta=document.createElement('textarea'); ta.value=t; document.body.appendChild(ta); ta.select();
    try{ document.execCommand('copy'); document.getElementById('msg').textContent='已复制'; }catch(e){ document.getElementById('msg').textContent='请手动选择复制'; }
    ta.remove(); }
  if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(t).then(function(){ document.getElementById('msg').textContent='已复制'; },fb); }
  else fb();
}
</script>
</body></html>'''

    sug_rows = ''.join(
        '<tr><td class="c">%d</td><td>%s<div class="why">%s</div></td><td class="c now">%s</td><td class="c bad">%s</td></tr>'
        % (x['i'], esc(x['text'][:110]), esc(x['why']), esc(NEW_LABEL.get(x['type'], '')), esc(NEW_LABEL.get(x['sug'], '')))
        for x in items if x['sug'])

    html = (html.replace('__DIST__', dist_html).replace('__SUG__', sug_rows).replace('__ROWS__', ''.join(trs)))
    html = BK.inject(html, theme='light', tag='营销中心')
    io.open(OUT, 'w', encoding='utf-8').write(html)

    vr = BK.verify(html)
    bad = [k for k, ok in vr.items() if not ok]
    print('✅ 生成 %s  (%.1f KB)' % (OUT, len(html.encode('utf-8')) / 1024))
    print('   brand_kit verify:', '✅ 通过' if not bad else '❌ 缺 ' + ', '.join(bad))
    print('   条数 %d ｜ 建议改 %d ｜ 纯占位/空白 %d' % (len(items), n_sug, len(blank)))
    print('   分布:', dist)


main()
