#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把四份 PDF 的抽取结果合并为一份格式统一的 DOCX

格式口径（用户 2026-10-26 拍板）：
- 字号仅四档：22（封面）/16（标题）/12（正文）/10.5（表格、图注）
- 中文 宋体，英文 Times New Roman
- 页眉保留产品名+编号，页脚「第 X 页 共 Y 页」
- 图片原样保留
"""
import json
import os
import sys

from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor

OUT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类/资料/M2181-4_产品稳定性研究资料_合订本_20261026.docx'
HEADER_LEFT = '2×FastAmpli Premix-UNG Ⅳ（Probe qPCR）【货号：M2181-4】产品稳定性研究资料'
HEADER_RIGHT = 'CA-10-04-R13-1.0'

CHAPTERS = [
    ('主报告：产品稳定性研究资料', '/tmp/main.json', 'main'),
    ('运输稳定性验证资料（2024 年）', '/tmp/trans.json', 'trans'),
    ('加速稳定性测试报告', '/tmp/acc.json', 'acc'),
    ('冻融稳定性测试报告', '/tmp/frz.json', 'frz'),
]

BODY, H1, CAP, COVER = 12, 16, 10.5, 22


def set_font(run, size, bold=False, color=None):
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.name = 'Times New Roman'
    r = run._element.rPr.rFonts
    r.set(qn('w:eastAsia'), '宋体')
    r.set(qn('w:ascii'), 'Times New Roman')
    r.set(qn('w:hAnsi'), 'Times New Roman')
    if color:
        run.font.color.rgb = color


def shade(cell, hexcolor):
    tcPr = cell._tc.get_or_add_tcPr()
    shd = OxmlElement('w:shd')
    shd.set(qn('w:val'), 'clear')
    shd.set(qn('w:color'), 'auto')
    shd.set(qn('w:fill'), hexcolor)
    tcPr.append(shd)


def add_field(paragraph, instr, size=CAP):
    run = paragraph.add_run()
    set_font(run, size)
    fld = OxmlElement('w:fldSimple')
    fld.set(qn('w:instr'), instr)
    t = OxmlElement('w:t')
    t.text = '1'
    r = OxmlElement('w:r')
    r.append(t)
    fld.append(r)
    paragraph._p.append(fld)


def style_doc(doc):
    st = doc.styles['Normal']
    st.font.name = 'Times New Roman'
    st.font.size = Pt(BODY)
    st.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
    pf = st.paragraph_format
    pf.line_spacing = 1.5
    pf.space_after = Pt(3)
    sec = doc.sections[0]
    sec.page_height, sec.page_width = Cm(29.7), Cm(21.0)
    sec.top_margin = sec.bottom_margin = Cm(2.4)
    sec.left_margin = sec.right_margin = Cm(2.2)
    sec.header_distance = Cm(1.2)
    sec.footer_distance = Cm(1.2)

    # 页眉
    hp = sec.header.paragraphs[0]
    hp.text = ''
    tab_stops = hp.paragraph_format.tab_stops
    tab_stops.add_tab_stop(Cm(16.6), alignment=2)
    r1 = hp.add_run(HEADER_LEFT)
    set_font(r1, CAP, color=RGBColor(0x44, 0x44, 0x44))
    r2 = hp.add_run('\t' + HEADER_RIGHT)
    set_font(r2, CAP, color=RGBColor(0x44, 0x44, 0x44))
    # 页脚下边框
    pPr = hp._p.get_or_add_pPr()
    pbdr = OxmlElement('w:pBdr')
    bottom = OxmlElement('w:bottom')
    bottom.set(qn('w:val'), 'single'); bottom.set(qn('w:sz'), '4')
    bottom.set(qn('w:space'), '1'); bottom.set(qn('w:color'), 'AAAAAA')
    pbdr.append(bottom); pPr.append(pbdr)

    fp = sec.footer.paragraphs[0]
    fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = fp.add_run('第 ')
    set_font(r, CAP)
    add_field(fp, 'PAGE')
    r = fp.add_run(' 页 共 ')
    set_font(r, CAP)
    add_field(fp, 'NUMPAGES')
    r = fp.add_run(' 页')
    set_font(r, CAP)


def add_cover(doc):
    for _ in range(4):
        doc.add_paragraph()
    p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    set_font(p.add_run('珠海宝锐生物科技有限公司'), COVER, bold=True, color=RGBColor(0x0D, 0x47, 0xA1))
    p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p.paragraph_format.space_before = Pt(24)
    set_font(p.add_run('2×FastAmpli Premix-UNG Ⅳ（Probe qPCR）'), COVER, bold=True)
    p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    set_font(p.add_run('产品稳定性研究资料'), COVER, bold=True)
    p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p.paragraph_format.space_before = Pt(10)
    set_font(p.add_run('货号：M2181-4'), H1, color=RGBColor(0x44, 0x44, 0x44))
    for _ in range(7):
        doc.add_paragraph()
    for line in ['文件编号：CA-10-04-R13-1.0', '版本：1.0', '整理日期：2026 年 10 月']:
        p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        set_font(p.add_run(line), BODY, color=RGBColor(0x44, 0x44, 0x44))
    doc.add_page_break()


def add_toc(doc, entries):
    p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
    set_font(p.add_run('目  录'), H1, bold=True)
    p.paragraph_format.space_after = Pt(12)
    # Word 目录域：打开后自动/按 F9 更新页码
    para = doc.add_paragraph()
    run = para.add_run()
    set_font(run, BODY)
    fldChar = OxmlElement('w:fldChar'); fldChar.set(qn('w:fldCharType'), 'begin')
    instr = OxmlElement('w:instrText'); instr.set(qn('xml:space'), 'preserve')
    instr.text = 'TOC \\o "1-2" \\h \\z \\u'
    sep = OxmlElement('w:fldChar'); sep.set(qn('w:fldCharType'), 'separate')
    t = OxmlElement('w:t'); t.text = '（右键“更新域”生成目录页码）'
    end = OxmlElement('w:fldChar'); end.set(qn('w:fldCharType'), 'end')
    run._r.append(fldChar); run._r.append(instr); run._r.append(sep); run._r.append(t); run._r.append(end)
    doc.add_page_break()


def add_heading(doc, text, level=1):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(12 if level == 1 else 8)
    p.paragraph_format.space_after = Pt(6)
    p.paragraph_format.keep_with_next = True
    set_font(p.add_run(text), H1, bold=True, color=RGBColor(0x12, 0x44, 0x6E))


def add_body(doc, text):
    p = doc.add_paragraph()
    p.paragraph_format.first_line_indent = Pt(BODY * 2)
    p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
    set_font(p.add_run(text), BODY)


def add_caption(doc, text):
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    set_font(p.add_run(text), CAP, color=RGBColor(0x55, 0x55, 0x55))


def add_table(doc, rows):
    ncol = max(len(r) for r in rows)
    rows = [r + [''] * (ncol - len(r)) for r in rows]
    t = doc.add_table(rows=len(rows), cols=ncol)
    t.style = 'Table Grid'
    t.alignment = WD_TABLE_ALIGNMENT.CENTER
    t.autofit = True
    for i, row in enumerate(rows):
        for j, val in enumerate(row):
            cell = t.cell(i, j)
            cell.text = ''
            p = cell.paragraphs[0]
            p.paragraph_format.line_spacing = 1.15
            p.paragraph_format.space_after = Pt(0)
            set_font(p.add_run(val), CAP, bold=(i == 0))
            if i == 0:
                shade(cell, 'EDF3FA')
    # 表头重复
    trPr = t.rows[0]._tr.get_or_add_trPr()
    tblHeader = OxmlElement('w:tblHeader')
    tblHeader.set(qn('w:val'), 'true')
    trPr.append(tblHeader)
    doc.add_paragraph().paragraph_format.space_after = Pt(0)


def add_image(doc, path, w_px, h_px):
    width_cm = min(10.5, w_px / 96 * 2.54)
    p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p.paragraph_format.space_before = Pt(6)
    p.paragraph_format.space_after = Pt(3)
    p.add_run().add_picture(path, width=Cm(width_cm))




HDR_KW = ('分组', '样品', '检测项目', '批号', '结果', '项目', '孔数', '名称', '浓度', '时间', '编号', 'Ct')
def augment_tables(m):
    """跨页表格续接：同列宽、同横向范围、近 3 页内，缺表头的补上最近表头"""
    last = {}   # (ncol, x0, x1) -> (page_no, header_rows)
    for pg in m['pages']:
        for it in pg['items']:
            if it['type'] != 'table':
                continue
            rows = it['rows']
            if not rows:
                continue
            ncol = max(len(r) for r in rows)
            rows = [r + [''] * (ncol - len(r)) for r in rows]
            x0, y0, x1, y1 = it['bbox']
            key = (ncol, round(x0 / 20), round(x1 / 20))
            first = ' '.join(rows[0])
            is_head = sum(1 for k in HDR_KW if k in first) >= 2
            if is_head:
                last[key] = (pg['page'], [list(r) for r in rows[:3]])
            elif key in last and pg['page'] - last[key][0] <= 3:
                hdr = last[key][1]
                if not any(' '.join(h) == first for h in hdr):
                    rows = [list(h) for h in hdr] + rows
            rows = [[c.replace('\n', ' ').strip() for c in r] for r in rows]
            ded = []
            for r in rows:
                if ded and r == ded[-1]:
                    continue
                ded.append(r)
            it['rows'] = ded  # DEDUPE_MARK


def build(entries_override=None):
    doc = Document()
    style_doc(doc)
    add_cover(doc)

    # 先扫描目录条目
    entries = []
    for ct, path, pref in CHAPTERS:
        entries.append((ct, 1))
        if not os.path.exists(path):
            continue
        m = json.load(open(path, encoding='utf-8'))
        start = 1 if 'main' in pref else 0
        for pg in m['pages'][start:]:
            for it in pg['items']:
                if it['type'] == 'h' and len(it['text']) <= 40 and it['text'] != ct:
                    if it['text'] not in [e[0] for e in entries]:
                        entries.append((it['text'], 2))
    add_toc(doc, entries_override or entries)

    for ci, (ct, path, pref) in enumerate(CHAPTERS, 1):
        if not os.path.exists(path):
            continue
        m = json.load(open(path, encoding='utf-8'))
        augment_tables(m)
        add_heading(doc, f'第{["一","二","三","四"][ci-1]}部分  {ct}', 1)
        start = 1 if 'main' in pref else 0
        for pg in m['pages'][start:]:
            for it in pg['items']:
                if it['type'] == 'h':
                    add_heading(doc, it['text'], 2 if it['text'][0].isdigit() else 2)
                elif it['type'] == 'p':
                    if len(it['text']) <= 30 and (it['text'].startswith('表') or it['text'].startswith('图')):
                        add_caption(doc, it['text'])
                    else:
                        add_body(doc, it['text'])
                elif it['type'] == 'table':
                    add_table(doc, it['rows'])
                elif it['type'] == 'img':
                    add_image(doc, it['path'], it['w'], it['h'])
            # 原件一页结束 → 不强制分页，保持流式
    # 打开时提示更新域
    settings = doc.settings.element
    uf = OxmlElement('w:updateFields'); uf.set(qn('w:val'), 'true')
    settings.append(uf)
    doc.save(OUT)
    print('✅', OUT, '｜目录条目', len(entries_override or entries))


if __name__ == '__main__':
    build()
