#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
报告出厂校验门禁（pre_release_gate）
=====================================
基于《报告通用铁律 v1.0》自动校验报告 docx，校验不通过禁止下发。

覆盖铁律：
  铁律3 - 公文格式标准化（段落格式抽样校验）
  铁律4 - 全文格式统一（段落 left_indent / first_line_indent 一致性）
  铁律5 - 正文洁净（元痕迹/工具元数据/骨架装饰/markdown 残留/空小节）
  铁律6 - 文件命名与主标题规范（主题.docx，无日期）
  铁律1 - 禁用媒体黑名单
  铁律2 - 政治立场红线（关键词层）

用法：
  python pre_release_gate.py <报告docx路径> [docx路径2 ...]
  python pre_release_gate.py <目录>            # 校验目录下所有 docx

退出码：
  0 - 全部通过
  1 - 有报告未通过
"""
import os
import re
import sys
import glob
from docx import Document
from docx.shared import Emu

WORDML_NS = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
TARGET_FIRST_LINE_INDENT = Emu(406400)

# ===== 违规模式（来自历史真实问题归纳）=====

META_WORKFLOW_PATTERNS = [
    r"本次搜索", r"建议下一步", r"宁可省略", r"按技能要求",
    r"按您的指示", r"已纳入信源池", r"twscrape", r"scrapling",
    r"DuckDuckGo", r"已验证 status", r"PART[123]", r"R\d+HEAD",
    r"REPORT_END", r"／注：",
]

META_DATA_PATTERNS = [
    r"真实性核查报告", r"信源类型分布", r"信源国家分布",
    r"禁用媒体过滤", r"验证通过率", r"参考文献总数", r"域名权重",
    r"本报告基于\s*\d+\s*条已验证\s*URL",
]

SKELETON_PATTERNS = [
    r"^长官[：:]\s*按照您的指示",
    r"^对您忠诚",
    r"^呈报人[：:]",
    r"^报告完成时间[：:]",
    r"瞰宇（全球数据采集师",
]

BULLET_PREFIX_RE = re.compile(r"^[·•・]\s*")
MD_BOLD_RE = re.compile(r"\*\*[^*\n]+?\*\*")
MD_HR_RE = re.compile(r"^-{3,}$|^={3,}$")

# 禁用媒体：使用正则边界避免误匹配（如 IRFA 不应触发 RFA）
BANNED_MEDIA = [
    ("大纪元", "大纪元"),
    ("Epoch Times", r"\bEpoch Times\b"),
    ("美国之音", "美国之音"),
    ("VOA", r"(?<![A-Za-z])VOA(?![A-Za-z])"),
    ("自由亚洲电台", "自由亚洲电台"),
    ("RFA", r"(?<![A-Za-z])RFA(?![A-Za-z])"),
    ("Radio Free Asia", r"\bRadio Free Asia\b"),
    ("新唐人", "新唐人"),
    ("NTDTV", r"(?<![A-Za-z])NTDTV(?![A-Za-z])"),
    ("法轮", "法轮"),
    ("Falun", r"\bFalun\b"),
]

# “分析批判上下文”关键词：如果禁用媒体名出现在这些词汇 50 字范围内，
# 视为“识别批判”而非“作为信源引用”，不报错。
BANNED_MEDIA_CRITICAL_CONTEXT = [
    "造谣", "虚假叙事", "虚假信息", "反华叙事", "反华媒体",
    "极右翼", "不实报道", "谣言", "质疑可信度",
    "被揭露", "被批评", "含明显偏见", "偏见性",
    "点名批评", "点名揭露", "作为点名对象",
    "作为虚假", "作为造谣源", "作为反华",
    "造谣源", "造谣源头", "谣言制造者",
    "被中国定义为", "被认定为反华",
    # 2026-06-11 补充：批判/叙事场景关键词
    "伪证", "调查证伪", "叙事升级", "叙事工程",
    "长周期叙事", "被包装", "被编造", "被证伪",
    "族群标签", "标签捆绑", "捆绑炒作", "驳斥",
    "批驳", "炒作", "团体在", "（相关伪证",
    "调查机构", "美国国家民主基金会", "NED",
    "自由之家", "长期资助",
]

POLITICAL_RED_LINES = [
    r"the country of Taiwan", r"独立的台湾",
    r"台湾共和国", r"Republic of Taiwan",
]

# “台湾国” 的使用上下文判别：如果是引用台独表述进行批判，不报错
TAIWAN_CRITICAL_CONTEXT = [
    "所谓", "台独", "分裂", "虚假", "错误表述",
    "不实表述", "误称", "谬称", "造谣", "偷换",
    "伪造", "伪表述", "包装为", "反驳", "批驳",
    "中華民國", "中华民国",  # 台当局自称体被引用时
]

FILENAME_RE = re.compile(r"^[\u4e00-\u9fff].*\.docx$")
FILENAME_BANNED_TOKENS = ["请查阅", "请审阅", "最终版", "长官", "_final", "report_"]


class Violation:
    def __init__(self, rule, level, message):
        self.rule = rule
        self.level = level
        self.message = message

    def __str__(self):
        icon = "❌" if self.level == "ERROR" else "⚠️"
        return f"  {icon} [铁律{self.rule}] {self.message}"


def check_filename(path):
    violations = []
    fname = os.path.basename(path)
    if not FILENAME_RE.match(fname):
        violations.append(Violation("6", "ERROR",
            f"文件名不符合 报告主题.docx 格式（不得包含日期前缀）: {fname}"))
    for token in FILENAME_BANNED_TOKENS:
        if token in fname:
            violations.append(Violation("6", "ERROR",
                f"文件名含禁用词 '{token}': {fname}"))
    return violations


def check_paragraph_format(doc):
    violations = []
    chapter_title_re = re.compile(r"^第[一二三四五六七八九十]+章")
    appendix_re = re.compile(r"^(参考文献|附录|附件)")
    in_appendix = False
    bad_indent = 0
    bad_numpr = 0
    bullet_paras = []

    for p in doc.paragraphs:
        t = p.text.strip()
        if not t:
            continue
        if appendix_re.match(t):
            in_appendix = True
            continue
        if in_appendix:
            continue
        if chapter_title_re.match(t):
            continue

        pf = p.paragraph_format
        if pf.left_indent is not None:
            bad_indent += 1
        if pf.first_line_indent != TARGET_FIRST_LINE_INDENT:
            bad_indent += 1
        pPr = p._p.find(f"{WORDML_NS}pPr")
        if pPr is not None and pPr.find(f"{WORDML_NS}numPr") is not None:
            bad_numpr += 1
        if BULLET_PREFIX_RE.match(t):
            bullet_paras.append(t[:40])

    if bad_indent > 0:
        violations.append(Violation("3/4", "ERROR",
            f"段落缩进异常 {bad_indent} 处（应 left=None, first_line=2字符）"))
    if bad_numpr > 0:
        violations.append(Violation("4", "ERROR",
            f"残留 numPr 列表样式 {bad_numpr} 处"))
    if bullet_paras:
        violations.append(Violation("5.D", "ERROR",
            f"段首项目符号 {len(bullet_paras)} 处，示例: {bullet_paras[0]}..."))
    return violations


def check_clean_content(doc):
    violations = []
    full_text = "\n".join(p.text for p in doc.paragraphs)

    for pat in META_WORKFLOW_PATTERNS:
        m = re.search(pat, full_text)
        if m:
            violations.append(Violation("5.A", "ERROR",
                f"含元工作流痕迹 '{m.group(0)}'"))
    for pat in META_DATA_PATTERNS:
        m = re.search(pat, full_text)
        if m:
            violations.append(Violation("5.B", "ERROR",
                f"含工具元数据 '{m.group(0)}'"))
    for p in doc.paragraphs:
        t = p.text.strip()
        for pat in SKELETON_PATTERNS:
            if re.search(pat, t):
                violations.append(Violation("5.C", "ERROR",
                    f"含骨架装饰 '{t[:40]}'"))
                break
    if MD_BOLD_RE.search(full_text):
        violations.append(Violation("5.D", "ERROR",
            "含 markdown **加粗** 标记残留"))
    for p in doc.paragraphs:
        t = p.text.strip()
        if MD_HR_RE.match(t):
            violations.append(Violation("5.D", "ERROR",
                f"含 markdown 分隔线 '{t}'"))
            break
    return violations


def _is_critical_context(full_text, match_start, match_end, context_keywords, window=80):
    """判断匹配位置前后 window 字符是否含有批判上下文关键词。"""
    start = max(0, match_start - window)
    end = min(len(full_text), match_end + window)
    context = full_text[start:end]
    return any(kw in context for kw in context_keywords)


def check_banned_media(doc):
    """铁律1：禁用媒体。区分“引用作信源”（ERROR）vs“识别批判”（不报警）。"""
    violations = []
    full_text = "\n".join(p.text for p in doc.paragraphs)
    for label, pattern in BANNED_MEDIA:
        matches = list(re.finditer(pattern, full_text))
        if not matches:
            continue
        cite_count = 0
        for m in matches:
            if not _is_critical_context(full_text, m.start(), m.end(),
                                          BANNED_MEDIA_CRITICAL_CONTEXT):
                cite_count += 1
        if cite_count > 0:
            violations.append(Violation("1", "ERROR",
                f"使用禁用媒体 '{label}' 作为信源（出现 {cite_count} 次）"))
    return violations


def check_political_stance(doc):
    """铁律2：政治立场红线。台湾相关表述上下文鉴别。"""
    violations = []
    full_text = "\n".join(p.text for p in doc.paragraphs)
    for pat in POLITICAL_RED_LINES:
        for m in re.finditer(pat, full_text, re.IGNORECASE):
            # 同样鉴别：是引用还是批判
            if not _is_critical_context(full_text, m.start(), m.end(),
                                         TAIWAN_CRITICAL_CONTEXT):
                violations.append(Violation("2", "ERROR",
                    f"政治立场红线表述 '{m.group(0)}'"))
    return violations


def check_empty_sections(doc):
    """铁律5.E：空小节。“第N章”下面允许紧跟“一、”小节标题，不算空。"""
    violations = []
    paras = doc.paragraphs
    chapter_re = re.compile(r"^第[一二三四五六七八九十]+章")
    subsection_re = re.compile(r"^[（(]?[一二三四五六七八九十]+[）)]?[、\s]")
    appendix_re = re.compile(r"^(参考文献|附录|附件)")

    for i, p in enumerate(paras):
        t = p.text.strip()
        if not t:
            continue
        is_chapter = bool(chapter_re.match(t))
        is_subsec = bool(subsection_re.match(t))
        if not (is_chapter or is_subsec):
            continue
        # 看后面上限 8 段是否有实质内容。
        # 对于第N章，后跟小节标题也算有内容（子小节就是它的内容架构）。
        # 对于小节标题，必须在遇到下一小节标题/章节标题前有正文。
        has_content = False
        for j in range(i + 1, min(i + 10, len(paras))):
            nt = paras[j].text.strip()
            if not nt:
                continue
            if appendix_re.match(nt):
                break
            j_is_chapter = bool(chapter_re.match(nt))
            j_is_subsec = bool(subsection_re.match(nt))
            if is_chapter:
                # 第N章：后跟任何非空（含小节标题）皆算有内容
                has_content = True
                break
            else:
                # 小节：后跟必须是正文，不能是另一个标题
                if j_is_chapter or j_is_subsec:
                    break
                has_content = True
                break
        if not has_content:
            violations.append(Violation("5.E", "WARN",
                f"疑似空小节 '{t[:30]}'"))
    return violations


def validate_file(path):
    violations = []
    violations.extend(check_filename(path))
    try:
        doc = Document(path)
    except Exception as e:
        violations.append(Violation("-", "ERROR", f"docx 打开失败: {e}"))
        return violations
    violations.extend(check_paragraph_format(doc))
    violations.extend(check_clean_content(doc))
    violations.extend(check_banned_media(doc))
    violations.extend(check_political_stance(doc))
    violations.extend(check_empty_sections(doc))
    return violations


def collect_files(paths):
    files = []
    for path in paths:
        if os.path.isdir(path):
            files.extend(sorted(
                glob.glob(os.path.join(path, "**", "*.docx"), recursive=True)))
        elif os.path.isfile(path) and path.endswith(".docx"):
            files.append(path)
    return files


def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    files = collect_files(sys.argv[1:])
    if not files:
        print("未找到 docx 文件")
        sys.exit(1)

    print(f"\n🔒 报告出厂校验门禁 - 共 {len(files)} 个文件\n" + "=" * 60)
    total_pass = 0
    total_fail = 0
    for f in files:
        print(f"\n📄 {os.path.basename(f)}")
        violations = validate_file(f)
        errors = [v for v in violations if v.level == "ERROR"]
        warns = [v for v in violations if v.level == "WARN"]
        if not errors:
            print(f"  ✅ 通过（警告 {len(warns)} 项）")
            total_pass += 1
        else:
            print(f"  ❌ 不通过（错误 {len(errors)} 项，警告 {len(warns)} 项）")
            total_fail += 1
        for v in violations:
            print(str(v))

    print("\n" + "=" * 60)
    print(f"总结: 通过 {total_pass} | 不通过 {total_fail}")
    if total_fail > 0:
        print("\n🚫 校验失败的报告禁止下发，请修复后重新校验。")
        sys.exit(1)
    print("\n✅ 全部通过，可以下发。")
    sys.exit(0)


if __name__ == "__main__":
    main()
