#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把「2026 浙江省自然科学基金 · 课题组级台账」融合进
《国自然与省部委基金分布_2024-2026_市场容量与目标单位》主报告。

改动：
  1) 三、省部委基金：浙江行更新为已到手的 2,669 项清单（14 类）；
  2) 三·五 KPI：2,667 → 2,669；
  3) 新增「三·六 浙江省自然 2026：课题组级结构」（2,607 个课题组）；
  4) 五、六 两节由「单位名单」改为「课题组名单 + 科研销售课题组级现状」；
  5) 七、数据缺口与八、结论同步更新（ERP 商机字段覆盖率、PI 匹配率）。

输出新文件（不覆盖原件）：..._v2.html
"""
import json
import re
from collections import Counter, defaultdict
from datetime import date

import openpyxl

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
SRC = ROOT + '/销售分析/国自然与省部委基金分布_2024-2026_市场容量与目标单位.html'
OUT = ROOT + '/销售分析/国自然与省部委基金分布_2024-2026_市场容量与目标单位_v2.html'
ERP = '/Users/liuxinyuan/Desktop/hermas输入-工作台数据库/月度销售额数据/销售订单_20260101-1021（含科研课题组名称）.xlsx'

DISC = {'A': '数理', 'B': '化学', 'C': '生命科学', 'D': '地球', 'E': '工程与材料',
        'F': '信息', 'G': '管理', 'H': '医学'}
BIG = {'重大项目', '重点项目', '杰青项目', '联合基金重点项目', '联合基金重大项目',
       '自主立项重大项目', '杰青项目延续项目', '重大项目延续项目'}
MID = {'探索项目', '联合基金探索项目', '地区科学基金项目', '青稞项目', '联合基金青基项目'}
LINES = [('PCR/扩增', ['PCR', 'qPCR', '扩增', '引物', '探针', '熔解曲线', '数字PCR']),
         ('测序/建库', ['测序', '转录组', '基因组', '单细胞', '宏基因组', '表观']),
         ('核酸提取', ['核酸', 'DNA提取', 'RNA提取', '磁珠', '提取']),
         ('分子克隆', ['克隆', '载体', '质粒', 'CRISPR', '基因编辑', '连接酶', '感受态']),
         ('蛋白', ['蛋白', '酶', '抗体', '多肽', '表达纯化']),
         ('细胞/类器官', ['细胞', '类器官', '干细胞', '培养']),
         ('冻干/预混', ['冻干', '预混', '保护剂', '稳定剂']),
         ('mRNA/核酸药', ['mRNA', 'siRNA', '核酸药', '递送', 'LNP', '环状RNA', 'circRNA'])]


def esc(s):
    return str(s or '').replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')


def n1(x):
    return '{:,.0f}'.format(x)


# ---------- 1. 省自然 2026 ----------
P = json.load(open(ROOT + '/数据/zjnsf_2026_projects.json', encoding='utf-8'))
for p in P:
    m = re.match(r'[A-Z]+(\d{2})([A-Z])\d+', p['code'])
    p['disc'] = DISC.get(m.group(2), '?') if m else '?'

grp = defaultdict(lambda: {'unit': '', 'pi': '', 'projs': [], 'cats': Counter(),
                           'discs': Counter(), 'lines': Counter()})
for p in P:
    g = grp[p['pi'] + '课题组']
    g['unit'] = p['unit']
    g['pi'] = p['pi']
    g['projs'].append(p)
    g['cats'][p['cat']] += 1
    g['discs'][p['disc']] += 1
    for name, kws in LINES:
        if any(w.lower() in p['name'].lower() for w in kws):
            g['lines'][name] += 1

# ---------- 2. ERP 课题组（商机字段） ----------
wb = openpyxl.load_workbook(ERP, data_only=True)
ws = wb['Sheet1']
rows = ws.iter_rows(values_only=True)
H = [str(x or '').strip() for x in next(rows)]
ix = {h: j for j, h in enumerate(H)}
erp = defaultdict(lambda: {'amt': 0.0, 'ords': set(), 'cust': Counter(), 'sp': Counter(), 'sku': Counter()})
tot_amt = 0.0
map_amt = 0.0
for r in rows:
    amt = float(r[ix['销售数量']] or 0) * float(r[ix['含税单价']] or 0)
    tot_amt += amt
    k = str(r[ix['商机(飞书)#']] or '').strip()
    if not k:
        continue
    map_amt += amt
    x = erp[k]
    x['amt'] += amt
    x['ords'].add(str(r[ix['单据编号']] or ''))
    x['cust'][str(r[ix['客户']] or '')] += 1
    x['sp'][str(r[ix['销售员']] or '')] += 1
    x['sku'][str(r[ix['货号#']] or '')] += 1

matched = [k for k in grp if k in erp]

# ---------- 3. 广东 PI（沿用既有 PI 级文件，作为课题组口径） ----------
PI = json.load(open(ROOT + '/数据/科研经费_课题组PI级.json', encoding='utf-8'))['rows']
gd = [r for r in PI if r['prov'] == '广东']


def tier_of(g):
    cats = g['cats']
    top = 3 if any(c in BIG for c in cats) else (2 if any(c in MID for c in cats) else 1)
    k = 2 if len(g['lines']) >= 2 else (1 if g['lines'] else 0)
    own = 1 if (g['pi'] + '课题组') in erp else 0
    s = top + k + own
    return ('A' if s >= 5 else 'B' if s >= 3 else 'C'), s


rank = []
for name, g in grp.items():
    t, s = tier_of(g)
    rank.append((t, s, name, g))
rank.sort(key=lambda x: ({'A': 0, 'B': 1, 'C': 2}[x[0]], -x[1], -len(x[3]['projs'])))

# ---------- 4. 生成 HTML 片段 ----------
def pill(t):
    cls = {'A': 'ok', 'B': 'est', 'C': 'gap'}[t]
    return '<span class="tag %s">%s</span>' % (cls, t)


# 三·六
cnt_disc = Counter()
for g in grp.values():
    cnt_disc[g['discs'].most_common(1)[0][0]] += 1
multi = sum(1 for g in grp.values() if len(g['projs']) > 1)
hit = sum(1 for g in grp.values() if g['lines'])
sec36 = []
sec36.append('<div class="card"><h2>三·六 2026 浙江省自然：课题组级结构 <span>清单已到手，2,669 项 / 2,607 个课题组</span></h2>')
sec36.append('<div class="kpis" style="grid-template-columns:repeat(4,1fr)">')
sec36.append('<div class="kpi"><div class="kl">课题组数</div><div class="kv">%s</div><div class="ks">2,669 个项目按申请人归并</div></div>' % f'{len(grp):,}')
sec36.append('<div class="kpi"><div class="kl">产品线命中课题组</div><div class="kv">%d</div><div class="ks">占 %.1f%%（关键词命中，推断）</div></div>' % (hit, hit / len(grp) * 100))
sec36.append('<div class="kpi"><div class="kl">多项目课题组</div><div class="kv">%d</div><div class="ks">同一 PI 获批 ≥2 项</div></div>' % multi)
sec36.append('<div class="kpi"><div class="kl">与 ERP 商机重合</div><div class="kv">%d</div><div class="ks">已有采购记录的课题组</div></div>' % len(matched))
sec36.append('</div>')
sec36.append('<table><thead><tr><th style="width:22%">学部（按课题组主项目）</th><th class="n" style="width:12%">课题组数</th><th class="n" style="width:12%">占比</th><th>含义</th></tr></thead><tbody>')
for d, n in cnt_disc.most_common():
    mean = {'工程与材料': '材料/化工，分子生物学需求集中在少数交叉课题',
            '医学': '临床与基础医学，WB/RT-qPCR 主战场',
            '信息': '生物信息/影像，试剂需求低',
            '生命科学': '生物学核心，转化率最高',
            '化学': '化学生物学交叉，提取/克隆有需求',
            '数理': '需求弱',
            '地球': '需求弱'}.get(d, '')
    sec36.append('<tr><td>%s</td><td class="n">%d</td><td class="n">%.1f%%</td><td>%s</td></tr>' % (d, n, n / len(grp) * 100, mean))
sec36.append('</tbody></table>')
sec36.append('<div class="fn" style="margin-top:7px">口径：<b>课题组名 = 申请人 + “课题组”</b>（与 ERP「商机(飞书)#」命名一致）；清单无经费金额字段，本节只用项目数与类别，不做金额推算。产品线为项目名称关键词命中，属推断，可逐条回溯。</div></div>')
sec36 = '\n'.join(sec36)

# 五：科研销售现状（课题组级）
top_erp = sorted(erp.items(), key=lambda kv: -kv[1]['amt'])[:20]
sec5 = []
sec5.append('<div class="card"><h2>五、科研销售现状：课题组级 <span>ERP「商机(飞书)#」= 课题组/项目，不再按院校汇总</span></h2>')
sec5.append('<div class="kpis" style="grid-template-columns:repeat(4,1fr)">')
sec5.append('<div class="kpi"><div class="kl">挂课题组的订单行</div><div class="kv">3,112</div><div class="ks">占全量 26,427 行的 11.8%</div></div>')
sec5.append('<div class="kpi"><div class="kl">课题组 / 项目数</div><div class="kv">%d</div><div class="ks">其中以“XX课题组”命名 689 个</div></div>' % len(erp))
sec5.append('<div class="kpi"><div class="kl">可归集金额</div><div class="kv">%.0f 万</div><div class="ks">占 2026 年总额 %.1f%%</div></div>' % (map_amt / 10000, map_amt / tot_amt * 100))
sec5.append('<div class="kpi"><div class="kl">未挂课题组金额</div><div class="kv">%.0f 万</div><div class="ks">只能落到客户（院校）级</div></div>' % ((tot_amt - map_amt) / 10000))
sec5.append('</div>')
sec5.append('<table><thead><tr><th style="width:26%">课题组 / 项目</th><th style="width:22%">客户</th><th style="width:8%">销售员</th><th class="n" style="width:10%">金额(元)</th><th class="n" style="width:7%">单数</th><th>主销货号</th></tr></thead><tbody>')
for k, v in top_erp:
    sec5.append('<tr><td>%s</td><td>%s</td><td>%s</td><td class="n">%s</td><td class="n">%d</td><td>%s</td></tr>' % (
        esc(k), esc(v['cust'].most_common(1)[0][0]), esc(v['sp'].most_common(1)[0][0]),
        n1(v['amt']), len(v['ords']), esc('、'.join(s for s, _ in v['sku'].most_common(3)))))
sec5.append('</tbody></table>')
sec5.append('<div class="box warn" style="margin-top:9px"><h4>为什么课题组级统计只覆盖 7.3% 的金额</h4>'
            '<p style="margin:3px 0">ERP 的「商机(飞书)#」字段不是必填：26,427 行里 3,112 行有值（11.8%），金额覆盖 7.3%。'
            '<b>凡未填商机的订单，只能归到客户（院校）级</b>——这正是过去“统计到院校大名称”的根因，不是分析方法问题，是源字段缺失。'
            '要让课题组级统计完整，必须在下单环节把商机设为必填，并对历史订单做一次回填。</p></div></div>')
sec5 = '\n'.join(sec5)

# 六：目标课题组名单（浙江 A 级 Top 40 + 广东 Top 15）
sec6 = []
sec6.append('<div class="card"><h2>六、目标课题组名单 <span>浙江来自 2026 省自然清单；广东来自既有 PI 级底表</span></h2>')
sec6.append('<h3>6.1 浙江：A 级课题组全量（共 21 个；项目量级 + 产品线契合 + 已有采购）</h3>')
sec6.append('<table><thead><tr><th style="width:14%">课题组</th><th style="width:8%">PI</th><th style="width:18%">依托单位</th><th class="n" style="width:6%">项目</th><th style="width:11%">最高类别</th><th style="width:8%">学部</th><th style="width:16%">可切入产品线</th><th class="n" style="width:9%">我方采购(元)</th><th style="width:7%">销售员</th><th style="width:4%">级</th></tr></thead><tbody>')
a_list = [x for x in rank if x[0] == 'A']
for t, s, name, g in a_list:
    e = erp.get(name)
    top_cat = g['cats'].most_common(1)[0][0]
    sec6.append('<tr><td><b>%s</b></td><td>%s</td><td>%s</td><td class="n">%d</td><td>%s</td><td>%s</td><td>%s</td><td class="n">%s</td><td>%s</td><td>%s</td></tr>' % (
        esc(name), esc(g['pi']), esc(g['unit']), len(g['projs']), esc(top_cat),
        esc(g['discs'].most_common(1)[0][0]), esc('、'.join(g['lines']) or '—'),
        n1(e['amt']) if e else '0', esc(e['sp'].most_common(1)[0][0]) if e else '—', pill(t)))
sec6.append('</tbody></table>')
sec6.append('<div class="fn" style="margin-top:6px">A 级判定：最高类别为重大/重点/杰青（3 分）或中期类别 + 产品线双命中（2+2+1）；已有采购 +1 分。A ≥5 分、B 3–4 分、C ≤2 分。完整 2,607 个课题组见附件台账。</div>')
sec6.append('<h3 style="margin-top:14px">6.2 广东：可及经费 Top 15 课题组（既有 PI 级底表，金额为估算）</h3>')
sec6.append('<table><thead><tr><th style="width:12%">PI</th><th style="width:26%">依托单位</th><th style="width:10%">项目类别</th><th class="n" style="width:10%">可及金额(万)</th><th style="width:16%">主产品线</th><th>研究方向</th></tr></thead><tbody>')
for r in sorted(gd, key=lambda x: -x['addr'])[:15]:
    sec6.append('<tr><td>%s</td><td>%s</td><td>%s</td><td class="n">%.1f</td><td>%s</td><td>%s</td></tr>' % (
        esc(r['pi']), esc(r['unit']), esc(r['cats']), r['addr'], esc(r['main']), esc(r['topic'])))
sec6.append('</tbody></table>')
sec6.append('<div class="fn" style="margin-top:6px">广东课题组金额为既有底表估算值（按项目类别定额），与浙江“不推算金额”的口径不同，二者不可直接相加。</div></div>')
sec6 = '\n'.join(sec6)

# 七：缺口
sec7 = '''<div class="card"><h2>七、数据缺口与测算方法（必须写清）</h2>
<div class="grid2">
<div class="box"><h4>测算方法</h4><ul>
<li>国自然：2026 预算 418.6 亿（官方）为基数。</li>
<li>省部委：以广东 4.2 亿、浙江 5.2 亿、福建 2.32 亿三个已核样本外推 → <b>150–250 亿</b>。</li>
<li>非双一流+医院份额：按“前 100 单位占 82.4%”与省级样本反推，取 <b>30%（25–35%）</b>。</li>
<li>试剂耗材占科研经费：取 <b>20%（15–25%）</b>。</li>
<li>两省可及市场 ≈ <b>3.0–4.0 亿/年</b>；浙江 2026 省自然清单已拿到全量（2,669 项），项目数可核，金额仍为定额测算。</li>
</ul></div>
<div class="box warn"><h4>还缺什么（直接影响课题组级统计）</h4><ul>
<li><b>ERP 商机字段覆盖率仅 11.8%</b>（3,112 / 26,427 行）、金额覆盖 7.3%——这是课题组级统计最大的硬缺口，需下单必填 + 历史回填。</li>
<li><b>省自然 PI 与 ERP 课题组名重合仅 36 个</b>（2,607 个课题组中 1.4%）：飞书商机命名不统一（有的写“XX课题组”，有的写项目名）。</li>
<li><b>省自然清单无经费金额</b>：只能按类别定额，不能逐项核。</li>
<li><b>广东 2026 省自然清单未重新解析</b>：现用的 PI 底表为旧口径，金额为估算。</li>
<li><b>国自然单位级/PI 级立项名单未公开</b>：国家层面只公布总量，无法做国自然 × 课题组级交叉。</li>
<li><b>产品线权重（30/20/15/5/10/20）</b>：经验值，未用客户实际采购结构校准。</li>
</ul></div>
</div></div>'''

# 八：结论
sec8 = '''<div class="card"><h2>八、结论与下一步</h2>
<div class="grid2">
<div class="box ok2"><h4>四条结论</h4><ul>
<li><b>盘子足够大</b>：两省可及 3–4 亿/年，300 万只是 1% 份额。</li>
<li><b>浙江的结构看清了</b>：2,669 项落在 <b>2,607 个课题组</b>，其中 <b>516 个</b>产品线可命中、<b>51 个</b>课题组手握 2 项以上——这是可以逐个报名字的名单。</li>
<li><b>课题组级销售统计目前只能覆盖 7.3%</b>：不是方法问题，是 ERP 商机字段没填。在补字段之前，任何“按课题组”的销售数字都必须标注覆盖范围。</li>
<li><b>省自然与国自然是两拨人</b>：省自然 PI 与 ERP 课题组名重合 1.4%，说明我们现有商机主要来自企业项目与少量高校课题组，省自然这批新 PI 基本是空白。</li>
</ul></div>
<div class="box"><h4>下一步（按优先级）</h4><ul>
<li><b>① 补字段（最高优先）</b>：销售下单时「商机(飞书)#」设为必填；对 2026 年 2.6 万行历史订单按客户+项目回填，目标覆盖率 ≥80%。</li>
<li><b>② 统一命名</b>：飞书商机统一为「PI姓名+课题组」，与省自然/国自然申报人对齐，才能跨表匹配。</li>
<li><b>③ 派单</b>：把 516 个可命中课题组按依托单位分给浙江区销售，A 级 40 个先跑。</li>
<li><b>④ 补广东</b>：重新解析广东 2026 省自然 3,171 项清单，按同一口径出课题组台账。</li>
</ul></div>
</div></div>'''

# ---------- 5. 拼装 ----------
html = open(SRC, encoding='utf-8').read()

# 更新三 表格浙江行
html = html.replace(
    '<tr><td>浙江</td><td>省自然科学基金 2026：2,667 项（重大 63 / 杰青 128 / 重点 304 / 探索 630 / 青年 1,048 / 联合 494）</td>',
    '<tr><td>浙江</td><td>省自然科学基金 2026：<b>2,669 项</b>（青基 1,049 / 探索 630 / 重点 205 / 联合探索 148 / 地区 119 / 联合重点 104 / 杰青 100 / 青稞 99 / 联合青基 97 / 重大 45 等 14 类）</td>')
html = html.replace('<div class="ks">2,667 项（清单逐项可核）</div>', '<div class="ks">2,669 项（清单逐项可核）</div>')
html = html.replace('<div class="ks">989 项（学科代码 C/H）</div>', '<div class="ks">991 项（学科代码 C/H）</div>')

# 插入三·六：插在 四、市场容量 之前
html = html.replace('<div class="card"><h2>四、市场容量测算', sec36 + '\n\n<div class="card"><h2>四、市场容量测算', 1)

# 替换 五、六 两张卡
def replace_card(h, start_title, new_html):
    i = h.find(start_title)
    if i < 0:
        raise SystemExit('找不到卡片：' + start_title)
    j = h.find('<div class="card">', i + 10)
    k = h.find('__FOOTER__')
    end = j if (j > 0 and j < (k if k > 0 else len(h))) else (k if k > 0 else len(h))
    return h[:i] + new_html + '\n\n' + h[end:]

html = replace_card(html, '<h2>五、目标单位名单（广东）', sec5)
html = replace_card(html, '<h2>六、目标单位名单（浙江）', sec6)
html = replace_card(html, '<h2>七、数据缺口与测算方法', sec7)
html = replace_card(html, '<h2>八、结论与下一步', sec8)

html = html.replace('<title>国自然与省部委基金分布 2024–2026：市场容量与目标单位</title>',
                    '<title>国自然与省部委基金分布 2024–2026：市场容量与目标课题组</title>')

# 移动端：所有表格外包横滑容器，避免整页横向溢出
html = html.replace('输出<b>市场容量 + 目标单位名单</b>', '输出<b>市场容量 + 目标课题组名单</b>')
html = html.replace('<div class="card"><div class="card">', '<div class="card">')
html = html.replace('<table>', '<div class="tw"><table>').replace('</table>', '</table></div>')
html = html.replace('</style>', '.tw{overflow-x:auto;-webkit-overflow-scrolling:touch}.tw table{min-width:620px}\n@media(max-width:480px){.bior-tagwrap{display:none}}\n</style>', 1)

open(OUT, 'w', encoding='utf-8').write(html)
print('已写', OUT)
print('省自然 课题组', len(grp), '｜A', sum(1 for x in rank if x[0] == 'A'),
      '｜B', sum(1 for x in rank if x[0] == 'B'), '｜C', sum(1 for x in rank if x[0] == 'C'))
print('ERP 商机', len(erp), '｜金额覆盖 %.1f%%' % (map_amt / tot_amt * 100), '｜匹配课题组', len(matched))
