#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""第15项 文学剧本 · v2.9 附录G.2.15 格式与已固化阈值确定性核查。

阈值来源（唯一出处＝工程包运行区，不新造）：
  - 单集场次 3–5 场            → 附录D·C2 已固化三项之一（术语表第9行/J17）
  - 正文字数 500–1000 字（去标注字数）→ 附录D·C2 已固化三项之一
  - 台词单句 ≤15 字            → 附录D·C2 已固化三项之一
格式来源：附录G.1 结构依据段＋附录G.2.15；剔除分镜执行内容＝v2.6 无例外声明（S4-4 强制边界）。
用法：python3 check_g215_gates.py 文件.md [文件2.md ...]
"""
import re
import sys
import unicodedata

HDR_RE = re.compile(r"^第(\d+)场\s+(\S+)\s+(日|夜)\s+(内|外)\s*$")
DIALOG_RE = re.compile(r"^([\u4e00-\u9fa5A-Za-z]{2,10})：（([^）]*)）(.*)$")
BANNED_CAMERA = ["镜号", "景别", "机位", "运镜", "构图", "特写", "镜头号", "画面提示", "推拉摇移"]
BANNED_MIND = ["心里想", "心中想", "内心想着", "感受到一种", "感到一阵", "情绪涌上", "内心独白"]


def visible_len(s: str) -> int:
    """去标注字数：只计中日韩文字与字母数字，不计标点、空格与【】标记。"""
    return sum(1 for ch in s if unicodedata.category(ch)[0] in ("L", "N"))


def check(path: str) -> int:
    lines = open(path, encoding="utf-8").read().split("\n")
    fails, notes = [], []
    scenes = [i for i, ln in enumerate(lines) if HDR_RE.match(ln.strip())]
    body = [ln for ln in lines if ln.strip() and not HDR_RE.match(ln.strip())
            and not ln.startswith(("#", "版本：")) and not re.match(r"^第\d+集", ln.strip())]

    # 门1 场标头格式
    if len(scenes) < 1:
        fails.append("门1 场标头：未找到符合「第X场 场景名 日/夜 内/外」的行")
    else:
        bad = [lines[i].strip() for i in range(scenes[0], len(lines))
               if re.match(r"^第\d+场", lines[i].strip()) and not HDR_RE.match(lines[i].strip())]
        if bad:
            fails.append(f"门1 场标头：格式不符 {bad}")

    # 门2 单集场次 3–5
    n = len(scenes)
    if not 3 <= n <= 5:
        fails.append(f"门2 单集场次＝{n}，须 3–5 场")
    else:
        notes.append(f"门2 单集场次＝{n} ✅")

    # 门3 正文字数 500–1000（去标注）
    content = [ln.strip() for ln in body if ln.strip()]
    stripped = [re.sub(r"【[^】]*】", "", ln) for ln in content]
    wc = visible_len("".join(stripped))
    if not 500 <= wc <= 1000:
        fails.append(f"门3 正文字数（去标注）＝{wc}，须 500–1000 字")
    else:
        notes.append(f"门3 正文字数（去标注）＝{wc} ✅")

    # 门4 台词单句 ≤15 字 + 门10 台词行格式（除场标头/【】标注行/文头/集标题外的行一律须为台词行）
    over, nonstd, dcount = [], [], 0
    for i, ln in enumerate(lines):
        s = ln.strip()
        if not s:
            continue
        if (HDR_RE.match(s) or s.startswith(("【", "#", "版本："))
                or re.match(r"^第\d+集", s)):
            continue
        dcount += 1
        m = DIALOG_RE.match(s)
        if not m:
            nonstd.append(s[:30])
            continue
        text = visible_len(m.group(3))
        if text > 15:
            over.append((s[:28], text))
    if over:
        fails.append(f"门4 台词单句超 15 字（{len(over)} 处）：{over}")
    else:
        notes.append(f"门4 台词单句 ≤15 字 ✅（共 {dcount} 句，去标点计字）")
    if nonstd:
        fails.append(f"门10 台词行格式非「人物名：（动作/表情）台词」：{nonstd}")
    else:
        notes.append(f"门10 台词行格式规范 ✅（{dcount} 句）")

    # 门5 每场至少一条【动作描写】独立动作行
    for k, i in enumerate(scenes):
        end = scenes[k + 1] if k + 1 < len(scenes) else len(lines)
        seg = [x.strip() for x in lines[i + 1:end]]
        acts = [x for x in seg if x.startswith("【动作描写】")]
        if not acts:
            fails.append(f"门5 第{lines[i].strip().split()[0]} 无【动作描写】独立动作行")
        else:
            notes.append(f"门5 {lines[i].strip().split()[0]} 动作节拍 {len(acts)} 条 ✅")

    # 门6 无分镜执行内容
    hit = [w for w in BANNED_CAMERA if w in open(path, encoding="utf-8").read()]
    if hit:
        fails.append(f"门6 出现分镜执行内容：{hit}")
    else:
        notes.append("门6 无镜号/景别/机位/运镜/构图 ✅")

    # 门7 无 INT./EXT.
    if re.search(r"\b(INT|EXT)\b", open(path, encoding="utf-8").read()):
        fails.append("门7 出现 INT./EXT. 标头（v2.9 不作运行期填写标准）")
    else:
        notes.append("门7 无 INT./EXT. ✅")

    # 门8 无 OS（统一 V.O.）
    raw = open(path, encoding="utf-8").read()
    if re.search(r"OS\b|OS：|（OS", raw):
        fails.append("门8 出现 OS（别名，不作正式标注，须改 V.O.）")
    else:
        notes.append("门8 无 OS，V.O. 用法规范 ✅")

    # 门9 无【转场】行
    if re.search(r"【转场】|^转场[:：]", raw, re.M):
        fails.append("门9 出现转场标注（G.2.15 不设固定槽位）")
    else:
        notes.append("门9 无转场标注 ✅")

    # 门11 无抽象心理描写
    hit2 = [w for w in BANNED_MIND if w in raw]
    if hit2:
        fails.append(f"门11 抽象心理描写：{hit2}")
    else:
        notes.append("门11 无抽象心理直接陈述 ✅")

    print(f"\n===== {path.split('/')[-1]} =====")
    for x in notes:
        print("  " + x)
    for x in fails:
        print("  ❌ " + x)
    return 0 if not fails else 1


if __name__ == "__main__":
    rc = 0
    for p in sys.argv[1:]:
        rc |= check(p)
    sys.exit(rc)
