#!/usr/bin/env python3
import hashlib
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path

PROJECT = Path(__file__).resolve().parents[1]
STORY = PROJECT / "story"
OUTLINE = PROJECT / "script" / "outline.json"
IDENTITY = PROJECT / "script" / "identity-registry.json"
REPORT = STORY / "promotion-report.json"
WORD_RE = re.compile(r"[\wÀ-ỹĐđ]+", re.UNICODE)
HEADING_RE = re.compile(r"^\s*(?:chương|phần|cảnh|chapter|part|scene)\s+(?:\d+|[ivxlcdm]+)\s*[:.\-]?", re.I)
PRODUCTION_RE = re.compile(r"\[(?:sfx|music|nhạc|âm thanh|pause|transition)\]", re.I)


def sha(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def words(text):
    return WORD_RE.findall(text)


def exact_join(texts):
    return "\n\n".join(text.strip() for text in texts).strip() + "\n"


def identity_hits(text, registry):
    scrubbed = text
    fragments = set()
    for entity in registry["characters"]:
        allowed = sorted(set(entity.get("allowed_narrative_names", []) + entity.get("allowed_title_names", []) + entity.get("role_address", []) + [entity["full_name"]]), key=len, reverse=True)
        for value in allowed:
            scrubbed = re.sub(rf"(?<![\wÀ-ỹĐđ]){re.escape(value)}(?![\wÀ-ỹĐđ])", " ", scrubbed, flags=re.I)
        fragments.update(entity.get("forbidden_bare_names", []))
    lead = r"(?:cô|anh|chị|ông|bà|cậu|em|gọi|hỏi|bảo|đáp|quát|nhìn|nói với|thì thầm với)\s+"
    hits = []
    for fragment in sorted(fragments):
        patterns = [
            re.compile(rf"(?i)(?<![\wÀ-ỹĐđ]){lead}{re.escape(fragment)}(?![\wÀ-ỹĐđ])"),
            re.compile(rf"[,;:]\s*{re.escape(fragment)}\s*[,!?.:]"),
            re.compile(rf"[\"“]\s*{re.escape(fragment)}\s*[,!?.:]"),
        ]
        for pattern in patterns:
            for match in pattern.finditer(scrubbed):
                hits.append({"fragment": fragment, "line": scrubbed.count("\n", 0, match.start()) + 1, "context": scrubbed[max(0, match.start()-60):match.end()+80].strip()})
                if len(hits) >= 50:
                    return hits
    return hits


def reveal_hits(chapters):
    checks = [
        (8, "forged_waivers", r"bảy (?:giấy|chữ ký).{0,180}?(?:giả|giả mạo)|giấy từ bỏ.{0,180}?(?:giả|giả mạo)"),
        (5, "seven_compartments_function", r"bảy chìa.{0,180}?mở bảy ngăn"),
        (8, "transfer_condition", r"chỉ được bán khi.{0,220}?(?:đồng thuận|quỹ hoàn trả)|điều kiện chuyển quyền.{0,220}?(?:đồng thuận|hoàn trả)"),
        (9, "inspection_false_signature", r"Tử Dương.{0,240}?(?:ký|xác nhận).{0,180}?(?:chưa đến|không đến).{0,120}?(?:hiện trường|căn nhà)"),
    ]
    hits = []
    for before_count, key, pattern in checks:
        early = "\n".join(chapters[:before_count])
        for match in re.finditer(pattern, early, re.I | re.S):
            context = early[max(0, match.start()-100):match.end()+100]
            if key == "forged_waivers" and re.search(r"(?:chưa|không)\s+(?:đủ|thể).{0,80}?(?:chứng minh|xác định)|(?:nghi|nếu).{0,100}$", context[:100], re.I | re.S):
                continue
            hits.append({"reveal": key, "before_chapter": before_count + 1, "excerpt": context})
            break
    return hits


def main():
    issues = []
    outline = json.loads(OUTLINE.read_text(encoding="utf-8"))
    registry = json.loads(IDENTITY.read_text(encoding="utf-8"))
    expected = len(outline["chapters"])
    paths = [STORY / "chapters" / f"{i:02d}.txt" for i in range(1, expected + 1)]
    chapter_texts, rows = [], []
    for i, path in enumerate(paths, 1):
        if not path.exists():
            issues.append(f"missing_chapter:{i:02d}")
            chapter_texts.append("")
            continue
        text = path.read_text(encoding="utf-8")
        chapter_texts.append(text)
        rows.append({"chapter": i, "path": str(path.relative_to(PROJECT)), "words": len(words(text)), "sha256": sha(text)})
        if not text.strip():
            issues.append(f"empty_chapter:{i:02d}")
        for line_no, line in enumerate(text.splitlines(), 1):
            if HEADING_RE.match(line): issues.append(f"heading:{i:02d}:{line_no}")
            if PRODUCTION_RE.search(line): issues.append(f"production_marker:{i:02d}:{line_no}")
    narration_path = STORY / "spoken-narration.txt"
    narration = narration_path.read_text(encoding="utf-8") if narration_path.exists() else ""
    if not narration: issues.append("missing_or_empty_spoken_narration")
    expected_text = exact_join(chapter_texts) if all(chapter_texts) else ""
    if narration and expected_text and narration != expected_text: issues.append("spoken_narration_not_exact_sequential_join")
    count = len(words(narration))
    low, high = outline["allowed_word_range"]
    if not low <= count <= high: issues.append(f"word_count_out_of_range:{count}")
    for line_no, line in enumerate(narration.splitlines(), 1):
        if HEADING_RE.match(line): issues.append(f"heading_in_spoken:{line_no}")
        if PRODUCTION_RE.search(line): issues.append(f"production_marker_in_spoken:{line_no}")
    id_hits = identity_hits(narration, registry) if narration else []
    if id_hits: issues.append(f"standalone_identity_fragments:{len(id_hits)}")
    protected = reveal_hits(chapter_texts) if all(chapter_texts) else []
    if protected: issues.append(f"protected_reveal_too_early:{len(protected)}")
    dead_now = []
    if narration:
        for m in re.finditer(r"Tô Nhã Cầm\s+(?:đi|bước|mở|đứng|ngồi|gọi|nói|đưa|gửi)\b", narration, re.I):
            context = narration[max(0,m.start()-120):m.end()+140]
            if not re.search(r"(?:nhớ|hồi ức|năm ấy|ngày trước|lá thư|bản ghi|giọng.*?vang|trong mơ)", context, re.I | re.S):
                dead_now.append({"line": narration.count("\n",0,m.start())+1,"context":context.strip()})
    if dead_now: issues.append(f"deceased_character_outside_historical_container:{len(dead_now)}")
    report = {"version":1,"status":"passed" if not issues else "failed","verified":not issues,"canonical_title":outline["canonical_title"],"expected_chapters":expected,"chapters_present":sum(p.exists() for p in paths),"chapter_rows":rows,"spoken_narration":str(narration_path.relative_to(PROJECT)),"total_words":count,"target_range":[low,high],"estimated_minutes_at_baseline":round(count/233.33,3) if count else 0,"spoken_sha256":sha(narration) if narration else None,"identity_hits":id_hits,"protected_reveal_hits":protected,"deceased_container_hits":dead_now,"issues":issues,"checked_at":datetime.now(timezone.utc).isoformat()}
    REPORT.write_text(json.dumps(report,ensure_ascii=False,indent=2)+"\n",encoding="utf-8")
    print(json.dumps({"verified":report["verified"],"chapters":report["chapters_present"],"total_words":count,"issues":issues[:20],"report":str(REPORT)},ensure_ascii=False))
    return 0 if report["verified"] else 1

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