#!/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" / "v3"
OUTLINE = STORY / "outline.json"
REPORT = STORY / "promotion-report.json"
WORD_RE = re.compile(r"\b[\wÀ-ỹĐđ]+\b", re.UNICODE)
HEADING_RE = re.compile(
    r"^\s*(?:chương|phần|cảnh|chapter|part|scene)\s*(?:\d+|[ivxlcdm]+)?\s*[:.\-]?",
    re.IGNORECASE,
)
PRODUCTION_RE = re.compile(r"\[(?:sfx|music|nhạc|âm thanh|pause|transition)\]", re.IGNORECASE)


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


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


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


def standalone_identity_hits(text, registry):
    # Remove complete registered names first. Flag fragments only in name-like contexts;
    # many Chinese-name syllables are also ordinary Vietnamese words at sentence start.
    scrubbed = text
    fragments = set()
    excluded = {"Quỹ Cứu Trợ Trường Phong", "Lâm Giang"}
    for full_name in registry:
        scrubbed = re.sub(rf"(?<!\w){re.escape(full_name)}(?!\w)", " ", scrubbed)
        if full_name not in excluded:
            parts = full_name.split()
            if len(parts) >= 2:
                fragments.update(parts)

    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*[,!?.:]"),
        ]
        seen = set()
        for pattern in patterns:
            for match in pattern.finditer(scrubbed):
                key = match.start()
                if key in seen:
                    continue
                seen.add(key)
                line = scrubbed.count("\n", 0, match.start()) + 1
                context = scrubbed[max(0, match.start() - 50):match.end() + 80].strip()
                hits.append({"fragment": fragment, "line": line, "context": context})
                if len(hits) >= 50:
                    return hits
    return hits


def reveal_hits(chapter_texts):
    # Protected facts may only become explicit in chapters 9-10 or later.
    early = "\n".join(chapter_texts[:8])
    patterns = {
        "backup_token_mechanism": r"token dự phòng|mã dự phòng",
        "original_recording": r"bản ghi gốc",
        "money_returns_to_relative_project": r"người thân Mộng Dao|dự án của người thân",
        "same_signing_device": r"cùng một thiết bị|một thiết bị duy nhất",
        "benefit_exchange": r"đổi lấy lợi ích|sau khi ký.*(?:hợp đồng|tiền|thưởng)",
    }
    found = []
    for key, pattern in patterns.items():
        match = re.search(pattern, early, re.IGNORECASE | re.DOTALL)
        if match:
            found.append({"reveal": key, "excerpt": early[max(0, match.start()-50):match.end()+80]})
    return found


def main():
    issues = []
    if not OUTLINE.exists():
        raise SystemExit(f"Missing outline: {OUTLINE}")

    outline = json.loads(OUTLINE.read_text(encoding="utf-8"))
    expected_chapters = len(outline["chapters"])
    chapter_paths = [STORY / "chapters" / f"{number:02d}.txt" for number in range(1, expected_chapters + 1)]
    chapter_texts = []
    chapter_rows = []

    for number, path in enumerate(chapter_paths, 1):
        if not path.exists():
            issues.append(f"missing_chapter:{number:02d}")
            chapter_texts.append("")
            continue
        text = path.read_text(encoding="utf-8")
        chapter_texts.append(text)
        count = len(words(text))
        chapter_rows.append({
            "number": number,
            "path": str(path.relative_to(PROJECT)),
            "words": count,
            "sha256": sha256_text(text),
        })
        if not text.strip():
            issues.append(f"empty_chapter:{number:02d}")
        for line_number, line in enumerate(text.splitlines(), 1):
            if HEADING_RE.match(line):
                issues.append(f"heading_in_chapter:{number:02d}:{line_number}:{line.strip()}")
            if PRODUCTION_RE.search(line):
                issues.append(f"production_marker:{number:02d}:{line_number}:{line.strip()}")

    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_join = normalize_join(chapter_texts) if all(chapter_texts) else ""
    if narration and expected_join and narration != expected_join:
        issues.append("spoken_narration_is_not_exact_sequential_chapter_join")

    total_words = len(words(narration))
    if not 13600 <= total_words <= 14400:
        issues.append(f"word_count_out_of_range:{total_words}")

    for line_number, line in enumerate(narration.splitlines(), 1):
        if HEADING_RE.match(line):
            issues.append(f"heading_in_spoken_narration:{line_number}:{line.strip()}")
        if PRODUCTION_RE.search(line):
            issues.append(f"production_marker_in_spoken_narration:{line_number}:{line.strip()}")

    identity_hits = standalone_identity_hits(narration, outline["identity_registry"]) if narration else []
    if identity_hits:
        issues.append(f"standalone_identity_fragments:{len(identity_hits)}")

    protected_reveals = reveal_hits(chapter_texts) if all(chapter_texts) else []
    if protected_reveals:
        issues.append(f"protected_reveal_before_chapter_9:{len(protected_reveals)}")

    report = {
        "version": 3,
        "status": "passed" if not issues else "failed",
        "verified": not issues,
        "canonical_title": outline["canonical_title"],
        "expected_chapters": expected_chapters,
        "chapters_present": sum(path.exists() for path in chapter_paths),
        "chapter_rows": chapter_rows,
        "spoken_narration": str(narration_path.relative_to(PROJECT)),
        "total_words": total_words,
        "target_range": [13600, 14400],
        "estimated_minutes_at_baseline": round(total_words / outline["baseline_words_per_minute"], 3) if total_words else 0,
        "spoken_sha256": sha256_text(narration) if narration else None,
        "identity_hits": identity_hits,
        "protected_reveal_hits": protected_reveals,
        "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": total_words,
        "issues": issues[:20],
        "report": str(REPORT),
    }, ensure_ascii=False))
    return 0 if report["verified"] else 1


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