#!/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()).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 = [
        (6, "nine_plates_table_swap", r"(?:chín|9) (?:chiếc )?đĩa.{0,240}?(?:đổi bàn|chuyển bàn|thẻ phục vụ lệch)|(?:đổi bàn|chuyển bàn).{0,220}?(?:chín|9) (?:chiếc )?đĩa"),
        (7, "duplicate_label_job", r"(?:hàng đợi|máy in|lệnh in).{0,220}?(?:lệnh trùng|thứ mười|chèn lệnh).{0,180}?(?:Nhã Tâm|rời trạm)|(?:Nhã Tâm|rời trạm).{0,220}?(?:lệnh trùng|thứ mười|chèn lệnh)"),
        (8, "post_cook_allergen", r"(?:mẫu xốt|mẫu lưu).{0,260}?(?:chất gây dị ứng|hạnh nhân).{0,180}?(?:sau khi chia|sau công đoạn|đưa vào sau)|(?:đưa vào sau|sau khi chia).{0,220}?(?:mẫu xốt|mẫu lưu)"),
        (9, "minh_uy_order_and_motive", r"Hứa Minh Uy.{0,280}?(?:ra lệnh|chỉ đạo).{0,180}?(?:chín|9) (?:chiếc )?đĩa.{0,220}?(?:chiếm công thức|loại Nhã Tâm)|(?:chiếm công thức|loại Nhã Tâm).{0,260}?Hứa Minh Uy"),
    ]
    hits = []
    for before, key, pattern in checks:
        early = "\n".join(chapters[:before])
        for match in re.finditer(pattern, early, re.I | re.S):
            context = early[max(0, match.start() - 120):match.end() + 120]
            if re.search(r"(?:chưa|không)\s+(?:đủ|thể).{0,120}?(?:chứng minh|xác định|kết luận)|(?:nghi|nếu|có thể).{0,120}$", context[:170], re.I | re.S):
                continue
            hits.append({"reveal": key, "before_chapter": before + 1, "excerpt": context})
            break
    return hits


def main():
    issues = []
    outline = json.loads(OUTLINE.read_text())
    registry = json.loads(IDENTITY.read_text())
    expected = len(outline["chapters"])
    paths = [STORY / "chapters" / f"{index:02d}.txt" for index in range(1, expected + 1)]
    texts = []
    rows = []
    for index, path in enumerate(paths, 1):
        if not path.exists():
            issues.append(f"missing_chapter:{index:02d}")
            texts.append("")
            continue
        text = path.read_text()
        texts.append(text)
        rows.append({"chapter": index, "path": str(path.relative_to(PROJECT)), "words": len(words(text)), "sha256": sha(text)})
        if not text.strip():
            issues.append(f"empty_chapter:{index:02d}")
        for line_number, line in enumerate(text.splitlines(), 1):
            if HEADING_RE.match(line):
                issues.append(f"heading:{index:02d}:{line_number}")
            if PRODUCTION_RE.search(line):
                issues.append(f"production_marker:{index:02d}:{line_number}")
    narration_path = STORY / "spoken-narration.txt"
    narration = narration_path.read_text() if narration_path.exists() else ""
    if not narration:
        issues.append("missing_or_empty_spoken_narration")
    expected_text = exact_join(texts) if all(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_number, line in enumerate(narration.splitlines(), 1):
        if HEADING_RE.match(line):
            issues.append(f"heading_in_spoken:{line_number}")
        if PRODUCTION_RE.search(line):
            issues.append(f"production_marker_in_spoken:{line_number}")
    ids = identity_hits(narration, registry) if narration else []
    if ids:
        issues.append(f"standalone_identity_fragments:{len(ids)}")
    protected = reveal_hits(texts) if all(texts) else []
    if protected:
        issues.append(f"protected_reveal_too_early:{len(protected)}")
    report = {
        "version": 1,
        "status": "passed" if not issues else "failed",
        "verified": not issues,
        "canonical_title": outline["canonical_title"],
        "expected_chapters": expected,
        "chapters_present": sum(path.exists() for path 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": ids,
        "protected_reveal_hits": protected,
        "issues": issues,
        "checked_at": datetime.now(timezone.utc).isoformat(),
    }
    REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
    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())
