#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""课题组 / PI 级台账：省基金项目按 (单位, 负责人) 归集
说明：官方清单无「课题组」字段，按「1 位负责人 = 1 个课题组」归集
输出：数据/科研经费_课题组PI级.json + Excel sheet + 可检索 HTML
"""
import csv
import io
import json
import re
from collections import defaultdict

from openpyxl import load_workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
BASE = W + '/销售分析/基金源文件'
J = W + '/数据/科研经费_统一口径_产品线.json'
OUT_J = W + '/数据/科研经费_课题组PI级.json'
OUT_X = W + '/销售分析/科研经费_单位级清单_广东浙江_2026.xlsx'
OUT_H = W + '/销售分析/科研经费_课题组PI级台账_2026-10-17.html'

d = json.load(io.open(J, encoding='utf-8'))
STRUCT = d['struct']
L = ['分子·RT-qPCR', '分子·PCR/克隆', '分子·核酸提取', '分子·LAMP/恒温', '分子·其他', 'WB']

TOPICS = [
    ('生物材料/传感器', ['生物材料', '生物传感器', '水凝胶', '纳米药物', '纳米递送', '载药', '组织工程', '支架', '抗菌', '生物相容', '微流控', '生物芯片', '荧光探针', '核酸探针', '生物传感']),
    ('肿瘤', ['肿瘤', '癌', '转移', '耐药', '微环境', '白血病', '淋巴瘤', '肉瘤']),
    ('免疫/感染/炎症', ['免疫', '炎症', '感染', '病毒', '细菌', '菌', '疫苗', '脓毒症', '巨噬', 'T细胞', 'B细胞', '抗体', '过敏', '自身免疫']),
    ('神经科学', ['神经', '脑', '认知', '突触', '阿尔茨海默', '帕金森', '卒中', '脊髓', '癫痫', '抑郁', '焦虑', '胶质']),
    ('代谢/内分泌', ['代谢', '糖尿', '肥胖', '脂', '胰岛素', '肝', '肾', '甲状腺', '骨代谢', '肠道菌']),
    ('遗传/表观/基因组', ['基因', '基因组', '表观', '甲基化', '遗传', '突变', '编辑', '染色体', '转录组', '单细胞', '测序']),
    ('蛋白/信号/修饰', ['蛋白', '信号', '通路', '磷酸化', '乙酰化', '泛素', '受体', '激酶', '互作', '复合物', '酶', '结构', '折叠']),
    ('细胞生物学', ['细胞', '凋亡', '自噬', '增殖', '迁移', '侵袭', '分化', '干细胞', '周期', '衰老', '氧化应激']),
    ('药理/中药', ['药', '中药', '成分', '靶点', '治疗', '干预', '方剂', '活性']),
    ('临床/流行病', ['临床', '患者', '队列', '流行病', '诊断', '预后', '样本', '影像', '护理', '公共卫生']),
    ('农学/动植', ['植物', '作物', '水稻', '玉米', '小麦', '畜禽', '水产', '林木', '育种', '抗逆', '园艺', '兽医']),
]
BIO_ANY = ['基因', '蛋白', '细胞', '免疫', '分子', '微生物', '病毒', '菌', '肿瘤', '癌', '疾病', '临床', '药物', 'RNA', 'DNA', '代谢', '神经',
           '干细胞', '组织', '器官', '病理', '药理', '中医', '中药', '护理', '公共卫生', '流行病', '诊断', '治疗', '靶点', '受体', '酶', '抗体',
           '疫苗', '感染', '炎症', '心血管', '糖尿', '肝', '肾', '肺', '脑', '骨', '眼', '口腔', '生殖', '发育', '遗传', '表观', '转录', '信号',
           '通路', '生物', '医学', '药学', '检验', '影像', '健康', '衰老', '氧化', '应激', '凋亡', '自噬', '分化', '增殖', '迁移', '侵袭', '转移',
           '生物材料', '生物传感器', '水凝胶', '载药', '组织工程']
BIO_ANCHOR = ['生物', '医学', '医', '细胞', '蛋白', '基因', '抗菌', '组织', '药物', '递送', '诊断', '检测', '核酸', '免疫', '肿瘤', '癌', '菌', '病毒']


def topic_of(title):
    t = title or ''
    for name, kws in TOPICS:
        if not any(k in t for k in kws):
            continue
        if name == '生物材料/传感器' and not any(k in t for k in BIO_ANCHOR):
            continue
        return name
    return '其他生医' if any(k in t for k in BIO_ANY) else None


ZJ_AMT = [('重大', 100), ('杰青', 80), ('重点', 30), ('探索', 10), ('青基', 10), ('青稞', 10), ('地区', 10), ('延续', 80)]


def zj_amt(cat):
    for k, v in ZJ_AMT:
        if k in cat:
            return v
    return 10


rows = []
for r in csv.DictReader(io.open(BASE + '/parsed/浙江_2026省基金_明细.csv', encoding='utf-8-sig')):
    if not (r['no'].isdigit() and int(r['no']) <= 2667):
        continue
    rows.append({'prov': '浙江', 'unit': r['unit'], 'pi': r['pi'].strip() or '未填', 'name': r['name'],
                 'amt': zj_amt(r['cat']), 'cat': r['cat'], 'topic': topic_of(r['name'])})
for r in csv.DictReader(io.open(BASE + '/parsed/广东_2026省基金_明细.csv', encoding='utf-8-sig')):
    rows.append({'prov': '广东', 'unit': r['unit'], 'pi': (r['pi'] or '').strip() or '未填', 'name': r['name'],
                 'amt': int(r['amt']), 'cat': r['cat'], 'topic': topic_of(r['name'])})

print('总项目', len(rows), '｜有方向', sum(1 for r in rows if r['topic']))

groups = defaultdict(lambda: {'n': 0, 'amt': 0, 'addr': 0.0, 'lines': {l: 0.0 for l in L},
                             'topics': defaultdict(int), 'projects': [], 'cats': set()})
for r in rows:
    k = (r['prov'], r['unit'], r['pi'])
    g = groups[k]
    g['n'] += 1; g['amt'] += r['amt']; g['cats'].add(r['cat'])
    if r['topic']:
        rate, mix = STRUCT[r['topic']]['rate'], STRUCT[r['topic']]['mix']
        g['addr'] += r['amt'] * rate
        for l in L:
            g['lines'][l] += r['amt'] * rate * mix[l] / 100
        g['topics'][r['topic']] += r['amt']
    if len(g['projects']) < 3:
        g['projects'].append(r['name'])

out = []
for (prov, unit, pi), g in groups.items():
    main = max(L, key=lambda l: g['lines'][l]) if g['addr'] > 0 else ''
    topic = max(g['topics'], key=g['topics'].get) if g['topics'] else ''
    out.append({'prov': prov, 'unit': unit, 'pi': pi, 'n': g['n'], 'amt': g['amt'],
                'addr': round(g['addr'], 1), 'rate': round(g['addr'] / g['amt'], 3) if g['amt'] else 0,
                'main': main, 'topic': topic, 'cats': '/'.join(sorted(g['cats'])),
                'projects': g['projects']})
out.sort(key=lambda x: (x['prov'], -x['addr'], -x['amt']))

print('课题组数', len(out), '｜广东', sum(1 for x in out if x['prov'] == '广东'), '｜浙江', sum(1 for x in out if x['prov'] == '浙江'))
print('可及 Top5:', [(x['pi'], x['unit'][:8], x['addr']) for x in out[:5]])

io.open(OUT_J, 'w', encoding='utf-8').write(json.dumps({'meta': {'note': '清单无课题组字段，按 1 位负责人 = 1 个课题组归集'}, 'rows': out}, ensure_ascii=False, indent=1))

# ── Excel sheet ──
wb = load_workbook(OUT_X)
for s in ('课题组_PI级',):
    if s in wb.sheetnames:
        del wb[s]
ws = wb.create_sheet('课题组_PI级')
ws.append(['省', '单位', '负责人', '项数', '经费(万元)', '宝锐可及(万元)', '可及率', '主产品线', '主要方向', '项目类别', '代表项目'])
for x in out:
    ws.append([x['prov'], x['unit'], x['pi'], x['n'], x['amt'], x['addr'], x['rate'], x['main'], x['topic'], x['cats'], '；'.join(x['projects'])])
HEAD = PatternFill('solid', fgColor='1F4E79'); HF = Font(color='FFFFFF', bold=True, size=10)
for c in range(1, ws.max_column + 1):
    cell = ws.cell(row=1, column=c)
    cell.fill = HEAD; cell.font = HF; cell.alignment = Alignment(horizontal='center', wrap_text=True)
for r in range(2, ws.max_row + 1):
    ws.cell(row=r, column=7).number_format = '0.0%'
for i, w in enumerate([6, 30, 12, 7, 13, 15, 9, 16, 18, 22, 70], start=1):
    ws.column_dimensions[get_column_letter(i)].width = w
ws.freeze_panes = 'A2'
ws.auto_filter.ref = ws.dimensions
wb.save(OUT_X)
print('✅ Excel sheet 课题组_PI级')

# ── 可检索 HTML ──
def esc(s):
    return (s or '').replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')

trs = []
for x in out:
    if x['addr'] <= 0:
        continue
    trs.append('<tr data-p="%s" data-q="%s|%s" data-a="%.2f"><td>%s</td><td>%s</td><td>%s</td><td class="n">%d</td><td class="n">%s</td><td class="n">%s</td><td class="n">%.0f%%</td><td>%s</td><td>%s</td><td class="pj">%s</td></tr>' % (
        x['prov'], esc(x['unit']), esc(x['pi']), x['addr'], x['prov'], esc(x['unit']), esc(x['pi']), x['n'],
        f"{x['amt']:,}", f"{x['addr']:,.0f}", x['rate'] * 100, x['main'], x['topic'],
        esc('；'.join(x['projects']))))

html = """<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>科研经费 课题组/PI 级台账（广东·浙江 2026 省基金）</title>
<style>
*{box-sizing:border-box}body{margin:0;background:#f4f6fa;font-family:-apple-system,"PingFang SC",sans-serif;color:#1b2430;font-size:13px}
.wrap{max-width:1320px;margin:0 auto;padding:0 18px 40px}
h1{font-size:21px;margin:12px 0 4px}.meta{color:#5b6675;font-size:12.2px;margin-bottom:12px}
.card{background:#fff;border:1px solid #e3e8f1;border-radius:10px;padding:14px 16px}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:8px 0 10px}
input,select{padding:7px 10px;border:1px solid #cfd8e6;border-radius:7px;font-size:12.5px;background:#fff}
input{width:280px}button{padding:7px 12px;border:1px solid #1F4E79;background:#1F4E79;color:#fff;border-radius:7px;cursor:pointer}
.kpi{display:inline-block;background:#f2f7fd;border:1px solid #d6e2f2;border-radius:8px;padding:6px 12px;margin-right:8px;font-size:12px}
.kpi b{color:#1F4E79;font-size:15px}
table{width:100%;border-collapse:collapse;font-size:11.8px}
th{background:#1F4E79;color:#fff;padding:7px 6px;text-align:left;position:sticky;top:0;cursor:pointer;user-select:none}
th.n{text-align:right}td{padding:6px 6px;border-bottom:1px solid #eef1f6;vertical-align:top}
td.n{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
td.pj{color:#6b7684;font-size:11.2px}
tr:hover{background:#f7fafd}.count{color:#5b6675;font-size:12px;margin:6px 0}
</style></head><body><div class="wrap">
<h1>课题组 / PI 级台账：广东 · 浙江 2026 省基金</h1>
<div class="meta">口径：官方清单无「课题组」字段，按 <b>1 位负责人 = 1 个课题组</b> 归集；可及经费 = 项目经费 × 研究方向可及率（经验值）。合计 __N__ 个课题组、__PROJ__ 个项目。截至 2026-10-17。</div>
<div class="card">
<div class="bar">
 <input id="q" placeholder="搜单位 / 负责人 / 项目关键词">
 <select id="p"><option value="">全部省份</option><option>广东</option><option>浙江</option></select>
 <select id="t"><option value="">全部方向</option>__TOPICS__</select>
 <button onclick="render()">筛选</button>
 <span class="kpi">课题组 <b id="k1">0</b></span><span class="kpi">可及经费 <b id="k2">0</b> 万</span>
</div>
<div class="count" id="cnt"></div>
<table id="tb"><thead><tr>
<th onclick="sortBy(0)">省</th><th onclick="sortBy(1)">单位</th><th onclick="sortBy(2)">负责人</th>
<th class="n" onclick="sortBy(3)">项数</th><th class="n" onclick="sortBy(4)">经费(万)</th><th class="n" onclick="sortBy(5)">可及(万)</th>
<th class="n" onclick="sortBy(6)">可及率</th><th onclick="sortBy(7)">主产品线</th><th onclick="sortBy(8)">主要方向</th><th>代表项目</th>
</tr></thead><tbody id="tbody">__ROWS__</tbody></table>
</div>
</div>
<script>
var ALL=[].slice.call(document.querySelectorAll('#tbody tr'));
var dir=1,last=-1;
function render(){
  var q=(document.getElementById('q').value||'').trim().toLowerCase();
  var p=document.getElementById('p').value, t=document.getElementById('t').value;
  var n=0,sum=0,vis=[];
  ALL.forEach(function(tr){
    var ok=(!q||tr.dataset.q.toLowerCase().indexOf(q)>=0||tr.innerText.toLowerCase().indexOf(q)>=0)
        &&(!p||tr.dataset.p===p)&&(!t||tr.children[8].innerText===t);
    tr.style.display=ok?'':'none';
    if(ok){n++;sum+=parseFloat(tr.dataset.a)||0;vis.push(tr);}
  });
  document.getElementById('k1').innerText=n;
  document.getElementById('k2').innerText=sum.toLocaleString(undefined,{maximumFractionDigits:0});
  document.getElementById('cnt').innerText='当前显示 '+n+' 个课题组';
}
function sortBy(i){
  dir=(last===i)?-dir:1;last=i;
  var tb=document.getElementById('tbody');
  ALL.sort(function(a,b){
    var x=a.children[i].innerText.replace(/,/g,'').replace(/%/g,''),y=b.children[i].innerText.replace(/,/g,'').replace(/%/g,'');
    var nx=parseFloat(x),ny=parseFloat(y);
    if(!isNaN(nx)&&!isNaN(ny))return (nx-ny)*dir;
    return x.localeCompare(y,'zh')*dir;
  });
  ALL.forEach(function(tr){tb.appendChild(tr);});
}
render();
</script></body></html>"""
html = (html.replace('__N__', str(len([x for x in out if x['addr'] > 0])))
            .replace('__PROJ__', str(len(rows)))
            .replace('__TOPICS__', ''.join('<option>%s</option>' % t for t, _ in TOPICS))
            .replace('__ROWS__', '\n'.join(trs)))
io.open(OUT_H, 'w', encoding='utf-8').write(html)
print('✅ HTML %.0f KB' % (len(html.encode()) / 1024))
