#!/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]
SCRIPT = PROJECT / "script"
STORY = PROJECT / "story"
CHAPTERS = STORY / "chapters"
REPORT = STORY / "promotion-report.json"
WORD_RE = re.compile(r"[\wÀ-ỹĐđ]+", re.UNICODE)
HEADING_RE = re.compile(r"(?im)^\s*(?:chương|phần|cảnh)\s+(?:\d+|[ivxlcdm]+)\s*[:.\-–—]?\s*$")
PRODUCTION_RE = re.compile(r"(?i)\b(?:scene_id|target_words|production note|ghi chú sản xuất|end state|reveal_id)\b")


def load(name):
    return json.loads((SCRIPT / name).read_text())


def sha_bytes(data):
    return hashlib.sha256(data).hexdigest()


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


def main():
    outline = load("outline.json")
    registry = load("identity-registry.json")
    ledger = load("reveal-ledger.json")
    plan = load("production-plan.json")
    manifest = load("project-manifest.json")
    issues = []
    authority = {
        "outline_sha256": sha_bytes((SCRIPT / "outline.json").read_bytes()),
        "identity_registry_sha256": sha_bytes((SCRIPT / "identity-registry.json").read_bytes()),
        "reveal_ledger_sha256": sha_bytes((SCRIPT / "reveal-ledger.json").read_bytes()),
    }
    if authority != plan.get("authority_hashes"):
        issues.append("current authority bytes differ from production-plan lock")
    expected_manifest = {
        "outline_sha256": manifest.get("outline_hash", "").removeprefix("sha256:"),
        "identity_registry_sha256": manifest.get("identity_registry_hash", "").removeprefix("sha256:"),
        "reveal_ledger_sha256": manifest.get("reveal_ledger_hash", "").removeprefix("sha256:"),
    }
    if authority != expected_manifest:
        issues.append("current authority bytes differ from project-manifest lock")
    expected = int(plan["expected_chapters"])
    paths = [CHAPTERS / f"{index:02d}.txt" for index in range(1, expected + 1)]
    missing = [str(path.relative_to(PROJECT)) for path in paths if not path.exists()]
    if missing:
        issues.append("missing chapters: " + ", ".join(missing))
    rows, texts = [], []
    for index, path in enumerate(paths, 1):
        if not path.exists():
            continue
        text = path.read_text().strip()
        texts.append(text)
        if not text:
            issues.append(f"chapter {index:02d} empty")
        if HEADING_RE.search(text):
            issues.append(f"chapter {index:02d} contains spoken heading")
        if PRODUCTION_RE.search(text):
            issues.append(f"chapter {index:02d} contains production marker")
        rows.append({"chapter": index, "path": str(path.relative_to(PROJECT)), "words": words(text), "sha256": sha_bytes(path.read_bytes())})
    joined = "\n\n".join(texts).strip() + "\n" if len(texts) == expected else ""
    spoken_path = STORY / "spoken-narration.txt"
    actual = spoken_path.read_text() if spoken_path.exists() else ""
    exact_join = bool(joined) and actual == joined
    if not exact_join:
        issues.append("spoken narration is not exact sequential join")
    total_words = words(actual)
    low, high = plan["accepted_range"]
    if not low <= total_words <= high:
        issues.append(f"word count {total_words} outside {low}-{high}")
    identity_hits = []
    all_allowed_spans = []
    for entity in registry.get("entities", []):
        for name in entity.get("allowed_narrative_names", []) + entity.get("allowed_title_names", []) + entity.get("role_address", []):
            if not name:
                continue
            all_allowed_spans.extend((match.start(), match.end()) for match in re.finditer(re.escape(name), actual))
    for entity in registry.get("entities", []):
        if entity.get("entity_type", "character") != "character":
            continue
        for bare in entity.get("forbidden_bare_names", []):
            pattern = re.compile(rf"(?<![\wÀ-ỹĐđ]){re.escape(bare)}(?![\wÀ-ỹĐđ])")
            for match in pattern.finditer(actual):
                if any(start <= match.start() and match.end() <= end for start, end in all_allowed_spans):
                    continue
                context = actual[max(0, match.start()-28):match.end()+28]
                identity_hits.append({"entity": entity["full_name"], "bare": bare, "context": context})
    if identity_hits:
        issues.append(f"identity gate hits: {len(identity_hits)}")
    reveal_hits = []
    chapter_text = {i + 1: text for i, text in enumerate(texts)}
    detection_terms = {
        "rev_001": ["tám lọ có lịch sử mở nắp khác nhau", "tám lớp cặn không giống nhau"],
        "rev_002": ["máy ghi nhiệt thứ hai", "thiết bị ghi nhiệt dự phòng còn lưu dữ liệu gốc"],
        "rev_003": ["mã niêm phong bị tách đôi", "mã niêm phong bị chia qua hai bộ phận"],
        "rev_004": ["Minh Khải ra lệnh sửa hồ sơ", "chiếm quyền quy trình ổn định nhiệt", "chủ mưu tráo nhãn"],
    }
    for reveal in ledger.get("reveals", []):
        threshold = int(reveal["not_before_part"])
        for part in range(1, threshold):
            lower = chapter_text.get(part, "").lower()
            for term in detection_terms.get(reveal["reveal_id"], []):
                if term.lower() in lower:
                    reveal_hits.append({"reveal_id": reveal["reveal_id"], "part": part, "term": term})
    if reveal_hits:
        issues.append(f"protected reveal hits: {len(reveal_hits)}")
    report = {
        "version": 1,
        "status": "passed" if not issues else "failed",
        "verified": not issues,
        "canonical_title": outline["canonical_title"],
        "expected_chapters": expected,
        "chapters_present": len(rows),
        "chapter_rows": rows,
        "spoken_narration": str(spoken_path.relative_to(PROJECT)),
        "exact_sequential_join": exact_join,
        "total_words": total_words,
        "target_range": [low, high],
        "estimated_minutes_at_baseline": round(total_words / 233.33, 3),
        "spoken_sha256": sha_bytes(actual.encode()) if actual else None,
        "authority_hashes_current": authority,
        "identity_hits": identity_hits,
        "protected_reveal_hits": reveal_hits,
        "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"], "words": total_words, "issues": issues}, ensure_ascii=False))
    return 0 if report["verified"] else 1

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