#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把 M5341-014804/014805 快速扩增对比报告整理成两页 A4 对外技术资料。"""
import html
import os
import sys

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
sys.path.insert(0, os.path.join(ROOT, '脚本'))
import brand_kit as BK

OUT_HTML = os.path.join(ROOT, '对外物料', 'M5341-014804_014805_快速扩增对比_20260920_V01.html')

QRT = [
    {'group': 'M5341-014804', 'template': '0.464 pg/µl', 'ct': ['31.36', '33.03', '31.73', '34.32'], 'rn': ['896', '324', '1075', '552']},
    {'group': 'M5341-014804', 'template': '4.64 pg/µl', 'ct': ['28.12', '29.89', '28.27', '30.92'], 'rn': ['1106', '380', '1418', '688']},
    {'group': 'M5341-014805', 'template': '0.464 pg/µl', 'ct': ['31.41', '33.22', '31.73', '34.39'], 'rn': ['1111', '366', '1272', '594']},
    {'group': 'M5341-014805', 'template': '4.64 pg/µl', 'ct': ['28.11', '29.93', '28.65', '31.20'], 'rn': ['1207', '403', '1398', '710']},
    {'group': 'FM5101-136104（对照）', 'template': '0.464 pg/µl', 'ct': ['32.57', '32.85', '31.97', '34.16'], 'rn': ['804', '323', '1104', '385']},
    {'group': 'FM5101-136104（对照）', 'template': '4.64 pg/µl', 'ct': ['29.49', '30.14', '28.93', '31.54'], 'rn': ['984', '342', '1388', '586']},
]
RESP = [
    {'group': 'M5341-014804', 'template': '0.8 copies/µl', 'ct': ['NoCt', '33.65', '36.51', '37.19'], 'rn': ['/', '1206', '1379', '474']},
    {'group': 'M5341-014804', 'template': '8 copies/µl', 'ct': ['37.19', '33.58', '34.12', '33.48'], 'rn': ['502', '1244', '2258', '1003']},
    {'group': 'M5341-014805', 'template': '0.8 copies/µl', 'ct': ['35.86', '33.99', '34.98', '36.82'], 'rn': ['989', '1198', '1731', '482']},
    {'group': 'M5341-014805', 'template': '8 copies/µl', 'ct': ['31.87', '34.25', '31.15', '33.09'], 'rn': ['2094', '1081', '2982', '845']},
    {'group': 'FM5101-136104（对照）', 'template': '0.8 copies/µl', 'ct': ['NoCt', '35.39', 'NoCt', '37.80'], 'rn': ['/', '606', '/', '293']},
    {'group': 'FM5101-136104（对照）', 'template': '8 copies/µl', 'ct': ['37.90', '34.10', '35.98', '35.98'], 'rn': ['233', '850', '1450', '490']},
]

PRODUCTS = ['M5341-014804', 'M5341-014805', 'FM5101-136104（对照）']


def esc(x):
    return html.escape(str(x), quote=True)


def table_html(rows):
    out = ['<table class="data-table"><thead><tr>',
           '<th class="grp">分组</th><th class="tplh">模板</th>',
           '<th>Ct FAM</th><th>Ct VIC</th><th>Ct ROX</th><th>Ct CY5</th>',
           '<th>Rn FAM</th><th>Rn VIC</th><th>Rn ROX</th><th>Rn CY5</th></tr>',
           '</thead><tbody>']
    last_group = None
    row_index = 0
    for r in rows:
        out.append('<tr>')
        if r['group'] != last_group:
            out.append('<td class="grp" rowspan="2">%s</td>' % esc(r['group']))
            last_group = r['group']
        out.append('<td class="tpl">%s</td>' % esc(r['template']))
        for v in r['ct']:
            cls = 'ct noct' if v == 'NoCt' else 'ct'
            out.append('<td class="%s">%s</td>' % (cls, esc(v)))
        for v in r['rn']:
            cls = 'rn dash' if v == '/' else 'rn'
            out.append('<td class="%s">%s</td>' % (cls, esc(v)))
        out.append('</tr>')
        row_index += 1
    out.append('</tbody></table>')
    return ''.join(out)


def chart(title, groups, min_v, max_v, note):
    colors = ['#174E79', '#2E86C1', '#9AA7B8']
    short = {'M5341-014804': '014804', 'M5341-014805': '014805', 'FM5101-136104（对照）': '对照'}
    legend = ''.join(
        '<span><i style="background:%s"></i>%s</span>' % (colors[i], esc(p))
        for i, p in enumerate(PRODUCTS)
    )
    rows = []
    for g in groups:
        rows.append('<div class="bar-group-label">%s</div>' % esc(g['label']))
        for i, p in enumerate(PRODUCTS):
            v = g['values'][i]
            if v is None:
                rows.append('<div class="bar-row"><span class="bar-lab">%s</span><div class="track"><i class="na"></i></div><b>NoCt</b></div>' % esc(short.get(p, p)))
                continue
            pct = max(10, min(100, (v - min_v) / (max_v - min_v) * 100))
            rows.append('<div class="bar-row"><span class="bar-lab">%s</span><div class="track"><i style="width:%.1f%%;background:%s"></i></div><b>%s</b></div>' % (esc(short.get(p, p)), pct, colors[i], v))
    return ('<div class="chart"><div class="chart-title">%s</div><div class="legend">%s</div>%s'
            '<div class="chart-note">%s</div></div>') % (esc(title), legend, ''.join(rows), esc(note))


qrt_chart = chart(
    'qRT 四重 · FAM Ct',
    [
        {'label': '0.464 pg/µl', 'values': [31.36, 31.41, 32.57]},
        {'label': '4.64 pg/µl', 'values': [28.12, 28.11, 29.49]},
    ],
    27.5, 33.5,
    'Ct 越低表示扩增越早；阈值 20；2T。'
)
resp_chart = chart(
    '呼吸道四重 · 8 copies/µl',
    [
        {'label': 'FAM', 'values': [37.19, 31.87, 37.90]},
        {'label': 'ROX', 'values': [34.12, 31.15, 35.98]},
    ],
    30.0, 39.0,
    'Ct 越低表示扩增越早；阈值 20；2T。'
)

CSS = BK.CSS + r'''
@page{size:A4;margin:0}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:#eaf0f6}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",Arial,sans-serif;color:#17212e;font-size:9.1pt;line-height:1.45;-webkit-print-color-adjust:exact;print-color-adjust:exact}
.sheet{width:210mm;height:297mm;background:#fff;margin:14px auto;display:flex;flex-direction:column;position:relative;overflow:hidden;box-shadow:0 6px 26px rgba(15,37,64,.13)}
.sheet + .sheet{margin-top:18px}
.sheet .bior-header,.sheet .bior-footer{flex:none}
@media print{
  body{background:#fff}
  .sheet{margin:0!important;box-shadow:none!important;break-after:page}
  .sheet:last-child{break-after:auto}
}
.sheet main{flex:1 1 auto;min-height:0;padding:7mm 12mm 4.5mm}
.bior-header-inner{padding:9px 16px!important}
.bior-logo-chip img{height:29px!important}
.bior-name{font-size:13px!important}
.bior-cn{font-size:9.5px!important}
.bior-slogan{font-size:11px!important;letter-spacing:1.3px!important}
.bior-tag{font-size:9.5px!important}
.bior-footer{padding:7px 16px!important}
.bior-footer-slogan{font-size:11px!important;letter-spacing:1.8px!important;margin-bottom:2px!important}
.bior-footer-meta{font-size:8px!important;line-height:1.45!important}
.bior-footer-extra{font-size:7.4px!important;margin-top:2px!important}
.kicker{font-size:7.5pt;letter-spacing:2.1px;color:#2E6DA4;font-weight:800;margin-bottom:3mm}
h1{font-size:25pt;line-height:1.18;letter-spacing:-.3px;margin:0 0 2.5mm;color:#0E3155;font-weight:800}
.sub{font-size:9.6pt;color:#344054;margin:0 0 1.5mm;line-height:1.45}
.meta{font-size:8.2pt;color:#667085;margin:0 0 5mm}
.conclusion{border-left:4px solid #2E6DA4;background:#EAF2FA;padding:4mm 5mm;margin:0 0 4mm;border-radius:0 2mm 2mm 0}
.conclusion .ct{font-size:8pt;font-weight:800;color:#1F4E79;letter-spacing:1px;margin-bottom:1mm}
.conclusion p{font-size:9.3pt;line-height:1.55;margin:0;color:#123A61}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:2.5mm;margin:0 0 4mm}
.kpi{border:1px solid #D8E2EE;border-radius:2mm;padding:2.4mm 3mm;background:#F8FBFE}
.kpi b{display:block;font-size:15pt;line-height:1;color:#1F4E79;letter-spacing:-.2px}
.kpi span{display:block;font-size:7.2pt;color:#667085;margin-top:1mm}
.results{display:grid;grid-template-columns:1fr 1fr;gap:4mm;margin:0 0 3mm}
.rcard{border:1px solid #D8E2EE;border-radius:2mm;padding:3.2mm 3.5mm;background:#fff}
.rt{font-size:7.2pt;font-weight:800;letter-spacing:1px;color:#2E6DA4;margin-bottom:1mm}
.big{font-size:15pt;line-height:1.15;color:#0E3155;font-weight:800;margin-bottom:1.5mm}
.big small{font-size:8pt;font-weight:600;color:#475467}
.rline{font-size:7.7pt;color:#344054;line-height:1.5;border-top:1px solid #EDF2F7;padding-top:1.3mm;margin-top:1.2mm}
.charts{display:grid;grid-template-columns:1fr 1fr;gap:4mm;margin-top:1mm}
.chart{border:1px solid #D8E2EE;border-radius:2mm;padding:2.8mm 3mm;background:#FBFCFE}
.chart-title{font-size:8pt;font-weight:800;color:#174E79;margin-bottom:1.2mm}
.legend{display:flex;gap:2.5mm;flex-wrap:wrap;margin-bottom:1.5mm}
.legend span{font-size:6.2pt;color:#475467;display:inline-flex;align-items:center;gap:1mm;white-space:nowrap}
.legend i{width:2.2mm;height:2.2mm;border-radius:.5mm;display:inline-block}
.bar-group-label{font-size:6.7pt;color:#667085;margin:1.8mm 0 .8mm;font-weight:700}
.bar-row{display:grid;grid-template-columns:24mm 1fr 9mm;gap:1.5mm;align-items:center;margin:.7mm 0}
.bar-lab{font-size:6.2pt;color:#344054;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.track{height:2.5mm;background:#EDF2F8;border-radius:1mm;overflow:hidden}
.track i{display:block;height:100%;border-radius:1mm}
.track i.na{width:100%;background:repeating-linear-gradient(45deg,#E4EAF2,#E4EAF2 1mm,#F4F7FA 1mm,#F4F7FA 2mm)}
.bar-row b{font-size:6.4pt;color:#123A61;text-align:right;font-variant-numeric:tabular-nums}
.chart-note{font-size:6.1pt;color:#7C8797;margin-top:1.5mm;line-height:1.45}
.page-title{font-size:16pt;font-weight:800;color:#0E3155;margin:0 0 1mm;line-height:1.2}
.page-sub{font-size:8pt;color:#667085;margin:0 0 4mm}
.method{border:1px solid #D8E2EE;border-radius:2mm;background:#F8FBFE;padding:2.3mm 3.5mm;margin-bottom:3mm}
.method b{color:#174E79}
.method p{font-size:7.8pt;line-height:1.6;margin:.8mm 0}
.method .mline{display:grid;grid-template-columns:20mm 1fr;gap:2mm;font-size:7.2pt;line-height:1.45;margin:.45mm 0}
.method .mline span:first-child{color:#667085}
.table-title{display:flex;justify-content:space-between;align-items:baseline;margin:0 0 1mm}
.table-title b{font-size:9.2pt;color:#174E79}
.table-title span{font-size:6.8pt;color:#667085}
.data-table{width:100%;border-collapse:collapse;table-layout:fixed;font-variant-numeric:tabular-nums;font-size:7.8pt}
.data-table th,.data-table td{border:1px solid #DCE6F0;padding:1.45mm 1mm;text-align:center;line-height:1.25}
.data-table thead th{background:#EAF2FA;color:#174E79;font-weight:800;font-size:6.6pt}
.data-table thead tr.subhead th{background:#F3F7FB;color:#667085;font-weight:700;font-size:6.1pt;padding:.5mm}
.data-table th.grp{width:25mm}
.data-table th.tplh{width:17mm}
.data-table td.grp{background:#F8FBFE;font-weight:800;color:#174E79;font-size:7pt;vertical-align:middle}
.data-table td.tpl{background:#FBFCFE;color:#344054;font-size:7pt}
.data-table td.ct{font-size:7.8pt;color:#17212E}
.data-table td.rn{font-size:7.5pt;color:#344054}
.data-table td.noct{color:#9B2C2C;font-weight:700}
.data-table td.dash{color:#98A2B3}
.data-table + .table-note{font-size:6.1pt;color:#7C8797;margin:.7mm 0 2.5mm}
.end-conclusion{margin-top:1mm;border-top:1px solid #D8E2EE;padding-top:2mm;font-size:7.4pt;line-height:1.45;color:#344054}
.end-conclusion b{color:#174E79}
.note-strip{display:grid;grid-template-columns:1fr 1fr 1fr;gap:3mm;margin:3mm 0 1mm}
.note-strip .n{border-top:1px solid #D8E2EE;padding-top:2mm}
.note-strip b{display:block;font-size:7pt;color:#174E79;margin-bottom:.6mm}
.note-strip span{display:block;font-size:6.8pt;color:#475467;line-height:1.45}
.bottom-note{font-size:6.5pt;color:#7C8797;line-height:1.5;margin-top:1mm}
.readout{display:grid;grid-template-columns:1fr 1fr 1fr;gap:3mm;margin:2.5mm 0 1mm}
.readout .r{border-left:3px solid #2E6DA4;background:#F8FBFE;padding:2.2mm 2.8mm}
.readout b{display:block;font-size:7pt;color:#174E79;margin-bottom:.7mm}
.readout span{display:block;font-size:6.7pt;color:#344054;line-height:1.45}
.summary-strip{display:grid;grid-template-columns:1fr 1fr 1fr;gap:3mm;margin:2.5mm 0 1mm}
.summary-strip .s{border:1px solid #D8E2EE;border-radius:2mm;background:#F8FBFE;padding:2.2mm 2.5mm}
.summary-strip b{display:block;font-size:7pt;color:#174E79;margin-bottom:.7mm}
.summary-strip span{display:block;font-size:6.6pt;color:#344054;line-height:1.45}
'''

page1 = f'''
<div class="sheet">
  {BK.header_html('dark', '快速扩增对比 · 结果摘要')}
  <main>
    <div class="kicker">FAST AMPLIFICATION · TECHNICAL SUMMARY</div>
    <h1>M5341-014804 / M5341-014805<br>快速扩增对比</h1>
    <p class="sub">qRT 四重与呼吸道四重｜48S 快速程序（31 min）｜对照样品 FM5101-136104</p>
    <p class="meta">数据日期：2026-09-20　｜　检测通道：FAM / VIC / ROX / CY5　｜　Ct 阈值：20　｜　每条件 2T</p>

    <div class="conclusion">
      <div class="ct">结论</div>
      <p>在 48S 快速程序（31 min）下，M5341-014804 与 M5341-014805 的 qRT 四重 FAM 通道 Ct 较对照样品 FM5101-136104 提前约 1.2–1.4；两款候选总体接近。呼吸道四重中，M5341-014805 的 FAM/ROX 通道 Ct 更早（8 copies/µl 分别提前 6.0 和 4.8），0.8 copies/µl 的 FAM 由 NoCt 变为 35.86；VIC 通道 M5341-014804 更低。</p>
    </div>

    <div class="kpis">
      <div class="kpi"><b>31 min</b><span>48S 快速程序</span></div>
      <div class="kpi"><b>45</b><span>扩增循环</span></div>
      <div class="kpi"><b>2T</b><span>每条件重复</span></div>
      <div class="kpi"><b>4</b><span>荧光检测通道</span></div>
    </div>

    <div class="results">
      <div class="rcard">
        <div class="rt">qRT 四重</div>
        <div class="big">FAM Ct 提前 1.2–1.4 <small>vs 对照</small></div>
        <div class="rline">4.64 pg/µl：014804 <b>28.12</b> / 014805 <b>28.11</b> / 对照 <b>29.49</b></div>
        <div class="rline">0.464 pg/µl：014804 31.36 / 014805 31.41 / 对照 32.57　｜　CY5 Rn：552 / 594 / 385</div>
      </div>
      <div class="rcard">
        <div class="rt">呼吸道四重</div>
        <div class="big">FAM / ROX 提前 4.8–6.0 <small>M5341-014805</small></div>
        <div class="rline">8 copies/µl FAM：014804 37.19 / 014805 <b>31.87</b> / 对照 37.90　｜　ROX：34.12 / <b>31.15</b> / 35.98</div>
        <div class="rline">0.8 copies/µl FAM：014804 NoCt / 014805 <b>35.86</b> / 对照 NoCt　｜　VIC（8）：33.58 / 34.25 / 34.10</div>
      </div>
    </div>

    <div class="charts">
      {qrt_chart}
      {resp_chart}
    </div>

    <div class="note-strip">
      <div class="n"><b>qRT 四重</b><span>25× Pbmix 2 µl/25 µl · 人总 0.464 / 4.64 pg/µl · 5 µl/T · 2T</span></div>
      <div class="n"><b>呼吸道四重</b><span>25× Pbmix 2 µl/25 µl · 0.8 / 8 copies/µl · 5 µl/T · 2T</span></div>
      <div class="n"><b>读数规则</b><span>Ct 阈值 20 · Ct 越低越早 · NoCt = 未检出</span></div>
    </div>
    <div class="readout">
      <div class="r"><b>qRT 四重</b><span>FAM 通道较对照提前 1.2–1.4 Ct；VIC/CY5 在低输入未显示同一方向。</span></div>
      <div class="r"><b>呼吸道四重</b><span>M5341-014805 的 FAM/ROX 更早；VIC 通道 M5341-014804 更低。</span></div>
      <div class="r"><b>数据边界</b><span>2T 单次实验；不据此做统计显著性判断。</span></div>
    </div>
    <div class="bottom-note">数据为单次实验 2T 读数，未做统计显著性检验；本页用于两组快速扩增体系的读数比较。</div>
  </main>
  {BK.footer_html('2026-09-20 数据 · 48S 快速程序 · 2T')}
</div>
'''

page2 = f'''
<div class="sheet">
  {BK.header_html('dark', '快速扩增对比 · 实验数据')}
  <main>
    <div class="page-title">实验条件与完整读数</div>
    <div class="page-sub">两套四重体系、三种样品、两种模板浓度；数据为单次实验的 Ct 与 Rn 读数。</div>

    <div class="method">
      <div class="mline"><span>扩增程序</span><span><b>48S 快速程序（31 min）</b>：50°C 2 min；90°C 30 s；60°C 5 min；95°C 30 s；45 cycles（95°C 1 s，60°C 7 s）。</span></div>
      <div class="mline"><span>qRT 四重</span><span>25× Pbmix 2 µl/25 µl；人总模板 0.464 / 4.64 pg/µl，5 µl/T，2T。</span></div>
      <div class="mline"><span>呼吸道四重</span><span>25× Pbmix 2 µl/25 µl；模板 0.8 / 8 copies/µl，5 µl/T，2T。</span></div>
      <div class="mline"><span>检测/分析</span><span>FAM / VIC / ROX / CY5；Ct 阈值均为 20；绝对分析法。</span></div>
    </div>

    <div class="table-title"><b>表 1　qRT 四重：Ct 与 Rn</b><span>单位：Ct；Rn 为终点荧光读数</span></div>
    {table_html(QRT)}
    <div class="table-note">NoCt = 未检出；Ct 越低表示扩增越早；Rn 为原报告读数。</div>

    <div class="table-title"><b>表 2　呼吸道四重：Ct 与 Rn</b><span>单位：Ct；Rn 为终点荧光读数</span></div>
    {table_html(RESP)}
    <div class="table-note">NoCt = 未检出；“/”表示原报告未给出对应 Rn；Ct 阈值 20。</div>

    <div class="summary-strip">
      <div class="s"><b>qRT 四重</b><span>FAM 通道 Ct 较对照提前约 1.2–1.4；两款候选总体接近；CY5 Rn 在 0.464 pg/µl 为 552 / 594 / 385。</span></div>
      <div class="s"><b>呼吸道四重</b><span>M5341-014805 的 FAM/ROX 在 8 copies/µl 提前 6.0 / 4.8；0.8 copies/µl FAM 为 35.86，对照为 NoCt。</span></div>
      <div class="s"><b>读数边界</b><span>单次实验、2T；Ct 低表示扩增更早；Rn 为终点信号；未做统计检验。</span></div>
    </div>

    <div class="end-conclusion"><b>综合结论：</b>在 31 min 快速程序中，M5341-014804 与 M5341-014805 的 qRT 四重 FAM 通道 Ct 较对照样品 FM5101-136104 提前约 1.2–1.4，两款候选总体接近；呼吸道四重中 M5341-014805 的 FAM/ROX 通道 Ct 更早，0.8 copies/µl 的 FAM 可检出；VIC 通道 M5341-014804 更低。以上为 2T 单次实验读数，未做统计显著性检验。</div>
    <div class="bottom-note">数据说明：表中数值为单次实验读数，未提供重复间标准差；不同仪器、试剂批次和样本基质下的绝对值可能不同。Ct 低表示扩增更早，不等同于定量准确度；Rn 为终点荧光信号。</div>
  </main>
  {BK.footer_html('2026-09-20 数据 · Ct 阈值 20 · 2T')}
</div>
'''

HTML = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M5341-014804 / M5341-014805 快速扩增对比</title>
<style>{CSS}</style>
</head>
<body>
{page1}
{page2}
</body>
</html>'''

os.makedirs(os.path.dirname(OUT_HTML), exist_ok=True)
open(OUT_HTML, 'w', encoding='utf-8').write(HTML)
print('WROTE', OUT_HTML, os.path.getsize(OUT_HTML))
print('BRAND', BK.verify(HTML))
print('headers', HTML.count('class="bior-header'), 'footers', HTML.count('class="bior-footer'), 'slogan', HTML.count('守护生命健康，缔造美好生活'))
