#!/usr/bin/env python3
"""用 pdftotext 抽条款，核对 AI 答复草稿里的引用（v3）。

用法: python3 check_citations.py <制度目录> <答复草稿.md> [--questions 群聊_已脱敏.txt]

逐条检查：
  ① 引用的文件 + 条款号是否存在
  ② 是否把已废止版本当依据
  ③ 答复里的数字能否在所引现行条款原文里找到（找不到 = 推算/编造/引错，需人工看）
每段答复的结论只会是以下之一：
  已核        至少一条引用被脚本核到
  需人工      有实际答复文字，且依据里明说“无制度依据/转人工”，脚本无可核的条款，交人判断（合法情形）
  未完成核对  认不出答复、答复正文为空（只有标签/提问/依据）、认不出依据字段、或依据里有条款号却认不出文件 —— 不等于没问题
退出码：0 = 全部已核/需人工且无问题；1 = 核到了问题；2 = 未完成核对（含识别到 0 段、段数少于问题数、空输入）
"""
import re, subprocess, sys, pathlib, argparse

CN = "零一二三四五六七八九十百"
CLAUSE = rf"第[{CN}]+条"
ap = argparse.ArgumentParser()
ap.add_argument("pdf_dir"); ap.add_argument("draft"); ap.add_argument("--questions")
a = ap.parse_args()

# ---- 1. 抽条款 ----
clauses, deprecated, ver2file = {}, {}, {}
for pdf in sorted(pathlib.Path(a.pdf_dir).glob("*.pdf")):
    txt = subprocess.run(["pdftotext", "-layout", str(pdf), "-"], capture_output=True, text=True, check=True).stdout
    v = re.search(r"[（(]v(\d+\.\d+)[）)]", txt[:300])
    if v: ver2file["v" + v.group(1)] = pdf.name          # 让「考勤 v3.0」这类简称也能对上文件
    deprecated[pdf.name] = bool(re.search(r"已.{0,20}废止", txt[:400]))
    parts = re.split(f"({CLAUSE})", txt)
    clauses[pdf.name] = {parts[i]: re.sub(r"\s+", "", parts[i+1].split("第")[0]) for i in range(1, len(parts)-1, 2)}
    print(f"[抽取] {pdf.name}: {len(clauses[pdf.name])} 条{'  ⚠已废止' if deprecated[pdf.name] else ''}")
if not clauses:
    print("✗ 未完成核对：制度目录里没有可抽取的 PDF"); sys.exit(2)

# ---- 2. 输入问题数（可选）----
n_questions = None
if a.questions:
    chat = open(a.questions, encoding="utf-8").read()
    msgs = re.findall(r"^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d (.+)\n(.+)$", chat, re.M)
    n_questions = sum(1 for who, body in msgs if "(HR)" not in who and body.strip() != "[图片]")
    print(f"[问题] 群聊里员工文字消息 {n_questions} 条（不含 HR 发言和纯图片）")

# ---- 3. 切分答复段：任意级别 Markdown 标题，或「数字. / 问题N」开头的行 ----
draft = open(a.draft, encoding="utf-8").read()
head_re = re.compile(r"^(?:#{2,6}\s*(?:Q|问题)?\s*\d+|\*\*(?:Q|问题)?\s*\d+[.、．]|(?:Q|问题)\s*\d+[.、．:：])", re.M)
starts = [m.start() for m in head_re.finditer(draft)]
sections = [draft[s:e] for s, e in zip(starts, starts[1:] + [len(draft)])]
# 去掉文末的汇总表段（标题里没有数字的不会进来；带数字但没有任何字段的，算未识别）
BASIS_RE = re.compile(r"^\s*(?:[-*>]\s*)?(?:\*\*)?(?:依据|出处|来源)(?:\*\*)?\s*[:：]", re.M)
# 非答复正文的行：答复类空标签、提问/原问题、备注、分隔线
ANS_LABEL = re.compile(r"^\s*(?:[-*>]\s*)?(?:\*\*)?(?:答复草稿|答复|回复|建议答复|拟答复)(?:\*\*)?\s*[:：]\s*(?:\*\*)?")
NON_ANSWER = re.compile(r"^\s*(?:[-*>]\s*)?(?:\*\*)?(?:提问|原问题|员工原问|问题|备注|HR\s*内部)[^:：]{0,12}(?:\*\*)?\s*[:：]|^\s*(?:-{3,}|\*{3,}|_{3,})\s*$")

def extract_answer(body_lines):
    """从段落正文（已去掉标题、依据行）里取真正的答复文字；空标签、提问、备注、分隔线都不算。"""
    quoted = [l for l in body_lines if re.match(r"^\s*>", l)]
    if quoted and _collect(quoted): return _collect(quoted)    # 有引用块答复时只认引用块（与 v2 一致）
    return _collect(body_lines)

def _collect(body_lines):
    out = []
    for l in body_lines:
        if NON_ANSWER.match(l): continue
        l = ANS_LABEL.sub("", l)            # 「**答复草稿**：xxx」只保留 xxx
        l = re.sub(r"^\s*>\s?", "", l)     # 引用块符号
        if re.search(r"[\u4e00-\u9fa5A-Za-z0-9]", l): out.append(l.strip())
    text = "".join(out)
    return text if len(re.findall(r"[\u4e00-\u9fa5A-Za-z]", text)) >= 4 else ""   # 少于 4 个字不算有答复

problems = unfinished = manual = checked = 0
for sec in sections:
    lines = sec.splitlines(); title = lines[0].lstrip("#* ").strip()
    for i, l in enumerate(lines[1:], 1):          # 段内遇到不带编号的新标题（如「### 汇总」）就截断
        if re.match(r"^#{1,6}\s", l): lines = lines[:i]; break
    print(f"\n== {title}")
    basis_lines = [l for l in lines if BASIS_RE.match(l)]
    if not basis_lines:
        print("  ✗ 未完成核对：没找到「依据/出处/来源：」字段"); unfinished += 1; continue
    basis = "\n".join(basis_lines)
    answer = extract_answer([l for l in lines[1:] if l not in basis_lines])
    if not answer:
        print("  ✗ 未完成核对：没有实际答复文字（只有标题/标签/提问/依据/分隔线）"); unfinished += 1; continue
    cites, cur = [], None
    for tok in re.finditer(rf"([^\s/《》【】\[\]]+?\.pdf)|(v\d+\.\d+)|({CLAUSE})", basis):
        if tok.group(1): cur = tok.group(1)
        elif tok.group(2): cur = ver2file.get(tok.group(2), cur)
        elif cur: cites.append((cur, tok.group(3)))
    if not cites:
        if re.search(CLAUSE, basis):
            print("  ✗ 未完成核对：依据里有条款号，但认不出是哪个文件"); unfinished += 1
        elif re.search(r"[:：]\s*(\*\*)?无|无.{0,12}(依据|条款|规定)|没有.{0,8}(依据|条款|规定)|未涉及|转人工|负责人确认", basis):
            print("  · 需人工：依据写明无制度条款，交人判断"); manual += 1
        else:
            print("  ✗ 未完成核对：依据字段里认不出任何条款"); unfinished += 1
        continue
    used_text, sec_ok = "", False
    for f, c in cites:
        if f not in clauses: print(f"  ✗ 文件不存在: {f}"); problems += 1; continue
        if c not in clauses[f]: print(f"  ✗ 条款不存在: {f} {c}"); problems += 1; continue
        sec_ok = True
        i = basis.find(f); ctx = basis[max(0, i-30):i+len(f)+15] if i >= 0 else basis
        if deprecated[f]:
            if re.search(r"对照|旧|废止|当时有效", ctx): print(f"  · 引了废止版 {f} {c}，标明为对照/当时有效（人工确认措辞）")
            else: print(f"  ✗ 以废止版为依据: {f} {c}"); problems += 1; continue
        else: used_text += clauses[f][c]
        print(f"  ✓ {f} {c}: {clauses[f][c][:40]}")
    checked += sec_ok
    clean = re.sub(r"v\d+\.\d+|20\d\d[-年./]\s*\d{1,2}\s*[-月./]\s*\d{1,2}\s*日?|20\d\d\s*年\s*\d{1,2}\s*月|\d{1,2}\s*月\s*\d{1,2}\s*日|20\d\d\s*年?", "", answer)  # 去掉日期，避免把 7 月 1 日 当成可疑数字
    for n in sorted(set(re.findall(r"\d+(?:\.\d+)?", clean))):
        if used_text and n not in used_text:
            print(f"  ? 答复里的数字 {n} 不在所引现行条款原文中（推算/编造/引错，需人工核）"); problems += 1

n = len(sections)
print("\n---- 汇总 ----")
print(f"识别答复段 {n}" + (f" / 输入问题 {n_questions}" if n_questions is not None else "（未给 --questions，无法核对是否每个问题都有答复）"))
print(f"已核 {checked} 段；需人工 {manual} 段；未完成核对 {unfinished} 段；需处理问题 {problems} 处")
if n == 0:
    print("✗ 未完成核对：一段答复都没认出来（格式变了或文件为空），这不代表没有问题"); sys.exit(2)
if n_questions is not None and n < n_questions:
    print(f"✗ 未完成核对：答复段 {n} 少于问题 {n_questions}，有问题没答或没认出"); sys.exit(2)
if unfinished:
    print("✗ 未完成核对：上面标「未完成核对」的段落脚本没核，请人工看或调整格式后重跑"); sys.exit(2)
sys.exit(1 if problems else 0)
