# -*- coding: utf-8 -*-
"""把「定点补读」恢复的三处教材原文打回成品讲稿，并把分片反向同步为成品的内容。

背景：`_parts_enriched/ch2-a*.md` 是 21:08 的初版分片，成品讲稿后来被润色过
（口播更详），两者一度不一致；重跑 `_assemble_enriched.py` 会把成品退化。
本脚本以**成品为唯一准据**：先补丁 → 并入分片里多出的 3 张速查表 → 再按
页块把成品反向切成 a1–a4，使「拼装 == 成品」幂等。

用法：python _resync_enriched.py [--check]
  --check 只校验幂等性（不写任何文件）
"""
import os
import re
import sys

BASE = os.path.dirname(os.path.abspath(__file__))
MD = os.path.join(BASE, '第2章-软件项目启动-讲稿-教材精读增强版.md')
ASM = os.path.join(BASE, '第2章-软件项目启动-讲稿-教材精读增强版.md.assembled-2112')
PARTS = os.path.join(BASE, '_parts_enriched')
ANCHOR = re.compile(r'^###\s*【(\d+)\.(\d+)】(.+?)（\s*(\d+(?:\.\d+)?)\s*分钟\s*）\s*$', re.M)

# ---------- 定点补读：三处教材原文（书页 22 权力五类释义 / 图 2-1 / 书页 30 表头行）----------
PAIRS = [
    # ① 参照权力释义（书页 22）
    ('> **教材精读**：按来源分的五项，前四项教材都有释义（列在表 4），而"**参照权力**"的释义正好落在教材书页 22 到 23 的断页处，'
     '**OCR 文本到此为止、没有保留下来**，我们按现有讲稿口径理解为"靠人格与认同赢得追随"，考试只需记住名称。'
     '教材本章习题的填空题也点了这一条：职位权力、奖赏权力、惩罚权力、专家权力和________——答案是**参照权力**。',
     '> **教材精读**：按来源分的五项，教材原文的释义如下（**这五条已通过"定点补读"从教材书页 22 原图恢复**，'
     '此前逐页 OCR 在此处断行漏掉了"参照权力"一条）：'
     '（1）**职位权力**：源于项目经理的职位和角色，赋予其一定的管理和决策权；'
     '（2）**奖赏权力**：源于能够给予他人奖励的能力，如晋升或奖金；'
     '（3）**惩罚权力**：源于对未达预期目标的成员施加惩罚的能力，如降职、罚款；'
     '（4）**专家权力**：源于项目经理的专业知识和技能，使其在团队中具备影响力；'
     '（5）**参照权力**：**源于项目经理的个人魅力和人际关系，团队成员因为对其的尊重和认同而跟随其指引。** '
     '请对照记：前三条是"组织给的"，后两条是"自己挣的"。教材本章习题填空题也点了这一条：'
     '职位权力、奖赏权力、惩罚权力、专家权力和________——答案是**参照权力**。'),
    # ② 权力对照表：标题与"参照权力"行
    ('**表 4　项目经理权力的两种分法（教材 2.3.1；"参照权力"释义在教材断页处缺失）**',
     '**表 4　项目经理权力的两种分法（教材 2.3.1；五项释义均为教材原文，"参照权力"一条经定点补读恢复）**'),
    ('| | 参照权力 | **教材原文在书页 22→23 断页处中断，OCR 无释义**；按现有讲稿口径理解为靠人格与认同赢得追随 |',
     '| | 参照权力 | 源于项目经理的**个人魅力和人际关系**，团队成员因为对其的**尊重和认同**而跟随其指引（教材原文，经定点补读恢复） |'),
    # ③ 图 2-1 职能型组织结构标签（书页 22）
    ('> **教材精读**：这里要如实说明教材的情况。教材 2.3.2 的组织结构顺序是"**职能型 → 项目型 → 矩阵型 → 项目管理办公室**"，'
     '但"1. 职能型组织结构"的正文正好落在教材书页 22 到 23 的断页上，**OCR 只保留下标题位置，这一段正文缺失**，'
     '所以本节的定义、优缺点与适用场景沿用现有讲稿口径。',
     '> **教材精读**：先交代材料情况与恢复结果。教材 2.3.2 的组织结构顺序是"**职能型 → 项目型 → 矩阵型 → 项目管理办公室**"；'
     '"1. 职能型组织结构"的正文与配图落在教材书页 22，逐页 OCR 在此断了行，'
     '**我们已用"定点补读"从书页 22 原图恢复出配图（图 2-1 职能型组织结构）的完整结构标签**：'
     '**总经理**之下并列**市场部经理、财务部经理、研发部经理**（各部门经理之下为**员工**），'
     '另有**项目经理**与**项目协调**两个角色横向介入；该图的标题原文即"**图 2-1 职能型组织结构**"。'
     '本节的定义、优缺点与适用场景仍沿用现有讲稿口径。'),
    # ④ 会议记录表表头行（书页 30，跨 30–31 印刷）
    ('> **教材精读**：这里要交代一处材料情况——**教材 OCR 没有捕获到该表的表头行**，所以"会议名称／会议主题、时间、地点／记录人、审核人／参会人员"这几行，'
     '我们沿用**课件（现有讲稿）的表 2-4 口径**来补全；同时，教材参会人员名单'
     '（项目经理、团队成员、公司领导、PMO 代表、CCB 成员、相关职能部门负责人、其他相关干系人）'
     '可以直接作为"参会人员"行的填写依据。**凡出现 OCR 缺行的地方，课堂上都要讲明来源，不能混着当成教材原文。**',
     '> **教材精读**：**该表的表头行已经从教材原图补读回来了**——教材的表头是：'
     '**会议名称｜会议主题｜会议时间｜会议地点｜记录人｜审核人｜参会人员**'
     '（这张表跨书页 30–31 印刷，逐页 OCR 只抓到 31 页的"续表"三行，表头在 30 页，已定点补读恢复）。'
     '填写时，"参会人员"一行直接照教材名单写（项目经理、项目团队成员、公司领导、PMO 代表、CCB 成员、'
     '相关职能部门负责人、其他相关干系人，**名单由项目经理审核确认**）。'),
    ('| 会议名称 | 项目启动会（**课件表 2-4 口径**；教材 OCR 未捕获该表头行） |',
     '| 会议名称 | 项目启动会（**教材表头行：会议名称**，经定点补读恢复） |'),
    ('| 会议主题 / 时间 / 地点 | 写明主题与召开的时间地点（**课件表 2-4 口径**） |',
     '| 会议主题 / 时间 / 地点 | **教材表头行：会议主题 · 会议时间 · 会议地点**（经定点补读恢复） |'),
    ('| 记录人 / 审核人 | 记录人如实记录，审核人确认结论（**课件表 2-4 口径**；教材仅正文提及"编制纪要并分发至所有相关责任人"） |',
     '| 记录人 / 审核人 | **教材表头行：记录人 · 审核人**（经定点补读恢复）；教材正文另要求"编制《项目启动会议纪要》并分发至所有相关责任人" |'),
]

GROUPS = [('ch2-a1.md', range(1, 8), '分片 A1：2.1–2.7（启动概述 / 项目立项）'),
          ('ch2-a2.md', range(8, 14), '分片 A2：2.8–2.13（可行性研究 · 评估决策）'),
          ('ch2-a3.md', range(14, 22), '分片 A3：2.14–2.21（项目经理 · 组织结构 · 干系人 · 章程）'),
          ('ch2-a4.md', range(22, 29), '分片 A4：2.22–2.28（章程三要素 · 启动大会 · 小结）')]


def page_blocks(text):
    ms = list(ANCHOR.finditer(text))
    out = {}
    for k, m in enumerate(ms):
        end = ms[k + 1].start() if k + 1 < len(ms) else len(text)
        out[int(m.group(2))] = text[m.start():end].strip()
    return out


def main():
    check_only = '--check' in sys.argv
    t = open(MD, encoding='utf-8').read()
    hit = miss = 0
    for old, new in PAIRS:
        if old in t:
            if not check_only:
                t = t.replace(old, new)
            hit += 1
        elif new in t:
            hit += 1  # 已打过（幂等）
        else:
            miss += 1
            print('  ⚠ 未匹配：%s…' % old[:38])
    print('补丁：命中/已打 %d，未匹配 %d（共 %d）' % (hit, miss, len(PAIRS)))

    # 并入初版分片里多出的 3 张速查表（纯新增，成品原本没有）
    if not check_only and os.path.exists(ASM):
        asm = open(ASM, encoding='utf-8').read()
        i = asm.find('**教材 2.1–2.5 与启动会议 · 全景速查表**')
        if i >= 0 and '全景速查表' not in t:
            t = t.rstrip() + '\n\n' + asm[i:].rstrip() + '\n'
            print('  并入速查表块 %d 字符' % (len(asm) - i))

    blk = page_blocks(t)
    print('成品：%d 字符｜页块 %d 个（2.1–2.%d）｜定点补读 %d 处｜表格行 %d'
          % (len(t), len(blk), max(blk), t.count('定点补读'), t.count('\n| ')))

    if check_only:
        ok = True
        for fn, rng, _ in GROUPS:
            p = os.path.join(PARTS, fn)
            if not os.path.exists(p):
                print('  ⚠ 缺分片 %s' % fn); ok = False; continue
            pb = page_blocks(open(p, encoding='utf-8').read())
            for i in rng:
                if pb.get(i, '').strip() != blk[i].strip():
                    print('  ✗ %s 页 2.%d 与成品不一致' % (fn, i)); ok = False
        print('幂等校验：%s' % ('通过（分片 == 成品）' if ok else '不通过'))
        return 0 if ok else 1

    if not check_only:
        open(MD, 'w', encoding='utf-8').write(t)
        os.makedirs(PARTS, exist_ok=True)
        for fn, rng, desc in GROUPS:
            body = '\n\n'.join(blk[i] for i in rng)
            open(os.path.join(PARTS, fn), 'w', encoding='utf-8').write(
                '<!-- %s：由成品讲稿反向同步（以成品为准），勿单改分片 -->\n\n%s\n' % (desc, body))
            print('  ✓ 同步 %s（%d 页，%d 字符）' % (fn, len(list(rng)), len(body)))
        print('✓ 已写出 %s' % os.path.basename(MD))
    return 0


if __name__ == '__main__':
    sys.exit(main())
